From f48ca2b19fa09a4076e5082149bb45b4b6a069fe Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 11 Feb 2025 10:12:15 +0200 Subject: [PATCH 1/2] Correctly updating global phase when removing gates that are identity up to a global phase (#13785) * correctly updating global phase when removing -I gates from the circuit * similar fix for specialized rotation gates * Correctly updating global phase when removing gates that are equivalent to identity up to a global phase * also handling UnitaryGates after the unitary gates PR was merged * applying review suggestions * removing code duplication (cherry picked from commit 7995cabbdf47021b5d1b6d4c8f1daec206a4f6af) # Conflicts: # crates/accelerate/src/remove_identity_equiv.rs --- .../accelerate/src/remove_identity_equiv.rs | 48 ++++++++++++------- .../optimization/remove_identity_equiv.py | 5 +- ...remove-id-equivalent-6480da0c62f20df1.yaml | 8 ++++ .../test_remove_identity_equivalent.py | 37 +++++++++++--- 4 files changed, 70 insertions(+), 28 deletions(-) create mode 100644 releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index a3eb921628e2..34921fa97273 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -9,7 +9,6 @@ // 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. - use num_complex::Complex64; use num_complex::ComplexFloat; use pyo3::prelude::*; @@ -27,11 +26,13 @@ use qiskit_circuit::packed_instruction::PackedInstruction; #[pyfunction] #[pyo3(signature=(dag, approx_degree=Some(1.0), target=None))] fn remove_identity_equiv( + py: Python, dag: &mut DAGCircuit, approx_degree: Option, target: Option<&Target>, ) { let mut remove_list: Vec = Vec::new(); + let mut global_phase_update: f64 = 0.; let get_error_cutoff = |inst: &PackedInstruction| -> f64 { match approx_degree { @@ -74,14 +75,25 @@ fn remove_identity_equiv( } }; +<<<<<<< HEAD for op_node in dag.op_nodes(false) { let inst = dag.dag()[op_node].unwrap_operation(); match inst.op.view() { OperationRef::Standard(gate) => { +======= + for (op_node, inst) in dag.op_nodes(false) { + if inst.is_parameterized() { + // Skip parameterized gates + continue; + } + let view = inst.op.view(); + match view { + OperationRef::StandardGate(gate) => { +>>>>>>> 7995cabbd (Correctly updating global phase when removing gates that are identity up to a global phase (#13785)) let (dim, trace) = match gate { StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { if let Param::Float(theta) = inst.params_view()[0] { - let trace = (theta / 2.).cos() * 2.; + let trace = Complex64::new((theta / 2.).cos() * 2., 0.); (2., trace) } else { continue; @@ -92,20 +104,16 @@ fn remove_identity_equiv( | StandardGate::RZZGate | StandardGate::RZXGate => { if let Param::Float(theta) = inst.params_view()[0] { - let trace = (theta / 2.).cos() * 4.; + let trace = Complex64::new((theta / 2.).cos() * 4., 0.); (4., trace) } else { continue; } } _ => { - // Skip global phase gate - if gate.num_qubits() < 1 { - continue; - } if let Some(matrix) = gate.matrix(inst.params_view()) { let dim = matrix.shape()[0] as f64; - let trace = matrix.diag().iter().sum::().abs(); + let trace = matrix.diag().iter().sum::(); (dim, trace) } else { continue; @@ -113,34 +121,38 @@ fn remove_identity_equiv( } }; let error = get_error_cutoff(inst); - let f_pro = (trace / dim).powi(2); + let f_pro = (trace / dim).abs().powi(2); let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); if (1. - gate_fidelity).abs() < error { - remove_list.push(op_node) + remove_list.push(op_node); + global_phase_update += (trace / dim).arg(); } } - OperationRef::Gate(gate) => { - // Skip global phase like gate - if gate.num_qubits() < 1 { - continue; - } - if let Some(matrix) = gate.matrix(inst.params_view()) { + _ => { + let matrix = view.matrix(inst.params_view()); + // If view.matrix() returns None, then there is no matrix and we skip the operation. + if let Some(matrix) = matrix { let error = get_error_cutoff(inst); let dim = matrix.shape()[0] as f64; let trace: Complex64 = matrix.diag().iter().sum(); let f_pro = (trace / dim).abs().powi(2); let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); if (1. - gate_fidelity).abs() < error { - remove_list.push(op_node) + remove_list.push(op_node); + global_phase_update += (trace / dim).arg(); } } } - _ => continue, } } for node in remove_list { dag.remove_op_node(node); } + + if global_phase_update != 0. { + dag.add_global_phase(py, &Param::Float(global_phase_update)) + .expect("The global phase is guaranteed to be a float"); + } } pub fn remove_identity_equiv_mod(m: &Bound) -> PyResult<()> { diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py index fbf132d958a2..17445eb5ad2a 100644 --- a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -23,9 +23,8 @@ class RemoveIdentityEquivalent(TransformationPass): r"""Remove gates with negligible effects. - Removes gates whose effect is close to an identity operation, up to the specified - tolerance. Zero qubit gates such as :class:`.GlobalPhaseGate` are not considered - by this pass. + Removes gates whose effect is close to an identity operation up to a global phase + and up to the specified tolerance. Parameterized gates are not considered by this pass. For a cutoff fidelity :math:`f`, this pass removes gates whose average gate fidelity with respect to the identity is below :math:`f`. Concretely, diff --git a/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml b/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml new file mode 100644 index 000000000000..3ee4c4accc21 --- /dev/null +++ b/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.RemoveIdentityEquivalent` transpiler pass, where gates close + to identity up to a global phase were removed from the circuit, + but the global phase of the circuit was not updated. In particular, + :class:`.RemoveIdentityEquivalent` now removes non-parameterized :class:`.GlobalPhaseGate` + gates. diff --git a/test/python/transpiler/test_remove_identity_equivalent.py b/test/python/transpiler/test_remove_identity_equivalent.py index 1db392d3654b..736c02b765bd 100644 --- a/test/python/transpiler/test_remove_identity_equivalent.py +++ b/test/python/transpiler/test_remove_identity_equivalent.py @@ -12,6 +12,7 @@ """Tests for the DropNegligible transpiler pass.""" +import ddt import numpy as np from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate @@ -26,6 +27,7 @@ XXMinusYYGate, XXPlusYYGate, GlobalPhaseGate, + UnitaryGate, ) from qiskit.quantum_info import Operator from qiskit.transpiler.passes import RemoveIdentityEquivalent @@ -34,6 +36,7 @@ from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt.ddt class TestDropNegligible(QiskitTestCase): """Test the DropNegligible pass.""" @@ -173,13 +176,33 @@ def to_matrix(self): expected = QuantumCircuit(3) self.assertEqual(expected, transpiled) - def test_global_phase_ignored(self): - """Test that global phase gate isn't considered.""" + @ddt.data( + RXGate(0), + RXGate(2 * np.pi), + RYGate(0), + RYGate(2 * np.pi), + RZGate(0), + RZGate(2 * np.pi), + UnitaryGate(np.array([[1, 0], [0, 1]])), + UnitaryGate(np.array([[-1, 0], [0, -1]])), + UnitaryGate(np.array([[np.exp(1j * np.pi / 4), 0], [0, np.exp(1j * np.pi / 4)]])), + GlobalPhaseGate(0), + GlobalPhaseGate(np.pi / 4), + ) + def test_remove_identity_up_to_global_phase(self, gate): + """Test that gates equivalent to identity up to a global phase are removed from the circuit, + and the global phase of the circuit is updated correctly. + """ + qc = QuantumCircuit(gate.num_qubits) + qc.append(gate, qc.qubits) + transpiled = RemoveIdentityEquivalent()(qc) + self.assertEqual(transpiled.size(), 0) + self.assertEqual(Operator(qc), Operator(transpiled)) + def test_parameterized_global_phase_ignored(self): + """Test that parameterized global phase gates are not removed by the pass.""" + theta = Parameter("theta") qc = QuantumCircuit(1) - qc.id(0) - qc.append(GlobalPhaseGate(0)) + qc.append(GlobalPhaseGate(theta), []) transpiled = RemoveIdentityEquivalent()(qc) - expected = QuantumCircuit(1) - expected.append(GlobalPhaseGate(0)) - self.assertEqual(transpiled, expected) + self.assertEqual(qc, transpiled) From 4c97e4a47930fc4bb2989e0afa250a7c702c1096 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 11 Feb 2025 13:45:40 +0200 Subject: [PATCH 2/2] Fixing merge conflicts --- crates/accelerate/src/remove_identity_equiv.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index 34921fa97273..d5d644919c8f 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -75,21 +75,15 @@ fn remove_identity_equiv( } }; -<<<<<<< HEAD for op_node in dag.op_nodes(false) { let inst = dag.dag()[op_node].unwrap_operation(); - match inst.op.view() { - OperationRef::Standard(gate) => { -======= - for (op_node, inst) in dag.op_nodes(false) { if inst.is_parameterized() { // Skip parameterized gates continue; } let view = inst.op.view(); match view { - OperationRef::StandardGate(gate) => { ->>>>>>> 7995cabbd (Correctly updating global phase when removing gates that are identity up to a global phase (#13785)) + OperationRef::Standard(gate) => { let (dim, trace) = match gate { StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { if let Param::Float(theta) = inst.params_view()[0] {