Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the ergonomics of the TranspileLayout class #10835

Merged
merged 14 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
34 changes: 30 additions & 4 deletions qiskit/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,36 @@
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:
Version 10 adds support for symengine-native serialization for objects of type
:class:`~.ParameterExpression` as well as symbolic expressions in Pulse schedule blocks.
adds support for new fields in the :class:`~.TranspileLayout` class added in the Qiskit
0.45.0 release.
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

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
encoding and ``e`` refers to symengine encoding.

FILE_HEADER
-----------

The contents of FILE_HEADER after V10 are defined as a C struct as:

.. code-block:: c

struct {
uint8_t qpy_version;
uint8_t qiskit_major_version;
uint8_t qiskit_minor_version;
uint8_t qiskit_patch_version;
uint64_t num_circuits;
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

Expand All @@ -167,7 +194,6 @@
``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:

Version 9
Expand Down
2 changes: 1 addition & 1 deletion qiskit/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ def _read_layout_v2(file_obj, circuit):
circuit._layout._output_qubit_list = circuit.qubits


def write_circuit(file_obj, circuit, metadata_serializer=None):
def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=False):
"""Write a single QuantumCircuit object in the file like object.

Args:
Expand Down
33 changes: 9 additions & 24 deletions qiskit/transpiler/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
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 @@ -482,10 +481,10 @@ class TranspileLayout:
_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
"""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 analgous
circuit to the physical qubit selected during layout. This is analogous
to the :attr:`.initial_layout` attribute.

Args:
Expand All @@ -507,15 +506,15 @@ def initial_virtual_layout(self, filter_ancillas: bool = False) -> Layout:
)

def initial_index_layout(self, filter_ancillas: bool = False) -> List[int]:
"""Generate an initial layout as a
"""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
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
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()
Expand All @@ -531,15 +530,15 @@ def initial_index_layout(self, filter_ancillas: bool = False) -> List[int]:
return output

def routing_permutation(self) -> List[int]:
"""Generate a final layout as a an array of integers
"""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]``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly I guess this would go to n - 1?

to indicate this

Returns:
A layout array that maps a position in the array to it's new position in the output
A layout array that maps a position in the array to its new position in the output
circuit
"""
if self.final_layout is None:
Expand All @@ -548,7 +547,7 @@ def routing_permutation(self) -> List[int]:
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 a list of integers
"""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::
Expand Down Expand Up @@ -656,17 +655,3 @@ 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)})

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.final_index_layout()
return identity.compose(operator, qargs=qargs)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
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
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::
Expand All @@ -14,15 +14,22 @@ features:
qr[3]: 1,
})

then :meth:`~.TranspileLayout.final_layout_list` will return::
then :meth:`~.TranspileLayout.routing_permutation` 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::
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)
Expand All @@ -32,45 +39,21 @@ features:
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)
tqc.h(2)
tqc.cx(2, 1)
tqc.swap(0, 1)
tqc.cx(2, 1)

then the output from :func:`~.TranspileLayout.full_layout` would return a
then the output from :meth:`~.TranspileLayout.final_index_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

The :meth:`~.TranspileLayout.final_virtual_layout` returns this as a :class:`.Layout` object,
so the return from the above example would be::

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.
Layout({
qc.qubits[0]: 2,
qc.qubits[1]: 0,
qc.qubits[2]: 1,
})
33 changes: 0 additions & 33 deletions test/python/transpiler/test_transpile_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,11 @@

"""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):
Expand Down Expand Up @@ -189,33 +183,6 @@ def test_final_virtual_layout_full_path_with_ancilla_no_filter(self):
)
self.assertEqual(res, expected)

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_index_layout(filter_ancillas=True)
final_layout = transpiled_psi.layout.routing_permutation()
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_routing_permutation(self):
qr = QuantumRegister(5)
final_layout = Layout(
Expand Down