Repository: PennyLaneAI/pennylane
Pull request #5188 (2024-02-09 16:32:40)

[BUGFIX] Fix Pauli.to_mat() for empty PauliSentence and add case handling of empty wires
Fix https://github.com/PennyLaneAI/pennylane/issues/5187 and handle empty wire Identities since that is now allowed

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-commenter: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5188?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 [(`6b127b8`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/6b127b89f42f0c2e3759c80d6f4bced20e35a9df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.68% compared to head [(`dcd382b`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5188?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    #5188      +/-   ##
==========================================
- Coverage   99.68%   99.68%   -0.01%     
==========================================
  Files         394      394              
  Lines       36341    36067     -274     
==========================================
- Hits        36227    35952     -275     
- Misses        114      115       +1     
```



</details>

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

astralcai: Hi, can I ask why the matrix of an empty `PauliWord` is identity but of an empty `PauliSentence` is a zero-matrix?
Qottmann: > Hi, can I ask why the matrix of an empty PauliWord is identity but of an empty PauliSentence is a zero-matrix?

Yes, that is a good question! An empty PauliWord constitutes the identity with respect to multiplication, whereas and empty PauliSentence consistutes the identity with respect to addition. I added some comments in the `note` of the PauliWord and PauliSentence class to make that more clear.

Does that help?
astralcai: > > Hi, can I ask why the matrix of an empty PauliWord is identity but of an empty PauliSentence is a zero-matrix?
> 
> Yes, that is a good question! An empty PauliWord constitutes the identity with respect to multiplication, whereas and empty PauliSentence consistutes the identity with respect to addition. I added some comments in the `note` of the PauliWord and PauliSentence class to make that more clear.
> 
> Does that help?

Thank you. That makes sense!

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 92ecbc2fc..b72be8aa7 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -494,6 +494,9 @@
   operators have a valid `pauli_rep` property.
   [(#5177)](https://github.com/PennyLaneAI/pennylane/pull/5177)
 
+* The matrix of an empty `PauliSentence` instance is now correct (all-zeros).
+  Further, matrices of empty `PauliWord` and `PauliSentence` instances can be turned to matrices now.
+  [(#5188)](https://github.com/PennyLaneAI/pennylane/pull/5188)
 
 <h3>Contributors ✍️</h3>
 
diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py
index 60dd45377..241b29918 100644
--- a/pennylane/pauli/pauli_arithmetic.py
+++ b/pennylane/pauli/pauli_arithmetic.py
@@ -148,7 +148,9 @@ class PauliWord(dict):
     .. note::
 
         An empty :class:`~.PauliWord` will be treated as the multiplicative
-        identity (i.e identity on all wires).
+        identity (i.e identity on all wires). Its matrix is the identity matrix
+        (trivially the :math:`1\times 1` one matrix when no ``wire_order`` is passed to
+        ``PauliWord({}).to_mat()``).
 
     **Examples**
 
@@ -428,12 +430,11 @@ class PauliWord(dict):
             )
 
         if len(self) == 0:
-            if not wire_order:
-                raise ValueError("Can't get the matrix of an empty PauliWord.")
+            n = len(wire_order) if wire_order is not None else 0
             return (
-                np.diag([coeff] * 2 ** len(wire_order))
+                np.diag([coeff] * 2**n)
                 if format == "dense"
-                else coeff * sparse.eye(2 ** len(wire_order), format=format, dtype="complex128")
+                else coeff * sparse.eye(2**n, format=format, dtype="complex128")
             )
 
         if format == "dense":
@@ -539,7 +540,9 @@ class PauliSentence(dict):
     .. note::
 
         An empty :class:`~.PauliSentence` will be treated as the additive
-        identity (i.e ``0 * Identity()``).
+        identity (i.e ``0 * Identity()``). Its matrix is the all-zero matrix
+        (trivially the :math:`1\times 1` zero matrix when no ``wire_order`` is passed to
+        ``PauliSentence({}).to_mat()``).
 
     **Examples**
 
@@ -834,11 +837,10 @@ class PauliSentence(dict):
             return w or Wires(self.wires[0]) if self.wires else self.wires
 
         if len(self) == 0:
-            if not wire_order:
-                raise ValueError("Can't get the matrix of an empty PauliSentence.")
+            n = len(wire_order) if wire_order is not None else 0
             if format == "dense":
-                return np.eye(2 ** len(wire_order))
-            return sparse.eye(2 ** len(wire_order), format=format, dtype="complex128")
+                return np.zeros((2**n, 2**n))
+            return sparse.csr_matrix((2**n, 2**n), dtype="complex128")
 
         if format != "dense":
             return self._to_sparse_mat(wire_order, buffer_size=buffer_size)


Test patch:
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
index c8729309d..3a1ea54d3 100644
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -21,7 +21,6 @@ import pytest
 from scipy import sparse
 import pennylane as qml
 from pennylane import numpy as np
-from pennylane.wires import Wires
 from pennylane.pauli.pauli_arithmetic import PauliWord, PauliSentence, I, X, Y, Z
 
 
@@ -392,14 +391,10 @@ class TestPauliWord:
         (pw3, [0, "b", "c"], np.kron(np.kron(matZ, matZ), matZ)),
     )
 
-    def test_to_mat_error(self):
-        """Test that an appropriate error is raised when an empty
-        PauliWord is cast to matrix."""
-        with pytest.raises(ValueError, match="Can't get the matrix of an empty PauliWord."):
-            pw4.to_mat(wire_order=[])
-
-        with pytest.raises(ValueError, match="Can't get the matrix of an empty PauliWord."):
-            pw4.to_mat(wire_order=Wires([]))
+    def test_to_mat_empty(self):
+        """Test that an empty PauliWord is turned to the trivial 1 matrix"""
+        res = pw4.to_mat(wire_order=[])
+        assert res == np.ones((1, 1))
 
     pw_wire_order = ((pw1, [0, 1]), (pw1, [0, 1, 3]), (pw2, [0]))
 
@@ -839,11 +834,6 @@ class TestPauliSentence:
         assert res1 == true_res1
         assert res2 == true_res2
 
-    ps_match = (
-        (ps4, "Can't get the matrix of an empty PauliWord."),
-        (ps5, "Can't get the matrix of an empty PauliSentence."),
-    )
-
     @pytest.mark.parametrize("string1, string2, result", add_ps_ps)
     def test_iadd_ps_ps(self, string1, string2, result):
         """Test that the correct result of inplace addition with PauliSentence is produced and other object is not changed."""
@@ -864,15 +854,25 @@ class TestPauliSentence:
         assert copy_ps1 == res  # Check if the modified object matches the expected result
         assert copy_ps2 == ps  # Ensure the original object is not modified
 
-    @pytest.mark.parametrize("ps, match", ps_match)
-    def test_to_mat_error_empty(self, ps, match):
-        """Test that an appropriate error is raised when an empty
-        PauliSentence or PauliWord is cast to matrix."""
-        with pytest.raises(ValueError, match=match):
-            ps.to_mat(wire_order=[])
+    PS_EMPTY_CASES = (
+        (PauliSentence({}), np.zeros((1, 1))),
+        (PauliSentence({PauliWord({}): 1.0}), np.ones((1, 1))),
+        (PauliSentence({PauliWord({}): 2.5}), 2.5 * np.ones((1, 1))),
+        (
+            PauliSentence({PauliWord({}): 2.5, PauliWord({0: "X"}): 1.0}),
+            2.5 * np.eye(2) + qml.matrix(qml.PauliX(0)),
+        ),
+    )
 
-        with pytest.raises(ValueError, match=match):
-            ps.to_mat(wire_order=Wires([]))
+    @pytest.mark.parametrize("ps, true_res", PS_EMPTY_CASES)
+    def test_to_mat_empty(self, ps, true_res):
+        """Test that empty PauliSentences and PauliSentences with empty PauliWords are handled correctly"""
+
+        res_dense = ps.to_mat()
+        assert np.allclose(res_dense, true_res)
+        res_sparse = ps.to_mat(format="csr")
+        assert sparse.issparse(res_sparse)
+        assert qml.math.allclose(res_sparse.todense(), true_res)
 
     ps_wire_order = ((ps1, []), (ps1, [0, 1, 2, "a", "b"]), (ps3, [0, 1, "c"]))
 
@@ -884,10 +884,14 @@ class TestPauliSentence:
         with pytest.raises(ValueError, match=match):
             ps.to_mat(wire_order=wire_order)
 
-    def test_to_mat_identity(self):
-        """Test that an identity matrix is return if wire_order is provided."""
-        assert np.allclose(ps5.to_mat(wire_order=[0, 1]), np.eye(4))
-        assert sparse.issparse(ps5.to_mat(wire_order=[0, 1], format="csr"))
+    def test_to_mat_empty_sentence_with_wires(self):
+        """Test that a zero matrix is returned if wire_order is provided on an empty PauliSentence."""
+        true_res = np.zeros((4, 4))
+        res_dense = ps5.to_mat(wire_order=[0, 1])
+        assert np.allclose(res_dense, true_res)
+        res_sparse = ps5.to_mat(wire_order=[0, 1], format="csr")
+        assert sparse.issparse(res_sparse)
+        assert qml.math.allclose(res_sparse.todense(), true_res)
 
     tup_ps_mat = (
         (