Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 237 additions & 1 deletion qiskit/transpiler/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@
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
from qiskit.circuit import Qubit, QuantumRegister
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."""
Expand Down Expand Up @@ -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
Comment thread
mtreinish marked this conversation as resolved.
``{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 = <current explicit circuit/DAG>
# # 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))
Comment thread
mtreinish marked this conversation as resolved.

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
66 changes: 9 additions & 57 deletions qiskit/transpiler/passmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down Expand Up @@ -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
Expand All @@ -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],
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
Loading