Skip to content

Commit

Permalink
Fix QPY serialisation of ControlledGate with open controls (#8571) (#…
Browse files Browse the repository at this point in the history
…8578)

* 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`.

* Add QPY backwards compatibility test

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit a0964c1)

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
  • Loading branch information
mergify[bot] and jakelishman authored Aug 18, 2022
1 parent d856313 commit 5e26264
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 3 deletions.
16 changes: 13 additions & 3 deletions qiskit/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/Qiskit/qiskit-terra/issues/8549>`__.
11 changes: 11 additions & 0 deletions test/python/circuit/test_circuit_load_from_qpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, [])
Expand Down
26 changes: 26 additions & 0 deletions test/qpy_compat/test_qpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit 5e26264

Please sign in to comment.