Repository: PennyLaneAI/pennylane
Pull request #5106 (2024-01-25 10:58:17)

Allow `Identity()` to have no wires
Allows `Identity()` to have no wires, similar to e.g. `GlobalPhase`

Also updating `PauliWord.operation()` to return `qml.Identity()` and `PauliSentence.operation()` to return `qml.s_prod(0, qml.Identity())` in case of empty word and sentence, respectively.

As a temporary fix, we explicitly dont allow expvals of `Identity()`, but plan to fix this by updating hamiltonian_expand and sum_expand immediately after.

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.
Qottmann: [sc-55334]
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5106?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:
> Comparison is base [(`5cfdedd`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/5cfdeddbb59a7147146a0c550b8e1099de0cc022?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.36% compared to head [(`b68c356`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5106?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68%.


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


```diff
@@            Coverage Diff             @@
##           master    #5106      +/-   ##
==========================================
+ Coverage   99.36%   99.68%   +0.31%     
==========================================
  Files         392      392              
  Lines       35850    35581     -269     
==========================================
- Hits        35623    35468     -155     
+ Misses        227      113     -114     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5106?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).

Qottmann: For the moment, we don't allow measuring `qml.Identity()` as there are some non-trivial concerns for who should handle the case (pennylane or the device). Trying to measure qml.Identity() on default qubit raises `ValueError: Cannot set an empty list of wires.`. However, the lightning error is cryptic and needs to be improved: `ValueError: shapes (2,) and (1,) not aligned: 2 (dim 0) != 1 (dim 0)`

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index c5df3b7ec..0da8efd7f 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -153,6 +153,9 @@
   and the codecov check itself would never execute.
   [(#5101)](https://github.com/PennyLaneAI/pennylane/pull/5101)
 
+* `qml.Identity()` can be initialized without wires. Measuring it is currently not possible though.
+  [(#5106)](https://github.com/PennyLaneAI/pennylane/pull/5106)
+
 <h4>Community contributions 🥳</h4>
 
 * The transform `split_non_commuting` now accepts measurements of type `probs`, `sample` and `counts` which accept both wires and observables.
diff --git a/pennylane/measurements/expval.py b/pennylane/measurements/expval.py
index 74bf3c5b1..9ecc66cae 100644
--- a/pennylane/measurements/expval.py
+++ b/pennylane/measurements/expval.py
@@ -62,6 +62,12 @@ def expval(op: Union[Operator, MeasurementValue]):
             "qml.expval does not support measuring sequences of measurements or observables"
         )
 
+    if isinstance(op, qml.Identity) and len(op.wires) == 0:
+        # temporary solution to merge https://github.com/PennyLaneAI/pennylane/pull/5106
+        raise NotImplementedError(
+            "Expectation values of qml.Identity() without wires are currently not allowed."
+        )
+
     if not op.is_hermitian:
         warnings.warn(f"{op.name} might not be hermitian.")
 
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 9f3d33ef7..03aabf22e 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1043,6 +1043,7 @@ class Operator(abc.ABC):
 
     def __init__(self, *params, wires=None, id=None):
         # pylint: disable=too-many-branches
+
         self._name = self.__class__.__name__  #: str: name of the operator
         self._id = id
         self.queue_idx = None  #: int, None: index of the Operator in the circuit queue, or None if not in a queue
@@ -1077,7 +1078,10 @@ class Operator(abc.ABC):
         # check that the number of wires given corresponds to required number
         if self.num_wires in {AllWires, AnyWires}:
             if (
-                not isinstance(self, (qml.Barrier, qml.Snapshot, qml.Hamiltonian, qml.GlobalPhase))
+                not isinstance(
+                    self,
+                    (qml.Barrier, qml.Snapshot, qml.Hamiltonian, qml.GlobalPhase, qml.Identity),
+                )
                 and len(qml.wires.Wires(wires)) == 0
             ):
                 raise ValueError(
diff --git a/pennylane/ops/identity.py b/pennylane/ops/identity.py
index 36b47a9af..30a342f06 100644
--- a/pennylane/ops/identity.py
+++ b/pennylane/ops/identity.py
@@ -54,8 +54,8 @@ class Identity(CVObservable, Operation):
     def _flatten(self):
         return tuple(), (self.wires, tuple())
 
-    def __init__(self, *params, wires=None, id=None):
-        super().__init__(*params, wires=wires, id=id)
+    def __init__(self, wires=None, id=None):
+        super().__init__(wires=[] if wires is None else wires, id=id)
         self._hyperparameters = {"n_wires": len(self.wires)}
         self._pauli_rep = qml.pauli.PauliSentence({qml.pauli.PauliWord({}): 1.0})
 
@@ -113,6 +113,10 @@ class Identity(CVObservable, Operation):
     def compute_sparse_matrix(n_wires=1):  # pylint: disable=arguments-differ
         return sparse.eye(int(2**n_wires), format="csr")
 
+    def matrix(self, wire_order=None):
+        n_wires = len(wire_order) if wire_order else len(self.wires)
+        return self.compute_matrix(n_wires=n_wires)
+
     @staticmethod
     def _heisenberg_rep(p):
         return qml.math.array([1, 0, 0])
diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py
index 888590656..470a9a44a 100644
--- a/pennylane/pauli/pauli_arithmetic.py
+++ b/pennylane/pauli/pauli_arithmetic.py
@@ -429,8 +429,6 @@ class PauliWord(dict):
     def operation(self, wire_order=None, get_as_tensor=False):
         """Returns a native PennyLane :class:`~pennylane.operation.Operation` representing the PauliWord."""
         if len(self) == 0:
-            if wire_order in (None, [], Wires([])):
-                raise ValueError("Can't get the operation for an empty PauliWord.")
             return Identity(wires=wire_order)
 
         factors = [op_map[op](wire) for wire, op in self.items()]
@@ -814,8 +812,6 @@ class PauliSentence(dict):
     def operation(self, wire_order=None):
         """Returns a native PennyLane :class:`~pennylane.operation.Operation` representing the PauliSentence."""
         if len(self) == 0:
-            if wire_order in (None, [], Wires([])):
-                raise ValueError("Can't get the operation for an empty PauliSentence.")
             return qml.s_prod(0, Identity(wires=wire_order))
 
         summands = []


Test patch:
diff --git a/tests/devices/default_qubit/test_default_qubit.py b/tests/devices/default_qubit/test_default_qubit.py
index 35a76028f..67d0ee4a5 100644
--- a/tests/devices/default_qubit/test_default_qubit.py
+++ b/tests/devices/default_qubit/test_default_qubit.py
@@ -23,6 +23,8 @@ import pennylane as qml
 
 from pennylane.devices import DefaultQubit, ExecutionConfig
 
+np.random.seed(0)
+
 
 def test_name():
     """Tests the name of DefaultQubit."""
diff --git a/tests/measurements/test_expval.py b/tests/measurements/test_expval.py
index 135cfaa7b..68577c718 100644
--- a/tests/measurements/test_expval.py
+++ b/tests/measurements/test_expval.py
@@ -21,6 +21,30 @@ from pennylane.measurements import Expectation, Shots
 from pennylane.measurements.expval import ExpectationMP
 
 
+def test_expval_identity_nowires_DQ():
+    """Test that attempting to measure qml.Identity() with no wires raises an explicit error in default.qubit"""
+
+    # temporary solution to merge https://github.com/PennyLaneAI/pennylane/pull/5106
+    @qml.qnode(qml.device("default.qubit"))
+    def qnode():
+        return qml.expval(qml.Identity())
+
+    with pytest.raises(NotImplementedError, match="Expectation values of qml.Identity()"):
+        _ = qnode()
+
+
+def test_expval_identity_nowires_LQ():
+    """Test that attempting to measure qml.Identity() with no wires raises an explicit error in lightning.qubit"""
+
+    # temporary solution to merge https://github.com/PennyLaneAI/pennylane/pull/5106
+    @qml.qnode(qml.device("lightning.qubit", wires=[0]))
+    def qnode():
+        return qml.expval(qml.Identity())
+
+    with pytest.raises(NotImplementedError, match="Expectation values of qml.Identity()"):
+        _ = qnode()
+
+
 class TestExpval:
     """Tests for the expval function"""
 
diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py
index cfddda99d..791a6043d 100644
--- a/tests/ops/qubit/test_parametric_ops.py
+++ b/tests/ops/qubit/test_parametric_ops.py
@@ -726,6 +726,20 @@ class TestMatrix:
         assert np.allclose(qml.GlobalPhase.compute_matrix(phi), expected, atol=tol, rtol=0)
         assert np.allclose(qml.GlobalPhase(phi).matrix(wire_order=[0]), expected, atol=tol, rtol=0)
 
+    def test_identity(self, tol):
+        """Test Identity matrix is correct with no wires"""
+
+        # test Identity().compute_matrix()
+        assert np.allclose(qml.Identity().compute_matrix(1), np.identity(2), atol=tol, rtol=0)
+        assert np.allclose(qml.Identity().compute_matrix(2), np.identity(4), atol=tol, rtol=0)
+
+        # test Identity().matrix()
+        assert np.allclose(qml.Identity().matrix(), np.identity(1), atol=tol, rtol=0)
+        assert np.allclose(qml.Identity().matrix(wire_order=[0]), np.identity(2), atol=tol, rtol=0)
+        assert np.allclose(
+            qml.Identity().matrix(wire_order=[0, "a"]), np.identity(4), atol=tol, rtol=0
+        )
+
     def test_rx(self, tol):
         """Test x rotation is correct"""
 
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
index 70e2691aa..7aa688afb 100644
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -439,11 +439,10 @@ class TestPauliWord:
         assert op.name == id.name
         assert op.wires == id.wires
 
-    def test_operation_empty_error(self):
-        """Test that a ValueError is raised if an empty PauliWord is
-        cast to a PL operation."""
-        with pytest.raises(ValueError, match="Can't get the operation for an empty PauliWord."):
-            pw4.operation()
+    def test_operation_empty_nowires(self):
+        """Test that an empty PauliWord is cast to qml.Identity() operation."""
+        res = pw4.operation()
+        assert res == qml.Identity()
 
     tup_pw_hamiltonian = (
         (PauliWord({0: X}), 1 * qml.PauliX(wires=0)),
@@ -996,13 +995,13 @@ class TestPauliSentence:
         assert op.name == id.name
         assert op.wires == id.wires
 
-    def test_operation_empty_error(self):
+    def test_operation_empty_nowires(self):
         """Test that a ValueError is raised if an empty PauliSentence is
         cast to a PL operation."""
-        with pytest.raises(ValueError, match="Can't get the operation for an empty PauliWord."):
-            ps4.operation()
-        with pytest.raises(ValueError, match="Can't get the operation for an empty PauliSentence."):
-            ps5.operation()
+        res1 = ps4.operation()
+        assert res1 == qml.Identity()
+        res2 = ps5.operation()
+        assert res2 == qml.s_prod(0, qml.Identity())
 
     def test_operation_wire_order(self):
         """Test that the wire_order parameter is used when the pauli representation is empty"""