From 3dbbb32e762850db265c7bb40787a36351aad917 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 16 Feb 2023 19:07:09 +0000 Subject: [PATCH] Retain all deprecated `Bit` properties in QPY roundtrip (#9525) (#9603) * Retain more deprecated Bit properties in QPY roundtrip A major bugfix for QPY's handling of loose circuit bits in c0ac5fbd21 (gh-9095) caused the deprecated bit properties `index` and `register` to be lost after the QPY roundtrip. As far as Terra's data model is concerned, the two circuits would still compare equal after this loss; the output registers would still be equal to the inputs, and bit equality is not considered directly since it general bit instances are not expected to be equal between circuits. Losing this information caused some downstream issues for the IBM runtime, which was still relying on `Bit.index` working in some cases, despite it issuing a deprecating warning since Terra 0.17 (April 2021). While this can be corrected downstream, QPY can still do a better job of roundtripping the deprecated information while it is still present. The QPY format does not currently store enough information to _completely_ roundtrip this information in cases that some but not all owned bits from a register are present in the circuit. (This partial data is a decent part of the cause of the bugs that gh-9095 fixed.) Since this is just in the support of deprecated functionality that Terra's data model does not even require for circuit equality (QPY's goal), it seems not worth it to produce a new QPY binary format version to store this, when the deprecated properties being removed would obsolete the format again immediately. * Fix lint * Correct deprecated bit information in QPY This allows complete round-tripping of all the deprecated register+index information in the `Bit` instances through QPY, restoring us to the _intended_ behaviour before gh-9095. The behaviour in the dumper before that did not allow full reconstruction, because some of the information was lost for bits that were encountered in more than one register. This fixes the dumper to always output all the indexing information for all bits, not to use `-1` whenever a bit _is_ in the circuit but has previously been encountered. The `standalone` field on a register is sufficient to tell whether the bits contained in it should have their "owner" information set; it's not possible (in valid Qiskit code) to have a register that owns only _some_ of its bits. To accomodate this, the register reader now needs to be two-pass. * Add deprecated-bit checks to backwards compatibility tests * Rewrite release note * Improve internal documentation --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> (cherry picked from commit b6aba599f946a97a9c8d6ee4e76029ecb17749c6) Co-authored-by: Jake Lishman --- qiskit/qpy/__init__.py | 6 +- qiskit/qpy/binary_io/circuits.py | 82 +++++++++++----- ...ed-bit-qpy-roundtrip-9a23a795aa677c71.yaml | 13 +++ .../circuit/test_circuit_load_from_qpy.py | 95 +++++++++++++++++++ test/qpy_compat/test_qpy.py | 41 +++++--- 5 files changed, 199 insertions(+), 38 deletions(-) create mode 100644 releasenotes/notes/fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 9d5c4cb5becb..0b0ea88184d6 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -796,10 +796,10 @@ the register ``qr`` would be a standalone register. While something like:: bits = [Qubit(), Qubit()] - qr = QuantumRegister(bits=bits) - qc = QuantumCircuit(bits=bits) + qr2 = QuantumRegister(bits=bits) + qc = QuantumCircuit(qr2) -``qr`` would have ``standalone`` set to ``False``. +``qr2`` would have ``standalone`` set to ``False``. .. _qpy_custom_definition: diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 527d7ec5442e..62ef8c885228 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -701,7 +701,6 @@ def _write_calibrations(file_obj, calibrations, metadata_serializer): def _write_registers(file_obj, in_circ_regs, full_bits): bitmap = {bit: index for index, bit in enumerate(full_bits)} - processed_indices = set() out_circ_regs = set() for bit in full_bits: @@ -727,12 +726,7 @@ def _write_registers(file_obj, in_circ_regs, full_bits): REGISTER_ARRAY_PACK = "!%sq" % reg.size bit_indices = [] for bit in reg: - bit_index = bitmap.get(bit, -1) - if bit_index in processed_indices: - bit_index = -1 - if bit_index >= 0: - processed_indices.add(bit_index) - bit_indices.append(bit_index) + bit_indices.append(bitmap.get(bit, -1)) file_obj.write(struct.pack(REGISTER_ARRAY_PACK, *bit_indices)) return len(in_circ_regs) + len(out_circ_regs) @@ -842,30 +836,70 @@ def read_circuit(file_obj, version, metadata_deserializer=None): num_clbits = header["num_clbits"] num_registers = header["num_registers"] num_instructions = header["num_instructions"] + # `out_registers` is two "name: registter" maps segregated by type for the rest of QPY, and + # `all_registers` is the complete ordered list used to construct the `QuantumCircuit`. out_registers = {"q": {}, "c": {}} - circ = QuantumCircuit( - [Qubit() for _ in [None] * num_qubits], - [Clbit() for _ in [None] * num_clbits], - name=name, - global_phase=global_phase, - metadata=metadata, - ) + all_registers = [] + out_bits = {"q": [None] * num_qubits, "c": [None] * num_clbits} if num_registers > 0: if version < 4: registers = _read_registers(file_obj, num_registers) else: registers = _read_registers_v4(file_obj, num_registers) - - for bit_type_label, reg_type in [("q", QuantumRegister), ("c", ClassicalRegister)]: - # Add quantum registers and bits - circuit_bits = {"q": circ.qubits, "c": circ.clbits}[bit_type_label] - for register_name, (_, indices, in_circuit) in registers[bit_type_label].items(): - register = reg_type( - name=register_name, bits=[circuit_bits[x] for x in indices if x >= 0] - ) - if in_circuit: - circ.add_register(register) + for bit_type_label, bit_type, reg_type in [ + ("q", Qubit, QuantumRegister), + ("c", Clbit, ClassicalRegister), + ]: + # This does two passes through the registers. In the first, we're actually just + # constructing the `Bit` instances: any register that is `standalone` "owns" all its + # bits in the old Qiskit data model, so we have to construct those by creating the + # register and taking the bits from them. That's the case even if that register isn't + # actually in the circuit, which is why we stored them (with `in_circuit=False`) in QPY. + # + # Since there's no guarantees in QPY about the ordering of registers, we have to pass + # through all registers to create the bits first, because we can't reliably know if a + # non-standalone register contains bits from a standalone one until we've seen all + # standalone registers. + typed_bits = out_bits[bit_type_label] + typed_registers = registers[bit_type_label] + for register_name, (standalone, indices, _incircuit) in typed_registers.items(): + if not standalone: + continue + register = reg_type(len(indices), register_name) out_registers[bit_type_label][register_name] = register + for owned, index in zip(register, indices): + # Negative indices are for bits that aren't in the circuit. + if index >= 0: + typed_bits[index] = owned + # Any remaining unset bits aren't owned, so we can construct them in the standard way. + typed_bits = [bit if bit is not None else bit_type() for bit in typed_bits] + # Finally _properly_ construct all the registers. Bits can be in more than one + # register, including bits that are old-style "owned" by a register. + for register_name, (standalone, indices, in_circuit) in typed_registers.items(): + if standalone: + register = out_registers[bit_type_label][register_name] + else: + register = reg_type( + name=register_name, + bits=[typed_bits[x] if x >= 0 else bit_type() for x in indices], + ) + out_registers[bit_type_label][register_name] = register + if in_circuit: + all_registers.append(register) + out_bits[bit_type_label] = typed_bits + else: + out_bits = { + "q": [Qubit() for _ in out_bits["q"]], + "c": [Clbit() for _ in out_bits["c"]], + } + circ = QuantumCircuit( + out_bits["q"], + out_bits["c"], + *all_registers, + name=name, + global_phase=global_phase, + metadata=metadata, + ) custom_operations = _read_custom_operations(file_obj, version, vectors) for _instruction in range(num_instructions): _read_instruction(file_obj, circ, out_registers, custom_operations, version, vectors) diff --git a/releasenotes/notes/fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml b/releasenotes/notes/fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml new file mode 100644 index 000000000000..a6f1a0db1f68 --- /dev/null +++ b/releasenotes/notes/fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + The deprecated :class:`.Qubit` and :class:`.Clbit` properties :attr:`~.Qubit.register` and + :attr:`~.Qubit.index` will now be correctly round-tripped by QPY (:mod:`qiskit.qpy`) in all + valid usages of :class:`.QuantumRegister` and :class:`.ClassicalRegister`. In earlier releases + in the Terra 0.23 series, this information would be lost. In versions before 0.23.0, this + information was partially reconstructed but could be incorrect or produce invalid circuits for + certain register configurations. + + The correct way to retrieve the index of a bit within a circuit, and any registers in that + circuit the bit is contained within is to call :meth:`.QuantumCircuit.find_bit`. This method + will return the correct information in all versions of Terra since its addition in version 0.19. diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 349bca64f3f1..ea9823d70873 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -51,6 +51,21 @@ class TestLoadFromQPY(QiskitTestCase): """Test circuit.from_qasm_* set of methods.""" + def assertDeprecatedBitProperties(self, original, roundtripped): + """Test that deprecated bit attributes are equal if they are set in the original circuit.""" + owned_qubits = [ + (a, b) for a, b in zip(original.qubits, roundtripped.qubits) if a._register is not None + ] + if owned_qubits: + original_qubits, roundtripped_qubits = zip(*owned_qubits) + self.assertEqual(original_qubits, roundtripped_qubits) + owned_clbits = [ + (a, b) for a, b in zip(original.clbits, roundtripped.clbits) if a._register is not None + ] + if owned_clbits: + original_clbits, roundtripped_clbits = zip(*owned_clbits) + self.assertEqual(original_clbits, roundtripped_clbits) + def test_qpy_full_path(self): """Test full path qpy serialization for basic circuit.""" qr_a = QuantumRegister(4, "a") @@ -80,6 +95,7 @@ def test_qpy_full_path(self): self.assertEqual(q_circuit.global_phase, new_circ.global_phase) self.assertEqual(q_circuit.metadata, new_circ.metadata) self.assertEqual(q_circuit.name, new_circ.name) + self.assertDeprecatedBitProperties(q_circuit, new_circ) def test_circuit_with_conditional(self): """Test that instructions with conditions are correctly serialized.""" @@ -90,6 +106,7 @@ def test_circuit_with_conditional(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_int_parameter(self): """Test that integer parameters are correctly serialized.""" @@ -100,6 +117,7 @@ def test_int_parameter(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_float_parameter(self): """Test that float parameters are correctly serialized.""" @@ -110,6 +128,7 @@ def test_float_parameter(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_numpy_float_parameter(self): """Test that numpy float parameters are correctly serialized.""" @@ -120,6 +139,7 @@ def test_numpy_float_parameter(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_numpy_int_parameter(self): """Test that numpy integer parameters are correctly serialized.""" @@ -130,6 +150,7 @@ def test_numpy_int_parameter(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_unitary_gate(self): """Test that numpy array parameters are correctly serialized""" @@ -141,6 +162,7 @@ def test_unitary_gate(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_opaque_gate(self): """Test that custom opaque gate is correctly serialized""" @@ -152,6 +174,7 @@ def test_opaque_gate(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_opaque_instruction(self): """Test that custom opaque instruction is correctly serialized""" @@ -163,6 +186,7 @@ def test_opaque_instruction(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_custom_gate(self): """Test that custom gate is correctly serialized""" @@ -181,6 +205,7 @@ def test_custom_gate(self): new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) self.assertEqual(qc.decompose(), new_circ.decompose()) + self.assertDeprecatedBitProperties(qc, new_circ) def test_custom_instruction(self): """Test that custom instruction is correctly serialized""" @@ -198,6 +223,7 @@ def test_custom_instruction(self): new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) self.assertEqual(qc.decompose(), new_circ.decompose()) + self.assertDeprecatedBitProperties(qc, new_circ) def test_parameter(self): """Test that a circuit with a parameter is correctly serialized.""" @@ -221,6 +247,7 @@ def test_parameter(self): new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) self.assertEqual(qc.bind_parameters({theta: 3.14}), new_circ.bind_parameters({theta: 3.14})) + self.assertDeprecatedBitProperties(qc, new_circ) def test_bound_parameter(self): """Test a circuit with a bound parameter is correctly serialized.""" @@ -244,6 +271,7 @@ def test_bound_parameter(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_parameter_expression(self): """Test a circuit with a parameter expression.""" @@ -270,6 +298,7 @@ def test_parameter_expression(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_string_parameter(self): """Test a PauliGate instruction that has string parameters.""" @@ -279,6 +308,7 @@ def test_string_parameter(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(circ, new_circuit) + self.assertDeprecatedBitProperties(circ, new_circuit) def test_multiple_circuits(self): """Test multiple circuits can be serialized together.""" @@ -292,6 +322,8 @@ def test_multiple_circuits(self): qpy_file.seek(0) new_circs = load(qpy_file) self.assertEqual(circuits, new_circs) + for old, new in zip(circuits, new_circs): + self.assertDeprecatedBitProperties(old, new) def test_shared_bit_register(self): """Test a circuit with shared bit registers.""" @@ -311,6 +343,7 @@ def test_shared_bit_register(self): qpy_file.seek(0) new_qc = load(qpy_file)[0] self.assertEqual(qc, new_qc) + self.assertDeprecatedBitProperties(qc, new_qc) def test_hybrid_standalone_register(self): """Test qpy serialization with registers that mix bit types""" @@ -330,6 +363,7 @@ def test_hybrid_standalone_register(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_mixed_registers(self): """Test circuit with mix of standalone and shared registers.""" @@ -355,6 +389,7 @@ def test_mixed_registers(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_standalone_and_shared_out_of_order(self): """Test circuit with register bits inserted out of order.""" @@ -385,6 +420,7 @@ def test_standalone_and_shared_out_of_order(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_unitary_gate_with_label(self): """Test that numpy array parameters are correctly serialized with a label""" @@ -400,6 +436,7 @@ def test_unitary_gate_with_label(self): self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_opaque_gate_with_label(self): """Test that custom opaque gate is correctly serialized with a label""" @@ -415,6 +452,7 @@ def test_opaque_gate_with_label(self): self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_opaque_instruction_with_label(self): """Test that custom opaque instruction is correctly serialized with a label""" @@ -430,6 +468,7 @@ def test_opaque_instruction_with_label(self): self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_custom_gate_with_label(self): """Test that custom gate is correctly serialized with a label""" @@ -452,6 +491,7 @@ def test_custom_gate_with_label(self): self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_custom_instruction_with_label(self): """Test that custom instruction is correctly serialized with a label""" @@ -473,6 +513,7 @@ def test_custom_instruction_with_label(self): self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_custom_gate_with_noop_definition(self): """Test that a custom gate whose definition contains no elements is serialized with a @@ -495,6 +536,7 @@ def test_custom_gate_with_noop_definition(self): self.assertEqual(len(new_circ), 2) self.assertIsInstance(new_circ.data[0].operation.definition, QuantumCircuit) self.assertIs(new_circ.data[1].operation.definition, None) + self.assertDeprecatedBitProperties(qc, new_circ) def test_custom_instruction_with_noop_definition(self): """Test that a custom instruction whose definition contains no elements is serialized with a @@ -517,6 +559,7 @@ def test_custom_instruction_with_noop_definition(self): self.assertEqual(len(new_circ), 2) self.assertIsInstance(new_circ.data[0].operation.definition, QuantumCircuit) self.assertIs(new_circ.data[1].operation.definition, None) + self.assertDeprecatedBitProperties(qc, new_circ) def test_standard_gate_with_label(self): """Test a standard gate with a label.""" @@ -532,6 +575,7 @@ def test_standard_gate_with_label(self): self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_circuit_with_conditional_with_label(self): """Test that instructions with conditions are correctly serialized.""" @@ -547,6 +591,7 @@ def test_circuit_with_conditional_with_label(self): self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_initialize_qft(self): """Test that initialize with a complex statevector and qft work.""" @@ -577,6 +622,7 @@ def test_initialize_qft(self): self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_single_bit_teleportation(self): """Test a teleportation circuit with single bit conditions.""" @@ -595,6 +641,7 @@ def test_single_bit_teleportation(self): self.assertEqual( [x.operation.label for x in qc.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qc, new_circ) def test_qaoa(self): """Test loading a QAOA circuit works.""" @@ -608,6 +655,7 @@ def test_qaoa(self): self.assertEqual( [x.operation.label for x in qaoa.data], [x.operation.label for x in new_circ.data] ) + self.assertDeprecatedBitProperties(qaoa, new_circ) def test_evolutiongate(self): """Test loading a circuit with evolution gate works.""" @@ -627,6 +675,7 @@ def test_evolutiongate(self): new_evo = new_circ.data[0].operation self.assertIsInstance(new_evo, PauliEvolutionGate) + self.assertDeprecatedBitProperties(qc, new_circ) def test_evolutiongate_param_time(self): """Test loading a circuit with an evolution gate that has a parameter for time.""" @@ -647,6 +696,7 @@ def test_evolutiongate_param_time(self): new_evo = new_circ.data[0].operation self.assertIsInstance(new_evo, PauliEvolutionGate) + self.assertDeprecatedBitProperties(qc, new_circ) def test_evolutiongate_param_expr_time(self): """Test loading a circuit with an evolution gate that has a parameter for time.""" @@ -667,6 +717,7 @@ def test_evolutiongate_param_expr_time(self): new_evo = new_circ.data[0].operation self.assertIsInstance(new_evo, PauliEvolutionGate) + self.assertDeprecatedBitProperties(qc, new_circ) def test_evolutiongate_param_vec_time(self): """Test loading a an evolution gate that has a param vector element for time.""" @@ -687,6 +738,7 @@ def test_evolutiongate_param_vec_time(self): new_evo = new_circ.data[0].operation self.assertIsInstance(new_evo, PauliEvolutionGate) + self.assertDeprecatedBitProperties(qc, new_circ) def test_op_list_evolutiongate(self): """Test loading a circuit with evolution gate works.""" @@ -705,6 +757,7 @@ def test_op_list_evolutiongate(self): new_evo = new_circ.data[0].operation self.assertIsInstance(new_evo, PauliEvolutionGate) + self.assertDeprecatedBitProperties(qc, new_circ) def test_op_evolution_gate_suzuki_trotter(self): """Test qpy path with a suzuki trotter synthesis method on an evolution gate.""" @@ -724,6 +777,7 @@ def test_op_evolution_gate_suzuki_trotter(self): new_evo = new_circ.data[0].operation self.assertIsInstance(new_evo, PauliEvolutionGate) + self.assertDeprecatedBitProperties(qc, new_circ) def test_parameter_expression_global_phase(self): """Test a circuit with a parameter expression global_phase.""" @@ -750,6 +804,7 @@ def test_parameter_expression_global_phase(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_parameter_global_phase(self): """Test a circuit with a parameter expression global_phase.""" @@ -779,6 +834,7 @@ def test_parameter_vector(self): new_circuit = load(qpy_file)[0] expected_params = [x.name for x in qc.parameters] self.assertEqual([x.name for x in new_circuit.parameters], expected_params) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_parameter_vector_element_in_expression(self): """Test a circuit with a parameter vector used in a parameter expression.""" @@ -803,6 +859,7 @@ def test_parameter_vector_element_in_expression(self): new_circuit = load(qpy_file)[0] expected_params = [x.name for x in qc.parameters] self.assertEqual([x.name for x in new_circuit.parameters], expected_params) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_parameter_vector_incomplete_warns(self): """Test that qpy's deserialization warns if a ParameterVector isn't fully identical.""" @@ -815,6 +872,7 @@ def test_parameter_vector_incomplete_warns(self): with self.assertWarnsRegex(UserWarning, r"^The ParameterVector.*Elements 0, 2.*fun$"): new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_parameter_vector_global_phase(self): """Test that a circuit with a standalone ParameterVectorElement phase works.""" @@ -825,6 +883,7 @@ def test_parameter_vector_global_phase(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_custom_metadata_serializer_full_path(self): """Test that running with custom metadata serialization works.""" @@ -878,6 +937,8 @@ def object_hook(self, o): # pylint: disable=invalid-name,method-hidden self.assertEqual(circuits[0].metadata["key"], CustomObject("Circuit 1")) self.assertEqual(qc, new_circuits[1]) self.assertEqual(circuits[1].metadata["key"], CustomObject("Circuit 2")) + self.assertDeprecatedBitProperties(qc, new_circuits[0]) + self.assertDeprecatedBitProperties(qc, new_circuits[1]) def test_qpy_with_ifelseop(self): """Test qpy serialization with an if block.""" @@ -892,6 +953,7 @@ def test_qpy_with_ifelseop(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_qpy_with_ifelseop_with_else(self): """Test qpy serialization with an else block.""" @@ -908,6 +970,7 @@ def test_qpy_with_ifelseop_with_else(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_qpy_with_while_loop(self): """Test qpy serialization with a for loop.""" @@ -922,6 +985,7 @@ def test_qpy_with_while_loop(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_qpy_with_for_loop(self): """Test qpy serialization with a for loop.""" @@ -937,6 +1001,7 @@ def test_qpy_with_for_loop(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_qpy_with_for_loop_iterator(self): """Test qpy serialization with a for loop.""" @@ -952,6 +1017,7 @@ def test_qpy_with_for_loop_iterator(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_standalone_register_partial_bit_in_circuit(self): """Test qpy with only some bits from standalone register.""" @@ -963,6 +1029,7 @@ def test_standalone_register_partial_bit_in_circuit(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_nested_tuple_param(self): """Test qpy with an instruction that contains nested tuples.""" @@ -974,6 +1041,7 @@ def test_nested_tuple_param(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_empty_tuple_param(self): """Test qpy with an instruction that contains an empty tuple.""" @@ -985,6 +1053,7 @@ def test_empty_tuple_param(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_ucr_gates(self): """Test qpy with UCRX, UCRY, and UCRZ gates.""" @@ -998,6 +1067,7 @@ def test_ucr_gates(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc.decompose().decompose(), new_circuit.decompose().decompose()) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_controlled_gate(self): """Test a custom controlled gate.""" @@ -1009,6 +1079,7 @@ def test_controlled_gate(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_controlled_gate_open_controls(self): """Test a controlled gate with open controls round-trips exactly.""" @@ -1020,6 +1091,7 @@ def test_controlled_gate_open_controls(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_nested_controlled_gate(self): """Test a custom nested controlled gate.""" @@ -1040,6 +1112,7 @@ def test_nested_controlled_gate(self): new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) self.assertEqual(qc.decompose(), new_circ.decompose()) + self.assertDeprecatedBitProperties(qc, new_circ) def test_open_controlled_gate(self): """Test an open control is preserved across serialization.""" @@ -1051,6 +1124,7 @@ 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) + self.assertDeprecatedBitProperties(qc, new_circ) def test_standard_control_gates(self): """Test standard library controlled gates.""" @@ -1074,6 +1148,7 @@ def test_standard_control_gates(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_controlled_gate_subclass_custom_definition(self): """Test controlled gate with overloaded definition. @@ -1102,6 +1177,7 @@ def _define(self) -> None: new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) self.assertEqual(qc.decompose(), new_circ.decompose()) + self.assertDeprecatedBitProperties(qc, new_circ) def test_load_with_loose_bits(self): """Test that loading from a circuit with loose bits works.""" @@ -1113,6 +1189,7 @@ def test_load_with_loose_bits(self): self.assertEqual(tuple(new_circuit.qregs), ()) self.assertEqual(tuple(new_circuit.cregs), ()) self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_load_with_loose_bits_and_registers(self): """Test that loading from a circuit with loose bits and registers works.""" @@ -1122,6 +1199,7 @@ def test_load_with_loose_bits_and_registers(self): qpy_file.seek(0) new_circuit = load(qpy_file)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_registers_after_loose_bits(self): """Test that a circuit whose registers appear after some loose bits roundtrips. Regression @@ -1135,6 +1213,7 @@ def test_registers_after_loose_bits(self): fptr.seek(0) new_circuit = load(fptr)[0] self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_roundtrip_empty_register(self): """Test that empty registers round-trip correctly.""" @@ -1146,6 +1225,7 @@ def test_roundtrip_empty_register(self): self.assertEqual(qc, new_circuit) self.assertEqual(qc.qregs, new_circuit.qregs) self.assertEqual(qc.cregs, new_circuit.cregs) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_roundtrip_several_empty_registers(self): """Test that several empty registers round-trip correctly.""" @@ -1162,6 +1242,7 @@ def test_roundtrip_several_empty_registers(self): self.assertEqual(qc, new_circuit) self.assertEqual(qc.qregs, new_circuit.qregs) self.assertEqual(qc.cregs, new_circuit.cregs) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_roundtrip_empty_registers_with_loose_bits(self): """Test that empty registers still round-trip correctly in the presence of loose bits.""" @@ -1184,6 +1265,20 @@ def test_roundtrip_empty_registers_with_loose_bits(self): self.assertEqual(qc, new_circuit) self.assertEqual(qc.qregs, new_circuit.qregs) self.assertEqual(qc.cregs, new_circuit.cregs) + self.assertDeprecatedBitProperties(qc, new_circuit) + + def test_incomplete_owned_bits(self): + """Test that a circuit that contains only some bits that are owned by a register are + correctly roundtripped.""" + reg = QuantumRegister(5, "q") + qc = QuantumCircuit(reg[:3]) + qc.ccx(0, 1, 2) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_circuit = load(fptr)[0] + self.assertEqual(qc, new_circuit) + self.assertDeprecatedBitProperties(qc, new_circuit) def test_qpy_deprecation(self): """Test the old import path's deprecations fire.""" diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index f669a461b810..e932897b475c 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -14,6 +14,7 @@ """Test cases to verify qpy backwards compatibility.""" import argparse +import itertools import random import re import sys @@ -516,13 +517,8 @@ def generate_open_controlled_gates(): return circuits -def generate_circuits(version_str=None): +def generate_circuits(version_parts): """Generate reference circuits.""" - version_parts = None - if version_str: - version_match = re.search(VERSION_PATTERN, version_str, re.VERBOSE | re.IGNORECASE) - version_parts = tuple(int(x) for x in version_match.group("release").split(".")) - output_circuits = { "full.qpy": [generate_full_circuit()], "unitary.qpy": [generate_unitary_gate_circuit()], @@ -559,7 +555,7 @@ def generate_circuits(version_str=None): return output_circuits -def assert_equal(reference, qpy, count, bind=None): +def assert_equal(reference, qpy, count, version_parts, bind=None): """Compare two circuits.""" if bind is not None: reference_parameter_names = [x.name for x in reference.parameters] @@ -580,6 +576,22 @@ def assert_equal(reference, qpy, count, bind=None): ) sys.stderr.write(msg) sys.exit(1) + # Check deprecated bit properties, if set. The QPY dumping code before Terra 0.23.2 didn't + # include enough information for us to fully reconstruct this, so we only test if newer. + if version_parts >= (0, 23, 2) and isinstance(reference, QuantumCircuit): + for ref_bit, qpy_bit in itertools.chain( + zip(reference.qubits, qpy.qubits), zip(reference.clbits, qpy.clbits) + ): + if ref_bit._register is not None and ref_bit != qpy_bit: + msg = ( + f"Reference Circuit {count}:\n" + "deprecated bit-level register information mismatch\n" + f"reference bit: {ref_bit}\n" + f"loaded bit: {qpy_bit}\n" + ) + sys.stderr.write(msg) + sys.exit(1) + # Don't compare name on bound circuits if bind is None and reference.name != qpy.name: msg = f"Circuit {count} name mismatch {reference.name} != {qpy.name}\n{reference}\n{qpy}" @@ -598,7 +610,7 @@ def generate_qpy(qpy_files): dump(circuits, fd) -def load_qpy(qpy_files): +def load_qpy(qpy_files, version_parts): """Load qpy circuits from files and compare to reference circuits.""" for path, circuits in qpy_files.items(): print(f"Loading qpy file: {path}") @@ -618,7 +630,7 @@ def load_qpy(qpy_files): elif path == "parameter_vector_expression.qpy": bind = np.linspace(1.0, 2.0, 15) - assert_equal(circuit, qpy_circuits[i], i, bind=bind) + assert_equal(circuit, qpy_circuits[i], i, version_parts, bind=bind) def _main(): @@ -634,11 +646,18 @@ def _main(): ), ) args = parser.parse_args() - qpy_files = generate_circuits(args.version) + + # Terra 0.18.0 was the first release with QPY, so that's the default. + version_parts = (0, 18, 0) + if args.version: + version_match = re.search(VERSION_PATTERN, args.version, re.VERBOSE | re.IGNORECASE) + version_parts = tuple(int(x) for x in version_match.group("release").split(".")) + + qpy_files = generate_circuits(version_parts) if args.command == "generate": generate_qpy(qpy_files) else: - load_qpy(qpy_files) + load_qpy(qpy_files, version_parts) if __name__ == "__main__":