Annotations

Annotations attach information to a circuit without changing what it computes. They do not act on the quantum state or on the classical register, and simulators skip them. They exist so that a circuit can carry the metadata used in quantum error correction workflows, most notably the annotations of the Stim format, and so that a processed circuit can record where events happened.

Available annotations: Detector, ObservableInclude, QubitCoordinates, ShiftCoordinates, Tick, Lost, Reloaded. All of them are subtypes of AbstractAnnotation.

When a Stim file is read by MIMIQ (see Import & Export), its DETECTOR, OBSERVABLE_INCLUDE, QUBIT_COORDS, SHIFT_COORDS and TICK instructions become the annotations of the same name.

Detectors and observables

A Detector marks a set of classical bits whose parity is deterministic when the circuit runs without noise. A change of that parity signals an error. The first argument is the number of bits it looks at; the remaining arguments are optional coordinates, used to place the detector when visualizing it.

An ObservableInclude adds a set of measured bits to a logical observable. The first argument is again the number of bits, followed by the index of the observable.

using MimiqCircuits

c = Circuit()

# a 3-qubit repetition code: qubit 2 checks the parity of qubits 1 and 3
push!(c, GateCX(), 1, 2)
push!(c, GateCX(), 3, 2)
push!(c, Measure(), 2, 1)

# the outcome in c[1] is 0 without noise; this detector sits at coordinates (1, 0)
push!(c, Detector(1, 1, 0), 1)

# measure the data qubit 1 into c[2] and include it in observable 0
push!(c, Measure(), 1, 2)
push!(c, ObservableInclude(1, 0), 2)
3-qubit, 2-bit circuit with 6 instructions:
├── CX @ q[1], q[2]
├── CX @ q[3], q[2]
├── M @ q[2], c[1]
├── Detector(1.0,0.0) @ c[1]
├── M @ q[1], c[2]
└── ObservableInclude(0) @ c[2]

A detector or observable can look at any number of bits:

# a detector on the parity of c[1] and c[2], with coordinates (2.0, 0.5)
push!(Circuit(), Detector(2, 2.0, 0.5), 1, 2)
2-bit circuit with 1 instruction:
└── Detector(2.0,0.5) @ c[1:2]

Coordinates and time steps

QubitCoordinates assigns a position to a qubit. ShiftCoordinates adds an offset to the coordinates of the annotations that follow it, which is useful when the same block of operations is repeated in time. Tick separates the circuit into time steps. None of them has targets except QubitCoordinates, which targets the qubit it describes.

c = Circuit()

# place three qubits on a line
push!(c, QubitCoordinates(0, 0), 1)
push!(c, QubitCoordinates(1, 0), 2)
push!(c, QubitCoordinates(2, 0), 3)

# first time step
push!(c, GateCX(), 1, 2)
push!(c, Tick())

# second time step
push!(c, GateCX(), 3, 2)
push!(c, Tick())

# the next annotations are shifted by one unit along the second coordinate
push!(c, ShiftCoordinates(0, 1))
3-qubit circuit with 8 instructions:
├── QubitCoordinates(0.0,0.0) @ q[1]
├── QubitCoordinates(1.0,0.0) @ q[2]
├── QubitCoordinates(2.0,0.0) @ q[3]
├── CX @ q[1], q[2]
├── Tick()
├── CX @ q[3], q[2]
├── Tick()
└── ShiftCoordinates(0.0,1.0)

The values stored in an annotation are returned by getnotes:

getnotes(Detector(2, 2.0, 0.5))
2-element Vector{Float64}:
 2.0
 0.5

Loss markers

Lost and Reloaded record that a qubit was lost or reloaded at a point of the circuit. They are emitted by lower_losses and resolve_losses, so a resolved circuit still shows where the losses landed. They can also be added by hand.

c = Circuit()
push!(c, GateH(), 1)
push!(c, Lost(), 1)
push!(c, Reloaded(), 1)
1-qubit circuit with 3 instructions:
├── H @ q[1]
├── Lost @ q[1]
└── Reloaded @ q[1]

See Qubit Loss for how losses are resolved.

Annotations and decompositions

Decompositions keep annotations as they are: they are terminal operations of CanonicalBasis and StimBasis, so decompose leaves them in place next to the decomposed gates.

c = Circuit()
push!(c, GateH(), 1)
push!(c, Tick())
push!(c, Measure(), 1, 1)
push!(c, Detector(1), 1)

decompose(c)
1-qubit, 1-bit circuit with 4 instructions:
├── U(π/2,0,π) @ q[1]
├── Tick()
├── M @ q[1], c[1]
└── Detector() @ c[1]