Improve the ergonomics of the TranspileLayout class (#10835)

* Improve the ergonomics of the TranspileLayout class

This commit updates the TranspileLayout class to make it easier to work
with. The TranspileLayout class was orginally just an a container class
that took the information returned from the transpiler needed to reverse
the transpiler layout permutation (in the absence of ancillas) so that
`QuantumCircuit.from_circuit()` could compare Operator instances across
a `transpile()`. However, the internal data structures used by the
transpiler are hard to work with, and don't map well to the normal
reasoning around how the transpiler transforms the circuit. To improve
the usability of the class this commit adds 4 new methods to the class:

 - initial_layout_list() and final_layout_list() which compute a list
   form of the initial_layout and final_layout respectively
 - full_layout() which generates a list that maps the input circuits
   qubit positions in the input circuit to the final position at
   the end of the circuit (which is what most people think of when they
   hear "final layout")
 - permute_sparse_pauli_op() which takes in a SparsePauliOp object and
   permutes it based on the full layout. This is especially useful when
   combined with the Estimator primitive

To implement these functions a few extra pieces of information are
needed to fully implement them. The first is we need to know the number
of original circuit qubits. This is used to account for any ancillas
that are added in the circuit, once we have the input circuit count we
can use the original_qubit_indices attribute to discern which qubits in
the initial layout are from the original circuit and which are ancillas.
The second piece of information needed is the list of qubits in the
output circuit. as this is needed to look up the position of the virtual
qubit in the final_layout. These are both added as optional private
attributes to the TranspileLayout class and there are some small changes
to the pass manager and QPY to accomodate them.

Similarly the change in the TranspileLayout class requires a new QPY
version to include the missing details that were not being serialized
in QPY and weren't representable in the previous payload format.

Fixes #10826
Fixes #10818

* Handle None for the private TranspileLayout fields in QPY

* Add test for full_layout with ancillas

* Rework interface to be more consistent

This commit reworks the names of the new methods added in this API to be
more self consistent. It uses the format '*_index_layout' to replace the
`*_layout_list` name. Similarly new methods `*_virtual_layout` are added
for methods that return layout objects.

* Apply suggestions from code review

Co-authored-by: Jake Lishman <jake@binhbar.com>

* Remove permute_sparse_pauli_op()

This commit remove the TranspileLayout.permute_sparse_pauli_op() method.
This was a bit out of place on the TranspileLayout class, and it will
instead be replaced in a follow-up PR by a apply_layout() method on the
SparePauliOp class that takes in a TranspileLayout object.

* Update docs

* Fix docs build post-rebase

* Fix docs issues

---------

Co-authored-by: Jake Lishman <jake@binhbar.com>
This commit is contained in:
Matthew Treinish 2023-10-02 20:43:22 -04:00 committed by GitHub
parent 81e28dc2a6
commit 19854d193a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 688 additions and 13 deletions

View File

@ -88,11 +88,14 @@ class BasePassRunner(ABC):
pass
@abstractmethod
def _to_target(self, passmanager_ir):
def _to_target(self, passmanager_ir, in_program):
"""Convert pass manager IR into output program.
Args:
passmanager_ir: Pass manager IR after optimization.
in_program: The input program, this can be used if you need
any metadata about the original input for the output. It
should not be mutated.
Returns:
Output program.
@ -229,7 +232,6 @@ class BasePassRunner(ABC):
self.metadata = metadata
passmanager_ir = self._to_passmanager_ir(in_program)
del in_program
for controller in self.working_list:
passmanager_ir = self._run_pass_generic(
@ -237,7 +239,8 @@ class BasePassRunner(ABC):
passmanager_ir=passmanager_ir,
options=self.passmanager_options,
)
out_program = self._to_target(passmanager_ir)
out_program = self._to_target(passmanager_ir, in_program)
del in_program
if not isinstance(out_program, self.OUT_PROGRAM_TYPE):
raise TypeError(

View File

@ -149,7 +149,9 @@ Version 10
==========
Version 10 adds support for symengine-native serialization for objects of type
:class:`~.ParameterExpression` as well as symbolic expressions in Pulse schedule blocks.
:class:`~.ParameterExpression` as well as symbolic expressions in Pulse schedule blocks. Version
10 also adds support for new fields in the :class:`~.TranspileLayout` class added in the Qiskit
0.45.0 release.
The symbolic_encoding field is added to the file header, and a new encoding type char
is introduced, mapped to each symbolic library as follows: ``p`` refers to sympy
@ -171,6 +173,26 @@ The contents of FILE_HEADER after V10 are defined as a C struct as:
char symbolic_encoding;
}
LAYOUT
------
The ``LAYOUT`` struct is updated to have an additional ``input_qubit_count`` field.
With version 10 the ``LAYOUT`` struct is now:
.. code-block:: c
struct {
char exists;
int32_t initial_layout_size;
int32_t input_mapping_size;
int32_t final_layout_size;
uint32_t extra_registers;
int32_t input_qubit_count;
}
The rest of the layout data after the ``LAYOUT`` struct is represented as in previous versions. If
``input qubit_count`` is < 0 that indicates that both ``_input_qubit_count``
and ``_output_qubit_list`` in the :class:`~.TranspileLayout` object are ``None``.
.. _qpy_version_9:

View File

@ -831,7 +831,7 @@ def _write_registers(file_obj, in_circ_regs, full_bits):
def _write_layout(file_obj, circuit):
if circuit.layout is None:
# Write a null header if there is no layout present
file_obj.write(struct.pack(formats.LAYOUT_PACK, False, -1, -1, -1, 0))
file_obj.write(struct.pack(formats.LAYOUT_V2_PACK, False, -1, -1, -1, 0, 0))
return
initial_size = -1
input_qubit_mapping = {}
@ -874,14 +874,18 @@ def _write_layout(file_obj, circuit):
virtual_bit = final_layout_physical[i]
final_layout_array.append(circuit.find_bit(virtual_bit).index)
input_qubit_count = circuit._layout._input_qubit_count
if input_qubit_count is None:
input_qubit_count = -1
file_obj.write(
struct.pack(
formats.LAYOUT_PACK,
formats.LAYOUT_V2_PACK,
True,
initial_size,
input_qubit_size,
final_layout_size,
len(extra_registers),
input_qubit_count,
)
)
_write_registers(
@ -910,6 +914,10 @@ def _read_layout(file_obj, circuit):
)
if not header.exists:
return
_read_common_layout(file_obj, header, circuit)
def _read_common_layout(file_obj, header, circuit):
registers = {
name: QuantumRegister(len(v[1]), name)
for name, v in _read_registers_v4(file_obj, header.extra_registers)["q"].items()
@ -958,6 +966,18 @@ def _read_layout(file_obj, circuit):
circuit._layout = TranspileLayout(initial_layout, input_qubit_mapping, final_layout)
def _read_layout_v2(file_obj, circuit):
header = formats.LAYOUT_V2._make(
struct.unpack(formats.LAYOUT_V2_PACK, file_obj.read(formats.LAYOUT_V2_SIZE))
)
if not header.exists:
return
_read_common_layout(file_obj, header, circuit)
if header.input_qubit_count >= 0:
circuit._layout._input_qubit_count = header.input_qubit_count
circuit._layout._output_qubit_list = circuit.qubits
def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=False):
"""Write a single QuantumCircuit object in the file like object.
@ -1161,5 +1181,8 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa
UserWarning,
)
if version >= 8:
_read_layout(file_obj, circ)
if version >= 10:
_read_layout_v2(file_obj, circ)
else:
_read_layout(file_obj, circ)
return circ

