Skip to content

Commit

Permalink
Fix QPY support for control flow instructions (#7584)
Browse files Browse the repository at this point in the history
* Fix QPY support for control flow instructions

This commit fixes support for using qpy serialization with circuits that
contained control flow instructions. Previously, if you attempted to
serialize a circuit that contained control flow instructions it would
error as qpy didn't support embedding the circuit blocks as parameters
for the instructions. This has been fixed and other missing pieces for
fully reconstructing circuits with control flow were added. This
requires bumping the version string to v4 because to accurately
reconstruct control flow circuits we needed to be able to represent
registers that were only partially present in a circuit fully. V4 also
adds a representation for ranges which didn't exist in earlier versions.

Fixes #7583

* Fix handling of standalone register bits in circuit

This commit fixes the failing tests by correcting the handling of
standalone register bits that are part of a circuit (without their
register). Previosuly, if the circuit was solely composed of register
bits but the circuit didn't contain that register, qpy deserialization
would incorrectly add the register to the circuit. This corrects this by
having the deserialization function treat a register that's not in
circuit in the same manner as if it were an out of order register and
add each bit one at a time.

* Add test for for loop with iterator

* Add backwards compat tests for control flow

* Finish release notes

* Fix docs build

* Apply suggestions from code review

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

* Add handling for ContinueLoopOp too

* Cleanup note about register index array type change

* Use unique doc reference names for qpy module

* Update qpy version 3 ref in release note

* Switch tuple to support any qpy type instead of just ints

This commit changes the tuple formatting to work with any parameter type
that qpy currently supports, not just ints which is what for loop
currently uses a tuple for. This should provide some future proofing as
it enables tuples of other types, which is being considered in the
future, to work without modification.

* Update another broken sphinx ref

* Fix last dangling broken sphinx ref

* Fix yet another dangling sphinx ref

* Fix failing test

* Set fixed Python hash seed in qpy compat tests

This commit updates the qpy compat test runner script to set a fixed
hash seed. The control flow builders internally use sets which means
that to get consistent ordering between python processes we need to
ensure the hash seeds are the same. Without this the argument order
might differ between creating the circuit for qpy serialization and
reconstructing the circuit in another process by deserializing that
qpy data. Setting a fixed hash seed between processes ensures this type
of failure won't happen as the set order will be consistent between
multiple runs of Python.

* Update qiskit/circuit/qpy_serialization.py

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

* Fix docstring for TUPLE payload

This commit fixes an oversight in earlier commits where the
documentation for the TUPLE payload wasn't actually finished wasn't
really coherent. This commit finishes the documentation to explain how a
tuple is serialized in qpy.

* Add test case using nested tuples

* Directly construct WhileLoopOp in qpy compat tests

In an earlier commit we set a fixed (but still randomly generated) python
hash seed to force multiple invocations of python to use the same hash
seed. This was done in order to fix a non-deterministic failure we were
seeing with the while loop circuit. The while loop context builder uses
a set internally and is sensitive to argument ordering. However, just
hash seed randomization doesn't fix the issue in practice because the
difference isn't between set order strictly, but also that set order
matches the circuit insertion order for what gets written in QPY. To
address this and make the tests work stably this commit switches away
from using the builder interface and instead manually constructs the
while loop and appends it to the circuit with a fixed argument order.
This should hopefully make the compat tests run consistently and fix the
occasional failures we're seeing in CI.

* Fix lint

* Add test case for serializing an empty tuple instruction parameter

* Avoid builder interface for qpy compat tests

Just as was changed for the while loop generator function in the qpy
compat tests this commit updates the other control flow example circuits
to not use the builder interface. The same non-determinism in the
ordering of arguments applies to all the control flow instructions. So
to avoid spurious failures we should explicitly set a arg order and not
use the builder interface.

Co-authored-by: Jake Lishman <jake@binhbar.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 05cbc44)
  • Loading branch information
