Adding Symmetry to Pauli Propagation
using PauliPropagation
using PauliPropagation: _translatetolowestinteger, inttostringIn this notebook, we show how symmetry can be added in Pauli Propagation. The basic idea is to merge Pauli strings if they are related by a symmetry.
The related publication is Leveraging Symmetry Merging in Pauli Propagation.
Lowest integer representation (canonical representation)
For instance, consider the following operator:
\[ P = XII + IXI + IIX. \]
Clearly, the Pauli string $P$ is translational invariant. Suppose we can just work with a representative $XII$, then the effective Pauli string is:
\[ \tilde{P} = 3 XII. \]
# Let us define the initial Pauli string
nq = 3
p = PauliSum(nq)
for i in 1:nq
add!(p, :X, i)
end
println("Initial Pauli string: ", p)Initial Pauli string: PauliSum(nqubits: 3, 3 Pauli terms:
1.0 * IXI
1.0 * IIX
1.0 * XII
)First we discuss how to find the representatives.
Consider the translation symmetru in $1d$, we adopt representatives that are given by the lowest integers.
# To find the representative, we define a function that maps a Pauli string to an integer.
# Consider the second Pauli string:
pstr = PauliString(nq, :X, 2)
# The integer representation is given by:
println("Integer representation of ", pstr, " is ", pstr.term)
# We can then translate the integer to the lowest integer in its symmetry class:
rep_int = _translatetolowestinteger(pstr.term, nq)
println("The representative integer is ", rep_int)
# We can verify that this lowest integer representation corresponds to XII
println("The representative Pauli string is XII: ", PauliString(nq, :X, 1).term)Integer representation of PauliString(nqubits: 3, 1.0 * IXI)
is 4
The representative integer is
1
The representative Pauli string is XII: 1# To get the effective representation of the original Pauli string, we can do:
translationmerge(p)PauliSum(nqubits: 3, 1 Pauli term:
3.0 * XII
)As another example, consider the two Pauli strings $XIZI$ and $ZIXI$. Their representative is given by the latter because we want the smaller Pauli to be in the higher position and identities as far as possible.
# Let's verify that is the case:
nq = 4
pstr = PauliString(nq, [:X, :Z], [2, 4])
println("The integer representation of ", pstr, " is ", pstr.term)
println("The lowest integer representation of ", pstr, " is ", _translatetolowestinteger(pstr.term, nq))
pstr = PauliString(nq, [:Z, :X], [1, 3])
println("The integer representation of ", pstr, " is ", pstr.term)
# println("The lowest integer representation of ", pstr, " is ", _translatetolowestinteger(pstr.term, nq))The integer representation of PauliString(nqubits: 4, 1.0 * IXIZ)
is 196
The lowest integer representation of PauliString(nqubits: 4, 1.0 * IXIZ) is 19
The integer representation of PauliString(nqubits: 4, 1.0 * ZIXI) is 19We can also apply this in $2d$. It's a bit tricky to visualize a $2d$ Pauli string but we can use the helper function
# Imagine we have a 2x3 lattice with periodic boundary conditions.
# We can represent a Pauli string on this lattice using a 6-qubit Pauli string.
# For example, consider the following Pauli string:
nx, ny = 2, 3
nq = nx * ny
pstr = PauliString(nq, [:X, :Y, :Z], [1, 3, 5])
println("The integer representation of ", pstr, " is ", pstr.term)
# To find its representative under 2D translations, we can use the following helper function:
println("The 2d visualization of ", pstr, " is:" )
println(inttostring(pstr.term, nx, ny))
The integer representation of PauliString(nqubits: 6, 1.0 * XIYIZI)
is 801
The 2d visualization of PauliString(nqubits: 6, 1.0 * XIYIZI) is:
XI
YI
ZI# Now consider another Pauli string that is a translation of the previous one:
pstr2 = PauliString(nq, [:X, :Y, :Z], [2, 4, 6])
println("The integer representation of ", pstr2, " is ", pstr2.term)
println("The 2d visualization of ", pstr2, " is:" )
println(inttostring(pstr2.term, nx, ny))
pstr3 = PauliString(nq, [:Z, :X, :Y], [4, 6, 2])
println("The integer representation of ", pstr3, " is ", pstr3.term)
println("The 2d visualization of ", pstr3, " is:" )
println(inttostring(pstr3.term, nx, ny))The integer representation of PauliString(nqubits: 6, 1.0 * IXIYIZ) is 3204
The 2d visualization of PauliString(nqubits: 6, 1.0 * IXIYIZ) is:
IX
IY
IZ
The integer representation of PauliString(nqubits: 6, 1.0 * IYIZIX) is 1224
The 2d visualization of PauliString(nqubits: 6, 1.0 * IYIZIX) is:
IY
IZ
IX# Turns out for 2d finding the lowest integer representation can be speed up
# by precomputing "masks" for translations in x and y directions.
# See `_translatetolowestinteger` in the source code for details.
# We will just call the function `translationmerge` here.
# The representative Pauli string is given by
psum = PauliSum([pstr, pstr2, pstr3])
rep_int_2d = translationmerge(psum, nx, ny)PauliSum(nqubits: 6, 1 Pauli term:
3.0 * YIZIXI
)You can also define the merging function for your own symmetry. Let's see an example of this using permutation symmetry
We can define the representative of Paulis under permutation symmetry by counting the number of Pauli $X, Y, Z$
function _permutationcanonicalform(pstr::PauliStringType)
if pstr == 0
return pstr
end
num_x = countx(pstr)
num_y = county(pstr)
num_z = countz(pstr)
# construct the canonical form PauliString
pstr_canonical = identitylike(pstr)
for ii in 1:num_x
pstr_canonical = setpauli(pstr_canonical, :X, ii)
end
for ii in (num_x+1):(num_x+num_y)
pstr_canonical = setpauli(pstr_canonical, :Y, ii)
end
for ii in (num_x+num_y+1):(num_x+num_y+num_z)
pstr_canonical = setpauli(pstr_canonical, :Z, ii)
end
return pstr_canonical
end_permutationcanonicalform (generic function with 1 method)# Now define a shorthand for merging under permutation symmetry
permutationmerge(psum::PauliSum) = symmetrymerge(
psum, (pstr -> _permutationcanonicalform(pstr))
)permutationmerge (generic function with 1 method)# Let's see some examples of merging!
pstr1 = PauliString(4, [:X, :Y, :Z], [1, 2, 3])
pstr2 = PauliString(4, [:Y, :Z, :X], [2, 3, 4])
pstr3 = PauliString(4, [:Z, :X, :Y], [1, 3, 4])
psum = PauliSum([pstr1, pstr2, pstr3])
rep_perm = permutationmerge(psum)
println("The representative under permutation symmetry is: ", rep_perm)The representative under permutation symmetry is: PauliSum(nqubits: 4, 1 Pauli term:
3.0 * XYZI
)Let's use translation symmetry in propagation of a Hamiltonian evolution
Consider the dynamics by the tilted-field Ising model implemented in tiltedtfitrottercircuit.
\[ U(\Delta t) = \prod_{a=1}^{l} \prod_{j=1}^n e^{-i \Delta t g_x X_j} \prod_{j=1}^{n-1} e^{-i \Delta t g_z Z_j} e^{-i \Delta t g_z Z_j Z_{j+1}}. \]
# Let's set up the circuit
nq = 11
periodic = true # periodic boundary condition for translation symmetry
deltat = 0.1
layers = [3, 5] # number of layers to look at
circ = tiltedtfitrottercircuit(nq, 1; topology=bricklayertopology(nq, periodic=periodic))
33-element Vector{Gate}:
PauliRotation([:Z, :Z], [1, 2])
PauliRotation([:Z, :Z], [3, 4])
PauliRotation([:Z, :Z], [5, 6])
PauliRotation([:Z, :Z], [7, 8])
PauliRotation([:Z, :Z], [9, 10])
PauliRotation([:Z, :Z], [11, 1])
PauliRotation([:Z, :Z], [2, 3])
PauliRotation([:Z, :Z], [4, 5])
PauliRotation([:Z, :Z], [6, 7])
PauliRotation([:Z, :Z], [8, 9])
PauliRotation([:Z, :Z], [10, 11])
PauliRotation([:Z], [1])
PauliRotation([:Z], [2])
⋮
PauliRotation([:Z], [11])
PauliRotation([:X], [1])
PauliRotation([:X], [2])
PauliRotation([:X], [3])
PauliRotation([:X], [4])
PauliRotation([:X], [5])
PauliRotation([:X], [6])
PauliRotation([:X], [7])
PauliRotation([:X], [8])
PauliRotation([:X], [9])
PauliRotation([:X], [10])
PauliRotation([:X], [11])# Helper functions for the evolution angles
function get_tfim_couplings(circuit, deltat::Float64; J=1, hz=0.9045, hx=1.4)
thetas = []
indices_gate_zz = getparameterindices(circuit, PauliRotation, [:Z, :Z])
indices_gate_x = getparameterindices(circuit, PauliRotation, [:X])
indices_gate_z = getparameterindices(circuit, PauliRotation, [:Z])
for (idx, gate) in enumerate(circuit)
if idx in indices_gate_zz
coupling_strength = - deltat * J
elseif idx in indices_gate_x
coupling_strength = - deltat * hx
elseif idx in indices_gate_z
coupling_strength = - deltat * hz
else
throw(ArgumentError("Gate at index $idx is neither ZZ, X nor Z rotation"))
end
push!(thetas, coupling_strength * 2) # e^i t P / 2 * 2
end
return thetas
endget_tfim_couplings (generic function with 1 method)# Build evolution angles for TFIM model
nl = maximum(layers)
thetas = get_tfim_couplings(circ, deltat) # angle per layer
# initialie a list to store results
results = []
for merging in (:full, :none) # :full means translation merging
# Consider the inital pauli string
p_evolved = PauliString(nq, :Z, div(nq, 2) + 1)
println("Initial Pauli string: ", p_evolved)
for l in collect(1:maximum(layers))
# Evolution 1 full Trotter layer
p_evolved = propagate(circ, p_evolved, thetas; layer=1)
if merging == :none
# no merging
elseif merging == :full
p_evolved = translationmerge(p_evolved)
end
if l in layers
println("Layer $l with merging type: $merging")
overlap = overlapwithzero(p_evolved)
push!(results, (
merging=merging, layer=l, nterms=length(p_evolved),
overlap=overlap
)
)
end
end
end Initial Pauli string: PauliString(nqubits: 11, 1.0 * IIIIIZIIIII)
Layer 3 with merging type: full
Layer 5 with merging type: full
Initial Pauli string: PauliString(nqubits: 11, 1.0 * IIIIIZIIIII)
Layer 3 with merging type: none
Layer 5 with merging type: none# Check the results:
results4-element Vector{Any}:
(merging = :full, layer = 3, nterms = 1422, overlap = 0.7309034078384146)
(merging = :full, layer = 5, nterms = 12172, overlap = 0.5258230815192094)
(merging = :none, layer = 3, nterms = 1972, overlap = 0.7309034078384142)
(merging = :none, layer = 5, nterms = 34617, overlap = 0.5258230802947047)# verify that the expectatin values of symmetry merged Pauli strings are the same as those without merging
for l in layers
overlap_none = filter(r -> r.merging == :none && r.layer == l, results)[1].overlap
overlap_full = filter(r -> r.merging == :full && r.layer == l, results)[1].overlap
@assert isapprox(overlap_none, overlap_full)
endNotice that the minimal symmetric circuit layer is actually not necessary the full layer! For instance, instead of working with the trotter layer implemented in tiltedtfitrottercircuit. let's merge after each sub-layers as the follows: apply
\[ U(\Delta t)_Z = \prod_{j=1}^{n-1} e^{-i \Delta t g_z Z_j} e^{-i \Delta t g_z Z_j Z_{j+1}}, \]
merge and apply
\[ U(\Delta t)_X = \prod_{j=1}^n e^{-i \Delta t g_x X_j}, \]
merge. The two sublayers are each translation invariant but form the total Trotter layer.
# let's do the above merging for the sublayers
circ_z = circ[1:2*nq] # get the ZZ and Z layers
circ_x = circ[2*nq+1:end] # get the X layers
thetas_z = thetas[1:2*nq]
thetas_x = thetas[2*nq+1:end]
p_evolved = PauliString(nq, :Z, div(nq, 2) + 1)
println("Initial Pauli string: ", p_evolved)
for l in collect(1:maximum(layers))
# Evolution under Z layer
p_evolved = propagate(circ_z, p_evolved, thetas_z; layer=1)
p_evolved = translationmerge(p_evolved)
# Evolution under X layer
p_evolved = propagate(circ_x, p_evolved, thetas_x; layer=1)
p_evolved = translationmerge(p_evolved)
if l in layers
overlap = overlapwithzero(p_evolved)
push!(results, (
merging=:sublayer, layer=l, nterms=length(p_evolved),
overlap=overlap
)
)
end
end Initial Pauli string: PauliString(nqubits: 11, 1.0 * IIIIIZIIIII)results6-element Vector{Any}:
(merging = :full, layer = 3, nterms = 1422, overlap = 0.7309034078384146)
(merging = :full, layer = 5, nterms = 12172, overlap = 0.5258230815192094)
(merging = :none, layer = 3, nterms = 1972, overlap = 0.7309034078384142)
(merging = :none, layer = 5, nterms = 34617, overlap = 0.5258230802947047)
(merging = :sublayer, layer = 3, nterms = 300, overlap = 0.7309034078384145)
(merging = :sublayer, layer = 5, nterms = 6753, overlap = 0.5258230815192095)! Interestingly merging every sublayers reduce the number of Pauli terms even further!
# verify that the expectatin values of symmetry merged Pauli strings are the same as those without merging
for l in layers
overlap_none = filter(r -> r.merging == :none && r.layer == l, results)[1].overlap
overlap_full = filter(r -> r.merging == :full && r.layer == l, results)[1].overlap
overlap_sublayer = filter(r -> r.merging == :sublayer && r.layer == l, results)[1].overlap
@assert isapprox(overlap_none, overlap_full)
@assert isapprox(overlap_none, overlap_sublayer)
end