2. Explaining our Data Types

What are data types? In imprecise terms, they are wrappers around attributes or values that one can define computational behavior on. In the Julia programming language, one defines data types via a struct.

Our most important data type is PauliSum, which is a wrapper for a sum of Pauli strings. What is a Pauli string? Mathematically they are a tensor product of single-qubit Pauli matrices. For example, $I \otimes X \otimes Z$ is a 3-qubit Pauli string. For usability, we also have a PauliString type, but it is currently only meant as a higher-level representation of one Pauli string.

using PauliPropagation

Let us start by defining the number of qubits, here 3.

nqubits = 3
3

We start by defining a PauliString. As arguments, it expects the number of qubits, a list of symbols and a list of qubit indices. For convenience you can also pass one symbol and one qubit index, in case the Pauli string really only has support on one site. The way we denote Paulis at the high level is via Julia Symbols, which start with a colon :.

# this works 
PauliString(nqubits, :X, 2)
PauliString(nqubits: 3, 1.0 * IXI)
# and this works more generally
pstr = PauliString(nqubits, [:X, :Y], [2, 3])
PauliString(nqubits: 3, 1.0 * IXY)

PauliString has the attributes nqubits and term. The latter is our efficient low-level implementation.

bit_pstr = pstr.term
0x24

What is 0x24? This is how unsigned integer types display in many languages, and unsigned integers (here the 8-bit version UInt8) is how we encode our Pauli strings at a low level - as pairs of two bits!

See that this is just a number, but don't worry too much about what particular number it is or why it displays as `0x24.

print(0x24)
36

Now to the more important part: Bits. With the Bits package we can easily display the bits of the unsigned integer. Note that there qubits are indexed from right to left, but you do not need to remember this if you use our functions.

using Bits
bits(bit_pstr)
<00100100>

Read this as 00 10 01 00 in pairs from right to left. The above shows that the first Pauli (on the right) is 00 (the number 0), which is I. The second is 01 (the number 1), which is X. The third is 10 (the number 2), which is Y. All bits beyond the left Pauli string limit will be zero. Do you see how those bits match the definition of pstr? Note that under the hood, we try to use the smallest integer type that can carry the full Pauli string. We can retrieve the Paulis on a low level via getpauli(), which is very important when implementing custom gates.

# use `getpauli(bit_pstr, qind) to get the Paulis as 0, 1, 2, or 3 on the site `qind`
getpauli(bit_pstr, 1), getpauli(bit_pstr, 2), getpauli(bit_pstr, 3) # IXY, as espected
(0x00, 0x01, 0x02)

Equality checks between unsigned integers and regular integers work:

0x00 == 0 , 0x01 == 1, 0x02 == 2, 0x03 == 3
(true, true, true, true)

And for clarity, here the bits:

bits(0x00), bits(0x01), bits(0x02), bits(0x03)
(<00000000>, <00000001>, <00000010>, <00000011>)

If you wanted, you could also get several Paulis from a Pauli string and pack them into one integer. This can also be important for highly efficient gates.

paulis = getpauli(bit_pstr, [2, 3])
0x09
bits(paulis)
<00001001>

Here, paulis has the Pauli on site 2 of pstr.term on its first site, and the Pauli on site 3 of pstr.term on its second site. Setting Paulis on the bit representation is of course also possible. We do that via setpauli().

# use `setpauli(bit_pstr, target_bit_pauli, qind) to get the Paulis as 0, 1, 2, or 3 on the site `qind`
new_pauli = :X  # 1 also works for very high-performance case, similarly :Y vs 2 and :Z vs 3
new_bit_pstr = setpauli(bit_pstr, :X, 1)
0x25
bits(new_bit_pstr)
<00100101>

Note that you can also set sequences of bits like with getpauli() above:

bits(setpauli(bit_pstr, paulis, [1, 2]))  # [X, Y] from above set into the bits of site 1 and 2
<00101001>

Now you have got to know our PauliString type and the lower workings of integer Pauli strings, let's briefly cover our high-level working horse: The PauliSum type. Create an empty Pauli sum:

psum = PauliSum(nqubits)
PauliSum(nqubits: 3, (no Pauli strings))

If we inspect the Pauli sum psum, it carries two attributes: nqubits and terms.

psum.nqubits
3
psum.terms
Dict{UInt8, Float64}()

terms are the collection of integer Pauli strings and their respective coefficients. We store them as a dictionary, which is currently empty. It says the type of that dictionary is Dict{UInt8, Float64}, and we will see what that means.

Let us now add terms to the Pauli sum. We simply do this by calling the add!() function on psum with some extra information about the Pauli string that we want to add. All the ways in which you can add terms to a PauliSum you can also create PauliStrings from above. They use the same syntax.

add!(psum, :X, 2)  # this adds 1.0 * IXI
add!(psum, [:Y, :Z], [1, 3], 0.5)  # this adds 0.5 * YIZ

psum  # the display order usually does not match the order in which you added the terms, but that is fine.
PauliSum(nqubits: 3, 2 Pauli terms:
 1.0 * IXI
 0.5 * YIZ
)
# operations like "+" or "-" work, but copy the entire Pauli sum
psum - pstr
PauliSum(nqubits: 3, 3 Pauli terms:
 1.0 * IXI
 0.5 * YIZ
 -1.0 * IXY
)
psum + psum
PauliSum(nqubits: 3, 2 Pauli terms:
 2.0 * IXI
 1.0 * YIZ
)
# this modifies in-place and is thus faster
add!(psum, pstr)
PauliSum(nqubits: 3, 3 Pauli terms:
 1.0 * IXI
 0.5 * YIZ
 1.0 * IXY
)

Let us now dig a bit deeper and look at the terms:

psum.terms
Dict{UInt8, Float64} with 3 entries:
  0x04 => 1.0
  0x32 => 0.5
  0x24 => 1.0

Here 0x04, 0x32, 0x24 are again our low-level implementation of Pauli strings as unsigned integers, here as 8-bit unsigned integers UInt8. The values of the dictionary are the coefficients.

println(0x4)
println(0x32)
println(0x24)
4


50
36

Here are some more examples of code snippets that work:

getcoeff(psum, :X, 2)
1.0
getcoeff(psum, [:Y, :Z], [1, 3])
0.5
mult!(psum, 0.3) # in-place
PauliSum(nqubits: 3, 3 Pauli terms:
 0.3 * IXI
 0.15 * YIZ
 0.3 * IXY
)
set!(psum, 0x45, -1.3)  # this is for the low level currently and cannot be used with Symbols :X, :Y, :Z
PauliSum(nqubits: 3, 4 Pauli terms:
 0.3 * IXI
 0.15 * YIZ
 -1.3 * XXI
 0.3 * IXY
)