Operations#
All the data structures of PauliArray can be acted on or combined using a set of operations. We give some examples using some data structures but most operations are available for all data structures. Also, while it is possible to infer expected behaviour for operations between different kinds of data structures, at the moment most operations involving two objects can only be performed between objects of the same data structure. This might change in the future.
Indexing and Masking#
Indexing and masking in PauliArray works similarly as in Numpy. For example, the following code shows how to access the first two Pauli strings of the second column of a PauliArray.
from pauliarray import PauliArray
paulis = PauliArray.from_labels(
[
["IIIX", "IIIY"],
["IIXZ", "IIYZ"],
["IXZZ", "IYZZ"],
["XZZZ", "YZZZ"],
]
)
new_paulis = paulis[:2, 1]
The result can be seen using the inspect
method.
print(new_paulis.inspect())
PauliArray
IIIY
IIYZ
Composition#
The operation of acting on an operator with another operator is called composition. It is equivalent to a matrix product between the matrices representation of the operators. In PauliArray the composition of two arrays is element-wise. For example, the composition of two WeightedPauliArray
yields a new WeightedPauliArray
from pauliarray import WeightedPauliArray
wpaulis_1 = WeightedPauliArray.from_labels_and_weights(["IZ", "ZI"], [1, 2])
wpaulis_2 = WeightedPauliArray.from_labels_and_weights(["ZZ", "XX"], [3, 4])
wpaulis_3 = wpaulis_1.compose(wpaulis_2)
print(wpaulis_3.inspect())
PauliArray
(+3.0000 +0.0000j) ZI
(+0.0000 +8.0000j) YX
The same two WeightedPauliArray
can be composed in a outer product fashion such that all the elements from the first WeightedPauliArray
are composed with all the elements of a second WeightedPauliArray
This results into a 2-dimensional WeightedPauliArray
.
In PauliArray this can be achieved by making use of broadcasting by introducing new dimensions to the arrays with None
. See Numpy’s documentation for more details.
wpaulis_4 = wpaulis_1[:, None].compose(wpaulis_2[None, :])
print(wpaulis_4.inspect())
PauliArray
(+3.0000 +0.0000j) ZI (+0.0000 +4.0000j) XY
(+6.0000 +0.0000j) IZ (+0.0000 +8.0000j) YX
The composition of two Operator
\(\hat{O}^{(1)} = \sum_{i} w_i^{(1)} \hat{P}_i^{(1)}\) and \(\hat{O}^{(2)} = \sum_{j} w_j^{(2)} \hat{P}_j^{(2)}\) involves such a 2-dimensional WeightedPauliArray
.
However, it needs to be flattened (\((i,j) \to k\)) to represent an Operator
.
PauliArray handles compositions of Operator
this way. It also combines the coefficients of repeated Pauli strings within the sum.
from pauliarray import Operator
operator_1 = Operator.from_labels_and_weights(["IZ", "XI"], [1, 2])
operator_2 = Operator.from_labels_and_weights(["II", "XZ"], [2, 1])
operator_3 = operator_1.compose(operator_2)
print(operator_3.inspect())
Operator Sum of
(+5.0000 +0.0000j) XI
(+4.0000 +0.0000j) IZ
Attention
Composition, and all other operations based on composition, behave a bit differently for the data structure PauliArray
. The composition of two Pauli strings produces a new Pauli string as well as a possible factor
Therefore, the composition of two PauliArray
returns a PauliArray
and a numpy.ndarray[complex]
.
from pauliarray import PauliArray
paulis_1 = PauliArray.from_labels(["IZ", "ZI"])
paulis_2 = PauliArray.from_labels(["ZZ", "XX"])
paulis_3, factors = paulis_1.compose(paulis_2)
This behaviour extends to all PauliArray
methods and functions were such factors are produced.
Commutation#
Based on the encoding of Pauli strings with bit strings \(\mathbf{z}\) and \(\mathbf{x}\), it’s easy to show that Pauli strings \(\hat{P}^{(1)}\) and \(\hat{P}^{(2)}\) commute if
is equal to \(0\) and anticommute otherwise. Commutation can be assessed element-wise using the commute_with
method.
from pauliarray import PauliArray
paulis_1 = PauliArray.from_labels(["IZ", "ZI"])
paulis_2 = PauliArray.from_labels(["ZZ", "XX"])
do_commute = paulis_1.commute_with(paulis_2)
print(do_commute)
[ True False]
Actual commutators can be computed element-wise between two arrays of Pauli strings
For efficiency, this operation can be reduced to a single composition
where \(c_i\) is given by the equation (1). In practice, the result for commutating Pauli strings is set to \(0\hat{I}\).
from pauliarray.pauli.weighted_pauli_array import WeightedPauliArray, commutator
wpaulis_1 = WeightedPauliArray.from_labels_and_weights(["IZ", "ZI"], [1, 2])
wpaulis_2 = WeightedPauliArray.from_labels_and_weights(["ZZ", "XX"], [3, 4])
comm_wpaulis = commutator(wpaulis_1, wpaulis_2)
print(comm_wpaulis.inspect())