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

Add support for QuantumCircuit.layout to qpy #10148

Merged
merged 32 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a78c691
Add support for QuantumCircuit.layout to qpy
mtreinish May 23, 2023
526b448
Fix handling of empty layout
mtreinish May 24, 2023
2b44bb1
Expand test coverage
mtreinish May 24, 2023
3588d05
Fix lint
mtreinish May 24, 2023
d3e1d31
Add qpy compat tests
mtreinish May 24, 2023
a58df9a
Fix compat tests
mtreinish May 24, 2023
8ea9cc9
Add release notes
mtreinish May 24, 2023
057f769
Adjust layout creation to be register independent
mtreinish May 24, 2023
c02f2b2
Finish docs
mtreinish May 24, 2023
6add8b4
Only check layout in compat tests with circuits
mtreinish May 24, 2023
03579cd
Merge branch 'main' into qpy-layout
mtreinish May 24, 2023
fb40807
Fix typos
mtreinish May 26, 2023
ecc9679
Merge branch 'main' into qpy-layout
mtreinish May 26, 2023
567ec15
Fix doc typo in qiskit/qpy/__init__.py
mtreinish Jun 5, 2023
75d37a8
Merge branch 'main' into qpy-layout
mtreinish Jun 5, 2023
0ab3405
Merge remote-tracking branch 'origin/main' into qpy-layout
mtreinish Jun 6, 2023
9a3e14a
Adjust introduction version for layout qpy compat tests
mtreinish Jun 6, 2023
c87c8f4
Unify qpy compat test version filter style
mtreinish Jun 6, 2023
659a391
Add new line to layout error message
mtreinish Jun 6, 2023
07fcec9
Simplify serialization logic
mtreinish Jun 6, 2023
dd0efc1
Doc fixes
mtreinish Jun 6, 2023
f67ded4
Improve test coverage
mtreinish Jun 6, 2023
af4e827
Don't reuse bits between initial layout and circuit in qpy compat tests.
mtreinish Jun 6, 2023
3544fd0
Merge branch 'main' into qpy-layout
mtreinish Jun 6, 2023
da23039
Update qiskit/qpy/__init__.py
mtreinish Jun 6, 2023
df56d6e
Merge branch 'main' into qpy-layout
mtreinish Jun 17, 2023
10798b8
Merge branch 'main' into qpy-layout
mtreinish Jun 19, 2023
f3354f3
Fix test typo
mtreinish Jun 19, 2023
34c9fa8
Merge branch 'main' into qpy-layout
mtreinish Jul 6, 2023
431368c
Use a register in compat tests for consistent equality
mtreinish Jul 7, 2023
acad493
Update test/python/qpy/test_circuit_load_from_qpy.py
mtreinish Jul 7, 2023
6b03f20
Merge branch 'main' into qpy-layout
mtreinish Jul 7, 2023
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
57 changes: 57 additions & 0 deletions qiskit/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,63 @@
by ``num_circuits`` in the file header). There is no padding between the
circuits in the data.

.. _qpy_version_8:

Version 8
=========

Version 8 adds support for handling a :class:`~.TranspileLayout` stored in the
:attr:`.QuantumCircuit.layout` attribute. In version 8 immediately following the
calibrations block at the end of the circuit payload there is now the
``LAYOUT`` struct. This struct outlines the size of the three attributes of a
:class:`~.TranspileLayout` class.

LAYOUT
------

.. 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;
}

If any of the signed values are ``-1`` this indicates the corresponding
attribute is ``None``.

Immediately following the ``LAYOUT`` struct there is a :ref:`qpy_registers` struct
for ``extra_registers`` (specifically the format introduced in :ref:`qpy_version_4`)
standalone register definitions that aren't present in the circuit. Then there
are ``initial_layout_size`` ``INITIAL_LAYOUT_BIT`` structs to define the
:attr:`.TranspileLayout.initial_layout` attribute.

INITIAL_LAYOUT_BIT
------------------

.. code-block:: c

struct {
int32_t index;
int32_t register_size;
}

Where a value of ``-1`` indicates ``None`` (as in no register is associated
with the bit). Following each ``INITIAL_LAYOUT_BIT`` struct is ``register_size``
bytes for a ``utf8`` encoded string for the register name.

