Skip to content

Commit

Permalink
Improve the ergonomics of the TranspileLayout class
Browse files Browse the repository at this point in the history
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 Qiskit#10826
Fixes Qiskit#10818
  • Loading branch information
mtreinish committed Sep 13, 2023
1 parent 579084e commit d423924
Show file tree
Hide file tree
Showing 10 changed files with 470 additions and 12 deletions.
9 changes: 6 additions & 3 deletions qiskit/passmanager/passrunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,14 @@ def _to_passmanager_ir(self, in_program):
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.
Expand Down Expand Up @@ -229,15 +232,15 @@ def run(
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(
pass_sequence=controller,
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(
Expand Down
22 changes: 22 additions & 0 deletions qiskit/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,28 @@
by ``num_circuits`` in the file header). There is no padding between the
circuits in the data.
.. _qpy_version_10:
Version 10
==========
Version 10 adds support for new fields in the :class:`~.TranspileLayout` class added in the Qiskit
0.45.0 release. 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;
uint32_t input_qubit_size;
}
The rest of the layout data after the ``LAYOUT`` struct is represented as in previous versions.
.. _qpy_version_9:
Expand Down
25 changes: 22 additions & 3 deletions qiskit/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,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 = {}
Expand Down Expand Up @@ -837,12 +837,13 @@ def _write_layout(file_obj, circuit):

file_obj.write(
struct.pack(
formats.LAYOUT_PACK,
formats.LAYOUT_V2_PACK,
True,
initial_size,
input_qubit_size,
final_layout_size,
len(extra_registers),
circuit._layout._input_qubit_count,
)
)
_write_registers(
Expand Down Expand Up @@ -871,6 +872,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()
Expand Down Expand Up @@ -919,6 +924,17 @@ 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)
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):
"""Write a single QuantumCircuit object in the file like object.
Expand Down Expand Up @@ -1110,5 +1126,8 @@ def read_circuit(file_obj, version, metadata_deserializer=None):
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
2 changes: 1 addition & 1 deletion qiskit/qpy/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from qiskit.qpy import formats

QPY_VERSION = 9
QPY_VERSION = 10
ENCODE = "utf8"


Expand Down
15 changes: 15 additions & 0 deletions qiskit/qpy/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,21 @@
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"],
Expand Down
2 changes: 2 additions & 0 deletions qiskit/transpiler/basepasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def __call__(self, circuit, property_set=None):
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"]
Expand Down
199 changes: 195 additions & 4 deletions qiskit/transpiler/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
Physical (qu)bits are integers.
"""
from __future__ import annotations
from typing import List
from dataclasses import dataclass

from qiskit.circuit.quantumregister import Qubit, QuantumRegister
from qiskit.transpiler.exceptions import LayoutError
from qiskit.converters import isinstanceint
from qiskit.quantum_info.operators.symplectic.sparse_pauli_op import SparsePauliOp


class Layout:
Expand Down Expand Up @@ -375,7 +377,74 @@ 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`. 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 :attr:`initial_layout` for the :class:`.TranspileLayout` 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 final layout of this circuit would be::
Layout({
qc.qubits[0]: 1,
qc.qubits[1]: 0,
qc.qubits[2]: 2,
})
which maps the qubits at the beginning of the circuit to their final position
after any swap insertions caused by routing. If ``final_layout`` is ``None``
this implies that no routing was performed and there is no output permutation.
There are three attributes associated with the class:
Expand All @@ -393,12 +462,134 @@ 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:`.full_layout` to generate this.
Additionally, this class provides several methods to return alternative views of
the layout generated by the transpiler to help working with the permutation the
transpiler might cause.
"""

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_layout_list(self, filter_ancillas: bool = False) -> List[int]:
"""Generate an initial layout as a
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 it's 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 final_layout_list(self) -> List[int]:
"""Generate a final layout as a 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 it's 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 full_layout(self) -> List[int]:
"""Generate the full layout as a list 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.
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 = []
for index in range(num_source_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 permute_sparse_pauli_op(self, operator: SparsePauliOp) -> SparsePauliOp:
"""Permute an operator based on a transpiled circuit's layout
Args:
operator: An input :class:`.SparsePauliOp` to permute according to the
permutation caused by the transpiler.
Return:
A new sparse Pauli op which has been permuted according to the output of the transpiler
"""
identity = SparsePauliOp("I" * len(self._output_qubit_list))
qargs = self.full_layout()
return identity.compose(operator, qargs=qargs)
Loading

0 comments on commit d423924

Please sign in to comment.