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]
source
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]
source
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]
source
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.

See also Loss, Reset.

Examples

julia> push!(Circuit(), Reload(), 1)
1-qubit circuit with 1 instruction:
└── Reload @ q[1]
source

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.

source
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 passive Lost marker.
  • Reload: emits a Reset and a Reloaded marker, and marks the qubit present again.
  • Check: emits SetBit1 if the qubit is present, SetBit0 if it is lost.
  • MeasureCheck: present, a Measure plus SetBit1; lost, SetBit0 on both bits.
  • Measure on a lost qubit: emits SetBit0, 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 LossModel rules, 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] = 0
source
MimiqCircuitsBase.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:

  1. sample_losses is the random stage. It draws every Loss(p), keeping it as a certain Loss(1.0) or dropping it, and leaves everything else (including Reload, 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.
  2. lower_losses is the deterministic stage. It rewrites the loss bookkeeping into Reset/Measure/SetBit0/SetBit1 and applies the LossModel rules to gates on lost qubits. It treats any remaining Loss as 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 using Loss(), or the output of sample_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] = 0
source
MimiqCircuitsBase.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]
source
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]
source

Loss models

MimiqCircuitsBase.CustomRule — Type
CustomRule(matcher, generator)

A circuit rule with user-defined matching and generation logic.

Arguments

  • matcher: Function (inst::Instruction) -> Bool that determines whether the rule applies.
  • generator: Function (inst::Instruction, lost::Dict{Int,Bool}; rng) -> result where result is nothing (drop), an Instruction, or a Vector{Instruction}.

Notes

  • Not serializable (same limitation as CustomNoiseRule).
  • The generator receives the lost qubit map and rng as keyword arguments when used in sample_losses.

Examples

rule = CustomRule(
    inst -> getoperation(inst) isa GateSWAP,
    (inst, lost; rng=nothing) -> nothing  # drop all SWAPs
)
source
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 Operation with 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: If true, 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)
source
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(*)
source
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 of AbstractCircuitRule (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))
source
MimiqCircuitsBase.ReplaceRule — Type
ReplaceRule(operation, replacement)

A circuit rule that replaces the original instruction with replacement instructions.

The replacement can be:

  • An Operation with 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))
source
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)
source
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))
source
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))
source
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.

source
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 on before.
  • 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.

source
MimiqCircuitsBase.before — Method
before(rule::AbstractCircuitRule) -> Bool

Return whether the rule's effect should be applied before the original instruction. Default is false (apply after).

source
MimiqCircuitsBase.matches — Method
matches(rule::AbstractCircuitRule, inst::Instruction) -> Bool

Check if a rule matches a given instruction. Must be implemented by concrete subtypes.

source
MimiqCircuitsBase.priority — Method
priority(rule::AbstractCircuitRule) -> Int

Return the priority of a circuit rule. Lower numbers have higher priority. Default is 100.

source
MimiqCircuitsBase.replaces — Method
replaces(rule::AbstractCircuitRule) -> Bool

Return whether the rule replaces the original instruction. Default is false (rule adds alongside the original).

source

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.

See also Operator, Kraus.

Arguments

  • matrix: A $2^N \times 2^N$ complex matrix.
  • lossy: One or more qubit indices in 1: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]
source
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)
source