Repository: Qiskit/qiskit — pull request #13825 (python)
Title: Correctly updating global phase when removing gates that are identity up to a global phase (backport #13785)

\n\n### Summary\n\nThis fixes a bug in the ``RemoveIdentityEquivalent`` transpiler pass (the pass was introduced in #12384), where unitary gates close to identity _up to a global phase_ were removed from the circuit but the global phase of the circuit was not updated. This includes, for instance, single-qubit unitary gates with matrices close to [[ $e^{i\\theta}$, 0], [0, $e^{i\\theta}$]] for fix floating-point values of $\\theta$, standard gates $R_X(2\\pi)$, $R_Y(2\\pi)$, $R_Z(2\\pi)$ (whose matrices are close to $-I$), and some others. Now the pass removes these gates and correctly updates the global phase of the circuit. In particular, the pass now handles non-parameterized ``GlobalPhaseGates`` as well.  \n\nFixes #13778.\n\n### Details and comments:\n\nThe pass ignores parameterized gates. We could probably relatively easily extend it to handle parameterized global phase gates.\n\nThe thing to notice [in the current documentation of the pass](https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes.RemoveIdentityEquivalent) is that the current condition for removing a gate is indeed satisfied for gates of the form $e^{i\\theta} I$. And in fact the opposite is also true (thanks to @ShellyGarion for helping me work out the math): if the removal condition is satisfied, then a gate is necessarily of the form $e^{i\\theta} I$.\n\nLol, yet another change after the #13759 was merged is to now also explicitly handle Unitary gates.\n\nThanks to @Cryoris and @ShellyGarion for joint debugging of the problem. \n<hr>This is an automatic backport of pull request #13785 done by [Mergify](https://mergify.com).

Resolved issues:
title: incorrect statevector simulation using qiskit & qiskit-aer
body: ### Environment\n\n- **Qiskit version**: 1.3.0 (I believe the bug still exists in 1.3.2)\n- **Python version**: 3.12\n- **Operating system**: linux\n\nmirror ticket from https://github.com/Qiskit/qiskit-aer/issues/2271 for visibility\n\n### What is happening?\n\nFor a fixed QuantumCircuit with custom unitary gate matrix, QiskitAer is producing different statevector simulation results for qiskit v.1.2.4 and v.1.3.0\n\n### How can we reproduce the issue?\n\nUsing `qiskit-aer=0.15.1`, run the following script with `qiskit==1.2.4` and `qiskit==1.3.0` will reproduce different statevector results\n```python\nimport qiskit\n\nfrom qiskit_aer import AerSimulator\nfrom qiskit.circuit.library import UnitaryGate\nfrom qiskit.quantum_info import random_unitary\n\nimport numpy as np\n\ndef get_qiskit_unitary_gate(rng, control=None):\n    rand_unitary = random_unitary(4, seed=rng)\n    gate = UnitaryGate(rand_unitary)\n    print(f"unitary gate matrix:")\n    print(gate)\n    if control is None:\n        return gate\n    else:\n        return gate.control(control)\n    \ndef get_qiskit_multi_control_circuit():\n    qubits = qiskit.QuantumRegister(5)\n    circuit = qiskit.QuantumCircuit(qubits)\n    for q in qubits:\n        circuit.h(q)\n    qs = list(qubits)\n\n    rng = np.random.default_rng(1234)\n    for i in range(2):\n        rng.shuffle(qs)\n        ccu_gate = get_qiskit_unitary_gate(rng, control=2)\n        print(f"{i}th gate on {qs}")\n        circuit.append(ccu_gate, qs[:4])\n    circuit.p(0.1, qubits[0])\n    return circuit\n\nprint(f"{qiskit.__version__=}")\n\ncircuit = get_qiskit_multi_control_circuit()\nprint(circuit)\n\ncircuit.save_statevector()\n\n# Transpile for simulator\nsimulator = AerSimulator(method='statevector')\ncirc = qiskit.transpile(circuit, simulator)\n\n# Run and get statevector\nresult = simulator.run(circ).result()\nsv = np.asarray(result.get_statevector(circ))\nprint("Sum of state vector:", sv.sum())\n```\n\n### What should happen?\n\nThe gate matrices defining the custom unitary gates are identical between `qiskit==1.2.4` and `qiskit==1.3.0` and therefore the returned statevector should be the same. On my end I have the following results:\n\n`qiskit==1.2.4`\n```\nqiskit.__version__='1.2.4'\nunitary gate matrix:\nInstruction(name='unitary', num_qubits=2, num_clbits=0, params=[array([[ 0.26093746+0.18318812j,  0.12558674-0.38936694j,\n         0.56486648-0.53659333j, -0.03587912+0.35025885j],\n       [-0.5208327 +0.41955884j,  0.38293134-0.20373037j,\n        -0.26997299-0.01611016j,  0.47100825+0.26375647j],\n       [-0.18047959-0.0114164j ,  0.42182388+0.05310412j,\n         0.01892911-0.48459796j, -0.10334574-0.73530097j],\n       [-0.44557601+0.46884564j, -0.63238151+0.25262043j,\n         0.15882664-0.24378088j, -0.17446941-0.05317653j]])])\n0th gate on [Qubit(QuantumRegister(5, 'q1'), 0), Qubit(QuantumRegister(5, 'q1'), 4), Qubit(QuantumRegister(5, 'q1'), 1), Qubit(QuantumRegister(5, 'q1'), 3), Qubit(QuantumRegister(5, 'q1'), 2)]\nunitary gate matrix:\nInstruction(name='unitary', num_qubits=2, num_clbits=0, params=[array([[-0.41490662-0.1027918j , -0.45229939-0.45892536j,\n         0.1965442 +0.27907236j,  0.53283662+0.04090535j],\n       [-0.54960898-0.38886747j, -0.18060452+0.35532657j,\n        -0.42299907-0.41452735j,  0.00811512+0.19238008j],\n       [-0.48710529+0.16105765j,  0.30802545-0.50934349j,\n         0.16188328-0.37911161j, -0.36497996-0.28166686j],\n       [ 0.16591044+0.27066769j,  0.26600357-0.02948626j,\n         0.10303047-0.5934482j ,  0.6357136 +0.24628756j]])])\n1th gate on [Qubit(QuantumRegister(5, 'q1'), 1), Qubit(QuantumRegister(5, 'q1'), 4), Qubit(QuantumRegister(5, 'q1'), 2), Qubit(QuantumRegister(5, 'q1'), 0), Qubit(QuantumRegister(5, 'q1'), 3)]                      \nSum of state vector: (3.450103311811794-0.308774856832824j)\n```\n\n`qiskit==1.3.0`\n```\nqiskit.__version__='1.3.0'\nunitary gate matrix:\nInstruction(name='unitary', num_qubits=2, num_clbits=0, params=[array([[ 0.26093746+0.18318812j,  0.12558674-0.38936694j,\n         0.56486648-0.53659333j, -0.03587912+0.35025885j],\n       [-0.5208327 +0.41955884j,  0.38293134-0.20373037j,\n        -0.26997299-0.01611016j,  0.47100825+0.26375647j],\n       [-0.18047959-0.0114164j ,  0.42182388+0.05310412j,\n         0.01892911-0.48459796j, -0.10334574-0.73530097j],\n       [-0.44557601+0.46884564j, -0.63238151+0.25262043j,\n         0.15882664-0.24378088j, -0.17446941-0.05317653j]])])\n0th gate on [Qubit(QuantumRegister(5, 'q1'), 0), Qubit(QuantumRegister(5, 'q1'), 4), Qubit(QuantumRegister(5, 'q1'), 1), Qubit(QuantumRegister(5, 'q1'), 3), Qubit(QuantumRegister(5, 'q1'), 2)]\nunitary gate matrix:\nInstruction(name='unitary', num_qubits=2, num_clbits=0, params=[array([[-0.41490662-0.1027918j , -0.45229939-0.45892536j,\n         0.1965442 +0.27907236j,  0.53283662+0.04090535j],\n       [-0.54960898-0.38886747j, -0.18060452+0.35532657j,\n        -0.42299907-0.41452735j,  0.00811512+0.19238008j],\n       [-0.48710529+0.16105765j,  0.30802545-0.50934349j,\n         0.16188328-0.37911161j, -0.36497996-0.28166686j],\n       [ 0.16591044+0.27066769j,  0.26600357-0.02948626j,\n         0.10303047-0.5934482j ,  0.6357136 +0.24628756j]])])\n1th gate on [Qubit(QuantumRegister(5, 'q1'), 1), Qubit(QuantumRegister(5, 'q1'), 4), Qubit(QuantumRegister(5, 'q1'), 2), Qubit(QuantumRegister(5, 'q1'), 0), Qubit(QuantumRegister(5, 'q1'), 3)]                        \nSum of state vector: (2.6579282427026834+2.2212546524499133j)\n```\n\nFrom our experiment, I believe the results from `qiskit=1.2.4` are correct. There might be some parsing issue between `qiskit-aer` and `qiskit==1.3.0`\n\n### Any suggestions?\n\n_No response_

Hints:

Fix patch:
diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs\nindex a3eb921628e2..d5d644919c8f 100644\n--- a/crates/accelerate/src/remove_identity_equiv.rs\n+++ b/crates/accelerate/src/remove_identity_equiv.rs\n@@ -9,7 +9,6 @@\n // Any modifications or derivative works of this code must retain this\n // copyright notice, and modified files need to carry a notice indicating\n // that they have been altered from the originals.\n-\n use num_complex::Complex64;\n use num_complex::ComplexFloat;\n use pyo3::prelude::*;\n@@ -27,11 +26,13 @@ use qiskit_circuit::packed_instruction::PackedInstruction;\n #[pyfunction]\n #[pyo3(signature=(dag, approx_degree=Some(1.0), target=None))]\n fn remove_identity_equiv(\n+    py: Python,\n     dag: &mut DAGCircuit,\n     approx_degree: Option<f64>,\n     target: Option<&Target>,\n ) {\n     let mut remove_list: Vec<NodeIndex> = Vec::new();\n+    let mut global_phase_update: f64 = 0.;\n \n     let get_error_cutoff = |inst: &PackedInstruction| -> f64 {\n         match approx_degree {\n@@ -76,12 +77,17 @@ fn remove_identity_equiv(\n \n     for op_node in dag.op_nodes(false) {\n         let inst = dag.dag()[op_node].unwrap_operation();\n-        match inst.op.view() {\n+        if inst.is_parameterized() {\n+            // Skip parameterized gates\n+            continue;\n+        }\n+        let view = inst.op.view();\n+        match view {\n             OperationRef::Standard(gate) => {\n                 let (dim, trace) = match gate {\n                     StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => {\n                         if let Param::Float(theta) = inst.params_view()[0] {\n-                            let trace = (theta / 2.).cos() * 2.;\n+                            let trace = Complex64::new((theta / 2.).cos() * 2., 0.);\n                             (2., trace)\n                         } else {\n                             continue;\n@@ -92,20 +98,16 @@ fn remove_identity_equiv(\n                     | StandardGate::RZZGate\n                     | StandardGate::RZXGate => {\n                         if let Param::Float(theta) = inst.params_view()[0] {\n-                            let trace = (theta / 2.).cos() * 4.;\n+                            let trace = Complex64::new((theta / 2.).cos() * 4., 0.);\n                             (4., trace)\n                         } else {\n                             continue;\n                         }\n                     }\n                     _ => {\n-                        // Skip global phase gate\n-                        if gate.num_qubits() < 1 {\n-                            continue;\n-                        }\n                         if let Some(matrix) = gate.matrix(inst.params_view()) {\n                             let dim = matrix.shape()[0] as f64;\n-                            let trace = matrix.diag().iter().sum::<Complex64>().abs();\n+                            let trace = matrix.diag().iter().sum::<Complex64>();\n                             (dim, trace)\n                         } else {\n                             continue;\n@@ -113,34 +115,38 @@ fn remove_identity_equiv(\n                     }\n                 };\n                 let error = get_error_cutoff(inst);\n-                let f_pro = (trace / dim).powi(2);\n+                let f_pro = (trace / dim).abs().powi(2);\n                 let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.);\n                 if (1. - gate_fidelity).abs() < error {\n-                    remove_list.push(op_node)\n+                    remove_list.push(op_node);\n+                    global_phase_update += (trace / dim).arg();\n                 }\n             }\n-            OperationRef::Gate(gate) => {\n-                // Skip global phase like gate\n-                if gate.num_qubits() < 1 {\n-                    continue;\n-                }\n-                if let Some(matrix) = gate.matrix(inst.params_view()) {\n+            _ => {\n+                let matrix = view.matrix(inst.params_view());\n+                // If view.matrix() returns None, then there is no matrix and we skip the operation.\n+                if let Some(matrix) = matrix {\n                     let error = get_error_cutoff(inst);\n                     let dim = matrix.shape()[0] as f64;\n                     let trace: Complex64 = matrix.diag().iter().sum();\n                     let f_pro = (trace / dim).abs().powi(2);\n                     let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.);\n                     if (1. - gate_fidelity).abs() < error {\n-                        remove_list.push(op_node)\n+                        remove_list.push(op_node);\n+                        global_phase_update += (trace / dim).arg();\n                     }\n                 }\n             }\n-            _ => continue,\n         }\n     }\n     for node in remove_list {\n         dag.remove_op_node(node);\n     }\n+\n+    if global_phase_update != 0. {\n+        dag.add_global_phase(py, &Param::Float(global_phase_update))\n+            .expect("The global phase is guaranteed to be a float");\n+    }\n }\n \n pub fn remove_identity_equiv_mod(m: &Bound<PyModule>) -> PyResult<()> {\ndiff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py\nindex fbf132d958a2..17445eb5ad2a 100644\n--- a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py\n+++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py\n@@ -23,9 +23,8 @@\n class RemoveIdentityEquivalent(TransformationPass):\n     r"""Remove gates with negligible effects.\n \n-    Removes gates whose effect is close to an identity operation, up to the specified\n-    tolerance. Zero qubit gates such as :class:`.GlobalPhaseGate` are not considered\n-    by this pass.\n+    Removes gates whose effect is close to an identity operation up to a global phase\n+    and up to the specified tolerance. Parameterized gates are not considered by this pass.\n \n     For a cutoff fidelity :math:`f`, this pass removes gates whose average\n     gate fidelity with respect to the identity is below :math:`f`. Concretely,\ndiff --git a/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml b/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml\nnew file mode 100644\nindex 000000000000..3ee4c4accc21\n--- /dev/null\n+++ b/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml\n@@ -0,0 +1,8 @@\n+---\n+fixes:\n+  - |\n+    Fixed a bug in the :class:`.RemoveIdentityEquivalent` transpiler pass, where gates close\n+    to identity up to a global phase were removed from the circuit,\n+    but the global phase of the circuit was not updated. In particular,\n+    :class:`.RemoveIdentityEquivalent` now removes non-parameterized :class:`.GlobalPhaseGate`\n+    gates.\n

Test patch:
diff --git a/test/python/transpiler/test_remove_identity_equivalent.py b/test/python/transpiler/test_remove_identity_equivalent.py\nindex 1db392d3654b..736c02b765bd 100644\n--- a/test/python/transpiler/test_remove_identity_equivalent.py\n+++ b/test/python/transpiler/test_remove_identity_equivalent.py\n@@ -12,6 +12,7 @@\n \n """Tests for the DropNegligible transpiler pass."""\n \n+import ddt\n import numpy as np\n \n from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate\n@@ -26,6 +27,7 @@\n     XXMinusYYGate,\n     XXPlusYYGate,\n     GlobalPhaseGate,\n+    UnitaryGate,\n )\n from qiskit.quantum_info import Operator\n from qiskit.transpiler.passes import RemoveIdentityEquivalent\n@@ -34,6 +36,7 @@\n from test import QiskitTestCase  # pylint: disable=wrong-import-order\n \n \n+@ddt.ddt\n class TestDropNegligible(QiskitTestCase):\n     """Test the DropNegligible pass."""\n \n@@ -173,13 +176,33 @@ def to_matrix(self):\n         expected = QuantumCircuit(3)\n         self.assertEqual(expected, transpiled)\n \n-    def test_global_phase_ignored(self):\n-        """Test that global phase gate isn't considered."""\n+    @ddt.data(\n+        RXGate(0),\n+        RXGate(2 * np.pi),\n+        RYGate(0),\n+        RYGate(2 * np.pi),\n+        RZGate(0),\n+        RZGate(2 * np.pi),\n+        UnitaryGate(np.array([[1, 0], [0, 1]])),\n+        UnitaryGate(np.array([[-1, 0], [0, -1]])),\n+        UnitaryGate(np.array([[np.exp(1j * np.pi / 4), 0], [0, np.exp(1j * np.pi / 4)]])),\n+        GlobalPhaseGate(0),\n+        GlobalPhaseGate(np.pi / 4),\n+    )\n+    def test_remove_identity_up_to_global_phase(self, gate):\n+        """Test that gates equivalent to identity up to a global phase are removed from the circuit,\n+        and the global phase of the circuit is updated correctly.\n+        """\n+        qc = QuantumCircuit(gate.num_qubits)\n+        qc.append(gate, qc.qubits)\n+        transpiled = RemoveIdentityEquivalent()(qc)\n+        self.assertEqual(transpiled.size(), 0)\n+        self.assertEqual(Operator(qc), Operator(transpiled))\n \n+    def test_parameterized_global_phase_ignored(self):\n+        """Test that parameterized global phase gates are not removed by the pass."""\n+        theta = Parameter("theta")\n         qc = QuantumCircuit(1)\n-        qc.id(0)\n-        qc.append(GlobalPhaseGate(0))\n+        qc.append(GlobalPhaseGate(theta), [])\n         transpiled = RemoveIdentityEquivalent()(qc)\n-        expected = QuantumCircuit(1)\n-        expected.append(GlobalPhaseGate(0))\n-        self.assertEqual(transpiled, expected)\n+        self.assertEqual(qc, transpiled)\n