diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index 5710424d4734..9271445cf853 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -18,7 +18,8 @@ Physical (qu)bits are integers. """ from __future__ import annotations -from typing import List + +from typing import List, TYPE_CHECKING from dataclasses import dataclass from qiskit import circuit @@ -26,6 +27,10 @@ from qiskit.transpiler.exceptions import LayoutError from qiskit.converters import isinstanceint +if TYPE_CHECKING: + from qiskit.dagcircuit import DAGCircuit + from qiskit.transpiler import PropertySet + class Layout: """Two-ways dict to represent a Layout.""" @@ -738,3 +743,234 @@ def final_virtual_layout(self, filter_ancillas: bool = True) -> Layout: 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)}) + + @classmethod + def from_property_set( + cls, dag: DAGCircuit, property_set: PropertySet + ) -> TranspileLayout | None: + """Construct the :class:`TranspileLayout` by reading out the fields from the given + :class:`.PropertySet`. Returns ``None`` if there are no layout-setting keys present. + + This includes combining the different keys of the property set into the full set of initial + and final layouts, including virtual permutations. + + This does not invalidate or in any way mutate the given property set. In order to + "canonicalize" the property set afterwards, call :meth:`write_into_property_set`. + + This reads the following property-set keys: + + ``layout`` + **Required**. The :class:`.Layout` object mapping virtual qubits (potentially expanded + with ancillas) to physical-qubit indices. This corresponds directly to + :attr:`initial_layout`. + + .. note:: + In all standard use, this is a required field. However, if + ``virtual_permutation_layout`` is set, then a "trivial" layout will be inferred, + even if the circuit is not actually laid out to hardware. This is an unfortunate + limitation of this class's data model, where it is not possible to specify a final + permutation without also having an initial layout. This deficiency will be corrected + in Qiskit 3.0. + + ``original_qubit_indices`` + **Required** (but automatically set by the :class:`.PassManager`). The mapping + ``{virtual: index}`` that indicates which relative index each incoming virtual qubit + was, in the input circuit. This can be expanded with ancillas too (in which case the + ancilla indices don't mean much, since they weren't in the incoming circuit). + + ``num_input_qubits`` + **Required** (but automatically set by the :class:`.PassManager`). The number of + explicit virtual qubits in the input circuit (i.e. not including implicit ancillas). + + ``final_layout`` + **Optional**. The effective final permutation, in terms of the current qubits of the + :class:`.DAGCircuit`. This corresponds directly to :attr:`final_layout`. + + ``virtual_permutation_layout`` + **Optional**. This is set by certain optimization passes that run before layout + selection, such as :class:`.ElidePermutations`. It is similar in spirit to + ``final_layout``, but typically only applies to the input virtual qubits. + + .. warning:: + This object uses the opposite permutation convention to ``final_layout`` due to an + oversight in Qiskit during its introduction. In other words, + ``virtual_permutation_layout`` maps a :class:`.Qubit` instance at the end of the + circuit to its integer index at the start of the circuit. + + Args: + dag: the current state of the :class:`.DAGCircuit`. + property_set: the current transpiler's property set. This must at least have the + ``layout`` key set. + """ + initial_layout = property_set["layout"] + final_layout = property_set["final_layout"] + input_qubit_indices = property_set["original_qubit_indices"] + virtual_permutation_layout = property_set["virtual_permutation_layout"] + num_input_qubits = property_set["num_input_qubits"] + + output_qubits = list(dag.qubits) + + if initial_layout is None and virtual_permutation_layout is None and final_layout is None: + # Nothing that truly sets a Python-space `TranspileLayout` is set. + return None + if initial_layout is not None and virtual_permutation_layout is None: + # This is the "happy" path where everything is already (in theory) normalised to the + # original state of how the transpiler handled these properties. + return cls( + initial_layout, input_qubit_indices, final_layout, num_input_qubits, output_qubits + ) + + # Due to current (at least as of Qiskit 2.x) limitations of `TranspileLayout`, the only + # way to return a routing permutation if `virtual_permutation_layout` is set is to force + # an initial layout, even if there isn't actually any laying out to hardware. + if initial_layout is None: + initial_layout = Layout(dict(enumerate(dag.qubits))) + if virtual_permutation_layout is None: + virtual_permutation_layout = Layout(input_qubit_indices) + if final_layout is None: + final_layout = Layout(dict(enumerate(dag.qubits))) + + input_qubits = sorted(input_qubit_indices, key=input_qubit_indices.get) + + num_qubits = len(dag.qubits) + + # Throughout the rest of this, we will speak about index permutations as lists that mean: + # + # qubit `permutation[i]` goes to new index `i` + # + # or in alternative langauge, + # + # after the permutation, qubit `i` contains qubit `permutation[i]`. + # + # This is to match the convention that `PermutationGate` uses, but beware: it might not be + # the way you think about permutations (it's not my preferred convention---Jake). + # + # Now, we'll step through the transpilation process. At each point, we'll relate the + # objects we have back to a 3-tuple of abstract objects, which are applied in order: + # + # (relabelling, explicit instructions, implicit instructions) + # + # The "explicit instructions" are always just the DAG itself. The "relabelling" is + # generally associated with the "initial layout" and the metadata linking the original + # virtual qubit objects and their indices. The "implicit instructions" is where all the + # interesting stuff happens; at the moment, in Qiskit, we only track an implicit final + # permutation, though you could imagine a world where we allow a lot more things to be + # tracked, such as necessary classical post-processing steps. + # + # We will attempt to always have in hand the permutation that needs to be appended to the + # current explicit circuit to "undo" all the elided/added permutations. For example, we + # want the permutation that adds back in what `ElidePermutations` might have removed, or + # "undoes" the swaps that routing added. Explicitly, we want to have a ``permutation`` such + # that this sequence of operations brings us back to the same semantics as the original + # virtual circuit: + # + # current = + # # Make the permutation explicit; the permutation is defined on the current qubit labels. + # current.append(PermutationGate(permutation), current.qubits) + # # Now revert the `initial_layout` relabelling. + # relabel_qubits_from_physical_to_virtual(qc, initial_layout) + # + # where "semantics" would mean exact unitary equivalence for a unitary input, and something + # a bit hand-wavier once measurements are involved. + + # First, virtual permutation modifications happen. For example, `ElidePermutations` or + # `StarPreRouting`. Note that `virtual_permutation_layout` uses an opposite convention to + # `final_layout` for defining the permutation. + undo_elided_on_virtuals = [ + virtual_permutation_layout[virtual_bit] + for virtual_bit in input_qubits[:num_input_qubits] + ] + # `virtual_permutation_layout` is defined without ancillas. If they got added later, extend + # the virtual permutation with the implicit identity on the other components. + if num_qubits > len(undo_elided_on_virtuals): + undo_elided_on_virtuals.extend(range(len(undo_elided_on_virtuals), num_qubits)) + + def relabel_virtual_to_physical(virtual_index: int): + return initial_layout[input_qubits[virtual_index]] + + def relabel_physical_to_virtual(physical_index: int): + return input_qubit_indices[initial_layout[physical_index]] + + # Next, a layout pass runs, and maps the virtual qubits to physical qubits. We want to + # update our permutation so that it can be applied to the _physical_ circuit instead. This + # means relabelling both references to circuit indices: the actual values in the list, but + # also the indices in the list that they're located at. + undo_elided_on_physicals = [ + relabel_virtual_to_physical( + undo_elided_on_virtuals[relabel_physical_to_virtual(physical_index)] + ) + for physical_index in range(num_qubits) + ] + + # Next, routing runs. This adds in an extra permutation, which comes between "the circuit" + # and the "undoing permutation" we just calculated. Routing returns the total + # permutation that it has applied, so if we send the "index before routing" to "index after + # routing", our permutation will then properly undo everything. Note that `final_layout` + # uses the opposite permutation convention to `virtual_permutation_layout`. + undo_routing_on_physicals = [ + dag.find_bit(final_layout[physical_index]).index for physical_index in range(num_qubits) + ] + undo_total_on_physicals = [ + undo_elided_on_physicals[undo_routing_on_physicals[physical_index]] + for physical_index in range(num_qubits) + ] + + # Finally, turn what we have into the same convention that `final_layout` uses. + final_layout = Layout( + { + qubit_index: dag.qubits[is_set_to] + for qubit_index, is_set_to in enumerate(undo_total_on_physicals) + } + ) + + return cls( + initial_layout, input_qubit_indices, final_layout, num_input_qubits, list(dag.qubits) + ) + + def write_into_property_set(self, property_set: dict[str, object]): + """'Unpack' this layout into the loose-constraints form of the ``property_set``. + + This is the inverse method of :meth:`from_property_set`. + + This always writes the follow property-set keys, overwriting them if they were already set: + + ``layout`` + Directly corresponds to :attr:`initial_layout`. + + ``original_qubit_indices`` + Directly corresponds to :attr:`input_qubit_mapping`. + + ``final_layout`` + Directly corresponds to :attr:`final_layout`. Note that this might not be identical to + the ``final_layout`` from before a call to :meth:`from_property_set`, because the + effects of ``virtual_permutation_layout`` will have been combined into it. + + ``virtual_permutation_layout`` + Deleted from the property set; :class:`TranspileLayout` "finalizes" the multiple + separate permutations into one single permutation, to retain the canonical form. + + In addition, the following keys are updated, if this :class:`TranspileLayout` has a known + value for them. They are left as-is if not, to handle cases where this class was manually + constructed without setting certain optional fields. + + ``num_input_qubits`` + The number of non-ancilla virtual qubits in the input circuit. + + Args: + property_set: the :class:`.PropertySet` (or general :class:`dict`) that the output + should be written into. This mutates the input in place. + """ + for always_overwrite in ( + "layout", + "final_layout", + "original_qubit_indices", + "virtual_permutation_layout", + ): + property_set.pop(always_overwrite, None) + + property_set["layout"] = self.initial_layout.copy() + property_set["original_qubit_indices"] = self.input_qubit_mapping.copy() + if self.final_layout is not None: + property_set["final_layout"] = self.final_layout.copy() + if self._input_qubit_count is not None: + property_set["num_input_qubits"] = self._input_qubit_count diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index 87145a71013a..919110ccb7c0 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -29,7 +29,7 @@ from qiskit.passmanager.exceptions import PassManagerError from .basepasses import BasePass from .exceptions import TranspilerError -from .layout import TranspileLayout, Layout +from .layout import TranspileLayout _CircuitsT = TypeVar("_CircuitsT", bound=Union[List[QuantumCircuit], QuantumCircuit]) @@ -72,23 +72,18 @@ def _passmanager_backend( **kwargs, ) -> QuantumCircuit: out_program = dag_to_circuit(passmanager_ir, copy_operations=False) - - self._finalize_layouts(passmanager_ir) - out_name = kwargs.get("output_name", None) - if out_name is not None: + if (out_name := kwargs.get("output_name", None)) is not None: out_program.name = out_name - if self.property_set["layout"] is not None: - out_program._layout = TranspileLayout( - 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=out_program.qubits, - ) + if ( + layout := TranspileLayout.from_property_set(passmanager_ir, self.property_set) + ) is not None: + out_program._layout = layout + # Write the canonicalized form back out. This is for backwards compatibility. + layout.write_into_property_set(self.property_set) + out_program._clbit_write_latency = self.property_set["clbit_write_latency"] out_program._conditional_latency = self.property_set["conditional_latency"] - if self.property_set["node_start_time"]: # This is dictionary keyed on the DAGOpNode, which is invalidated once # dag is converted into circuit. So this schedule information is @@ -101,49 +96,6 @@ def _passmanager_backend( return out_program - def _finalize_layouts(self, dag): - if (virtual_permutation_layout := self.property_set["virtual_permutation_layout"]) is None: - return - - self.property_set.pop("virtual_permutation_layout") - - # virtual_permutation_layout is usually created before extending the layout with ancillas, - # so we extend the permutation to be identity on ancilla qubits - original_qubit_indices = self.property_set.get("original_qubit_indices", None) - for oq in original_qubit_indices: - if oq not in virtual_permutation_layout: - virtual_permutation_layout[oq] = original_qubit_indices[oq] - - t_qubits = dag.qubits - - if (t_initial_layout := self.property_set.get("layout", None)) is None: - t_initial_layout = Layout(dict(enumerate(t_qubits))) - - if (t_final_layout := self.property_set.get("final_layout", None)) is None: - t_final_layout = Layout(dict(enumerate(t_qubits))) - - # Ordered list of original qubits - original_qubits_reverse = {v: k for k, v in original_qubit_indices.items()} - original_qubits = [] - # pylint: disable-next=consider-using-enumerate - for i in range(len(original_qubits_reverse)): - original_qubits.append(original_qubits_reverse[i]) - - virtual_permutation_layout_inv = virtual_permutation_layout.inverse( - original_qubits, original_qubits - ) - - t_initial_layout_inv = t_initial_layout.inverse(original_qubits, t_qubits) - - # ToDo: this can possibly be made simpler - new_final_layout = t_initial_layout_inv - new_final_layout = new_final_layout.compose(virtual_permutation_layout_inv, original_qubits) - new_final_layout = new_final_layout.compose(t_initial_layout, original_qubits) - new_final_layout = new_final_layout.compose(t_final_layout, t_qubits) - - self.property_set["layout"] = t_initial_layout - self.property_set["final_layout"] = new_final_layout - def append( # pylint:disable=arguments-renamed self, passes: Task | list[Task], diff --git a/releasenotes/notes/transpilelayout-from_property_set-97eb505156c13230.yaml b/releasenotes/notes/transpilelayout-from_property_set-97eb505156c13230.yaml new file mode 100644 index 000000000000..5beb41f4f3df --- /dev/null +++ b/releasenotes/notes/transpilelayout-from_property_set-97eb505156c13230.yaml @@ -0,0 +1,14 @@ +--- +features_transpiler: + - | + :class:`.TranspileLayout` has two new methods: :meth:`~.TranspileLayout.from_property_set` and + :meth:`~.TranspileLayout.write_into_property_set`, which formalize the current ad-hoc structure + of transpilation properties, and how they are converted into a :class:`.TranspileLayout`. This + makes it possible for passes _during_ a transpiler pipeline to access what the + :class:`.TranspileLayout` will be, modify it in the fully structured form, and then write it + back out in canonical form. + + It is expected that in the future version 3.0 of Qiskit, the :class:`.TranspileLayout` + (or something akin to it) will be a direct attribute of the :class:`.DAGCircuit` transpiler + intermediate representation, and required by passes to be kept in sync with the rest of the + :class:`.DAGCircuit`.