Repository: PennyLaneAI/pennylane
Pull request #5477 (2024-04-05 10:11:54)

[DLA 4] qml.dla.center function
A simple `qml.dla.center` function

https://github.com/PennyLaneAI/pennylane/pull/5161 (DLA1) https://github.com/PennyLaneAI/pennylane/pull/5169 (DLA2) https://github.com/PennyLaneAI/pennylane/pull/5406 (DLA3) https://github.com/PennyLaneAI/pennylane/pull/5477 (DLA4)

Implement center [sc-51435]

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/5477?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.67%. Comparing base [(`5ad3a2c`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/5ad3a2c16b87aa946303cf6b96b3616d156d38be?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`0628a69`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5477?dropdown=coverage&src=pr&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              @@
##           adjointrepr    #5477   +/-   ##
============================================
  Coverage        99.67%   99.67%           
============================================
  Files              405      406    +1     
  Lines            37427    37448   +21     
============================================
+ Hits             37306    37327   +21     
  Misses             121      121           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5477?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/code/qml_pauli.rst b/doc/code/qml_pauli.rst
index a58b867b8..8fd5e836a 100644
--- a/doc/code/qml_pauli.rst
+++ b/doc/code/qml_pauli.rst
@@ -14,7 +14,7 @@ for Pauli-word partitioning functionality used in measurement optimization.
     :no-heading:
     :no-main-docstr:
     :no-inherited-members:
-    :skip: lie_closure, structure_constants
+    :skip: lie_closure, structure_constants, center
 
 PauliWord and PauliSentence
 ---------------------------
@@ -166,3 +166,4 @@ See our `introduction to Dynamical Lie Algebras for quantum practitioners <https
 
     ~lie_closure
     ~structure_constants
+    ~center
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 88e524484..1cd9fd5c0 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -166,13 +166,24 @@
 
   For example, we can compute the adjoint representation of the transverse field Ising model DLA.
 
-  ```pycon
+  ```python
   >>> dla = [X(0) @ X(1), Z(0), Z(1), Y(0) @ X(1), X(0) @ Y(1), Y(0) @ Y(1)]
   >>> structure_const = qml.structure_constants(dla)
   >>> structure_constp.shape
   (6, 6, 6)
   ```
 
+* We can compute the center of a dynamical Lie algebra.
+  [(#5477)](https://github.com/PennyLaneAI/pennylane/pull/5477)
+
+  Given a DLA `g`, we can now compute its center. The `center` is the collection of operators that commute with _all_ other operators in the DLA.
+
+  ```pycon
+  >>> g = [X(0), X(1) @ X(0), Y(1), Z(1) @ X(0)]
+  >>> qml.center(g)
+  [X(0)]
+  ```
+
 <h4>Simulate mixed-state qutrit systems 3️⃣</h4>
 
 * Functions `measure_with_samples` and `sample_state` have been added to the new `qutrit_mixed` module found in
@@ -190,8 +201,6 @@
   for `preprocess`.
   [(#5451)](https://github.com/PennyLaneAI/pennylane/pull/5451)
 
-<h4>Work easily and efficiently with operators 🔧</h4>
-
 <h3>Improvements 🛠</h3>
 
 * Fixed typo and string formatting in error message in `ClassicalShadow._convert_to_pauli_words` when the input is not a valid pauli.
diff --git a/pennylane/__init__.py b/pennylane/__init__.py
index 27243c818..07aa57f55 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, lie_closure, structure_constants
+from pennylane.pauli import pauli_decompose, lie_closure, structure_constants, center
 from pennylane.resource import specs
 import pennylane.resource
 import pennylane.qchem
diff --git a/pennylane/pauli/__init__.py b/pennylane/pauli/__init__.py
index e190d3497..87decb1cb 100644
--- a/pennylane/pauli/__init__.py
+++ b/pennylane/pauli/__init__.py
@@ -50,4 +50,4 @@ from .grouping import (
     graph_colouring,
 )
 
-from .dla import PauliVSpace, lie_closure, structure_constants
+from .dla import PauliVSpace, lie_closure, structure_constants, center
diff --git a/pennylane/pauli/dla/__init__.py b/pennylane/pauli/dla/__init__.py
index 84689526d..83cfd459a 100644
--- a/pennylane/pauli/dla/__init__.py
+++ b/pennylane/pauli/dla/__init__.py
@@ -16,4 +16,5 @@ This subpackage defines functions and classes for dynamical Lie algebra function
 """
 
 from .structure_constants import structure_constants
+from .center import center
 from .lie_closure import PauliVSpace, lie_closure
diff --git a/pennylane/pauli/dla/center.py b/pennylane/pauli/dla/center.py
new file mode 100644
index 000000000..ebf137948
--- /dev/null
+++ b/pennylane/pauli/dla/center.py
@@ -0,0 +1,78 @@
+# Copyright 2024 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""A function to compute the center of a Lie algebra"""
+from typing import List, Union
+from itertools import combinations
+
+import numpy as np
+
+from pennylane.pauli import PauliSentence, PauliWord
+from pennylane.operation import Operator
+
+
+def center(
+    g: List[Union[Operator, PauliWord, PauliSentence]], pauli: bool = False
+) -> List[Union[Operator, PauliSentence]]:
+    r"""
+    A function to compute the center of a Lie algebra.
+
+    Given a Lie algebra :math:`\mathfrak{g} = \{h_1,.., h_d\}`, the center :math:`\mathfrak{\xi}(\mathfrak{g})`
+    is given by all elements in :math:`\mathfrak{g}` that commute with `all` other elements in :math:`\mathfrak{g}`,
+
+    .. math:: \mathfrak{\xi}(\mathfrak{g}) := \{h \in \mathfrak{g} | [h, h_i]=0 \ \forall h_i \in \mathfrak{g} \}
+
+    Args:
+        g (List[Union[Operator, PauliSentence, PauliWord]]): List of operators for which to find the center.
+        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:
+        List[Union[Operator, PauliSentence]]: Center of ``g``
+
+    .. seealso:: :func:`~lie_closure`, :func:`~structure_constants`, :class:`~pennylane.pauli.PauliVSpace`, `Demo: Introduction to Dynamical Lie Algebras for quantum practitioners <https://pennylane.ai/qml/demos/tutorial_liealgebra/>`__
+
+    **Example**
+
+    We can compute the center of a DLA ``g``. For that, we compute the DLA via :func:`~lie_closure`.
+
+    >>> generators = [qml.X(0), qml.X(0) @ qml.X(1), qml.Y(1)]
+    >>> g = qml.lie_closure(generators)
+
+    The ``center`` is then the collection of operators that commute with `all` other operators in the DLA.
+    In this case, just ``X(0)``.
+
+    >>> qml.center(g)
+    [X(0)]
+
+    """
+
+    if not pauli:
+        g = [o.pauli_rep for o in g]
+
+    d = len(g)
+    commutators = np.zeros((d, d), dtype=int)
+    for (j, op1), (k, op2) in combinations(enumerate(g), r=2):
+        res = op1.commutator(op2)
+        res.simplify()
+        if res != PauliSentence({}):
+            commutators[j, k] = 1  # dummy value to indicate operators dont commute
+            commutators[k, j] = 1
+
+    mask = np.all(commutators == 0, axis=0)
+    res = list(np.array(g)[mask])
+
+    if not pauli:
+        res = [op.operation() for op in res]
+    return res
diff --git a/pennylane/pauli/dla/lie_closure.py b/pennylane/pauli/dla/lie_closure.py
index 97916114d..85c3b542f 100644
--- a/pennylane/pauli/dla/lie_closure.py
+++ b/pennylane/pauli/dla/lie_closure.py
@@ -52,6 +52,8 @@ def lie_closure(
         Union[list[:class:`~.PauliSentence`], list[:class:`~.Operator`]]: a basis of either :class:`~.PauliSentence` or :class:`~.Operator` instances that is closed under
         commutators (Lie closure).
 
+    .. seealso:: :func:`~structure_constants`, :func:`~center`, :class:`~pennylane.pauli.PauliVSpace`, `Demo: Introduction to Dynamical Lie Algebras for quantum practitioners <https://pennylane.ai/qml/demos/tutorial_liealgebra/>`__
+
     **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.
@@ -74,7 +76,7 @@ def lie_closure(
     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)
+    >>> dla = qml.lie_closure(ops)
     >>> print(dla)
     [X(1) @ X(0),
      Z(0),
@@ -98,7 +100,7 @@ def lie_closure(
         ...     PauliSentence({PauliWord({0: "Z"}): 1.}),
         ...     PauliSentence({PauliWord({1: "Z"}): 1.}),
         ... ]
-        >>> dla = qml.pauli.lie_closure(ops, pauli=True)
+        >>> dla = qml.lie_closure(ops, pauli=True)
         >>> print(dla)
         [1.0 * X(0) @ X(1),
          1.0 * Z(0),
diff --git a/pennylane/pauli/dla/structure_constants.py b/pennylane/pauli/dla/structure_constants.py
index 3a0abf915..34d741157 100644
--- a/pennylane/pauli/dla/structure_constants.py
+++ b/pennylane/pauli/dla/structure_constants.py
@@ -63,6 +63,8 @@ def structure_constants(
     Returns:
         TensorLike: The adjoint representation of shape ``(d, d, d)``, corresponding to indices ``(gamma, alpha, beta)``.
 
+    .. seealso:: :func:`~lie_closure`, :func:`~center`, :class:`~pennylane.pauli.PauliVSpace`, `Demo: Introduction to Dynamical Lie Algebras for quantum practitioners <https://pennylane.ai/qml/demos/tutorial_liealgebra/>`__
+
     **Example**
 
     Let us generate the DLA of the transverse field Ising model using :func:`~lie_closure`.
@@ -70,13 +72,13 @@ def structure_constants(
     >>> n = 2
     >>> gens = [X(i) @ X(i+1) for i in range(n-1)]
     >>> gens += [Z(i) for i in range(n)]
-    >>> dla = qml.pauli.lie_closure(gens)
+    >>> dla = qml.lie_closure(gens)
     >>> 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))]
 
     The dimension of the DLA is :math:`d = 6`. Hence, the structure constants have shape ``(6, 6, 6)``.
 
-    >>> adjoint_rep = qml.pauli.structure_constants(dla)
+    >>> adjoint_rep = qml.structure_constants(dla)
     >>> adjoint_rep.shape
     (6, 6, 6)
 
diff --git a/pennylane/pulse/transmon.py b/pennylane/pulse/transmon.py
index 1cf978aab..62789426b 100644
--- a/pennylane/pulse/transmon.py
+++ b/pennylane/pulse/transmon.py
@@ -215,7 +215,7 @@ def callable_freq_to_angular(fn):
 class TransmonSettings:
     """Dataclass that contains the information of a Transmon setup.
 
-    .. see-also:: :func:`transmon_interaction`
+    .. seealso:: :func:`transmon_interaction`
 
     Args:
             connections (List): List `[[idx_q0, idx_q1], ..]` of connected qubits (wires)


Test patch:
diff --git a/tests/pauli/dla/test_center.py b/tests/pauli/dla/test_center.py
new file mode 100644
index 000000000..7b7b766f1
--- /dev/null
+++ b/tests/pauli/dla/test_center.py
@@ -0,0 +1,85 @@
+# Copyright 2024 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for pennylane/dla/center.py functionality"""
+
+import pytest
+
+import pennylane as qml
+
+from pennylane.pauli import PauliSentence, center
+
+
+def test_trivial_center():
+    """Test that the center of an empty list of generators is an empty list of generators."""
+    ops = []
+    res = center(ops)
+    assert res == []
+
+
+DLA_CENTERS = (
+    ([qml.I()], [qml.I()]),  # just the identity
+    ([qml.I(), qml.X(0)], [qml.I(), qml.X(0)]),  # identity and some other operator
+    ([qml.X(0), qml.X(1)], [qml.X(0), qml.X(1)]),  # two non-overlapping wires
+    ([qml.X(0), qml.Y(1)], [qml.X(0), qml.Y(1)]),  # two non-overlapping wires with different ops
+    ([qml.X(0), qml.Y(0), qml.Z(0), qml.I()], [qml.I()]),  # non-trivial DLA, but trivial center
+)
+
+
+@pytest.mark.parametrize("ops, true_res", DLA_CENTERS)
+def test_center(ops, true_res):
+    """Test centers with Identity operators or non-overlapping wires"""
+    res = center(ops)
+    assert res == true_res
+
+
+@pytest.mark.parametrize("ops, true_res", DLA_CENTERS)
+def test_center_pauli(ops, true_res):
+    """Test centers with Identity operators or non-overlapping wires using their pauli_rep"""
+    ops = [op.pauli_rep for op in ops]
+    res = center(ops, pauli=True)
+
+    assert all(isinstance(op, PauliSentence) for op in res)
+    true_res = [op.pauli_rep for op in true_res]
+    assert res == true_res
+
+
+@pytest.mark.parametrize("pauli", [False, True])
+def test_center_pauli_word_pauli_True(pauli):
+    """Test that PauliWord instances can be passed for both pauli=True/False"""
+    ops = [
+        qml.pauli.PauliWord({0: "X"}),
+        qml.pauli.PauliWord({0: "X", 1: "X"}),
+        qml.pauli.PauliWord({1: "Y"}),
+    ]
+    if pauli:
+        assert qml.center(ops, pauli=pauli) == [qml.pauli.PauliWord({0: "X"})]
+    else:
+        assert qml.center(ops, pauli=pauli) == [qml.X(0)]
+
+
+GENERATOR_CENTERS = (
+    ([qml.X(0), qml.X(0) @ qml.X(1), qml.Y(1)], [qml.X(0)]),
+    ([qml.X(0) @ qml.X(1), qml.Y(1), qml.X(0)], [qml.X(0)]),
+    ([qml.X(0) @ qml.X(1), qml.Y(1), qml.X(1)], []),
+    ([qml.X(0) @ qml.X(1), qml.Y(1), qml.Z(0)], []),
+    ([p(0) @ p(1) for p in [qml.X, qml.Y, qml.Z]], [p(0) @ p(1) for p in [qml.X, qml.Y, qml.Z]]),
+)
+
+
+@pytest.mark.parametrize("generators, true_res", GENERATOR_CENTERS)
+def test_center_dla(generators, true_res):
+    """Test computing the center for a non-trivial DLA"""
+    g = qml.pauli.lie_closure(generators)
+    res = center(g)
+    assert res == true_res