Repository: Qiskit/qiskit — pull request #15131 (python)
Title: Check target gates are parameterized in optimize1qgatesdecomposition

<!--\r\n⚠️  If you do not respect this template, your pull request will be closed.\r\n⚠️  Your pull request title should be short detailed and understandable for all.\r\n⚠️  Also, please add a release note file using reno if the change needs to be documented in the release notes.\r\n⚠️  If your pull request fixes an open issue, please link to the issue. Use "Fixes #XXXX" if this PR *fully* closes the issue XXXX.  \r\n☢️  If you used an AI tool to code this PR, add "AI tool used: <Name and version of the tool>". For example, "AI tool used: Microsoft Copilot Chat with GPT-5". Failing to disclose the use of AI tools may result in the PR being closed without further review.  \r\n\r\n\r\n- [ ] I have added the tests to cover my changes.\r\n- [ ] I have updated the documentation accordingly.\r\n- [ ] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\n\r\nThis commit updates the Optimize1qGatesDecomposition transpiler pass to check the defined operation in the target to ensure it's a valid gate to use for the decomposition. Previously the pass would just use the gate name for each qubit and treat that as a basis gate list. However this ignored the extra details that the target provides as a target gate can match the name but not be valid for use in synthesis. This commit fixes that oversight by checking the gates directly and ensuring they're valid for use in the decomposition.\r\n\r\n### Details and comments\r\n\r\nFixes #14743

Resolved issues:
title: Optimize1qGatesDecomposition doesn't check if 1q gates support arbitrary angles
body: ### Environment\n\n- **Qiskit version**: main but it will be present in 2.1.x and 1.4.x\n- **Python version**:  3.13\n- **Operating system**:  Linux\n\n\n### What is happening?\n\nThe optimize 1q gates decomposition transpiler pass doesn't check the 1q gates support any angle or not. If a single qubit gate is only registered with a fixed angle the pass treats that as a valid target basis for decomposition.\n\n### How can we reproduce the issue?\n\n```python\nfrom qiskit.transpiler import Target\nfrom qiskit.circuit.library import UGate, SXGate, RZGate\nfrom qiskit.circuit import QuantumCircuit, Parameter\nfrom qiskit.transpiler.passes import Optimize1qGatesDecomposition\n\ntarget = Target(num_qubits=1)\ntarget.add_instruction(UGate(3.14, 0, -3.14), {(1,): None})\ntarget.add_instruction(SXGate(), {(1,): None})\ntarget.add_instruction(RZGate(Parameter("t")), {(1,): None})\n\nopt_pass = Optimize1qGatesDecomposition(target)\n\nqc = QuantumCircuit(1)\nqc.u(1, 2, 3, 0)\nprint(opt_pass(qc))\n```\n\n### What should happen?\n\nThis should return a gate in the ZSX basis, but it instead is returning a circuit with `UGate(1, 2, 3)` because it doesn't recognize that `UGate` is only supported in the target with $\\pi$, 0, $-\\pi$ and 1, 2, 3 isn't a valid output for a physical optimization pass.\n\n### Any suggestions?\n\nWe need to update the target check in the pass code to check that the gate supports any angle before trying to use it for synthesis.

Hints:

