Repository: PennyLaneAI/pennylane
Pull request #7760 (2025-06-27 18:22:20)

Allow `PauliError` to accept `I`
**Context:** Was trying to implement this [paper](https://journals.aps.org/prxquantum/pdf/10.1103/PRXQuantum.6.010310) with reference to their provided code [here](https://github.com/zhihan-z/MCM_Learnability/tree/master) and was blocked by this.

**Description of the Change:** Allows for `I` to be passed to `PauliError`

**Benefits:** Moar

**Possible Drawbacks:** not sure

**Related GitHub Issues:** 🫥 


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7760?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.68%. Comparing base [(`8a1453c`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/8a1453c16eac18d6a4ceb4ab41ce735797a95f5c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`9f24c5a`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/9f24c5aa5888f724d65fa804c5ba63f76cf3b36a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #7760   +/-   ##
=======================================
  Coverage   99.68%   99.68%           
=======================================
  Files         542      542           
  Lines       55093    55094    +1     
=======================================
+ Hits        54920    54921    +1     
  Misses        173      173           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7760?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

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