Repository: Qiskit/qiskit — pull request #14092 (python)
Title: Fix global phase update in `BasisTranslator` Pass (backport #14078)

\n\n### Summary\n\nFixes #14074. \n\nThe `basis_translator` pass was not always updating the DAG's global phase as a part of  `replace_node`: this was only done when the target dag's global phase matched ``Param::ParameterExpression``. Updating the global phase in the case of ``Param::Float`` was missing, and this is fixed now. \n\nI am somewhat unsure if we also need to handle the ``Param::Obj`` case. I don't understand in what kind of a circuit we would see this, and I don't believe it's even supported, as per ``dag_circuit.rs.add_global_phase``, which throws an error in the case of ``Param::Obj``. Update: @jakelishman confirmed that ``Param::Obj`` is never permitted in ``global_phase``.\n\nUpdate: added release notes.\n<hr>This is an automatic backport of pull request #14078 done by [Mergify](https://mergify.com).

Resolved issues:
title: Transplie Rx introduce an incorrect global phase.
body: ### Environment\n\n- **Qiskit version**: 1.4.2 \n- **Python version**: 2.9.0\n- **Operating system**: linux\n\n\n### What is happening?\n\nRx(theta) = H Rz(theta) H \n\nThis conversion theoretically does not generate any global phase. However, the transpiler from qiskit will generate an extra global phase pi here.\n\n### How can we reproduce the issue?\n\n```\nfrom qiskit import QuantumCircuit, transpile\nimport numpy as np\ntheta = 0.5*np.pi\ncircuit1 = QuantumCircuit(1)\ncircuit1.rx(theta, 0)\ncircuit2 = transpile(circuit1, basis_gates=['h','rz' ])\nglobal_phase = circuit2.global_phase-circuit1.global_phase\nprint(global_phase)\ncircuit2.draw('mpl')\n```\noutput: \n\n3.141592653589793\n\n![Image](https://github.com/user-attachments/assets/7a2d6cea-c838-4560-811f-299c3c73ff9c)\n\n### What should happen?\n\nDuring the transformation of Rx gate, the global phase may be added incorrectly, resulting in an error in the final result.\n\n### Any suggestions?\n\n_No response_

Hints:

Fix patch:
diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs\nindex d47fc5e04022..1bc066b988c6 100644\n--- a/crates/accelerate/src/basis/basis_translator/mod.rs\n+++ b/crates/accelerate/src/basis/basis_translator/mod.rs\n@@ -762,38 +762,46 @@ fn replace_node(\n             )?;\n         }\n \n-        if let Param::ParameterExpression(old_phase) = target_dag.global_phase() {\n-            let bound_old_phase = old_phase.bind(py);\n-            let bind_dict = PyDict::new(py);\n-            for key in target_dag.global_phase().iter_parameters(py)? {\n-                let key = key?;\n-                bind_dict.set_item(&key, parameter_map.get_item(&key)?)?;\n-            }\n-            let mut new_phase: Bound<PyAny>;\n-            if bind_dict.values().iter().any(|param| {\n-                param\n-                    .is_instance(PARAMETER_EXPRESSION.get_bound(py))\n-                    .is_ok_and(|x| x)\n-            }) {\n-                new_phase = bound_old_phase.clone();\n-                for key_val in bind_dict.items() {\n-                    new_phase =\n-                        new_phase.call_method1(intern!(py, "assign"), key_val.downcast()?)?;\n+        match target_dag.global_phase() {\n+            Param::ParameterExpression(old_phase) => {\n+                let bound_old_phase = old_phase.bind(py);\n+                let bind_dict = PyDict::new(py);\n+                for key in target_dag.global_phase().iter_parameters(py)? {\n+                    let key = key?;\n+                    bind_dict.set_item(&key, parameter_map.get_item(&key)?)?;\n                 }\n-            } else {\n-                new_phase = bound_old_phase.call_method1(intern!(py, "bind"), (bind_dict,))?;\n-            }\n-            if !new_phase.getattr(intern!(py, "parameters"))?.is_truthy()? {\n-                new_phase = new_phase.call_method0(intern!(py, "numeric"))?;\n-                if new_phase.is_instance(&PyComplex::type_object(py))? {\n-                    return Err(TranspilerError::new_err(format!(\n-                        "Global phase must be real, but got {}",\n-                        new_phase.repr()?\n-                    )));\n+                let mut new_phase: Bound<PyAny>;\n+                if bind_dict.values().iter().any(|param| {\n+                    param\n+                        .is_instance(PARAMETER_EXPRESSION.get_bound(py))\n+                        .is_ok_and(|x| x)\n+                }) {\n+                    new_phase = bound_old_phase.clone();\n+                    for key_val in bind_dict.items() {\n+                        new_phase =\n+                            new_phase.call_method1(intern!(py, "assign"), key_val.downcast()?)?;\n+                    }\n+                } else {\n+                    new_phase = bound_old_phase.call_method1(intern!(py, "bind"), (bind_dict,))?;\n                 }\n+                if !new_phase.getattr(intern!(py, "parameters"))?.is_truthy()? {\n+                    new_phase = new_phase.call_method0(intern!(py, "numeric"))?;\n+                    if new_phase.is_instance(&PyComplex::type_object(py))? {\n+                        return Err(TranspilerError::new_err(format!(\n+                            "Global phase must be real, but got {}",\n+                            new_phase.repr()?\n+                        )));\n+                    }\n+                }\n+                let new_phase: Param = new_phase.extract()?;\n+                dag.add_global_phase(py, &new_phase)?;\n             }\n-            let new_phase: Param = new_phase.extract()?;\n-            dag.add_global_phase(py, &new_phase)?;\n+\n+            Param::Float(_) => {\n+                dag.add_global_phase(py, target_dag.global_phase())?;\n+            }\n+\n+            _ => {}\n         }\n     }\n \ndiff --git a/releasenotes/notes/fix-global-phase-in-basis-translator-d08d665198589828.yaml b/releasenotes/notes/fix-global-phase-in-basis-translator-d08d665198589828.yaml\nnew file mode 100644\nindex 000000000000..f73afd91e79e\n--- /dev/null\n+++ b/releasenotes/notes/fix-global-phase-in-basis-translator-d08d665198589828.yaml\n@@ -0,0 +1,6 @@\n+---\n+fixes:\n+  - |\n+    Fixed a problem in :class:`.BasisTranslator` transpiler pass, where the global\n+    phase of the DAG was not updated correctly. \n+    Fixed `#14074 <https://github.com/Qiskit/qiskit/issues/14074>`__.\n

Test patch:
diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py\nindex a03765778a3f..751073deb28c 100644\n--- a/test/python/transpiler/test_basis_translator.py\n+++ b/test/python/transpiler/test_basis_translator.py\n@@ -1008,6 +1008,15 @@ def test_global_phase(self):\n         )\n         self.assertEqual(Operator(dag_to_circuit(out_dag)), Operator(expected))\n \n+    def test_rx_to_rz(self):\n+        """Verify global phase is updated correctly in basis translation.\n+        See https://github.com/Qiskit/qiskit/issues/14074."""\n+        theta = 0.5 * pi\n+        circ = QuantumCircuit(1)\n+        circ.rx(theta, 0)\n+        out_circ = BasisTranslator(std_eqlib, ["h", "rz"])(circ)\n+        self.assertEqual(Operator(circ), Operator(out_circ))\n+\n     def test_skip_target_basis_equivalences_1(self):\n         """Test that BasisTranslator skips gates in the target_basis - #6085"""\n         circ = QuantumCircuit()\n