Source code for mimiqcircuits.lossmodel

#
# Copyright © 2023-2025 QPerfect. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Loss model definitions and qubit-loss sampling.

This module provides the rule system used by :func:`lower_losses` (and hence
:func:`resolve_losses`) when a circuit instruction touches both lost and
surviving qubits. A :class:`LossModel` lets users decide whether such an
instruction should be dropped, replaced, decorated, or handled by custom logic.

Examples:
    >>> from mimiqcircuits import *
    >>> circuit = Circuit()
    >>> _ = circuit.push(Loss(), 1)
    >>> _ = circuit.push(GateCX(), 0, 1)
    >>> model = LossModel().add_replace(GateCX(), Depolarizing1(0.2))
    >>> resolve_losses(circuit, lossmodel=model)
    2-qubit circuit with 2 instructions:
    ├── Lost @ q[1]
    └── Depolarizing(0.2) @ q[0]
    <BLANKLINE>
"""

from __future__ import annotations

import inspect
import random
from typing import Iterable, List, Optional

import mimiqcircuits as mc
from mimiqcircuits.circuitrules import AbstractCircuitRule
from mimiqcircuits.symbolics import (
    UndefinedValue,
    _extract_variables,
    _validate_rule_gate_params,
    applyparams,
    unwrapvalue,
)


def _is_reset(operation: mc.Operation) -> bool:
    return isinstance(operation, (mc.Reset, mc.ResetX, mc.ResetY, mc.ResetZ))


def _supports_symbolic_operation_pattern(operation: mc.Operation) -> bool:
    return isinstance(operation, (mc.Gate, mc.AbstractMeasurement)) or _is_reset(
        operation
    )


def _is_symbolic_operation_pattern(operation: mc.Operation) -> bool:
    return _supports_symbolic_operation_pattern(operation) and operation.is_symbolic()


def _validate_rule_operation_target(operation: mc.Operation):
    if not isinstance(
        operation,
        (
            mc.Gate,
            mc.AbstractMeasurement,
            mc.Block,
            mc.Repeat,
            mc.IfStatement,
            mc.Reset,
            mc.ResetX,
            mc.ResetY,
            mc.ResetZ,
        ),
    ):
        raise ValueError(
            "Rule target operation must be a gate, measurement, reset, "
            "Block, Repeat, or IfStatement operation."
        )


def _matches_operation_pattern(op_inst: mc.Operation, op_rule: mc.Operation) -> bool:
    if type(op_inst) is not type(op_rule):
        return False

    if not _is_symbolic_operation_pattern(op_rule):
        return op_inst == op_rule

    return True


def _resolve_symbolic_replacement(
    op_inst: mc.Operation, op_rule: mc.Operation, replacement: mc.Operation
):
    if not _is_symbolic_operation_pattern(op_rule):
        return replacement

    variables = _extract_variables(op_rule)
    if variables is None or all(var is None for var in variables):
        return replacement

    return applyparams(op_inst, (variables, replacement))


def _normalize_to_instructions(result) -> List[mc.Instruction]:
    if result is None:
        return []

    if isinstance(result, mc.Instruction):
        return [result]

    if hasattr(result, "instructions"):
        insts = list(result.instructions)
    elif isinstance(result, (list, tuple)):
        insts = list(result)
    else:
        raise TypeError(
            "Expected None, Instruction, Circuit, Block, or a sequence of Instructions."
        )

    if not all(isinstance(inst, mc.Instruction) for inst in insts):
        raise TypeError("All generated items must be Instruction instances.")

    return insts


def _validate_replacement_operation(
    operation: mc.Operation, replacement: mc.Operation, label: str
):
    if replacement.num_bits != 0 or replacement.num_zvars != 0:
        raise ValueError(
            f"{label} operation must not target classical bits or z-variables."
        )

    nq_op = operation.num_qubits
    nq_repl = replacement.num_qubits
    if nq_repl != nq_op and nq_repl != 1:
        raise ValueError(
            f"{label} must have the same number of qubits as the operation ({nq_op}) "
            f"or exactly 1 qubit. Got {nq_repl}."
        )


def _validate_replacement_instructions(
    operation: mc.Operation, instructions: List[mc.Instruction], label: str
):
    nq = operation.num_qubits
    for inst in instructions:
        if inst.get_bits() or inst.get_zvars():
            raise ValueError(
                f"{label} instructions must not target classical bits or z-variables."
            )
        for q in inst.get_qubits():
            if q < 0 or q >= nq:
                raise ValueError(
                    f"{label} instructions must use canonical qubits in range(0, {nq})."
                )


def _build_replacement_instructions(replacement, op_pattern: mc.Operation, inst):
    if isinstance(replacement, mc.Operation):
        op_inst = inst.get_operation()
        qs = inst.get_qubits()
        resolved = _resolve_symbolic_replacement(op_inst, op_pattern, replacement)

        if resolved.num_qubits == len(qs):
            return [mc.Instruction(resolved, tuple(qs))]

        return [mc.Instruction(resolved, (q,)) for q in qs]

    qubit_map = {i: q for i, q in enumerate(inst.get_qubits())}
    return [
        mc.Instruction(
            repl_inst.get_operation(),
            tuple(qubit_map[q] for q in repl_inst.get_qubits()),
            tuple(repl_inst.get_bits()),
            tuple(repl_inst.get_zvars()),
        )
        for repl_inst in replacement
    ]


def _call_custom_generator(generator, inst, lost, rng):
    signature = inspect.signature(generator)
    params = list(signature.parameters.values())

    positional = [
        param
        for param in params
        if param.kind
        in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
    ]
    has_varargs = any(
        param.kind == inspect.Parameter.VAR_POSITIONAL for param in params
    )
    has_kwargs = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in params)

    if "rng" in signature.parameters or has_kwargs:
        return generator(inst, lost, rng=rng)
    if has_varargs or len(positional) >= 3:
        return generator(inst, lost, rng)
    if len(positional) >= 2:
        return generator(inst, lost)
    return generator(inst)


[docs] class DropRule(AbstractCircuitRule): """Drop matched instructions that touch lost qubits. A ``DropRule`` is useful when a partially affected operation should not be salvaged. If ``operation`` is omitted, the rule matches any operation that reaches the loss model. Args: operation (optional): Operation pattern to drop. If omitted, the rule is a catch-all rule. Examples: >>> from mimiqcircuits import * >>> model = LossModel().add_drop(GateCX()) >>> model LossModel (unnamed, 1 rules) └── DropRule(CX) >>> circuit = Circuit() >>> _ = circuit.push(Loss(), 1) >>> _ = circuit.push(GateCX(), 0, 1) >>> circuit.resolve_losses(lossmodel=model) 2-qubit circuit with 1 instruction: └── Lost @ q[1] <BLANKLINE> """
[docs] def __init__(self, operation: Optional[mc.Operation] = None): if operation is not None: _validate_rule_operation_target(operation) if _supports_symbolic_operation_pattern(operation): _validate_rule_gate_params(operation) self.operation = operation
[docs] def priority(self): return 0
[docs] def matches(self, inst: mc.Instruction): if self.operation is None: return True return _matches_operation_pattern(inst.get_operation(), self.operation)
[docs] def replaces(self): return True
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None return []
def __str__(self): if self.operation is None: return "DropRule(*)" return f"DropRule({self.operation})" __repr__ = __str__
[docs] class DecorateRule(AbstractCircuitRule): """Add a decoration before or after a matched instruction. During loss sampling, any generated instruction that still touches a lost qubit is filtered out. This makes ``DecorateRule`` useful for modeling a side effect on surviving qubits when an operation was attempted but one of its qubits was missing. Args: operation: Operation pattern to match. decoration: Operation or instruction sequence to add. before (bool): If ``True``, add the decoration before the matched instruction. Otherwise, add it after. Examples: >>> from mimiqcircuits import * >>> model = LossModel().add_decorate(GateCZ(), Depolarizing1(0.01), before=True) >>> model LossModel (unnamed, 1 rules) └── DecorateRule(CZ, Depolarizing(0.01), before) >>> circuit = Circuit() >>> _ = circuit.push(Loss(), 1) >>> _ = circuit.push(GateCZ(), 0, 1) >>> circuit.resolve_losses(lossmodel=model) 2-qubit circuit with 2 instructions: ├── Lost @ q[1] └── Depolarizing(0.01) @ q[0] <BLANKLINE> """
[docs] def __init__(self, operation, decoration=None, *, before=False): if decoration is None: if not isinstance(operation, tuple) or len(operation) != 2: raise TypeError( "DecorateRule expects (operation, decoration) or separate arguments." ) operation, decoration = operation _validate_rule_operation_target(operation) if _supports_symbolic_operation_pattern(operation): _validate_rule_gate_params(operation) self.operation = operation self._before = bool(before) if isinstance(decoration, mc.Operation): _validate_replacement_operation(operation, decoration, "Decoration") self.decoration = decoration else: decoration_insts = _normalize_to_instructions(decoration) _validate_replacement_instructions( operation, decoration_insts, "Decoration" ) self.decoration = decoration_insts
[docs] def before(self): return self._before
[docs] def matches(self, inst: mc.Instruction): return _matches_operation_pattern(inst.get_operation(), self.operation)
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None decoration_insts = _build_replacement_instructions( self.decoration, self.operation, inst ) if self.before(): return decoration_insts + [inst] return [inst] + decoration_insts
def __str__(self): position = "before" if self.before() else "after" return f"DecorateRule({self.operation}, {self.decoration}, {position})" __repr__ = __str__
[docs] class ReplaceRule(AbstractCircuitRule): """Replace a matched instruction with new instructions. Use ``ReplaceRule`` when a partially affected operation should be removed and replaced by another operation on surviving qubits. A one-qubit replacement is broadcast to each target of the matched instruction, and copies on lost qubits are filtered out. Args: operation: Operation pattern to match. replacement: Replacement operation or instruction sequence. Examples: >>> from mimiqcircuits import * >>> model = LossModel().add_replace(GateCX(), Depolarizing1(0.2)) >>> model LossModel (unnamed, 1 rules) └── ReplaceRule(CX => Depolarizing(0.2)) >>> circuit = Circuit() >>> _ = circuit.push(Loss(), 1) >>> _ = circuit.push(GateCX(), 0, 1) >>> circuit.resolve_losses(lossmodel=model) 2-qubit circuit with 2 instructions: ├── Lost @ q[1] └── Depolarizing(0.2) @ q[0] <BLANKLINE> """
[docs] def __init__(self, operation, replacement=None): if replacement is None: if not isinstance(operation, tuple) or len(operation) != 2: raise TypeError( "ReplaceRule expects (operation, replacement) or separate arguments." ) operation, replacement = operation _validate_rule_operation_target(operation) if _supports_symbolic_operation_pattern(operation): _validate_rule_gate_params(operation) self.operation = operation if isinstance(replacement, mc.Operation): _validate_replacement_operation(operation, replacement, "Replacement") self.replacement = replacement else: replacement_insts = _normalize_to_instructions(replacement) _validate_replacement_instructions( operation, replacement_insts, "Replacement" ) self.replacement = replacement_insts
[docs] def matches(self, inst: mc.Instruction): return _matches_operation_pattern(inst.get_operation(), self.operation)
[docs] def replaces(self): return True
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None return _build_replacement_instructions(self.replacement, self.operation, inst)
def __str__(self): return f"ReplaceRule({self.operation} => {self.replacement})" __repr__ = __str__
[docs] class CustomRule(AbstractCircuitRule): """User-defined loss rule. ``CustomRule`` is the escape hatch for loss policies that cannot be expressed with :class:`DropRule`, :class:`ReplaceRule`, or :class:`DecorateRule`. The matcher decides whether the rule applies. The generator returns ``None`` to drop the instruction, one :class:`Instruction`, or a sequence of instructions. The generator may accept ``(inst)``, ``(inst, lost)``, or ``(inst, lost, rng)``. It may also accept ``rng`` as a keyword argument. Args: matcher: Callable that receives an instruction and returns ``True`` if the rule should apply. generator: Callable that generates replacement instructions. Examples: Define a custom fallback for a partially lost ``CX``. If the control qubit survives, replace the failed ``CX`` by ``X`` on the control. If the control is lost, return ``None`` to drop the instruction. >>> from mimiqcircuits import * >>> def cx_control_fallback(inst, lost): ... control = inst.get_qubits()[0] ... if lost.get(control, False): ... return None ... return Instruction(GateX(), (control,)) ... >>> model = LossModel([ ... CustomRule( ... lambda inst: isinstance(inst.get_operation(), GateCX), ... cx_control_fallback, ... ) ... ]) >>> model LossModel (unnamed, 1 rules) └── CustomRule(<callable>) >>> circuit = Circuit() >>> _ = circuit.push(Loss(), 1) >>> _ = circuit.push(GateCX(), 0, 1) >>> circuit.resolve_losses(lossmodel=model) 2-qubit circuit with 2 instructions: ├── Lost @ q[1] └── X @ q[0] <BLANKLINE> Another custom rule can generate one instruction for each surviving qubit. Here a partially lost ``CX`` becomes ``Z`` on every qubit that is still present: >>> 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) ... ], ... ) ... ]) >>> circuit.resolve_losses(lossmodel=model) 2-qubit circuit with 2 instructions: ├── Lost @ q[1] └── Z @ q[0] <BLANKLINE> """
[docs] def __init__(self, matcher, generator): self.matcher = matcher self.generator = generator
[docs] def matches(self, inst: mc.Instruction): return self.matcher(inst)
[docs] def replaces(self): return True
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None return _normalize_to_instructions( _call_custom_generator(self.generator, inst, {}, None) )
def __str__(self): return "CustomRule(<callable>)" __repr__ = __str__
[docs] class LossModel: """Collection of prioritized loss rules. A ``LossModel`` tells :func:`sample_losses` what to do when an instruction touches both lost and surviving qubits. With no rules, the conservative behavior is used: instructions touching lost qubits are dropped. Args: rules (optional): Iterable of loss rules. name (str): Optional model name used in display output. Examples: >>> from mimiqcircuits import * >>> model = LossModel(name="My Loss Model") >>> model LossModel (My Loss Model, 0 rules) Rules can be added incrementally: >>> model.add_replace(GateCX(), Depolarizing1(0.2)) LossModel (My Loss Model, 1 rules) └── ReplaceRule(CX => Depolarizing(0.2)) The model can then be passed to ``sample_losses``: >>> circuit = Circuit() >>> _ = circuit.push(Loss(), 1) >>> _ = circuit.push(GateCX(), 0, 1) >>> circuit.resolve_losses(lossmodel=model) 2-qubit circuit with 2 instructions: ├── Lost @ q[1] └── Depolarizing(0.2) @ q[0] <BLANKLINE> """
[docs] def __init__(self, rules: Optional[Iterable[AbstractCircuitRule]] = None, name=""): self.rules = list(rules or []) self.name = name for rule in self.rules: if not isinstance(rule, AbstractCircuitRule): raise TypeError("LossModel rules must inherit from AbstractCircuitRule.") self._sort_rules()
def _sort_rules(self): self.rules.sort(key=lambda rule: rule.priority())
[docs] def add_rule(self, rule: AbstractCircuitRule): if not isinstance(rule, AbstractCircuitRule): raise TypeError("LossModel rules must inherit from AbstractCircuitRule.") self.rules.append(rule) self._sort_rules() return self
[docs] def add_drop(self, operation: Optional[mc.Operation] = None): return self.add_rule(DropRule(operation))
[docs] def add_replace(self, operation, replacement=None): return self.add_rule(ReplaceRule(operation, replacement))
[docs] def add_decorate(self, operation, decoration=None, *, before=False): return self.add_rule(DecorateRule(operation, decoration, before=before))
[docs] def lower_losses(self, circuit: mc.Circuit, rng=None): return lower_losses(circuit, rng=rng, lossmodel=self)
[docs] def resolve_losses(self, circuit: mc.Circuit, rng=None): return resolve_losses(circuit, rng=rng, lossmodel=self)
[docs] def describe(self): title = f"LossModel: {self.name}" if self.name else "LossModel" print(title) print("=" * 50) if not self.rules: print(" (no rules - all gates touching lost qubits will be dropped)") return for index, rule in enumerate(self.rules, start=1): print(f"Rule {index}: {type(rule).__name__}") if isinstance(rule, DropRule): if rule.operation is None: print(" -> Drop any gate touching lost qubits") else: print(f" -> Drop {rule.operation} when touching lost qubits") elif isinstance(rule, ReplaceRule): print( f" -> Replace {rule.operation} with {rule.replacement} on surviving qubits" ) elif isinstance(rule, DecorateRule): position = "before" if rule.before() else "after" print( f" -> Keep {rule.operation}, add {rule.decoration} {position} on surviving qubits" ) elif isinstance(rule, CustomRule): print(" -> Custom rule (callable)")
[docs] def saveproto(self, file): from mimiqcircuits.proto.protoio import saveproto return saveproto(self, file)
[docs] @staticmethod def loadproto(file): from mimiqcircuits.proto.protoio import loadproto return loadproto(file, LossModel)
def __str__(self): name = self.name if self.name else "unnamed" return f"LossModel ({name}, {len(self.rules)} rules)" def __repr__(self): if not self.rules: return str(self) lines = [str(self)] for index, rule in enumerate(self.rules): prefix = "└── " if index == len(self.rules) - 1 else "├── " lines.append(f"{prefix}{rule}") return "\n".join(lines)
def lossmodel_rewrite(inst: mc.Instruction, lost, model: LossModel, rng): """Decide how a single instruction touching both lost and present qubits is rewritten under a :class:`LossModel`, returning the instructions to emit in its place (an empty list when the instruction is dropped). This is the per-instruction decision core shared by the offline :func:`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. """ for rule in model.rules: if isinstance(rule, CustomRule): if not rule.matches(inst): continue result = _normalize_to_instructions( _call_custom_generator(rule.generator, inst, dict(lost), rng) ) else: result = rule.apply_rule(inst) if result is None: continue # discard instructions where any target qubit is lost; first match wins return [ replacement for replacement in result if not any(lost.get(q, False) for q in replacement.get_qubits()) ] # No rule matched -> drop (default) return [] def _apply_lossmodel_rules(out, inst: mc.Instruction, model: LossModel, lost, rng): for replacement in lossmodel_rewrite(inst, lost, model, rng): out.push(replacement) def _sample_loss_probability(op: "mc.Loss"): try: value = unwrapvalue(op.p) except UndefinedValue as exc: raise ValueError( "Loss probability must be numeric for sampling. " "Use evaluate() to substitute symbolic parameters first." ) from exc if isinstance(value, complex): if value.imag != 0: raise ValueError("Loss probability must be real for sampling.") value = value.real # Range is not re-checked here: the `Loss` constructor already rejects # out-of-range numeric `p`, and `_loss_fires` treats `p >= 1` as certain # loss, matching the Julia `_loss_fires`. return value def _loss_fires(op: "mc.Loss", rng) -> bool: """Whether a ``Loss(p)`` fires. A certain loss (p == 1) always fires.""" p = _sample_loss_probability(op) return p >= 1.0 or rng.random() < p def _coerce_rng_lossmodel(rng, lossmodel): if isinstance(rng, LossModel): if lossmodel is not None: raise TypeError( "LossModel provided twice: once positionally and once via lossmodel=." ) lossmodel = rng rng = None if rng is None: rng = random.Random() elif not hasattr(rng, "random"): raise TypeError("rng must provide a random() method.") if lossmodel is None: lossmodel = LossModel() return rng, lossmodel
[docs] def sample_losses(circuit: mc.Circuit, rng=None): """Resolve the random qubit-loss events in a circuit. Each :class:`mimiqcircuits.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 deterministic but still expressed with loss operations; use :func:`lower_losses` or :func:`resolve_losses` to turn it into a runnable, loss-free circuit. Args: circuit: Circuit to sample. rng (optional): Random number generator providing a ``random()`` method. Returns: mc.Circuit: A circuit whose random loss events have been resolved. """ if rng is None: rng = random.Random() elif not hasattr(rng, "random"): raise TypeError("rng must provide a random() method.") out = mc.Circuit() for inst in circuit: op = inst.get_operation() if isinstance(op, mc.Loss): if _loss_fires(op, rng): out.push(mc.Loss(1.0), inst.get_qubits()[0]) else: out.push(inst) return out
[docs] def lower_losses(circuit: mc.Circuit, rng=None, lossmodel: Optional[LossModel] = None): """Lower a circuit with loss operations into one using only primitives. This is the deterministic half of loss resolution. It expects the random ``Loss(p)`` events to be resolved already (see :func:`sample_losses`) and treats any remaining ``Loss`` as a certain loss. It rewrites every loss operation into ``Reset`` / ``Measure`` / ``SetBit0`` / ``SetBit1`` plus passive ``Lost`` / ``Reloaded`` markers, and drops or replaces gates on lost qubits according to ``lossmodel``. The returned circuit contains no loss operations. Args: circuit: Circuit to lower. rng (optional): Random number generator for stochastic loss-model rules. A positional :class:`LossModel` is also accepted. lossmodel (optional): A :class:`LossModel` for gates touching lost qubits. Returns: mc.Circuit: A loss-free circuit that runs on any backend. """ rng, lossmodel = _coerce_rng_lossmodel(rng, lossmodel) lost = {} out = mc.Circuit() for inst in circuit: op = inst.get_operation() qubits = tuple(inst.get_qubits()) if isinstance(op, mc.Loss): q = qubits[0] if lost.get(q, False): continue lost[q] = True out.push(mc.Lost(), q) continue if isinstance(op, mc.Reload): q = qubits[0] out.push(mc.Reset(), q) out.push(mc.Reloaded(), q) lost[q] = False continue if isinstance(op, mc.Check): q = qubits[0] b = inst.get_bits()[0] out.push(mc.SetBit0() if lost.get(q, False) else mc.SetBit1(), b) continue if isinstance(op, mc.MeasureCheck): q = qubits[0] bits = tuple(inst.get_bits()) if lost.get(q, False): out.push(mc.SetBit0(), bits[0]) out.push(mc.SetBit0(), bits[1]) else: out.push(mc.Measure(), q, bits[0]) out.push(mc.SetBit1(), bits[1]) continue # a single-qubit measurement on a lost qubit reads 0 if ( isinstance(op, mc.AbstractMeasurement) and len(qubits) == 1 and lost.get(qubits[0], False) ): out.push(mc.SetBit0(), inst.get_bits()[0]) continue if not any(lost.get(q, False) for q in qubits): out.push(inst) continue if all(lost.get(q, False) for q in qubits): continue _apply_lossmodel_rules(out, inst, lossmodel, lost, rng) return out
[docs] def resolve_losses(circuit: mc.Circuit, rng=None, lossmodel: Optional[LossModel] = None): """Fully resolve loss into a runnable, loss-free circuit. The user-facing entry point: it runs :func:`sample_losses` to draw the random ``Loss(p)`` events, then :func:`lower_losses` to rewrite the result into primitives. The returned circuit contains no loss operations. Args: circuit: Circuit to resolve. rng (optional): Random number generator. A positional :class:`LossModel` is also accepted. lossmodel (optional): A :class:`LossModel` for gates touching lost qubits. Returns: mc.Circuit: A loss-free circuit that runs on any backend. """ rng, lossmodel = _coerce_rng_lossmodel(rng, lossmodel) return lower_losses(sample_losses(circuit, rng=rng), rng=rng, lossmodel=lossmodel)
def _normalize_loss_indices(loss_indices) -> set: if isinstance(loss_indices, int): loss_indices = (loss_indices,) indices = set() for idx in loss_indices: idx = int(idx) if idx < 1: raise ValueError(f"Loss indices must be >= 1, got {idx}.") indices.add(idx) return indices
[docs] def sample_loss_scenario( circuit: mc.Circuit, loss_indices, *, p: float = 1.0, rng=None, lossmodel: Optional[LossModel] = None, ): """Build a deterministic "what-if" loss scenario from a circuit. The selected :class:`mimiqcircuits.Loss` instructions, counted in circuit order (1-based), are forced to ``Loss(p)`` while every other ``Loss`` is forced to ``Loss(0.0)``. The result is then resolved with :func:`resolve_losses`, so the returned circuit shows the effect of losing exactly those sites. Args: circuit: Circuit containing ``Loss`` instructions. loss_indices: A 1-based index or iterable of indices selecting which ``Loss`` instructions to force. p (optional): Probability assigned to the selected sites (default 1.0). rng (optional): Random number generator. lossmodel (optional): A :class:`LossModel` for gates touching lost qubits. Returns: mc.Circuit: A loss-free circuit for the chosen scenario. Examples: >>> from mimiqcircuits import * >>> circuit = Circuit() >>> _ = circuit.push(Loss(0.2), 0) >>> _ = circuit.push(GateCX(), 0, 1) >>> _ = circuit.push(Loss(0.4), 1) >>> sample_loss_scenario(circuit, 2) 2-qubit circuit with 2 instructions: ├── CX @ q[0], q[1] └── Lost @ q[1] <BLANKLINE> """ forced_indices = _normalize_loss_indices(loss_indices) if not (0.0 <= p <= 1.0): raise ValueError(f"Probability p must be between 0 and 1, got {p}.") scenario = mc.Circuit() current_index = 0 for inst in circuit: op = inst.get_operation() if isinstance(op, mc.Loss): current_index += 1 forced_p = p if current_index in forced_indices else 0.0 scenario.push(mc.Loss(forced_p), inst.get_qubits()[0]) else: scenario.push(inst) if current_index == 0: raise ValueError("Circuit does not contain any Loss instructions.") invalid = sorted(i for i in forced_indices if i > current_index) if invalid: raise ValueError( f"Loss index/indices {invalid} out of range for a circuit with " f"{current_index} Loss instruction(s)." ) return resolve_losses(scenario, rng=rng, lossmodel=lossmodel)
__all__ = [ "AbstractCircuitRule", "DropRule", "DecorateRule", "ReplaceRule", "CustomRule", "LossModel", "sample_losses", "lower_losses", "resolve_losses", "sample_loss_scenario", ]