mtreinish authored and mergify-bot committed Feb 2, 2022
1 parent e58cb71 commit e8fc90f
Show file tree
Hide file tree
Showing 8 changed files with 584 additions and 159 deletions.
540 changes: 385 additions & 155 deletions qiskit/circuit/qpy_serialization.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion releasenotes/notes/0.19/qpy-v2-f1c380b40936cccf.yaml
Expand Up @@ -9,5 +9,5 @@ features:
QPY would only accept a :attr:`~qiskit.circuit.QuantumCircuit.global_phase`
that was a ``float``.
This requires the QPY format :ref:`version_2` which was introduced in
This requires the QPY format :ref:`qpy_version_2` which was introduced in
this release to represent the additional types.
Expand Up @@ -10,8 +10,8 @@ fixes:
order of :attr:`~qiskit.circuit.QuantumCircuit.parameters` could be
incorrect.
To fix this issue a new QPY format version, :ref:`version_3`, was required.
To fix this issue a new QPY format version, :ref:`qpy_version_3`, was required.
This new format version includes a representation of the
:class:`~qiskit.circuit.ParameterVectorElement` class which is
described in the :mod:`~qiskit.circuit.qpy_serialization` documentation at
:ref:`param_vector`.
:ref:`qpy_param_vector`.
Expand Up @@ -17,7 +17,7 @@ fixes:
available from :mod:`qiskit.synthesis` can be used with a
:class:`~qiskit.circuit.library.PauliEvolutionGate` for qpy serialization.
To fix this issue a new QPY format version, :ref:`version_3`, was required.
To fix this issue a new QPY format version, :ref:`qpy_version_3`, was required.
This new format version includes a representation of the
:class:`~qiskit.circuit.library.PauliEvolutionGate` class which is
described in the :mod:`~qiskit.circuit.qpy_serialization` documentation at
Expand Down
29 changes: 29 additions & 0 deletions releasenotes/notes/qpy-controlflow-97608dbfee5f3e7e.yaml
@@ -0,0 +1,29 @@
---
fixes:
- |
Fixed QPY serialization of :class:`.QuantumCircuit` objects that contained
control flow instructions. Previously if you attempted to serialize a
circuit containing :class:`.IfElseOp`, :class:`.WhileLoopOp`, or
:class:`ForLoopOp` the serialization would fail.
Fixed `#7583 <https://github.com/Qiskit/qiskit-terra/issues/7583>`__.
- |
Fixed QPY serialization of :class:`.QuantumCircuit` containing subsets of
bits from a :class:`.QuantumRegister` or :class:`.ClassicalRegister`.
Previously if you tried to serialize a circuit like this it would
incorrectly treat these bits as standalone :class:`.Qubit` or
:class:`.Clbit` without having a register set. For example, if you try to
serialize a circuit like::
import io
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.circuit.qpy_serialization import load, dump
qr = QuantumRegister(2)
qc = QuantumCircuit([qr[0]])
qc.x(0)
with open('file.qpy', 'wb') as fd:
dump(qc, fd)
when that circuit is loaded now the registers will be correctly populated
fully even though the circuit only contains a subset of the bits from the
register.
107 changes: 107 additions & 0 deletions test/python/circuit/test_circuit_load_from_qpy.py
Expand Up @@ -780,3 +780,110 @@ def test_parameter_vector_global_phase(self):
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)

def test_qpy_with_ifelseop(self):
"""Test qpy serialization with an if block."""
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.measure(0, 0)
with qc.if_test((qc.clbits[0], True)):
qc.x(1)
qc.measure(1, 1)
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)

def test_qpy_with_ifelseop_with_else(self):
"""Test qpy serialization with an else block."""
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.measure(0, 0)
with qc.if_test((qc.clbits[0], True)) as else_:
qc.x(1)
with else_:
qc.y(1)
qc.measure(1, 1)
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)

def test_qpy_with_while_loop(self):
"""Test qpy serialization with a for loop."""
qc = QuantumCircuit(2, 1)

with qc.while_loop((qc.clbits[0], 0)):
qc.h(0)
qc.cx(0, 1)
qc.measure(0, 0)
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)

def test_qpy_with_for_loop(self):
"""Test qpy serialization with a for loop."""
qc = QuantumCircuit(2, 1)

