Stabilizer Tableau Algebra Manual
The library consists of two main parts: Tools for working with the algebra of Stabilizer tableaux and tools specifically for efficient Circuit Simulation. This chapter discusses the former "lower level" Stabilizer tableau algebra tools.
Pauli Operators
The PauliOperator object represents multi-qubit Pauli operator ($±\{1,i\}\{I,Z,X,Y\}^{\otimes n}$). It is stored in memory as a phase (a single byte where 0x0,0x1,0x2,0x3 corresponds to $1,i,-1,-i$) and two bit-arrays, for X and for Z components.
You can create them with a P string.
julia> P"-iXZ"
-iXZOr by specifying phase and X/Z components:
julia> PauliOperator(0x0,Bool[0,1,0],Bool[0,0,1])
+ _XZBoth underscore and I can be used for identity.
julia> P"I_XYZ"
+ __XYZMultiplication with scalars or other Pauli operators works as expected, as well as tensor products of Pauli operators.
julia> -1im*P"X"
-iX
julia> P"X" * P"Z"
-iY
julia> P"X" ⊗ P"Z"
+ XZOne can check for commutativity with comm.
julia> comm(P"X",P"Z")
0x01
julia> comm(P"XX",P"ZZ")
0x00And check the phase of a product with prodphase.
julia> prodphase(P"X", P"Z")
0x03
julia> prodphase(P"X", P"iZ")
0x00
julia> prodphase(P"X",P"Y")
0x01Indexing operations are available.
julia> p = P"IXYZ";
julia> p[1], p[2], p[3], p[4]
((false, false), (true, false), (true, true), (false, true))
julia> p = P"III";
julia> p[2] = (true, true);
julia> p
+ _Y_Including fancy indexing:
julia> P"IXYZ"[[2,3]]
+ XY
julia> P"IXYZ"[[false,true,true,false]]
+ XYThe operator is represented in memory by bit arrays (much denser than using byte arrays).
julia> p = P"-IXYZ";
julia> p.nqubits, p.xz
(4, UInt64[0x0000000000000006, 0x000000000000000c])Views that give just the X or Z components of the xz bitarray are available through xview and zview.
julia> xview(P"XYZI")
1-element view(::Vector{UInt64}, 1:1) with eltype UInt64:
0x0000000000000003The convenience methods xbit and zbit give you Bool (GF2) vectors.
julia> xbit(P"XYZI")
4-element Vector{Bool}:
1
1
0
0Stabilizers
A Stabilizer object is a tableau of Pauli operators. When the tableau is meant to represent a (pure or mixed) stabilizer state, all of these operators should commute (but that is not enforced, rather Stabilizer is a generic tableau data structure). It is stored in memory as a phase list and a bit-matrix for X and Z components. It can be instantiated by an S string, or with a number of different constructors.
In many cases you probably would prefer to use the MixedDestabilizer data structure, as it caries a lot of useful additional information, like tracking rank and destabilizer operators. Stabilizer has mostly a pedagogical value, and it is also used for slightly faster simulation of a particular subset of Clifford operations. See also the data structures discussion page.
julia> S"-XX
+ZZ"
- XX
+ ZZ
julia> Stabilizer([P"-XX",P"+ZZ"])
- XX
+ ZZ
julia> Stabilizer([0x2, 0x0],
Bool[1 1;
0 0],
Bool[0 0;
1 1])
- XX
+ ZZDirect sums can be performed,
julia> S"-XX" ⊗ S"ZZ"
- XX__
+ __ZZIndexing operations are available, including fancy indexing. Be careful about how phase information gets transferred during sub-indexing.
julia> s = S"-XYZ
-ZIX
+XIZ";
julia> s[1]
- XYZ
julia> s[1,2]
(true, true)
julia> s[[3,1]]
+ X_Z
- XYZ
julia> s[[3,1],[2]]
+ _
- YConsistency at creation is not verified so nonsensical stabilizers can be created, both in terms of content and shape.
julia> S"iX
+Z"
+iX
+ ZSimilarly to the Pauli operators, a bit array representation is used.
julia> s = S"-XXX
+ZZI
-IZZ"
- XXX
+ ZZ_
- _ZZ
julia> phases(s), tab(s).xzs
(UInt8[0x02, 0x00, 0x02], UInt64[0x0000000000000007 0x0000000000000000 0x0000000000000000; 0x0000000000000000 0x0000000000000003 0x0000000000000006])And there are convenience functions that can extract the corresponding binary check matrix.
julia> stab_to_gf2(s)
3×6 Matrix{Bool}:
1 1 1 0 0 0
0 0 0 1 1 0
0 0 0 0 1 1Canonicalization of Stabilizers
Canonicalization (akin to Gaussian elimination over F(2,2)) is implemented in the canonicalize! function. Besides the default canonicalization prescription, alternative ones are available as described in the canonicalization page.
julia> s = S"-XXX
+ZZX
+III";
julia> canonicalize!(s)
+ YY_
+ ZZX
+ ___If phases are inconsequential, the operations can be faster by not tracking and updating them.
julia> s = S"-XXX
+ZZX
+III";
julia> canonicalize!(s; phases=false)
- YY_
+ ZZX
+ ___These operations are in place (as customarily signified by "!").
julia> s = S"-XXX
+ZZX
+III";
julia> canonicalize!(s; phases=false);
julia> s
- YY_
+ ZZX
+ ___Projective Measurements
The project! function is used to perform generic projective measurements.
To observe the effect of different projections, we will start with a GHZ state.
julia> s = S"-XXX
+ZZI
-IZZ";The project! function returns the new stabilizer, the index where the anticommutation was detected, and the result of the projection (nothing being an undetermined result). For instance here we project on an operator that does not commute with all stabilizer generators.
julia> project!(copy(s), P"ZII")[1]
+ Z__
+ ZZ_
- _ZZImportantly, when there is an undetermined result, we return nothing and leave the phase of the new stabilizer the same as the phase of the projection operator. If you want to perform a Monte Carlo simulation, you need to randomize the phase of the stabilizer at the anticommuting index yourself. For instance, one can do:
julia> newstate, anticomindex, result = project!(copy(s), P"XII")
if isnothing(result)
phases(newstate)[anticomindex] = rand([0x0,0x2])
end
result, anticomindex
(nothing, 2)Of course, this is a rather cumbersome way to run a simulation, so we also provide projectrand! which does the necessary randomization automatically, for cases where you do not need the fine grained control of project!.
We can project on a commuting operator, hence no anticommuting terms (the index is zero), and the result is perfectly determined (-1, or in our convention to represent the phase, 0x2).
julia> project!(copy(s), P"-ZZI")
(Stabilizer 3×3, 0, 0x02)When the projection is consistent with the stabilizer (i.e. the measurement result is not nothing), this would trigger an expensive canonicalization procedure in order to calculate the measurement result (unless we are using more advanced data structures to represent the state, which are discussed later). If all you want to know is whether the projection is consistent with the stabilizer, but you do not care about the measurement result, you can skip the canonicalization and calculation of the result.
julia> project!(copy(s), P"-ZZI", keep_result=false)
(Stabilizer 3×3, 0, nothing)Lastly, in either case, you can skip the calculation of the phases as well, if they are unimportant.
julia> project!(copy(s), P"ZZI", phases=false)
(Stabilizer 3×3, 0, 0x00)Sparse single-qubit measurements
In many circumstances only a single-qubit operator is being measured. In that case one should use the projectX!, projectY!, and projectZ! functions as they are much faster thanks to tracking only a single qubit. They have versions that randomize the phase as necessary as well: projectXrand!, projectYrand!, and projectZrand!.
Gate-like interface
If you do not need all this boilerplate, and especially if you want to perform the randomization automatically, you can use the gate-like "symbolic/sparse/named" operations sMX, sMY, and sMZ, that perform the measurement and the necessary randomization of phase. If the measurement result is to be stored, you can use the Register structure that stores both stabilizer tableaux and bit values.
julia> state = Register(ghz(3), [false,false])
Register{Vector{UInt8}, Matrix{UInt64}}(Rank 3 stabilizer
+ Z__
+ _X_
+ __X
═════
+ XXX
+ ZZ_
+ Z_Z
═════
, Bool[0, 0])
julia> apply!(state, sMX(3,2)) # which qubit is measured and in which bit it is stored
Register{Vector{UInt8}, Matrix{UInt64}}(Rank 3 stabilizer
+ Z__
+ _X_
+ Z_Z
═════
+ XXX
+ ZZ_
- __X
═════
, Bool[0, 1])
julia> bitview(state)
2-element Vector{Bool}:
0
1Or you can use the projectXrand!, projectYrand!, and projectZrand! if you prefer a function-call interface.
Partial Traces
Partial trace (using traceout!) over even a single qubit might cause many of them to decohere due to entanglement.
julia> ghz = S"XXX
ZZ_
_ZZ";
julia> traceout!(ghz, [1])
+ _ZZ
+ ___
+ ___This is somewhat more elegant when the datastructure being used explicitly supports mixed states.
julia> ghz = MixedStabilizer(S"XXX
ZZ_
_ZZ");
julia> traceout!(ghz, [1])
+ _ZZGenerating a Pauli Operator with Stabilizer Generators
The generate! function attempts to generate a Pauli operator by multiplying together the operators belonging to a given stabilizer (or reports their independence). This particular function requires the stabilizer to be already canonicalized.
julia> s = S"-XXX
+ZZI
-IZZ";
julia> s = canonicalize!(s)
- XXX
- Z_Z
- _ZZIt modifies the Pauli operator in place, reducing it to identity if possible. The leftover phase is present to indicate if the phase itself could not have been canceled. The list of indices specifies which rows of the stabilizer were used to generated the desired Pauli operator.
julia> generate!(P"XYY", s)
(- ___, [1, 3])Phases can be neglected, for higher performance.
julia> generate!(P"XYY", s, phases=false)
(+ ___, [1, 3])If the Pauli operator can not be generated by the stabilizer, nothing value is returned.
julia> generate!(P"ZZZ", s)
julia> generate!(P"XZX", s)
julia> generate!(P"YYY", s)Clifford Operators
The CliffordOperator structure represents a linear mapping between stabilizers (which should also preserve commutation relationships, but that is not checked at instantiation). These are n-qubit dense tableaux, representing an operation on n-qubit states. For single- or two-qubit gates, it is much more efficient to use small sparse/named/symbolic Clifford operators. A number of predefined Clifford operators are available, their name prefixed with t to mark them as dense tableaux.
julia> tHadamard
X₁ ⟼ + Z
Z₁ ⟼ + X
julia> tPhase
X₁ ⟼ + Y
Z₁ ⟼ + Z
julia> tCNOT
X₁ ⟼ + XX
X₂ ⟼ + _X
Z₁ ⟼ + Z_
Z₂ ⟼ + ZZ
julia> tId1
X₁ ⟼ + X
Z₁ ⟼ + ZChaining and tensor products are possible. Same for qubit permutations.
julia> tHadamard ⊗ tPhase
X₁ ⟼ + Z_
X₂ ⟼ + _Y
Z₁ ⟼ + X_
Z₂ ⟼ + _Z
julia> tHadamard * tPhase
X₁ ⟼ - Y
Z₁ ⟼ + X
julia> permutesubsystem(tCNOT, [2,1])
X₁ ⟼ + X_
X₂ ⟼ + XX
Z₁ ⟼ + ZZ
Z₂ ⟼ + _ZYou can create custom Clifford operators with C-strings or with a list of Pauli operators.
julia> C"-ZZ
+_Z
-X_
+XX"
X₁ ⟼ - ZZ
X₂ ⟼ + _Z
Z₁ ⟼ - X_
Z₂ ⟼ + XX
julia> CliffordOperator([P"-ZZ", P"_Z", P"-X_", P"XX"])
X₁ ⟼ - ZZ
X₂ ⟼ + _Z
Z₁ ⟼ - X_
Z₂ ⟼ + XXNaturally, the operators can be applied to stabilizer states. This includes high performance in-place operations (and the phase can be neglected with phases=false for faster computation).
julia> tCNOT * S"X_"
+ XX
julia> s = S"X_";
julia> apply!(s,tCNOT)
+ XXSparse applications where a small Clifford operator is applied only on a particular subset of a larger stabilizer is also possible, but in such circumstances it is useful to consider using symbolic/sparse/named operators too.
julia> s = S"Z_YX";
julia> apply!(s, tCNOT, [4,2]) # Apply the CNOT on qubits 4 and 2
+ ZXYXPauli operators act as Clifford operators too (but they are rather boring, as they only change signs).
julia> P"XII" * S"ZXX"
- ZXXInternally, the CliffordOperator structure simply stores the tableau representation of the operation.
The apply! function is efficiently multithreaded for CliffordOperators. To start multithreaded Julia, use julia -t<N> where <N> specifies the number of threads.
Symbolic/Sparse/Named Clifford Operators
Much faster implementations for a number of common Clifford operators are available. They are stored as special named small structs, instead of as a full tableau. These are the subtypes of AbstractSingleQubitOperator and AbstractTwoQubitOperator. Currently these are:
subtypes(QuantumClifford.AbstractSingleQubitOperator)16-element Vector{Any}:
SingleQubitOperator
sCXYZ
sCZYX
sHadamard
sHadamardXY
sHadamardYZ
sId1
sInvPhase
sInvSQRTX
sInvSQRTY
sPhase
sSQRTX
sSQRTY
sX
sY
sZsubtypes(QuantumClifford.AbstractTwoQubitOperator)26-element Vector{Any}:
sCNOT
sCPHASE
sCXSWAP
sCZSWAP
sISWAP
sInvISWAP
sInvSQRTXX
sInvSQRTYY
sInvSQRTZZ
sInvSWAPCX
sInvZCrY
sSQRTXX
sSQRTYY
sSQRTZZ
sSWAP
sSWAPCX
sXCX
sXCY
sXCZ
sYCX
sYCY
sYCZ
sZCX
sZCY
sZCZ
sZCrYGenerally, they have the prefix s for symbolic/small/sparse. They are used slightly differently, as one needs to specify the qubits on which they act while instantiating them:
julia> sHadamard(2)
sHadamard on qubit 2
X₁ ⟼ + Z
Z₁ ⟼ + X
julia> sHadamard(2)*S"XXX"
+ XZX
julia> sCNOT(2,3)*S"XYY"
- XXZThe apply! function is efficiently multithreaded for these symbolic operators as well. To start multithreaded Julia, use julia -t<N> where <N> specifies the number of threads.
Symbolic projectors on single qubits also exist: sMX, sMY, sMZ. When used with the Register state representation, they can store the measurement results in the corresponding classical register.
Destabilizers
Slightly abusing the name: What we call "destabilizers" here is a stabilizer and its destabilizing operators saved together. They are implemented with the Destabilizer object and are initialized from a stabilizer.
julia> s=S"-XXX
-ZZI
+IZZ";
julia> d = Destabilizer(s)
𝒟ℯ𝓈𝓉𝒶𝒷
+ Z__
+ _XX
+ __X
𝒮𝓉𝒶𝒷━
- XXX
- ZZ_
+ _ZZThey have convenience methods to extract only the stabilizer and destabilizer pieces:
julia> stabilizerview(d)
- XXX
- ZZ_
+ _ZZ
julia> destabilizerview(d)
+ Z__
+ _XX
+ __XImportantly commuting projections are much faster when tracking the destabilizer as canonicalization is not necessary (an $\mathcal{O}(n^2)$ complexity because it avoids the expensive $\mathcal{O}(n^3)$ canonicalization operation).
julia> project!(d,P"ZZI")
(Destablizer 3×3, 0, 0x02)Non-commuting projections are just as fast as when using only stabilizers.
julia> project!(d,P"ZZZ")
(Destablizer 3×3, 1, nothing)Clifford operations can be applied the same way they are applied to stabilizers.
julia> apply!(d,tCNOT⊗tHadamard)
𝒟ℯ𝓈𝓉𝒶𝒷
- X_Z
+ _XZ
+ X__
𝒮𝓉𝒶𝒷━
+ _ZX
- _Z_
+ ZZXMixed States
Both the Stabilizer and Destabilizer structures have more general forms that enable work with mixed stabilizer states. They are the MixedStabilizer and MixedDestabilizer structures, described in Mixed States. More information that can be seen in the data structures page, which expands upon the algorithms available for each structure.
Random States and Circuits
random_clifford, random_stabilizer, and enumerate_cliffords can be used for the generation of random states.