Repository: PennyLaneAI/pennylane

Issue:
Add matrix support for fermionic objects
### Feature details

⚠️ This issue is part of an internal assignment and not meant for external contributors.

The [fermi](https://docs.pennylane.ai/en/stable/code/qml_fermi.html) module has several functions and classes for creating and manipulating fermionic operators. It will be useful to have tools for obtaining the matrix representation of these operators. The [FermiWord](https://docs.pennylane.ai/en/stable/code/api/pennylane.fermi.FermiWord.html) and [FermiSentence](https://docs.pennylane.ai/en/stable/code/api/pennylane.fermi.FermiSentence.html) objects can be upgraded to have a `to_mat()` method that allows computing their matrix representation, similar to the `PauliWord` and `PauliSentence` classes. The users can then compute the matrix as

```
>>> import pennylane as qml
>>> op = qml.fermi.FermiWord({(0, 0): '+', (1, 1): '-'})
>>> op.to_mat()
array([[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]])
```

### Implementation
The `to_mat()` method should be added as an instance method to `FermiWord` and `FermiSentence`. The method should have an optional keyword argument, `n`,  for specifying the total number of orbitals which determines the matrix size. 

### Requirements
1. The computed matrix should properly describe the behaviour of fermionic operators.
2. The computed matrix should satisfy the commutation relationship between fermionic operators.

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5920?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: Patch coverage is `86.36364%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 99.66%. Comparing base [(`e74c9bf`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/e74c9bf821518196ce3bc1c8a740d63179cef9c3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`324846a`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/324846ad677d82eeac1391df559c5846a112221b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5920?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/fermi/fermionic.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5920?src=pr&el=tree&filepath=pennylane%2Ffermi%2Ffermionic.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2Zlcm1pL2Zlcm1pb25pYy5weQ==) | 86.36% | [3 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5920?src=pr&el=tree&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    #5920      +/-   ##
==========================================
- Coverage   99.67%   99.66%   -0.02%     
==========================================
  Files         425      425              
  Lines       40772    40498     -274     
==========================================
- Hits        40640    40361     -279     
- Misses        132      137       +5     
```

</details>

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


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 6cd04adf4..8559de76b 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -21,6 +21,10 @@
 
 * `SProd.terms` now flattens out the terms if the base is a multi-term observable.
   [(#5885)](https://github.com/PennyLaneAI/pennylane/pull/5885)
+  
+* A new method `to_mat` has been added to the `FermiWord` and `FermiSentence` classes, which allows
+  computing the matrix representation of these Fermi operators.
+  [(#5920)](https://github.com/PennyLaneAI/pennylane/pull/5920)
 
 <h3>Improvements 🛠</h3>
 
@@ -146,6 +150,7 @@ Ahmed Darwish,
 Lillian M. A. Frederiksen,
 Pietropaolo Frisoni,
 Emiliano Godinez,
+Renke Huang,
 Christina Lee,
 Austin Huang,
 Christina Lee,
diff --git a/pennylane/fermi/fermionic.py b/pennylane/fermi/fermionic.py
index 01b5049e4..73d8935ce 100644
--- a/pennylane/fermi/fermionic.py
+++ b/pennylane/fermi/fermionic.py
@@ -273,6 +273,36 @@ class FermiWord(dict):
 
         return operator
 
+    def to_mat(self, n_orbitals=None):
+        r"""Return the matrix representation.
+
+        Args:
+            n_orbitals (int or None): Number of orbitals. If not provided, it will be inferred from
+                the largest orbital index in the Fermi operator.
+
+        Returns:
+            NumpyArray: Matrix representation of the :class:`~.FermiWord`.
+
+        **Example**
+
+        >>> w = FermiWord({(0, 0): '+', (1, 1): '-'})
+        >>> w.to_mat()
+        array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
+              [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
+              [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
+              [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j])
+        """
+        largest_orb_id = max(key[1] for key in self.keys()) + 1
+        if n_orbitals and n_orbitals < largest_orb_id:
+            raise ValueError(
+                f"n_orbitals cannot be smaller than {largest_orb_id}, got: {n_orbitals}."
+            )
+
+        largest_order = n_orbitals or largest_orb_id
+        mat = qml.jordan_wigner(self, ps=True).to_mat(wire_order=list(range(largest_order)))
+
+        return mat
+
 
 # pylint: disable=useless-super-delegation
 class FermiSentence(dict):
@@ -463,6 +493,36 @@ class FermiSentence(dict):
             if abs(coeff) <= tol:
                 del self[fw]
 
+    def to_mat(self, n_orbitals=None):
+        r"""Return the matrix representation.
+
+        Args:
+            n_orbitals (int or None): Number of orbitals. If not provided, it will be inferred from
+                the largest orbital index in the Fermi operator
+
+        Returns:
+            NumpyArray: Matrix representation of the :class:`~.FermiSentence`.
+
+        **Example**
+
+        >>> fs = FermiSentence({FermiWord({(0, 0): "+", (1, 1): "-"}): 1.2, FermiWord({(0, 0): "+", (1, 0): "-"}): 3.1})
+        >>> fs.to_mat()
+        array([0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+              [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+              [0.0 + 0.0j, 1.2 + 0.0j, 3.1 + 0.0j, 0.0 + 0.0j],
+              [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 3.1 + 0.0j])
+        """
+        largest_orb_id = max(key[1] for fermi_word in self.keys() for key in fermi_word.keys()) + 1
+        if n_orbitals and n_orbitals < largest_orb_id:
+            raise ValueError(
+                f"n_orbitals cannot be smaller than {largest_orb_id}, got: {n_orbitals}."
+            )
+
+        largest_order = n_orbitals or largest_orb_id
+        mat = qml.jordan_wigner(self, ps=True).to_mat(wire_order=list(range(largest_order)))
+
+        return mat
+
 
 def from_string(fermi_string):
     r"""Return a fermionic operator object from its string representation.


Test patch:
diff --git a/tests/fermi/test_fermionic.py b/tests/fermi/test_fermionic.py
index b5a9b237f..3e0c324e4 100644
--- a/tests/fermi/test_fermionic.py
+++ b/tests/fermi/test_fermionic.py
@@ -125,6 +125,22 @@ class TestFermiWord:
         with pytest.raises(ValueError, match="The operator indices must belong to the set"):
             FermiWord(operator)
 
+    def test_to_mat(self):
+        """Test that the matrix representation of FermiWord is correct."""
+
+        expected_mat = np.zeros((4, 4), dtype=complex)
+        expected_mat[2, 1] = 1.0
+
+        mat = fw1.to_mat()
+        assert np.allclose(mat, expected_mat)
+
+    def test_to_mat_error(self):
+        """Test that an error is raised if the requested matrix dimension is smaller than the
+        dimension inferred from the largest orbital index.
+        """
+        with pytest.raises(ValueError, match="n_orbitals cannot be smaller than 2"):
+            fw1.to_mat(n_orbitals=1)
+
 
 class TestFermiWordArithmetic:
     WORDS_MUL = (
@@ -442,6 +458,14 @@ fs2_hamiltonian = FermiSentence({fw1: -1.23, fw2: -4, fw3: 0.5})
 fs3 = FermiSentence({fw3: -0.5, fw4: 1})
 fs4 = FermiSentence({fw4: 1})
 fs5 = FermiSentence({})
+fs6 = FermiSentence({fw1: 1.2, fw2: 3.1})
+fs7 = FermiSentence(
+    {
+        FermiWord({(0, 0): "+", (1, 1): "-"}): 1.23,  # a+(0) a(1)
+        FermiWord({(0, 0): "+", (1, 0): "-"}): 4.0j,  # a+(0) a(0) = n(0) (number operator)
+        FermiWord({(0, 0): "+", (1, 2): "-", (2, 1): "+"}): -0.5,  # a+(0) a(2) a+(1)
+    }
+)
 
 fs1_x_fs2 = FermiSentence(  # fs1 * fs1, computed by hand
     {
@@ -600,6 +624,25 @@ class TestFermiSentence:
         new_fs = pickle.loads(serialization)
         assert fs == new_fs
 
+    def test_to_mat(self):
+        """Test that the matrix representation of FermiSentence is correct."""
+        expected_mat = np.zeros((8, 8), dtype=complex)
+        expected_mat[4, 2] = 1.23 + 0j
+        expected_mat[5, 3] = 1.23 + 0j
+        for i in [4, 5, 6, 7]:
+            expected_mat[i, i] = 4.0j
+        expected_mat[6, 1] = 0.5 + 0j
+
+        mat = fs7.to_mat()
+        assert np.allclose(mat, expected_mat)
+
+    def test_to_mat_error(self):
+        """Test that an error is raised if the requested matrix dimension is smaller than the
+        dimension inferred from the largest orbital index.
+        """
+        with pytest.raises(ValueError, match="n_orbitals cannot be smaller than 3"):
+            fs7.to_mat(n_orbitals=2)
+
 
 class TestFermiSentenceArithmetic:
     tup_fs_mult = (  # computed by hand