Fix patch:
diff --git a/crates/transpiler/src/passes/optimize_1q_gates_decomposition.rs b/crates/transpiler/src/passes/optimize_1q_gates_decomposition.rs\nindex c000c8fe9560..adcc17caf9a5 100644\n--- a/crates/transpiler/src/passes/optimize_1q_gates_decomposition.rs\n+++ b/crates/transpiler/src/passes/optimize_1q_gates_decomposition.rs\n@@ -20,9 +20,9 @@ use ndarray::prelude::*;\n use rustworkx_core::petgraph::stable_graph::NodeIndex;\n \n use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType};\n-use qiskit_circuit::operations::{Operation, Param};\n+use qiskit_circuit::operations::{Operation, OperationRef, Param};\n \n-use crate::target::Target;\n+use crate::target::{Target, TargetOperation};\n use qiskit_circuit::{PhysicalQubit, gate_matrix};\n use qiskit_synthesis::euler_one_qubit_decomposer::{\n     EULER_BASES, EULER_BASIS_NAMES, EulerBasis, EulerBasisSet, OneQubitGateSequence,\n@@ -76,7 +76,30 @@ pub fn run_optimize_1q_gates_decomposition(\n         };\n         if basis_gates_per_qubit[qubit.index()].is_none() {\n             let basis_gates = match target {\n-                Some(target) => Some(target.operation_names_for_qargs(&[qubit])?),\n+                Some(target) => Some(\n+                    target\n+                        .operation_names_for_qargs(&[qubit])?\n+                        .into_iter()\n+                        .filter(|gate_name| {\n+                            let target_op = target.operation_from_name(gate_name).unwrap();\n+                            let TargetOperation::Normal(gate) = target_op else {\n+                                return false;\n+                            };\n+                            if let OperationRef::StandardGate(_) = gate.operation.view() {\n+                                // For standard gates check that the target entry accepts any\n+                                // params and if so then we can use the gate in the pass\n+                                // else filter the operation since arbitrary angles are not\n+                                // supported\n+                                gate.params\n+                                    .iter()\n+                                    .all(|x| matches!(x, Param::ParameterExpression(_)))\n+                            } else {\n+                                // For all other gates pass it through\n+                                true\n+                            }\n+                        })\n+                        .collect(),\n+                ),\n                 None => {\n                     let basis = basis_gates.as_ref();\n                     basis.map(|basis| basis.iter().map(|x| x.as_str()).collect())\ndiff --git a/releasenotes/notes/fix-1q-decomp-non-param-gates-99624b82227e5608.yaml b/releasenotes/notes/fix-1q-decomp-non-param-gates-99624b82227e5608.yaml\nnew file mode 100644\nindex 000000000000..70fb67621289\n--- /dev/null\n+++ b/releasenotes/notes/fix-1q-decomp-non-param-gates-99624b82227e5608.yaml\n@@ -0,0 +1,9 @@\n+---\n+fixes:\n+  - |\n+    Fixed an issue in the :class:`.Optimize1qGatesDecomposition` when the\n+    pass was initialized with a :class:`.Target` that contains 1q gates with fixed angle\n+    parameters. Previously, the pass would potentially output gates outside\n+    the target as it wasn't checking that the gate in the target supported\n+    arbitrary parameter values.\n+    Fixed `#14743 <https://github.com/Qiskit/qiskit/issues/14743>`__.\n

Test patch:
diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py\nindex 86bcbc241506..a75db7e16515 100644\n--- a/test/python/transpiler/test_optimize_1q_decomposition.py\n+++ b/test/python/transpiler/test_optimize_1q_decomposition.py\n@@ -782,6 +782,21 @@ def test_unitary_gate_column_major(self):\n         expected.u(0, 0, np.pi / 2, [0])\n         self.assertEqual(result, expected)\n \n+    def test_target_fixed_angle_1q_gate(self):\n+        """Test that a fixed angle gate is not used for decomposition."""\n+        target = Target(num_qubits=1)\n+        target.add_instruction(UGate(3.14, 0, -3.14), {(0,): None})\n+        target.add_instruction(SXGate(), {(0,): None})\n+        target.add_instruction(RZGate(Parameter("t")), {(0,): None})\n+\n+        opt_pass = Optimize1qGatesDecomposition(target=target)\n+        qc = QuantumCircuit(1)\n+        qc.u(1, 2, 3, 0)\n+        res = opt_pass(qc)\n+        self.assertNotIn("u", res.count_ops())\n+        self.assertIn("rz", res.count_ops())\n+        self.assertIn("sx", res.count_ops())\n+\n \n if __name__ == "__main__":\n     unittest.main()\n