Processing math: 100%

qml.liealg.lie_closure

lie_closure(generators, *, max_iterations=10000, verbose=False, pauli=False, matrix=False, tol=None)[source]

Compute the dynamical Lie algebra from a set of generators.

The Lie closure, pronounced “Lee” closure, is a way to compute the so-called dynamical Lie algebra (DLA) of a set of generators G={G1,..,GN}. For such generators, one computes all nested commutators [Gi,[Gj,..,[Gk,G]]] until no new operators are generated from commutation. All these operators together form the DLA, see e.g. section IIB of arXiv:2308.01432.

Parameters
  • generators (Iterable[Union[PauliWord, PauliSentence, Operator, TensorLike]]) – generating set for which to compute the Lie closure.

  • max_iterations (int) – maximum depth of nested commutators to consider. Default is 10000.

  • verbose (bool) – whether to print out progress updates during Lie closure calculation. Default is False.

  • pauli (bool) – Indicates whether it is assumed that PauliSentence or PauliWord instances are input and returned. This can help with performance to avoid unnecessary conversions to Operator and vice versa. Default is False.

  • matrix (bool) – Whether or not matrix representations should be used and returned in the Lie closure computation. This can help speed up the computation when using sums of Paulis with many terms. Default is False.

  • tol (float) – Numerical tolerance for the linear independence check used in PauliVSpace.

Returns

A basis of either PauliSentence, Operator, or np.ndarray instances that is closed under commutators (Lie closure).

Return type

Union[list[PauliSentence], list[Operator], np.ndarray]

Example

>>> ops = [X(0) @ X(1), Z(0), Z(1)]
>>> dla = qml.lie_closure(ops)

Let us walk through what happens in this simple example of computing the Lie closure of these generators (the transverse field Ising model on two qubits). A first round of commutators between all elements yields:

>>> qml.commutator(X(0) @ X(1), Z(0))
-2j * (Y(0) @ X(1))
>>> qml.commutator(X(0) @ X(1), Z(1))
-2j * (X(0) @ Y(1))

A next round of commutators between all elements further yields the new operator Y(0) @ Y(1).

>>> qml.commutator(X(0) @ Y(1), Z(0))
-2j * (Y(0) @ Y(1))

After that, no new operators emerge from taking nested commutators and we have the resulting DLA. This can be done in short via lie_closure as follows.

>>> ops = [X(0) @ X(1), Z(0), Z(1)]
>>> dla = qml.lie_closure(ops)
>>> print(dla)
[X(1) @ X(0),
 Z(0),
 Z(1),
 -1.0 * (Y(0) @ X(1)),
 -1.0 * (X(0) @ Y(1)),
 -1.0 * (Y(0) @ Y(1))]

Note that we normalize by removing the factors of 2i, though minus signs are left intact.

Note that by default, lie_closure returns PennyLane operators. Internally we use the more efficient representation in terms of PauliSentence by making use of the op.pauli_rep attribute of operators composed of Pauli operators. If desired, this format can be returned by using the keyword pauli=True. In that case, the input is also assumed to be a PauliSentence instance.

>>> ops = [
...     PauliSentence({PauliWord({0: "X", 1: "X"}): 1.}),
...     PauliSentence({PauliWord({0: "Z"}): 1.}),
...     PauliSentence({PauliWord({1: "Z"}): 1.}),
... ]
>>> dla = qml.lie_closure(ops, pauli=True)
>>> print(dla)
[1.0 * X(0) @ X(1),
 1.0 * Z(0),
 1.0 * Z(1),
 -1.0 * Y(0) @ X(1),
 -1.0 * X(0) @ Y(1),
 -1.0 * Y(0) @ Y(1)]
>>> type(dla[0])
pennylane.pauli.pauli_arithmetic.PauliSentence

In the case of sums of Pauli operators with many terms, it is often faster to use the matrix representation of the operators rather than the semi-analytic PauliSentence or Operator representation. We can force this by using the matrix keyword. The resulting dla is a np.ndarray of dimension (dim_g, 2**n, 2**n), where dim_g is the dimension of the DLA and n the number of qubits.

>>> dla = qml.lie_closure(ops, matrix=True)
>>> dla.shape
(6, 4, 4)

You can retrieve a semi-analytic representation again by using pauli_decompose().

>>> dla_ops = [qml.pauli_decompose(op) for op in dla]