Qubit Loss on MIMIQ¶
MIMIQ supports explicit qubit loss for simulations where a physical qubit may leave the computational space instead of only accumulating ordinary gate or measurement noise. This is useful when testing quantum algorithms against more realistic near-term hardware behavior, especially in workflows where missing qubits should change the later circuit execution.
In the Python API, loss can be represented directly in a circuit. You can insert
stochastic loss events, mark a qubit as lost deterministically, reload it later,
write its loss status into classical bits, and rewrite partially affected
instructions with a LossModel.
Loss can also be part of a custom Kraus channel. A
Kraus channel becomes
loss-aware when one or more branches are tagged with
LossyOperator, which
lets the channel separate survival branches from branches that lose a qubit.
Summary of Loss Functionality¶
Loss operations: LossErr,
QubitLoss,
QubitReload,
CheckLoss,
MeasureCheckLoss.
Loss processing: sample_losses() with
LossModel. The same sampling functionality is
also available as sample_losses().
Loss-model rules: DropRule,
ReplaceRule,
DecorateRule,
CustomRule.
Loss-aware Kraus:
LossyOperator
branches inside Kraus,
inspected with hasloss, lossoperators, survivaloperators, and
losseffect.
Loss Operations¶
Loss in MIMIQ is represented explicitly in the circuit. You can add operations that mark a qubit as lost, sample stochastic loss events, reload a lost qubit, or query whether a qubit is still present.
Loss Error¶
LossErr represents a
probabilistic loss event. At that point in the circuit, the qubit is lost with
probability p.
>>> circuit = Circuit()
>>> circuit.push(LossErr(0.1), 0)
1-qubit circuit with 1 instruction:
└── LossErr(0.1) @ q[0]
The probability may also be symbolic, but it must be numeric before calling
sample_losses().
Deterministic Loss¶
QubitLoss marks a qubit as lost
unconditionally.
>>> circuit = Circuit()
>>> circuit.push(QubitLoss(), 1)
2-qubit circuit with 1 instruction:
└── QubitLoss @ q[1]
Once a qubit is lost, later instructions touching that qubit are removed by
sample_losses() until the qubit is reloaded.
Reloading a Lost Qubit¶
QubitReload makes a previously
lost qubit available again. After reloading, the qubit is reset to |0> and
can be used by later operations.
>>> circuit = Circuit()
>>> circuit.push(QubitLoss(), 0)
1-qubit circuit with 1 instruction:
└── QubitLoss @ q[0]
>>> circuit.push(QubitReload(), 0)
1-qubit circuit with 2 instructions:
├── QubitLoss @ q[0]
└── QubitReload @ q[0]
>>> circuit.push(GateX(), 0)
1-qubit circuit with 3 instructions:
├── QubitLoss @ q[0]
├── QubitReload @ q[0]
└── X @ q[0]
Checking for Loss¶
MIMIQ provides two operations to query the loss status of a qubit.
CheckLoss writes one classical
bit:
1if the qubit is present0if the qubit is lost
It does not measure the quantum state.
>>> circuit = Circuit()
>>> circuit.push(CheckLoss(), 0, 0)
1-qubit, 1-bit circuit with 1 instruction:
└── CL @ q[0], c[0]
MeasureCheckLoss both measures
the qubit and reports whether it is still present.
>>> circuit = Circuit()
>>> circuit.push(MeasureCheckLoss(), 0, 0, 1)
1-qubit, 2-bit circuit with 1 instruction:
└── MCL @ q[0], c[0:1]
The first classical bit stores the measurement result, and the second classical bit stores the loss status.
Sampling Loss Events¶
To process loss operations in a circuit, use
sample_losses() or the convenience wrapper
sample_losses(). These functions walk the
circuit, sample LossErr events,
keep track of which qubits are currently lost, and return a rewritten circuit.
The rng argument is a random number generator. It is only used to make the
random loss samples reproducible. You can omit it if you do not need the same
random result every time.
>>> rng = random.Random(42)
>>> circuit = Circuit()
>>> circuit.push(LossErr(0.2), 0)
1-qubit circuit with 1 instruction:
└── LossErr(0.2) @ q[0]
>>> circuit.push(GateH(), 0)
1-qubit circuit with 2 instructions:
├── LossErr(0.2) @ q[0]
└── H @ q[0]
>>> circuit.push(CheckLoss(), 0, 0)
1-qubit, 1-bit circuit with 3 instructions:
├── LossErr(0.2) @ q[0]
├── H @ q[0]
└── CL @ q[0], c[0]
>>> circuit.sample_losses(rng=rng)
1-qubit, 1-bit circuit with 2 instructions:
├── H @ q[0]
└── CL @ q[0], c[0]
The basic behavior is:
QubitLossmarks a qubit as lostQubitReloadmakes the qubit available againCheckLossandMeasureCheckLossare always keptInstructions acting only on lost qubits are dropped
If an instruction touches some lost qubits but not all of them,
sample_losses() consults a
LossModel.
Loss Models¶
Why Loss Models Exist¶
A LossModel is the user-defined policy used by
sample_losses() when an instruction is only
partially affected by loss. This happens, for example, when a two-qubit gate is
scheduled but one of its qubits has already been lost while the other one is
still present.
MIMIQ can detect this situation, but it should not guess the physics for the
remaining qubits. Different hardware models and approximations can lead to
different choices: drop the instruction entirely, apply a one-qubit error
channel to each surviving qubit, keep a side-effect before or after the
attempted operation, or generate custom replacement instructions. A
LossModel is where you specify that choice explicitly.
If no rule is provided, MIMIQ uses the conservative behavior and drops instructions that touch lost qubits. Add rules when your hardware model or simulation workflow has a more specific response to partial loss.
When Rules Are Used¶
During sample_losses(), MIMIQ tracks which
qubits are currently lost and rewrites the circuit as follows:
If an instruction touches no lost qubits, it is kept unchanged.
If an instruction touches only lost qubits, it is dropped.
If an instruction touches both lost and surviving qubits, the
LossModelis consulted.If no rule in the model matches, the instruction is dropped.
Rules are evaluated by priority and then by insertion order. A
DropRule has higher priority than replacement or
decoration rules, so it can be used to exclude specific operations before a
broader salvage rule is applied. Once a rule matches, MIMIQ builds the rule’s
output and filters out any generated instruction that still touches a lost
qubit.
This last filtering step is important. A one-qubit replacement such as
Depolarizing1(0.2) is broadcast to the targets of the matched gate, and the
copies on lost qubits are removed. A multi-qubit replacement that still touches
a lost qubit is removed entirely.
You can create an empty model and add rules incrementally:
>>> model = LossModel(name="My Loss Model")
>>> model
LossModel (My Loss Model, 0 rules)
The main helpers are:
Replacing a Partially Lost Gate¶
Use ReplaceRule when the original instruction
should be removed and replaced by another operation on the surviving qubits. In
this example, a CX whose target qubit has been lost is replaced by a
one-qubit depolarizing channel on the remaining control qubit.
>>> circuit = Circuit()
>>> circuit.push(QubitLoss(), 1)
2-qubit circuit with 1 instruction:
└── QubitLoss @ q[1]
>>> circuit.push(GateCX(), 0, 1)
2-qubit circuit with 2 instructions:
├── QubitLoss @ q[1]
└── CX @ q[0], q[1]
>>> model = LossModel().add_replace(GateCX(), Depolarizing1(0.2))
>>> circuit.sample_losses(lossmodel=model)
2-qubit circuit with 2 instructions:
├── QubitLoss @ q[1]
└── Depolarizing(0.2) @ q[0]
If the lost qubit is the control instead, the same rule keeps the replacement on the surviving target qubit.
>>> circuit = Circuit()
>>> circuit.push(QubitLoss(), 0)
1-qubit circuit with 1 instruction:
└── QubitLoss @ q[0]
>>> circuit.push(GateCX(), 0, 1)
2-qubit circuit with 2 instructions:
├── QubitLoss @ q[0]
└── CX @ q[0], q[1]
>>> circuit.sample_losses(lossmodel=model)
2-qubit circuit with 2 instructions:
├── QubitLoss @ q[0]
└── Depolarizing(0.2) @ q[1]
Drop Rules¶
DropRule removes matching instructions when
they touch lost qubits. Use this when a partially affected operation should not
be salvaged. A DropRule without an operation is a catch-all rule.
>>> model = LossModel().add_rule(DropRule(GateSWAP()))
>>> model
LossModel (unnamed, 1 rules)
└── DropRule(SWAP)
You can also use the convenience form:
>>> model = LossModel().add_drop(GateSWAP())
>>> model
LossModel (unnamed, 1 rules)
└── DropRule(SWAP)
Because drop rules have higher priority, they can override broader replacement rules:
>>> model = LossModel([
... ReplaceRule(GateSWAP(), GateX()),
... DropRule(GateSWAP()),
... ])
>>> model
LossModel (unnamed, 2 rules)
├── DropRule(SWAP)
└── ReplaceRule(SWAP => X)
Decorating a Partially Lost Gate¶
DecorateRule adds another operation before or
after the matched instruction. In a loss model, generated instructions touching
lost qubits are filtered out, so if the original gate still touches a lost
qubit it is removed and only surviving decorations remain.
>>> model = LossModel().add_decorate(GateCZ(), Depolarizing1(0.01), before=True)
>>> model
LossModel (unnamed, 1 rules)
└── DecorateRule(CZ, Depolarizing(0.01), before)
Use decoration when your model says that the attempted operation still causes a side effect, such as a local error channel on the qubits that were present.
Custom Rules¶
Use CustomRule when the rewrite depends on
more than the operation type. The generator receives the matched instruction and
the current loss map. It may return None to drop the instruction, one
Instruction, or a sequence of instructions.
If your custom rule needs randomness, define the generator with an rng
argument or keyword. Otherwise, a two-argument generator (inst, lost) is
enough.
>>> circuit = Circuit()
>>> circuit.push(QubitLoss(), 1)
2-qubit circuit with 1 instruction:
└── QubitLoss @ q[1]
>>> circuit.push(GateCX(), 0, 1)
2-qubit circuit with 2 instructions:
├── QubitLoss @ q[1]
└── CX @ q[0], q[1]
>>> model = LossModel([
... CustomRule(
... lambda inst: isinstance(inst.get_operation(), GateCX),
... lambda inst, lost: [
... Instruction(GateZ(), (q,))
... for q in inst.get_qubits()
... if not lost.get(q, False)
... ],
... )
... ])
>>> model
LossModel (unnamed, 1 rules)
└── CustomRule(<callable>)
>>> circuit.sample_losses(lossmodel=model)
2-qubit circuit with 2 instructions:
├── QubitLoss @ q[1]
└── Z @ q[0]
For most workflows, prefer DropRule, ReplaceRule, or DecorateRule
because those rules are simpler to inspect and serialize. CustomRule is the
escape hatch for policies that cannot be expressed with the built-in rule
types.
Loss-Aware Kraus Channels¶
Custom Kraus channels can
also model loss. A Kraus channel becomes loss-aware when one or more of its
branches are tagged with
LossyOperator.
Use this when the loss event is part of the physical noise channel itself,
rather than a separate
LossErr instruction. The
LossyOperator matrix contains amplitudes, not probabilities, and the
remaining Kraus branches describe the no-loss evolution.
>>> k = Kraus([
... Matrix([[1, 0], [0, sqrt(0.9)]]),
... LossyOperator(Matrix([[0, sqrt(0.1)], [0, 0]])),
... ])
>>> k
Kraus(Operator([[1, 0], [0, 0.948683298050514]]), LossyOperator([[0, 0.316227766016838], [0, 0]]; lossy=(1,)))
The helper methods separate the loss branches from the survival branches and compute the total loss effect carried by the channel.
>>> k.hasloss()
True
>>> k.lossoperators()
[1-qubit LossyOperator (lossy=(1,)):
├── 0 0.316227766016838
└── 0 0]
>>> k.survivaloperators()
[1-qubit Operator:
├── 1 0
└── 0 0.948683298050514]
>>> k.losseffect()
1-qubit Operator:
├── 0.0 + 0.0*I 0.0 + 0.0*I
└── 0.0 + 0.0*I 0.1 + 0.0*I
If you only need the general Kraus formalism, see Kraus operators.