From c28ae308b35f95855f33a65db648d3a7ee92c8b2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 11 May 2022 18:54:02 -0400 Subject: [PATCH 1/9] 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 --- qiskit/qpy/binary_io/circuits.py | 218 ++++++++++++++---- qiskit/qpy/common.py | 7 +- qiskit/qpy/formats.py | 39 ++++ .../circuit/test_circuit_load_from_qpy.py | 39 +++- test/qpy_compat/test_qpy.py | 27 ++- 5 files changed, 275 insertions(+), 55 deletions(-) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index e09515263b11..4dd6fe8b48be 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -27,6 +27,7 @@ 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,13 @@ 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 == "MCPhaseGate": + 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 else: if gate_name in {"Initialize", "UCRXGate", "UCRYGate", "UCRZGate"}: gate = gate_class(params) @@ -251,14 +280,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 +315,22 @@ def _parse_custom_instruction(custom_instructions, gate_name, params): inst_obj.definition = definition return inst_obj + if 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 +386,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 +414,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 +457,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 +468,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 +500,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 +513,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 +534,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 +577,17 @@ def _write_elem(buffer, op): 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 +597,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 +719,22 @@ 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: + for name in new_custom_instructions: + instruction = custom_instructions[name] + new_custom_instructions = _write_custom_instruction( + custom_instructions_buffer, name, instruction, custom_instructions + ) + + file_obj.write( + struct.pack(formats.CUSTOM_CIRCUIT_DEF_HEADER_PACK, len(custom_instructions)) + ) + file_obj.write(custom_instructions_buffer.getvalue()) - instruction_buffer.seek(0) - file_obj.write(instruction_buffer.read()) + file_obj.write(instruction_buffer.getvalue()) instruction_buffer.close() diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 3658f3c84c41..c988034244d3 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -26,10 +26,10 @@ 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 @@ def assign(cls, obj): """ 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): diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index ba8f900a1c9e..6e8f869ecc70 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -85,6 +85,26 @@ 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_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", diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 241779447b12..4d0a89b16bca 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -24,7 +24,13 @@ 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, +) from qiskit.circuit.instruction import Instruction from qiskit.circuit.parameter import Parameter from qiskit.circuit.parametervector import ParameterVector @@ -954,3 +960,34 @@ def test_ucr_gates(self): 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 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()) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index eb69653e2c98..c66ca499c29a 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -27,7 +27,8 @@ 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,28 @@ 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) + return circuits + + def generate_circuits(version_str=None): """Generate reference circuits.""" version_parts = None @@ -372,6 +395,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, 20, 2): + output_circuits["controlled_gates.qpy"] = generate_controlled_gates() return output_circuits From 855235152eb638d262a4e28226e7f4437e5632b6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 12 May 2022 09:47:12 -0400 Subject: [PATCH 2/9] 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. --- qiskit/qpy/binary_io/circuits.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 4dd6fe8b48be..32b07aef8e13 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -268,6 +268,7 @@ def _read_instruction(file_obj, circuit, registers, custom_instructions, version 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) From c19e57f0e968aa452a0ad7f876732fcab67d6224 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 12 May 2022 10:40:12 -0400 Subject: [PATCH 3/9] Add QPY version 5 payload format description --- qiskit/qpy/__init__.py | 80 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 2ede88a790eb..3e88f480e87f 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -98,6 +98,82 @@ 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 @@ -453,6 +529,8 @@ struct { uint16_t name_size; char type; + uint32_t num_qubits; + uint32_t num_clbits; _Bool custom_definition; uint64_t size; } @@ -468,6 +546,8 @@ 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 ------------ From ea7b085f62b21dfad0644eaa998e3a363db2a2b5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 12 May 2022 11:37:10 -0400 Subject: [PATCH 4/9] Add release note --- ...qpy-controlled-gates-e653cbeee067f90b.yaml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 releasenotes/notes/fix-qpy-controlled-gates-e653cbeee067f90b.yaml diff --git a/releasenotes/notes/fix-qpy-controlled-gates-e653cbeee067f90b.yaml b/releasenotes/notes/fix-qpy-controlled-gates-e653cbeee067f90b.yaml new file mode 100644 index 000000000000..2ccf42474a20 --- /dev/null +++ b/releasenotes/notes/fix-qpy-controlled-gates-e653cbeee067f90b.yaml @@ -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 `__ + - | + 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. From 7a90f4fba914c35cf2bdf75534ffdb1739f271ef Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 12 May 2022 11:39:18 -0400 Subject: [PATCH 5/9] Expand test coverage --- test/python/circuit/test_circuit_load_from_qpy.py | 13 ++++++++++++- test/qpy_compat/test_qpy.py | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 4d0a89b16bca..314445bc6d67 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -973,7 +973,7 @@ def test_controlled_gate(self): self.assertEqual(qc, new_circuit) def test_nested_controlled_gate(self): - """Test a custom controlled gate.""" + """Test a custom nested controlled gate.""" custom_gate = Gate("black_box", 1, []) custom_definition = QuantumCircuit(1) custom_definition.h(0) @@ -991,3 +991,14 @@ def test_nested_controlled_gate(self): 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) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index c66ca499c29a..e3a9c37c8cc0 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -360,6 +360,9 @@ def generate_controlled_gates(): 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 From dd25b3853fbdb6864bd86c6d1f71e38e7e451e12 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 17 Jun 2022 12:07:04 -0400 Subject: [PATCH 6/9] Only check controlled gate type key on version 5 or newer --- qiskit/qpy/binary_io/circuits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 32b07aef8e13..761ddf8b8352 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -316,7 +316,7 @@ def _parse_custom_instruction(custom_instructions, gate_name, params, version, v inst_obj.definition = definition return inst_obj - if type_key == common.CircuitInstructionTypeKey.CONTROLLED_GATE: + 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 From 354cf8562c5ba82046a717c51e5ddfdb132701f5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 17 Jun 2022 12:24:08 -0400 Subject: [PATCH 7/9] Fix mcu1 deserialization --- qiskit/qpy/binary_io/circuits.py | 2 +- .../circuit/test_circuit_load_from_qpy.py | 23 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 761ddf8b8352..0e949bf8a8ab 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -262,7 +262,7 @@ 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 == "MCPhaseGate": + if gate_name in {"MCPhaseGate", "MCU1Gate"}: gate = gate_class(*params, instruction.num_ctrl_qubits) else: gate = gate_class(*params) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 314445bc6d67..de5fdcbf2132 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -24,13 +24,7 @@ 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, - DCXGate, -) +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 @@ -1002,3 +996,18 @@ def test_open_controlled_gate(self): 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) From 6c7279c5ac2ac0a969503c41254735c362c2d973 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 17 Jun 2022 12:37:26 -0400 Subject: [PATCH 8/9] Add copy to avoid mutating list while iterating over it --- qiskit/qpy/binary_io/circuits.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 0e949bf8a8ab..408942a7ee2a 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -724,7 +724,8 @@ def write_circuit(file_obj, circuit, metadata_serializer=None): with io.BytesIO() as custom_instructions_buffer: new_custom_instructions = list(custom_instructions.keys()) while new_custom_instructions: - for name in 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 From ab396291ec01b2b3b23ce24be8dd45a9cffcca57 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 17 Jun 2022 15:30:11 -0400 Subject: [PATCH 9/9] 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. --- test/qpy_compat/test_qpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index e3a9c37c8cc0..3956da41737f 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -398,7 +398,7 @@ 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, 20, 2): + if version_parts >= (0, 21, 0): output_circuits["controlled_gates.qpy"] = generate_controlled_gates() return output_circuits