Qubit Loss
Loss operations
MimiqCircuitsBase.Check — Type
Check()Record whether a qubit is present into a classical bit.
- bit = 1 → the qubit is present
- bit = 0 → the qubit is lost
Like Measure records a qubit's value, Check records its presence. It does not touch the quantum state.
See also MeasureCheck, Loss, Reload.
Examples
julia> push!(Circuit(), Check(), 1, 1)
1-qubit, 1-bit circuit with 1 instruction:
└── Check @ q[1], c[1]MimiqCircuitsBase.Loss — Type
Loss(p)
Loss()A qubit-loss event: the qubit is lost with probability p.
Loss() is Loss(1.0), a certain loss. Loss is a kind of noise, so it sits alongside the other noise channels.
The probability is resolved by sample_losses, which turns each Loss(p) into a Loss(1.0) (the qubit is lost here) or removes it. A circuit is turned into a runnable, loss-free form by lower_losses.
See also Reload, Check, MeasureCheck.
Examples
julia> push!(Circuit(), Loss(0.1), 1)
1-qubit circuit with 1 instruction:
└── Loss(0.1) @ q[1]
julia> push!(Circuit(), Loss(), 1)
1-qubit circuit with 1 instruction:
└── Loss(1.0) @ q[1]MimiqCircuitsBase.MeasureCheck — Type
MeasureCheck()Measure a qubit if it is present, and record its presence.
- first bit = measurement result (0 or 1); forced to 0 if the qubit is lost
- second bit = presence (1 present, 0 lost)
The state is collapsed only when the qubit is present.
See also Check, Measure, Loss.
Examples
julia> push!(Circuit(), MeasureCheck(), 1, 1, 2)
1-qubit, 2-bit circuit with 1 instruction:
└── MeasureCheck @ q[1], c[1:2]MimiqCircuitsBase.Reload — Type
Reload()Reload a qubit: re-initialise it to $|0\rangle$ and mark it present.
Reload always resets the qubit, whether or not it was lost. lower_losses turns it into a Reset plus a Reloaded marker.
Examples
julia> push!(Circuit(), Reload(), 1)
1-qubit circuit with 1 instruction:
└── Reload @ q[1]Resolving loss
MimiqCircuitsBase.lossmodel_rewrite — Method
lossmodel_rewrite(inst::Instruction, lost::Dict{Int,Bool}, model::LossModel; rng=Random.default_rng())Decide how a single instruction that touches both lost and present qubits is rewritten under a LossModel, returning the instructions to emit in its place (an empty vector when the instruction is dropped).
This is the per-instruction decision core shared by the offline lower_losses and the online runtime-loss driver: both feed it the same lost map (qubit → lost?) and apply whatever it returns. Rules are tried in order, the first matching rule wins, and any emitted instruction still touching a lost qubit is filtered out. When no rule matches, the instruction is dropped.
rng is passed to the generator of a CustomRule; the other rules ignore it.
MimiqCircuitsBase.lower_losses — Method
lower_losses(c::Circuit; rng=Random.default_rng(), lossmodel=LossModel())Lower a circuit with loss operations into an equivalent circuit that uses only primitives (Reset, Measure, SetBit0, SetBit1) and therefore runs on any backend.
This is the deterministic half of loss resolution. It expects the random Loss(p) events to be resolved already (see sample_losses) and treats any remaining Loss, whatever its probability, as a certain loss. For the full pipeline that samples then lowers, use resolve_losses.
It tracks which qubits are lost and rewrites every loss operation:
Loss: marks the qubit lost and emits a passiveLostmarker.Reload: emits aResetand aReloadedmarker, and marks the qubit present again.Check: emitsSetBit1if the qubit is present,SetBit0if it is lost.MeasureCheck: present, aMeasureplusSetBit1; lost,SetBit0on both bits.Measureon a lost qubit: emitsSetBit0, so a lost qubit reads 0.- Gates with no lost qubits pass through; gates with all qubits lost are dropped; gates with some qubits lost are handled by the
LossModelrules, with instructions touching a lost qubit filtered out.
The returned circuit contains no loss operations.
Examples
julia> c = Circuit();
julia> push!(c, Loss(), 1);
julia> push!(c, GateX(), 1);
julia> push!(c, Check(), 1, 1);
julia> lower_losses(c)
1-qubit, 1-bit circuit with 2 instructions:
├── Lost @ q[1]
└── c[1] = 0MimiqCircuitsBase.resolve_losses — Method
resolve_losses(c::Circuit; rng=Random.default_rng(), lossmodel=LossModel())Fully resolve loss in a circuit into a runnable, loss-free circuit.
This is the entry point to reach for in the common case. It runs the two steps of loss resolution back to back: sample_losses draws the random Loss(p) events, then lower_losses rewrites the result into primitives. The returned circuit contains no loss operations and runs on any backend.
Choosing a function
Loss resolution is split into two stages so each can be used on its own:
sample_lossesis the random stage. It draws everyLoss(p), keeping it as a certainLoss(1.0)or dropping it, and leaves everything else (includingReload,Check,MeasureCheck) untouched. The output is still a loss circuit, just with the randomness fixed. Call it on its own to inspect or post-process one sampled loss pattern before lowering, or to draw many patterns from the same circuit.lower_lossesis the deterministic stage. It rewrites the loss bookkeeping intoReset/Measure/SetBit0/SetBit1and applies theLossModelrules to gates on lost qubits. It treats any remainingLossas certain, so it is meaningful only after the probabilities are resolved. Call it on its own when the losses are already deterministic (for example a hand-written circuit usingLoss(), or the output ofsample_losses).
resolve_losses is lower_losses ∘ sample_losses; prefer it unless you need one stage in isolation. For a targeted "what if I lose exactly these sites" study, see sample_loss_scenario.
Examples
julia> c = Circuit();
julia> push!(c, Loss(), 1);
julia> push!(c, GateX(), 1);
julia> push!(c, Check(), 1, 1);
julia> resolve_losses(c)
1-qubit, 1-bit circuit with 2 instructions:
├── Lost @ q[1]
└── c[1] = 0MimiqCircuitsBase.sample_loss_scenario — Method
sample_loss_scenario(c::Circuit, loss_indices; p=1.0, rng=Random.default_rng(), lossmodel=LossModel())Build a deterministic "what if" loss scenario from a circuit containing Loss instructions.
The selected Loss instructions, in circuit order, are forced to Loss(p) while every other Loss is forced to Loss(0.0). The result is then resolved with resolve_losses, so the returned circuit shows the effect of losing exactly those sites.
Examples
julia> c = Circuit();
julia> push!(c, Loss(0.2), 1);
julia> push!(c, GateCX(), 1, 2);
julia> push!(c, Loss(0.4), 2);
julia> sample_loss_scenario(c, [2])
2-qubit circuit with 2 instructions:
├── CX @ q[1], q[2]
└── Lost @ q[2]MimiqCircuitsBase.sample_losses — Method
sample_losses(c::Circuit; rng=Random.default_rng())Resolve the random qubit-loss events in a circuit.
Each Loss(p) is drawn: with probability p it becomes a certain loss Loss(1.0), otherwise it is removed. Every other operation, including the deterministic loss bookkeeping (Reload, Check, MeasureCheck), is kept unchanged. The result is therefore deterministic but still expressed with loss operations; use lower_losses to turn it into a runnable, loss-free circuit.
Examples
julia> c = push!(Circuit(), Loss(), 2);
julia> sample_losses(c)
2-qubit circuit with 1 instruction:
└── Loss(1.0) @ q[2]Loss models
MimiqCircuitsBase.AbstractCircuitRule — Type
AbstractCircuitRuleAbstract base type for all circuit transformation rules.
Both noise rules (AbstractNoiseRule) and loss rules (DropRule, DecorateRule, ReplaceRule, CustomRule) inherit from this type.
MimiqCircuitsBase.CustomRule — Type
CustomRule(matcher, generator)A circuit rule with user-defined matching and generation logic.
Arguments
matcher: Function(inst::Instruction) -> Boolthat determines whether the rule applies.generator: Function(inst::Instruction, lost::Dict{Int,Bool}; rng) -> resultwhere result isnothing(drop), anInstruction, or aVector{Instruction}.
Notes
- Not serializable (same limitation as
CustomNoiseRule). - The
generatorreceives thelostqubit map andrngas keyword arguments when used insample_losses.
Examples
rule = CustomRule(
inst -> getoperation(inst) isa GateSWAP,
(inst, lost; rng=nothing) -> nothing # drop all SWAPs
)MimiqCircuitsBase.DecorateRule — Type
DecorateRule(operation, decoration; before=false)A circuit rule that keeps the original instruction and adds a decoration (noise channel, gate, or vector of instructions) before or after it.
The decoration can be:
- An
Operationwith the same qubit count as the matched gate → applied on all qubits. - A 1-qubit
Operation→ broadcast to each qubit individually. - A
Vector{Instruction}on canonical qubits(1, 2, ...)→ remapped to actual qubits.
When used in a LossModel, instructions targeting only lost qubits are automatically filtered out.
Arguments
operation: Operation pattern to match.decoration: What to add (Operation or Vector{Instruction}).before: Iftrue, insert decoration before the original (default:false).
Examples
julia> DecorateRule(GateCX(), Depolarizing1(0.01))
DecorateRule(GateCX(), Depolarizing(1, 0.01), after)
julia> DecorateRule(GateCX(), Depolarizing1(0.01); before=true)
DecorateRule(GateCX(), Depolarizing(1, 0.01), before)MimiqCircuitsBase.DropRule — Type
DropRule([operation])A circuit rule that drops (removes) matched instructions.
When used in a LossModel, drops gates touching lost qubits.
Arguments
operation: Operation pattern to match (e.g.,GateCX(),GateRXX(θ)). If omitted, matches any operation (catch-all).
Examples
julia> DropRule(GateCX())
DropRule(GateCX())
julia> DropRule()
DropRule(*)MimiqCircuitsBase.LossModel — Type
LossModel([rules]; name="")A model that specifies how to handle gates when some (but not all) of their target qubits are lost during sample_losses.
Rules are evaluated by priority (lower value first), then by insertion order within the same priority. DropRule is evaluated before other loss rules so it can exclude special cases from broader salvage rules. If multiple rules of the same priority match, the first one wins.
If no rule matches, the gate is dropped by default.
When all qubits of a gate are lost, the gate is always dropped regardless of rules. When no qubits are lost, the gate passes through unchanged.
Arguments
rules: A vector ofAbstractCircuitRule(default: empty = drop everything).name: Optional name for the model.
Examples
julia> LossModel()
LossModel (unnamed, 0 rules)
julia> LossModel([ReplaceRule(GateCX() => Depolarizing1(0.2)), DropRule(GateSWAP())])
LossModel (unnamed, 2 rules)
├── DropRule(GateSWAP())
└── ReplaceRule(GateCX() => Depolarizing(1, 0.2))MimiqCircuitsBase.ReplaceRule — Type
ReplaceRule(operation, replacement)A circuit rule that replaces the original instruction with replacement instructions.
The replacement can be:
- An
Operationwith the same qubit count as the matched gate → single instruction on all qubits. - A 1-qubit
Operation→ broadcast to each qubit individually. - A
Vector{Instruction}on canonical qubits(1, 2, ...)→ remapped to actual qubits.
When used in a LossModel, instructions targeting only lost qubits are automatically filtered out. The broadcast form naturally handles per-qubit loss filtering.
Arguments
operation: Operation pattern to match.replacement: What to substitute (Operation or Vector{Instruction}).
Examples
julia> ReplaceRule(GateCX(), Depolarizing1(0.2))
ReplaceRule(GateCX() => Depolarizing(1, 0.2))
julia> ReplaceRule(GateCX() => Depolarizing1(0.2))
ReplaceRule(GateCX() => Depolarizing(1, 0.2))MimiqCircuitsBase.add_decorate! — Method
add_decorate!(model::LossModel, operation, decoration; before=false)
add_decorate!(model::LossModel, operation => decoration; before=false)Add a DecorateRule to the loss model. When a gate matching operation touches lost qubits, the original gate is kept and decoration is added on the surviving qubits (after by default, or before if before=true).
Examples
julia> model = LossModel();
julia> add_decorate!(model, GateCX(), Depolarizing1(0.01))
LossModel (unnamed, 1 rules)
└── DecorateRule(GateCX(), Depolarizing(1, 0.01), after)
julia> add_decorate!(model, GateCZ() => Depolarizing1(0.01); before=true)
LossModel (unnamed, 2 rules)
├── DecorateRule(GateCX(), Depolarizing(1, 0.01), after)
└── DecorateRule(GateCZ(), Depolarizing(1, 0.01), before)MimiqCircuitsBase.add_drop! — Method
add_drop!(model::LossModel[, operation])Add a DropRule to the loss model.
DropRule has higher priority than other loss rules, so it can be used to exclude special cases before broader replacement or decoration rules are applied.
Arguments
operation: Operation pattern to match (e.g.,GateCX()). If omitted, matches any operation (catch-all).
Examples
julia> model = LossModel();
julia> add_replace!(model, GateCX() => Depolarizing1(0.2))
LossModel (unnamed, 1 rules)
└── ReplaceRule(GateCX() => Depolarizing(1, 0.2))
julia> add_drop!(model, GateSWAP())
LossModel (unnamed, 2 rules)
├── DropRule(GateSWAP())
└── ReplaceRule(GateCX() => Depolarizing(1, 0.2))MimiqCircuitsBase.add_replace! — Method
add_replace!(model::LossModel, operation, replacement)
add_replace!(model::LossModel, operation => replacement)Add a ReplaceRule to the loss model. When a gate matching operation touches lost qubits, it is replaced with replacement on the surviving qubits.
Supports symbolic parameters: add_replace!(model, GateRXX(θ) => Depolarizing1(θ/π)).
Examples
julia> model = LossModel();
julia> add_replace!(model, GateCX() => Depolarizing1(0.2))
LossModel (unnamed, 1 rules)
└── ReplaceRule(GateCX() => Depolarizing(1, 0.2))
julia> add_replace!(model, GateCZ(), Depolarizing1(0.1))
LossModel (unnamed, 2 rules)
├── ReplaceRule(GateCX() => Depolarizing(1, 0.2))
└── ReplaceRule(GateCZ() => Depolarizing(1, 0.1))MimiqCircuitsBase.add_rule! — Method
add_rule!(model::LossModel, rule::AbstractCircuitRule)Add a rule to the loss model. Rules are kept ordered by priority (lower value means higher priority). Within the same priority, insertion order is preserved.
Returns the model for chaining.
MimiqCircuitsBase.apply_rule — Method
apply_rule(rule::AbstractCircuitRule, inst::Instruction) -> Union{Vector{Instruction}, Nothing}Apply the rule to an instruction and return the complete output sequence.
Returns nothing if the rule does not match. Returns a Vector{Instruction} with the full output (including the original instruction for decoration rules):
DropRule:Instruction[](empty — gate is dropped).ReplaceRule:[replacement_instructions...](original removed).DecorateRule:[inst, decoration...]or[decoration..., inst]depending onbefore.CustomRule: delegates to the user-defined generator.
The caller emits the vector as-is. before and replaces remain available for introspection but are not checked by application loops.
MimiqCircuitsBase.before — Method
before(rule::AbstractCircuitRule) -> BoolReturn whether the rule's effect should be applied before the original instruction. Default is false (apply after).
MimiqCircuitsBase.describe — Method
describe(model::LossModel)Print a human-readable description of the loss model and its rules.
MimiqCircuitsBase.matches — Method
matches(rule::AbstractCircuitRule, inst::Instruction) -> BoolCheck if a rule matches a given instruction. Must be implemented by concrete subtypes.
MimiqCircuitsBase.priority — Method
priority(rule::AbstractCircuitRule) -> IntReturn the priority of a circuit rule. Lower numbers have higher priority. Default is 100.
MimiqCircuitsBase.replaces — Method
replaces(rule::AbstractCircuitRule) -> BoolReturn whether the rule replaces the original instruction. Default is false (rule adds alongside the original).
Loss-aware Kraus channels
MimiqCircuitsBase.LossyOperator — Type
LossyOperator(matrix, lossy)
LossyOperator(matrix) # 1-qubit shorthand, lossy = (1,)Tagged custom $N$-qubit operator representing a loss branch in a Kraus channel, annotated with the qubits that leak.
The operator is a $2^N \times 2^N$ matrix in the computational basis (same ordering as Operator) plus a tuple lossy of qubit indices in 1:N listing which of the operator's qubits leak when this branch is selected.
A Kraus channel that contains one or more LossyOperator branches is a loss-aware channel; the helpers hasloss, lossoperators, survivaloperators, and losseffect inspect those branches. Qubit identity is positional: when the surrounding Kraus channel is pushed onto a circuit with target qubits $q_1,\dots,q_N$, the physical qubit $q_i$ is marked as lost for every $i \in$ lossy. The order of operators inside the channel carries no semantic meaning.
Arguments
matrix: A $2^N \times 2^N$ complex matrix.lossy: One or more qubit indices in1:N. May be passed as a tuple ((1, 2)), a vector ([2]), or a single integer (2). Order is canonical (sorted ascending); duplicates are not allowed; the set must be non-empty.
Examples
julia> LossyOperator([0 1; 0 0])
LossyOperator(0.0, 0.0, 1.0, 0.0; lossy=(1,))
julia> push!(Circuit(), Kraus([[1 0; 0 sqrt(0.9)], LossyOperator([0 sqrt(0.1); 0 0])]), 1)
1-qubit circuit with 1 instruction:
└── Kraus(Operator([1.0 0.0; 0.0 0.948683…]), LossyOperator(0.0,0.0,0.316228…,0.0;lossy=(1,))) @ q[1]MimiqCircuitsBase.lossyqubits — Method
lossyqubits(op)Return the tuple of qubit indices (within the operator's own 1:N numbering) that this LossyOperator marks as lost.
MimiqCircuitsBase.lossytargets — Method
lossytargets(op, targets)
lossytargets(op, targets...)Translate the operator-relative lossy indices into the corresponding physical qubit targets. targets is the tuple of physical qubit targets the surrounding instruction acts on (length must match numqubits(op)).
Returns an NTuple{K, eltype(targets)} where K = length(lossyqubits(op)) — the physical qubits that should be marked as lost when this branch is selected during simulation.
Examples
julia> op = LossyOperator(zeros(4, 4), 2); # 2-qubit, qubit 2 is lossy
julia> lossytargets(op, (3, 7))
(7,)
julia> both = LossyOperator(zeros(4, 4), (1, 2));
julia> lossytargets(both, (3, 7))
(3, 7)