From e6bd59061c57b70d8c630f76efb19451c1f3c3dc Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 17 Aug 2022 18:39:11 +0100 Subject: [PATCH 1/2] Fix QPY serialisation of ControlledGate with open controls Previously, an incorrect definition and name would be re-instated on QPY deserialisation of a `ControlledGate` instance with open controls. The base name would include the dynamic `_o{ctrl_state}` suffix, causing the suffix to later be duplicated, and the definition would duplicate the logic that added the open controls. This fixes both by stripping the suffix on re-read before it is assigned, and serialising only the "11...1" state definition, since this is what is required and stored by `ControlledGate`. --- qiskit/qpy/binary_io/circuits.py | 16 +++++++++++++--- ...rolledgate-open-control-35c8ccb4c7466f4c.yaml | 6 ++++++ .../python/circuit/test_circuit_load_from_qpy.py | 11 +++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index ff30f00e21c4..49ca334e4a33 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -320,6 +320,9 @@ def _parse_custom_operation(custom_operations, gate_name, params, version, vecto base_gate = _read_instruction( base_gate_obj, None, registers, custom_operations, version, vectors ) + if ctrl_state < 2**num_ctrl_qubits - 1: + # If open controls, we need to discard the control suffix when setting the name. + gate_name = gate_name.rsplit("_", 1)[0] inst_obj = ControlledGate( gate_name, num_qubits, @@ -623,14 +626,21 @@ def _write_custom_operation(file_obj, name, operation, custom_operations): has_definition = True data = common.data_to_binary(operation, _write_pauli_evolution_gate) size = len(data) - elif operation.definition is not None: + elif type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: + # For ControlledGate, we have to access and store the private `_definition` rather than the + # public one, because the public one is mutated to include additional logic if the control + # state is open, and the definition setter (during a subsequent read) uses the "fully + # excited" control definition only. has_definition = True - data = common.data_to_binary(operation.definition, write_circuit) + data = common.data_to_binary(operation._definition, write_circuit) size = len(data) - if type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: num_ctrl_qubits = operation.num_ctrl_qubits ctrl_state = operation.ctrl_state base_gate = operation.base_gate + elif operation.definition is not None: + has_definition = True + data = common.data_to_binary(operation.definition, write_circuit) + size = len(data) if base_gate is None: base_gate_raw = b"" else: diff --git a/releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml b/releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml new file mode 100644 index 000000000000..d7a45238b2f4 --- /dev/null +++ b/releasenotes/notes/fix-qpy-controlledgate-open-control-35c8ccb4c7466f4c.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed QPY serialisation and deserialisation of :class:`.ControlledGate` + with open controls (*i.e.* those whose ``ctrl_state`` is not all ones). + Fixed `#8549 `__. diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 8292cee07780..9cc5f277580c 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -998,6 +998,17 @@ def test_controlled_gate(self): new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + def test_controlled_gate_open_controls(self): + """Test a controlled gate with open controls round-trips exactly.""" + qc = QuantumCircuit(3) + controlled_gate = DCXGate().control(1, ctrl_state=0) + 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, []) From 53d80f03f97ca66096ba582b892a9e56205a7cb9 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 18 Aug 2022 02:25:33 +0100 Subject: [PATCH 2/2] Add QPY backwards compatibility test --- test/qpy_compat/test_qpy.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 8c23af7fdb0f..418202f59d49 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -488,6 +488,30 @@ def generate_controlled_gates(): return circuits +def generate_open_controlled_gates(): + """Test QPY serialization with custom ControlledGates with open controls.""" + circuits = [] + qc = QuantumCircuit(3) + controlled_gate = DCXGate().control(1, ctrl_state=0) + 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) + nested_qc.append(custom_gate, [0]) + controlled_gate = custom_gate.control(2, ctrl_state=1) + 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 @@ -525,6 +549,8 @@ def generate_circuits(version_str=None): output_circuits["controlled_gates.qpy"] = generate_controlled_gates() output_circuits["schedule_blocks.qpy"] = generate_schedule_blocks() output_circuits["pulse_gates.qpy"] = generate_calibrated_circuits() + if version_parts >= (0, 21, 2): + output_circuits["open_controlled_gates.qpy"] = generate_open_controlled_gates() return output_circuits