Repository: Qiskit/qiskit — pull request #14730 (python)
Title: Fix non-determinism VF2Layout with only 1q gates

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

Resolved issues:
title: Transpile with seed_transpiler doesn't yield the same circuit
body: ### 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?

Hints:

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

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