qml.labs.dla.cartan_subalgebra

cartan_subalgebra(g, k, m, ad, start_idx=0, tol=1e-10, verbose=0, return_adjvec=False, is_orthogonal=True)[source]

Compute a Cartan subalgebra (CSA) \(\mathfrak{a} \subseteq \mathfrak{m}\).

A non-unique CSA is a maximal Abelian subalgebra in the horizontal subspace \(\mathfrak{m}\) of a Cartan decomposition. Note that this is sometimes called a horizontal CSA, and is different from the definition of a CSA on Wikipedia.

Parameters
  • g (List[Union[PauliSentence, np.ndarray]]) – Lie algebra \(\mathfrak{g}\), which is assumed to be ordered as \(\mathfrak{g} = \mathfrak{k} \oplus \mathfrak{m}\)

  • k (List[Union[PauliSentence, np.ndarray]]) – Vertical space \(\mathfrak{k}\) from Cartan decomposition \(\mathfrak{g} = \mathfrak{k} \oplus \mathfrak{m}\)

  • m (List[Union[PauliSentence, np.ndarray]]) – Horizontal space \(\mathfrak{m}\) from Cartan decomposition \(\mathfrak{g} = \mathfrak{k} \oplus \mathfrak{m}\)

  • ad (Array) – The \(|\mathfrak{g}| \times |\mathfrak{g}| \times |\mathfrak{g}|\) dimensional adjoint representation of \(\mathfrak{g}\) (see structure_constants())

  • start_idx (bool) – Indicates from which element in m the CSA computation starts.

  • tol (float) – Numerical tolerance for linear independence check

  • verbose (bool) – Whether or not to output progress during computation

  • return_adjvec (bool) – The output format. If False, returns operators in their original input format (matrices or PauliSentence). If True, returns the spaces as adjoint representation vectors.

  • is_orthogonal (bool) – Whether the basis elements are all orthogonal, both within and between g, k and m.

Returns

A tuple of adjoint vector representations (newg, k, mtilde, a, new_adj), corresponding to \(\mathfrak{g}\), \(\mathfrak{k}\), \(\tilde{\mathfrak{m}}\), \(\mathfrak{a}\) and the new adjoint representation. The dimensions are (|g|, |g|), (|k|, |g|), (|mtilde|, |g|), (|a|, |g|) and (|g|, |g|, |g|), respectively.

Return type

Tuple(np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray)

Example

A quick example computing a Cartan subalgebra of \(\mathfrak{su}(4)\) using the Cartan involution even_odd_involution().

>>> import pennylane as qml
>>> from pennylane.labs.dla import cartan_decomp, cartan_subalgebra, even_odd_involution
>>> g = list(qml.pauli.pauli_group(2)) # u(4)
>>> g = g[1:] # remove identity -> su(4)
>>> g = [op.pauli_rep for op in g] # optional; turn to PauliSentence for convenience
>>> k, m = cartan_decomp(g, even_odd_involution)
>>> g = k + m # re-order g to separate k and m
>>> adj = qml.structure_constants(g)
>>> newg, k, mtilde, a, new_adj = cartan_subalgebra(g, k, m, adj)
>>> newg == k + mtilde + a
True
>>> a
[-1.0 * Z(0) @ Z(1), 1.0 * Y(0) @ Y(1), -1.0 * X(0) @ X(1)]

We can confirm that these all commute with each other, as the CSA is Abelian (= all operators commute).

>>> from pennylane.labs.dla import check_all_commuting
>>> check_all_commuting(a)
True

We can opt-in to return what we call adjoint vectors of dimension \(|\mathfrak{g}|\), where each component corresponds to an entry in (the ordered) g. The adjoint vectors for the Cartan subalgebra are in np_a.

>>> np_newg, np_k, np_mtilde, np_a, new_adj = cartan_subalgebra(g, k, m, adj, return_adjvec=True)

We can reconstruct an operator by computing \(\hat{O}_v = \sum_i v_i g_i\) for an adjoint vector \(v\) and \(g_i \in \mathfrak{g}\).

>>> v = np_a[0]
>>> op = sum(v_i * g_i for v_i, g_i in zip(v, g))
>>> op.simplify()
>>> op
-1.0 * Z(0) @ Z(1)

For convenience, we provide a helper function adjvec_to_op() for the collections of adjoint vectors in the returns.

>>> from pennylane.labs.dla import adjvec_to_op
>>> a = adjvec_to_op(np_a, g)
>>> a
[-1.0 * Z(0) @ Z(1), 1.0 * Y(0) @ Y(1), -1.0 * X(0) @ X(1)]

