Fix handling of ControlledGates in QPY (#8055)

* Fix handling of ControlledGates in QPY

This commit fixes the handling of ControlledGates in QPY. Previously the
extra parameters needed to reconstruct a custom controlled gate were not
encoded into the QPY payload. Fixing this required a version bump to the
QPY format to modify the payload for a custom instruction entry. Once we
added to the format the extra data required for a controlled gate, the
number of control qubits, the control state, and the base gate object,
the deserializer has enough information to recreate the custom
ControlledGate objects. However, fixing this exposed another bug with
standard library multicontrolled gates where they often didn't contain
sufficient data in the payload to reconstruct either.

Fixes #7999

* Fix test failure caused by missing condition

This commit fixes the qpy test failure. This was caused by the omission
of the classical condition on a controlled gate when reconstructing the
circuit in deserialization. Fixing this oversight fixes the test
failures.

* Add QPY version 5 payload format description

* Add release note

* Expand test coverage

* Only check controlled gate type key on version 5 or newer

* Fix mcu1 deserialization

* Add copy to avoid mutating list while iterating over it

* Fix compat test minimum version

This commit fixes the failing compat test which was incorrectly trying
to test ControlledGates with 0.20.2 generation. We should only run the
controlled gate tests starting with 0.21.0.

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Matthew Treinish 2022-06-17 19:50:55 -04:00 committed by GitHub
parent 755310e3c3
commit a34f0ce1ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 408 additions and 55 deletions

View File

@ -100,6 +100,82 @@ There is a circuit payload for each circuit (where the total number is dictated
by ``num_circuits`` in the file header). There is no padding between the
circuits in the data.
.. _qpy_version_5:
Version 5
=========
Version 5 changes from :ref:`qpy_version_4` by changing two payloads the INSTRUCTION metadata
payload and the CUSTOM_INSTRUCTION block. These now have new fields to better account for
:class:`~.ControlledGate` objects in a circuit.
INSTRUCTION
-----------
The INSTRUCTION block was modified to add two new fields ``num_ctrl_qubits`` and ``ctrl_state``
which are used to model the :attr:`.ControlledGate.num_ctrl_qubits` and
:attr:`.ControlledGate.ctrl_state` attributes. The new payload packed struct
format is:
.. code-block:: c
struct {
uint16_t name_size;
uint16_t label_size;
uint16_t num_parameters;
uint32_t num_qargs;
uint32_t num_cargs;
_Bool has_conditional;
uint16_t conditional_reg_name_size;
int64_t conditional_value;
uint32_t num_ctrl_qubits;
uint32_t ctrl_state;
}
The rest of the instruction payload is the same. You can refer to
:ref:`qpy_instructions` for the details of the full payload.
CUSTOM_INSTRUCTION
------------------
The CUSTOM_INSTRUCTION block in QPY version 5 adds a new field
``base_gate_size`` which is used to define the size of the
:class:`qiskit.circuit.Instruction` object stored in the
:attr:`.ControlledGate.base_gate` attribute for a custom
:class:`~.ControlledGate` object. With this change the CUSTOM_INSTRUCTION
metadata block becomes:
.. code-block:: c
struct {
uint16_t name_size;
char type;
uint32_t num_qubits;
uint32_t num_clbits;
_Bool custom_definition;
uint64_t size;
uint32_t num_ctrl_qubits;
uint32_t ctrl_state;
uint64_t base_gate_size
}
Immediately following the CUSTOM_INSTRUCTION struct is the utf8 encoded name
of size ``name_size``.
If ``custom_definition`` is ``True`` that means that the immediately following
``size`` bytes contains a QPY circuit data which can be used for the custom
definition of that gate. If ``custom_definition`` is ``False`` then the
instruction can be considered opaque (ie no definition). The ``type`` field
determines what type of object will get created with the custom definition.
If it's ``'g'`` it will be a :class:`~qiskit.circuit.Gate` object, ``'i'``
it will be a :class:`~qiskit.circuit.Instruction` object.
Following this the next ``base_gate_size`` bytes contain the ``INSTRUCTION``
payload for the :attr:`.ControlledGate.base_gate`.
Additionally an addition value for ``type`` is added ``'c'`` which is used to
indicate the custom instruction is a custom :class:`~.ControlledGate`.
.. _qpy_version_4:
Version 4
@ -455,6 +531,8 @@ Each custom instruction is defined with a CUSTOM_INSTRUCTION block defined as:
struct {
uint16_t name_size;
char type;
uint32_t num_qubits;
uint32_t num_clbits;
_Bool custom_definition;
uint64_t size;
}
@ -470,6 +548,8 @@ determines what type of object will get created with the custom definition.
If it's ``'g'`` it will be a :class:`~qiskit.circuit.Gate` object, ``'i'``
it will be a :class:`~qiskit.circuit.Instruction` object.
.. _qpy_instructions:
INSTRUCTIONS
------------

View File

@ -27,6 +27,7 @@ from qiskit import extensions
from qiskit.circuit import library, controlflow
from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
from qiskit.circuit.gate import Gate
from qiskit.circuit.controlledgate import ControlledGate
from qiskit.circuit.instruction import Instruction
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.circuit.quantumregister import QuantumRegister, Qubit
@ -150,12 +151,20 @@ def _read_instruction_parameter(file_obj, version, vectors):
def _read_instruction(file_obj, circuit, registers, custom_instructions, version, vectors):
instruction = formats.CIRCUIT_INSTRUCTION._make(
struct.unpack(
formats.CIRCUIT_INSTRUCTION_PACK,
file_obj.read(formats.CIRCUIT_INSTRUCTION_SIZE),
if version < 5:
instruction = formats.CIRCUIT_INSTRUCTION._make(
struct.unpack(
formats.CIRCUIT_INSTRUCTION_PACK,
file_obj.read(formats.CIRCUIT_INSTRUCTION_SIZE),
)
)
else:
instruction = formats.CIRCUIT_INSTRUCTION_V2._make(
struct.unpack(
formats.CIRCUIT_INSTRUCTION_V2_PACK,
file_obj.read(formats.CIRCUIT_INSTRUCTION_V2_SIZE),
)
)
)
gate_name = file_obj.read(instruction.name_size).decode(common.ENCODE)
label = file_obj.read(instruction.label_size).decode(common.ENCODE)
condition_register = file_obj.read(instruction.condition_register_size).decode(common.ENCODE)
@ -179,30 +188,35 @@ def _read_instruction(file_obj, circuit, registers, custom_instructions, version
)
else:
condition_tuple = (registers["c"][condition_register], instruction.condition_value)
qubit_indices = dict(enumerate(circuit.qubits))
clbit_indices = dict(enumerate(circuit.clbits))
if circuit is not None:
qubit_indices = dict(enumerate(circuit.qubits))
clbit_indices = dict(enumerate(circuit.clbits))
else:
qubit_indices = {}
clbit_indices = {}
# Load Arguments
for _qarg in range(instruction.num_qargs):
qarg = formats.CIRCUIT_INSTRUCTION_ARG._make(
struct.unpack(
formats.CIRCUIT_INSTRUCTION_ARG_PACK,
file_obj.read(formats.CIRCUIT_INSTRUCTION_ARG_SIZE),
if circuit is not None:
for _qarg in range(instruction.num_qargs):
qarg = formats.CIRCUIT_INSTRUCTION_ARG._make(
struct.unpack(
formats.CIRCUIT_INSTRUCTION_ARG_PACK,
file_obj.read(formats.CIRCUIT_INSTRUCTION_ARG_SIZE),
)
)
)
if qarg.type.decode(common.ENCODE) == "c":
raise TypeError("Invalid input carg prior to all qargs")
qargs.append(qubit_indices[qarg.size])
for _carg in range(instruction.num_cargs):
carg = formats.CIRCUIT_INSTRUCTION_ARG._make(
struct.unpack(
formats.CIRCUIT_INSTRUCTION_ARG_PACK,
file_obj.read(formats.CIRCUIT_INSTRUCTION_ARG_SIZE),
if qarg.type.decode(common.ENCODE) == "c":
raise TypeError("Invalid input carg prior to all qargs")
qargs.append(qubit_indices[qarg.size])
for _carg in range(instruction.num_cargs):
carg = formats.CIRCUIT_INSTRUCTION_ARG._make(
struct.unpack(
formats.CIRCUIT_INSTRUCTION_ARG_PACK,
file_obj.read(formats.CIRCUIT_INSTRUCTION_ARG_SIZE),
)
)
)
if carg.type.decode(common.ENCODE) == "q":
raise TypeError("Invalid input qarg after all qargs")
cargs.append(clbit_indices[carg.size])
if carg.type.decode(common.ENCODE) == "q":
raise TypeError("Invalid input qarg after all qargs")
cargs.append(clbit_indices[carg.size])
# Load Parameters
for _param in range(instruction.num_parameters):
@ -210,20 +224,28 @@ def _read_instruction(file_obj, circuit, registers, custom_instructions, version
params.append(param)
# Load Gate object
if gate_name in ("Gate", "Instruction"):
inst_obj = _parse_custom_instruction(custom_instructions, gate_name, params)
if gate_name in {"Gate", "Instruction", "ControlledGate"}:
inst_obj = _parse_custom_instruction(
custom_instructions, gate_name, params, version, vectors, registers
)
inst_obj.condition = condition_tuple
if instruction.label_size > 0:
inst_obj.label = label
if circuit is None:
return inst_obj
circuit._append(inst_obj, qargs, cargs)
return
return None
elif gate_name in custom_instructions:
inst_obj = _parse_custom_instruction(custom_instructions, gate_name, params)
inst_obj = _parse_custom_instruction(
custom_instructions, gate_name, params, version, vectors, registers
)
inst_obj.condition = condition_tuple
if instruction.label_size > 0:
inst_obj.label = label
if circuit is None:
return inst_obj
circuit._append(inst_obj, qargs, cargs)
return
return None
elif hasattr(library, gate_name):
gate_class = getattr(library, gate_name)
elif hasattr(circuit_mod, gate_name):
@ -239,6 +261,14 @@ def _read_instruction(file_obj, circuit, registers, custom_instructions, version
if gate_name in {"IfElseOp", "WhileLoopOp"}:
gate = gate_class(condition_tuple, *params)
elif version >= 5 and issubclass(gate_class, ControlledGate):
if gate_name in {"MCPhaseGate", "MCU1Gate"}:
gate = gate_class(*params, instruction.num_ctrl_qubits)
else:
gate = gate_class(*params)
gate.num_ctrl_qubits = instruction.num_ctrl_qubits
gate.ctrl_state = instruction.ctrl_state
gate.condition = condition_tuple
else:
if gate_name in {"Initialize", "UCRXGate", "UCRYGate", "UCRZGate"}:
gate = gate_class(params)
@ -251,14 +281,28 @@ def _read_instruction(file_obj, circuit, registers, custom_instructions, version
gate.condition = condition_tuple
if instruction.label_size > 0:
gate.label = label
if circuit is None:
return gate
if not isinstance(gate, Instruction):
circuit.append(gate, qargs, cargs)
else:
circuit._append(gate, qargs, cargs)
return None
def _parse_custom_instruction(custom_instructions, gate_name, params):
type_str, num_qubits, num_clbits, definition = custom_instructions[gate_name]
def _parse_custom_instruction(custom_instructions, gate_name, params, version, vectors, registers):
if version >= 5:
(
type_str,
num_qubits,
num_clbits,
definition,
num_ctrl_qubits,
ctrl_state,
base_gate_raw,
) = custom_instructions[gate_name]
else:
type_str, num_qubits, num_clbits, definition = custom_instructions[gate_name]
type_key = common.CircuitInstructionTypeKey(type_str)
if type_key == common.CircuitInstructionTypeKey.INSTRUCTION:
@ -272,6 +316,22 @@ def _parse_custom_instruction(custom_instructions, gate_name, params):
inst_obj.definition = definition
return inst_obj
if version >= 5 and type_key == common.CircuitInstructionTypeKey.CONTROLLED_GATE:
with io.BytesIO(base_gate_raw) as base_gate_obj:
base_gate = _read_instruction(
base_gate_obj, None, registers, custom_instructions, version, vectors
)
inst_obj = ControlledGate(
gate_name,
num_qubits,
params,
num_ctrl_qubits=num_ctrl_qubits,
ctrl_state=ctrl_state,
base_gate=base_gate,
)
inst_obj.definition = definition
return inst_obj
if type_key == common.CircuitInstructionTypeKey.PAULI_EVOL_GATE:
return definition
@ -327,12 +387,21 @@ def _read_custom_instructions(file_obj, version, vectors):
)
if custom_definition_header.size > 0:
for _ in range(custom_definition_header.size):
data = formats.CUSTOM_CIRCUIT_INST_DEF._make(
struct.unpack(
formats.CUSTOM_CIRCUIT_INST_DEF_PACK,
file_obj.read(formats.CUSTOM_CIRCUIT_INST_DEF_SIZE),
if version < 5:
data = formats.CUSTOM_CIRCUIT_INST_DEF._make(
struct.unpack(
formats.CUSTOM_CIRCUIT_INST_DEF_PACK,
file_obj.read(formats.CUSTOM_CIRCUIT_INST_DEF_SIZE),
)
)
)
else:
data = formats.CUSTOM_CIRCUIT_INST_DEF_V2._make(
struct.unpack(
formats.CUSTOM_CIRCUIT_INST_DEF_V2_PACK,
file_obj.read(formats.CUSTOM_CIRCUIT_INST_DEF_V2_SIZE),
)
)
name = file_obj.read(data.gate_name_size).decode(common.ENCODE)
type_str = data.type
definition_circuit = None
@ -346,12 +415,20 @@ def _read_custom_instructions(file_obj, version, vectors):
definition_circuit = common.data_from_binary(
def_binary, _read_pauli_evolution_gate, version=version, vectors=vectors
)
custom_instructions[name] = (
type_str,
data.num_qubits,
data.num_clbits,
definition_circuit,
)
if version < 5:
data_payload = (type_str, data.num_qubits, data.num_clbits, definition_circuit)
else:
base_gate = file_obj.read(data.base_gate_size)
data_payload = (
type_str,
data.num_qubits,
data.num_clbits,
definition_circuit,
data.num_ctrl_qubits,
data.ctrl_state,
base_gate,
)
custom_instructions[name] = data_payload
return custom_instructions
@ -381,6 +458,7 @@ def _write_instruction_parameter(file_obj, param):
# pylint: disable=too-many-boolean-expressions
def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_map):
gate_class_name = instruction_tuple[0].__class__.__name__
custom_instructions_list = []
if (
(
not hasattr(library, gate_class_name)
@ -391,15 +469,18 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m
)
or gate_class_name == "Gate"
or gate_class_name == "Instruction"
or gate_class_name == "ControlledGate"
or isinstance(instruction_tuple[0], library.BlueprintCircuit)
):
if instruction_tuple[0].name not in custom_instructions:
custom_instructions[instruction_tuple[0].name] = instruction_tuple[0]
custom_instructions_list.append(instruction_tuple[0].name)
gate_class_name = instruction_tuple[0].name
elif isinstance(instruction_tuple[0], library.PauliEvolutionGate):
gate_class_name = r"###PauliEvolutionGate_" + str(uuid.uuid4())
custom_instructions[gate_class_name] = instruction_tuple[0]
custom_instructions_list.append(gate_class_name)
has_condition = False
condition_register = b""
@ -420,8 +501,11 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m
label_raw = label.encode(common.ENCODE)
else:
label_raw = b""
num_ctrl_qubits = getattr(instruction_tuple[0], "num_ctrl_qubits", 0)
ctrl_state = getattr(instruction_tuple[0], "ctrl_state", 0)
instruction_raw = struct.pack(
formats.CIRCUIT_INSTRUCTION_PACK,
formats.CIRCUIT_INSTRUCTION_V2_PACK,
len(gate_class_name),
len(label_raw),
len(instruction_tuple[0].params),
@ -430,6 +514,8 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m
has_condition,
len(condition_register),
condition_value,
num_ctrl_qubits,
ctrl_state,
)
file_obj.write(instruction_raw)
file_obj.write(gate_class_name)
@ -449,6 +535,7 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m
# Encode instruction params
for param in instruction_tuple[0].params:
_write_instruction_parameter(file_obj, param)
return custom_instructions_list
def _write_pauli_evolution_gate(file_obj, evolution_gate):
@ -491,13 +578,17 @@ def _write_pauli_evolution_gate(file_obj, evolution_gate):
file_obj.write(synth_data)
def _write_custom_instruction(file_obj, name, instruction):
def _write_custom_instruction(file_obj, name, instruction, custom_instructions):
type_key = common.CircuitInstructionTypeKey.assign(instruction)
has_definition = False
size = 0
data = None
num_qubits = instruction.num_qubits
num_clbits = instruction.num_clbits
ctrl_state = 0
num_ctrl_qubits = 0
base_gate = None
new_custom_instruction = []
if type_key == common.CircuitInstructionTypeKey.PAULI_EVOL_GATE:
has_definition = True
@ -507,20 +598,37 @@ def _write_custom_instruction(file_obj, name, instruction):
has_definition = True
data = common.data_to_binary(instruction.definition, write_circuit)
size = len(data)
if type_key == common.CircuitInstructionTypeKey.CONTROLLED_GATE:
num_ctrl_qubits = instruction.num_ctrl_qubits
ctrl_state = instruction.ctrl_state
base_gate = instruction.base_gate
if base_gate is None:
base_gate_raw = b""
else:
with io.BytesIO() as base_gate_buffer:
new_custom_instruction = _write_instruction(
base_gate_buffer, (base_gate, [], []), custom_instructions, {}
)
base_gate_raw = base_gate_buffer.getvalue()
name_raw = name.encode(common.ENCODE)
custom_instruction_raw = struct.pack(
formats.CUSTOM_CIRCUIT_INST_DEF_PACK,
formats.CUSTOM_CIRCUIT_INST_DEF_V2_PACK,
len(name_raw),
type_key,
num_qubits,
num_clbits,
has_definition,
size,
num_ctrl_qubits,
ctrl_state,
len(base_gate_raw),
)
file_obj.write(custom_instruction_raw)
file_obj.write(name_raw)
if data:
file_obj.write(data)
file_obj.write(base_gate_raw)
return new_custom_instruction
def _write_registers(file_obj, in_circ_regs, full_bits):
@ -612,13 +720,23 @@ def write_circuit(file_obj, circuit, metadata_serializer=None):
index_map["c"] = {bit: index for index, bit in enumerate(circuit.clbits)}
for instruction in circuit.data:
_write_instruction(instruction_buffer, instruction, custom_instructions, index_map)
file_obj.write(struct.pack(formats.CUSTOM_CIRCUIT_DEF_HEADER_PACK, len(custom_instructions)))
for name, instruction in custom_instructions.items():
_write_custom_instruction(file_obj, name, instruction)
with io.BytesIO() as custom_instructions_buffer:
new_custom_instructions = list(custom_instructions.keys())
while new_custom_instructions:
instructions_to_serialize = new_custom_instructions.copy()
for name in instructions_to_serialize:
instruction = custom_instructions[name]
new_custom_instructions = _write_custom_instruction(
custom_instructions_buffer, name, instruction, custom_instructions
)
instruction_buffer.seek(0)
file_obj.write(instruction_buffer.read())
file_obj.write(
struct.pack(formats.CUSTOM_CIRCUIT_DEF_HEADER_PACK, len(custom_instructions))
)
file_obj.write(custom_instructions_buffer.getvalue())
file_obj.write(instruction_buffer.getvalue())
instruction_buffer.close()

View File

@ -26,10 +26,10 @@ from qiskit.circuit.parameter import Parameter
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.circuit.parametervector import ParameterVectorElement
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.circuit import Gate, Instruction as CircuitInstruction, QuantumCircuit
from qiskit.circuit import Gate, Instruction as CircuitInstruction, QuantumCircuit, ControlledGate
from qiskit.qpy import formats, exceptions
QPY_VERSION = 4
QPY_VERSION = 5
ENCODE = "utf8"
@ -39,6 +39,7 @@ class CircuitInstructionTypeKey(bytes, Enum):
INSTRUCTION = b"i"
GATE = b"g"
PAULI_EVOL_GATE = b"p"
CONTROLLED_GATE = b"c"
@classmethod
def assign(cls, obj):
@ -55,6 +56,8 @@ class CircuitInstructionTypeKey(bytes, Enum):
"""
if isinstance(obj, PauliEvolutionGate):
return cls.PAULI_EVOL_GATE
if isinstance(obj, ControlledGate):
return cls.CONTROLLED_GATE
if isinstance(obj, Gate):
return cls.GATE
if isinstance(obj, CircuitInstruction):

View File

@ -85,6 +85,26 @@ CIRCUIT_INSTRUCTION = namedtuple(
CIRCUIT_INSTRUCTION_PACK = "!HHHII?Hq"
CIRCUIT_INSTRUCTION_SIZE = struct.calcsize(CIRCUIT_INSTRUCTION_PACK)
# CIRCUIT_INSTRUCTION_V2
CIRCUIT_INSTRUCTION_V2 = namedtuple(
"CIRCUIT_INSTRUCTION",
[
"name_size",
"label_size",
"num_parameters",
"num_qargs",
"num_cargs",
"has_condition",
"condition_register_size",
"condition_value",
"num_ctrl_qubits",
"ctrl_state",
],
)
CIRCUIT_INSTRUCTION_V2_PACK = "!HHHII?HqII"
CIRCUIT_INSTRUCTION_V2_SIZE = struct.calcsize(CIRCUIT_INSTRUCTION_V2_PACK)
# CIRCUIT_INSTRUCTION_ARG
CIRCUIT_INSTRUCTION_ARG = namedtuple("CIRCUIT_INSTRUCTION_ARG", ["type", "size"])
CIRCUIT_INSTRUCTION_ARG_PACK = "!1cI"
@ -108,6 +128,25 @@ CUSTOM_CIRCUIT_DEF_HEADER = namedtuple("CUSTOM_CIRCUIT_DEF_HEADER", ["size"])
CUSTOM_CIRCUIT_DEF_HEADER_PACK = "!Q"
CUSTOM_CIRCUIT_DEF_HEADER_SIZE = struct.calcsize(CUSTOM_CIRCUIT_DEF_HEADER_PACK)
# CUSTOM_CIRCUIT_INST_DEF_V2
CUSTOM_CIRCUIT_INST_DEF_V2 = namedtuple(
"CUSTOM_CIRCUIT_INST_DEF",
[
"gate_name_size",
"type",
"num_qubits",
"num_clbits",
"custom_definition",
"size",
"num_ctrl_qubits",
"ctrl_state",
"base_gate_size",
],
)
CUSTOM_CIRCUIT_INST_DEF_V2_PACK = "!H1cII?QIIQ"
CUSTOM_CIRCUIT_INST_DEF_V2_SIZE = struct.calcsize(CUSTOM_CIRCUIT_INST_DEF_V2_PACK)
# CUSTOM_CIRCUIT_INST_DEF
CUSTOM_CIRCUIT_INST_DEF = namedtuple(
"CUSTOM_CIRCUIT_INST_DEF",

View File

@ -0,0 +1,28 @@
---
upgrade:
- |
The QPY version format version emitted by :func:`.qpy.dump` has been
increased to version 5. This new format version is incompatible with the
previous versions and will result in an error when trying to load it with
a deserializer that isn't able to handle QPY version 5. This change was
necessary to fix support for representing controlled gates properly and
representing non-default control states.
fixes:
- |
Fixed support for QPY serialization (:func:`.qpy.dump`) and deserialization
(:func:`.qpy.load`) of a :class:`~.QuantumCircuit` object containing custom
:class:`~.ControlledGate` objects. Previously, an exception would be raised
by :func:`.qpy.load` when trying to reconstruct the custom
:class:`~.ControlledGate`.
Fixed `#7999 <https://github.com/Qiskit/qiskit-terra/issues/7999>`__
- |
Fixed support for QPY serialization (:func:`.qpy.dump`) and deserialization
(:func:`.qpy.load`) of a :class:`~.QuantumCircuit` object containing custom
:class:`~.MCPhaseGate` objects. Previously, an exception would be raised
by :func:`.qpy.load` when trying to reconstruct the :class:`~.MCPhaseGate`.
- |
Fixed support for QPY serialization (:func:`.qpy.dump`) and deserialization
(:func:`.qpy.load`) of a :class:`~.QuantumCircuit` object containing
controlled gates with an open control state. Previously, the open control
state would be lost by the serialization process and the reconstructed
circuit.

View File

@ -24,7 +24,7 @@ from qiskit.circuit.classicalregister import Clbit
from qiskit.circuit.quantumregister import Qubit
from qiskit.circuit.random import random_circuit
from qiskit.circuit.gate import Gate
from qiskit.circuit.library import XGate, QFT, QAOAAnsatz, PauliEvolutionGate
from qiskit.circuit.library import XGate, QFT, QAOAAnsatz, PauliEvolutionGate, DCXGate, MCU1Gate
from qiskit.circuit.instruction import Instruction
from qiskit.circuit.parameter import Parameter
from qiskit.circuit.parametervector import ParameterVector
@ -954,3 +954,60 @@ class TestLoadFromQPY(QiskitTestCase):
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc.decompose().decompose(), new_circuit.decompose().decompose())
def test_controlled_gate(self):
"""Test a custom controlled gate."""
qc = QuantumCircuit(3)
controlled_gate = DCXGate().control(1)
qc.append(controlled_gate, [0, 1, 2])
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_controlled_gate(self):
"""Test a custom nested controlled gate."""
custom_gate = Gate("black_box", 1, [])
custom_definition = QuantumCircuit(1)
custom_definition.h(0)
custom_definition.rz(1.5, 0)
custom_definition.sdg(0)
custom_gate.definition = custom_definition
qc = QuantumCircuit(3)
qc.append(custom_gate, [0])
controlled_gate = custom_gate.control(2)
qc.append(controlled_gate, [0, 1, 2])
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circ = load(qpy_file)[0]
self.assertEqual(qc, new_circ)
self.assertEqual(qc.decompose(), new_circ.decompose())
def test_open_controlled_gate(self):
"""Test an open control is preserved across serialization."""
qc = QuantumCircuit(2)
qc.cx(0, 1, ctrl_state=0)
with io.BytesIO() as fd:
dump(qc, fd)
fd.seek(0)
new_circ = load(fd)[0]
self.assertEqual(qc, new_circ)
self.assertEqual(qc.data[0][0].ctrl_state, new_circ.data[0][0].ctrl_state)
def test_standard_control_gates(self):
"""Test standard library controlled gates."""
qc = QuantumCircuit(3)
mcu1_gate = MCU1Gate(np.pi, 2)
qc.append(mcu1_gate, [0, 2, 1])
qc.mcp(np.pi, [0, 2], 1)
qc.mct([0, 2], 1)
qc.mcx([0, 2], 1)
qc.measure_all()
qpy_file = io.BytesIO()
dump(qc, qpy_file)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]
self.assertEqual(qc, new_circuit)

View File

@ -27,7 +27,8 @@ from qiskit.circuit.parametervector import ParameterVector
from qiskit.circuit.qpy_serialization import dump, load
from qiskit.opflow import X, Y, Z, I
from qiskit.quantum_info.random import random_unitary
from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, QFT
from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, QFT, DCXGate
from qiskit.circuit.gate import Gate
def generate_full_circuit():
@ -340,6 +341,31 @@ def generate_control_flow_circuits():
return circuits
def generate_controlled_gates():
"""Test QPY serialization with custom ControlledGates."""
circuits = []
qc = QuantumCircuit(3)
controlled_gate = DCXGate().control(1)
qc.append(controlled_gate, [0, 1, 2])
circuits.append(qc)
custom_gate = Gate("black_box", 1, [])
custom_definition = QuantumCircuit(1)
custom_definition.h(0)
custom_definition.rz(1.5, 0)
custom_definition.sdg(0)
custom_gate.definition = custom_definition
nested_qc = QuantumCircuit(3)
qc.append(custom_gate, [0])
controlled_gate = custom_gate.control(2)
nested_qc.append(controlled_gate, [0, 1, 2])
nested_qc.measure_all()
circuits.append(nested_qc)
qc_open = QuantumCircuit(2)
qc_open.cx(0, 1, ctrl_state=0)
circuits.append(qc_open)
return circuits
def generate_circuits(version_str=None):
"""Generate reference circuits."""
version_parts = None
@ -372,6 +398,8 @@ def generate_circuits(version_str=None):
]
if version_parts >= (0, 19, 2):
output_circuits["control_flow.qpy"] = generate_control_flow_circuits()
if version_parts >= (0, 21, 0):
output_circuits["controlled_gates.qpy"] = generate_controlled_gates()
return output_circuits