Repository: Qiskit/qiskit — pull request #7952 (python)
Title: Make SabreSwap account for clbits in predecessors

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

Resolved issues:
title: `SabreSwap` can reorder operations that depend on the same classical bit
body: ### 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\n<details><summary>Expand for logs of recent failed randomised run</summary>\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</details>\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.

Hints:

Fix patch:
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

Test patch:
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