Let us walk through an example of computing the Cartan subalgebra. The basis for computing the Cartan subalgebra is having the Lie algebra \(\mathfrak{g}\), a Cartan decomposition \(\mathfrak{g} = \mathfrak{k} \oplus \mathfrak{m}\) and its adjoint representation.

We start by computing these ingredients using cartan_decomp() and structure_constants(). As an example, we take the Lie algebra of the Heisenberg model with generators \(\{X_i X_{i+1}, Y_i Y_{i+1}, Z_i Z_{i+1}\}\).

>>> from pennylane.labs.dla import lie_closure_dense, cartan_decomp
>>> from pennylane import X, Y, Z
>>> n = 3
>>> gens = [X(i) @ X(i+1) for i in range(n-1)]
>>> gens += [Y(i) @ Y(i+1) for i in range(n-1)]
>>> gens += [Z(i) @ Z(i+1) for i in range(n-1)]
>>> g = lie_closure_dense(gens)

Taking the Heisenberg Lie algebra, we can perform the Cartan decomposition. We take the even_odd_involution() as a valid Cartan involution. The resulting vertical and horizontal subspaces \(\mathfrak{k}\) and \(\mathfrak{m}\) need to fulfill the commutation relations \([\mathfrak{k}, \mathfrak{k}] \subseteq \mathfrak{k}\), \([\mathfrak{k}, \mathfrak{m}] \subseteq \mathfrak{m}\) and \([\mathfrak{m}, \mathfrak{m}] \subseteq \mathfrak{k}\), which we can check using the helper function check_cartan_decomp().

>>> from pennylane.labs.dla import even_odd_involution, check_cartan_decomp
>>> k, m = cartan_decomp(g, even_odd_involution)
>>> check_cartan_decomp(k, m) # check commutation relations of Cartan decomposition
True

Our life is easier when we use a canonical ordering of the operators. This is why we re-define g with the new ordering in terms of operators in k first, and then all remaining operators from m.

>>> import numpy as np
>>> from pennylane.labs.dla import structure_constants_dense
>>> g = np.vstack([k, m]) # re-order g to separate k and m operators
>>> adj = structure_constants_dense(g) # compute adjoint representation of g

Finally, we can compute a Cartan subalgebra \(\mathfrak{a}\), a maximal Abelian subalgebra of \(\mathfrak{m}\).

>>> newg, k, mtilde, a, new_adj = cartan_subalgebra(g, k, m, adj, start_idx=3)

The new DLA newg is just the concatenation of k, mtilde, a. Each component is returned in the original input format. Here we obtain collections of \(8\times 8\) matrices (numpy arrays), as this is what we started from.

>>> newg.shape, k.shape, mtilde.shape, a.shape, new_adj.shape
((15, 8, 8), (6, 8, 8), (6, 8, 8), (3, 8, 8), (15, 15, 15))

We can also let the function return what we call adjoint representation vectors.

>>> kwargs = {"start_idx": 3, "return_adjvec": True}
>>> np_newg, np_k, np_mtilde, np_a, new_adj = cartan_subalgebra(g, k, m, adj, **kwargs)
>>> np_newg.shape, np_k.shape, np_mtilde.shape, np_a.shape, new_adj.shape
((15, 15), (6, 15), (6, 15), (3, 15), (15, 15, 15))

These are dense vector representations of dimension \(|\mathfrak{g}|\), in which each entry corresponds to the respective operator in \(\mathfrak{g}\). Given an adjoint representation vector \(v\), we can reconstruct the respective operator by simply computing \(\hat{O}_v = \sum_i v_i g_i\), where \(g_i \in \mathfrak{g}\) (hence the need for a canonical ordering).

We provide a convenience function adjvec_to_op() that works with both g represented as dense matrices or PL operators. Because we used dense matrices in this example, we transform the operators back to PennyLane operators using pauli_decompose().

>>> from pennylane.labs.dla import adjvec_to_op
>>> a = adjvec_to_op(np_a, g)
>>> h_op = [qml.pauli_decompose(op).pauli_rep for op in a]
>>> h_op
[-1.0 * Y(1) @ Y(2), -1.0 * Z(1) @ Z(2), 1.0 * X(1) @ X(2)]

In that case we chose a Cartan subalgebra from which we can readily see that it is commuting, but we also provide a small helper function to check that.

>>> from pennylane.labs.dla import check_all_commuting
>>> assert check_all_commuting(h_op)

Last but not least, the adjoint representation new_adj is updated to represent the new basis and its ordering of g.