Repository: PennyLaneAI/pennylane
Pull request #5169 (2024-02-07 12:40:05)

[DLA 2] Add `qml.dla.lie_closure` function
[WIP]

- [x] Basic functionality
- [x] Basic tests
- [x] compatibility with PauliWord + tests
- [x] compatibility with Operators + tests
- [x] Figure out normalization standard
- [x] changelog


Follow-up on https://github.com/PennyLaneAI/pennylane/pull/5161

**Shortcut Stories:**
[sc-51421]

https://github.com/PennyLaneAI/pennylane/pull/5161 (DLA1) https://github.com/PennyLaneAI/pennylane/pull/5169 (DLA2) https://github.com/PennyLaneAI/pennylane/pull/5406 (DLA3)

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/5169?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:
> :exclamation: No coverage uploaded for pull request base (`lieclosure@ee0f7d3`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#section-missing-base-commit).


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


```diff
@@              Coverage Diff              @@
##             lieclosure    #5169   +/-   ##
=============================================
  Coverage              ?   99.60%           
=============================================
  Files                 ?      396           
  Lines                 ?    36129           
  Branches              ?        0           
=============================================
  Hits                  ?    35987           
  Misses                ?      142           
  Partials              ?        0           
```



</details>

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

trbromley: Sorry for the delay in reviewing this @Qottmann, this week has been a bit busy. I'll try to get to this asap early next week, but feel free to ping me again.

Patch:
diff --git a/doc/code/qml_pauli.rst b/doc/code/qml_pauli.rst
index 1dfe4da7b..11f485ca2 100644
--- a/doc/code/qml_pauli.rst
+++ b/doc/code/qml_pauli.rst
@@ -14,6 +14,7 @@ for Pauli-word partitioning functionality used in measurement optimization.
     :no-heading:
     :no-main-docstr:
     :no-inherited-members:
+    :skip: lie_closure
 
 PauliWord and PauliSentence
 ---------------------------
@@ -151,3 +152,16 @@ the groups of observables:
 
 For a larger example of how grouping can be used with PennyLane, check out the
 `Measurement Optimization demo <https://pennylane.ai/qml/demos/tutorial_measurement_optimize/>`_.
+
+Dynamical Lie Algebras
+----------------------
+
+PennyLane provides support for working with dynamical Lie algebras (DLA) of Pauli operators.
+See our `introduction to Dynamical Lie Algebras for quantum practitioners <https://pennylane.ai/qml/demos/tutorial_liealgebra/>`__.
+
+.. currentmodule:: pennylane
+
+.. autosummary::
+    :toctree: api
+
+    ~lie_closure
\ No newline at end of file
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 8681fe6af..3f672d6e3 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -71,6 +71,53 @@
 * Added new function `qml.operation.convert_to_legacy_H` to convert `Sum`, `SProd`, and `Prod` to `Hamiltonian` instances.
   [(#5309)](https://github.com/PennyLaneAI/pennylane/pull/5309)
 
+<h4>Dynamical Lie Algebra functionality</h4>
+
+* A new `qml.lie_closure` function to compute the Lie closure of a list of operators.
+  [(#5161)](https://github.com/PennyLaneAI/pennylane/pull/5161)
+  [(#5169)](https://github.com/PennyLaneAI/pennylane/pull/5169)
+
+  The Lie closure, pronounced "Lee closure", is a way to compute the so-called dynamical Lie algebra (DLA) of a set of operators.
+  For a list of operators `ops = [op1, op2, op3, ..]`, one computes all nested commutators between `ops` until no new operators are generated from commutation.
+  All these operators together form the DLA, see e.g. section IIB of [arXiv:2308.01432](https://arxiv.org/abs/2308.01432).
+
+  Take for example the following ops
+
+  ```python
+  ops = [X(0) @ X(1), Z(0), Z(1)]
+  ```
+
+  A first round of commutators between all elements yields the new operators `Y(0) @ X(1)` and `X(0) @ Y(1)` (omitting scalar prefactors).
+
+  ```python
+  >>> qml.commutator(X(0) @ X(1), Z(0))
+  -2j * (X(1) @ Y(0))
+  >>> qml.commutator(X(0) @ X(1), Z(1))
+  -2j * (Y(1) @ X(0))
+  ```
+
+  A next round of commutators between all elements further yields the new operator `Y(0) @ Y(1)`.
+
+  ```python
+  >>> qml.commutator(X(0) @ Y(1), Z(0))
+  -2j * (Y(1) @ Y(0))
+  ```
+
+  After that, no new operators emerge from taking nested commutators and we have the resulting DLA.
+  This can now be done in short via `qml.lie_closure` as follows.
+
+  ```python
+  >>> ops = [X(0) @ X(1), Z(0), Z(1)]
+  >>> dla = qml.dla.lie_closure(ops)
+  >>> print(dla)
+  [1.0 * X(1) @ X(0),
+   1.0 * Z(0),
+   1.0 * Z(1),
+   -1.0 * X(1) @ Y(0),
+   -1.0 * Y(1) @ X(0),
+   -1.0 * Y(1) @ Y(0)]
+  ```
+
 <h3>Improvements 🛠</h3>
 
 * Gradient transforms may now be applied to batched/broadcasted QNodes, as long as the
diff --git a/pennylane/__init__.py b/pennylane/__init__.py
index 85bb1b7b0..139ffaeed 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -33,7 +33,7 @@ import pennylane.operation
 import pennylane.qnn
 import pennylane.templates
 import pennylane.pauli
-from pennylane.pauli import pauli_decompose
+from pennylane.pauli import pauli_decompose, lie_closure
 from pennylane.resource import specs
 import pennylane.resource
 import pennylane.qchem
diff --git a/pennylane/pauli/__init__.py b/pennylane/pauli/__init__.py
index 26ab2ea89..df220473e 100644
--- a/pennylane/pauli/__init__.py
+++ b/pennylane/pauli/__init__.py
@@ -50,4 +50,4 @@ from .grouping import (
     graph_colouring,
 )
 
-from .dla import PauliVSpace
+from .dla import PauliVSpace, lie_closure
diff --git a/pennylane/pauli/dla/__init__.py b/pennylane/pauli/dla/__init__.py
index 204de97fa..d02ee78dd 100644
--- a/pennylane/pauli/dla/__init__.py
+++ b/pennylane/pauli/dla/__init__.py
@@ -15,4 +15,4 @@
 This subpackage defines functions and classes for dynamical Lie algebra functionality
 """
 
-from .lie_closure import PauliVSpace
+from .lie_closure import PauliVSpace, lie_closure
diff --git a/pennylane/pauli/dla/lie_closure.py b/pennylane/pauli/dla/lie_closure.py
index 6215d3867..97916114d 100644
--- a/pennylane/pauli/dla/lie_closure.py
+++ b/pennylane/pauli/dla/lie_closure.py
@@ -13,15 +13,147 @@
 # limitations under the License.
 """A function to compute the Lie closure of a set of operators"""
 # pylint: disable=too-many-arguments
+import itertools
 from functools import reduce
 
+from typing import Union, Iterable
 from copy import copy
 import numpy as np
 
 import pennylane as qml
 
 from pennylane.operation import Operator
-from ..pauli_arithmetic import PauliSentence
+from ..pauli_arithmetic import PauliWord, PauliSentence
+
+
+def lie_closure(
+    generators: Iterable[Union[PauliWord, PauliSentence, Operator]],
+    max_iterations: int = 10000,
+    verbose: bool = False,
+    pauli: bool = False,
+) -> Iterable[Union[PauliWord, PauliSentence, Operator]]:
+    r"""Compute the dynamical Lie algebra from a set of generators.
+
+    The Lie closure, pronounced "Lee" closure, is a way to compute the so-called dynamical Lie algebra (DLA) of a set of generators :math:`\mathcal{G} = \{G_1, .. , G_N\}`.
+    For such generators, one computes all nested commutators :math:`[G_i, [G_j, .., [G_k, G_\ell]]]` until no new operators are generated from commutation.
+    All these operators together form the DLA, see e.g. section IIB of `arXiv:2308.01432 <https://arxiv.org/abs/2308.01432>`__.
+
+    Args:
+        generators (Iterable[Union[PauliWord, PauliSentence, Operator]]): generating set for which to compute the
+            Lie closure.
+        max_iterations (int): maximum depth of nested commutators to consider. Default is ``10000``.
+        verbose (bool): whether to print out progress updates during Lie closure
+            calculation. Default is ``False``.
+        pauli (bool): Indicates whether it is assumed that :class:`~.PauliSentence` or :class:`~.PauliWord` instances are input and returned.
+            This can help with performance to avoid unnecessary conversions to :class:`~pennylane.operation.Operator`
+            and vice versa. Default is ``False``.
+
+    Returns:
+        Union[list[:class:`~.PauliSentence`], list[:class:`~.Operator`]]: a basis of either :class:`~.PauliSentence` or :class:`~.Operator` instances that is closed under
+        commutators (Lie closure).
+
+    **Example**
+
+    Let us walk through a simple example of computing the Lie closure of the generators of the transverse field Ising model on two qubits.
+
+    >>> ops = [X(0) @ X(1), Z(0), Z(1)]
+
+    A first round of commutators between all elements yields:
+
+    >>> qml.commutator(X(0) @ X(1), Z(0))
+    -2j * (X(1) @ Y(0))
+    >>> qml.commutator(X(0) @ X(1), Z(1))
+    -2j * (Y(1) @ X(0))
+
+    A next round of commutators between all elements further yields the new operator ``Y(0) @ Y(1)``.
+
+    >>> qml.commutator(X(0) @ Y(1), Z(0))
+    -2j * (Y(0) @ Y(1))
+
+    After that, no new operators emerge from taking nested commutators and we have the resulting DLA.
+    This can be done in short via ``lie_closure`` as follows.
+
+    >>> ops = [X(0) @ X(1), Z(0), Z(1)]
+    >>> dla = qml.pauli.lie_closure(ops)
+    >>> print(dla)
+    [X(1) @ X(0),
+     Z(0),
+     Z(1),
+     -1.0 * (X(1) @ Y(0)),
+     -1.0 * (Y(1) @ X(0)),
+     -1.0 * (Y(1) @ Y(0))]
+
+    Note that we normalize by removing the factors of :math:`2i`, though minus signs are left intact.
+
+    .. details::
+        :title: Usage Details
+
+        Note that by default, ``lie_closure`` returns PennyLane operators. Internally we use the more
+        efficient representation in terms of :class:`~pennylane.pauli.PauliSentence` by making use of the ``op.pauli_rep``
+        attribute of operators composed of Pauli operators. If desired, this format can be returned by using
+        the keyword ``pauli=True``. In that case, the input is also assumed to be a :class:`~pennylane.pauli.PauliSentence` instance.
+
+        >>> ops = [
+        ...     PauliSentence({PauliWord({0: "X", 1: "X"}): 1.}),
+        ...     PauliSentence({PauliWord({0: "Z"}): 1.}),
+        ...     PauliSentence({PauliWord({1: "Z"}): 1.}),
+        ... ]
+        >>> dla = qml.pauli.lie_closure(ops, pauli=True)
+        >>> print(dla)
+        [1.0 * X(0) @ X(1),
+         1.0 * Z(0),
+         1.0 * Z(1),
+         -1.0 * Y(0) @ X(1),
+         -1.0 * X(0) @ Y(1),
+         -1.0 * Y(0) @ Y(1)]
+        >>> type(dla[0])
+        pennylane.pauli.pauli_arithmetic.PauliSentence
+
+    """
+    if not all(isinstance(op, (PauliSentence, PauliWord)) for op in generators):
+        if pauli:
+            raise TypeError(
+                "All generators need to be of type PauliSentence or PauliWord when using pauli=True in lie_closure."
+            )
+
+        generators = [
+            rep if (rep := op.pauli_rep) is not None else qml.pauli.pauli_sentence(op)
+            for op in generators
+        ]
+
+    vspace = PauliVSpace(generators)
+
+    epoch = 0
+    old_length = 0  # dummy value
+    new_length = len(vspace)
+
+    while (new_length > old_length) and (epoch < max_iterations):
+        if verbose:
+            print(f"epoch {epoch+1} of lie_closure, DLA size is {new_length}")
+        for ps1, ps2 in itertools.combinations(vspace.basis, 2):
+            com = ps1.commutator(ps2)
+            if len(com) == 0:  # skip because operators commute
+                continue
+
+            # result is always purely imaginary
+            # remove common factor 2 with Pauli commutators
+            for pw, val in com.items():
+                com[pw] = val.imag / 2
+            vspace.add(com)
+
+        # Updated number of linearly independent PauliSentences from previous and current step
+        old_length = new_length
+        new_length = len(vspace)
+        epoch += 1
+
+    if verbose > 0:
+        print(f"After {epoch} epochs, reached a DLA size of {new_length}")
+
+    res = vspace.basis
+    if not pauli:
+        res = [op.operation() for op in res]
+
+    return res
 
 
 class PauliVSpace:
@@ -284,6 +416,13 @@ class PauliVSpace:
 
         rank1 = np.linalg.matrix_rank(self._M)
         rank2 = np.linalg.matrix_rank(other._M)
-        rank3 = np.linalg.matrix_rank(np.concatenate([self._M, other._M], axis=1))
+
+        # To accommodate the case where the _pw_to_idx have
+        # different permutations, re-arrange ``other`` with the order of ``self``
+        other_M = np.zeros((other._num_pw, other._rank), dtype=float)
+        for i, ps in enumerate(other.basis):
+            for pw, value in ps.items():
+                other_M[self._pw_to_idx[pw], i] = value
+        rank3 = np.linalg.matrix_rank(np.concatenate([self._M, other_M], axis=1))
 
         return rank1 == rank2 and rank2 == rank3


Test patch:
diff --git a/tests/pauli/dla/test_lie_closure.py b/tests/pauli/dla/test_lie_closure.py
index 528c2e0c7..04aa341b7 100644
--- a/tests/pauli/dla/test_lie_closure.py
+++ b/tests/pauli/dla/test_lie_closure.py
@@ -19,7 +19,9 @@ import numpy as np
 
 import pennylane as qml
 
-from pennylane.pauli import PauliWord, PauliSentence, PauliVSpace
+from pennylane import X, Y, Z
+
+from pennylane.pauli import PauliWord, PauliSentence, PauliVSpace, lie_closure
 
 ops1 = [
     PauliSentence({PauliWord({0: "X", 1: "X"}): 1.0, PauliWord({0: "Y", 1: "Y"}): 1.0}),
@@ -283,3 +285,205 @@ class TestPauliVSpace:
         assert v1._pw_to_idx == vcopy._pw_to_idx
         assert v1._rank == vcopy._rank
         assert v1._num_pw == vcopy._num_pw
+
+
+dla11 = [
+    PauliSentence({PauliWord({0: "X", 1: "X"}): 1.0, PauliWord({0: "Y", 1: "Y"}): 1.0}),
+    PauliSentence({PauliWord({0: "Z"}): 1.0}),
+    PauliSentence({PauliWord({1: "Z"}): 1.0}),
+    PauliSentence({PauliWord({0: "Y", 1: "X"}): -1.0, PauliWord({0: "X", 1: "Y"}): 1.0}),
+]
+
+
+class TestLieClosure:
+    """Tests for qml.pauli.lie_closure()"""
+
+    def test_verbose(self, capsys):
+        """Test the verbose output"""
+        gen11 = dla11[:-1]
+        _ = lie_closure(gen11, verbose=True)
+        captured = capsys.readouterr()
+        assert "epoch 1 of lie_closure, DLA size is 3" in captured.out
+        assert "epoch 2 of lie_closure, DLA size is 4" in captured.out
+        assert "After 2 epochs, reached a DLA size of 4" in captured.out
+
+    def test_verbose_false(self, capsys):
+        """Test that there is no output when verbose is False"""
+        gen11 = dla11[:-1]
+        _ = lie_closure(gen11, verbose=False)
+        captured = capsys.readouterr()
+        assert captured.out == ""
+
+    def test_pauli_true_wrong_inputs(self):
+        """Test that an error with a meaningful error message is raised when inputting the wrong types while using pauli=True"""
+        gens = [X(0), X(1), Y(0) @ Y(1)]
+
+        with pytest.raises(TypeError, match="All generators need to be of type PauliSentence"):
+            _ = qml.pauli.lie_closure(gens, pauli=True)
+
+    def test_max_iterations(self, capsys):
+        """Test that max_iterations truncates the lie closure iteration at the right point"""
+        n = 3
+        generators = [
+            PauliSentence({PauliWord({i: "X", (i + 1) % n: "X"}): 1.0}) for i in range(n - 1)
+        ]
+        generators += [
+            PauliSentence({PauliWord({i: "X", (i + 1) % n: "Z"}): 1.0}) for i in range(n - 1)
+        ]
+
+        res = qml.pauli.lie_closure(generators, verbose=True, max_iterations=1)
+        captured = capsys.readouterr()
+        assert (
+            captured.out
+            == "epoch 1 of lie_closure, DLA size is 4\nAfter 1 epochs, reached a DLA size of 8\n"
+        )
+        assert len(res) == 8
+
+    def test_simple_lie_closure(self):
+        """Test simple lie_closure example"""
+        gen11 = dla11[:-1]
+        res11 = lie_closure(gen11, pauli=True)
+        assert res11 == dla11
+
+        dla12 = [
+            PauliSentence({PauliWord({0: "X", 1: "X"}): 1.0, PauliWord({0: "Y", 1: "Y"}): 1.0}),
+            PauliSentence({PauliWord({0: "Z"}): 1.0}),
+            PauliSentence({PauliWord({0: "Y", 1: "X"}): -1.0, PauliWord({0: "X", 1: "Y"}): 1.0}),
+            PauliSentence({PauliWord({0: "Z"}): -2.0, PauliWord({1: "Z"}): 2.0}),
+        ]
+
+        gen12 = dla12[:-1]
+        res12 = lie_closure(gen12, pauli=True)
+        assert PauliVSpace(res12) == PauliVSpace(dla12)
+
+    def test_lie_closure_with_pl_ops(self):
+        """Test that lie_closure works properly with PennyLane ops instead of PauliSentences"""
+        dla = [
+            qml.sum(qml.prod(X(0), X(1)), qml.prod(Y(0), Y(1))),
+            Z(0),
+            Z(1),
+            qml.sum(qml.prod(Y(0), X(1)), qml.s_prod(-1.0, qml.prod(X(0), Y(1)))),
+        ]
+        gen11 = dla[:-1]
+        res11 = lie_closure(gen11)
+
+        res11 = [op.pauli_rep for op in res11]  # back to pauli_rep for easier comparison
+        assert PauliVSpace(res11) == PauliVSpace(dla11)
+
+    @pytest.mark.parametrize("pauli", [True, False])
+    def test_lie_closure_with_PauliWords(self, pauli):
+        """Test that lie_closure works properly with PauliWords"""
+        gen = [
+            PauliWord({0: "X", 1: "X"}),
+            PauliWord({0: "Z"}),
+            PauliWord({1: "Z"}),
+        ]
+        dla = gen + [
+            PauliWord({0: "Y", 1: "X"}),
+            PauliWord({0: "X", 1: "Y"}),
+            PauliWord({0: "Y", 1: "Y"}),
+        ]
+        dla = [op.pauli_rep for op in dla]
+
+        res = lie_closure(gen, pauli=pauli)
+
+        res = [op.pauli_rep for op in res]  # convert to pauli_rep for easier comparison
+        assert PauliVSpace(res) == PauliVSpace(dla)
+
+    def test_lie_closure_with_sentences(self):
+        """Test that lie_closure returns the correct results when using (true) PauliSentences, i.e. not just single PauliWords"""
+        n = 3
+        gen = [
+            PauliSentence(
+                {
+                    PauliWord({i: "X", (i + 1) % n: "X"}): 1.0,
+                    PauliWord({i: "Y", (i + 1) % n: "Y"}): 1.0,
+                }
+            )
+            for i in range(n - 1)
+        ]
+        gen += [PauliSentence({PauliWord({i: "Z"}): 1.0}) for i in range(n)]
+
+        res = qml.pauli.lie_closure(gen, pauli=True)
+        true_res = [
+            PauliSentence({PauliWord({0: "X", 1: "X"}): 1.0, PauliWord({0: "Y", 1: "Y"}): 1.0}),
+            PauliSentence({PauliWord({1: "X", 2: "X"}): 1.0, PauliWord({1: "Y", 2: "Y"}): 1.0}),
+            PauliSentence({PauliWord({0: "Y", 1: "X"}): -1.0, PauliWord({0: "X", 1: "Y"}): 1.0}),
+            PauliSentence({PauliWord({1: "Y", 2: "X"}): -1.0, PauliWord({1: "X", 2: "Y"}): 1.0}),
+            PauliSentence(
+                {
+                    PauliWord({0: "X", 1: "Z", 2: "Y"}): 1.0,
+                    PauliWord({0: "Y", 1: "Z", 2: "X"}): -1.0,
+                }
+            ),
+            PauliSentence(
+                {
+                    PauliWord({0: "X", 1: "Z", 2: "X"}): -1.0,
+                    PauliWord({0: "Y", 1: "Z", 2: "Y"}): -1.0,
+                }
+            ),
+            PauliSentence({PauliWord({0: "Z"}): 1.0}),
+            PauliSentence({PauliWord({1: "Z"}): 1.0}),
+            PauliSentence({PauliWord({2: "Z"}): 1.0}),
+        ]
+        assert PauliVSpace(res) == PauliVSpace(true_res)
+
+    @pytest.mark.parametrize("n", range(2, 5))
+    def test_lie_closure_transverse_field_ising_1D_open(self, n):
+        """Test the lie closure works correctly for the transverse Field Ising model with open boundary conditions, a8 in theorem IV.1 in https://arxiv.org/pdf/2309.05690.pdf"""
+        generators = [
+            PauliSentence({PauliWord({i: "X", (i + 1) % n: "X"}): 1.0}) for i in range(n - 1)
+        ]
+        generators += [
+            PauliSentence({PauliWord({i: "X", (i + 1) % n: "Z"}): 1.0}) for i in range(n - 1)
+        ]
+
+        res = qml.pauli.lie_closure(generators)
+        assert len(res) == (2 * n - 1) * (2 * n - 2) // 2
+
+    @pytest.mark.parametrize("n", range(3, 5))
+    def test_lie_closure_transverse_field_ising_1D_cyclic(self, n):
+        """Test the lie closure works correctly for the transverse Field Ising model with cyclic boundary conditions, a8 in theorem IV.2 in https://arxiv.org/pdf/2309.05690.pdf"""
+        generators = [PauliSentence({PauliWord({i: "X", (i + 1) % n: "X"}): 1.0}) for i in range(n)]
+        generators += [
+            PauliSentence({PauliWord({i: "X", (i + 1) % n: "Z"}): 1.0}) for i in range(n)
+        ]
+
+        res = qml.pauli.lie_closure(generators)
+        assert len(res) == 2 * n * (2 * n - 1)
+
+    def test_lie_closure_heisenberg_generators_odd(self):
+        """Test the resulting DLA from Heisenberg generators with odd n, a7 in theorem IV.1 in https://arxiv.org/pdf/2309.05690.pdf"""
+        n = 3
+        # dim of su(N) is N ** 2 - 1
+        # Heisenberg generates su(2**(n-1)) for n odd            => dim should be (2**(n-1))**2 - 1
+        generators = [
+            PauliSentence({PauliWord({i: "X", (i + 1) % n: "X"}): 1.0}) for i in range(n - 1)
+        ]
+        generators += [
+            PauliSentence({PauliWord({i: "Y", (i + 1) % n: "Y"}): 1.0}) for i in range(n - 1)
+        ]
+        generators += [
+            PauliSentence({PauliWord({i: "Z", (i + 1) % n: "Z"}): 1.0}) for i in range(n - 1)
+        ]
+
+        res = qml.pauli.lie_closure(generators)
+        assert len(res) == (2 ** (n - 1)) ** 2 - 1
+
+    def test_lie_closure_heisenberg_generators_even(self):
+        """Test the resulting DLA from Heisenberg generators with even n, a7 in theorem IV.1 in https://arxiv.org/pdf/2309.05690.pdf"""
+        n = 4
+        # dim of su(N) is N ** 2 - 1
+        # Heisenberg generates (su(2**(n-2)))**4 for n>=4 even   => dim should be 4*((2**(n-2))**2 - 1)
+        generators = [
+            PauliSentence({PauliWord({i: "X", (i + 1) % n: "X"}): 1.0}) for i in range(n - 1)
+        ]
+        generators += [
+            PauliSentence({PauliWord({i: "Y", (i + 1) % n: "Y"}): 1.0}) for i in range(n - 1)
+        ]
+        generators += [
+            PauliSentence({PauliWord({i: "Z", (i + 1) % n: "Z"}): 1.0}) for i in range(n - 1)
+        ]
+
+        res = qml.pauli.lie_closure(generators)
+        assert len(res) == 4 * ((2 ** (n - 2)) ** 2 - 1)
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
index ca81edbe0..1f96498eb 100644
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -107,9 +107,10 @@ class TestPauliWord:
         assert 3 not in pw.keys()
         assert pw[3] == I
 
-    def test_pauli_rep(self):
-        """Test trivial pauli_rep property"""
-        pw = PauliWord({0: "I", 1: "X", 2: Y})
+    @pytest.mark.parametrize("pw", words)
+    def test_trivial_pauli_rep(self, pw):
+        """Test the pauli_rep property of PauliWord instances"""
+        assert pw.pauli_rep is not None
         assert pw.pauli_rep == PauliSentence({pw: 1})
 
     def test_set_items(self):
@@ -508,6 +509,12 @@ class TestPauliSentence:
         ps = PauliSentence({pw: 1.0})
         assert ps.wires == true_wires
 
+    @pytest.mark.parametrize("ps", sentences)
+    def test_trivial_pauli_rep(self, ps):
+        """Test the pauli_rep property of PauliSentence instances"""
+        assert ps.pauli_rep is not None
+        assert ps.pauli_rep == ps
+
     def test_set_items(self):
         """Test that we can add to a PauliSentence"""
         pw = PauliWord({0: X})