diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 5acbbcf7c82..eba6d01e282 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -65,6 +65,10 @@ * Added utility functions for handling dense matrices in the Lie theory context. [(#6563)](https://github.com/PennyLaneAI/pennylane/pull/6563) + [(#6392)](https://github.com/PennyLaneAI/pennylane/pull/6392) + +* Added a ``cartan_decomp`` function along with two standard involutions ``even_odd_involution`` and ``concurrence_involution``. + [(#6392)](https://github.com/PennyLaneAI/pennylane/pull/6392) * Added `unary_mapping()` function to map `BoseWord` and `BoseSentence` to qubit operators, using unary mapping [(#6576)](https://github.com/PennyLaneAI/pennylane/pull/6576); diff --git a/pennylane/labs/dla/__init__.py b/pennylane/labs/dla/__init__.py index f67b3a0dda1..eb4c4ccbd76 100644 --- a/pennylane/labs/dla/__init__.py +++ b/pennylane/labs/dla/__init__.py @@ -22,6 +22,7 @@ ~lie_closure_dense ~structure_constants_dense + ~cartan_decomp Utility functions @@ -35,17 +36,32 @@ ~adjvec_to_op ~change_basis_ad_rep ~check_orthonormal + ~check_commutation + ~check_all_commuting + ~check_cartan_decomp ~op_to_adjvec ~orthonormalize ~pauli_coefficients ~batched_pauli_decompose ~trace_inner_product +Involutions +~~~~~~~~~~~ + +.. currentmodule:: pennylane.labs.dla + +.. autosummary:: + :toctree: api + + ~even_odd_involution + ~concurrence_involution + """ from .lie_closure_dense import lie_closure_dense from .structure_constants_dense import structure_constants_dense +from .cartan import cartan_decomp, even_odd_involution, concurrence_involution from .dense_util import ( change_basis_ad_rep, pauli_coefficients, @@ -55,4 +71,7 @@ trace_inner_product, adjvec_to_op, op_to_adjvec, + check_commutation, + check_all_commuting, + check_cartan_decomp, ) diff --git a/pennylane/labs/dla/cartan.py b/pennylane/labs/dla/cartan.py new file mode 100644 index 00000000000..1bc1af3ee60 --- /dev/null +++ b/pennylane/labs/dla/cartan.py @@ -0,0 +1,253 @@ +# 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. +"""Functionality for Cartan decomposition""" +from functools import singledispatch +from typing import List, Tuple, Union + +import numpy as np + +import pennylane as qml +from pennylane import Y +from pennylane.operation import Operator +from pennylane.pauli import PauliSentence + + +def cartan_decomp( + g: List[Union[PauliSentence, Operator]], involution: callable +) -> Tuple[List[Union[PauliSentence, Operator]], List[Union[PauliSentence, Operator]]]: + r"""Cartan Decomposition :math:`\mathfrak{g} = \mathfrak{k} \oplus \mathfrak{m}`. + + 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}`. + + 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.labs.dla 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.labs.dla 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 + # TODO: Figure out more general way to do this when the above is not the case + m = [] + k = [] + + for op in g: + if involution(op): # odd parity + k.append(op) + else: # even parity + m.append(op) + + return k, m + + +# dispatch to different input types +def even_odd_involution(op: Union[PauliSentence, np.ndarray, Operator]) -> bool: + r"""The Even-Odd involution + + This is defined in `quant-ph/0701193 `__. + For Pauli words and sentences, it comes down to counting non-trivial Paulis in Pauli words. + + Args: + op ( Union[PauliSentence, np.ndarray, Operator]): Input operator + + Returns: + bool: Boolean output ``True`` or ``False`` for odd (:math:`\mathfrak{k}`) and even parity subspace (:math:`\mathfrak{m}`), respectively + + .. seealso:: :func:`~cartan_decomp` + + **Example** + + >>> from pennylane import X, Y, Z + >>> from pennylane.labs.dla import even_odd_involution + >>> ops = [X(0), X(0) @ Y(1), X(0) @ Y(1) @ Z(2)] + >>> [even_odd_involution(op) for op in ops] + [True, False, True] + + Operators with an odd number of non-identity Paulis yield ``1``, whereas even ones yield ``0``. + + The function also works with dense matrix representations. + + >>> ops_m = [qml.matrix(op, wire_order=range(3)) for op in ops] + >>> [even_odd_involution(op_m) for op_m in ops_m] + [True, False, True] + + """ + return _even_odd_involution(op) + + +@singledispatch +def _even_odd_involution(op): # pylint:disable=unused-argument + return NotImplementedError(f"Involution not defined for operator {op} of type {type(op)}") + + +@_even_odd_involution.register(PauliSentence) +def _even_odd_involution_ps(op: PauliSentence): + # Generalization to sums of Paulis: check each term and assert they all have the same parity + parity = [] + for pw in op.keys(): + parity.append(len(pw) % 2) + + # only makes sense if parity is the same for all terms, e.g. Heisenberg model + assert all( + parity[0] == p for p in parity + ), f"The Even-Odd involution is not well-defined for operator {op} as individual terms have different parity" + return bool(parity[0]) + + +@_even_odd_involution.register(np.ndarray) +def _even_odd_involution_matrix(op: np.ndarray): + """see Table CI in https://arxiv.org/abs/2406.04418""" + n = int(np.round(np.log2(op.shape[-1]))) + YYY = qml.prod(*[Y(i) for i in range(n)]) + YYY = qml.matrix(YYY, range(n)) + + transformed = YYY @ op.conj() @ YYY + if np.allclose(transformed, op): + return False + if np.allclose(transformed, -op): + return True + raise ValueError(f"The Even-Odd involution is not well-defined for operator {op}.") + + +@_even_odd_involution.register(Operator) +def _even_odd_involution_op(op: Operator): + """use pauli representation""" + return _even_odd_involution_ps(op.pauli_rep) + + +# dispatch to different input types +def concurrence_involution(op: Union[PauliSentence, np.ndarray, Operator]) -> bool: + r"""The Concurrence Canonical Decomposition :math:`\Theta(g) = -g^T` as a Cartan involution function + + This is defined in `quant-ph/0701193 `__. + For Pauli words and sentences, it comes down to counting Pauli-Y operators. + + Args: + op ( Union[PauliSentence, np.ndarray, Operator]): Input operator + + Returns: + bool: Boolean output ``True`` or ``False`` for odd (:math:`\mathfrak{k}`) and even parity subspace (:math:`\mathfrak{m}`), respectively + + .. seealso:: :func:`~cartan_decomp` + + **Example** + + >>> from pennylane import X, Y, Z + >>> from pennylane.labs.dla import concurrence_involution + >>> ops = [X(0), X(0) @ Y(1), X(0) @ Y(1) @ Z(2), Y(0) @ Y(2)] + >>> [concurrence_involution(op) for op in ops] + [False, True, True, False] + + Operators with an odd number of ``Y`` operators yield ``1``, whereas even ones yield ``0``. + + The function also works with dense matrix representations. + + >>> ops_m = [qml.matrix(op, wire_order=range(3)) for op in ops] + >>> [even_odd_involution(op_m) for op_m in ops_m] + [False, True, True, False] + + """ + return _concurrence_involution(op) + + +@singledispatch +def _concurrence_involution(op): + return NotImplementedError(f"Involution not defined for operator {op} of type {type(op)}") + + +@_concurrence_involution.register(PauliSentence) +def _concurrence_involution_pauli(op: PauliSentence): + # Generalization to sums of Paulis: check each term and assert they all have the same parity + parity = [] + for pw in op.keys(): + result = sum(1 if el == "Y" else 0 for el in pw.values()) + parity.append(result % 2) + + # only makes sense if parity is the same for all terms, e.g. Heisenberg model + assert all( + parity[0] == p for p in parity + ), f"The concurrence canonical decomposition is not well-defined for operator {op} as individual terms have different parity" + return bool(parity[0]) + + +@_concurrence_involution.register(Operator) +def _concurrence_involution_operator(op: Operator): + return _concurrence_involution_matrix(op.matrix()) + + +@_concurrence_involution.register(np.ndarray) +def _concurrence_involution_matrix(op: np.ndarray): + if np.allclose(op, -op.T): + return True + if np.allclose(op, op.T): + return False + raise ValueError( + f"The concurrence canonical decomposition is not well-defined for operator {op}" + ) diff --git a/pennylane/labs/dla/dense_util.py b/pennylane/labs/dla/dense_util.py index 44972834685..c73b93fa2b1 100644 --- a/pennylane/labs/dla/dense_util.py +++ b/pennylane/labs/dla/dense_util.py @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. """Utility tools for dense Lie algebra representations""" -# pylint: disable=possibly-used-before-assignment +# pylint: disable=possibly-used-before-assignment, too-many-return-statements from functools import reduce from itertools import combinations, combinations_with_replacement -from typing import Iterable, Optional, Union +from typing import Iterable, List, Optional, Union import numpy as np from scipy.linalg import sqrtm @@ -234,6 +234,174 @@ def batched_pauli_decompose(H: TensorLike, tol: Optional[float] = None, pauli: b return H_ops +def check_commutation(ops1, ops2, vspace): + r"""Helper function to check :math:`[\text{ops1}, \text{ops2}] \subseteq \text{vspace}` + + .. warning:: This function is expensive to compute + + Args: + ops1 (Iterable[PauliSentence]): First set of operators + ops2 (Iterable[PauliSentence]): Second set of operators + vspace (:class:`~PauliVSpace`): 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.labs.dla import check_commutation + >>> ops1 = [qml.X(0).pauli_rep] + >>> ops2 = [qml.Y(0).pauli_rep] + >>> vspace1 = qml.pauli.PauliVSpace([qml.X(0).pauli_rep, qml.Y(0).pauli_rep], dtype=complex) + + Because :math:`[X_0, Y_0] = 2i Z_0`, the commutators do not map to the selected vector space. + + >>> check_commutation(ops1, ops2, vspace1) + False + + Instead, we need the full :math:`\mathfrak{su}(2)` space. + + >>> vspace2 = qml.pauli.PauliVSpace([qml.X(0).pauli_rep, qml.Y(0).pauli_rep, qml.Z(0).pauli_rep], dtype=complex) + >>> check_commutation(ops1, ops2, vspace2) + True + """ + 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 check_all_commuting(ops: List[Union[PauliSentence, np.ndarray, Operator]]): + r"""Helper function to check if all operators in a set of operators commute + + .. warning:: This function is expensive to compute + + Args: + ops (List[Union[PauliSentence, np.ndarray, Operator]]): List of operators to check for mutual commutation + + Returns: + bool: Whether or not all operators commute with each other + + **Example** + + >>> from pennylane.labs.dla import check_all_commuting + >>> from pennylane import X + >>> ops = [X(i) for i in range(10)] + >>> check_all_commuting(ops) + True + + Operators on different wires (trivially) commute with each other. + """ + if all(isinstance(op, PauliSentence) for op in ops): + for oi, oj in combinations(ops, 2): + com = oj.commutator(oi) + com.simplify() + if len(com) != 0: + return False + + return True + + if all(isinstance(op, Operator) for op in ops): + for oi, oj in combinations(ops, 2): + com = qml.simplify(qml.commutator(oj, oi)) + if not qml.equal(com, 0 * qml.Identity()): + return False + + return True + + if all(isinstance(op, np.ndarray) for op in ops): + for oi, oj in combinations(ops, 2): + com = oj @ oi - oi @ oj + if not np.allclose(com, np.zeros_like(com)): + return False + + return True + + return NotImplemented + + +def check_cartan_decomp(k: List[PauliSentence], m: List[PauliSentence], 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[PauliSentence]): List of operators of the vertical subspace + m (List[PauliSentence]): 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 properties are fulfilled + + .. seealso:: :func:`~cartan_decomp` + + **Example** + + We first construct a Lie algebra. + + >>> from pennylane import X, Z + >>> from pennylane.labs.dla 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.labs.dla import check_cartan_decomp + >>> check_cartan_decomp(k, m) + True + + """ + if any(isinstance(op, np.ndarray) for op in k): + k = [qml.pauli_decompose(op).pauli_rep for op in k] + if any(isinstance(op, np.ndarray) for op in m): + m = [qml.pauli_decompose(op).pauli_rep 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] + + k_space = qml.pauli.PauliVSpace(k, dtype=complex) + m_space = qml.pauli.PauliVSpace(m, dtype=complex) + + # Commutation relations for Cartan pair + if not (check_kk := check_commutation(k, k, k_space)): + _ = print("[k, k] sub k not fulfilled") if verbose else None + if not (check_km := check_commutation(k, m, m_space)): + _ = print("[k, m] sub m not fulfilled") if verbose else None + if not (check_mm := check_commutation(m, m, k_space)): + _ = print("[m, m] sub k not fulfilled") if verbose else None + + return all([check_kk, check_km, check_mm]) + + def orthonormalize(basis: Iterable[Union[PauliSentence, Operator, np.ndarray]]) -> np.ndarray: r"""Orthonormalize a list of basis vectors. @@ -330,7 +498,7 @@ def gram_schmidt(X): def check_orthonormal(g: Iterable[Union[PauliSentence, Operator]], inner_product: callable) -> bool: - """ + r""" Utility function to check if operators in ``g`` are orthonormal with respect to the provided ``inner_product``. Args: diff --git a/pennylane/labs/tests/dla/test_cartan.py b/pennylane/labs/tests/dla/test_cartan.py new file mode 100644 index 00000000000..e7f6323b4e6 --- /dev/null +++ b/pennylane/labs/tests/dla/test_cartan.py @@ -0,0 +1,113 @@ +# 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/lie_closure_dense.py functionality""" +# pylint: disable=no-self-use,too-few-public-methods,missing-class-docstring +import pytest + +import pennylane as qml +from pennylane import X, Y, Z +from pennylane.labs.dla import cartan_decomp, concurrence_involution, even_odd_involution + + +def check_commutation(ops1, ops2, vspace): + """Helper function to check things like [k, m] subspace m; expensive""" + for o1 in ops1: + for o2 in ops2: + com = o1.commutator(o2) + assert not vspace.is_independent(com) + + return True + + +Ising2 = qml.lie_closure([X(0), X(1), Z(0) @ Z(1)]) +Ising3 = qml.lie_closure([X(0), X(1), X(2), Z(0) @ Z(1), Z(1) @ Z(2)]) +Heisenberg3 = qml.lie_closure( + [X(0) @ X(1), X(1) @ X(2), Y(0) @ Y(1), Y(1) @ Y(2), Z(0) @ Z(1), Z(1) @ Z(2)] +) + + +class TestCartanDecomposition: + @pytest.mark.parametrize("involution", [even_odd_involution, concurrence_involution]) + @pytest.mark.parametrize("g", [Ising2, Ising3, Heisenberg3]) + def test_cartan_decomp(self, g, involution): + """Test basic properties and Cartan decomposition definitions""" + + g = [op.pauli_rep for op in g] + k, m = cartan_decomp(g, involution) + + assert all(involution(op) is True for op in k) + assert all(involution(op) is False for op in m) + + k_space = qml.pauli.PauliVSpace(k) + m_space = qml.pauli.PauliVSpace(m) + + # Commutation relations for Cartan pair + assert check_commutation(k, k, k_space) + assert check_commutation(k, m, m_space) + assert check_commutation(m, m, k_space) + + @pytest.mark.parametrize("involution", [even_odd_involution, concurrence_involution]) + @pytest.mark.parametrize("g", [Ising2, Ising3, Heisenberg3]) + def test_cartan_decomp_dense(self, g, involution): + """Test basic properties and Cartan decomposition definitions using dense representations""" + + g = [qml.matrix(op, wire_order=range(3)) for op in g] + k, m = cartan_decomp(g, involution) + + assert all(involution(op) is True for op in k) + assert all(involution(op) is False for op in m) + + # check currently only works with pauli sentences + k_space = qml.pauli.PauliVSpace([qml.pauli_decompose(op).pauli_rep for op in k]) + m_space = qml.pauli.PauliVSpace([qml.pauli_decompose(op).pauli_rep for op in m]) + + # Commutation relations for Cartan pair + assert check_commutation(k_space.basis, k_space.basis, k_space) + assert check_commutation(k_space.basis, m_space.basis, m_space) + assert check_commutation(m_space.basis, m_space.basis, k_space) + + +involution_ops = [ + X(0) @ X(1), + X(0) @ X(1) + Y(0) @ Y(1), + X(1) + Z(0), + Y(0) - Y(0) @ Y(1) @ Y(2), + Y(0) @ X(1), +] + + +class TestInvolutions: + """Test involutions""" + + @pytest.mark.parametrize("op", involution_ops) + def test_concurrence_involution_inputs(self, op): + """Test different input types yield consistent results""" + res_op = concurrence_involution(op) + res_ps = concurrence_involution(op.pauli_rep) + res_m = concurrence_involution(op.matrix()) + + assert isinstance(res_op, bool) + assert res_op is res_ps + assert res_op is res_m + + @pytest.mark.parametrize("op", involution_ops) + def test_even_odd_involution_inputs(self, op): + """Test different input types yield consistent results""" + res_op = even_odd_involution(op) + res_ps = even_odd_involution(op.pauli_rep) + res_m = even_odd_involution(op.matrix()) + + assert isinstance(res_op, bool) + assert res_op is res_ps + assert res_op is res_m diff --git a/pennylane/labs/tests/dla/test_dense_utils.py b/pennylane/labs/tests/dla/test_dense_utils.py index 5a7fd5a93af..5e7ba5692f5 100644 --- a/pennylane/labs/tests/dla/test_dense_utils.py +++ b/pennylane/labs/tests/dla/test_dense_utils.py @@ -23,6 +23,7 @@ adjvec_to_op, batched_pauli_decompose, change_basis_ad_rep, + check_cartan_decomp, check_orthonormal, lie_closure_dense, op_to_adjvec, @@ -361,6 +362,14 @@ def test_orthonormalize(g): assert check_orthonormal(g, trace_inner_product) +def test_check_cartan_decomp(): + """Test that check_cartan_decomp correctly checks Ising cartan decomp from fdhs paper (https://arxiv.org/abs/2104.00728)""" + k = [Z(0) @ Y(1), Y(0) @ Z(1)] + m = [Z(0) @ Z(1), Y(0) @ Y(1), X(0), X(1)] + + assert check_cartan_decomp(k, m) + + class TestChangeBasisAdRep: """Tests for ``change_basis_ad_rep`` to change the adjoint representation into a new basis."""