Repository: Qiskit/qiskit — pull request #12095 (python)
Title: Support ECR gates in `Pauli.evolve(QuantumCircuit)`

<!--\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- [ ] I have updated the documentation accordingly.\r\n- [X] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\nShould fix #12086 and also #12093.\r\n\r\n\r\n### Details and comments\r\n\r\n\r\n

Resolved issues:
title: `Pauli.evolve()` should recognize ECR gates as Clifford gates
body: ### Environment\n\n- **Qiskit version**: 1.0.2\r\n- **Python version**: 3.11\r\n- **Operating system**: macos\r\n\n\n### What is happening?\n\n```\r\nqc = QuantumCircuit(2)\r\nqc.ecr(0,1)\r\nqi.Pauli('XX').evolve(qc)\r\n```\r\n\r\nThe above call to evolve() raises an error even though qc contains only Clifford gates:\r\n\r\n```\r\nQiskitError: 'Cannot apply Instruction: u'\r\n```\n\n### How can we reproduce the issue?\n\n```\r\nqc = QuantumCircuit(2)\r\nqc.ecr(0,1)\r\nqi.Pauli('XX').evolve(qc)\r\n```\n\n### What should happen?\n\nECR gates should be recognized as Clifford gates, and the Pauli should evolve successfully.\r\n\r\nRelated: #12085 \n\n### Any suggestions?\n\n_No response_

Hints:

Fix patch:
diff --git a/qiskit/quantum_info/operators/symplectic/base_pauli.py b/qiskit/quantum_info/operators/symplectic/base_pauli.py\nindex 28bb647d0364..e43eca4aff21 100644\n--- a/qiskit/quantum_info/operators/symplectic/base_pauli.py\n+++ b/qiskit/quantum_info/operators/symplectic/base_pauli.py\n@@ -541,75 +541,50 @@ def _append_circuit(self, circuit, qargs=None):\n         if qargs is None:\n             qargs = list(range(self.num_qubits))\n \n-        if isinstance(circuit, QuantumCircuit):\n-            gate = circuit.to_instruction()\n-        else:\n+        if not isinstance(circuit, QuantumCircuit):\n             gate = circuit\n \n-        # Basis Clifford Gates\n-        basis_1q = {\n-            "i": _evolve_i,\n-            "id": _evolve_i,\n-            "iden": _evolve_i,\n-            "x": _evolve_x,\n-            "y": _evolve_y,\n-            "z": _evolve_z,\n-            "h": _evolve_h,\n-            "s": _evolve_s,\n-            "sdg": _evolve_sdg,\n-            "sinv": _evolve_sdg,\n-        }\n-        basis_2q = {"cx": _evolve_cx, "cz": _evolve_cz, "cy": _evolve_cy, "swap": _evolve_swap}\n-\n-        # Non-Clifford gates\n-        non_clifford = ["t", "tdg", "ccx", "ccz"]\n-\n-        if isinstance(gate, str):\n-            # Check if gate is a valid Clifford basis gate string\n-            if gate not in basis_1q and gate not in basis_2q:\n-                raise QiskitError(f"Invalid Clifford gate name string {gate}")\n-            name = gate\n-        else:\n-            # Assume gate is an Instruction\n-            name = gate.name\n-\n-        # Apply gate if it is a Clifford basis gate\n-        if name in non_clifford:\n-            raise QiskitError(f"Cannot update Pauli with non-Clifford gate {name}")\n-        if name in basis_1q:\n-            if len(qargs) != 1:\n-                raise QiskitError("Invalid qubits for 1-qubit gate.")\n-            return basis_1q[name](self, qargs[0])\n-        if name in basis_2q:\n-            if len(qargs) != 2:\n-                raise QiskitError("Invalid qubits for 2-qubit gate.")\n-            return basis_2q[name](self, qargs[0], qargs[1])\n-\n-        # If not a Clifford basis gate we try to unroll the gate and\n-        # raise an exception if unrolling reaches a non-Clifford gate.\n-        if gate.definition is None:\n-            raise QiskitError(f"Cannot apply Instruction: {gate.name}")\n-        if not isinstance(gate.definition, QuantumCircuit):\n-            raise QiskitError(\n-                "{} instruction definition is {}; expected QuantumCircuit".format(\n-                    gate.name, type(gate.definition)\n+            if isinstance(gate, str):\n+                # Check if gate is a valid Clifford basis gate string\n+                if gate not in _basis_1q and gate not in _basis_2q:\n+                    raise QiskitError(f"Invalid Clifford gate name string {gate}")\n+                name = gate\n+            else:\n+                # Assume gate is an Instruction\n+                name = gate.name\n+\n+            # Apply gate if it is a Clifford basis gate\n+            if name in _non_clifford:\n+                raise QiskitError(f"Cannot update Pauli with non-Clifford gate {name}")\n+            if name in _basis_1q:\n+                if len(qargs) != 1:\n+                    raise QiskitError("Invalid qubits for 1-qubit gate.")\n+                return _basis_1q[name](self, qargs[0])\n+            if name in _basis_2q:\n+                if len(qargs) != 2:\n+                    raise QiskitError("Invalid qubits for 2-qubit gate.")\n+                return _basis_2q[name](self, qargs[0], qargs[1])\n+\n+            # If not a Clifford basis gate we try to unroll the gate and\n+            # raise an exception if unrolling reaches a non-Clifford gate.\n+            if gate.definition is None:\n+                raise QiskitError(f"Cannot apply Instruction: {gate.name}")\n+            if not isinstance(gate.definition, QuantumCircuit):\n+                raise QiskitError(\n+                    "{} instruction definition is {}; expected QuantumCircuit".format(\n+                        gate.name, type(gate.definition)\n+                    )\n                 )\n-            )\n \n-        flat_instr = gate.definition\n-        bit_indices = {\n-            bit: index\n-            for bits in [flat_instr.qubits, flat_instr.clbits]\n-            for index, bit in enumerate(bits)\n-        }\n+            circuit = gate.definition\n \n-        for instruction in flat_instr:\n+        for instruction in circuit:\n             if instruction.clbits:\n                 raise QiskitError(\n                     f"Cannot apply Instruction with classical bits: {instruction.operation.name}"\n                 )\n             # Get the integer position of the flat register\n-            new_qubits = [qargs[bit_indices[tup]] for tup in instruction.qubits]\n+            new_qubits = [qargs[circuit.find_bit(qb)[0]] for qb in instruction.qubits]\n             self._append_circuit(instruction.operation, new_qubits)\n \n         # Since the individual gate evolution functions don't take mod\n@@ -715,6 +690,42 @@ def _evolve_swap(base_pauli, q1, q2):\n     return base_pauli\n \n \n+def _evolve_ecr(base_pauli, q1, q2):\n+    """Update P -> ECR.P.ECR"""\n+    base_pauli = _evolve_s(base_pauli, q1)\n+    base_pauli = _evolve_h(base_pauli, q2)\n+    base_pauli = _evolve_s(base_pauli, q2)\n+    base_pauli = _evolve_h(base_pauli, q2)\n+    base_pauli = _evolve_cx(base_pauli, q1, q2)\n+    base_pauli = _evolve_x(base_pauli, q1)\n+    return base_pauli\n+\n+\n def _count_y(x, z, dtype=None):\n     """Count the number of I Paulis"""\n     return (x & z).sum(axis=1, dtype=dtype)\n+\n+\n+# Basis Clifford Gates\n+_basis_1q = {\n+    "i": _evolve_i,\n+    "id": _evolve_i,\n+    "iden": _evolve_i,\n+    "x": _evolve_x,\n+    "y": _evolve_y,\n+    "z": _evolve_z,\n+    "h": _evolve_h,\n+    "s": _evolve_s,\n+    "sdg": _evolve_sdg,\n+    "sinv": _evolve_sdg,\n+}\n+_basis_2q = {\n+    "cx": _evolve_cx,\n+    "cz": _evolve_cz,\n+    "cy": _evolve_cy,\n+    "swap": _evolve_swap,\n+    "ecr": _evolve_ecr,\n+}\n+\n+# Non-Clifford gates\n+_non_clifford = ["t", "tdg", "ccx", "ccz"]\ndiff --git a/releasenotes/notes/fix-pauli-evolve-ecr-and-name-bugs.yaml b/releasenotes/notes/fix-pauli-evolve-ecr-and-name-bugs.yaml\nnew file mode 100644\nindex 000000000000..143dfadf31e2\n--- /dev/null\n+++ b/releasenotes/notes/fix-pauli-evolve-ecr-and-name-bugs.yaml\n@@ -0,0 +1,6 @@\n+---\n+fixes:\n+  - |\n+    :meth:`.Pauli.evolve` now correctly handles quantum circuits containing ECR gates. Formerly they were not recognized as Clifford gates, and an error was raised.\n+  - |\n+    Fixed a bug in :meth:`.Pauli.evolve` where evolving by a circuit with a name matching certain Clifford gates ('cx', 'cz', etc) would evolve the Pauli according to the name of the circuit, not by the contents of the circuit. This bug occurred only with the non-default option ``frame='s'``.\n