Following the initial layout there is ``input_mapping_size`` array of
``uint32_t`` integers representing the positions of the phyiscal bit from the
initial layout. This enbables constructing a list of virtual bits where the
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
array index is its input mapping position.

Finally, there is an array of ``final_layout_size`` ``uint32_t`` integers. Each
element is an index in the circuit's ``qubits`` attribute which enables building
a mapping from qubit starting position to the output position at the end of the
circuit.

.. _qpy_version_7:

Version 7
Expand Down
136 changes: 135 additions & 1 deletion qiskit/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

"""Binary IO for circuit objects."""

from collections import defaultdict
import io
import json
import struct
Expand All @@ -36,6 +37,7 @@
from qiskit.qpy.binary_io import value, schedules
from qiskit.quantum_info.operators import SparsePauliOp
from qiskit.synthesis import evolution as evo_synth
from qiskit.transpiler.layout import Layout, TranspileLayout


def _read_header_v2(file_obj, version, vectors, metadata_deserializer=None):
Expand Down Expand Up @@ -761,6 +763,136 @@ def _write_registers(file_obj, in_circ_regs, full_bits):
return len(in_circ_regs) + len(out_circ_regs)


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))
return
initial_size = -1
input_qubit_mapping = {}
initial_layout_array = []
extra_registers = defaultdict(list)
if circuit.layout.initial_layout is not None:
initial_size = len(circuit.layout.initial_layout)
layout_mapping = circuit.layout.initial_layout.get_physical_bits()
for i in range(circuit.num_qubits):
qubit = layout_mapping[i]
input_qubit_mapping[qubit] = i
if qubit._register is not None or qubit._index is not None:
if qubit._register not in circuit.qregs:
extra_registers[qubit._register].append(qubit)
initial_layout_array.append((qubit._index, qubit._register))
else:
initial_layout_array.append((None, None))
input_qubit_size = -1
input_qubit_mapping_array = []
if circuit.layout.input_qubit_mapping is not None:
input_qubit_size = len(circuit.layout.input_qubit_mapping)
input_qubit_mapping_array = [None] * input_qubit_size
layout_mapping = circuit.layout.initial_layout.get_virtual_bits()
for qubit, index in circuit.layout.input_qubit_mapping.items():
if (
getattr(qubit, "_register", None) is not None
and getattr(qubit, "_index", None) is not None
):
if qubit._register not in circuit.qregs:
extra_registers[qubit._register].append(qubit)
input_qubit_mapping_array[index] = layout_mapping[qubit]
else:
input_qubit_mapping_array[index] = layout_mapping[qubit]
final_layout_size = -1
final_layout_array = []
if circuit.layout.final_layout is not None:
final_layout_size = len(circuit.layout.final_layout)
final_layout_physical = circuit.layout.final_layout.get_physical_bits()
for i in range(circuit.num_qubits):
virtual_bit = final_layout_physical[i]
final_layout_array.append(circuit.find_bit(virtual_bit).index)

file_obj.write(
struct.pack(
formats.LAYOUT_PACK,
True,
initial_size,
input_qubit_size,
final_layout_size,
len(extra_registers),
)
)
_write_registers(
file_obj, list(extra_registers), [x for bits in extra_registers.values() for x in bits]
)
for index, register in initial_layout_array:
reg_name_bytes = None if register is None else register.name.encode(common.ENCODE)
file_obj.write(
struct.pack(
formats.INITIAL_LAYOUT_BIT_PACK,
-1 if index is None else index,
-1 if reg_name_bytes is None else len(reg_name_bytes),
)
)
if reg_name_bytes is not None:
file_obj.write(reg_name_bytes)
for i in input_qubit_mapping_array:
file_obj.write(struct.pack("!I", i))
for i in final_layout_array:
file_obj.write(struct.pack("!I", i))


