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"
-iXZ

Or by specifying phase and X/Z components:

julia> PauliOperator(0x0,Bool[0,1,0],Bool[0,0,1])
+ _XZ

Both underscore and I can be used for identity.

julia> P"I_XYZ"
+ __XYZ

Multiplication 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"
+ XZ

One can check for commutativity with comm.

julia> comm(P"X",P"Z")
0x01

julia> comm(P"XX",P"ZZ")
0x00

And 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")
0x01

Indexing 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]]
+ XY

The 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:
 0x0000000000000003

The convenience methods xbit and zbit give you Bool (GF2) vectors.

julia> xbit(P"XYZI")
4-element Vector{Bool}:
 1
 1
 0
 0

Stabilizers

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.

Stabilizers and Destabilizers

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
+ ZZ

Direct sums can be performed,

julia> S"-XX" ⊗ S"ZZ"
- XX__
+ __ZZ

Indexing 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]]
+ _
- Y

Consistency at creation is not verified so nonsensical stabilizers can be created, both in terms of content and shape.

julia> S"iX
         +Z"
+iX
+ Z

Similarly 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  1

Canonicalization 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.

Single qubit projections

If you know your Pauli measurement operator acts on a single qubit, there are much faster projection functions available, discussed in the next section. Namely projectX!, projectY!, and projectZ!.

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_
- _ZZ

Importantly, 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" objects 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
 1

Or 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])
+ _ZZ

Generating 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
- _ZZ

It 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 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₁ ⟼ + Z

Chaining 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> permute(tCNOT, [2,1])
X₁ ⟼ + X_
X₂ ⟼ + XX
Z₁ ⟼ + ZZ
Z₂ ⟼ + _Z

You 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₂ ⟼ + XX

Naturally, 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)
+ XX

Sparse 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 operators too.

julia> s = S"Z_YX";

julia> apply!(s, tCNOT, [4,2]) # Apply the CNOT on qubits 4 and 2
+ ZXYX

Pauli operators act as Clifford operators too (but they are rather boring, as they only change signs).

julia> P"XII" * S"ZXX"
- ZXX

Internally, 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 Clifford Operators

Much faster implementations for a number of common Clifford operators are available. They are stored as special named 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
 sZ
subtypes(QuantumClifford.AbstractTwoQubitOperator)
14-element Vector{Any}:
 sCNOT
 sCPHASE
 sInvZCrY
 sSWAP
 sXCX
 sXCY
 sXCZ
 sYCX
 sYCY
 sYCZ
 sZCX
 sZCY
 sZCZ
 sZCrY

Generally, 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"
- XXZ

The 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__
+ _X_
+ __X
𝒮𝓉𝒶𝒷━
- XXX
- ZZ_
- Z_Z

They have convenience methods to extract only the stabilizer and destabilizer pieces:

julia> stabilizerview(d)
- XXX
- ZZ_
- Z_Z

julia> destabilizerview(d)
+ Z__
+ _X_
+ __X

Importantly 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
+ XXZ
+ X__
𝒮𝓉𝒶𝒷━
+ _ZX
- _Z_
- Z_X

Mixed 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.