diff --git a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py index 808bd51059ee..9eadf271d907 100644 --- a/qiskit/transpiler/passes/optimization/split_2q_unitaries.py +++ b/qiskit/transpiler/passes/optimization/split_2q_unitaries.py @@ -9,8 +9,8 @@ # Any modifications or derivative works of this code must retain this # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. + """Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" -from typing import Optional from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.quantumcircuitdata import CircuitInstruction @@ -20,34 +20,30 @@ class Split2QUnitaries(TransformationPass): - """Attempt to splits two-qubit gates in a :class:`.DAGCircuit` into two single-qubit gates + """Attempt to splits two-qubit unitaries in a :class:`.DAGCircuit` into two single-qubit gates. - This pass will analyze all the two qubit gates in the circuit and analyze the gate's unitary - matrix to determine if the gate is actually a product of 2 single qubit gates. In these - cases the 2q gate can be simplified into two single qubit gates and this pass will - perform this optimization and will replace the two qubit gate with two single qubit - :class:`.UnitaryGate`. + This pass will analyze all :class:`.UnitaryGate` instances and determine whether the + matrix is actually a product of 2 single qubit gates. In these cases the 2q gate can be + simplified into two single qubit gates and this pass will perform this optimization and will + replace the two qubit gate with two single qubit :class:`.UnitaryGate`. """ - def __init__(self, fidelity: Optional[float] = 1.0 - 1e-16): - """Split2QUnitaries initializer. - + def __init__(self, fidelity: float = 1.0 - 1e-16): + """ Args: - fidelity (float): Allowed tolerance for splitting two-qubit unitaries and gate decompositions + fidelity: Allowed tolerance for splitting two-qubit unitaries and gate decompositions. """ super().__init__() self.requested_fidelity = fidelity - def run(self, dag: DAGCircuit): + def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the Split2QUnitaries pass on `dag`.""" + for node in dag.topological_op_nodes(): - # skip operations without two-qubits and for which we can not determine a potential 1q split - if ( - len(node.cargs) > 0 - or len(node.qargs) != 2 - or node.matrix is None - or node.is_parameterized() - ): + # We only attempt to split UnitaryGate objects, but this could be extended in future + # -- however we need to ensure that we can compile the resulting single-qubit unitaries + # to the supported basis gate set. + if not (len(node.qargs) == 2 and node.op.name == "unitary"): continue decomp = TwoQubitWeylDecomposition(node.matrix, fidelity=self.requested_fidelity) diff --git a/releasenotes/notes/restrict-split2q-d51d840cc7a7a482.yaml b/releasenotes/notes/restrict-split2q-d51d840cc7a7a482.yaml new file mode 100644 index 000000000000..009a97720e30 --- /dev/null +++ b/releasenotes/notes/restrict-split2q-d51d840cc7a7a482.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed an edge case when transpiling a circuit with ``optimization_level`` 2 or 3 with an + incomplete 1-qubit basis gate set on a circuit containing 2-qubit gates, that can be + implemented as a product of single qubit gates. This bug is resolved by restricting + :class:`.Split2QUnitaries` to consider only :class:`.UnitaryGate` objects. + Fixed `#12970 `__. diff --git a/test/python/transpiler/test_split_2q_unitaries.py b/test/python/transpiler/test_split_2q_unitaries.py index adedb2106ab8..cfe4fb9c8fb4 100644 --- a/test/python/transpiler/test_split_2q_unitaries.py +++ b/test/python/transpiler/test_split_2q_unitaries.py @@ -18,7 +18,7 @@ import numpy as np from qiskit import QuantumCircuit, QuantumRegister, transpile -from qiskit.circuit.library import UnitaryGate, XGate, ZGate, HGate +from qiskit.circuit.library import UnitaryGate, XGate, ZGate, HGate, CPhaseGate from qiskit.circuit import Parameter, CircuitInstruction, Gate from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.quantum_info import Operator @@ -160,15 +160,11 @@ def test_no_split(self): def test_almost_identity(self): """Test that the pass handles QFT correctly.""" qc = QuantumCircuit(2) - qc.cp(pi * 2 ** -(26), 0, 1) + qc.unitary(CPhaseGate(pi * 2 ** -(26)).to_matrix(), [0, 1]) pm = PassManager() - pm.append(Collect2qBlocks()) - pm.append(ConsolidateBlocks()) pm.append(Split2QUnitaries(fidelity=1.0 - 1e-9)) qc_split = pm.run(qc) pm = PassManager() - pm.append(Collect2qBlocks()) - pm.append(ConsolidateBlocks()) pm.append(Split2QUnitaries()) qc_split2 = pm.run(qc) self.assertEqual(qc_split.num_nonlocal_gates(), 0) @@ -211,19 +207,6 @@ def test_single_q_gates(self): self.assertTrue(CircuitInstruction(ZGate(), qubits=[qr[1]], clbits=[]) in qc.data) self.assertTrue(CircuitInstruction(HGate(), qubits=[qr[2]], clbits=[]) in qc.data) - def test_split_qft(self): - """Test that the pass handles QFT correctly.""" - qc = QuantumCircuit(100) - qc.h(0) - for i in range(qc.num_qubits - 2, 0, -1): - qc.cp(pi * 2 ** -(qc.num_qubits - 1 - i), qc.num_qubits - 1, i) - pm = PassManager() - pm.append(Collect2qBlocks()) - pm.append(ConsolidateBlocks()) - pm.append(Split2QUnitaries()) - qc_split = pm.run(qc) - self.assertEqual(26, qc_split.num_nonlocal_gates()) - def test_gate_no_array(self): """ Test that the pass doesn't fail when the circuit contains a custom gate @@ -259,3 +242,26 @@ def mygate(self, qubit1, qubit2): self.assertTrue( matrix_equal(Operator(qc).data, Operator(qc_split).data, ignore_phase=False) ) + + def test_nosplit_custom(self): + """Test a single custom gate is not split, even if it is a product. + + That is because we cannot guarantee the split gate is valid in a given basis. + Regression test for #12970. + """ + + class MyGate(Gate): + """A custom gate that could be split.""" + + def __init__(self): + super().__init__("mygate", 2, []) + + def to_matrix(self): + return np.eye(4, dtype=complex) + + qc = QuantumCircuit(2) + qc.append(MyGate(), [0, 1]) + + no_split = Split2QUnitaries()(qc) + + self.assertDictEqual({"mygate": 1}, no_split.count_ops())