Test patch:
diff --git a/test/python/quantum_info/operators/symplectic/test_pauli.py b/test/python/quantum_info/operators/symplectic/test_pauli.py\nindex c760407ad3fa..875dd9237810 100644\n--- a/test/python/quantum_info/operators/symplectic/test_pauli.py\n+++ b/test/python/quantum_info/operators/symplectic/test_pauli.py\n@@ -36,6 +36,7 @@\n     CZGate,\n     CYGate,\n     SwapGate,\n+    ECRGate,\n     EfficientSU2,\n )\n from qiskit.circuit.library.generalized_gates import PauliGate\n@@ -410,7 +411,11 @@ def test_evolve_clifford1(self, gate, label):\n         self.assertEqual(value, value_h)\n         self.assertEqual(value_inv, value_s)\n \n-    @data(*it.product((CXGate(), CYGate(), CZGate(), SwapGate()), pauli_group_labels(2, False)))\n+    @data(\n+        *it.product(\n+            (CXGate(), CYGate(), CZGate(), SwapGate(), ECRGate()), pauli_group_labels(2, False)\n+        )\n+    )\n     @unpack\n     def test_evolve_clifford2(self, gate, label):\n         """Test evolve method for 2-qubit Clifford gates."""\n@@ -439,6 +444,7 @@ def test_evolve_clifford2(self, gate, label):\n                 CYGate(),\n                 CZGate(),\n                 SwapGate(),\n+                ECRGate(),\n             ),\n             [int, np.int8, np.uint8, np.int16, np.uint16, np.int32, np.uint32, np.int64, np.uint64],\n         )\n@@ -468,6 +474,13 @@ def test_evolve_clifford_qargs(self):\n         self.assertEqual(value, value_h)\n         self.assertEqual(value_inv, value_s)\n \n+    @data("s", "h")\n+    def test_evolve_with_misleading_name(self, frame):\n+        """Test evolve by circuit contents, not by name (fixed bug)."""\n+        circ = QuantumCircuit(2, name="cx")\n+        p = Pauli("IX")\n+        self.assertEqual(p, p.evolve(circ, frame=frame))\n+\n     def test_barrier_delay_sim(self):\n         """Test barrier and delay instructions can be simulated"""\n         target_circ = QuantumCircuit(2)\ndiff --git a/test/python/quantum_info/operators/symplectic/test_pauli_list.py b/test/python/quantum_info/operators/symplectic/test_pauli_list.py\nindex 7ab6feaefe98..0ef7079f461a 100644\n--- a/test/python/quantum_info/operators/symplectic/test_pauli_list.py\n+++ b/test/python/quantum_info/operators/symplectic/test_pauli_list.py\n@@ -33,6 +33,7 @@\n     XGate,\n     YGate,\n     ZGate,\n+    ECRGate,\n )\n from qiskit.quantum_info.operators import (\n     Clifford,\n@@ -1997,10 +1998,12 @@ def test_evolve_clifford1(self, gate):\n             CYGate(),\n             CZGate(),\n             SwapGate(),\n+            ECRGate(),\n             Clifford(CXGate()),\n             Clifford(CYGate()),\n             Clifford(CZGate()),\n             Clifford(SwapGate()),\n+            Clifford(ECRGate()),\n         )\n     )\n     def test_evolve_clifford2(self, gate):\n@@ -2033,6 +2036,7 @@ def test_phase_dtype_evolve_clifford(self):\n             CYGate(),\n             CZGate(),\n             SwapGate(),\n+            ECRGate(),\n         )\n         dtypes = [\n             int,\n