diff --git a/doc/code/qml_pauli.rst b/doc/code/qml_pauli.rst index 11f485ca226..a58b867b8ba 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 + :skip: lie_closure, structure_constants PauliWord and PauliSentence --------------------------- @@ -164,4 +164,5 @@ See our `introduction to Dynamical Lie Algebras for quantum practitioners >> 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), @@ -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) +

Improvements ๐Ÿ› 

* 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 139ffaeede4..27243c8183d 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -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 diff --git a/pennylane/pauli/__init__.py b/pennylane/pauli/__init__.py index df220473e72..e190d349710 100644 --- a/pennylane/pauli/__init__.py +++ b/pennylane/pauli/__init__.py @@ -50,4 +50,4 @@ graph_colouring, ) -from .dla import PauliVSpace, lie_closure +from .dla import PauliVSpace, lie_closure, structure_constants diff --git a/pennylane/pauli/dla/__init__.py b/pennylane/pauli/dla/__init__.py index d02ee78dd25..84689526d9d 100644 --- a/pennylane/pauli/dla/__init__.py +++ b/pennylane/pauli/dla/__init__.py @@ -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 diff --git a/pennylane/pauli/dla/structure_constants.py b/pennylane/pauli/dla/structure_constants.py new file mode 100644 index 00000000000..3a0abf915ee --- /dev/null +++ b/pennylane/pauli/dla/structure_constants.py @@ -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 diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py index 22e7616ab9f..b861b15d813 100644 --- a/pennylane/pauli/pauli_arithmetic.py +++ b/pennylane/pauli/pauli_arithmetic.py @@ -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. @@ -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. diff --git a/tests/pauli/dla/test_structure_constants.py b/tests/pauli/dla/test_structure_constants.py new file mode 100644 index 00000000000..364d98c3074 --- /dev/null +++ b/tests/pauli/dla/test_structure_constants.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/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) diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py index 1f96498ebc5..67d0c40db98 100644 --- a/tests/pauli/test_pauli_arithmetic.py +++ b/tests/pauli/test_pauli_arithmetic.py @@ -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):