4. Pauli Transfer Matrix

The Pauli Transfer Matrix(PTM) of a (potentially unitary) matrix is its representation in Pauli basis, i.e., how it acts on each Pauli string. While this package is based on transforming Pauli strings into one or many Pauli strings, most gates are not defined via the actual PT matrix. However, there are some tools that you can use to work with matrices, both in 0/1 basis and in Pauli basis, potentially to define your own gates.

using PauliPropagation
using LinearAlgebra

Let us generate a random 1-qubit unitary matrix via Pauli matrix exponentials:

# The Pauli matrices are not exported
using PauliPropagation: Xmat, Ymat, Zmat 

U = exp(-im * (randn() * Xmat + randn() * Ymat + randn() * Zmat))
2×2 Matrix{ComplexF64}:
  0.062319+0.34324im   0.126223-0.928639im
 -0.126223-0.928639im  0.062319-0.34324im

Verify that $U$ is unitary via $U \cdot U^\dagger = U^\dagger \cdot U = \mathbb{1}$,

U * U' ≈ U' * U ≈ I(2)
true

This unitary is in the very common 0/1 basis, also called the computational basis. Here is how you can transform it into the Pauli basis:

# note the default `heisenberg=true` kwarg
Uptm = calculateptm(U)
4×4 transpose(::Matrix{Float64}) with eltype Float64:
 1.0   0.0        0.0         0.0
 0.0   0.732509  -0.277211   -0.62176
 0.0  -0.19165   -0.960368    0.202393
 0.0  -0.653224  -0.0290944  -0.756606

This by default returns the PTM of U in the Heisenberg picture, i.e., how it acts in the backpropagation of Pauli strings - the default in this package. To get the Schrödinger version, you can take the transpose of this matrix or call calculateptm(U, heisenberg=false). To convince ourselves that Uptm is also a unitary in this basis, check $U_{ptm} \cdot U_{ptm}^T = U_{ptm}^T \cdot U_{ptm} = \mathbb{1}$ due to unitaries being real-valued in this basis.

Uptm * transpose(Uptm) ≈ transpose(Uptm) * Uptm ≈ I(4)
true

Great, but what does this unitary even represent? We mentioned that it represents the action of U on Pauli strings. A 1-qubit gate can act on 4 Paulis, I, X, Y, and Z, each being represented as $(1, 0, 0, 0)^T$, $(0, 1, 0, 0)^T$, $(0, 0, 1, 0)^T$, and $(0, 0, 0, 1)^T$, respectively. Uptm thus describes how each of those column vectors or an arbitrary sum thereof is transformed.

Defining Gates

These matrices can also be turned into gates via the TransferMapGate. It accepts either the matrix representation of the gate in the 0/1 basis or the Pauli basis.

# on qubit 1
qind = 1

# for the unitary in 0/1 basis
gU = TransferMapGate(U, qind)
TransferMapGate{TransferMap{UInt8, Float64}, TransferMap{UInt8, Float64}, UInt8, true}(TransferMap(4 columns, 10 entries, max 3 mapped terms/column), [1], 0x03, TransferMap(4 columns, 10 entries, max 3 mapped terms/column), 1, 1)
# and also for the PTM
gPTM = TransferMapGate(Uptm, qind)
TransferMapGate{TransferMap{UInt8, Float64}, TransferMap{UInt8, Float64}, UInt8, true}(TransferMap(4 columns, 10 entries, max 3 mapped terms/column), [1], 0x03, TransferMap(4 columns, 10 entries, max 3 mapped terms/column), 1, 1)

And they produce the same gate. What is going on under the hood is that these matrices are converted into what we call transfer maps. They are created in the following way:

ptmap = totransfermap(Uptm)
TransferMap(4 columns, 10 entries, max 3 mapped terms/column)

Remember that we encode our Pauli strings in integers, with single-qubit Paulis being 0 (I), 1 (X), 2 (Y), 3 (Z). If you index into ptmap with those numbers + 1, you will get the corresponding output Paulis with their coefficients. In other words, each entry of a PT map corresponds to a column of the PTM. The Paulis will be set onto the qubit index and the coefficients will be multiplied to the incoming Pauli string's coefficient.

What we also see is that this unitary is 3-branching in Pauli basis. X, Y, and Z Paulis will each map to all three with different coefficients. We can define a TransferMapGate from a PT map, specifying on what qubit it acts (here qubit 1).

g = TransferMapGate(ptmap, qind)
TransferMapGate{TransferMap{UInt8, Float64}, TransferMap{UInt8, Float64}, UInt8, true}(TransferMap(4 columns, 10 entries, max 3 mapped terms/column), [1], 0x03, TransferMap(4 columns, 10 entries, max 3 mapped terms/column), 1, 1)

The gates g, gU and gPTM are all the same gate. Keep in mind, however, that this gate is not parametrized. It always acts the same. This can be seen fact that it is a subtype of StaticGate. Gates subtyping ParametrizedGate will receive parameters from the propagate() function, but TransferMapGates don't use any.

g isa StaticGate
true

Finally, let us define a circuit consisting of this gate on every qubit:

# 6 qubits
nq = 6

