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__":