Skip to content

Commit

Permalink
Add support for QuantumCircuit.layout to qpy (#10148)
Browse files Browse the repository at this point in the history
* Add support for QuantumCircuit.layout to qpy

This commit adds the missing support for QuantumCircuit.layout to the
qpy format. This necessitates bumping the QPY format version to 8 to
accomodate the extra data needed for representing the details of the
layout. The tricky piece with representing the 3 TranspileLayout
attributes is representing the virtual bits in the initial layout
because there is no guarantee that the input circuit's registers are in
the output circuit (typically they are not when transpile() is used).

Fixes #10112

* Fix handling of empty layout

* Expand test coverage

* Fix lint

* Add qpy compat tests

* Fix compat tests

* Add release notes

* Adjust layout creation to be register independent

* Finish docs

* Only check layout in compat tests with circuits

* Fix typos

* Fix doc typo in qiskit/qpy/__init__.py

Co-authored-by: John Lapeyre <jlapeyre@users.noreply.github.com>

* Adjust introduction version for layout qpy compat tests

* Unify qpy compat test version filter style

* Add new line to layout error message

* Simplify serialization logic

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>

* Doc fixes

* Improve test coverage

* Don't reuse bits between initial layout and circuit in qpy compat tests.

* Update qiskit/qpy/__init__.py

* Fix test typo

* Use a register in compat tests for consistent equality

* Update test/python/qpy/test_circuit_load_from_qpy.py

---------

Co-authored-by: John Lapeyre <jlapeyre@users.noreply.github.com>
Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
(cherry picked from commit dbf1230)
  • Loading branch information
mtreinish authored and mergify[bot] committed Jul 7, 2023
1 parent c0f02c6 commit f4e9d50
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 5 deletions.
57 changes: 57 additions & 0 deletions qiskit/qpy/__init__.py
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 enables constructing a list of virtual bits where the
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
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
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
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
@@ -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>`__
107 changes: 104 additions & 3 deletions test/python/qpy/test_circuit_load_from_qpy.py
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,7 @@ def assert_roundtrip_equal(self, circuit):
new_circuit = load(qpy_file)[0]

self.assertEqual(circuit, new_circuit)
self.assertEqual(circuit.layout, new_circuit.layout)


@ddt
Expand Down Expand Up @@ -67,3 +69,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)

@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)

0 comments on commit f4e9d50

Please sign in to comment.