def _read_layout(file_obj, circuit):
header = formats.LAYOUT._make(
struct.unpack(formats.LAYOUT_PACK, file_obj.read(formats.LAYOUT_SIZE))
)
if not header.exists:
return
registers = {
name: QuantumRegister(len(v[1]), name)
for name, v in _read_registers_v4(file_obj, header.extra_registers)["q"].items()
}
initial_layout = None
initial_layout_virtual_bits = []
for _ in range(header.initial_layout_size):
virtual_bit = formats.INITIAL_LAYOUT_BIT._make(
struct.unpack(
formats.INITIAL_LAYOUT_BIT_PACK,
file_obj.read(formats.INITIAL_LAYOUT_BIT_SIZE),
)
)
if virtual_bit.index == -1 and virtual_bit.register_size == -1:
qubit = Qubit()
else:
register_name = file_obj.read(virtual_bit.register_size).decode(common.ENCODE)
if register_name in registers:
qubit = registers[register_name][virtual_bit.index]
else:
register = next(filter(lambda x, name=register_name: x.name == name, circuit.qregs))
qubit = register[virtual_bit.index]
initial_layout_virtual_bits.append(qubit)
if initial_layout_virtual_bits:
initial_layout = Layout.from_qubit_list(initial_layout_virtual_bits)
input_qubit_mapping = None
input_qubit_mapping_array = []
for _ in range(header.input_mapping_size):
input_qubit_mapping_array.append(
struct.unpack("!I", file_obj.read(struct.calcsize("!I")))[0]
)
if input_qubit_mapping_array:
input_qubit_mapping = {}
physical_bits = initial_layout.get_physical_bits()
for index, bit in enumerate(input_qubit_mapping_array):
input_qubit_mapping[physical_bits[bit]] = index
final_layout = None
final_layout_array = []
for _ in range(header.final_layout_size):
final_layout_array.append(struct.unpack("!I", file_obj.read(struct.calcsize("!I")))[0])

if final_layout_array:
layout_dict = {circuit.qubits[bit]: index for index, bit in enumerate(final_layout_array)}
final_layout = Layout(layout_dict)

circuit._layout = TranspileLayout(initial_layout, input_qubit_mapping, final_layout)


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

Expand Down Expand Up @@ -830,6 +962,7 @@ def write_circuit(file_obj, circuit, metadata_serializer=None):

# Write calibrations
_write_calibrations(file_obj, circuit.calibrations, metadata_serializer)
_write_layout(file_obj, circuit)


def read_circuit(file_obj, version, metadata_deserializer=None):
Expand Down Expand Up @@ -947,5 +1080,6 @@ def read_circuit(file_obj, version, metadata_deserializer=None):
f"as they weren't used in the circuit: {circ.name}",
UserWarning,
)

if version >= 8:
_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 = 7
QPY_VERSION = 8
ENCODE = "utf8"


Expand Down
11 changes: 11 additions & 0 deletions qiskit/qpy/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,14 @@
MAP_ITEM = namedtuple("MAP_ITEM", ["key_size", "type", "size"])
MAP_ITEM_PACK = "!H1cH"
MAP_ITEM_SIZE = struct.calcsize(MAP_ITEM_PACK)

LAYOUT = namedtuple(
"LAYOUT",
["exists", "initial_layout_size", "input_mapping_size", "final_layout_size", "extra_registers"],
)
LAYOUT_PACK = "!?iiiI"
LAYOUT_SIZE = struct.calcsize(LAYOUT_PACK)

INITIAL_LAYOUT_BIT = namedtuple("INITIAL_LAYOUT_BIT", ["index", "register_size"])
INITIAL_LAYOUT_BIT_PACK = "!ii"
INITIAL_LAYOUT_BIT_SIZE = struct.calcsize(INITIAL_LAYOUT_BIT_PACK)
12 changes: 12 additions & 0 deletions releasenotes/notes/qpy-layout-927ab34f2b47f4aa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
upgrade:
- |
The QPY format version emitted by :class:`~.qpy.dump` has increased to 8.
This new format version adds support for serializing the
:attr:`.QuantumCircuit.layout` attribute.
fixes:
- |
Fixed the :mod:`~qiskit.qpy` serialization of :attr:`.QuantumCircuit.layout`
attribue. Previously, the :attr:`~.QuantumCircuit.layout` attribute would
have been dropped when serializing a circuit to QPY.
Fixed `#10112 <https://github.com/Qiskit/qiskit-terra/issues/10112>`__
108 changes: 105 additions & 3 deletions test/python/qpy/test_circuit_load_from_qpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@

from ddt import ddt, data

