diff --git a/qiskit/passmanager/passrunner.py b/qiskit/passmanager/passrunner.py index 9ce983ce6591..16126b02fe18 100644 --- a/qiskit/passmanager/passrunner.py +++ b/qiskit/passmanager/passrunner.py @@ -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. @@ -229,7 +232,6 @@ 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( @@ -237,7 +239,8 @@ def run( 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( diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index d7c04ab5eeef..df0d8dbc2426 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -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: diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 5266bbb0346b..6dd157af470e 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -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 = {} @@ -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( @@ -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() @@ -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. @@ -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 diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 89eb1c7644b5..a8d615b28604 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -20,7 +20,7 @@ from qiskit.qpy import formats -QPY_VERSION = 9 +QPY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 64e34d9754a2..62b00a17c30a 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -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"], diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index 46b10b6510da..646bc836f130 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -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"] diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index 0e318e6a8db1..71b50fa49b08 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -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: @@ -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: @@ -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) diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py index aa574414667a..7962551e02d9 100644 --- a/qiskit/transpiler/runningpassmanager.py +++ b/qiskit/transpiler/runningpassmanager.py @@ -100,7 +100,7 @@ def _to_passmanager_ir(self, in_program: QuantumCircuit) -> DAGCircuit: 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 @@ def _to_target(self, passmanager_ir: DAGCircuit) -> QuantumCircuit: 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"] diff --git a/releasenotes/notes/transpile-layout-improvements-118dd902d93e5b96.yaml b/releasenotes/notes/transpile-layout-improvements-118dd902d93e5b96.yaml new file mode 100644 index 000000000000..96853f8dc2d0 --- /dev/null +++ b/releasenotes/notes/transpile-layout-improvements-118dd902d93e5b96.yaml @@ -0,0 +1,76 @@ +--- +features: + - | + Added new methods to :class:`.TranspileLayout`, :meth:`~.TranspileLayout.initial_layout_list` + and :meth:`~.TranspileLayout.final_layout_list`, 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.final_layout_list` will return:: + + [2, 3, 0, 1] + + - | + Added a new method, :meth:`~.TranspileLayout.full_layout`, to the :class:`~.TranspileLayout` + class. This method is used to return a full layout as a list to show the 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) + qc.h(2) + qc.cx(2, 1) + qc.swap(0, 1) + qc.cx(2, 1) + + then the output from :func:`~.TranspileLayout.full_layout` would return a + list of:: + + [2, 0, 1] + + - | + Added a new method, :meth:`~.TranspileLayout.permute_sparse_pauli_op`, + to the :class:~.TranspileLayout` class. This method is used to take + a :class:`~.SparsePauliOp` observable for a given input circuit and permute + it based on the layout from the transpiler. This enables working with + the :class:`~.BaseEstimator` implementations and local transpilation more + easily. For example:: + + from qiskit.circuit.library import RealAmplitudes + from qiskit.quantum_info import SparsePauliOp + from qiskit.primitives import BackendEstimator + from qiskit.compiler import transpile + from qiskit.providers.fake_provider import FakeNairobiV2 + + + psi = RealAmplitudes(num_qubits=2, reps=2) + H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + backend = FakeNairobiV2() + estimator = BackendEstimator(backend=backend, skip_transpilation=True) + thetas = [0, 1, 1, 2, 3, 5] + + transpiled_psi = transpile(psi, backend, optimization_level=3) + permuted_op = transpiled_psi.layout.permute_sparse_pauli_op(H1) + res = estimator.run(transpiled_psi, permuted_op, thetas) + + where you locally transpile the input circuit before passing it to + :class:`~.BaseEstimator.run`, the transpiled circuit will be expanded from + 2 qubits to 7 qubits and the qubits will be permuted as part of + transpilation. Using :meth:`~.TranspileLayout.permute_sparse_pauli_op` + transforms ``H1`` which was constructed assuming the original untranspiled + circuit to reflect the transformations :func:`~.transpile` performed on + the circuit. diff --git a/test/python/transpiler/test_transpile_layout.py b/test/python/transpiler/test_transpile_layout.py new file mode 100644 index 000000000000..78f6adcd85b3 --- /dev/null +++ b/test/python/transpiler/test_transpile_layout.py @@ -0,0 +1,128 @@ +# 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""" + +import numpy as np + +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 +from qiskit.circuit.library import EfficientSU2 +from qiskit.quantum_info import SparsePauliOp +from qiskit.primitives import BackendEstimator +from qiskit.providers.fake_provider import FakeNairobiV2 + + +class TranspileLayoutTest(QiskitTestCase): + """Test the methods in the TranspileLayout object.""" + + def test_full_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.full_layout() + self.assertEqual(res, [2, 0, 1]) + + def test_permute_sparse_pauli_op(self): + psi = EfficientSU2(4, reps=4, entanglement="circular") + op = SparsePauliOp.from_list([("IIII", 1), ("IZZZ", 2), ("XXXI", 3)]) + backend = FakeNairobiV2() + transpiled_psi = transpile(psi, backend, optimization_level=3, seed_transpiler=12345) + permuted_op = transpiled_psi.layout.permute_sparse_pauli_op(op) + identity_op = SparsePauliOp("I" * 7) + initial_layout = transpiled_psi.layout.initial_layout_list(filter_ancillas=True) + final_layout = transpiled_psi.layout.final_layout_list() + qargs = [final_layout[x] for x in initial_layout] + expected_op = identity_op.compose(op, qargs=qargs) + self.assertNotEqual(op, permuted_op) + self.assertEqual(permuted_op, expected_op) + + def test_permute_sparse_pauli_op_estimator_example(self): + psi = EfficientSU2(4, reps=4, entanglement="circular") + op = SparsePauliOp.from_list([("IIII", 1), ("IZZZ", 2), ("XXXI", 3)]) + backend = FakeNairobiV2() + backend.set_options(seed_simulator=123) + estimator = BackendEstimator(backend=backend, skip_transpilation=True) + thetas = list(range(len(psi.parameters))) + transpiled_psi = transpile(psi, backend, optimization_level=3) + permuted_op = transpiled_psi.layout.permute_sparse_pauli_op(op) + job = estimator.run(transpiled_psi, permuted_op, thetas) + res = job.result().values + np.testing.assert_allclose(res, [1.35351562], rtol=0.5, atol=0.2) + + def test_final_layout_list(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.final_layout_list() + self.assertEqual(res, [2, 4, 1, 0, 3]) + + def test_final_layout_list_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.final_layout_list() + self.assertEqual(res, list(range(5))) + + def test_initial_layout_list(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_layout_list(), [2, 1, 0]) + + def test_initial_layout_list_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_layout_list(), [2, 1, 0, 3, 4, 5]) + + def test_initial_layout_list_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_layout_list(True), [5, 2, 1])