with qc.for_loop(range(5)):
qc.h(0)
qc.cx(0, 1)
qc.measure(0, 0)
qc.break_loop().c_if(0, True)
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)

def test_qpy_with_for_loop_iterator(self):
"""Test qpy serialization with a for loop."""
qc = QuantumCircuit(2, 1)

with qc.for_loop(iter(range(5))):
qc.h(0)
qc.cx(0, 1)
qc.measure(0, 0)
qc.break_loop().c_if(0, True)
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)

def test_standalone_register_partial_bit_in_circuit(self):
"""Test qpy with only some bits from standalone register."""
qr = QuantumRegister(2)
qc = QuantumCircuit([qr[0]])
qc.x(0)
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)

def test_nested_tuple_param(self):
"""Test qpy with an instruction that contains nested tuples."""
inst = Instruction("tuple_test", 1, 0, [((((0, 1), (0, 1)), 2, 3), ("A", "B", "C"))])
qc = QuantumCircuit(1)
qc.append(inst, [0])
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)

def test_empty_tuple_param(self):
"""Test qpy with an instruction that contains an empty tuple."""
inst = Instruction("empty_tuple_test", 1, 0, [tuple()])
qc = QuantumCircuit(1)
qc.append(inst, [0])
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)
5 changes: 5 additions & 0 deletions test/qpy_compat/run_tests.sh
Expand Up @@ -14,6 +14,11 @@
set -e
set -x

# Set fixed hash seed to ensure set orders are identical between saving and
# loading.
export PYTHONHASHSEED=$(python -S -c "import random; print(random.randint(1, 4294967295))")
echo "PYTHONHASHSEED=$PYTHONHASHSEED"

python -m venv qiskit_venv
qiskit_venv/bin/pip install ../..

Expand Down
54 changes: 54 additions & 0 deletions test/qpy_compat/test_qpy.py
Expand Up @@ -288,6 +288,58 @@ def generate_evolution_gate():
return qc


def generate_control_flow_circuits():
"""Test qpy serialization with control flow instructions."""
from qiskit.circuit.controlflow import WhileLoopOp, IfElseOp, ForLoopOp

# If instruction
circuits = []
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.measure(0, 0)
true_body = QuantumCircuit(1)
true_body.x(0)
if_op = IfElseOp((qc.clbits[0], True), true_body=true_body)
qc.append(if_op, [1])
qc.measure(1, 1)
circuits.append(qc)
# If else instruction
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.measure(0, 0)
false_body = QuantumCircuit(1)
false_body.y(0)
if_else_op = IfElseOp((qc.clbits[0], True), true_body, false_body)
qc.append(if_else_op, [1])
qc.measure(1, 1)
circuits.append(qc)
# While loop
qc = QuantumCircuit(2, 1)
block = QuantumCircuit(2, 1)
block.h(0)
block.cx(0, 1)
block.measure(0, 0)
while_loop = WhileLoopOp((qc.clbits[0], 0), block)
qc.append(while_loop, [0, 1], [0])
circuits.append(qc)
# for loop range
qc = QuantumCircuit(2, 1)
body = QuantumCircuit(2, 1)
body.h(0)
body.cx(0, 1)
body.measure(0, 0)
body.break_loop().c_if(0, True)
for_loop_op = ForLoopOp(range(5), None, body=body)
qc.append(for_loop_op, [0, 1], [0])
circuits.append(qc)
# For loop iterator
qc = QuantumCircuit(2, 1)
for_loop_op = ForLoopOp(iter(range(5)), None, body=body)
qc.append(for_loop_op, [0, 1], [0])
circuits.append(qc)
return circuits


def generate_circuits(version_str=None):
"""Generate reference circuits."""
version_parts = None
Expand Down Expand Up @@ -318,6 +370,8 @@ def generate_circuits(version_str=None):
output_circuits["parameter_vector_expression.qpy"] = [
generate_parameter_vector_expression()
]
if version_parts >= (0, 19, 2):
output_circuits["control_flow.qpy"] = generate_control_flow_circuits()

return output_circuits

Expand Down

0 comments on commit e8fc90f

Please sign in to comment.