from qiskit.circuit import QuantumCircuit
from qiskit.providers.fake_provider import FakeHanoi
from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit
from qiskit.providers.fake_provider import FakeHanoi, FakeSherbrooke
from qiskit.qpy import dump, load
from qiskit.test import QiskitTestCase
from qiskit.transpiler import PassManager
from qiskit.transpiler import PassManager, TranspileLayout
from qiskit.transpiler import passes
from qiskit.compiler import transpile


class QpyCircuitTestCase(QiskitTestCase):
Expand All @@ -35,6 +36,8 @@ def assert_roundtrip_equal(self, circuit):
new_circuit = load(qpy_file)[0]

self.assertEqual(circuit, new_circuit)
if circuit.layout is not None:
self.assertEqual(circuit.layout, new_circuit.layout)
mtreinish marked this conversation as resolved.
Show resolved Hide resolved


@ddt
Expand Down Expand Up @@ -67,3 +70,102 @@ def test_rzx_calibration_echo(self, angle):
rzx_qc = pass_manager.run(test_qc)

self.assert_roundtrip_equal(rzx_qc)


@ddt
class TestLayout(QpyCircuitTestCase):
"""Test circuit serialization for layout preservation."""

@data(0, 1, 2, 3)
def test_transpile_layout(self, opt_level):
"""Test layout preserved after transpile."""
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
backend = FakeSherbrooke()
tqc = transpile(qc, backend, optimization_level=opt_level)
self.assert_roundtrip_equal(tqc)

mtreinish marked this conversation as resolved.
Show resolved Hide resolved
@data(0, 1, 2, 3)
def test_transpile_with_routing(self, opt_level):
"""Test full layout with routing is preserved."""
qc = QuantumCircuit(5)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.cx(0, 3)
qc.cx(0, 4)
qc.measure_all()
backend = FakeSherbrooke()
tqc = transpile(qc, backend, optimization_level=opt_level)
self.assert_roundtrip_equal(tqc)

@data(0, 1, 2, 3)
def test_transpile_layout_explicit_None_final_layout(self, opt_level):
"""Test layout preserved after transpile."""
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
backend = FakeSherbrooke()
tqc = transpile(qc, backend, optimization_level=opt_level)
tqc.layout.final_layout = None
self.assert_roundtrip_equal(tqc)

def test_empty_layout(self):
"""Test an empty layout is preserved correctly."""
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
qc._layout = TranspileLayout(None, None, None)
self.assert_roundtrip_equal(qc)

@data(0, 1, 2, 3)
def test_custom_register_name(self, opt_level):
"""Test layout preserved with custom register names."""
qr = QuantumRegister(5, name="abc123")
qc = QuantumCircuit(qr)
qc.h(0)
qc.cx(0, 1)
qc.cx(0, 2)
qc.cx(0, 3)
qc.cx(0, 4)
qc.measure_all()
backend = FakeSherbrooke()
tqc = transpile(qc, backend, optimization_level=opt_level)
self.assert_roundtrip_equal(tqc)

@data(0, 1, 2, 3)
def test_no_register(self, opt_level):
"""Test layout preserved with no register."""
qubits = [Qubit(), Qubit()]
qc = QuantumCircuit(qubits)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
backend = FakeSherbrooke()
tqc = transpile(qc, backend, optimization_level=opt_level)
# Manually validate to deal with qubit equality needing exact objects
qpy_file = io.BytesIO()
dump(tqc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(tqc, new_circuit)
initial_layout_old = tqc.layout.initial_layout.get_physical_bits()
initial_layout_new = new_circuit.layout.initial_layout.get_physical_bits()
for i in initial_layout_old:
self.assertIsInstance(initial_layout_old[i], Qubit)
self.assertIsInstance(initial_layout_new[i], Qubit)
if initial_layout_old[i]._register is not None:
self.assertEqual(initial_layout_new[i], initial_layout_old[i])
else:
self.assertIsNone(initial_layout_new[i]._register)
self.assertIsNone(initial_layout_old[i]._index)
self.assertIsNone(initial_layout_new[i]._index)
self.assertEqual(
list(tqc.layout.input_qubit_mapping.values()),
list(new_circuit.layout.input_qubit_mapping.values()),
)
self.assertEqual(tqc.layout.final_layout, new_circuit.layout.final_layout)
Loading