Repository: Qiskit/qiskit — pull request #14623 (python)
Title: Fix `ElidePermutations` pass in the presence of `PermutationGate`s (backport #14603)

\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).

Resolved issues:
title: ElidePermutations does not work correctly with PermutationGates
body: ### 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. 

Hints:

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

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