Numerical Pauli Strings
Pauli Operator Encoding
Pauli operators are central to the representation of spin 1/2 rotations, form a basis in the space of many-body spin 1/2 (or equivalently qubit) operators, and are mathematically simple to manipulate. A pauli string acting on N spin 1/2 particles is defined as the tensor product of the pauli operators:
The dimension of the operator space they span is which makes generating and handling large collections of Pauli strings numerically difficult. However, there exists a faithful binary encoding that compresses the product into bits:
The first bit is referred to as the "X" bit and the second as the "Z" bit. This reformulation does not solve the dimensionality problem, but ensures that can be efficiently represented and manipulated with bits. In this form, we can also perform operations between Pauli strings very quickly like multiplication, commutation, and query efficiently for string properties.
PauliString
Making A Pauli String
In Aleph Pauli strings can be handled with the lightweight PauliString data type. It contains two integers for the X and Z bits, as well as a complex coefficient. An object can be created by passing a string with the name of pauli operators, their corresponding sites, and the total number of spins in the Hilbert space.
var ps1 = PauliString("X",[0],2) // 1.0 * Z(0)
var ps2 = 0.5 * PauliString("YX",[0,1],2) // 0.5 * Y(0)*X(1)
Alternatively the more verbose binary form of the integers can also be passed
var ps = PauliString(0B011,0B100,3) // 1.0 * X(0) * X(1) * Z(2)
Some care is needed when constructing terms from their integer representations in either form. The bits are indexed from to from right to left and thus the PauliString will treat the Pauli operator on the right most bits as the first one. Ensure that if individual bits are used that this access order is respected.
Two PauliStrings can be multiplied in and out of place with the multiply and multiplied methods
var ps1 = PauliString("Z",[0],1) // 1.0 * Z(0)
var ps2 = 2.0 * PauliString("X",[0],1) // 2.0 * X(0)
var ps3 = ps1.multiplied(ps2) // 2.0i * Y(0)
var ps4 = ps1 * ps2 // using the * operator.
ps1.multiply(ps2) // ps1 now contains the output of the multiplication.
Constructing many desired Pauli strings can become tedious with these constructors, but there exist functions that allow you to convert from a ComplexOperatorSum and sums of PauliStrings that we will discuss below. This workflow is the recommended one as the Operator framework provides a more intuitive handle on complex Operator expressions.
Extracting Properties
Two member variables can be accessed directly:
coefficient(): returns the complex coefficient.num_spins: returns the number of spins in the Hilbert space of the Pauli string. Using thedisplay()method, a more verbose output of the bit pattern of the pauli string can be seen, along with the information above.
A useful quantity to extract from a PauliString is its weight: the number of non identity operators contained within the string. This can be used in algorithm design to help in filtering Pauli strings or truncate sums if larger weight Pauli's are deemed irrelevant. The weight can be extracted simply as
var ps1 = PauliString("YZ",[0,1],2) // 1.0 * Y(0) * Z(1)
var weight1 = ps1.weight() // weight1 = 2
var ps2 = PauliString(0,0,2) // 1.0 * Id()
var weight2 = ps2.weight // weight2 = 0
A property of Pauli strings is that they either commute or anti commute with each other, i.e. for two Pauli strings
To determine if two strings commute or anti commute the does_commute_with() method can be used
var ps1 = PauliString(0,1,1) // 1.0 * Z(0)
var ps2 = PauliString(1,0,1) // 1.0 * X(0)
var comm1 = ps1.commutes_with(ps2) // false, Z and X on same site anti commute.
var ps3 = PauliString(2,2,1) // 1.0 * Y(1)
var comm2 = ps1.commutes_with(ps3) // true, Z and Y on different sites commute.
PauliStringSum
Often, operations on Pauli strings can return sums of Pauli strings. The PauliStrings introduced above are useful for looking at individual strings but not at manipulating the collection or performing operations that generate an increasing number of strings. The PauliStringSum class does precisely this: it handles sums of Pauli strings for you and provides simple access to operations that increase the term count.
Making and Modifying a PauliStringSum
A PauliStringSum object can be constructed in couple ways:
var Sum1 = PauliStringSum(10) // Empty Sum
var ps1 = PauliString("Z",[0],10) // 1.0 * Z(0)
var Sum2 = PauliStringSum(ps1) // Initializes the sum with the first term being ps
The number of spins in the PauliString and PauliStringSum must match as they may act on different Hilbert spaces. If you have two PauliStrings you can add them with + to make a PauliStringSum object:
var ps1 = PauliString(0,1,2) // 1.0 * Z(0)
var ps2 = PauliString(1,0,2) // 1.0 * X(0)
var Sum = ps1 + ps2
and the Hilbert space information is automatically deduced in the sum.
Sums follow basic algebraic rules such that they can be scaled and added together to return a new sum:
var ps1 = PauliString(0,1,10)
var Sum1 = PauliStringSum(ps1)
var ps2 = PauliString(1,0,10)
var Sum2 = PauliStringSum(ps2)
var Sum3 = 2.0 * Sum1 + -1.0i * Sum2
The coefficients of the PauliStringSum can be modified with a function that acts on the coefficient of the Pauli string and returns a transformed version of the coefficient
def double(Complex val) {
return 2.0 * val
}
var sum = PauliStringSum(10)
sum += 2.0 * PauliString(0B10,0,10) // 2.0 * X(1)
sum.transform_coefficients(double) // sum = 4.0 * X(1)
The function supplied to transform_coefficients must work for complex numbers and cannot couple different coefficients together. Another useful operation is taking the adjoint of a Pauli string:
where in the last equality we use the fact that Pauli operators on different sites commute, they are Hermitian, and that in our representation theres only a single Pauli per site. Both in and out of place adjoints can be taken
var ps1 = 2.0 * PauliString(0,1,2)
var ps2 = 1.0i * PauliString(1,0,2)
var sum = PauliStringSum(ps1)
sum += ps2
var sum2 = sum.adjointed() // out of place, sum2 = 2.0 * Z(0) - 1.0 * X(0)
sum.adjoint() // in place sum = 2.0 * Z(0) - 1.0 * X(0)
Growing the Sum
The PauliStringSum class is designed to make the addition of terms to the sum simple and offer high level control over complicated calculations. Once a sum object has been created, PauliString objects can be added to it:
var Sum = PauliStringSum(4)
// use the += operator to add a term
Sum += 2.0 * PauliString(1,1,4)
// call the append method that does the same thing
Sum.append(-3.0i * PauliString(0,1,4))
Two sums can be multiplied into each other using the * operator
var ps1 = PauliString(0,1,2) // 1.0 * Z(0)
var ps2 = PauliString(1,0,2) // 1.0 * X(0)
var Sum1 = PauliStringSum(ps1)
var Sum2 = PauliStringSum(ps2)
var Sum3 = Sum1 * Sum2 // out of place multiplication.
Another useful operation is evaluating the commutator between two sums. This is useful when expressing many-body Hamiltonian's in PauliStringSum form and solving the Heisenberg equations of motion. It can be calculated explicitly with the commutator method
ar ps1 = PauliString(0,1,1) // 1.0 * Z(0)
var ps2 = PauliString(1,0,1) // 1.0 * X(0)
var Sum1 = PauliStringSum(ps1)
var Sum2 = PauliStringSum(ps2)
var comm = ps1.commutator(ps2) // comm = [ps1,ps2]
Lastly, Pauli strings have well defined behaviour under conjugation by a unitary
when and is a Pauli string. The conjugation can be performed efficiently when the unitary is of this form, regardless of the number of spins in the support of . If and commute, i.e. , then is unchanged under conjugation. Otherwise and anticommute and we find that
Unitaries that can be expressed as a product of such rotations can chain conjugations together to perform the conjugation by . PauliStringSum offers in place and out of place methods to this with pauli_rotate
var ps = PauliString(1,0) // 1.0 * X(0)
var Q = PauliString(0,1) // 1.0 * Z(0)
var sum = PauliStringSum(ps)
var out = sum.pauli_rotated(pi/2,Q) // out = -1.0 * X(0)
sum.pauli_rotate(pi/2,Q) // sum = -1.0 * X(0)
The rotation of the PauliStringSum is done with respect to not like is done for the rotation operations. If comparing expectation values with standard spin 1/2 rotation operators ensure the angles are indeed the same.
Managing The Growth of Terms
While multiplying, commuting, and conjugating two PauliStringSums, the number of terms may grow very fast. The sum can be truncated via the prune method. It takes two arguments:
- : a minimum cutoff value for the norm of the coefficient of the term .
- : a maximum value for the weight of a Pauli string.
This should be called at appropriate places where the number of terms is expected to grow. The values chosen for will depend on the physics of the system studied, the numerical accuracy desired, and the memory constraints.
Expectation Values
After undergoing some process that grows the number of terms, it is useful at time to get the expectation value of the PauliStringSum. One example where this arises naturally is taking the expectation value of Heisenberg evolved an Operator with an initial basis state. This can be done easily with the expval method
var ps = PauliStringSum(1,0,4) // 1.0 * X(0)
var Sum = PauliStringSum(ps)
var state = Qbit(4) // 1.0 * |0000>
var val = Sum.expval(state) // val = 0.0
Sum += PauliString(0,1) // 1.0 * Z(0)
val = Sum.expval(state) // val = 1.0;
Qbit objects are supported for spins but PauliStringSums can up to spins in their Hilbert space. Some care is needed to ensure that when taking expectation value, the support of the terms in the sum does not exceed the number of spins in the Qbit.
Converting to and From Operator Sums.
Often when trying to construct particular Pauli strings passing integers to the constructors can get complicated and tedious. Instead, one can use the Operator framework to first build your initial observable/Hamiltonian and then transform it to a PauliStringSum.
var N = 24
var J = 1.0
var h = 0.3
var H = operator_sum()
for(var s = 0 ; s < N-1 ; ++s)
{
H += J * (Z(s) * Z(s+1))
}
for(var s = 0 ; s < N ; ++s)
{
H += h * X(s)
}
var sum = convert_to_pauli_sum(H)
In the example, the transverse field Ising model is built in terms of a ComplexOperatorSum and then cast to PauliStringSum form using the convert_to_pauli_sum function. Heavy calculations can be done at this step, utilizing the power of the bit representation of the Pauli strings. Once completed, the transformed sum can be converted back to a ComplexOperatorSum via the convert_to_operator_sum function giving you back your symbolic expression.
The recommended workflow is then:
- Define your operator expression as a
ComplexOperatorSum - Perform some initial transformations on it.
- Convert to
PauliStringSumform. - Perform multiplication and conjugation heavy operations.
- Convert back to the symbolic form.
This pipeline allows for high level symbolic control while also turning complex expressions into numerically efficient ones.
Key Takeaways
PauliStrings represent Pauli strings in bit form.- In this bit form key operations like multiplication of Pauli strings can be done efficiently.
PauliStringSums represent sums of Pauli strings, allowing you to perform calculations like multiplying, commuting, and conjugating sums.Operators can be used to define your expressions and serve as an entry point to numerically intensive Pauli calculations.PauliStringSums andComplexOperatorSums can be converted between each other.