Source code for pennylane.liealg.cartan_decomp

# Copyright 2025 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.
"""Functionality for Cartan decomposition"""


from pennylane import math
from pennylane.operation import Operator
from pennylane.pauli import PauliSentence, PauliVSpace
from pennylane.typing import TensorLike


[docs] def cartan_decomp( g: list[PauliSentence | Operator], involution: callable ) -> tuple[list[PauliSentence | Operator], list[PauliSentence | Operator]]: r"""Compute the Cartan Decomposition :math:`\mathfrak{g} = \mathfrak{k} \oplus \mathfrak{m}` of a Lie algebra :math:`\mathfrak{g}`. Given a Lie algebra :math:`\mathfrak{g}`, the Cartan decomposition is a decomposition :math:`\mathfrak{g} = \mathfrak{k} \oplus \mathfrak{m}` into orthogonal complements. This is realized by an involution :math:`\Theta(g)` that maps each operator :math:`g \in \mathfrak{g}` back to itself after two consecutive applications, i.e., :math:`\Theta(\Theta(g)) = g \ \forall g \in \mathfrak{g}`. The ``involution`` argument can be any function that maps the operators in the provided ``g`` to a boolean output. ``True`` for operators that go into :math:`\mathfrak{k}` and ``False`` for operators in :math:`\mathfrak{m}`. It is assumed that all operators in the input ``g`` belong to either :math:`\mathfrak{k}` or :math:`\mathfrak{m}`. The resulting subspaces fulfill the Cartan commutation relations .. math:: [\mathfrak{k}, \mathfrak{k}] \subseteq \mathfrak{k} \text{ ; } [\mathfrak{k}, \mathfrak{m}] \subseteq \mathfrak{m} \text{ ; } [\mathfrak{m}, \mathfrak{m}] \subseteq \mathfrak{k} Args: g (List[Union[PauliSentence, Operator]]): the (dynamical) Lie algebra to decompose. involution (callable): Involution function :math:`\Theta(\cdot)` to act on the input operator, should return ``0/1`` or ``False/True``. E.g., :func:`~even_odd_involution` or :func:`~concurrence_involution`. Returns: Tuple(List[Union[PauliSentence, Operator]], List[Union[PauliSentence, Operator]]): Tuple ``(k, m)`` containing the even parity subspace :math:`\Theta(\mathfrak{k}) = \mathfrak{k}` and the odd parity subspace :math:`\Theta(\mathfrak{m}) = -\mathfrak{m}`. .. seealso:: :func:`~even_odd_involution`, :func:`~concurrence_involution`, :func:`~check_cartan_decomp` **Example** We first construct a Lie algebra. >>> from pennylane import X, Z >>> from pennylane.liealg import concurrence_involution, even_odd_involution, cartan_decomp >>> generators = [X(0) @ X(1), Z(0), Z(1)] >>> g = qml.lie_closure(generators) >>> g [X(0) @ X(1), Z(0), Z(1), -1.0 * (Y(0) @ X(1)), -1.0 * (X(0) @ Y(1)), -1.0 * (Y(0) @ Y(1))] We compute the Cartan decomposition with respect to the :func:`~concurrence_involution`. >>> k, m = cartan_decomp(g, concurrence_involution) >>> k, m ([-1.0 * (Y(0) @ X(1)), -1.0 * (X(0) @ Y(1))], [X(0) @ X(1), Z(0), Z(1), -1.0 * (Y(0) @ Y(1))]) We can check the validity of the decomposition using :func:`~check_cartan_decomp`. >>> check_cartan_decomp(k, m) True There are other Cartan decomposition induced by other involutions. For example using :func:`~even_odd_involution`. >>> from pennylane.liealg import check_cartan_decomp >>> k, m = cartan_decomp(g, even_odd_involution) >>> k, m ([Z(0), Z(1)], [X(0) @ X(1), -1.0 * (Y(0) @ X(1)), -1.0 * (X(0) @ Y(1)), -1.0 * (Y(0) @ Y(1))]) >>> check_cartan_decomp(k, m) True """ # simple implementation assuming all elements in g are already either in k and m m = [] k = [] for op in g: if involution(op): # theta(k) = k k.append(op) else: # theta(m) = -m m.append(op) return k, m
[docs] def check_commutation_relation( ops1: list[PauliSentence | TensorLike], ops2: list[PauliSentence | TensorLike], vspace: PauliVSpace | list[PauliSentence | TensorLike], ): r"""Helper function to check :math:`[\text{ops1}, \text{ops2}] \subseteq \text{vspace}`. .. warning:: This function is expensive to compute. Args: ops1 (List[Union[PauliSentence, TensorLike]]): First set of operators. ops2 (List[Union[PauliSentence, TensorLike]]): Second set of operators. vspace (Union[PauliVSpace, List[Union[PauliSentence, TensorLike]]]): The vector space in form of a :class:`~PauliVSpace` that the operators should map to. Returns: bool: Whether or not :math:`[\text{ops1}, \text{ops2}] \subseteq \text{vspace}`. **Example** >>> from pennylane.liealg import check_commutation_relation >>> ops1 = [qml.X(0)] >>> ops2 = [qml.Y(0)] >>> vspace1 = [qml.X(0), qml.Y(0)] Because :math:`[X_0, Y_0] = 2i Z_0`, the commutators do not map to the selected vector space. >>> check_commutation_relation(ops1, ops2, vspace1) False Instead, we need the full :math:`\mathfrak{su}(2)` space. >>> vspace2 = [qml.X(0), qml.Y(0), qml.Z(0)] >>> check_commutation_relation(ops1, ops2, vspace2) True """ ops1_is_tensor = any(isinstance(op, TensorLike) for op in ops1) ops2_is_tensor = any(isinstance(op, TensorLike) for op in ops2) if not isinstance(vspace, PauliVSpace): vspace_is_tensor = any(isinstance(op, TensorLike) for op in vspace) if any(isinstance(op, Operator) for op in vspace): vspace = PauliVSpace(vspace, dtype=complex) else: vspace_is_tensor = False all_tensors = all((ops1_is_tensor, ops2_is_tensor, vspace_is_tensor)) any_tensors = any((ops1_is_tensor, ops2_is_tensor, vspace_is_tensor)) if not all_tensors and any_tensors: raise TypeError( "All inputs `ops1`, `ops2` and `vspace` to qml.liealg.check_commutation_relation need to either be iterables of operators or matrices." ) if all_tensors: all_coms = _all_coms(ops1, ops2) return _is_subspace(all_coms, vspace) if any(isinstance(op, Operator) for op in ops1): ops1 = [op.pauli_rep for op in ops1] if any(isinstance(op, Operator) for op in ops2): ops2 = [op.pauli_rep for op in ops2] for o1 in ops1: for o2 in ops2: com = o1.commutator(o2) com.simplify() if len(com) != 0: if vspace.is_independent(com): return False return True
def _all_coms(vspace1, vspace2): r"""Compute all commutators [Vspace1, Vspace2]""" chi = len(vspace1[0]) m0m1 = math.moveaxis(math.tensordot(vspace1, vspace2, axes=[[2], [1]]), 1, 2) m0m1 = math.reshape(m0m1, (-1, chi, chi)) # Implement einsum "aij,bki->abkj" by tensordot and moveaxis m1m0 = math.moveaxis(math.tensordot(vspace1, vspace2, axes=[[1], [2]]), 1, 3) m1m0 = math.reshape(m1m0, (-1, chi, chi)) all_coms = m0m1 - m1m0 return all_coms def _is_subspace(subspace, vspace): r"""check if subspace <= vspace""" # Check if rank increases by adding matices from ``subspace`` # Use matrices as vectors -> flatten matrix dimensions (chi, chi) to (chi**2,) vspace = math.reshape(vspace, (len(vspace), -1)) subspace = math.reshape(subspace, (len(subspace), -1)) rank_V = math.linalg.matrix_rank(vspace) rank_both = math.linalg.matrix_rank(math.vstack([vspace, subspace])) return rank_both <= rank_V
[docs] def check_cartan_decomp( k: list[PauliSentence | TensorLike], m: list[PauliSentence | TensorLike], verbose=True, ): r"""Helper function to check the validity of a Cartan decomposition :math:`\mathfrak{g} = \mathfrak{k} \oplus \mathfrak{m}.` Check whether of not the following properties are fulfilled. .. math:: [\mathfrak{k}, \mathfrak{k}] \subseteq \mathfrak{k} & \text{ (subalgebra)}\\ [\mathfrak{k}, \mathfrak{m}] \subseteq \mathfrak{m} & \text{ (reductive property)}\\ [\mathfrak{m}, \mathfrak{m}] \subseteq \mathfrak{k} & \text{ (symmetric property)} .. warning:: This function is expensive to compute Args: k (List[Union[PauliSentence, TensorLike]]): List of operators of the vertical subspace. m (List[Union[PauliSentence, TensorLike]]): List of operators of the horizontal subspace. verbose: Whether failures to meet one of the criteria should be printed. Returns: bool: Whether or not all of the Cartan commutation relations are fulfilled. .. seealso:: :func:`~cartan_decomp` **Example** We first construct a Lie algebra. >>> from pennylane import X, Z >>> from pennylane.liealg import concurrence_involution, even_odd_involution, cartan_decomp >>> generators = [X(0) @ X(1), Z(0), Z(1)] >>> g = qml.lie_closure(generators) >>> g [X(0) @ X(1), Z(0), Z(1), -1.0 * (Y(0) @ X(1)), -1.0 * (X(0) @ Y(1)), -1.0 * (Y(0) @ Y(1))] We compute the Cartan decomposition with respect to the :func:`~concurrence_involution`. >>> k, m = cartan_decomp(g, concurrence_involution) >>> k, m ([-1.0 * (Y(0) @ X(1)), -1.0 * (X(0) @ Y(1))], [X(0) @ X(1), Z(0), Z(1), -1.0 * (Y(0) @ Y(1))]) We can check the validity of the decomposition using ``check_cartan_decomp``. >>> from pennylane.liealg import check_cartan_decomp >>> check_cartan_decomp(k, m) True """ if any(isinstance(op, TensorLike) for op in k) or any(isinstance(op, TensorLike) for op in m): if not all(isinstance(op, TensorLike) for op in k) or not all( isinstance(op, TensorLike) for op in m ): raise TypeError( "All inputs `k`, `m` to check_cartan_decomp need to either be iterables of " f"operators or matrices. Received `k` of types {[type(op) for op in k]} and " f"`m` of types {[type(op) for op in m]}" ) if any(isinstance(op, Operator) for op in k): k = [op.pauli_rep for op in k] if any(isinstance(op, Operator) for op in m): m = [op.pauli_rep for op in m] if any(isinstance(op, TensorLike) for op in k): k_space = k else: k_space = PauliVSpace(k, dtype=complex) if any(isinstance(op, TensorLike) for op in m): m_space = m else: m_space = PauliVSpace(m, dtype=complex) # Commutation relations for Cartan pair if not (check_kk := check_commutation_relation(k, k, k_space)): _ = print("[k, k] sub k not fulfilled") if verbose else None if not (check_km := check_commutation_relation(k, m, m_space)): _ = print("[k, m] sub m not fulfilled") if verbose else None if not (check_mm := check_commutation_relation(m, m, k_space)): _ = print("[m, m] sub k not fulfilled") if verbose else None return all([check_kk, check_km, check_mm])