Skip to content

Commit

Permalink
[DLA 3] Add qml.dla.adjoint_repr function (#5406)
Browse files Browse the repository at this point in the history
A function to compute the adjoint representation of a DLA

- [x] Basic functionality
- [x] Basic testing
- [x] Docs
- [x] changelog

[sc-51422]
#5161 (DLA1)
#5169 (DLA2)
#5406 (DLA3)

---------

Co-authored-by: David Wierichs <david.wierichs@xanadu.ai>
Co-authored-by: Vincent Michaud-Rioux <vincentm@nanoacademic.com>
Co-authored-by: Mudit Pandey <mudit.pandey@xanadu.ai>
Co-authored-by: Thomas R. Bromley <49409390+trbromley@users.noreply.github.com>
  • Loading branch information
5 people committed Apr 23, 2024
1 parent 44018e9 commit a8bba50
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 5 deletions.
5 changes: 3 additions & 2 deletions doc/code/qml_pauli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ for Pauli-word partitioning functionality used in measurement optimization.
:no-heading:
:no-main-docstr:
:no-inherited-members:
:skip: lie_closure
:skip: lie_closure, structure_constants

PauliWord and PauliSentence
---------------------------
Expand Down Expand Up @@ -164,4 +164,5 @@ See our `introduction to Dynamical Lie Algebras for quantum practitioners <https
.. autosummary::
:toctree: api

~lie_closure
~lie_closure
~structure_constants
12 changes: 11 additions & 1 deletion doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@

```python
>>> ops = [X(0) @ X(1), Z(0), Z(1)]
>>> dla = qml.dla.lie_closure(ops)
>>> dla = qml.lie_closure(ops)
>>> print(dla)
[1.0 * X(1) @ X(0),
1.0 * Z(0),
Expand All @@ -118,6 +118,16 @@
-1.0 * Y(1) @ Y(0)]
```

* We can compute the structure constants (the adjoint representation) of a dynamical Lie algebra.
[(5406)](https://github.com/PennyLaneAI/pennylane/pull/5406)

For example, we can compute the adjoint representation of the transverse field Ising model DLA.

>>> 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)

<h3>Improvements 🛠</h3>

* Gradient transforms may now be applied to batched/broadcasted QNodes, as long as the
Expand Down
2 changes: 1 addition & 1 deletion pennylane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import pennylane.qnn
import pennylane.templates
import pennylane.pauli
from pennylane.pauli import pauli_decompose, lie_closure
from pennylane.pauli import pauli_decompose, lie_closure, structure_constants
from pennylane.resource import specs
import pennylane.resource
import pennylane.qchem
Expand Down
2 changes: 1 addition & 1 deletion pennylane/pauli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@
graph_colouring,
)

from .dla import PauliVSpace, lie_closure
from .dla import PauliVSpace, lie_closure, structure_constants
1 change: 1 addition & 0 deletions pennylane/pauli/dla/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
This subpackage defines functions and classes for dynamical Lie algebra functionality
"""

from .structure_constants import structure_constants
from .lie_closure import PauliVSpace, lie_closure
125 changes: 125 additions & 0 deletions pennylane/pauli/dla/structure_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# 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 adjoint representation of a Lie algebra"""
from typing import List, Union
from itertools import combinations

import numpy as np

from pennylane.typing import TensorLike
from pennylane.operation import Operator
from ..pauli_arithmetic import PauliSentence, PauliWord


def _all_commutators(ops):
commutators = {}
for (j, op1), (k, op2) in combinations(enumerate(ops), r=2):
res = op1.commutator(op2)
if res != PauliSentence({}):
commutators[(j, k)] = res

return commutators


def structure_constants(
g: List[Union[Operator, PauliWord, PauliSentence]], pauli: bool = False
) -> TensorLike:
r"""
Compute the structure constants that make up the adjoint representation of a Lie algebra.
Given a DLA :math:`\{iG_1, iG_2, .. iG_d \}` of dimension :math:`d`,
the structure constants yield the decomposition of all commutators in terms of DLA elements,
.. math:: [i G_\alpha, i G_\beta] = \sum_{\gamma = 0}^{d-1} f^\gamma_{\alpha, \beta} iG_\gamma.
The adjoint representation :math:`\left(\text{ad}(iG_\gamma)\right)_{\alpha, \beta} = f^\gamma_{\alpha, \beta}` is given by those structure constants,
which can be computed via
.. math:: f^\gamma_{\alpha, \beta} = \frac{\text{tr}\left(i G_\gamma \cdot \left[i G_\alpha, i G_\beta \right] \right)}{\text{tr}\left( iG_\gamma iG_\gamma \right)}.
Note that this is just the projection of the commutator on the DLA element :math:`iG_\gamma` via the trace inner product.
The inputs are assumed to be orthogonal. However, we neither assume nor enforce normalization of the DLA elements
:math:`G_\alpha`, hence the normalization
factor :math:`\text{tr}\left( iG_\gamma iG_\gamma \right)` in the projection.
Args:
g (List[Union[Operator, PauliWord, PauliSentence]]): The (dynamical) Lie algebra for which we want to compute
its adjoint representation. DLAs can be generated by a set of generators via :func:`~lie_closure`.
pauli (bool): Indicates whether it is assumed that :class:`~.PauliSentence` or :class:`~.PauliWord` instances are input.
This can help with performance to avoid unnecessary conversions to :class:`~pennylane.operation.Operator`
and vice versa. Default is ``False``.
Returns:
TensorLike: The adjoint representation of shape ``(d, d, d)``, corresponding to indices ``(gamma, alpha, beta)``.
**Example**
Let us generate the DLA of the transverse field Ising model using :func:`~lie_closure`.
>>> 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)
>>> 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.shape
(6, 6, 6)
The structure constants tell us the commutation relation between operators in the DLA via
.. math:: [i G_\alpha, i G_\beta] = \sum_{\gamma = 0}^{d-1} f^\gamma_{\alpha, \beta} iG_\gamma.
Let us confirm those with an example. Take :math:`[iG_1, iG_3] = [iZ_0, -iY_0 X_1] = -i 2 X_0 X_1 = -i 2 G_0`, so
we should have :math:`f^0_{1, 3} = -2`, which is indeed the case.
>>> adjoint_rep[0, 1, 3]
-2.
We can also look at the overall adjoint action of the first element :math:`G_0 = X_{0} \otimes X_{1}` of the DLA on other elements.
In particular, at :math:`\left(\text{ad}(iG_0)\right)_{\alpha, \beta} = f^0_{\alpha, \beta}`, which corresponds to the following matrix.
>>> adjoint_rep[0]
array([[ 0., 0., 0., 0., 0., 0.],
[-0., 0., 0., -2., 0., 0.],
[-0., 0., 0., 0., -2., 0.],
[-0., 2., -0., 0., 0., 0.],
[-0., -0., 2., 0., 0., 0.],
[ 0., -0., -0., -0., -0., 0.]])
Note that we neither enforce nor assume normalization by default.
"""
if any((op.pauli_rep is None) for op in g):
raise ValueError(
f"Cannot compute adjoint representation of non-pauli operators. Received {g}."
)

if not pauli:
g = [op.pauli_rep for op in g]

commutators = _all_commutators(g)

rep = np.zeros((len(g), len(g), len(g)), dtype=float)
for i, op in enumerate(g):
for (j, k), res in commutators.items():
value = (1j * (op @ res).trace()).real
value = value / (op @ op).trace() # v = ∑ (v · e_j / ||e_j||^2) * e_j
rep[i, j, k] = value
rep[i, k, j] = -value

return rep
18 changes: 18 additions & 0 deletions pennylane/pauli/pauli_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,9 @@ def map_wires(self, wire_map: dict) -> "PauliWord":
return self.__class__({wire_map.get(w, w): op for w, op in self.items()})


pw_id = PauliWord({}) # empty pauli word to be re-used


class PauliSentence(dict):
r"""Dictionary representing a linear combination of Pauli words, with the keys
as :class:`~pennylane.pauli.PauliWord` instances and the values correspond to coefficients.
Expand Down Expand Up @@ -597,6 +600,21 @@ def __missing__(self, key):
associated with it should be 0."""
return 0.0

def trace(self):
r"""Return the normalized trace of the ``PauliSentence`` instance
.. math:: \frac{1}{2^n} \text{tr}\left( P \right).
The normalized trace does not scale with the number of qubits :math:`n`.
>>> PauliSentence({PauliWord({0:"I", 1:"I"}): 0.5}).trace()
0.5
>>> PauliSentence({PauliWord({}): 0.5}).trace()
0.5
"""
return self.get(pw_id, 0.0)

def __add__(self, other):
"""Add a PauliWord, scalar or other PauliSentence to a PauliSentence.
Expand Down
85 changes: 85 additions & 0 deletions tests/pauli/dla/test_structure_constants.py
Original file line number Diff line number Diff line change
@@ -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/pauli/dla/structure_constants.py functionality"""
import pytest
import numpy as np

import pennylane as qml

from pennylane.pauli import PauliWord, PauliSentence, structure_constants

## Construct some example DLAs
# TFIM
gens = [PauliSentence({PauliWord({i: "X", i + 1: "X"}): 1.0}) for i in range(2)]
gens += [PauliSentence({PauliWord({i: "Z"}): 1.0}) for i in range(3)]
Ising3 = qml.pauli.lie_closure(gens, pauli=True)

# XXZ-type DLA, i.e. with true PauliSentences
gens2 = [
PauliSentence(
{
PauliWord({i: "X", i + 1: "X"}): 1.0,
PauliWord({i: "Y", i + 1: "Y"}): 1.0,
}
)
for i in range(2)
]
gens2 += [PauliSentence({PauliWord({i: "Z"}): 1.0}) for i in range(3)]
XXZ3 = qml.pauli.lie_closure(gens2, pauli=True)


class TestAdjointRepr:
"""Tests for structure_constants"""

def test_structure_constants_dim(self):
"""Test the dimension of the adjoint repr"""
d = len(Ising3)
adjoint = structure_constants(Ising3, pauli=True)
assert adjoint.shape == (d, d, d)
assert adjoint.dtype == float

@pytest.mark.parametrize("dla", [Ising3, XXZ3])
def test_structure_constants_elements(self, dla):
r"""Test relation :math:`[i G_\alpha, i G_\beta] = \sum_{\gamma = 0}^{\mathfrak{d}-1} f^\gamma_{\alpha, \beta} iG_\gamma`."""

d = len(dla)
ad_rep = structure_constants(dla, pauli=True)
for i in range(d):
for j in range(d):

comm_res = 1j * dla[i].commutator(dla[j])

res = sum(
np.array(c, dtype=complex) * dla[gamma]
for gamma, c in enumerate(ad_rep[:, i, j])
)
res.simplify()
assert comm_res == res

@pytest.mark.parametrize("dla", [Ising3, XXZ3])
def test_use_operators(self, dla):
"""Test that operators can be passed and lead to the same result"""
ad_rep_true = structure_constants(dla, pauli=True)

ops = [op.operation() for op in dla]
ad_rep = structure_constants(ops, pauli=False)
assert qml.math.allclose(ad_rep, ad_rep_true)

def test_raise_error_for_non_paulis(self):
"""Test that an error is raised when passing operators that do not have a pauli_rep"""
generators = [qml.Hadamard(0), qml.X(0)]
with pytest.raises(
ValueError, match="Cannot compute adjoint representation of non-pauli operators"
):
qml.pauli.structure_constants(generators)
10 changes: 10 additions & 0 deletions tests/pauli/test_pauli_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,16 @@ def test_map_wires(self, word, wire_map, expected):
"""Test the map_wires conversion method."""
assert word.map_wires(wire_map) == expected

TEST_TRACE = (
(PauliSentence({PauliWord({0: "X"}): 1.0, PauliWord({}): 3.0}), 3.0),
(PauliSentence({PauliWord({0: "Y"}): 1.0, PauliWord({1: "X"}): 3.0}), 0.0),
)

@pytest.mark.parametrize("op, res", TEST_TRACE)
def test_trace(self, op, res):
"""Test the trace method of PauliSentence"""
assert op.trace() == res


class TestPauliSentence:
def test_missing(self):
Expand Down

0 comments on commit a8bba50

Please sign in to comment.