PauliMasks and PauliSums¶
In this notebook, we'll walk through some of the basic ways to represent Pauli operators in workbench_algorithms.
PauliMask¶
PauliMasks are workbench_algorithm's internal data representation of single-qubit Pauli operators.
Unlike other ways to store these operators - such as storing them explicitly as matrices in numpy - PauliMasks use a bit-wise representation to indicate the type of Pauli operator acting on each qubit due to the unique algebra associated with multiplying Pauli matrices together. Thanks to this bit-wise representation, manipulating PauliMasks is especially efficient!
Let's look at how to build some PauliMasks so we can get more comfortable with the notation!
Creating PauliMasks with Integers¶
The typical way to instantiate a PauliMask is by passing two integers: the x_mask and the z_mask.
As a quick sidenote, both WorkBench and Python use little-endian notation, so the least significant bit (corresponding to the top-most qubit) is at the rightmost place in the bitstring.
To understand how these masks are used to represent Pauli operators, imagine a system of two qubits. If we try to represent the Pauli-X operator on the first qubit ("X0"), we can think of this as simply noting that a Pauli-X operator is being applied to the top-most (rightmost) qubit (bit). Likewise, if we have a Pauli-Z operator acting on the second qubit ("Z1"), we can simply note that a Pauli-Z operator is being applied to the bottom-most (leftmost) qubit (bit). We can efficiently represent these two actions as binary numbers "acting" on our register of qubits.
If we wish to represent this operator ("X0 Z1"), then we have to pass the bit masks 01 and 10 as the x_mask and z_mask respectively. In integer notation, these two numbers are 1 and 2 repsectively, so we can create our PauliMask by passing these two integers:
%load_ext autoreload
%autoreload 2
from psiqworkbench import Qubits, QPU
from workbench_algorithms.utils.paulimask import PauliMask, PauliSum, pauli_sum_to_numpy
import numpy as np
xz = PauliMask(1, 2)
print(xz.get_pauli_string())
X0 Z1
If we want to represent a Pauli-Y operator, we simply note that we can represent represent Y as both X and Z acting on that qubit!
Therefore, if we want to represent the operator X0 Y1, we can apply our x_mask to both qubits and apply our z_mask to just the second qubit -- since both terms are "turned on" for the second qubit, the resulting operation will be a Y. We can realize this using the x_mask "11" (which is the integer 3) and the z_mask "10" (which is the integer 2):
xy = PauliMask(3, 2)
print(xy.get_pauli_string())
X0 Y1
Creating PauliMasks with PauliStrings¶
We began with creating PauliMasks using integers representing bit masks since that is the underlying data representation of the PauliMask class and what allows for very efficient manipulation of these objects. However, sometimes it is more user-friendly to create these PauliMasks from strings representing our Pauli operators.
We can also do this easily using the from_pauli_string method:
xy = PauliMask.from_pauli_string("X0 Y1")
print(xy.mask)
print(xy.get_pauli_string())
xyiiiy = PauliMask.from_pauli_string("X0 Y1 Y5")
print(xyiiiy.mask)
print(xyiiiy.get_pauli_string())
assert xyiiiy.mask == PauliMask(35, 34).mask
xyiiiy = PauliMask.from_pauli_string("X0 Y1 I2 I3 I4 Y5")
print(xyiiiy.mask)
print(xyiiiy.get_pauli_string())
assert xyiiiy.mask == PauliMask(35, 34).mask
(3, 2) X0 Y1 (35, 34) X0 Y1 Y5 (35, 34) X0 Y1 Y5
Manipulating PauliMasks¶
We can also do some handy algebra using the PauliMask object directly such as multiplying. These manipulations are where the efficiency of these bit mask representations shine. Let's check some basic Pauli algebra!
We should note though that the PauliMask object has no coefficient nor sign, so such algebraic rules are neglected here.
x = PauliMask(1, 0)
z = PauliMask(0, 1)
y = x * z
print(y.get_pauli_string())
Y0
import time
some_big_operator = PauliMask(122398295302235234624, 1232234634544352536)
another_big_operator = PauliMask(854347345634252822, 235197623462345645782)
start = time.time()
mult = (some_big_operator * another_big_operator).get_pauli_string()
time_to_compute = time.time() - start
print(mult)
print("Time to compute: {} sec".format(time_to_compute))
Y1 Y2 Z3 X4 Y6 Z7 Y8 Z9 X11 Y12 X13 X14 X15 X16 Y17 X19 Y22 Z23 Z25 Z26 Y27 X28 X29 Z31 Y32 Y33 Y34 Y36 Y37 X38 Z39 X40 Z41 Z42 X43 Y44 X46 X48 Y50 Z51 Z52 X54 Y56 X59 Z60 X61 Z62 Y63 X65 Y66 Z67 Time to compute: 6.0558319091796875e-05 sec
Now that's wicked fast for some really big operators!!!
We can also do some convenient checks regarding commutation:
i = PauliMask(0, 0)
print(x.commute_check(z))
print(x.commute_check(x))
print(z.commute_check(i))
False True True
We can get the commutator of PauliMask, with (returns a PauliSum) or without the phase (returns a PauliMask):
xxy = PauliMask(7,4)
zyx = PauliMask(6,3)
print("Without the phase ", xxy.commutator(zyx).get_pauli_string())
print("With the phase ", xxy.commutator(zyx,drop_phase=False))
Without the phase Y0 Z1 Z2 With the phase -2j*Y0 Z1 Z2
Inspecting Elements of a PauliMask¶
There are also some great helper functions to access particular elements of a PauliMask like getting the qubit indices that the operator acts on nontrivially using get_indices().
Likewise, we can also check a specific qubit index to see what Pauli is acting on the qubit at the given index using get_pauli().
print(xyiiiy.get_indices())
for index in range(6):
pauli = xyiiiy.get_pauli(index)
print("Operator acting on Qubit {}: ".format(index), pauli)
[0, 1, 5] Operator acting on Qubit 0: X Operator acting on Qubit 1: Y Operator acting on Qubit 2: None Operator acting on Qubit 3: None Operator acting on Qubit 4: None Operator acting on Qubit 5: Y
x_plus_y = x + y
print(x_plus_y)
print(pauli_sum_to_numpy(x_plus_y))
x_plus_y = PauliSum(
[1, x],
[1, y],
)
print("\n", x_plus_y)
print(pauli_sum_to_numpy(x_plus_y))
1*X0 + 1*Y0 [[0.+0.j 1.-1.j] [1.+1.j 0.+0.j]] 1*X0 + 1*Y0 [[0.+0.j 1.-1.j] [1.+1.j 0.+0.j]]
As you see above, workbench_algorithms also provides a function pauli_sum_to_numpy() so that we can easily display the PauliSum in matrix format :D
Getting Some Useful Information About a PauliSum¶
There are also some helpful methods to get some useful information about a PauliSum object.
Some commonly used ones are:
wires(): counts the number of qubits that thePauliSumspans across (including qubits only acted on by the identity operator)width(): counts the number of qubits acted on non-triviallynorm(): calculates the norm (sum of the magnitude of the coefficients) of thePauliSum
pauli_sum = PauliSum(
[1, PauliMask.from_pauli_string("Z0 X5 Y11")],
[-15, PauliMask.from_pauli_string("Y0 Z4")],
[0.25, PauliMask.from_pauli_string("Z5 Z8 Y9 X12")],
[0.25, PauliMask.from_pauli_string("Z3 Z8")],
[(1/3), PauliMask.from_pauli_string("X3 Z7 Z8 X9 Y11")],
)
print(pauli_sum)
print("Number of Wires: ", pauli_sum.wires())
print("Width: ", pauli_sum.width())
print("Norm: ", pauli_sum.norm())
1*Z0 X5 Y11 + -15*Y0 Z4 + 0.25*Z5 Z8 Y9 X12 + 0.25*Z3 Z8 + 0.3333333333333333*X3 Z7 Z8 X9 Y11 Number of Wires: 13 Width: 9 Norm: 16.833333333333332
We can also get a list of the coefficients using get_coefficients() and get the assoicated amplitudes using get_padded_amplitudes() - these are the coefficients divided by the norm and also padded so that the length of the list is an integer power of 2 - which can come in handy when building operations such as PREPARE and SELECT.
print(pauli_sum.get_coefficients())
print(pauli_sum.get_padded_abs_amplitudes())
[1, -15, 0.25, 0.25, 0.3333333333333333] [0.05940594059405941, 0.8910891089108911, 0.014851485148514853, 0.014851485148514853, 0.019801980198019802, 0, 0, 0]
Manipulating PauliSums¶
There are also lots of helper methods to perform some basic manipulations of PauliSums such as:
- normalizing the operator with
normalize() - adding an identity offset with
add_identity_offset() - removing all terms with coefficients below some threshold with
remove_below() - getting the commutator of two
PauliSumwithcommutator()
print("Unnormalized:\n", pauli_sum)
print("Normalized:\n", pauli_sum.normalize())
Unnormalized: 1*Z0 X5 Y11 + -15*Y0 Z4 + 0.25*Z5 Z8 Y9 X12 + 0.25*Z3 Z8 + 0.3333333333333333*X3 Z7 Z8 X9 Y11 Normalized: 0.05940594059405941*Z0 X5 Y11 + -0.8910891089108911*Y0 Z4 + 0.014851485148514853*Z5 Z8 Y9 X12 + 0.014851485148514853*Z3 Z8 + 0.019801980198019802*X3 Z7 Z8 X9 Y11
coefficient, shifted_sum = pauli_sum.add_identity_offset()
print(shifted_sum)
1*Z0 X5 Y11 + -15*Y0 Z4 + 0.25*Z5 Z8 Y9 X12 + 0.25*Z3 Z8 + 0.3333333333333333*X3 Z7 Z8 X9 Y11 + 16.833333333333332*I
print(shifted_sum.remove_below(1))
1*Z0 X5 Y11 + -15*Y0 Z4 + 16.833333333333332*I
print(pauli_sum.commutator(xxy+zyx))
2j*Y0 X1 Y2 X5 Y11 + 0.0*I + 30j*Z0 X1 Y2 Z4 + -30j*X0 Y1 X2 Z4