# define the circuit as a vector of gates
circuit = [TransferMapGate(ptmap, qind) for qind in 1:nq];

# make the observable a random global Pauli string because the gate acts trivially on identities `I`
pstr = PauliString(nq, [rand((:X, :Y, :Z)) for _ in 1:nq], 1:nq)
PauliString(nqubits: 6, 1.0 * YYXXXX)
psum = propagate(circuit, pstr)
PauliSum(nqubits: 6, 729 Pauli terms:
 -4.1587e-5 * ZXYYYX
 -0.00014175 * XZZYXY
 -0.0021047 * YZXYXX
 -0.015948 * XYZXYZ
 0.00019381 * ZZXZZX
 -0.00060753 * XZXYXX
 0.0018769 * YZZXYX
 0.26554 * YYXXXX
 0.00043791 * YZZYZY
 0.21117 * YYXXZZ
 5.6861e-5 * ZZZXYX
 -0.0013506 * XXYYZX
 0.017883 * XYYXXZ
 -0.0021047 * YZXXXY
 0.0014926 * YZZYZZ
 0.00055067 * YZYXYX
 0.002322 * XZXXXX
 0.00043791 * ZYYYZZ
 4.5218e-5 * ZZZZZY
 0.00015895 * ZXXYXY
  ⋮)

And there you go, you can now easily define gates from their matrix representations, both in 0/1 basis or Pauli basis.

Parametrized PTMs

Remember when we use TransferMapGate, we need a fixed matrix representation which is then transformed into the PTM. We can also compute that matrix for parametrized gates if tomatrix() is defined for them. Here is how to do that for a PauliRotation:

# the matrix in the 0/1 basis
U = tomatrix(PauliRotation(:X, 1), π/4)

# the matrix in the Pauli basis
ptm = calculateptm(U)

# they can again be transformed into gates
# TransferMapGate(U, 1)
TransferMapGate(ptm, 1)
TransferMapGate{TransferMap{UInt8, Float64}, TransferMap{UInt8, Float64}, UInt8, true}(TransferMap(4 columns, 6 entries, max 2 mapped terms/column), [1], 0x03, TransferMap(4 columns, 6 entries, max 2 mapped terms/column), 1, 1)

Compressing Circuits into one PT map

One functionality which may make some gate sequences more performant to simulate is the ability to compress them into one PT map via totransfermap(). Parameters for parametrized gates can be passed as well.

nq = 5

circuit = Gate[]
append!(circuit, CliffordGate(:CNOT, pair) for pair in bricklayertopology(nq))
append!(circuit, CliffordGate(:H, ii) for ii in 1:nq)
append!(circuit, PauliRotation(:Y, ii) for ii in 1:nq)

# the angles for the Pauli rotations
thetas = [π / 8 for _ in 1:nq]

# compile everything into one
ptmap = totransfermap(nq, circuit, thetas)
circuit_map = TransferMapGate(ptmap, 1:nq);

## feel free to print this huge PT map
# print("Now that is a mess! But it is correct:\n ", ptmap)
# it can be apply in one go
pstr = PauliString(nq, [:X for _ in 1:nq], 1:nq)
psum1 = propagate(circuit_map, pstr)
PauliSum(nqubits: 5, 32 Pauli terms:
 0.11548 * ZXIIZ
 -0.11548 * YZYXZ
 -0.047835 * ZXZYY
 -0.019814 * XIZYY
 -0.11548 * YYXXZ
 0.047835 * IZXII
 -0.047835 * YZXZY
 0.019814 * XIZZX
 0.27881 * IZXXZ
 0.047835 * XXZXI
 0.047835 * YYYYX
 -0.11548 * IZYZY
 0.27881 * ZIIZX
 0.047835 * YYYZY
 0.11548 * ZIZXI
 0.047835 * ZXZZX
 -0.11548 * IYXYX
 0.11548 * XXIZX
 -0.019814 * YZYII
 0.019814 * ZXIXI
  ⋮)

Let's verify that we get the same answer using a circuit constructed from TransferMapGates and natively supported gates.

psum2 = propagate(circuit, pstr, thetas)
@show psum1 == psum2
psum1 == psum2 = true







true

Finally, lets's compare their performance

using BenchmarkTools
println("Compiled circuit via transfer map:")
@btime propagate($circuit_map, $pstr);
println("Original circuit:")
@btime propagate($circuit, $pstr, $thetas)
Compiled circuit via transfer map:
  2.559 μs

 (34 allocations: 3.69 KiB)
Original circuit:
  24.305 μs

 (78 allocations: 5.64 KiB)

There are quite a few nuances to discuss here. First, the size of the circuit in terms of the number of qubits $n$ that can be compressed is limited. This is due to the $4^n$ best-case and $8^n$ worst-case memory and time scaling (depending on the branching behavior). It may still be possible and beneficial to compress common gate sequences, for example few-qubit entangling blocks occuring often throughout the circuit. However, it is not guaranteed that this type of PT map compression yields faster gates, especially when compressing few and otherwise highly optimized gates. The TransferMapGate is generic, but its application is not type stable and will induce a lot of memory movement. That being said - try it out for your use-case!

For an example of more low-level definition of of high-performance gates, check out the custom-gates notebook.