View File

@ -278,6 +278,21 @@ MAP_ITEM = namedtuple("MAP_ITEM", ["key_size", "type", "size"])
MAP_ITEM_PACK = "!H1cH"
MAP_ITEM_SIZE = struct.calcsize(MAP_ITEM_PACK)
LAYOUT_V2 = namedtuple(
"LAYOUT",
[
"exists",
"initial_layout_size",
"input_mapping_size",
"final_layout_size",
"extra_registers",
"input_qubit_count",
],
)
LAYOUT_V2_PACK = "!?iiiIi"
LAYOUT_V2_SIZE = struct.calcsize(LAYOUT_V2_PACK)
LAYOUT = namedtuple(
"LAYOUT",
["exists", "initial_layout_size", "input_mapping_size", "final_layout_size", "extra_registers"],

View File

@ -93,6 +93,8 @@ class BasePass(GenericPass):
initial_layout=self.property_set["layout"],
input_qubit_mapping=self.property_set["original_qubit_indices"],
final_layout=self.property_set["final_layout"],
_input_qubit_count=len(circuit.qubits),
_output_qubit_list=result_circuit.qubits,
)
if self.property_set["clbit_write_latency"] is not None:
result_circuit._clbit_write_latency = self.property_set["clbit_write_latency"]

View File

