Repository: PennyLaneAI/pennylane
Pull request #7772 (2025-07-02 13:45:08)

[Python Compiler] Do not use AnyAttr in AnyOf
**Context:** Recent changes in xDSL restrict the use of AnyAttr in AnyOf. See PR [here](https://github.com/xdslproject/xdsl/pull/4510)

**Description of the Change:** We don't need to say `AnyOf(AnyAttr(), AnyAttr())`. We can just say `AnyAttr()` and it means the same thing. We could make this check stricter by using the correct types which in most of the cases are TensorTypes or MemrefTypes, but I think this is ok for now.

**Benefits:** The latest version of xDSL works with the Python compiler.

**Possible Drawbacks:** None.

**Related GitHub Issues:**



Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 58fb73553..7a096dc12 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -597,6 +597,9 @@
 
 <h4>Other improvements</h4>
 
+* `qml.PauliError` now accepts Pauli strings that include the identity operator.
+  [(#7760)](https://github.com/PennyLaneAI/pennylane/pull/7760)
+
 * Caching with finite shots now always warns about the lack of expected noise.
   [(#7644)](https://github.com/PennyLaneAI/pennylane/pull/7644)
 
diff --git a/pennylane/compiler/python_compiler/quantum_dialect.py b/pennylane/compiler/python_compiler/quantum_dialect.py
index 1c25a18c1..f91f30cd1 100644
--- a/pennylane/compiler/python_compiler/quantum_dialect.py
+++ b/pennylane/compiler/python_compiler/quantum_dialect.py
@@ -29,7 +29,6 @@ from typing import Sequence, TypeAlias
 
 from xdsl.dialects.builtin import (
     AnyAttr,
-    AnyOf,
     BaseAttr,
     Float64Type,
     IntegerAttr,
@@ -469,7 +468,7 @@ class HamiltonianOp(IRDLOperation):
         `(` $coeffs `:` type($coeffs) `)` $terms attr-dict `:` type(results)
     """
 
-    coeffs = operand_def(AnyOf((AnyAttr(), AnyAttr())))
+    coeffs = operand_def(AnyAttr())
 
     terms = var_operand_def(BaseAttr(ObservableType))
 
@@ -486,7 +485,7 @@ class HermitianOp(IRDLOperation):
         `(` $matrix `:` type($matrix) `)` $qubits attr-dict `:` type(results)
     """
 
-    matrix = operand_def(AnyOf((AnyAttr(), AnyAttr())))
+    matrix = operand_def(AnyAttr())
 
     qubits = var_operand_def(BaseAttr(QubitType))
 
@@ -683,7 +682,7 @@ class QubitUnitaryOp(IRDLOperation):
         AttrSizedResultSegments(as_property=True),
     ]
 
-    matrix = operand_def(AnyOf((AnyAttr(), AnyAttr())))
+    matrix = operand_def(AnyAttr())
 
     in_qubits = var_operand_def(BaseAttr(QubitType))
 
@@ -716,9 +715,9 @@ class SampleOp(IRDLOperation):
 
     dynamic_shape = var_operand_def(EqAttrConstraint(IntegerType(64)))
 
-    in_data = opt_operand_def(AnyOf((AnyAttr(), AnyAttr())))
+    in_data = opt_operand_def(AnyAttr())
 
-    samples = opt_result_def(AnyOf((AnyAttr(), AnyAttr())))
+    samples = opt_result_def(AnyAttr())
 
 
 @irdl_op_definition
@@ -731,7 +730,7 @@ class SetBasisStateOp(IRDLOperation):
         `(` $basis_state`)` $in_qubits attr-dict `:` functional-type(operands, results)
     """
 
-    basis_state = operand_def(AnyOf((AnyAttr(), AnyAttr())))
+    basis_state = operand_def(AnyAttr())
 
     in_qubits = var_operand_def(BaseAttr(QubitType))
 
@@ -748,7 +747,7 @@ class SetStateOp(IRDLOperation):
         `(` $in_state `)` $in_qubits attr-dict `:` functional-type(operands, results)
     """
 
-    in_state = operand_def(AnyOf((AnyAttr(), AnyAttr())))
+    in_state = operand_def(AnyAttr())
 
     in_qubits = var_operand_def(BaseAttr(QubitType))
 
diff --git a/pennylane/io/qasm_interpreter.py b/pennylane/io/qasm_interpreter.py
index 23a332486..dd18c3338 100644
--- a/pennylane/io/qasm_interpreter.py
+++ b/pennylane/io/qasm_interpreter.py
@@ -667,7 +667,14 @@ class QasmInterpreter:
         Args:
             node (QASMNode): The QubitDeclaration QASMNode.
             context (Context): The current context.
+
+        Raises:
+            TypeError: if it is a qubit register declaration.
         """
+        if node.size is not None:
+            raise TypeError(
+                "Qubit registers are not yet supported, please declare each qubit individually."
+            )
         context.wires.append(node.qubit.name)
 
     @visit.register(ast.ClassicalAssignment)
diff --git a/pennylane/ops/channel.py b/pennylane/ops/channel.py
index d6be18afe..e63b5c8f3 100644
--- a/pennylane/ops/channel.py
+++ b/pennylane/ops/channel.py
@@ -543,7 +543,7 @@ class PauliError(Channel):
     * Number of parameters: 3
 
     Args:
-        operators (str): The Pauli operators acting on the specified (groups of) wires
+        operators (str): The Pauli operators (``'I'``, ``'X'``, ``'Y'``, or ``'Z'``) acting on the specified (groups of) wires
         p (float): The probability of the operator being applied
         wires (Sequence[int] or int): The wires the channel acts on
         id (str or None): String representing the operation (optional)
@@ -573,8 +573,8 @@ class PauliError(Channel):
         super().__init__(p, wires=wires, id=id)
 
         # check if the specified operators are legal
-        if not set(operators).issubset({"X", "Y", "Z"}):
-            raise ValueError("The specified operators need to be either of 'X', 'Y' or 'Z'")
+        if not set(operators).issubset({"X", "Y", "Z", "I"}):
+            raise ValueError("The specified operators need to be either of 'I', 'X', 'Y' or 'Z'.")
 
         # check if probabilities are legal
         if not np.is_abstract(p) and not 0.0 <= p <= 1.0:
@@ -638,6 +638,7 @@ class PauliError(Channel):
                 p = np.cast_like(p, 1j)
 
         ops = {
+            "I": np.convert_like(np.cast_like(np.eye(2), p), p),
             "X": np.convert_like(np.cast_like(np.array([[0, 1], [1, 0]]), p), p),
             "Y": np.convert_like(np.cast_like(np.array([[0, -1j], [1j, 0]]), p), p),
             "Z": np.convert_like(np.cast_like(np.array([[1, 0], [0, -1]]), p), p),


Test patch:
diff --git a/tests/io/qasm_interpreter/if_else.qasm b/tests/io/qasm_interpreter/if_else.qasm
index e67f3ea01..225a382c4 100644
--- a/tests/io/qasm_interpreter/if_else.qasm
+++ b/tests/io/qasm_interpreter/if_else.qasm
@@ -1,5 +1,5 @@
 bool target = false;
-qubit[1] q0;
+qubit q0;
 bit[2] bits = "00";
 
 if (target == bits[0]) {
diff --git a/tests/io/qasm_interpreter/loops.qasm b/tests/io/qasm_interpreter/loops.qasm
index 2e609c69c..d919758b3 100644
--- a/tests/io/qasm_interpreter/loops.qasm
+++ b/tests/io/qasm_interpreter/loops.qasm
@@ -1,4 +1,4 @@
-qubit[1] q0;
+qubit q0;
 bit[2] bits = "10";
 
 // while loop
diff --git a/tests/io/qasm_interpreter/nested_control_flow.qasm b/tests/io/qasm_interpreter/nested_control_flow.qasm
index 0ed161833..0c792df9f 100644
--- a/tests/io/qasm_interpreter/nested_control_flow.qasm
+++ b/tests/io/qasm_interpreter/nested_control_flow.qasm
@@ -1,5 +1,5 @@
 bool target = false;
-qubit[1] q0;
+qubit q0;
 bit[2] bits = "00";
 
 if (target == bits[0]) {
diff --git a/tests/io/qasm_interpreter/nested_end.qasm b/tests/io/qasm_interpreter/nested_end.qasm
index 49c7af98f..98a4cd6cf 100644
--- a/tests/io/qasm_interpreter/nested_end.qasm
+++ b/tests/io/qasm_interpreter/nested_end.qasm
@@ -1,5 +1,5 @@
 bool target = false;
-qubit[1] q0;
+qubit q0;
 bit[2] bits = "00";
 
 if (target == bits[0]) {
diff --git a/tests/io/qasm_interpreter/switch.qasm b/tests/io/qasm_interpreter/switch.qasm
index 7c574dba1..fdc95451e 100644
--- a/tests/io/qasm_interpreter/switch.qasm
+++ b/tests/io/qasm_interpreter/switch.qasm
@@ -1,4 +1,4 @@
-qubit[1] q0;
+qubit q0;
 
 switch (1) {
     case 1 {
diff --git a/tests/io/qasm_interpreter/test_interpreter.py b/tests/io/qasm_interpreter/test_interpreter.py
index 73c6ef4e8..f53f7b51f 100644
--- a/tests/io/qasm_interpreter/test_interpreter.py
+++ b/tests/io/qasm_interpreter/test_interpreter.py
@@ -595,11 +595,27 @@ class TestVariables:
         ):
             QasmInterpreter().interpret(ast, context={"wire_map": None, "name": "mutate-error"})
 
+    def test_declare_register(self):
+        # parse the QASM
+        ast = parse(
+            """
+            qubit[2] q;
+            """,
+            permissive=True,
+        )
+
+        with pytest.raises(
+            TypeError,
+            match="Qubit registers are not yet supported, "
+            "please declare each qubit individually.",
+        ):
+            QasmInterpreter().interpret(ast, context={"wire_map": None, "name": "qubit-register"})
+
     def test_retrieve_wire(self):
         # parse the QASM
         ast = parse(
             """
-            qubit[0] q;
+            qubit q;
             let s = q;
             """,
             permissive=True,
@@ -1071,7 +1087,7 @@ class TestGates:
             """
             qubit q0;
             qubit q1;
-            qubit[1] q2;
+            qubit q2;
             ccx q0, q2, q1;
             cswap q1, q2, q0;
             """,
diff --git a/tests/io/qasm_interpreter/updating_variables.qasm b/tests/io/qasm_interpreter/updating_variables.qasm
index 6c5c6e41f..b6b94bb4e 100644
--- a/tests/io/qasm_interpreter/updating_variables.qasm
+++ b/tests/io/qasm_interpreter/updating_variables.qasm
@@ -1,5 +1,5 @@
 int i = 1;
-qubit[1] q0;
+qubit q0;
 
 switch (i) {
     case 1 {
diff --git a/tests/ops/test_channel_ops.py b/tests/ops/test_channel_ops.py
index fbb579904..3799eb2a5 100644
--- a/tests/ops/test_channel_ops.py
+++ b/tests/ops/test_channel_ops.py
@@ -742,7 +742,7 @@ class TestPauliError:
     EXPECTED_MESSAGES = [
         "The number of operators must match the number of wires",
         "p must be in the interval \\[0,1\\]",
-        "The specified operators need to be either of 'X', 'Y' or 'Z'",
+        "The specified operators need to be either of 'I', 'X', 'Y' or 'Z'.",
         "Wires must be unique",
     ]
 
@@ -783,8 +783,8 @@ class TestPauliError:
 
         assert np.allclose(c.kraus_matrices(), expected_Ks, atol=tol, rtol=0)
 
-    OPERATORS = ["X", "XY", "ZX"]
-    WIRES = [[1], [0, 1], [3, 1]]
+    OPERATORS = ["X", "XY", "ZX", "ZI"]
+    WIRES = [[1], [0, 1], [3, 1], [1, 0]]
     EXPECTED_KS = [
         [
             np.sqrt(0.5) * np.eye(2),
@@ -817,6 +817,18 @@ class TestPauliError:
                 ]
             ),
         ],
+        [
+            np.sqrt(0.5) * np.eye(4),
+            np.sqrt(0.5)
+            * np.array(
+                [
+                    [1.0, 0.0, 0.0, 0.0],
+                    [0.0, 1.0, 0.0, 0.0],
+                    [0.0, 0.0, -1.0, 0.0],
+                    [0.0, 0.0, 0.0, -1.0],
+                ]
+            ),
+        ],
     ]
 
     @pytest.mark.parametrize(
@@ -837,9 +849,13 @@ class TestPauliError:
             -1 / (2 * qml.math.sqrt(1 - p)) * qml.math.eye(4),
             1 / (2 * qml.math.sqrt(p)) * (qml.math.diag([1j, -1j, 1j, -1j])[::-1]),
         ],
+        "ZI": lambda p: [
+            -1 / (2 * qml.math.sqrt(1 - p)) * qml.math.eye(4),
+            1 / (2 * qml.math.sqrt(p)) * (qml.math.diag([1, 1, -1, -1])),
+        ],
     }
 
-    @pytest.mark.parametrize("ops", ["X", "XY"])
+    @pytest.mark.parametrize("ops", ["X", "XY", "ZI"])
     @pytest.mark.autograd
     def test_kraus_jac_autograd(self, ops):
         p = pnp.array(0.43, requires_grad=True)
@@ -860,7 +876,7 @@ class TestPauliError:
         jac = jac_fn_real(p) + 1j * jac_fn_imag(p)
         assert qml.math.allclose(jac, self.expected_jac_fn[ops](p))
 
-    @pytest.mark.parametrize("ops", ["X", "XY"])
+    @pytest.mark.parametrize("ops", ["X", "XY", "ZI"])
     @pytest.mark.torch
     def test_kraus_jac_torch(self, ops):
         import torch
@@ -884,7 +900,7 @@ class TestPauliError:
             jac_real + 1j * jac_imag, self.expected_jac_fn[ops](p.detach().numpy())
         )
 
-    @pytest.mark.parametrize("ops", ["X", "XY"])
+    @pytest.mark.parametrize("ops", ["X", "XY", "ZI"])
     @pytest.mark.tf
     def test_kraus_jac_tf(self, ops):
         import tensorflow as tf
@@ -896,7 +912,7 @@ class TestPauliError:
         jac = tape.jacobian(out, p)
         assert qml.math.allclose(jac, self.expected_jac_fn[ops](p))
 
-    @pytest.mark.parametrize("ops", ["X", "XY"])
+    @pytest.mark.parametrize("ops", ["X", "XY", "ZI"])
     @pytest.mark.jax
     def test_kraus_jac_jax(self, ops):
         import jax