Fix `ElidePermutations` pass in the presence of `PermutationGate`s (backport #14603) (Qiskit/qiskit#14623)

\n\n### Summary\n\nFixes #14602.\n\n### Details and comments\n\nThe `ElidePermutations` transpiler pass removes swap gates and permutation gates from the circuit, tracking the current permutation of the qubits using the internal variable ``mapping``. Previously, this mapping was not updated correctly in the presence of permutation gates, leading to incorrect output circuits and updates to the pass manager's property set (namely, to ``"virtual_permutation_layout"``).\n\nThe correct update rule should be $$M [ Q [i] ] \\leftarrow M [Q [ P[i] ] ]$$ for $i=1,\\dots,k$, where $M$ is the current mapping within the `ElidePermutations` pass, $Q$ is the set of qubits the permutation gate is defined on, and $P$ is the "permutation pattern" of the permutation gate (stating which qubits get mapped to positions $0$, $1$, etc. when the permutation gate is applied). <hr>This is an automatic backport of pull request #14603 done by [Mergify](https://mergify.com).

ElidePermutations does not work correctly with PermutationGates

### Environment\n\n- **Qiskit version**: 2.0+\n- **Python version**: \n- **Operating system**: \n\n\n### What is happening?\n\nThe Rust version of the `ElidePermutations` transpiler pass does not work correctly when the circuit contains `PermutationGate`s.\n\n### How can we reproduce the issue?\n\nConsider the following code snippet:\n\n```\nfrom qiskit.circuit import QuantumCircuit\nfrom qiskit.transpiler import PassManager\nfrom qiskit.transpiler.passes import ElidePermutations\n\nqc = QuantumCircuit(5)\nqc.h(1)\nqc.swap(1, 2)\n\npm = PassManager([ElidePermutations()])\nres = pm.run(qc)\nprint(res.layout.final_index_layout())\n```\nThe `ElidePermutations` transpiler pass removes the swap gate and instead updates the ``"final_layout"`` property of the pass manager. Printing the ``final_index_layout`` outputs ``[0, 2, 1, 3, 4]``, which is correct.\n\nNow, let's modify the code snippet as follows:\n\n```\nfrom qiskit.circuit import QuantumCircuit\nfrom qiskit.transpiler import PassManager\nfrom qiskit.transpiler.passes import ElidePermutations\nfrom qiskit.circuit.library import PermutationGate\n\nqc = QuantumCircuit(5)\nqc.h(1)\nqc.swap(1, 2)\nqc.append(PermutationGate([0, 1, 2]), [1, 2, 3])\n\npm = PassManager([ElidePermutations()])\nres = pm.run(qc)\nprint(res.layout.final_index_layout())\n```\nNote that the permutation gate does not do anything -- it's the identity permutation of 3 qubits. However,  the ``final_index_layout`` is now ``[0, 1, 2, 3, 4]``.\n\nThe core of the problem is that the ``mapping`` in ``elide_permutations.rs`` is not updated correctly in the presence of permutation gates. Also note that the problem is worse than just an incorrect final layout: as soon as the ``mapping`` becomes incorrect, the following gates are not placed correctly by the pass.\n\n\n### What should happen?\n\nThe ElidePermutations pass should work correctly.\n\n### Any suggestions?\n\nI am going to submit a PR fixing this problem shortly.

diff --git a/crates/transpiler/src/passes/elide_permutations.rs b/crates/transpiler/src/passes/elide_permutations.rs\nindex 0db7e4ec45bf..59fa92a542b2 100644\n--- a/crates/transpiler/src/passes/elide_permutations.rs\n+++ b/crates/transpiler/src/passes/elide_permutations.rs\n@@ -63,17 +63,13 @@ pub fn run_elide_permutations(\n                             .map(|q| q.index())\n                             .collect();\n \n-                        let remapped_qindices: Vec<usize> = (0..qindices.len())\n-                            .map(|i| pattern[i])\n-                            .map(|i| qindices[i as usize])\n+                        let new_values: Vec<usize> = (0..qindices.len())\n+                            .map(|i| mapping[qindices[pattern[i] as usize]])\n                             .collect();\n \n-                        qindices\n-                            .iter()\n-                            .zip(remapped_qindices.iter())\n-                            .for_each(|(old, new)| {\n-                                mapping[*old] = *new;\n-                            });\n+                        for i in 0..qindices.len() {\n+                            mapping[qindices[i]] = new_values[i];\n+                        }\n                     } else {\n                         unreachable!();\n                     }\ndiff --git a/releasenotes/notes/fix-elide-permutation-gates-e4d2a718e1d50afa.yaml b/releasenotes/notes/fix-elide-permutation-gates-e4d2a718e1d50afa.yaml\nnew file mode 100644\nindex 000000000000..1a1f1282fe6e\n--- /dev/null\n+++ b/releasenotes/notes/fix-elide-permutation-gates-e4d2a718e1d50afa.yaml\n@@ -0,0 +1,7 @@\n+---\n+fixes:\n+  - |\n+    Fixed a bug in the :class:`.ElidePermutation` transpiler pass, where\n+    the qubit mapping was not updated correctly in the presence of\n+    :class:`.PermutationGate`\\s, leading to incorrect circuits and\n+    updates to the pass manager's property set.\n

diff --git a/test/python/transpiler/test_elide_permutations.py b/test/python/transpiler/test_elide_permutations.py\nindex dd59903db5ef..224fdd37cf03 100644\n--- a/test/python/transpiler/test_elide_permutations.py\n+++ b/test/python/transpiler/test_elide_permutations.py\n@@ -12,6 +12,7 @@\n \n """Test ElidePermutations pass"""\n \n+import itertools\n import unittest\n \n from qiskit.circuit.quantumcircuit import QuantumCircuit\n@@ -20,6 +21,7 @@\n from qiskit.transpiler.passes.routing import StarPreRouting\n from qiskit.circuit.controlflow import IfElseOp\n from qiskit.quantum_info import Operator\n+from qiskit.transpiler.passmanager import PassManager\n from qiskit.transpiler.coupling import CouplingMap\n from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n from test import QiskitTestCase  # pylint: disable=wrong-import-order\n@@ -366,6 +368,20 @@ def test_unitary_equivalence(self):\n             res = spm.run(qc)\n             self.assertTrue(Operator.from_circuit(res).equiv(Operator(qc)))\n \n+    def test_unitary_equivalence_permutation_gates(self):\n+        """Test unitary equivalence of the original and transpiled circuits."""\n+\n+        for perm in itertools.permutations([0, 1, 2]):\n+            qc = QuantumCircuit(5)\n+            qc.h(1)\n+            qc.swap(1, 2)\n+            qc.swap(4, 3)\n+            qc.append(PermutationGate(perm), [1, 2, 3])\n+\n+            pm = PassManager([ElidePermutations()])\n+            res = pm.run(qc)\n+            self.assertEqual(Operator.from_circuit(res), (Operator(qc)))\n+\n     def test_unitary_equivalence_routing_and_basis_translation(self):\n         """Test on a larger example that includes routing and basis translation."""\n \n


Fixes for dimacs-related boolean expression code (Qiskit/qiskit#14636)

<!--\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\r\n  documented in the release notes.\r\n⚠ If your pull request fixes an open issue, please link to the issue.\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\nThis PR fixes a bug in the code for creating phase and bit-flip oracles from DIMACS files.\r\n\r\nFixes #14634\r\n\r\n\r\n### Details and comments\r\nCurrently, oracles are generated from DIMACS files by using `BooleanExpression.from_dimacs` to generate an expression and pass it to the init of the oracle. However, the init currently expects a string representing a boolean expression, not the expression itself.\r\n\r\nThis PR changes the behavior such that the oracles can be initialized from a `BooleanExpression` object, and slightly improves DIMACS parsing.\r\n\r\n

`PhaseOracle` fails to instantiate from a Dimacs file

### Environment\n\n- **Qiskit version**: 2.0.2 (can't reproduce with 1.4.1)\n- **Python version**: 3.13.3\n- **Operating system**: Arch Linux\n\n\n### What is happening?\n\nTrying to instantiate a `PhaseOracle` using a Dimacs file via the `PhaseOracle.from_dimacs_file` method results in a `TypeError`.\n\n### How can we reproduce the issue?\n\n```python\nimport os\nimport tempfile\nfrom qiskit.circuit.library.phase_oracle import PhaseOracle\n\ninput_3sat_instance = """\nc DIMACS CNF file with 3 satisfying assignments: 1 -2 3, -1 -2 -3, 1 2 -3.\np cnf 3 5\n-1 -2 -3 0\n1 -2 3 0\n1 2 -3 0\n1 -2 -3 0\n-1 2 3 0\n"""\n\nwith open("tmpfile", 'w') as f:\n    f.write(input_3sat_instance)\n\noracle = PhaseOracle.from_dimacs_file("tmpfile")\n```\n\n### What should happen?\n\nThe `PhaseOracle` should be instantiated without error.\n\n### Any suggestions?\n\n_No response_

diff --git a/qiskit/circuit/library/bit_flip_oracle.py b/qiskit/circuit/library/bit_flip_oracle.py\nindex fbd45c11a4c2..c342a6e27ed1 100644\n--- a/qiskit/circuit/library/bit_flip_oracle.py\n+++ b/qiskit/circuit/library/bit_flip_oracle.py\n@@ -52,23 +52,27 @@ class BitFlipOracleGate(Gate):\n \n     def __init__(\n         self,\n-        expression: str,\n+        expression: str | BooleanExpression,\n         var_order: list[str] | None = None,\n         label: str | None = None,\n     ) -> None:\n         """\n         Args:\n-            expression: A Python-like boolean expression.\n+            expression: A Python-like boolean expression string or a `BooleanExpression` object.\n             var_order: A list with the order in which variables will be created.\n                (default: by appearance)\n             label: A label for the gate to display in visualizations. Per default, the label is\n                 set to display the textual represntation of the boolean expression (truncated if needed)\n         """\n-        self.boolean_expression = BooleanExpression(expression, var_order=var_order)\n-\n         if label is None:\n-            short_expr_for_name = (expression[:15] + "...") if len(expression) > 15 else expression\n-            label = short_expr_for_name\n+            if isinstance(expression, str):\n+                label = (expression[:15] + "...") if len(expression) > 15 else expression\n+            else:\n+                label = "Boolean Expression"\n+\n+        if isinstance(expression, str):\n+            expression = BooleanExpression(expression, var_order=var_order)\n+        self.boolean_expression = expression\n \n         super().__init__(\n             name="Bit-flip Oracle",\ndiff --git a/qiskit/circuit/library/phase_oracle.py b/qiskit/circuit/library/phase_oracle.py\nindex 4b687976b296..96ccc58a798e 100644\n--- a/qiskit/circuit/library/phase_oracle.py\n+++ b/qiskit/circuit/library/phase_oracle.py\n@@ -57,17 +57,19 @@ class PhaseOracle(QuantumCircuit):\n     )\n     def __init__(\n         self,\n-        expression: str,\n+        expression: str | BooleanExpression,\n         var_order: list[str] | None = None,\n     ) -> None:\n         """\n         Args:\n-            expression: A Python-like boolean expression.\n+            expression: A Python-like boolean expression string or a `BooleanExpression` object.\n             var_order: A list with the order in which variables will be created.\n                (default: by appearance)\n         """\n \n-        self.boolean_expression = BooleanExpression(expression, var_order=var_order)\n+        if isinstance(expression, str):\n+            expression = BooleanExpression(expression, var_order=var_order)\n+        self.boolean_expression = expression\n         oracle = self.boolean_expression.synth(circuit_type="phase")\n \n         super().__init__(oracle.num_qubits, name="Phase Oracle")\n@@ -168,23 +170,27 @@ class PhaseOracleGate(Gate):\n \n     def __init__(\n         self,\n-        expression: str,\n+        expression: str | BooleanExpression,\n         var_order: list[str] | None = None,\n         label: str | None = None,\n     ) -> None:\n         """\n         Args:\n-            expression: A Python-like boolean expression.\n+            expression: A Python-like boolean expression string or a `BooleanExpression` object.\n             var_order: A list with the order in which variables will be created.\n                (default: by appearance)\n             label: A label for the gate to display in visualizations. Per default, the label is\n                 set to display the textual represntation of the boolean expression (truncated if needed)\n         """\n-        self.boolean_expression = BooleanExpression(expression, var_order=var_order)\n-\n         if label is None:\n-            short_expr_for_name = (expression[:15] + "...") if len(expression) > 15 else expression\n-            label = short_expr_for_name\n+            if isinstance(expression, str):\n+                label = (expression[:15] + "...") if len(expression) > 15 else expression\n+            else:\n+                label = "Boolean Expression"\n+\n+        if isinstance(expression, str):\n+            expression = BooleanExpression(expression, var_order=var_order)\n+        self.boolean_expression = expression\n \n         super().__init__(\n             name="Phase Oracle",\ndiff --git a/qiskit/synthesis/boolean/boolean_expression.py b/qiskit/synthesis/boolean/boolean_expression.py\nindex 5122dbf9637d..1aaa348d16e8 100644\n--- a/qiskit/synthesis/boolean/boolean_expression.py\n+++ b/qiskit/synthesis/boolean/boolean_expression.py\n@@ -191,8 +191,9 @@ def from_dimacs(dimacs: str):\n         """\n         header_regex = re.compile(r"p\\s+cnf\\s+(\\d+)\\s+(\\d+)")\n         clause_regex = re.compile(r"(-?\\d+)")\n+        lines = [line.strip() for line in dimacs.split("\\n")]\n         lines = [\n-            line for line in dimacs.split("\\n") if not line.startswith("c") and line != ""\n+            line for line in lines if not line.startswith("c") and line != ""\n         ]  # DIMACS comment line start with c\n         header_match = header_regex.match(lines[0])\n         if not header_match:\ndiff --git a/releasenotes/notes/fix-dimacs-load-c48f4d08062ade74.yaml b/releasenotes/notes/fix-dimacs-load-c48f4d08062ade74.yaml\nnew file mode 100644\nindex 000000000000..98a708d5f0c1\n--- /dev/null\n+++ b/releasenotes/notes/fix-dimacs-load-c48f4d08062ade74.yaml\n@@ -0,0 +1,5 @@\n+---\n+fixes:\n+  - |\n+    Fixed a bug in :class:`.PhaseOracle`, :class:`.PhaseOracleGate` and :class:`.BitFlipOracleGate`\n+    where trying to load from dimacs file raised a TypeError.\n\\ No newline at end of file\n

diff --git a/test/python/circuit/library/test_phase_and_bitflip_oracles.py b/test/python/circuit/library/test_phase_and_bitflip_oracles.py\nindex ca67c6d232f3..71e209124c8d 100644\n--- a/test/python/circuit/library/test_phase_and_bitflip_oracles.py\n+++ b/test/python/circuit/library/test_phase_and_bitflip_oracles.py\n@@ -13,6 +13,7 @@\n """Test the phase and bit-flip oracle circuits."""\n \n import unittest\n+import tempfile\n from ddt import ddt, data, unpack\n from numpy import sqrt, isclose\n \n@@ -109,6 +110,28 @@ def test_variable_order(self, expression, var_order, good_states):\n                 self.assertListEqual(expected_valid, result_valid)\n                 self.assertListEqual(expected_invalid, result_invalid)\n \n+    def test_from_dimacs_file(self):\n+        """Initializing from DIMACS file"""\n+        input_3sat_instance = """\n+        c DIMACS CNF file with 3 satisfying assignments: 1 -2 3, -1 -2 -3, 1 2 -3.\n+        p cnf 3 5\n+        -1 -2 -3 0\n+        1 -2 3 0\n+        1 2 -3 0\n+        1 -2 -3 0\n+        -1 2 3 0\n+        """\n+        filename = tempfile.mkstemp(suffix=".dimacs")[1]\n+        with open(filename, "w") as file:\n+            file.write(input_3sat_instance)\n+        for use_gate in [True, False]:\n+            if use_gate:\n+                oracle = PhaseOracleGate.from_dimacs_file(filename)\n+            else:\n+                with self.assertWarns(DeprecationWarning):\n+                    oracle = PhaseOracle.from_dimacs_file(filename)\n+            self.assertEqual(oracle.num_qubits, 3)\n+\n \n @ddt\n class TestBitFlipOracleGate(QiskitTestCase):\n@@ -180,6 +203,23 @@ def test_variable_order(self, expression, var_order, truth_table):\n         self.assertListEqual(expected_valid, result_valid)\n         self.assertListEqual(expected_invalid, result_invalid)\n \n+    def test_from_dimacs_file(self):\n+        """Initializing from DIMACS file"""\n+        input_3sat_instance = """\n+        c DIMACS CNF file with 3 satisfying assignments: 1 -2 3, -1 -2 -3, 1 2 -3.\n+        p cnf 3 5\n+        -1 -2 -3 0\n+        1 -2 3 0\n+        1 2 -3 0\n+        1 -2 -3 0\n+        -1 2 3 0\n+        """\n+        filename = tempfile.mkstemp(suffix=".dimacs")[1]\n+        with open(filename, "w") as file:\n+            file.write(input_3sat_instance)\n+        oracle = BitFlipOracleGate.from_dimacs_file(filename)\n+        self.assertEqual(oracle.num_qubits, 4)\n+\n \n if __name__ == "__main__":\n     unittest.main()\ndiff --git a/test/python/synthesis/test_boolean.py b/test/python/synthesis/test_boolean.py\nindex 511e95e9b0e6..f84ad8f7da10 100644\n--- a/test/python/synthesis/test_boolean.py\n+++ b/test/python/synthesis/test_boolean.py\n@@ -169,10 +169,6 @@ def test_quinn(self):\n     def test_bad_formatting(self):\n         """Tests DIMACS parsing on edge cases"""\n         # pylint: disable=trailing-whitespace\n-        dimacs = """ \n-p cnf 10 5"""  # first line is not p cnf nor empty nor comment (it has whitespace)\n-        with self.assertRaisesRegex(ValueError, "First line must start with 'p cnf'"):\n-            exp = BooleanExpression.from_dimacs(dimacs)\n         dimacs = """p cnf 2 1\n          \n         1 2 0"""  # has empty line with whitespace - should ignore it\n


Fix SparsePauliOp.is_unitary() to respect input tolerance values (Qiskit/qiskit#14676)

Fixes #14107\r\n\r\n### Summary\r\n\r\nThis PR fixes an issue where `SparsePauliOp.is_unitary()` was not properly respecting the input tolerance values when checking if an operator is unitary. The method was not passing the tolerance parameters to the internal [simplify()] call.\r\n\r\n### Details and comments\r\n\r\nThe main changes in this PR are:\r\n\r\n1. Modified `SparsePauliOp.is_unitary()` to pass the atol and rtol parameters to the [simplify()] method\r\n2. Added a test case that verifies the fix works as expected by:\r\n   - Creating a matrix that's approximately unitary but not exactly\r\n   - Verifying it's considered unitary with appropriate tolerances\r\n   - Verifying it's not considered unitary with very tight tolerances\r\n\r\n### Testing\r\n- [x] I have added the tests to cover my changes.\r\n- [x] All tests pass locally with my changes\r\n- [x] I have read the CONTRIBUTING document.\r\n\r\n### Release Notes\r\n\r\nFixed an issue where `SparsePauliOp.is_unitary()` was not respecting the input tolerance values when checking if an operator is unitary.

SparsePauliOp.is_unitary() doesn't respect the input tolerance values

### Environment\n\n- **Qiskit version**: the main branch\n- **Python version**: -\n- **Operating system**: - \n\n\n### What is happening?\n\nThe comment of the method said the tolerance is used to check if the values are zero, but the implementation actually only used the tolerance to check if the coefficient of the identity operator is one.\n\nThe tolerance values should be propagated to SparsePauliOp.simplify() to set the coefficients smaller than the tolerance to zero.\n\nhttps://github.com/Qiskit/qiskit/blob/6ce77887008c181f74306887ec494fedd2911a56/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py#L433-L459\n\n### How can we reproduce the issue?\n\n```python\nimport numpy as np\n\na = np.array(  #\n    [[-9.9801135e-01 + 6.3036762e-02j, 5.6710692e-06 + 8.1099635e-05j],\n     [5.6710610e-06 + 8.1099643e-05j, -9.9707150e-01 + 7.6473624e-02j]])\nprint(a @ (a.conj().T))\n# output:\n# [[ 1.00000029e+00+0.00000000e+00j -2.83078694e-12+2.75849112e-11j]\n#  [-2.83078694e-12-2.75849112e-11j  9.99999798e-01+0.00000000e+00j]]\npauli_operators = quantum_info.SparsePauliOp.from_operator(a)\nprint(pauli_operators.compose(pauli_operators.adjoint()).simplify())\n# output:\n# SparsePauliOp(['I', 'Z'],\n#               coeffs=[1.00000004e+00+3.95107524e-18j, 2.48406162e-07+0.00000000e+00j])\nprint(pauli_operators.is_unitary(atol=1e-5, rtol=1e-3))\n# output: False\n# expect: True\n```\n\n### What should happen?\n\nSee the how to reproduce section.\n\n### Any suggestions?\n\nChange \n\n```python\n val = self.compose(self.adjoint()).simplify() \n```\n\nto \n\n```python\n val = self.compose(self.adjoint()).simplify(atol=atol, rtol=rtol)\n```

diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py\nindex 6c0ce6608267..91ab65fdf6f6 100644\n--- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py\n+++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py\n@@ -433,10 +433,14 @@ def _multiply(self, other):\n     def is_unitary(self, atol: float | None = None, rtol: float | None = None) -> bool:\n         """Return True if operator is a unitary matrix.\n \n+        This method checks whether the operator composed with its adjoint equals\n+        the identity, up to the provided tolerance. The tolerance is used when\n+        simplifying the composed operator and checking if the result is the identity.\n+\n         Args:\n             atol (float): Optional. Absolute tolerance for checking if\n                           coefficients are zero (Default: 1e-8).\n-            rtol (float): Optional. relative tolerance for checking if\n+            rtol (float): Optional. Relative tolerance for checking if\n                           coefficients are zero (Default: 1e-5).\n \n         Returns:\n@@ -449,7 +453,7 @@ def is_unitary(self, atol: float | None = None, rtol: float | None = None) -> bo\n             rtol = self.rtol\n \n         # Compose with adjoint\n-        val = self.compose(self.adjoint()).simplify()\n+        val = self.compose(self.adjoint()).simplify(atol=atol, rtol=rtol)\n         # See if the result is an identity\n         return (\n             val.size == 1\ndiff --git a/releasenotes/notes/fix-sparse-pauli-op-is-unitary-tolerance-28249b2d55f38284.yaml b/releasenotes/notes/fix-sparse-pauli-op-is-unitary-tolerance-28249b2d55f38284.yaml\nnew file mode 100644\nindex 000000000000..c899bb5cbdc0\n--- /dev/null\n+++ b/releasenotes/notes/fix-sparse-pauli-op-is-unitary-tolerance-28249b2d55f38284.yaml\n@@ -0,0 +1,7 @@\n+fixes:\n+  - |\n+    Fixed an issue where :meth:`~qiskit.quantum_info.SparsePauliOp.is_unitary`\n+    was not properly respecting the input tolerance values when checking if an\n+    operator is unitary. The method now correctly uses the provided ``atol`` and\n+    ``rtol`` parameters when simplifying the operator and checking if it equals\n+    the identity. This fixes `#14107 <https://github.com/Qiskit/qiskit/issues/14107>`__.\n

diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py\nindex 075313fd2d20..6ff669f7d79d 100644\n--- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py\n+++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py\n@@ -1357,6 +1357,39 @@ def test_simplify_sum_below_tolerance(self):\n         res = op.simplify(atol=1e-7)\n         self.assertEqual(SparsePauliOp(["II"], [0j]), res)\n \n+    def test_is_unitary_tolerance(self):\n+        """Test that is_unitary respects the input tolerance values.\n+\n+        This test verifies that the tolerance parameters are properly passed through\n+        to the internal operations. See issue #14107.\n+        """\n+        # Create a matrix that's approximately unitary but not exactly\n+        a = np.array(\n+            [\n+                [-9.9801135e-01 + 6.3036762e-02j, 5.6710692e-06 + 8.1099635e-05j],\n+                [5.6710610e-06 + 8.1099643e-05j, -9.9707150e-01 + 7.6473624e-02j],\n+            ]\n+        )\n+\n+        # Verify the matrix is approximately unitary\n+        identity = a @ a.conj().T\n+        np.testing.assert_allclose(identity, np.eye(2), atol=1e-5, rtol=1e-3)\n+\n+        # Create SparsePauliOp from the matrix\n+        op = SparsePauliOp.from_operator(a)\n+\n+        # Test with tolerance that should pass\n+        self.assertTrue(\n+            op.is_unitary(atol=1e-5, rtol=1e-3),\n+            "Operator should be considered unitary with given tolerance",\n+        )\n+\n+        # Test with very small tolerance that should fail\n+        self.assertFalse(\n+            op.is_unitary(atol=1e-10, rtol=1e-10),\n+            "Operator should not be considered unitary with very small tolerance",\n+        )\n+\n \n if __name__ == "__main__":\n     unittest.main()\n


ObservablesArray fixes: stop over-identification as non-Hermitian, raise for empty observables (Qiskit/qiskit#14714)

### Summary\r\n\r\nFix #14567.\r\nFix #14614.\r\n\r\n\r\n### Details and comments\r\n\r\nTo the reviewer: please pay extra care to review the decisions (see details in the issues):\r\n- Empty observables are not allowed in `ObservablesArray`.\r\n- An extra call to `SparseObservable.simplify` in `ObservablesArray.coerce`..\r\n\r\n

`EstimatorPub.validate` throws a `StopIteration` when called with a `SparsePauliOp` having nil coefficients

### Environment\n\n- **Qiskit version**: 2.2.0 (Couldn't replicate with 1.4.3 or 2.0.2)\n- **Python version**: 3.13.3\n- **Operating system**: Arch Linux\n\n\n### What is happening?\n\nOk, this one's kinda fun. When passing a `SparsePauliOp` having nil coefficients to `EstimatorPub.validate`, it throws a `StopIteration`. If you wonder why on earth would you use such operators, they may come up in the computations of some gradients.\n\n### How can we reproduce the issue?\n\n```python\nfrom qiskit import QuantumCircuit\nfrom qiskit.quantum_info import SparsePauliOp\nfrom qiskit.primitives import StatevectorEstimator\n\nop = SparsePauliOp(["Z"], [0.])\nqc = QuantumCircuit(1)\nStatevectorEstimator().run([(qc, op)])\n```\n\n### What should happen?\n\nIt should be possible to run jobs on operators with nil coefficients.\n\n### Any suggestions?\n\n_No response_

diff --git a/qiskit/primitives/containers/observables_array.py b/qiskit/primitives/containers/observables_array.py\nindex bc2092b321f3..6a9b05148050 100644\n--- a/qiskit/primitives/containers/observables_array.py\n+++ b/qiskit/primitives/containers/observables_array.py\n@@ -106,7 +106,7 @@ def __init__(\n \n     @staticmethod\n     def _obs_to_dict(obs: SparseObservable) -> Mapping[str, float]:\n-        """Convert a sparse observable to a mapping from Pauli strings to coefficients."""\n+        """Convert a simplified sparse observable to a mapping from Pauli strings to coefficients."""\n         result = {}\n         for sparse_pauli_str, pauli_qubits, coeff in obs.to_sparse_list():\n \n@@ -260,7 +260,7 @@ def coerce_observable(cls, observable: ObservableLike) -> SparseObservable:\n \n         Raises:\n             TypeError: If the input cannot be formatted because its type is not valid.\n-            ValueError: If the input observable is invalid.\n+            ValueError: If the input observable is invalid or empty.\n         """\n         # Pauli-type conversions\n         if isinstance(observable, SparsePauliOp):\n@@ -282,21 +282,28 @@ def coerce_observable(cls, observable: ObservableLike) -> SparseObservable:\n             observable = SparseObservable.from_list(term_list)\n \n         if isinstance(observable, SparseObservable):\n-            # Check that the operator has real coeffs\n+            observable = observable.simplify()\n+\n+            # Check that the simplified operator has real coeffs\n             coeffs = np.real_if_close(observable.coeffs)\n             if np.iscomplexobj(coeffs):\n                 raise ValueError(\n-                    "Non-Hermitian input observable: the input SparsePauliOp has non-zero"\n+                    "Non-Hermitian input observable: the simplified input observable has a non-zero"\n                     " imaginary part in its coefficients."\n                 )\n \n-            return SparseObservable.from_raw_parts(\n+            observable = SparseObservable.from_raw_parts(\n                 observable.num_qubits,\n                 coeffs,\n                 observable.bit_terms,\n                 observable.indices,\n                 observable.boundaries,\n-            ).simplify(tol=0)\n+            )\n+\n+            if observable == SparseObservable.zero(observable.num_qubits):\n+                raise ValueError("Empty observable was detected.")\n+\n+            return observable\n \n         raise TypeError(f"Invalid observable type: {type(observable)}")\n \ndiff --git a/releasenotes/notes/obs-array-small-fixes-2947927d2eccc633.yaml b/releasenotes/notes/obs-array-small-fixes-2947927d2eccc633.yaml\nnew file mode 100644\nindex 000000000000..21885127b647\n--- /dev/null\n+++ b/releasenotes/notes/obs-array-small-fixes-2947927d2eccc633.yaml\n@@ -0,0 +1,5 @@\n+---\n+fixes:\n+  - |\n+    Fixed a bug in :meth:`.ObservablesArray.coerce_observable` where an exception that an observable was not Hermitian was over-triggered. \n+    Observables equal to ``zero`` are not allowed and now invoke a graceful failure.\n\\ No newline at end of file\n

diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py\nindex 808f7925471f..e7836296d6aa 100644\n--- a/test/python/primitives/containers/test_observables_array.py\n+++ b/test/python/primitives/containers/test_observables_array.py\n@@ -626,6 +626,17 @@ def test_sparse_observables_array(self):\n         self.assertEqual(arr2, arr)\n         self.assertEqual(id(arr2), id(arr))\n \n+    def test_empty_observable(self):\n+        """Verify that empty observables are not allowed"""\n+        with self.assertRaisesRegex(ValueError, "Empty observable"):\n+            ObservablesArray([{"Z": 0}])\n+\n+    def test_hermitian_after_simplification(self):\n+        """Verify that no error is raised if observables contain complex coefficients\n+        that get cancelled during simplification"""\n+        obs = qi.SparseObservable.from_list([("Z", 1j), ("Z", -1j), ("X", 1)])\n+        ObservablesArray(obs)\n+\n     def test_invalid_basis_type_raises_type_error(self):\n         """Test that invalid basis type raises TypeError"""\n         invalid_basis = {1: "value", 2: "another_value"}  # Invalid keys (integers)\n


Fix non-determinism VF2Layout with only 1q gates (Qiskit/qiskit#14730)

<!--\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\r\n  documented in the release notes.\r\n⚠ If your pull request fixes an open issue, please link to the issue.\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 fixes an issue in the VF2Layout transpiler pass that would cause a non-deterministic output if the circuit had any qubit that only had single qubit operations. The dedicated internal function to deal with this special case sorts the qubits in the circuit by number of operations and the physical qubits on the target by error rate, to map the qubits with the lowest error rate. However this sorting wasn't fully stable as it was done on a hash set which doesn't have a stable iteration order. This commit fixes this by also sorting based on the qubit index so in the case of a tie the sort is stable.\r\n\r\n### Details and comments\r\n\r\nFixes #14729

Transpile with seed_transpiler doesn't yield the same circuit

### Environment\n\n- **Qiskit version**: 2.1.1\n- **Python version**: 3.12.9\n- **Operating system**: MacOS\n\n### What is happening?\n\nTranspiling with a pass manager created with `seed_transpiler` doesn't yield the same circuit after 2.1.\n\n### How can we reproduce the issue?\n\n```python\nimport numpy as np\nfrom qiskit.circuit import ParameterVector, QuantumCircuit\nfrom qiskit.providers.fake_provider import GenericBackendV2\nfrom qiskit.transpiler import generate_preset_pass_manager\n\nparams = ParameterVector("θ", length=3)\n\ncirc = QuantumCircuit(3)\nfor i, par in enumerate(params):\n    circ.rx(par, i)\ncirc.measure_all()\n\nbackend = GenericBackendV2(10, noise_info=True, seed=123)\nisa_circs = []\nfor _ in range(10):\n    pm = generate_preset_pass_manager(optimization_level=2, target=backend.target, seed_transpiler=123)\n    isa_circs.append(pm.run(circ))  \n\nassert all(x == isa_circs[0] for x in isa_circs)\n```\n\nThis code works okey with 2.0.\n\nWith 2.1.* the pass manger layout is inconsistent.\n\n```\nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░    ┌─┐   \nq_1 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░────┤M├───\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░    └╥┘┌─┐\nq_2 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░─────╫─┤M├\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ ┌─┐ ║ └╥┘\nq_0 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├─╫──╫─\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░ └╥┘ ║  ║ \n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░       ┌─┐\nq_2 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░───────┤M├\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░    ┌─┐└╥┘\nq_1 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░────┤M├─╫─\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ ┌─┐└╥┘ ║ \nq_0 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├─╫──╫─\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░ └╥┘ ║  ║ \n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░    ┌─┐   \nq_1 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░────┤M├───\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░    └╥┘┌─┐\nq_2 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░─────╫─┤M├\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ ┌─┐ ║ └╥┘\nq_0 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├─╫──╫─\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░ └╥┘ ║  ║ \n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░    ┌─┐   \nq_1 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░────┤M├───\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░    └╥┘┌─┐\nq_2 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░─────╫─┤M├\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ ┌─┐ ║ └╥┘\nq_0 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├─╫──╫─\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░ └╥┘ ║  ║ \n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░       ┌─┐\nq_2 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░───────┤M├\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ ┌─┐   └╥┘\nq_0 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├────╫─\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ └╥┘┌─┐ ║ \nq_1 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░──╫─┤M├─╫─\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░  ║ └╥┘ ║ \n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░    ┌─┐   \nq_1 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░────┤M├───\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░    └╥┘┌─┐\nq_2 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░─────╫─┤M├\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ ┌─┐ ║ └╥┘\nq_0 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├─╫──╫─\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░ └╥┘ ║  ║ \n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░    ┌─┐   \nq_1 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░────┤M├───\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░    └╥┘┌─┐\nq_2 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░─────╫─┤M├\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ ┌─┐ ║ └╥┘\nq_0 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├─╫──╫─\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░ └╥┘ ║  ║ \n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░       ┌─┐\nq_2 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░───────┤M├\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ ┌─┐   └╥┘\nq_0 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├────╫─\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ └╥┘┌─┐ ║ \nq_1 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░──╫─┤M├─╫─\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░  ║ └╥┘ ║ \n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░ ┌─┐      \nq_0 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├──────\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ └╥┘┌─┐   \nq_1 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░──╫─┤M├───\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░  ║ └╥┘┌─┐\nq_2 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░──╫──╫─┤M├\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░  ║  ║ └╥┘\n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \nglobal phase: π/2\n         ┌─────────┐┌────┐┌──────────────┐┌────┐┌──────────┐ ░ ┌─┐      \nq_0 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[0]) ├┤ √X ├┤ Rz(5π/2) ├─░─┤M├──────\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░ └╥┘┌─┐   \nq_1 -> 3 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[1]) ├┤ √X ├┤ Rz(5π/2) ├─░──╫─┤M├───\n         ├─────────┤├────┤├──────────────┤├────┤├──────────┤ ░  ║ └╥┘┌─┐\nq_2 -> 8 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π + θ[2]) ├┤ √X ├┤ Rz(5π/2) ├─░──╫──╫─┤M├\n         └─────────┘└────┘└──────────────┘└────┘└──────────┘ ░  ║  ║ └╥┘\n meas: 3/═══════════════════════════════════════════════════════╩══╩══╩═\n                                                                0  1  2 \n\n```\n\n### What should happen?\n\nTranspile with a fixed seed should always generate the same layout. This is problematic at least for unittest of my codebase. Not sure if applications can be also affected by this.\n\n### Any suggestions?\n\nI don't have bandwidth to investigate.\n\n(edit) \nAfter carefully looking into transpiled circuits, I realized that the circuit is always consistent at the level of physical qubit set. Since each wire in my circuit is logically equivalent, the behavior of transpiler is still reasonable. Maybe this is undocumented policy update?

diff --git a/crates/transpiler/src/passes/vf2/vf2_layout.rs b/crates/transpiler/src/passes/vf2/vf2_layout.rs\nindex c53419768e2c..c7241848dbaf 100644\n--- a/crates/transpiler/src/passes/vf2/vf2_layout.rs\n+++ b/crates/transpiler/src/passes/vf2/vf2_layout.rs\n@@ -347,10 +347,14 @@ fn map_free_qubits(\n             .get(&[PhysicalQubit::new(*qubit_b), PhysicalQubit::new(*qubit_b)])\n             .unwrap_or(&0.);\n         // Reverse comparison so lower error rates are at the end of the vec.\n-        score_b.partial_cmp(&score_a).unwrap()\n+        match score_b.partial_cmp(&score_a).unwrap() {\n+            Ordering::Equal => qubit_b.cmp(qubit_a),\n+            Ordering::Less => Ordering::Less,\n+            Ordering::Greater => Ordering::Greater,\n+        }\n     });\n     let mut free_indices: Vec<NodeIndex> = free_nodes.keys().copied().collect();\n-    free_indices.par_sort_by_key(|index| free_nodes[index].values().sum::<usize>());\n+    free_indices.par_sort_by_key(|index| (free_nodes[index].values().sum::<usize>(), *index));\n     for im_index in free_indices {\n         let selected_qubit = free_qubits.pop()?;\n         partial_layout.insert(\ndiff --git a/releasenotes/notes/fix-1q-non-determinism-vf2-75490119547bf656.yaml b/releasenotes/notes/fix-1q-non-determinism-vf2-75490119547bf656.yaml\nnew file mode 100644\nindex 000000000000..336b6a5c8f9c\n--- /dev/null\n+++ b/releasenotes/notes/fix-1q-non-determinism-vf2-75490119547bf656.yaml\n@@ -0,0 +1,8 @@\n+---\n+fixes:\n+  - |\n+    Fixed an issue in the :class:`.VF2Layout` transpiler pass where even with\n+    a fixed seed set the output from the pass was potentially non-deterministic;\n+    specifically if the input circuit had any active qubits that only contained\n+    single qubit operations.\n+    Fixed `#14729 <https://github.com/Qiskit/qiskit/issues/14729>`__\n

diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py\nindex a5eb228e347f..b0392c8e3984 100644\n--- a/test/python/compiler/test_transpiler.py\n+++ b/test/python/compiler/test_transpiler.py\n@@ -40,6 +40,7 @@\n     IfElseOp,\n     BoxOp,\n     Parameter,\n+    ParameterVector,\n     Qubit,\n     SwitchCaseOp,\n     WhileLoopOp,\n@@ -2304,6 +2305,27 @@ def test_stretch_integration_with_alignment(self, optimization_level, control_fl\n         # No meaningful assertions; this is a simple regression test for "stretches don't explode\n         # backends with alignments" more than anything.\n \n+    @data(0, 1, 2, 3)\n+    def test_single_qubit_circuit_deterministic_output(self, optimization_level):\n+        """Test that the transpiler's output is deterministic in a single qubit example.\n+\n+        Reproduce from `#14729 <https://github.com/Qiskit/qiskit/issues/14729>`__"""\n+        params = ParameterVector("θ", length=3)\n+\n+        circ = QuantumCircuit(3)\n+        for i, par in enumerate(params):\n+            circ.rx(par, i)\n+        circ.measure_all()\n+        backend = GenericBackendV2(10, noise_info=True, seed=123)\n+        isa_circs = []\n+        for _ in range(10):\n+            pm = generate_preset_pass_manager(\n+                optimization_level=optimization_level, target=backend.target, seed_transpiler=123\n+            )\n+            isa_circs.append(pm.run(circ))\n+        for i in range(10):\n+            self.assertEqual(isa_circs[0], isa_circs[i])\n+\n \n @ddt\n class TestPostTranspileIntegration(QiskitTestCase):\ndiff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py\nindex 71f26afc7c13..019ba2880ad5 100644\n--- a/test/python/transpiler/test_vf2_layout.py\n+++ b/test/python/transpiler/test_vf2_layout.py\n@@ -285,6 +285,28 @@ def test_target_some_error(self):\n         vf2_pass.run(dag)\n         self.assertLayout(dag, target.build_coupling_map(), vf2_pass.property_set)\n \n+    def test_determinism_all_1q(self):\n+        """Test that running vf2layout on a circuit with all single qubit gates is deterministic."""\n+\n+        circ = QuantumCircuit(3)\n+        for i in range(3):\n+            circ.rx(3.14159, i)\n+        circ.measure_all()\n+\n+        backend = GenericBackendV2(10, noise_info=True, seed=123456789)\n+        layouts = []\n+        for _ in range(10):\n+            layout_pass = VF2Layout(target=backend.target)\n+            property_set = {}\n+            layout_pass(circ, property_set=property_set)\n+            layouts.append(property_set["layout"])\n+        self.assertEqual(10, len(layouts), "Expected 10 layouts from 10 pass executions")\n+        for i, layout in enumerate(layouts):\n+            self.assertIsNotNone(layout, f"A layout was not found for layout {i}")\n+            self.assertEqual(\n+                layouts[0], layout, f"Layout for execution {i} differs from the expected"\n+            )\n+\n \n @ddt.ddt\n class TestVF2LayoutLattice(LayoutTestCase):\n


Un-provide `standard_operation` for `PyGate` (Qiskit/qiskit#14978)

<!--\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\r\n  documented in the release notes.\r\n⚠ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [x] I have added the tests to cover my changes.\r\n- [x] I have updated the documentation accordingly.\r\n- [x] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\n\r\nPreviously `PyGate` implemented the method `standard_gate`, which for example allowed to interpret a `PyGate` created for `CRX(controlled_state=0)` as `StandardGate::CRX`, effectively losing the control state and leading to problems down the line.\r\n\r\nFixes #14974.\r\n\r\n### Details and comments\r\n\r\nJake's additional comments: the solution not provide that method at all - if we can’t treat it as a StandardGate during extraction, there’s no real need to guess at other times. There’s at least a couple of reasons we can’t do it:\r\n* not-all-ones control state\r\n* the base_gate of a controlledgate has extra attributes set (e.g. label) and we have tests that assert that the label of the base gate is propagated\r\n\r\n

`CommutativeInverseCancellation` cancels `CS/CSdg` with `CRX` when control state is |0⟩

### Environment\n\n- **Qiskit version**: 2.1.2\n- **Python version**: 3.13\n- **Operating system**: macOS 15.6\n\n\n### What is happening?\n\n`CommutativeInverseCancellation` assumes that `CS` and its inverse commute with `CRX` and cancels them by moving gates across each other, changing the circuit's semantics. This only occurs when the control state is $\\vert 0 \\rangle$.\n\n### How can we reproduce the issue?\n\n```python\nfrom qiskit import QuantumCircuit\nfrom qiskit.transpiler import PassManager\nfrom qiskit.transpiler.passes import CommutativeInverseCancellation\n\npm = PassManager(CommutativeInverseCancellation())\n\ncircuit = QuantumCircuit(2, 1)\ncircuit.csdg(0, 1, ctrl_state=0)\ncircuit.crx(1, 0, 1, ctrl_state=0)\ncircuit.cs(0, 1, ctrl_state=0)\ncircuit.ry(1, 1)\ncircuit.measure(1, 0)\nprint(circuit)\n\ntrans_circuit = pm.run(circuit)\nprint(trans_circuit)\n\n# Optional: Simulation\nfrom qiskit_aer import Aer\nfrom qiskit import transpile\n\nsim = Aer.get_backend("aer_simulator")\nprint("Original:")\ncircuit = transpile(circuit, sim, optimization_level=0)\nprint(sim.run(circuit).result().get_counts())\nprint("Transpiled:")\ntrans_circuit = transpile(trans_circuit, sim, optimization_level=0)\nprint(sim.run(trans_circuit).result().get_counts())\n```\n```\nq_0: ───o───────o──────o──────────────\n     ┌──┴──┐┌───┴───┐┌─┴─┐┌───────┐┌─┐\nq_1: ┤ Sdg ├┤ Rx(1) ├┤ S ├┤ Ry(1) ├┤M├\n     └─────┘└───────┘└───┘└───────┘└╥┘\nc: 1/═══════════════════════════════╩═\n                                    0 \nq_0: ────o────────────────\n     ┌───┴───┐┌───────┐┌─┐\nq_1: ┤ Rx(1) ├┤ Ry(1) ├┤M├\n     └───────┘└───────┘└╥┘\nc: 1/═══════════════════╩═\n                        0 \nOriginal:\n{'0': 284, '1': 740}\nTranspiled:\n{'0': 674, '1': 350}\n```\n\n### What should happen?\n\n`CS` and `CSdg` should not be cancelled across `CRX`.\n\n### Any suggestions?\n\nThe bug only appears when `CS` and `CSdg` are used with `ctrl_state=0`. In this case, `CommutationChecker` seems to skip the standard gate commutation table, and instead falls back to a matrix-multiplication check. I suspect the issue lies in this fallback path, which incorrectly treats the gates as commuting.

diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs\nindex f6bd3bfedc6c..c2403ce7fef7 100644\n--- a/crates/circuit/src/operations.rs\n+++ b/crates/circuit/src/operations.rs\n@@ -256,7 +256,6 @@ pub trait Operation {\n     fn blocks(&self) -> Vec<CircuitData>;\n     fn matrix(&self, params: &[Param]) -> Option<Array2<Complex64>>;\n     fn definition(&self, params: &[Param]) -> Option<CircuitData>;\n-    fn standard_gate(&self) -> Option<StandardGate>;\n     fn directive(&self) -> bool;\n     fn matrix_as_static_1q(&self, params: &[Param]) -> Option<[[Complex64; 2]; 2]>;\n     fn matrix_as_nalgebra_1q(&self, params: &[Param]) -> Option<Matrix2<Complex64>> {\n@@ -370,17 +369,6 @@ impl Operation for OperationRef<'_> {\n         }\n     }\n     #[inline]\n-    fn standard_gate(&self) -> Option<StandardGate> {\n-        match self {\n-            Self::StandardGate(standard) => standard.standard_gate(),\n-            Self::StandardInstruction(instruction) => instruction.standard_gate(),\n-            Self::Gate(gate) => gate.standard_gate(),\n-            Self::Instruction(instruction) => instruction.standard_gate(),\n-            Self::Operation(operation) => operation.standard_gate(),\n-            Self::Unitary(unitary) => unitary.standard_gate(),\n-        }\n-    }\n-    #[inline]\n     fn directive(&self) -> bool {\n         match self {\n             Self::StandardGate(standard) => standard.directive(),\n@@ -552,10 +540,6 @@ impl Operation for StandardInstruction {\n         None\n     }\n \n-    fn standard_gate(&self) -> Option<StandardGate> {\n-        None\n-    }\n-\n     fn directive(&self) -> bool {\n         match self {\n             StandardInstruction::Barrier(_) => true,\n@@ -2259,10 +2243,6 @@ impl Operation for StandardGate {\n         }\n     }\n \n-    fn standard_gate(&self) -> Option<StandardGate> {\n-        Some(*self)\n-    }\n-\n     fn directive(&self) -> bool {\n         false\n     }\n@@ -2555,9 +2535,6 @@ impl Operation for PyInstruction {\n             }\n         })\n     }\n-    fn standard_gate(&self) -> Option<StandardGate> {\n-        None\n-    }\n \n     fn directive(&self) -> bool {\n         Python::with_gil(|py| -> bool {\n@@ -2659,14 +2636,6 @@ impl Operation for PyGate {\n             }\n         })\n     }\n-    fn standard_gate(&self) -> Option<StandardGate> {\n-        Python::with_gil(|py| -> Option<StandardGate> {\n-            match self.gate.getattr(py, intern!(py, "_standard_gate")) {\n-                Ok(stdgate) => stdgate.extract(py).unwrap_or_default(),\n-                Err(_) => None,\n-            }\n-        })\n-    }\n     fn directive(&self) -> bool {\n         false\n     }\n@@ -2749,10 +2718,6 @@ impl Operation for PyOperation {\n     fn definition(&self, _params: &[Param]) -> Option<CircuitData> {\n         None\n     }\n-    fn standard_gate(&self) -> Option<StandardGate> {\n-        None\n-    }\n-\n     fn directive(&self) -> bool {\n         Python::with_gil(|py| -> bool {\n             match self.operation.getattr(py, intern!(py, "_directive")) {\n@@ -2839,10 +2804,6 @@ impl Operation for UnitaryGate {\n         None\n     }\n \n-    fn standard_gate(&self) -> Option<StandardGate> {\n-        None\n-    }\n-\n     fn directive(&self) -> bool {\n         false\n     }\ndiff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs\nindex e38b4b759c01..1302f7fe09b5 100644\n--- a/crates/circuit/src/packed_instruction.rs\n+++ b/crates/circuit/src/packed_instruction.rs\n@@ -546,10 +546,6 @@ impl Operation for PackedOperation {\n         self.view().definition(params)\n     }\n     #[inline]\n-    fn standard_gate(&self) -> Option<StandardGate> {\n-        self.view().standard_gate()\n-    }\n-    #[inline]\n     fn directive(&self) -> bool {\n         self.view().directive()\n     }\ndiff --git a/crates/transpiler/src/commutation_checker.rs b/crates/transpiler/src/commutation_checker.rs\nindex f7152e7af4b1..df1514d1fef8 100644\n--- a/crates/transpiler/src/commutation_checker.rs\n+++ b/crates/transpiler/src/commutation_checker.rs\n@@ -395,8 +395,9 @@ impl CommutationChecker {\n         //  * gates we know are in the cache (SUPPORTED_OPS), or\n         //  * standard gates with float params (otherwise we cannot cache them)\n         let is_cachable = |op: &OperationRef, params: &[Param]| {\n-            if let Some(gate) = op.standard_gate() {\n-                SUPPORTED_OP[gate as usize] || params.iter().all(|p| matches!(p, Param::Float(_)))\n+            if let OperationRef::StandardGate(gate) = op {\n+                SUPPORTED_OP[(*gate) as usize]\n+                    || params.iter().all(|p| matches!(p, Param::Float(_)))\n             } else {\n                 false\n             }\n@@ -595,9 +596,9 @@ fn commutation_precheck(\n         return Some(false);\n     }\n \n-    if let Some(gate_1) = op1.standard_gate() {\n-        if let Some(gate_2) = op2.standard_gate() {\n-            if SUPPORTED_OP[gate_1 as usize] && SUPPORTED_OP[gate_2 as usize] {\n+    if let OperationRef::StandardGate(gate_1) = op1 {\n+        if let OperationRef::StandardGate(gate_2) = op2 {\n+            if SUPPORTED_OP[(*gate_1) as usize] && SUPPORTED_OP[(*gate_2) as usize] {\n                 return None;\n             }\n         }\n@@ -678,18 +679,18 @@ fn map_rotation<'a>(\n     params: &'a [Param],\n     tol: f64,\n ) -> (Option<StandardGate>, &'a [Param], bool) {\n-    if let Some(gate) = op.standard_gate() {\n-        if let Some(generator) = SUPPORTED_ROTATIONS[gate as usize] {\n+    if let OperationRef::StandardGate(gate) = op {\n+        if let Some(generator) = SUPPORTED_ROTATIONS[(*gate) as usize] {\n             // If the rotation angle is below the tolerance, the gate is assumed to\n             // commute with everything, and we simply return the operation with the flag that\n             // it commutes trivially.\n             if let Param::Float(angle) = params[0] {\n-                let (tr_over_dim, dim) = gate_metrics::rotation_trace_and_dim(gate, angle)\n+                let (tr_over_dim, dim) = gate_metrics::rotation_trace_and_dim(*gate, angle)\n                     .expect("All rotation should be covered at this point");\n                 let gate_fidelity = tr_over_dim.abs().powi(2);\n                 let process_fidelity = (dim * gate_fidelity + 1.) / (dim + 1.);\n                 if (1. - process_fidelity).abs() <= tol {\n-                    return (Some(gate), params, true);\n+                    return (Some(*gate), params, true);\n                 };\n             };\n \ndiff --git a/releasenotes/notes/fix-commutation-checking-with-not-all-one-states-45fdd8a779ae4277.yaml b/releasenotes/notes/fix-commutation-checking-with-not-all-one-states-45fdd8a779ae4277.yaml\nnew file mode 100644\nindex 000000000000..3098768a56c7\n--- /dev/null\n+++ b/releasenotes/notes/fix-commutation-checking-with-not-all-one-states-45fdd8a779ae4277.yaml\n@@ -0,0 +1,7 @@\n+---\n+fixes:\n+  - |\n+    Fixed a problem in `~.CommutationChecker`, where standard controlled gates\n+    were not handled correctly if they were controlled on something other than\n+    the all-ones state.\n+    Fixed `#14974 <https://github.com/Qiskit/qiskit/issues/14974>`__\n

diff --git a/test/python/transpiler/test_commutative_inverse_cancellation.py b/test/python/transpiler/test_commutative_inverse_cancellation.py\nindex c4f995deb3c4..61787047ccf8 100644\n--- a/test/python/transpiler/test_commutative_inverse_cancellation.py\n+++ b/test/python/transpiler/test_commutative_inverse_cancellation.py\n@@ -925,6 +925,24 @@ def test_initialize(self):\n         # The pass should run successfully but not reduce anything\n         self.assertEqual(circuit, tqc)\n \n+    def test_controlled_state_at_zero(self):\n+        """Regression test of #14974.\n+\n+        Two gates with not-all-ones control-states were wrongly\n+        detected to commute, leading to invalid simplification.\n+        """\n+        circuit = QuantumCircuit(2)\n+        circuit.csdg(0, 1, ctrl_state=0)\n+        circuit.crx(1, 0, 1, ctrl_state=0)\n+        circuit.cs(0, 1, ctrl_state=0)\n+        circuit.ry(1, 1)\n+\n+        pm = PassManager(CommutativeInverseCancellation())\n+        tqc = pm.run(circuit)\n+\n+        # The pass should run successfully but not reduce anything\n+        self.assertEqual(circuit, tqc)\n+\n \n if __name__ == "__main__":\n     unittest.main()\n


Fix `VF2PostLayout` with uncoupled qubits in `strict_direction=True` (backport #14998) (Qiskit/qiskit#15001)

When `VF2PostLayout` is set in strict mode, we have to include even active but uncoupled qubits in the interaction graph, because of the semantic constraints.  We need to limit the number of these uncoupled qubits we'll consider, though, because there's a factorial overhead to handling them.\n\nA previous commit[^1] added this skip to `strict_direction=False`, but didn't correctly handle the continuation case where we're within the limit.  This commit ~ups the limit to 3 uncoupled qubits (a potential 6x overhead) and~ correctly clears out the uncoupled-qubit metadata that was causing problems in later layout reconstruction.\n\n[^1]: b4094ddc: Fix calls to `VF2PostLayout` after optimization loop at O3\n\n\n\n### Summary\n\n\n\n### Details and comments\n\nNo release note because the bug isn't released.\n\nFix #14997 <hr>This is an automatic backport of pull request #14998 done by [Mergify](https://mergify.com).

VF2PostLayout fails during layout finalisation with 1q wires

### Environment\n\n- **Qiskit version**: 2.2.0rc1\n- **Python version**: 3.13\n- **Operating system**: macOS\n\n\n### What is happening?\n\n`VF2PostLayout` can fail during layout expansion, seemingly if there is a single qubit that has only 1q operations on it (but not more than one).\n\n### How can we reproduce the issue?\n\n```python\nfrom qiskit import QuantumCircuit, transpile\nfrom qiskit.providers.fake_provider import GenericBackendV2\n\nqc = QuantumCircuit(5, 5)\nqc.h(0)\nfor k in range(1, 4):\n    qc.cx(0, k)\nqc.measure(qc.qubits, qc.clbits)\n\nbackend = GenericBackendV2(num_qubits=5, basis_gates=["cx", "id", "rz", "sx", "x"], seed=0)\ntqc = transpile(qc, backend=backend, optimization_level=3, seed_transpiler=0)\n```\nraises\n```text\nTypeError                                 Traceback (most recent call last)\nCell In[7], line 11\n      8 qc.measure(qc.qubits, qc.clbits)\n     10 backend = GenericBackendV2(num_qubits=5, basis_gates=["cx", "id", "rz", "sx", "x"], seed=0)\n---> 11 tqc = transpile(qc, backend=backend, optimization_level=3, seed_transpiler=0)\n\n[...snip...]\n\nFile ~/code/qiskit/terra/qiskit/transpiler/passes/layout/vf2_post_layout.py:341, in VF2PostLayout.run(self, dag)\n    339 existing_layout = self.property_set["layout"]\n    340 # If any ancillas in initial layout map them back to the final layout output\n--> 341 if existing_layout is not None and len(existing_layout) > len(chosen_layout):\n    342     virtual_bits = chosen_layout.get_virtual_bits()\n    343     used_bits = set(virtual_bits.values())\n\nTypeError: object of type 'NoneType' has no len()\n```\n\n### What should happen?\n\nSuccessful compilation.\n\n### Any suggestions?\n\nI suspect that this was introduced during #14869 - it's suspicious to me that the catch for single-qubit operations is `if len(free) > 1` and not just `if free` - certainly the logic dictates that even a _single_ 1q wire can cause spurious failures.

diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py\nindex d5b2b3c73182..5473f1a57f6d 100644\n--- a/qiskit/transpiler/passes/layout/vf2_post_layout.py\n+++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py\n@@ -153,11 +153,19 @@ def run(self, dag):\n             self.property_set["VF2PostLayout_stop_reason"] = VF2PostLayoutStopReason.MORE_THAN_2Q\n             return\n         im_graph, im_graph_node_map, reverse_im_graph_node_map, free_nodes = result\n-        if len(free_nodes) > 1 and self.strict_direction:\n-            self.property_set["VF2PostLayout_stop_reason"] = (\n-                VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND\n-            )\n-            return\n+        if self.strict_direction and free_nodes:\n+            # If there are uncoupled qubits, in non-strict modes we just allocate them to the\n+            # lowest-error states at the end.  However, in strict mode, we have to consider them at\n+            # the same time to handle heterogeneous targets correctly.  This risks a factorial\n+            # combinatoric explosion in complexity, though, so we put a limit on how many we'll\n+            # handle.  The builder still builds the graph entirely, it just returns the free nodes\n+            # for us to check on, so we clear that out after we've checked it.\n+            if len(free_nodes) > 1:\n+                self.property_set["VF2PostLayout_stop_reason"] = (\n+                    VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND\n+                )\n+                return\n+            free_nodes.clear()\n         scoring_bit_list = vf2_utils.build_bit_list(im_graph, im_graph_node_map)\n         scoring_edge_list = vf2_utils.build_edge_list(im_graph)\n \n

diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py\nindex a1c4d728542b..d8f75cc2dbcf 100644\n--- a/test/python/transpiler/test_vf2_post_layout.py\n+++ b/test/python/transpiler/test_vf2_post_layout.py\n@@ -18,7 +18,7 @@\n from qiskit import QuantumRegister, QuantumCircuit\n from qiskit.circuit import ControlFlowOp\n from qiskit.circuit.library import CXGate, XGate\n-from qiskit.transpiler import Layout, TranspilerError\n+from qiskit.transpiler import Layout, TranspilerError, PassManager, passes\n from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayout, VF2PostLayoutStopReason\n from qiskit.converters import circuit_to_dag\n from qiskit.providers.fake_provider import GenericBackendV2\n@@ -352,6 +352,33 @@ def test_complete_layout_with_idle_qubits(self, seed, strict_direction):\n         }\n         self.assertEqual(unallocated, set())\n \n+    @ddt.data(-1, 12)\n+    def test_complete_layout_with_one_uncoupled_qubit(self, seed):\n+        """Test that a single uncoupled qubit is handled correctly.\n+\n+        Regression test of https://github.com/Qiskit/qiskit/issues/14997."""\n+        qc = QuantumCircuit(3)\n+        qc.x(0)\n+        qc.cx(1, 2)\n+\n+        bad = InstructionProperties(error=1e-1)\n+        good = InstructionProperties(error=1e-5)\n+\n+        target = Target()\n+        target.add_instruction(XGate(), {(0,): bad, (1,): good, (2,): good})\n+        target.add_instruction(CXGate(), {(0, 1): good, (1, 2): bad})\n+\n+        pm = PassManager(\n+            [\n+                passes.TrivialLayout(target),\n+                passes.ApplyLayout(),\n+                passes.VF2PostLayout(target, seed=seed, strict_direction=True),\n+                passes.ApplyLayout(),\n+            ]\n+        )\n+        out = pm.run(qc)\n+        self.assertEqual(out.layout.final_index_layout(), [2, 0, 1])\n+\n \n class TestVF2PostLayoutScoring(QiskitTestCase):\n     """Test scoring heuristic function for VF2PostLayout."""\n


Check target gates are parameterized in optimize1qgatesdecomposition (Qiskit/qiskit#15131)

<!--\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

Optimize1qGatesDecomposition doesn't check if 1q gates support arbitrary angles

### 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.

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

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


Fix incorrect mapping of ControlFlow block qargs in `BasisTranslator` (Qiskit/qiskit#15143)

<!--\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- [x] I have added the tests to cover my changes.\r\n- [x] I have updated the documentation accordingly.\r\n- [x] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\nWhen extracting the basis of the target and applying the translations onto the dag, the `BasisTranslator` would not use the absolute indices of the operating qubits when a control flow operation block was processed during recursion. The following commit adds a mapping to the following `BasisTranslator` methods:\r\n- `extract_target_basis_circuit` which only gets called during a recursion step.\r\n- `apply_translation` in which it is always `None` unless a recursion step happens.\r\n\r\nThe provided mapping will correctly direct local block qubit indices into the absolute ones stored by the original circuit from which the operation comes from.\r\n\r\n### Details and comments\r\nThis should help fix #13728.\r\n\r\n\r\n

Transpiler pass does not update qubits registers within body of control flow operations

### Environment\n\n- **Qiskit version**: 1.3.2\n- **Python version**: 3.10\n- **Operating system**: Mac OS Sequoia\n\n### What is happening?\n\nThe issue arises due to the way Qiskit handles control flow structures like for_loop when applying transpilation passes. Specifically, the for_loop construct internally stores a new QuantumCircuit object that does not retain the physical qubit mapping intended for the calibrated custom gate (as defined in the backend target object).\n\nWhen the transpilation pipeline reaches the HighLevelSynthesis pass, the method _definitely_skip_node should return True if the gate already has a valid calibration. However, it does not because the physical qubits referenced in the circuit structure do not align with those recorded in the initial_layout during pass execution.\n\n### How can we reproduce the issue?\n\n```python\nfrom qiskit.circuit import QuantumCircuit, ParameterVector, Gate, Parameter\nfrom qiskit import pulse, schedule, transpile\nfrom qiskit.providers.fake_provider import GenericBackendV2\nfrom qiskit.transpiler import InstructionProperties, generate_preset_pass_manager, CouplingMap\n\nparams = [Parameter(f'a_{i}') for i in range(2)]\nqc = QuantumCircuit(2)\necr_cal = Gate(name='ecr_cal', num_qubits=2, params=params)\nqc_loop = QuantumCircuit(2)\nqc.append(ecr_cal, [0, 1])\nwith qc_loop.for_loop([0, 3]) as i:\n    qc_loop.compose(qc, inplace=True)\n\nbackend = GenericBackendV2(num_qubits=4,\n                           basis_gates=["x", "sx", "ecr", "rz", "measure", "reset"],\n                           coupling_map=CouplingMap.from_full(4),\n                           calibrate_instructions=True,\n                           control_flow=True)\n\nwith pulse.build(backend, name="custom_sched") as custom_sched:\n    pulse.play(pulse.Constant(100, params[0]), pulse.DriveChannel(3))\n    pulse.play(pulse.Constant(100, params[1]), pulse.DriveChannel(4))\n\nbackend.target.add_instruction(ecr_cal, properties={(3,4): InstructionProperties(calibration=custom_sched)})\npm = generate_preset_pass_manager(optimization_level=1,\n                                  backend=backend,\n                                  initial_layout=(3,4))\n\ntranspiled_circ = pm.run(qc_loop)\ntranspiled_circ.draw('mpl')`\n```\n\n### What should happen?\n\nThe transpilation process should correctly recognize that the ecr_cal gate, mapped to physical qubits (3,4), already has a calibration, and avoid unnecessary synthesis steps.\nInstead of skipping synthesis, the transpiler does not identify the calibration, likely due to the for_loop internally storing logical qubit indices instead of physical qubits.\n\n### Any suggestions?\n\nA potential solution could involve ensuring that physical qubit mappings are correctly maintained within control flow constructs, or updating the handling of control flow operations within transpilation passes to correctly propagate the intended qubit mappings.\n

diff --git a/crates/transpiler/src/passes/basis_translator/mod.rs b/crates/transpiler/src/passes/basis_translator/mod.rs\nindex 70228ca6c62b..a8b9b0410da8 100644\n--- a/crates/transpiler/src/passes/basis_translator/mod.rs\n+++ b/crates/transpiler/src/passes/basis_translator/mod.rs\n@@ -173,7 +173,7 @@ pub fn run_basis_translator(\n             }\n         } else {\n             expanded_target = expanded_target\n-                .union(&qargs_with_non_global_operation[&Qargs::from_iter(qargs.iter().copied())])\n+                .union(&qargs_with_non_global_operation[&QargsRef::Concrete(qargs)])\n                 .cloned()\n                 .collect();\n         }\n@@ -213,6 +213,7 @@ pub fn run_basis_translator(\n         &extra_inst_map,\n         min_qubits,\n         &qargs_with_non_global_operation,\n+        None,\n     )?;\n     Ok(Some(out_dag))\n }\n@@ -320,12 +321,22 @@ fn extract_basis_target(\n         }\n         if node_obj.op.control_flow() {\n             for block in node_obj.op.blocks() {\n+                // Generate a mapping with the absolute indices and the local ones.\n+                let qarg_mapping: HashMap<Qubit, Qubit> = dag\n+                    .qargs_interner()\n+                    .get(node_obj.qubits)\n+                    .iter()\n+                    .copied()\n+                    .zip((0..(block.num_qubits() as u32)).map(Qubit))\n+                    .map(|(k, v)| (v, k))\n+                    .collect();\n                 extract_basis_target_circ(\n                     &block,\n                     source_basis,\n                     qargs_local_source_basis,\n                     min_qubits,\n                     qargs_with_non_global_operation,\n+                    &qarg_mapping,\n                 );\n             }\n         }\n@@ -339,6 +350,7 @@ fn extract_basis_target_circ(\n     qargs_local_source_basis: &mut AhashIndexMap<PhysicalQargs, AhashIndexSet<GateIdentifier>>,\n     min_qubits: usize,\n     qargs_with_non_global_operation: &AhashIndexMap<Qargs, AhashIndexSet<&str>>,\n+    qarg_mapping: &HashMap<Qubit, Qubit>,\n ) {\n     for node_obj in circuit.iter() {\n         let qargs = circuit.get_qargs(node_obj.qubits);\n@@ -354,7 +366,12 @@ fn extract_basis_target_circ(\n         // single qubit operation for (1,) as valid. This pattern also holds\n         // true for > 2q ops too (so for 4q operations we need to check for 3q, 2q,\n         // and 1q operations in the same manner)\n-        let physical_qargs: PhysicalQargs = qargs.iter().map(|x| PhysicalQubit(x.0)).collect();\n+        // Make sure to also map to the physical qubits of the target rather than\n+        // the virtual locations within the circuit or control-flow operation block.\n+        let physical_qargs: PhysicalQargs = qargs\n+            .iter()\n+            .map(|x| PhysicalQubit(qarg_mapping[x].0))\n+            .collect();\n         let physical_qargs_as_set: AhashIndexSet<PhysicalQubit> =\n             AhashIndexSet::from_iter(physical_qargs.iter().copied());\n         let physical_qargs: Qargs = physical_qargs.into();\n@@ -389,12 +406,22 @@ fn extract_basis_target_circ(\n         }\n         if node_obj.op.control_flow() {\n             for block in node_obj.op.blocks() {\n+                // Generate a mapping with the absolute qubit indices and the current local ones.\n+                let qarg_mapping: HashMap<Qubit, Qubit> = circuit\n+                    .qargs_interner()\n+                    .get(node_obj.qubits)\n+                    .iter()\n+                    .copied()\n+                    .zip((0..(block.num_qubits() as u32)).map(Qubit))\n+                    .map(|(k, v)| (v, qarg_mapping[&k]))\n+                    .collect();\n                 extract_basis_target_circ(\n                     &block,\n                     source_basis,\n                     qargs_local_source_basis,\n                     min_qubits,\n                     qargs_with_non_global_operation,\n+                    &qarg_mapping,\n                 );\n             }\n         }\n@@ -408,6 +435,7 @@ fn apply_translation(\n     extra_inst_map: &ExtraInstructionMap,\n     min_qubits: usize,\n     qargs_with_non_global_operation: &AhashIndexMap<Qargs, AhashIndexSet<&str>>,\n+    qarg_mapping: Option<&HashMap<Qubit, Qubit>>,\n ) -> Result<(DAGCircuit, bool), BasisTranslatorError> {\n     let mut is_updated = false;\n     let out_dag = dag.copy_empty_like(VarsMode::Alike).map_err(|_| {\n@@ -436,6 +464,13 @@ fn apply_translation(\n                     for block in node_obj.op.blocks() {\n                         let dag_block: DAGCircuit = DAGCircuit::from_circuit_data(&block, true, None, None, None, None)?;\n                         let updated_dag: DAGCircuit;\n+                        // Generate a mapping between the absolute and local qubit indices\n+                        // which will be used to correctly map the operation onto the dag.\n+                        let qarg_mapping: HashMap<Qubit, Qubit> = if let Some(qarg_mapping) = qarg_mapping {\n+                            dag.qargs_interner().get(node_obj.qubits).iter().zip((0..(block.num_qubits() as u32)).map(Qubit)).map(|(k, v)| (v, qarg_mapping[k])).collect()\n+                        } else {\n+                            dag.qargs_interner().get(node_obj.qubits).iter().zip((0..(block.num_qubits() as u32)).map(Qubit)).map(|(k, v)| (v, *k)).collect()\n+                        };\n                         (updated_dag, is_updated) = apply_translation(\n                             &dag_block,\n                             target_basis,\n@@ -443,6 +478,7 @@ fn apply_translation(\n                             extra_inst_map,\n                             min_qubits,\n                             qargs_with_non_global_operation,\n+                            Some(&qarg_mapping)\n                         ).map_err(PyErr::from)?;\n                         let flow_circ_block = if is_updated {\n                             QUANTUM_CIRCUIT.get_bound(py).call_method1(intern!(py, "_from_circuit_data"), (dag_to_circuit(&updated_dag, true)?,))?\n@@ -496,7 +532,16 @@ fn apply_translation(\n             }\n             continue;\n         }\n-        let node_qarg_as_physical: Qargs = node_qarg.iter().map(|x| PhysicalQubit(x.0)).collect();\n+        // Map to the absolute indices when provided to avoid mistakenly tracking\n+        // the operation as global.\n+        let node_qarg_as_physical: Qargs = if let Some(qarg_mapping) = qarg_mapping {\n+            node_qarg\n+                .iter()\n+                .map(|x| PhysicalQubit(qarg_mapping[x].0))\n+                .collect()\n+        } else {\n+            node_qarg.iter().map(|x| PhysicalQubit(x.0)).collect()\n+        };\n         if qargs_with_non_global_operation.contains_key(&node_qarg_as_physical)\n             && qargs_with_non_global_operation[&node_qarg_as_physical].contains(node_obj.op.name())\n         {\n@@ -518,7 +563,15 @@ fn apply_translation(\n             continue;\n         }\n \n-        let unique_qargs: PhysicalQargs = qubit_set.iter().map(|x| PhysicalQubit(x.0)).collect();\n+        // Map the unique qargs with the absolute indices as well\n+        let unique_qargs: PhysicalQargs = if let Some(qarg_mapping) = qarg_mapping {\n+            qubit_set\n+                .iter()\n+                .map(|x| PhysicalQubit(qarg_mapping[x].0))\n+                .collect()\n+        } else {\n+            qubit_set.iter().map(|x| PhysicalQubit(x.0)).collect()\n+        };\n         if extra_inst_map.contains_key(&unique_qargs) {\n             replace_node(\n                 &mut out_dag_builder,\ndiff --git a/releasenotes/notes/fix-basis-control-custom-gate-b62fd53c73776085.yaml b/releasenotes/notes/fix-basis-control-custom-gate-b62fd53c73776085.yaml\nnew file mode 100644\nindex 000000000000..40b719d7cb78\n--- /dev/null\n+++ b/releasenotes/notes/fix-basis-control-custom-gate-b62fd53c73776085.yaml\n@@ -0,0 +1,6 @@\n+---\n+fixes:\n+  - |\n+    Fixed incorrect behavior in the :class:`.BasisTranslator` pass where a multi-qubit\n+    gate within a :class:`.ControlFlowOp` block would track with its local qubit indices\n+    instead of using the absolute indices from the source circuit.\n\\ No newline at end of file\n

diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py\nindex 751073deb28c..ba002b6bfa2e 100644\n--- a/test/python/transpiler/test_basis_translator.py\n+++ b/test/python/transpiler/test_basis_translator.py\n@@ -87,6 +87,13 @@ def __init__(self, name="2q0p"):\n         super().__init__(name, 2, [])\n \n \n+class TwoQubitOneParamGate(Gate):\n+    """Mock two qubit one param gate."""\n+\n+    def __init__(self, theta, name="2q1p"):\n+        super().__init__(name, 2, [theta])\n+\n+\n class VariadicZeroParamGate(Gate):\n     """Mock variadic zero param gate."""\n \n@@ -459,6 +466,32 @@ def test_nested_loop(self):\n         dag_expected = circuit_to_dag(expected)\n         self.assertEqual(dag_translated, dag_expected)\n \n+    def test_loop_custom_gate_local_indices(self):\n+        """Test with a custom gate inside a loop operation with different local qubit indices"""\n+\n+        # Create 3 qubit backend\n+        backend = GenericBackendV2(num_qubits=3, control_flow=True)\n+\n+        # Add only this pair as native to the Target.\n+        pair = (0, 2)\n+        alpha = Parameter("alpha")\n+        gate = TwoQubitOneParamGate(alpha)\n+\n+        # Add custom gate to be supported by the Target.\n+        backend.target.add_instruction(gate, {pair: None})  # properties omitted for brevity\n+\n+        # Make the tracking register\n+        qubits = QuantumRegister(3, "data")\n+\n+        # Create circuit with loop\n+        circ = QuantumCircuit(qubits)\n+        with circ.for_loop(range(2)):\n+            circ.append(gate, [qubits[0], qubits[2]])\n+\n+        # Run the basis translator\n+        result = BasisTranslator(std_eq_lib, [], backend.target)(circ)\n+        self.assertEqual(circ, result)\n+\n     def test_different_bits(self):\n         """Test that the basis translator correctly works when the inner blocks of control-flow\n         operations are not over the same bits as the outer blocks."""\n


Make SabreSwap account for clbits in predecessors (Qiskit/qiskit#7952)

### Summary\r\n\r\nPreviously, SabreSwap only checked that it was valid to add a gate back\r\nto the DAG by ensuring that all its qubit predecessors had already been\r\nadded to the DAG.  It did not check for clbit successors.  This meant\r\nthat measurements and (potentially, unverified) classically conditioned\r\ngates could be re-ordered out from the correct topological ordering.\r\n\r\nThe most notable effect of this was that measurements writing to the\r\nsame bit could be re-ordered, changing the output of the circuit.\r\n\r\nSee gh-7950 for more detail.\r\n\r\n<!--\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\r\n  documented in the release notes.\r\n⚠ If your pull request fixes an open issue, please link to the issue.\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\r\n\r\n### Details and comments\r\n\r\nFix #7950.  I also believe this to be the cause of the sporadic randomised test failures (see an example in the linked issue).\r\n\r\nAli (@ajavadia) - this might be something you ought to look at, since I don't understand SabreSwap well.\r\n

`SabreSwap` can reorder operations that depend on the same classical bit

### Environment\n\n- **Qiskit Terra version**: main @ ef997f9\r\n- **Python version**: any\r\n- **Operating system**: any\n\n### What is happening?\n\n`SabreSwap` can re-order measurement operations (and probably classically conditioned gates) that write to the same classical bits.  This affects optimisation level 3 by default, since it uses sabre to route if nothing else is provided.\r\n\r\nI am 95% sure this is the cause of the failures we're seeing in the randomised test suite currently.\r\n\r\nExpand for logs of recent failed randomised run\r\nHypothesis failure at commit 618e0ea8b3daca194c1bde97292e57c15eddd762, in file `test/randomized/test_transpile_equivalence.py`\r\n\r\n```text\r\nFalsifying example:\r\nstate = QCircuitMachine()\r\nstate.qasm()\r\n(v1,) = state.add_creg(n=1)\r\nstate.qasm()\r\nv2, v3, v4 = state.add_qreg(n=3)\r\nstate.qasm()\r\nstate.add_3q_gate(gate=qiskit.circuit.library.standard_gates.x.CCXGate, qargs=[v4, v3, v2])\r\nstate.qasm()\r\nstate.add_1q1c_gate(carg=v1, gate=qiskit.circuit.measure.Measure, qarg=v3)\r\nstate.qasm()\r\nstate.add_1q2p_gate(gate=qiskit.circuit.library.standard_gates.u2.U2Gate, params=[0.0, 0.0], qarg=v4)\r\nstate.qasm()\r\nstate.add_1q1c_gate(carg=v1, gate=qiskit.circuit.measure.Measure, qarg=v4)\r\nstate.qasm()\r\nEvaluating circuit at level 3 on fake_ourense:\r\nOPENQASM 2.0;\r\ninclude "qelib1.inc";\r\nqreg q35069[3];\r\ncreg c1656[1];\r\nccx q35069[2],q35069[1],q35069[0];\r\nmeasure q35069[1] -> c1656[0];\r\nu2(0,0) q35069[2];\r\nmeasure q35069[2] -> c1656[0];\r\n\r\nstate.equivalent_transpile(backend=<FakeOurense('fake_ourense')>, opt_level=3)\r\nstate.teardown()\r\n\r\nYou can reproduce this example by temporarily adding @reproduce_failure('6.43.2', b'AXicY2Bk5GFgYGDkZWBiYARCIBPIYmAE8thAbEYOBkwAkgKSrEBlzAALaABL') as a decorator on your test case\r\n```\r\n\n\n### How can we reproduce the issue?\n\nGiven a circuit:\r\n```python\r\nimport qiskit\r\n\r\nqc = qiskit.QuantumCircuit(3, 1)\r\nqc.ccx(2, 1, 0)\r\nqc.h(0)\r\nqc.measure(0, 0)\r\nqc.measure(1, 0)\r\n```\r\nwhich has a drawing\r\n```text\r\n     ┌───┐┌───┐┌─┐\r\nq_0: ┤ X ├┤ H ├┤M├───\r\n     └─┬─┘└───┘└╥┘┌─┐\r\nq_1: ──■────────╫─┤M├\r\n       │        ║ └╥┘\r\nq_2: ──■────────╫──╫─\r\n                ║  ║\r\nc: 1/═══════════╩══╩═\r\n                0  0\r\n```\r\nClearly the output of the circuit should always be 0, because qubit 1 is never altered, and is measured second.\r\n\r\nIf we transpile it with a special coupling map that essentially only has one valid routing for the default decomposition of ccx, the orders of the measurements get flipped, which Aer confirms:\r\n```python\r\ntranspiled = qiskit.transpile(qc, coupling_map=[[(0, 2), (2, 0), (1, 2), (2, 1)]], routing_method="sabre")\r\nbackend = qiskit.Aer.get_backend("aer_simulator")\r\nprint(backend.run(qc, shots=1024).result().get_counts())\r\nprint(backend.run(transpiled, shots=1024).result().get_counts())\r\n```\r\nThis produces, for example:\r\n```python\r\n{'0': 1024}\r\n{'1': 503, '0': 521}\r\n```\r\nbut the second dictionary should be the same as the first.\r\n\r\n(It's too long to draw `transpiled` here, but if you do it locally, you can see the measurements are swapped.)\n\n### What should happen?\n\nSabre should take into account the topological ordering of measurements (and other clbits).  In the above examples, the counts should both be `{'0': 1024}` (which they are for any other routing method).\n\n### Any suggestions?\n\nIn both `SabreSwap._successors` and `._is_resolved`, only the quantum bits are accounted for.  My very very rough intuition for what's going on makes it feel like it should account for _all_ successors, not just qubits.

diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py\nindex 3690c763f734..5e44b29e5d13 100644\n--- a/qiskit/transpiler/passes/routing/sabre_swap.py\n+++ b/qiskit/transpiler/passes/routing/sabre_swap.py\n@@ -18,7 +18,6 @@\n import numpy as np\n \n from qiskit.circuit.library.standard_gates import SwapGate\n-from qiskit.circuit.quantumregister import Qubit\n from qiskit.transpiler.basepasses import TransformationPass\n from qiskit.transpiler.exceptions import TranspilerError\n from qiskit.transpiler.layout import Layout\n@@ -276,15 +275,18 @@ def _reset_qubits_decay(self):\n         self.qubits_decay = {k: 1 for k in self.qubits_decay.keys()}\n \n     def _successors(self, node, dag):\n-        for _, successor, edge_data in dag.edges(node):\n-            if not isinstance(successor, DAGOpNode):\n-                continue\n-            if isinstance(edge_data, Qubit):\n+        """Return an iterable of the successors along each wire from the given node.\n+\n+        This yields the same successor multiple times if there are parallel wires (e.g. two adjacent\n+        operations that have one clbit and qubit in common), which is important in the swapping\n+        algorithm for detecting if each wire has been accounted for."""\n+        for _, successor, _ in dag.edges(node):\n+            if isinstance(successor, DAGOpNode):\n                 yield successor\n \n     def _is_resolved(self, node):\n         """Return True if all of a node's predecessors in dag are applied."""\n-        return self.applied_predecessors[node] == len(node.qargs)\n+        return self.applied_predecessors[node] == len(node.qargs) + len(node.cargs)\n \n     def _obtain_extended_set(self, dag, front_layer):\n         """Populate extended_set by looking ahead a fixed number of gates.\ndiff --git a/releasenotes/notes/fix-sabreswap-clbits-428eb5f3a46063da.yaml b/releasenotes/notes/fix-sabreswap-clbits-428eb5f3a46063da.yaml\nnew file mode 100644\nindex 000000000000..0401e5a36f74\n--- /dev/null\n+++ b/releasenotes/notes/fix-sabreswap-clbits-428eb5f3a46063da.yaml\n@@ -0,0 +1,9 @@\n+---\n+fixes:\n+  - |\n+    Fixed :class:`.SabreSwap`, and by extension :func:`.transpile` with\n+    ``optimization_level=3``, occasionally re-ordering measurements invalidly.\n+    Previously, if two measurements wrote to the same classical bit,\n+    :class:`.SabreSwap` could (depending on the coupling map) re-order them to\n+    produce a non-equivalent circuit.  This behaviour was stochastic, so may\n+    not have appeared reliably.\n

diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py\nindex a19f574a305d..509225f65e75 100644\n--- a/test/python/transpiler/test_sabre_swap.py\n+++ b/test/python/transpiler/test_sabre_swap.py\n@@ -13,6 +13,7 @@\n """Test the Sabre Swap pass"""\n \n import unittest\n+from qiskit.circuit.library import CCXGate, HGate, Measure\n from qiskit.transpiler.passes import SabreSwap\n from qiskit.transpiler import CouplingMap, PassManager\n from qiskit import QuantumRegister, QuantumCircuit\n@@ -95,6 +96,34 @@ def test_do_not_change_cm(self):\n \n         self.assertEqual(set(cm_edges), set(coupling.get_edges()))\n \n+    def test_do_not_reorder_measurements(self):\n+        """Test that SabreSwap doesn't reorder measurements to the same classical bit.\n+\n+        With the particular coupling map used in this test and the 3q ccx gate, the routing would\n+        invariably the measurements if the classical successors are not accurately tracked.\n+        Regression test of gh-7950."""\n+        coupling = CouplingMap([(0, 2), (2, 0), (1, 2), (2, 1)])\n+        qc = QuantumCircuit(3, 1)\n+        qc.compose(CCXGate().definition, [0, 1, 2], [])  # Unroll CCX to 2q operations.\n+        qc.h(0)\n+        qc.barrier()\n+        qc.measure(0, 0)  # This measure is 50/50 between the Z states.\n+        qc.measure(1, 0)  # This measure always overwrites with 0.\n+        passmanager = PassManager(SabreSwap(coupling))\n+        transpiled = passmanager.run(qc)\n+\n+        last_h, h_qubits, _ = transpiled.data[-4]\n+        self.assertIsInstance(last_h, HGate)\n+        first_measure, first_measure_qubits, _ = transpiled.data[-2]\n+        second_measure, second_measure_qubits, _ = transpiled.data[-1]\n+        self.assertIsInstance(first_measure, Measure)\n+        self.assertIsInstance(second_measure, Measure)\n+        # Assert that the first measure is on the same qubit that the HGate was applied to, and the\n+        # second measurement is on a different qubit (though we don't care which exactly - that\n+        # depends a little on the randomisation of the pass).\n+        self.assertEqual(h_qubits, first_measure_qubits)\n+        self.assertNotEqual(h_qubits, second_measure_qubits)\n+\n \n if __name__ == "__main__":\n     unittest.main()\n