@ -18,6 +18,7 @@ Virtual (qu)bits are tuples, e.g. `(QuantumRegister(3, 'qr'), 2)` or simply `qr[
Physical (qu)bits are integers.
"""
from __future__ import annotations
from typing import List
from dataclasses import dataclass
from qiskit.circuit.quantumregister import Qubit, QuantumRegister
@ -375,9 +376,77 @@ class TranspileLayout:
by setting and applying initial layout during the :ref:`layout_stage`
and :class:`~.SwapGate` insertion during the :ref:`routing_stage`. To
provide an interface to reason about these permutations caused by
the :mod:`~qiskit.transpiler`.
the :mod:`~qiskit.transpiler`. In general the normal interface to access
and reason about the layout transformations made by the transpiler is to
use the helper methods defined on this class.
There are three attributes associated with the class:
For example, looking at the initial layout, the transpiler can potentially
remap the order of the qubits in your circuit as it fits the circuit to
the target backend. If the input circuit was:
.. plot:
:include-source:
from qiskit.circuit import QuantumCircuit, QuantumRegister
qr = QuantumRegister(3, name="MyReg")
qc = QuantumCircuit(qr)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.draw("mpl")
Then during the layout stage the transpiler reorders the qubits to be:
.. plot:
:include-source:
from qiskit import QuantumCircuit
qc = QuantumCircuit(3)
qc.h(2)
qc.cx(2, 1)
qc.cx(2, 0)
qc.draw("mpl")
then the output of the :meth:`.initial_virtual_layout` would be
equivalent to::
Layout({
qr[0]: 2,
qr[1]: 1,
qr[2]: 0,
})
(it is also this attribute in the :meth:`.QuantumCircuit.draw` and
:func:`.circuit_drawer` which is used to display the mapping of qubits to
positions in circuit visualizations post-transpilation)
Building on this above example for final layout, if the transpiler needed to
insert swap gates during routing so the output circuit became:
.. plot:
:include-source:
from qiskit import QuantumCircuit
qc = QuantumCircuit(3)
qc.h(2)
qc.cx(2, 1)
qc.swap(0, 1)
qc.cx(2, 1)
qc.draw("mpl")
then the output of the :meth:`routing_permutation` method would be::
[1, 0, 2]
which maps the qubits at each position to their final position after any swap
insertions caused by routing.
There are three public attributes associated with the class, however these
are mostly provided for backwards compatibility and represent the internal
state from the transpiler. They are defined as:
* :attr:`initial_layout` - This attribute is used to model the
permutation caused by the :ref:`layout_stage` it contains a
@ -393,12 +462,196 @@ class TranspileLayout:
the circuit (and used by :meth:`.Operator.from_circuit`).
* :attr:`final_layout` - This is a :class:`~.Layout` object used to
model the output permutation caused ny any :class:`~.SwapGate`\s
inserted into the :class:~.QuantumCircuit` during the
inserted into the :class:`~.QuantumCircuit` during the
:ref:`routing_stage`. It maps the output circuit's qubits from
:class:`.QuantumCircuit.qubits` to the final position after
routing.
:class:`.QuantumCircuit.qubits` in the output circuit to the final
position after routing. It is **not** a mapping from the original
input circuit's position to the final position at the end of the
transpiled circuit. If you need this you can use the
:meth:`.final_index_layout` to generate this. If this is set to ``None``
this indicates that routing was not run and it can be considered
equivalent to a trivial layout with the qubits from the output circuit's
:attr:`~.QuantumCircuit.qubits` list.
"""
initial_layout: Layout
input_qubit_mapping: dict[Qubit, int]
final_layout: Layout | None = None
_input_qubit_count: int | None = None
_output_qubit_list: List[Qubit] | None = None
def initial_virtual_layout(self, filter_ancillas: bool = False) -> Layout:
"""Return a :class:`.Layout` object for the initial layout.
This returns a mapping of virtual :class:`~.Qubit` objects in the input
circuit to the physical qubit selected during layout. This is analogous
to the :attr:`.initial_layout` attribute.
Args:
filter_ancillas: If set to ``True`` only qubits in the input circuit
will be in the returned layout. Any ancilla qubits added to the
output circuit will be filtered from the returned object.
Returns:
A layout object mapping the input circuit's :class:`~.Qubit`
objects to the selected physical qubits.
"""
if not filter_ancillas:
return self.initial_layout
return Layout(
{
k: v
for k, v in self.initial_layout.get_virtual_bits().items()
if self.input_qubit_mapping[k] < self._input_qubit_count
}
)
def initial_index_layout(self, filter_ancillas: bool = False) -> List[int]:
"""Generate an initial layout as an array of integers
Args:
filter_ancillas: If set to ``True`` any ancilla qubits added
to the transpiler will not be included in the output.
Return:
A layout array that maps a position in the array to its new position in the output
circuit.
"""
virtual_map = self.initial_layout.get_virtual_bits()
if filter_ancillas:
output = [None] * self._input_qubit_count
else:
output = [None] * len(virtual_map)
for index, (virt, phys) in enumerate(virtual_map.items()):
if filter_ancillas and index >= self._input_qubit_count:
break
pos = self.input_qubit_mapping[virt]
output[pos] = phys
return output
def routing_permutation(self) -> List[int]:
"""Generate a final layout as an array of integers
If there is no :attr:`.final_layout` attribute present then that indicates
there was no output permutation caused by routing or other transpiler
transforms. In this case the function will return a list of ``[0, 1, 2, .., n]``
to indicate this
Returns:
A layout array that maps a position in the array to its new position in the output
circuit
"""
if self.final_layout is None:
return list(range(len(self._output_qubit_list)))
virtual_map = self.final_layout.get_virtual_bits()
return [virtual_map[virt] for virt in self._output_qubit_list]
def final_index_layout(self, filter_ancillas: bool = True) -> List[int]:
"""Generate the final layout as an array of integers
This method will generate an array of final positions for each qubit in the output circuit.
For example, if you had an input circuit like::
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
and the output from the transpiler was::
tqc = QuantumCircuit(3)
qc.h(2)
qc.cx(2, 1)
qc.swap(0, 1)
qc.cx(2, 1)
then the return from this function would be a list of::
[2, 0, 1]
because qubit 0 in the original circuit's final state is on qubit 3 in the output circuit,
qubit 1 in the original circuit's final state is on qubit 0, and qubit 2's final state is
on qubit. The output list length will be as wide as the input circuit's number of qubits,
as the output list from this method is for tracking the permutation of qubits in the
original circuit caused by the transpiler.
Args:
filter_ancillas: If set to ``False`` any ancillas allocated in the output circuit will be
included in the layout.
Returns:
A list of final positions for each input circuit qubit
"""
if self._input_qubit_count is None:
# TODO: After there is a way to differentiate the ancilla qubits added by the transpiler
# don't use the ancilla name anymore.See #10817 for discussion on this.
num_source_qubits = len(
[
x
for x in self.input_qubit_mapping
if getattr(x, "_register", "").startswith("ancilla")
]
)
else:
num_source_qubits = self._input_qubit_count
if self._output_qubit_list is None:
circuit_qubits = list(self.final_layout.get_virtual_bits())
else:
circuit_qubits = self._output_qubit_list
pos_to_virt = {v: k for k, v in self.input_qubit_mapping.items()}
qubit_indices = []
if filter_ancillas:
num_qubits = num_source_qubits
else:
num_qubits = len(self._output_qubit_list)
for index in range(num_qubits):
qubit_idx = self.initial_layout[pos_to_virt[index]]
if self.final_layout is not None:
qubit_idx = self.final_layout[circuit_qubits[qubit_idx]]
qubit_indices.append(qubit_idx)
return qubit_indices
def final_virtual_layout(self, filter_ancillas: bool = True) -> Layout:
"""Generate the final layout as a :class:`.Layout` object
This method will generate an array of final positions for each qubit in the output circuit.
For example, if you had an input circuit like::
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
and the output from the transpiler was::
tqc = QuantumCircuit(3)
qc.h(2)
qc.cx(2, 1)
qc.swap(0, 1)
qc.cx(2, 1)
then the return from this function would be a layout object::
Layout({
qc.qubits[0]: 2,
qc.qubits[1]: 0,
qc.qubits[2]: 1,
})
because qubit 0 in the original circuit's final state is on qubit 3 in the output circuit,
qubit 1 in the original circuit's final state is on qubit 0, and qubit 2's final state is
on qubit. The output list length will be as wide as the input circuit's number of qubits,
as the output list from this method is for tracking the permutation of qubits in the
original circuit caused by the transpiler.
Args:
filter_ancillas: If set to ``False`` any ancillas allocated in the output circuit will be
included in the layout.
Returns:
A layout object mapping to the final positions for each qubit
"""
res = self.final_index_layout(filter_ancillas=filter_ancillas)
pos_to_virt = {v: k for k, v in self.input_qubit_mapping.items()}
return Layout({pos_to_virt[index]: phys for index, phys in enumerate(res)})

View File

@ -100,7 +100,7 @@ class RunningPassManager(BasePassRunner):
raise TranspilerError(f"Input {in_program.__class__} is not QuantumCircuit.")
return circuit_to_dag(in_program)
def _to_target(self, passmanager_ir: DAGCircuit) -> QuantumCircuit:
def _to_target(self, passmanager_ir: DAGCircuit, in_program: QuantumCircuit) -> QuantumCircuit:
if not isinstance(passmanager_ir, DAGCircuit):
raise TranspilerError(f"Input {passmanager_ir.__class__} is not DAGCircuit.")
@ -112,6 +112,8 @@ class RunningPassManager(BasePassRunner):
initial_layout=self.property_set["layout"],
input_qubit_mapping=self.property_set["original_qubit_indices"],
final_layout=self.property_set["final_layout"],
_input_qubit_count=len(in_program.qubits),
_output_qubit_list=circuit.qubits,
)
circuit._clbit_write_latency = self.property_set["clbit_write_latency"]
circuit._conditional_latency = self.property_set["conditional_latency"]

View File

@ -0,0 +1,59 @@
---
features:
- |
Added new methods to :class:`.TranspileLayout`, :meth:`~.TranspileLayout.initial_index_layout`
and :meth:`~.TranspileLayout.routing_permutation`, which are used to generate a list view of
the :attr:`.TranspileLayout.initial_layout` and
:attr:`.TranspileLayout.final_layout` attributes respectively. For example,
if the :attr:`~.TranspileLayout.final_layout` attribute was::
Layout({
qr[0]: 2,
qr[1]: 3,
qr[2]: 0,
qr[3]: 1,
})
then :meth:`~.TranspileLayout.routing_permutation` will return::
[2, 3, 0, 1]
- |
Added a new method to :class:`.TranspileLayout`, :meth:`~.TranspileLayout.initial_virtual_layout`,
which is equivalent to the :attr:`.TranspileLayout.initial_layout` attribute but gives the option
to filter ancilla qubits that were added to the circuit. By default the :attr:`.TranspileLayout.initial_layout`
will typically include any ancillas added by the transpiler.
- |
Added a new methods, :meth:`~.TranspileLayout.final_index_layout` and :meth:`~.TranspileLayout.final_virtual_layout`
to the :class:`~.TranspileLayout` class. These methods are used to return a final layout
(the mapping of input circuit qubits to the final position in the output). This is distinct
from the :attr:`~.TranspileLayout.final_layout` attribute which is the permutation caused by
routing as a :class:`.Layout` object. The :meth:`~.TranspileLayout.final_index_layout` method
returns a list to showthe output position for each qubit in the input circuit to the transpiler.
For example, with an original circuit::
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
and the output from the transpiler was::
tqc = QuantumCircuit(3)
tqc.h(2)
tqc.cx(2, 1)
tqc.swap(0, 1)
tqc.cx(2, 1)
then the output from :meth:`~.TranspileLayout.final_index_layout` would return a
list of::
[2, 0, 1]
The :meth:`~.TranspileLayout.final_virtual_layout` returns this as a :class:`.Layout` object,
so the return from the above example would be::
Layout({
qc.qubits[0]: 2,
qc.qubits[1]: 0,
qc.qubits[2]: 1,
})

View File

@ -0,0 +1,296 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
# pylint: disable=missing-function-docstring
"""Tests the layout object"""
from qiskit.circuit import QuantumCircuit, QuantumRegister
from qiskit.transpiler.layout import Layout, TranspileLayout
from qiskit.transpiler.coupling import CouplingMap
from qiskit.compiler import transpile
from qiskit.test import QiskitTestCase
class TranspileLayoutTest(QiskitTestCase):
"""Test the methods in the TranspileLayout object."""
def test_final_index_layout_full_path(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(3, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[2, 1, 0], seed_transpiler=42)
res = tqc.layout.final_index_layout()
self.assertEqual(res, [2, 0, 1])
def test_final_virtual_layout_full_path(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(3, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[2, 1, 0], seed_transpiler=42)
res = tqc.layout.final_virtual_layout()
self.assertEqual(res, Layout({qc.qubits[0]: 2, qc.qubits[1]: 0, qc.qubits[2]: 1}))
def test_final_index_layout_full_path_with_ancilla(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(10, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[9, 4, 0], seed_transpiler=42)
# tqc:
# q_2 -> 0 ──X─────────────────────────────────────────────────
# │
# ancilla_0 -> 1 ──X───X─────────────────────────────────────────────
# │
# ancilla_1 -> 2 ──────X──X──────────────────────────────────────────
# │ ┌───┐ ┌───┐
# ancilla_2 -> 3 ─────────X─┤ H ├────────────────────────────■──┤ H ├
# ┌───┐ └───┘ ┌───┐ ┌───┐┌─┴─┐├───┤
# q_1 -> 4 ┤ H ├─────────────────────■──┤ H ├─X─┤ H ├┤ X ├┤ H ├
# └───┘ ┌───┐┌─┴─┐├───┤ │ └───┘└───┘└───┘
# ancilla_3 -> 5 ─────────────────X─┤ H ├┤ X ├┤ H ├─X────────────────
# │ └───┘└───┘└───┘
# ancilla_4 -> 6 ─────────────X───X──────────────────────────────────
# │
# ancilla_5 -> 7 ─────────X───X──────────────────────────────────────
# │
# ancilla_6 -> 8 ──────X──X──────────────────────────────────────────
# ┌───┐ │
# q_0 -> 9 ┤ H ├─X─────────────────────────────────────────────
# └───┘
res = tqc.layout.final_index_layout()
self.assertEqual(res, [4, 5, 3])
def test_final_index_layout_full_path_with_ancilla_no_filter(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(10, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[9, 4, 0], seed_transpiler=42)
# tqc:
# q_2 -> 0 ──X─────────────────────────────────────────────────
# │
# ancilla_0 -> 1 ──X───X─────────────────────────────────────────────
# │
# ancilla_1 -> 2 ──────X──X──────────────────────────────────────────
# │ ┌───┐ ┌───┐
# ancilla_2 -> 3 ─────────X─┤ H ├────────────────────────────■──┤ H ├
# ┌───┐ └───┘ ┌───┐ ┌───┐┌─┴─┐├───┤
# q_1 -> 4 ┤ H ├─────────────────────■──┤ H ├─X─┤ H ├┤ X ├┤ H ├
# └───┘ ┌───┐┌─┴─┐├───┤ │ └───┘└───┘└───┘
# ancilla_3 -> 5 ─────────────────X─┤ H ├┤ X ├┤ H ├─X────────────────
# │ └───┘└───┘└───┘
# ancilla_4 -> 6 ─────────────X───X──────────────────────────────────
# │
# ancilla_5 -> 7 ─────────X───X──────────────────────────────────────
# │
# ancilla_6 -> 8 ──────X──X──────────────────────────────────────────
# ┌───┐ │
# q_0 -> 9 ┤ H ├─X─────────────────────────────────────────────
# └───┘
res = tqc.layout.final_index_layout(filter_ancillas=False)
self.assertEqual(res, [4, 5, 3, 0, 1, 2, 6, 7, 8, 9])
def test_final_virtual_layout_full_path_with_ancilla(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(10, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[9, 4, 0], seed_transpiler=42)
# tqc:
# q_2 -> 0 ──X─────────────────────────────────────────────────
# │
# ancilla_0 -> 1 ──X───X─────────────────────────────────────────────
# │
# ancilla_1 -> 2 ──────X──X──────────────────────────────────────────
# │ ┌───┐ ┌───┐
# ancilla_2 -> 3 ─────────X─┤ H ├────────────────────────────■──┤ H ├
# ┌───┐ └───┘ ┌───┐ ┌───┐┌─┴─┐├───┤
# q_1 -> 4 ┤ H ├─────────────────────■──┤ H ├─X─┤ H ├┤ X ├┤ H ├
# └───┘ ┌───┐┌─┴─┐├───┤ │ └───┘└───┘└───┘
# ancilla_3 -> 5 ─────────────────X─┤ H ├┤ X ├┤ H ├─X────────────────
# │ └───┘└───┘└───┘
# ancilla_4 -> 6 ─────────────X───X──────────────────────────────────
# │
# ancilla_5 -> 7 ─────────X───X──────────────────────────────────────
# │
# ancilla_6 -> 8 ──────X──X──────────────────────────────────────────
# ┌───┐ │
# q_0 -> 9 ┤ H ├─X─────────────────────────────────────────────
# └───┘
res = tqc.layout.final_virtual_layout()
self.assertEqual(res, Layout({qc.qubits[0]: 4, qc.qubits[1]: 5, qc.qubits[2]: 3}))
def test_final_virtual_layout_full_path_with_ancilla_no_filter(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(10, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[9, 4, 0], seed_transpiler=42)
# tqc:
# q_2 -> 0 ──X─────────────────────────────────────────────────
# │
# ancilla_0 -> 1 ──X───X─────────────────────────────────────────────
# │
# ancilla_1 -> 2 ──────X──X──────────────────────────────────────────
# │ ┌───┐ ┌───┐
# ancilla_2 -> 3 ─────────X─┤ H ├────────────────────────────■──┤ H ├
# ┌───┐ └───┘ ┌───┐ ┌───┐┌─┴─┐├───┤
# q_1 -> 4 ┤ H ├─────────────────────■──┤ H ├─X─┤ H ├┤ X ├┤ H ├
# └───┘ ┌───┐┌─┴─┐├───┤ │ └───┘└───┘└───┘
# ancilla_3 -> 5 ─────────────────X─┤ H ├┤ X ├┤ H ├─X────────────────
# │ └───┘└───┘└───┘
# ancilla_4 -> 6 ─────────────X───X──────────────────────────────────
# │
# ancilla_5 -> 7 ─────────X───X──────────────────────────────────────
# │
# ancilla_6 -> 8 ──────X──X──────────────────────────────────────────
# ┌───┐ │
# q_0 -> 9 ┤ H ├─X─────────────────────────────────────────────
# └───┘
res = tqc.layout.final_virtual_layout(filter_ancillas=False)
pos_to_virt = {v: k for k, v in tqc.layout.input_qubit_mapping.items()}
expected = Layout(
{
pos_to_virt[0]: 4,
pos_to_virt[1]: 5,
pos_to_virt[2]: 3,
pos_to_virt[3]: 0,
pos_to_virt[4]: 1,
pos_to_virt[5]: 2,
pos_to_virt[6]: 6,
pos_to_virt[7]: 7,
pos_to_virt[8]: 8,
pos_to_virt[9]: 9,
}
)
self.assertEqual(res, expected)
def test_routing_permutation(self):
qr = QuantumRegister(5)
final_layout = Layout(
{
qr[0]: 2,
qr[1]: 4,
qr[2]: 1,
qr[3]: 0,
qr[4]: 3,
}
)
layout_obj = TranspileLayout(
initial_layout=Layout.generate_trivial_layout(qr),
input_qubit_mapping={v: k for k, v in enumerate(qr)},
final_layout=final_layout,
_input_qubit_count=5,
_output_qubit_list=list(qr),
)
res = layout_obj.routing_permutation()
self.assertEqual(res, [2, 4, 1, 0, 3])
def test_routing_permutation_no_final_layout(self):
qr = QuantumRegister(5)
layout_obj = TranspileLayout(
initial_layout=Layout.generate_trivial_layout(qr),
input_qubit_mapping={v: k for k, v in enumerate(qr)},
final_layout=None,
_input_qubit_count=5,
_output_qubit_list=list(qr),
)
res = layout_obj.routing_permutation()
self.assertEqual(res, list(range(5)))
def test_initial_index_layout(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(3, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[2, 1, 0], seed_transpiler=42)
self.assertEqual(tqc.layout.initial_index_layout(), [2, 1, 0])
def test_initial_index_layout_with_ancillas(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(6, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[2, 1, 0], seed_transpiler=42)
self.assertEqual(tqc.layout.initial_index_layout(), [2, 1, 0, 3, 4, 5])
def test_initial_index_layout_filter_ancillas(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(6, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[5, 2, 1], seed_transpiler=42)
self.assertEqual(tqc.layout.initial_index_layout(True), [5, 2, 1])
def test_initial_virtual_layout(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(3, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[2, 1, 0], seed_transpiler=42)
self.assertEqual(
tqc.layout.initial_virtual_layout(),
Layout.from_qubit_list([qc.qubits[2], qc.qubits[1], qc.qubits[0]]),
)
def test_initial_virtual_layout_with_ancillas(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(6, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[2, 1, 0], seed_transpiler=42)
reverse_pos_map = {v: k for k, v in tqc.layout.input_qubit_mapping.items()}
self.assertEqual(
tqc.layout.initial_virtual_layout(),
Layout.from_qubit_list(
[
reverse_pos_map[2],
reverse_pos_map[1],
reverse_pos_map[0],
reverse_pos_map[3],
reverse_pos_map[4],
reverse_pos_map[5],
]
),
)
def test_initial_virtual_layout_filter_ancillas(self):
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
cmap = CouplingMap.from_line(6, bidirectional=False)
tqc = transpile(qc, coupling_map=cmap, initial_layout=[5, 2, 1], seed_transpiler=42)
self.assertEqual(
tqc.layout.initial_virtual_layout(True),
Layout(
{
qc.qubits[0]: 5,
qc.qubits[1]: 2,
qc.qubits[2]: 1,
}
),
)