From 417d6c84adc1388ed29748ba4fe92d6bdbd916e0 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 4 Sep 2024 17:19:32 +0200 Subject: [PATCH 1/4] add qregs to output circuits --- qiskit/circuit/quantumcircuit.py | 8 ++++++-- .../synthesis/clifford/clifford_decompose_bm.py | 2 +- .../clifford/clifford_decompose_greedy.py | 2 +- qiskit/synthesis/linear/cnot_synth.py | 2 +- qiskit/synthesis/linear_phase/cz_depth_lnn.py | 4 +++- .../synthesis/one_qubit/one_qubit_decompose.py | 3 ++- .../synthesis/permutation/permutation_full.py | 4 ++-- qiskit/synthesis/permutation/permutation_lnn.py | 4 +++- .../permutation/permutation_reverse_lnn.py | 4 +++- .../synthesis/two_qubit/two_qubit_decompose.py | 4 ++-- .../notes/fix-synth-qregs-7662681c0ff02511.yaml | 7 +++++++ .../operators/symplectic/test_clifford.py | 17 +++++++++++++++++ 12 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/fix-synth-qregs-7662681c0ff02511.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ebc63511ca7b..e2665ad761e6 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1157,9 +1157,13 @@ def __init__( transpiler and reattach it to the output, so you can track your own metadata.""" @classmethod - def _from_circuit_data(cls, data: CircuitData) -> typing.Self: + def _from_circuit_data(cls, data: CircuitData, add_regs: bool = False) -> typing.Self: """A private constructor from rust space circuit data.""" - out = QuantumCircuit() + if add_regs: + out = QuantumCircuit(data.num_qubits(), data.num_clbits()) + else: + out = QuantumCircuit() + out._data = data out._qubit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.qubits)} out._clbit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.clbits)} diff --git a/qiskit/synthesis/clifford/clifford_decompose_bm.py b/qiskit/synthesis/clifford/clifford_decompose_bm.py index ac4923446bcd..5ead6945eba2 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_bm.py +++ b/qiskit/synthesis/clifford/clifford_decompose_bm.py @@ -41,7 +41,7 @@ def synth_clifford_bm(clifford: Clifford) -> QuantumCircuit: `arXiv:2003.09412 [quant-ph] `_ """ circuit = QuantumCircuit._from_circuit_data( - synth_clifford_bm_inner(clifford.tableau.astype(bool)) + synth_clifford_bm_inner(clifford.tableau.astype(bool)), add_regs=True ) circuit.name = str(clifford) return circuit diff --git a/qiskit/synthesis/clifford/clifford_decompose_greedy.py b/qiskit/synthesis/clifford/clifford_decompose_greedy.py index 0a679a8a7a6f..9766efe4aa8f 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_greedy.py +++ b/qiskit/synthesis/clifford/clifford_decompose_greedy.py @@ -51,7 +51,7 @@ def synth_clifford_greedy(clifford: Clifford) -> QuantumCircuit: `arXiv:2105.02291 [quant-ph] `_ """ circuit = QuantumCircuit._from_circuit_data( - synth_clifford_greedy_inner(clifford.tableau.astype(bool)) + synth_clifford_greedy_inner(clifford.tableau.astype(bool)), add_regs=True ) circuit.name = str(clifford) return circuit diff --git a/qiskit/synthesis/linear/cnot_synth.py b/qiskit/synthesis/linear/cnot_synth.py index f900ff86d17e..bb3d9c6896ff 100644 --- a/qiskit/synthesis/linear/cnot_synth.py +++ b/qiskit/synthesis/linear/cnot_synth.py @@ -66,4 +66,4 @@ def synth_cnot_count_full_pmh( circuit_data = fast_pmh(normalized, section_size) # construct circuit from the data - return QuantumCircuit._from_circuit_data(circuit_data) + return QuantumCircuit._from_circuit_data(circuit_data, add_regs=True) diff --git a/qiskit/synthesis/linear_phase/cz_depth_lnn.py b/qiskit/synthesis/linear_phase/cz_depth_lnn.py index 419aec806f2e..d7dd071956e0 100644 --- a/qiskit/synthesis/linear_phase/cz_depth_lnn.py +++ b/qiskit/synthesis/linear_phase/cz_depth_lnn.py @@ -53,4 +53,6 @@ def synth_cz_depth_line_mr(mat: np.ndarray) -> QuantumCircuit: """ # Call Rust implementaton - return QuantumCircuit._from_circuit_data(synth_cz_depth_line_mr_inner(mat.astype(bool))) + return QuantumCircuit._from_circuit_data( + synth_cz_depth_line_mr_inner(mat.astype(bool)), add_regs=True + ) diff --git a/qiskit/synthesis/one_qubit/one_qubit_decompose.py b/qiskit/synthesis/one_qubit/one_qubit_decompose.py index f60f20f9524e..60da6ed6e82b 100644 --- a/qiskit/synthesis/one_qubit/one_qubit_decompose.py +++ b/qiskit/synthesis/one_qubit/one_qubit_decompose.py @@ -224,7 +224,8 @@ def _decompose(self, unitary, simplify=True, atol=DEFAULT_ATOL): return QuantumCircuit._from_circuit_data( euler_one_qubit_decomposer.unitary_to_circuit( unitary, [self.basis], 0, None, simplify, atol - ) + ), + add_regs=True, ) @property diff --git a/qiskit/synthesis/permutation/permutation_full.py b/qiskit/synthesis/permutation/permutation_full.py index 2fd892a0427e..7dd5ae99dc4c 100644 --- a/qiskit/synthesis/permutation/permutation_full.py +++ b/qiskit/synthesis/permutation/permutation_full.py @@ -42,7 +42,7 @@ def synth_permutation_basic(pattern: list[int] | np.ndarray[int]) -> QuantumCirc Returns: The synthesized quantum circuit. """ - return QuantumCircuit._from_circuit_data(_synth_permutation_basic(pattern)) + return QuantumCircuit._from_circuit_data(_synth_permutation_basic(pattern), add_regs=True) def synth_permutation_acg(pattern: list[int] | np.ndarray[int]) -> QuantumCircuit: @@ -75,4 +75,4 @@ def synth_permutation_acg(pattern: list[int] | np.ndarray[int]) -> QuantumCircui *Routing Permutations on Graphs Via Matchings.*, `(Full paper) `_ """ - return QuantumCircuit._from_circuit_data(_synth_permutation_acg(pattern)) + return QuantumCircuit._from_circuit_data(_synth_permutation_acg(pattern), add_regs=True) diff --git a/qiskit/synthesis/permutation/permutation_lnn.py b/qiskit/synthesis/permutation/permutation_lnn.py index d5da6929463c..5f1bfbeaa1a8 100644 --- a/qiskit/synthesis/permutation/permutation_lnn.py +++ b/qiskit/synthesis/permutation/permutation_lnn.py @@ -49,4 +49,6 @@ def synth_permutation_depth_lnn_kms(pattern: list[int] | np.ndarray[int]) -> Qua # In the permutation synthesis code below the notation is opposite: # [2, 4, 3, 0, 1] means that 0 maps to 2, 1 to 3, 2 to 3, 3 to 0, and 4 to 1. # This is why we invert the pattern. - return QuantumCircuit._from_circuit_data(_synth_permutation_depth_lnn_kms(pattern)) + return QuantumCircuit._from_circuit_data( + _synth_permutation_depth_lnn_kms(pattern), add_regs=True + ) diff --git a/qiskit/synthesis/permutation/permutation_reverse_lnn.py b/qiskit/synthesis/permutation/permutation_reverse_lnn.py index f214fd7ce294..ccc820d97e62 100644 --- a/qiskit/synthesis/permutation/permutation_reverse_lnn.py +++ b/qiskit/synthesis/permutation/permutation_reverse_lnn.py @@ -88,4 +88,6 @@ def synth_permutation_reverse_lnn_kms(num_qubits: int) -> QuantumCircuit: """ # Call Rust implementation - return QuantumCircuit._from_circuit_data(synth_permutation_reverse_lnn_kms_inner(num_qubits)) + return QuantumCircuit._from_circuit_data( + synth_permutation_reverse_lnn_kms_inner(num_qubits), add_regs=True + ) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 633de3a64f78..0e3427773387 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -233,7 +233,7 @@ def circuit( circuit_data = self._inner_decomposition.circuit( euler_basis=euler_basis, simplify=simplify, atol=atol ) - return QuantumCircuit._from_circuit_data(circuit_data) + return QuantumCircuit._from_circuit_data(circuit_data, add_regs=True) def actual_fidelity(self, **kwargs) -> float: """Calculates the actual fidelity of the decomposed circuit to the input unitary.""" @@ -672,7 +672,7 @@ def __call__( approximate, _num_basis_uses=_num_basis_uses, ) - return QuantumCircuit._from_circuit_data(circ_data) + return QuantumCircuit._from_circuit_data(circ_data, add_regs=True) else: sequence = self._inner_decomposer( np.asarray(unitary, dtype=complex), diff --git a/releasenotes/notes/fix-synth-qregs-7662681c0ff02511.yaml b/releasenotes/notes/fix-synth-qregs-7662681c0ff02511.yaml new file mode 100644 index 000000000000..d838032ffaf9 --- /dev/null +++ b/releasenotes/notes/fix-synth-qregs-7662681c0ff02511.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a bug where various synthesis methods created circuits without quantum or + classical registers. This also affected functions that internally used the synthesis + methods, such as :meth:`.Clifford.to_circuit`. + Fixed `#13041 `__. diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index 061348c78cdb..e78feaf3b78f 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -598,6 +598,23 @@ def test_to_circuit(self, num_qubits): # Convert back to clifford and check it is the same self.assertEqual(Clifford(decomp), target) + def test_to_circuit_manual(self): + """Test a manual comparison to a known circuit. + + This also tests whether the resulting Clifford circuit has quantum registers, thereby + regression testing #13041. + """ + # this is set to a circuit that remains the same under Clifford reconstruction + circuit = QuantumCircuit(2) + circuit.z(0) + circuit.h(0) + circuit.cx(0, 1) + + cliff = Clifford(circuit) + reconstructed = cliff.to_circuit() + + self.assertEqual(circuit, reconstructed) + @combine(num_qubits=[1, 2, 3, 4, 5]) def test_to_instruction(self, num_qubits): """Test to_instruction method""" From 9eabf6210f1dc575f1ab78ef5a0c354747f5e4bf Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 5 Sep 2024 09:05:04 +0200 Subject: [PATCH 2/4] never develop while not on up-to-date main --- qiskit/circuit/quantumcircuit.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index e2665ad761e6..3a71d4749e0f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1152,21 +1152,26 @@ def __init__( """The unit that :attr:`duration` is specified in.""" self.metadata = {} if metadata is None else metadata """Arbitrary user-defined metadata for the circuit. - + Qiskit will not examine the content of this mapping, but it will pass it through the transpiler and reattach it to the output, so you can track your own metadata.""" @classmethod def _from_circuit_data(cls, data: CircuitData, add_regs: bool = False) -> typing.Self: """A private constructor from rust space circuit data.""" - if add_regs: - out = QuantumCircuit(data.num_qubits(), data.num_clbits()) - else: - out = QuantumCircuit() - - out._data = data + out = QuantumCircuit() out._qubit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.qubits)} out._clbit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.clbits)} + out._data = data + + # To add consistent registers, we compose the circuit onto a new one with registers, + # as each instruction must be mapped to qubits that belong to a register (in the circuit + # data coming from Rust, the qubits are not attached to any register). + if add_regs: + out_with_regs = QuantumCircuit(out.num_qubits, out.num_clbits) + out_with_regs.compose(out, inplace=True, copy=False) + return out_with_regs + return out @staticmethod From 1fc9183abcb7952bcbdec41e7f8262304b66bb74 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 5 Sep 2024 10:48:46 +0200 Subject: [PATCH 3/4] one shan't commit faster than ones IDE can run black --- qiskit/circuit/quantumcircuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 3a71d4749e0f..22233d5244e0 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1167,7 +1167,7 @@ def _from_circuit_data(cls, data: CircuitData, add_regs: bool = False) -> typing # To add consistent registers, we compose the circuit onto a new one with registers, # as each instruction must be mapped to qubits that belong to a register (in the circuit # data coming from Rust, the qubits are not attached to any register). - if add_regs: + if add_regs: out_with_regs = QuantumCircuit(out.num_qubits, out.num_clbits) out_with_regs.compose(out, inplace=True, copy=False) return out_with_regs From 952f45bf24a4873a249674b075bf4dd5c6dd8847 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 10 Sep 2024 11:05:46 +0200 Subject: [PATCH 4/4] avoid compose --- qiskit/circuit/quantumcircuit.py | 56 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 22233d5244e0..566a6fef9f70 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1160,17 +1160,32 @@ def __init__( def _from_circuit_data(cls, data: CircuitData, add_regs: bool = False) -> typing.Self: """A private constructor from rust space circuit data.""" out = QuantumCircuit() - out._qubit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.qubits)} - out._clbit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.clbits)} - out._data = data - # To add consistent registers, we compose the circuit onto a new one with registers, - # as each instruction must be mapped to qubits that belong to a register (in the circuit - # data coming from Rust, the qubits are not attached to any register). - if add_regs: - out_with_regs = QuantumCircuit(out.num_qubits, out.num_clbits) - out_with_regs.compose(out, inplace=True, copy=False) - return out_with_regs + if data.num_qubits > 0: + if add_regs: + qr = QuantumRegister(name="q", bits=data.qubits) + out.qregs = [qr] + out._qubit_indices = { + bit: BitLocations(index, [(qr, index)]) for index, bit in enumerate(data.qubits) + } + else: + out._qubit_indices = { + bit: BitLocations(index, []) for index, bit in enumerate(data.qubits) + } + + if data.num_clbits > 0: + if add_regs: + cr = ClassicalRegister(name="c", bits=data.clbits) + out.cregs = [cr] + out._clbit_indices = { + bit: BitLocations(index, [(cr, index)]) for index, bit in enumerate(data.clbits) + } + else: + out._clbit_indices = { + bit: BitLocations(index, []) for index, bit in enumerate(data.clbits) + } + + out._data = data return out @@ -3022,16 +3037,7 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: self._ancillas.append(bit) if isinstance(register, QuantumRegister): - self.qregs.append(register) - - for idx, bit in enumerate(register): - if bit in self._qubit_indices: - self._qubit_indices[bit].registers.append((register, idx)) - else: - self._data.add_qubit(bit) - self._qubit_indices[bit] = BitLocations( - self._data.num_qubits - 1, [(register, idx)] - ) + self._add_qreg(register) elif isinstance(register, ClassicalRegister): self.cregs.append(register) @@ -3050,6 +3056,16 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: else: raise CircuitError("expected a register") + def _add_qreg(self, qreg: QuantumRegister) -> None: + self.qregs.append(qreg) + + for idx, bit in enumerate(qreg): + if bit in self._qubit_indices: + self._qubit_indices[bit].registers.append((qreg, idx)) + else: + self._data.add_qubit(bit) + self._qubit_indices[bit] = BitLocations(self._data.num_qubits - 1, [(qreg, idx)]) + def add_bits(self, bits: Iterable[Bit]) -> None: """Add Bits to the circuit.""" duplicate_bits = {