Source code for mimiqcircuits.noisemodel

#
# 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.
#
"""Noise model definitions and application.

This module implements rule-based noise injection for ``mimiqcircuits`` circuits.
Rules are evaluated by priority (lower value means higher priority), and the first
matching rule is applied to each instruction.
"""

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable, Iterable, List, Sequence, Set, Union

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


# Priority order (lower value = higher priority)
PRIORITY_USER_OVERRIDE = 0
PRIORITY_EXACT_OPERATION = 40
PRIORITY_EXACT_READOUT = 50
PRIORITY_SET_OPERATION = 60
PRIORITY_SET_READOUT = 70
PRIORITY_GLOBAL_OPERATION = 80
PRIORITY_GLOBAL_READOUT = 90
PRIORITY_SET_IDLE = 190
PRIORITY_IDLE = 200


def _bind_before_method(instance, before_value: bool):
    """Convert dataclass bool field `before` to bound method `before()`."""
    object.__delattr__(instance, "before")
    object.__setattr__(
        instance,
        "before",
        (lambda self=instance, value=before_value: value).__get__(
            instance, type(instance)
        ),
    )


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


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.WhileStatement,
            mc.Reset,
            mc.ResetX,
            mc.ResetY,
            mc.ResetZ,
            mc.PrepareStateT,
        ),
    ):
        raise ValueError(
            "Rule target operation must be a gate, measurement, reset, "
            "Block, Repeat, IfStatement, or WhileStatement operation."
        )


def _resolve_noise_for_operation_match(
    op_pattern: mc.Operation,
    op_instance: mc.Operation,
    noise_pattern: Union[mc.krauschannel, mc.Gate],
):
    """Build noise op from a matched operation instance."""
    # Non-symbolic patterns (including wrappers) are static.
    if not _is_symbolic_operation_pattern(op_pattern):
        return noise_pattern

    variables = _extract_variables(op_pattern)
    if variables is None or all(var is None for var in variables):
        return noise_pattern

    return applyparams(op_instance, (variables, noise_pattern))


[docs] class AbstractNoiseRule(AbstractCircuitRule): """Abstract base class for all noise rules."""
[docs] @dataclass(frozen=True) class GlobalReadoutNoise(AbstractNoiseRule): """Add a readout error after every measurement. The readout error acts on the classical bits the measurement writes to, so it is placed right after the measurement and does not replace it. Args: noise: The ``ReadoutErr`` to apply. Example: >>> import mimiqcircuits as mc >>> mc.GlobalReadoutNoise(mc.ReadoutErr(0.01, 0.02)) GlobalReadoutNoise(noise=RErr(0.01,0.02)) """ noise: mc.ReadoutErr
[docs] def priority(self): return PRIORITY_GLOBAL_READOUT
[docs] def matches(self, inst: mc.Instruction): return isinstance(inst.get_operation(), mc.AbstractMeasurement)
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None return mc.Instruction(self.noise, tuple(), tuple(inst.get_bits()), tuple())
[docs] @dataclass(frozen=True) class ExactQubitReadoutNoise(AbstractNoiseRule): """Add a readout error after measurements on exactly the given qubits. A measurement matches only when its qubits are ``qubits``, in the same order. Args: qubits: Qubits the measurement must act on, in order. noise: The ``ReadoutErr`` to apply. Raises: ValueError: If ``qubits`` is empty or has repetitions. Example: >>> import mimiqcircuits as mc >>> mc.ExactQubitReadoutNoise([0, 1], mc.ReadoutErr(0.01, 0.02)) ExactQubitReadoutNoise(qubits=(0, 1), noise=RErr(0.01,0.02)) """ qubits: Sequence[int] noise: mc.ReadoutErr def __post_init__(self): qs = list(self.qubits) if not qs: raise ValueError("Qubit list must not be empty") if len(set(qs)) != len(qs): raise ValueError("Qubit list must not contain repetitions") object.__setattr__(self, "qubits", tuple(qs))
[docs] def priority(self): return PRIORITY_EXACT_READOUT
[docs] def matches(self, inst: mc.Instruction): return isinstance(inst.get_operation(), mc.AbstractMeasurement) and tuple( inst.get_qubits() ) == self.qubits
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None return mc.Instruction(self.noise, tuple(), tuple(inst.get_bits()), tuple())
[docs] @dataclass(frozen=True) class SetQubitReadoutNoise(AbstractNoiseRule): """Add a readout error after measurements whose qubits are all in a set. Args: qubits: Qubits where the readout error applies. noise: The ``ReadoutErr`` to apply. Raises: ValueError: If ``qubits`` is empty or has repetitions. Example: >>> import mimiqcircuits as mc >>> mc.SetQubitReadoutNoise([0, 2, 4], mc.ReadoutErr(0.01, 0.02)) SetQubitReadoutNoise(qubits=[0, 2, 4], noise=RErr(0.01,0.02)) """ qubits: Iterable[int] noise: mc.ReadoutErr _qubit_set: frozenset = field(init=False, repr=False) def __post_init__(self): qs = list(self.qubits) if not qs: raise ValueError("Qubit list must not be empty") if len(set(qs)) != len(qs): raise ValueError("Qubit list must not contain repetitions") object.__setattr__(self, "_qubit_set", frozenset(qs))
[docs] def priority(self): return PRIORITY_SET_READOUT
[docs] def matches(self, inst: mc.Instruction): return isinstance(inst.get_operation(), mc.AbstractMeasurement) and all( q in self._qubit_set for q in inst.get_qubits() )
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None return mc.Instruction(self.noise, tuple(), tuple(inst.get_bits()), tuple())
[docs] @dataclass(frozen=True) class OperationInstanceNoise(AbstractNoiseRule): """Add noise to every instance of an operation. An instruction matches when its operation has the same type as ``operation``. If ``operation`` has symbolic parameters, such as ``GateRX(a)``, any parameter value matches and the symbols in ``noise`` are replaced by the values of the matched instance. Otherwise the operation must be equal to ``operation``. Args: operation: Operation to match: a gate, measurement, reset, ``Block``, ``Repeat``, ``IfStatement`` or ``WhileStatement``. Symbolic parameters must be plain symbols. noise: Kraus channel or gate to add, on the same number of qubits as ``operation``. before: Add the noise before the operation instead of after it. replace: Replace the operation with the noise. Raises: ValueError: If ``operation`` is of an unsupported kind, if ``noise`` acts on a different number of qubits, or if both ``before`` and ``replace`` are set. Example: >>> import mimiqcircuits as mc >>> from symengine import symbols, pi >>> a = symbols("a") >>> rule = mc.OperationInstanceNoise(mc.GateRX(a), mc.Depolarizing1(a / pi)) >>> c = mc.Circuit().push(mc.GateRX(pi / 2), 0) >>> mc.NoiseModel([rule]).apply_noise_model(c) 1-qubit circuit with 2 instructions: ├── RX((1/2)*pi) @ q[0] └── Depolarizing(0.5) @ q[0] <BLANKLINE> """ operation: mc.Operation noise: Union[mc.krauschannel, mc.Gate] before: bool = False replace: bool = False def __post_init__(self): _validate_rule_operation_target(self.operation) if _supports_symbolic_operation_pattern(self.operation): _validate_rule_gate_params(self.operation) if self.operation.num_qubits != self.noise.num_qubits: raise ValueError( "Noise operation must act on the same number of qubits as " "the operation instance" ) if self.before and self.replace: raise ValueError("Cannot set both before=True and replace=True") _bind_before_method(self, self.before)
[docs] def priority(self): return PRIORITY_GLOBAL_OPERATION
[docs] def replaces(self): return self.replace
[docs] def matches(self, inst: mc.Instruction): op_inst = inst.get_operation() op_rule = self.operation 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
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None op_inst = inst.get_operation() noise_op = _resolve_noise_for_operation_match(self.operation, op_inst, self.noise) return mc.Instruction(noise_op, tuple(inst.get_qubits()))
[docs] @dataclass(frozen=True) class ExactOperationInstanceQubitNoise(AbstractNoiseRule): """Add noise to an operation acting on exactly the given qubits. Matches like ``OperationInstanceNoise``, and also requires the instruction's qubits to be ``qubits``, in the same order. Args: operation: Operation to match (see ``OperationInstanceNoise``). qubits: Qubits the operation must act on, in order. noise: Kraus channel or gate to add, on the same number of qubits as ``operation``. before: Add the noise before the operation instead of after it. replace: Replace the operation with the noise. Raises: ValueError: If ``operation`` is of an unsupported kind, if ``qubits`` has repetitions or a length other than the number of qubits of ``operation``, if ``noise`` acts on a different number of qubits, or if both ``before`` and ``replace`` are set. Example: >>> import mimiqcircuits as mc >>> rule = mc.ExactOperationInstanceQubitNoise(mc.GateCX(), [0, 1], mc.Depolarizing2(0.01)) >>> c = mc.Circuit().push(mc.GateCX(), 0, 1).push(mc.GateCX(), 1, 0) >>> mc.NoiseModel([rule]).apply_noise_model(c) 2-qubit circuit with 3 instructions: ├── CX @ q[0], q[1] ├── Depolarizing(0.01) @ q[0:1] └── CX @ q[1], q[0] <BLANKLINE> """ operation: mc.Operation qubits: Sequence[int] noise: Union[mc.krauschannel, mc.Gate] before: bool = False replace: bool = False def __post_init__(self): _validate_rule_operation_target(self.operation) if _supports_symbolic_operation_pattern(self.operation): _validate_rule_gate_params(self.operation) qs = list(self.qubits) if len(set(qs)) != len(qs): raise ValueError("Qubit list must not contain repetitions") if len(qs) != self.operation.num_qubits: raise ValueError( "Qubit list length must match the number of qubits the operation acts on" ) if self.operation.num_qubits != self.noise.num_qubits: raise ValueError( "Noise operation must act on the same number of qubits as " "the operation instance" ) if self.before and self.replace: raise ValueError("Cannot set both before=True and replace=True") object.__setattr__(self, "qubits", tuple(qs)) _bind_before_method(self, self.before)
[docs] def priority(self): return PRIORITY_EXACT_OPERATION
[docs] def replaces(self): return self.replace
[docs] def matches(self, inst: mc.Instruction): op_inst = inst.get_operation() op_rule = self.operation if type(op_inst) is not type(op_rule): return False if tuple(inst.get_qubits()) != self.qubits: return False if not _is_symbolic_operation_pattern(op_rule): return op_inst == op_rule return True
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None op_inst = inst.get_operation() noise_op = _resolve_noise_for_operation_match(self.operation, op_inst, self.noise) return mc.Instruction(noise_op, tuple(inst.get_qubits()))
[docs] @dataclass(frozen=True) class SetOperationInstanceQubitNoise(AbstractNoiseRule): """Add noise to an operation whose qubits are all in a set. Matches like ``OperationInstanceNoise``, and also requires every qubit of the instruction to be in ``qubits``. Args: operation: Operation to match (see ``OperationInstanceNoise``). qubits: Qubits where the noise applies. noise: Kraus channel or gate to add, on the same number of qubits as ``operation``. before: Add the noise before the operation instead of after it. replace: Replace the operation with the noise. Raises: ValueError: If ``operation`` is of an unsupported kind, if ``qubits`` has repetitions or fewer qubits than ``operation``, if ``noise`` acts on a different number of qubits, or if both ``before`` and ``replace`` are set. Example: >>> import mimiqcircuits as mc >>> rule = mc.SetOperationInstanceQubitNoise(mc.GateH(), [0, 1], mc.AmplitudeDamping(0.001)) >>> c = mc.Circuit().push(mc.GateH(), 0).push(mc.GateH(), 2) >>> mc.NoiseModel([rule]).apply_noise_model(c) 3-qubit circuit with 3 instructions: ├── H @ q[0] ├── AmplitudeDamping(0.001) @ q[0] └── H @ q[2] <BLANKLINE> """ operation: mc.Operation qubits: Iterable[int] noise: Union[mc.krauschannel, mc.Gate] before: bool = False replace: bool = False _qubit_set: frozenset = field(init=False, repr=False) def __post_init__(self): _validate_rule_operation_target(self.operation) if _supports_symbolic_operation_pattern(self.operation): _validate_rule_gate_params(self.operation) qs = list(self.qubits) if len(qs) < self.operation.num_qubits: raise ValueError( "Qubit set must contain at least as many qubits as the operation acts on" ) if len(qs) != len(set(qs)): raise ValueError("Qubit set must not contain repetitions") if self.operation.num_qubits != self.noise.num_qubits: raise ValueError( "Noise operation must act on the same number of qubits as " "the operation instance" ) if self.before and self.replace: raise ValueError("Cannot set both before=True and replace=True") object.__setattr__(self, "_qubit_set", frozenset(qs)) _bind_before_method(self, self.before)
[docs] def priority(self): return PRIORITY_SET_OPERATION
[docs] def replaces(self): return self.replace
[docs] def matches(self, inst: mc.Instruction): op_inst = inst.get_operation() op_rule = self.operation if type(op_inst) is not type(op_rule): return False if not all(q in self._qubit_set for q in inst.get_qubits()): return False if not _is_symbolic_operation_pattern(op_rule): return op_inst == op_rule return True
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None op_inst = inst.get_operation() noise_op = _resolve_noise_for_operation_match(self.operation, op_inst, self.noise) return mc.Instruction(noise_op, tuple(inst.get_qubits()))
[docs] @dataclass(frozen=True) class IdleNoise(AbstractNoiseRule): """Replace every ``Delay`` with a noise operation. Idle time is marked explicitly with ``Delay(t)`` instructions; qubits that are simply not acted upon get no noise. Each matching ``Delay`` is replaced by the noise on the same qubits. Args: relation: Either a constant noise operation, or a tuple ``(t, noise(t))`` where ``t`` is a symbol. In the second form ``t`` is replaced by the duration of each ``Delay``. Raises: ValueError: If ``relation`` is neither an operation nor a ``(symbol, operation)`` tuple. Example: >>> import mimiqcircuits as mc >>> from symengine import symbols >>> t = symbols("t") >>> rule = mc.IdleNoise((t, mc.AmplitudeDamping(t / 1000))) >>> c = mc.Circuit().push(mc.GateH(), 0).push(mc.Delay(10), 1) >>> mc.NoiseModel([rule]).apply_noise_model(c) 2-qubit circuit with 2 instructions: ├── H @ q[0] └── AmplitudeDamping(0.01) @ q[1] <BLANKLINE> """ relation: Union[tuple, mc.Operation] def __post_init__(self): if isinstance(self.relation, mc.Operation): return if not isinstance(self.relation, tuple) or len(self.relation) != 2: raise ValueError("IdleNoise relation must be (var, target) or Operation") var, target = self.relation if not (hasattr(var, "is_symbol") and var.is_symbol): raise ValueError( f"Left side must be a simple symbolic variable, got: {var}" ) if not isinstance(target, mc.Operation): raise ValueError("Right side of idle relation must be an Operation")
[docs] def priority(self): return PRIORITY_IDLE
[docs] def replaces(self): return True
[docs] def matches(self, inst: mc.Instruction): return isinstance(inst.get_operation(), mc.Delay)
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None op_inst = inst.get_operation() if isinstance(self.relation, mc.Operation): noise_op = self.relation else: var, target = self.relation noise_op = applyparams(op_inst, ((var,), target)) return mc.Instruction(noise_op, tuple(inst.get_qubits()))
[docs] @dataclass(frozen=True) class SetIdleQubitNoise(AbstractNoiseRule): """Replace every ``Delay`` on qubits in a set with a noise operation. Like ``IdleNoise``, restricted to ``Delay`` instructions whose qubits are all in ``qubits``. Args: relation: Constant noise operation or ``(t, noise(t))`` tuple (see ``IdleNoise``). qubits: Qubits where the noise applies. Raises: ValueError: If ``qubits`` is empty, or ``relation`` is neither an operation nor a ``(symbol, operation)`` tuple. Example: >>> import mimiqcircuits as mc >>> rule = mc.SetIdleQubitNoise(mc.AmplitudeDamping(1e-4), [0]) >>> c = mc.Circuit().push(mc.Delay(10), 0).push(mc.Delay(10), 1) >>> mc.NoiseModel([rule]).apply_noise_model(c) 2-qubit circuit with 2 instructions: ├── AmplitudeDamping(0.0001) @ q[0] └── Delay(10) @ q[1] <BLANKLINE> """ relation: Union[tuple, mc.Operation] qubits: Iterable[int] def __post_init__(self): qs = list(self.qubits) if not qs: raise ValueError("Qubit set must not be empty") object.__setattr__(self, "qubits", frozenset(qs)) if isinstance(self.relation, mc.Operation): return if not isinstance(self.relation, tuple) or len(self.relation) != 2: raise ValueError("IdleNoise relation must be (var, target) or Operation") var, target = self.relation if not (hasattr(var, "is_symbol") and var.is_symbol): raise ValueError("Left side must be symbolic variable") if not isinstance(target, mc.Operation): raise ValueError("Right side must be Operation")
[docs] def priority(self): return PRIORITY_SET_IDLE
[docs] def replaces(self): return True
[docs] def matches(self, inst: mc.Instruction): op = inst.get_operation() return isinstance(op, mc.Delay) and all(q in self.qubits for q in inst.get_qubits())
[docs] def apply_rule(self, inst: mc.Instruction): if not self.matches(inst): return None op_inst = inst.get_operation() if isinstance(self.relation, mc.Operation): noise_op = self.relation else: var, target = self.relation noise_op = applyparams(op_inst, ((var,), target)) return mc.Instruction(noise_op, tuple(inst.get_qubits()))
[docs] @dataclass(frozen=True) class CustomNoiseRule(AbstractNoiseRule): """Add noise chosen by user-supplied functions. Args: matcher: Function ``inst -> bool`` that selects the instructions. generator: Function ``inst -> Instruction`` that builds the noise instruction for a matched instruction. priority_val: Priority of the rule; lower runs first. Defaults to ``PRIORITY_USER_OVERRIDE``, ahead of every built-in rule. before: Add the noise before the instruction instead of after it. replace: Replace the instruction with the noise. A noise model containing a ``CustomNoiseRule`` cannot be saved with ``saveproto``: the matcher and generator are Python callables with no serialized form. Example: >>> import mimiqcircuits as mc >>> rule = mc.CustomNoiseRule( ... lambda inst: inst.get_operation().num_qubits == 2, ... lambda inst: mc.Instruction(mc.Depolarizing2(0.01), inst.get_qubits()), ... ) >>> c = mc.Circuit().push(mc.GateCX(), 0, 1) >>> mc.NoiseModel([rule]).apply_noise_model(c) 2-qubit circuit with 2 instructions: ├── CX @ q[0], q[1] └── Depolarizing(0.01) @ q[0:1] <BLANKLINE> """ matcher: Callable[[mc.Instruction], bool] generator: Callable[[mc.Instruction], mc.Instruction] priority_val: int = PRIORITY_USER_OVERRIDE before: bool = False replace: bool = False def __post_init__(self): _bind_before_method(self, self.before)
[docs] def priority(self): return self.priority_val
[docs] def matches(self, inst: mc.Instruction): return self.matcher(inst)
[docs] def apply_rule(self, inst: mc.Instruction): return self.generator(inst) if self.matcher(inst) else None
[docs] def replaces(self): return self.replace
[docs] @dataclass class NoiseModel: """Collection of prioritized noise rules. Rules are always kept sorted by ``priority()`` so more specific rules win over generic fallbacks. Priority order (lower number = higher priority): - ``CustomNoiseRule`` (default ``PRIORITY_USER_OVERRIDE``) - ``ExactOperationInstanceQubitNoise`` (``PRIORITY_EXACT_OPERATION``) - ``ExactQubitReadoutNoise`` (``PRIORITY_EXACT_READOUT``) - ``SetOperationInstanceQubitNoise`` (``PRIORITY_SET_OPERATION``) - ``SetQubitReadoutNoise`` (``PRIORITY_SET_READOUT``) - ``OperationInstanceNoise`` (``PRIORITY_GLOBAL_OPERATION``) - ``GlobalReadoutNoise`` (``PRIORITY_GLOBAL_READOUT``) - ``SetIdleQubitNoise`` (``PRIORITY_SET_IDLE``) - ``IdleNoise`` (``PRIORITY_IDLE``) Args: rules: Initial list of rules. name: Optional model name. Example: >>> import mimiqcircuits as mc >>> from symengine import symbols, pi >>> theta = symbols("theta") >>> model = mc.NoiseModel( ... [ ... mc.OperationInstanceNoise(mc.GateRX(theta), mc.Depolarizing(1, theta / pi)), ... mc.ExactOperationInstanceQubitNoise(mc.GateCX(), [0, 1], mc.Depolarizing(2, 0.01)), ... mc.GlobalReadoutNoise(mc.ReadoutErr(0.01, 0.02)), ... mc.IdleNoise(mc.AmplitudeDamping(1e-4)), ... ], ... name="angle-dependent", ... ) """ rules: List[AbstractNoiseRule] = field(default_factory=list) name: str = "" def __post_init__(self): self.rules.sort(key=lambda r: r.priority())
[docs] def add_rule(self, rule: AbstractNoiseRule): """Add a rule and keep model rules sorted by priority.""" self.rules.append(rule) self.rules.sort(key=lambda r: r.priority()) return self
[docs] def add_readout_noise(self, noise: mc.ReadoutErr, *, qubits=None, exact=False): """Add a readout-noise rule. Behavior: - ``qubits is None``: add ``GlobalReadoutNoise``. - ``qubits`` with ``exact=False``: add ``SetQubitReadoutNoise``. - ``qubits`` with ``exact=True``: add ``ExactQubitReadoutNoise``. Args: noise: Readout error channel. qubits: Optional qubit targets. exact: When ``True``, qubit order must match exactly. Returns: ``self`` (for chaining). Example: >>> import mimiqcircuits as mc >>> model = mc.NoiseModel() >>> model.add_readout_noise(mc.ReadoutErr(0.01, 0.02)) NoiseModel(rules=[GlobalReadoutNoise(noise=RErr(0.01,0.02))], name='') >>> model.add_readout_noise(mc.ReadoutErr(0.03, 0.04), qubits=[0, 2]) NoiseModel(rules=[SetQubitReadoutNoise(qubits=(0, 2), noise=RErr(0.03,0.04)), GlobalReadoutNoise(noise=RErr(0.01,0.02))], name='') >>> model.add_readout_noise(mc.ReadoutErr(0.05, 0.06), qubits=[2, 0], exact=True) NoiseModel(rules=[ExactQubitReadoutNoise(qubits=(2, 0), noise=RErr(0.05,0.06)), SetQubitReadoutNoise(qubits=(0, 2), noise=RErr(0.03,0.04)), GlobalReadoutNoise(noise=RErr(0.01,0.02))], name='') """ if qubits is None: rule = GlobalReadoutNoise(noise) elif exact: rule = ExactQubitReadoutNoise(tuple(qubits), noise) else: rule = SetQubitReadoutNoise(tuple(qubits), noise) return self.add_rule(rule)
[docs] def add_operation_noise( self, operation, noise, *, qubits=None, exact=False, before=False, replace=False ): """Add operation-instance noise. ``operation`` can be concrete (exact parameter match) or symbolic (match by operation type and substitute parameters into ``noise``). Args: operation: Operation pattern to match. noise: Noise operation/channel to inject. qubits: Optional qubit restriction. exact: If ``True``, qubits must match the exact ordered tuple. before: If ``True``, insert noise before the matched instruction. replace: If ``True``, replace the matched instruction with noise. Returns: ``self`` (for chaining). Raises: ValueError: If operation target is unsupported or arguments are invalid. Examples: >>> import mimiqcircuits as mc >>> from symengine import symbols, pi >>> theta, alpha, beta = symbols("theta alpha beta") >>> model = mc.NoiseModel() >>> _ = model.add_operation_noise(mc.GateRX(pi / 2), mc.AmplitudeDamping(0.001)) >>> _ = model.add_operation_noise(mc.GateRX(theta), mc.Depolarizing(1, theta / pi)) >>> _ = model.add_operation_noise( ... mc.GateU(alpha, beta, 0), ... mc.Depolarizing(1, (alpha**2 + beta**2) / (2 * pi**2)), ... ) >>> _ = model.add_operation_noise( ... mc.GateRX(theta), ... mc.Depolarizing(1, theta / pi), ... qubits=[0, 1, 2], ... ) >>> _ = model.add_operation_noise( ... mc.GateRX(theta), ... mc.Depolarizing(1, theta / pi), ... qubits=[0], ... exact=True, ... ) >>> _ = model.add_operation_noise(mc.Measure(), mc.PauliX(0.02), before=True) >>> _ = model.add_operation_noise(mc.Reset(), mc.Depolarizing(1, 0.01)) >>> _ = model.add_operation_noise(mc.GateH(), mc.AmplitudeDamping(0.001), replace=True) """ _validate_rule_operation_target(operation) if qubits is None: rule = OperationInstanceNoise( operation, noise, before=before, replace=replace ) elif exact: rule = ExactOperationInstanceQubitNoise( operation, tuple(qubits), noise, before=before, replace=replace ) else: rule = SetOperationInstanceQubitNoise( operation, tuple(qubits), noise, before=before, replace=replace ) return self.add_rule(rule)
[docs] def add_idle_noise(self, noise, qubits=None): """Add idle-noise rule(s) for ``Delay`` instructions. ``noise`` can be a constant operation or a relation tuple ``(time_symbol, target_operation_expr)``. Args: noise: Idle noise operation or symbolic relation. qubits: Optional qubit subset where idle noise is allowed. Returns: ``self`` (for chaining). Example: >>> import mimiqcircuits as mc >>> from symengine import symbols >>> t = symbols("t") >>> model = mc.NoiseModel() >>> model.add_idle_noise(mc.AmplitudeDamping(1e-4)) NoiseModel(rules=[IdleNoise(relation=AmplitudeDamping(0.0001))], name='') >>> model.add_idle_noise((t, mc.AmplitudeDamping(t / 1000)), qubits=[0, 1, 2]) NoiseModel(rules=[SetIdleQubitNoise(relation=(t, AmplitudeDamping((1/1000)*t)), qubits=frozenset({0, 1, 2})), IdleNoise(relation=AmplitudeDamping(0.0001))], name='') """ if qubits is None: rule = IdleNoise(noise) else: rule = SetIdleQubitNoise(noise, qubits) return self.add_rule(rule)
[docs] def apply_noise_model(self, circuit: mc.Circuit): """Apply this noise model and return a new noisy circuit. This is a convenience wrapper around module-level ``apply_noise_model``. """ return apply_noise_model(circuit, self)
[docs] def describe(self): title = f"NoiseModel: {self.name}" if self.name else "NoiseModel" print(title) print("=" * 80) for i, rule in enumerate(self.rules, 1): print(f"Rule {i} (priority {rule.priority()}): {type(rule).__name__}") if isinstance(rule, GlobalReadoutNoise): print(f" -> Applies {rule.noise} to all measurements") elif isinstance(rule, ExactQubitReadoutNoise): print( f" -> Applies {rule.noise} to measurements on qubits " f"{list(rule.qubits)} (exact order)" ) elif isinstance(rule, SetQubitReadoutNoise): print( f" -> Applies {rule.noise} to measurements on qubits in " f"{sorted(rule._qubit_set)}" ) elif isinstance(rule, OperationInstanceNoise): print( f" -> Applies {rule.noise} to operation instances matching " f"{rule.operation}" ) elif isinstance(rule, ExactOperationInstanceQubitNoise): print( f" -> Applies {rule.noise} to {rule.operation} on qubits " f"{list(rule.qubits)} (exact order)" ) elif isinstance(rule, SetOperationInstanceQubitNoise): print( f" -> Applies {rule.noise} to {rule.operation} on qubits in " f"{sorted(rule._qubit_set)}" ) elif isinstance(rule, IdleNoise): if isinstance(rule.relation, mc.Operation): print(f" -> Applies {rule.relation} to all idle qubits") else: var, target = rule.relation print( f" -> Applies time-dependent idle noise {target} with " f"variable {var} to all idle qubits" ) elif isinstance(rule, SetIdleQubitNoise): if isinstance(rule.relation, mc.Operation): print(f" -> Applies {rule.relation} to idle qubits {sorted(rule.qubits)}") else: var, target = rule.relation print( f" -> Applies time-dependent idle noise {target} with " f"variable {var} to idle qubits {sorted(rule.qubits)}" ) elif isinstance(rule, CustomNoiseRule): print(" -> Custom rule (user-defined matcher)") if rule.before(): print(" (applied BEFORE the operation)") if rule.replaces(): print(" (REPLACES the matched operation)") print("-" * 80)
[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, NoiseModel)
def _canonical_targets(op: mc.Operation): return ( tuple(range(op.num_qubits)), tuple(range(op.num_bits)), tuple(range(op.num_zvars)), ) def _collapse_local_instructions_to_operation( instructions: List[mc.Instruction], op: mc.Operation ): qcanon, bcanon, zcanon = _canonical_targets(op) if ( len(instructions) == 1 and instructions[0].get_qubits() == qcanon and instructions[0].get_bits() == bcanon and instructions[0].get_zvars() == zcanon ): return instructions[0].get_operation() return mc.Block(op.num_qubits, op.num_bits, op.num_zvars, instructions) def _apply_rules_to_instruction(inst: mc.Instruction, model: NoiseModel): for rule in model.rules: noise_inst = rule.apply_rule(inst) if noise_inst is None: continue if rule.replaces(): return [noise_inst], True if rule.before(): return [noise_inst, inst], True return [inst, noise_inst], True return [inst], False def _rewrite_nested_operation( op: mc.Operation, model: NoiseModel, active_decls: Set[object] ): if isinstance(op, mc.Block): noisy_instructions = _apply_noise_to_instructions( op.instructions, model, active_decls ) if noisy_instructions == op.instructions: return op return mc.Block(op.num_qubits, op.num_bits, op.num_zvars, noisy_instructions) if isinstance(op, mc.IfStatement): inner = op.get_operation() qcanon, bcanon, zcanon = _canonical_targets(inner) inner_inst = mc.Instruction(inner, qcanon, bcanon, zcanon) noisy_inner = _apply_noise_to_instruction(inner_inst, model, active_decls) rewritten_inner = _collapse_local_instructions_to_operation(noisy_inner, inner) if rewritten_inner == inner: return op return mc.IfStatement(rewritten_inner, op.get_bitstring()) if isinstance(op, mc.WhileStatement): inner = op.get_operation() qcanon, bcanon, zcanon = _canonical_targets(inner) inner_inst = mc.Instruction(inner, qcanon, bcanon, zcanon) noisy_inner = _apply_noise_to_instruction(inner_inst, model, active_decls) rewritten_inner = _collapse_local_instructions_to_operation(noisy_inner, inner) if rewritten_inner == inner: return op return mc.WhileStatement(rewritten_inner, op.get_bitstring()) if isinstance(op, mc.Parallel): inner = op.get_operation() nq = inner.num_qubits nb = inner.num_bits nz = inner.num_zvars repeated_instructions = [] for rep in range(op.num_repeats): qtargets = tuple(nq * rep + i for i in range(nq)) btargets = tuple(nb * rep + i for i in range(nb)) ztargets = tuple(nz * rep + i for i in range(nz)) repeated_instructions.append( mc.Instruction(inner, qtargets, btargets, ztargets) ) noisy_instructions = _apply_noise_to_instructions( repeated_instructions, model, active_decls ) if noisy_instructions == repeated_instructions: return op return mc.Block(op.num_qubits, op.num_bits, op.num_zvars, noisy_instructions) if isinstance(op, mc.Repeat): inner = op.get_operation() qcanon, bcanon, zcanon = _canonical_targets(inner) repeated_instructions = [ mc.Instruction(inner, qcanon, bcanon, zcanon) for _ in range(op.repeats) ] noisy_instructions = _apply_noise_to_instructions( repeated_instructions, model, active_decls ) if noisy_instructions == repeated_instructions: return op return mc.Block(op.num_qubits, op.num_bits, op.num_zvars, noisy_instructions) if isinstance(op, mc.GateCall): decl = op.decl # Prevent infinite recursion for self-referential declarations. if decl in active_decls: return op active_decls.add(decl) try: substitutions = dict(zip(decl.arguments, op.arguments)) expanded_instructions = [] for inst in decl.circuit: expanded_op = inst.get_operation().evaluate(substitutions) expanded_instructions.append( mc.Instruction( expanded_op, inst.get_qubits(), inst.get_bits(), inst.get_zvars() ) ) noisy_instructions = _apply_noise_to_instructions( expanded_instructions, model, active_decls ) if noisy_instructions == expanded_instructions: return op return mc.Block(op.num_qubits, op.num_bits, op.num_zvars, noisy_instructions) finally: active_decls.remove(decl) return op def _apply_noise_to_instruction( inst: mc.Instruction, model: NoiseModel, active_decls: Set[object] ): rewritten_instructions, matched = _apply_rules_to_instruction(inst, model) if matched: return rewritten_instructions op = inst.get_operation() rewritten_op = _rewrite_nested_operation(op, model, active_decls) if rewritten_op is op: return rewritten_instructions return [ mc.Instruction( rewritten_op, inst.get_qubits(), inst.get_bits(), inst.get_zvars() ) ] def _apply_noise_to_instructions( instructions: List[mc.Instruction], model: NoiseModel, active_decls: Set[object] ): noisy_instructions = [] for inst in instructions: noisy_instructions.extend(_apply_noise_to_instruction(inst, model, active_decls)) return noisy_instructions
[docs] def apply_noise_model(circuit: mc.Circuit, model: NoiseModel): """Apply a noise model to a circuit and return a new circuit. Rules are evaluated in priority order. For each instruction, only the first matching rule is applied. Wrapper operations are traversed recursively: ``Block``, ``IfStatement``, ``Parallel``, ``Repeat``, and ``GateCall``. Args: circuit: Input circuit. model: Noise model to apply. Returns: A new circuit with injected noise. Examples: >>> import mimiqcircuits as mc >>> from symengine import symbols, pi >>> theta = symbols("theta") >>> c = mc.Circuit() >>> c.push(mc.GateRX(0.4), 0) 1-qubit circuit with 1 instruction: └── RX(0.4) @ q[0] <BLANKLINE> >>> c.push(mc.GateRX(0.8), 1) 2-qubit circuit with 2 instructions: ├── RX(0.4) @ q[0] └── RX(0.8) @ q[1] <BLANKLINE> >>> c.push(mc.Measure(), 0, 0) 2-qubit, 1-bit circuit with 3 instructions: ├── RX(0.4) @ q[0] ├── RX(0.8) @ q[1] └── M @ q[0], c[0] <BLANKLINE> >>> c.push(mc.Measure(), 1, 1) 2-qubit, 2-bit circuit with 4 instructions: ├── RX(0.4) @ q[0] ├── RX(0.8) @ q[1] ├── M @ q[0], c[0] └── M @ q[1], c[1] <BLANKLINE> >>> model = mc.NoiseModel([mc.OperationInstanceNoise(mc.GateRX(theta), mc.Depolarizing(1, theta / pi)), ... mc.GlobalReadoutNoise(mc.ReadoutErr(0.01, 0.02)),]) >>> noisy = mc.apply_noise_model(c, model) Recursive wrapper examples: >>> model = mc.NoiseModel([mc.OperationInstanceNoise(mc.GateH(), mc.AmplitudeDamping(0.01))]) >>> inner = mc.Circuit().push(mc.GateH(), 0) >>> c_block = mc.Circuit().push(mc.Block(inner), 0) >>> noisy_block= mc.apply_noise_model(c_block, model) >>> noisy_block.decompose_step() 1-qubit circuit with 2 instructions: ├── H @ q[0] └── AmplitudeDamping(0.01) @ q[0] <BLANKLINE> >>> decl = mc.GateDecl("local_h", (), inner) >>> c_call = mc.Circuit().push(mc.GateCall(decl, ()), 0) >>> noisy_call = mc.apply_noise_model(c_call, model) >>> noisy_call.decompose_step() 1-qubit circuit with 2 instructions: ├── H @ q[0] └── AmplitudeDamping(0.01) @ q[0] <BLANKLINE> >>> c_parallel = mc.Circuit().push(mc.Parallel(2, mc.GateH()), 0, 1) >>> noisy_parallel = mc.apply_noise_model(c_parallel, model) >>> noisy_parallel.decompose_step() 2-qubit circuit with 4 instructions: ├── H @ q[0] ├── AmplitudeDamping(0.01) @ q[0] ├── H @ q[1] └── AmplitudeDamping(0.01) @ q[1] <BLANKLINE> >>> c_repeat = mc.Circuit().push(mc.Repeat(2, mc.GateH()), 0) >>> noisy_repeat = mc.apply_noise_model(c_repeat, model) >>> noisy_repeat.decompose_step() 1-qubit circuit with 4 instructions: ├── H @ q[0] ├── AmplitudeDamping(0.01) @ q[0] ├── H @ q[0] └── AmplitudeDamping(0.01) @ q[0] <BLANKLINE> >>> c_if = mc.Circuit().push(mc.IfStatement(mc.GateH(), mc.BitString("1")), 0, 0) >>> noisy_if = mc.apply_noise_model(c_if, model) >>> noisy_if.decompose_step() 1-qubit, 1-bit circuit with 2 instructions: ├── IF(c==1) H @ q[0], condition[0] └── IF(c==1) AmplitudeDamping(0.01) @ q[0], condition[0] <BLANKLINE> >>> decl_inner = mc.GateDecl("inner_h", (), inner) >>> middle = mc.Circuit().push(mc.GateCall(decl_inner, ()), 0) >>> decl_outer = mc.GateDecl("outer_call", (), middle) >>> c_nested = mc.Circuit().push(mc.GateCall(decl_outer, ()), 0) >>> noisy_nested = mc.apply_noise_model(c_nested, model) >>> noisy_nested.decompose_step().decompose_step() 1-qubit circuit with 2 instructions: ├── H @ q[0] └── AmplitudeDamping(0.01) @ q[0] <BLANKLINE> Recursive wrapper examples deeply nested: >>> c_nested2 = mc.Circuit().push(mc.GateCall(decl_outer, ()), 0).push(mc.GateCall(decl_outer, ()), 1) >>> c_nested2.decompose_step() 2-qubit circuit with 2 instructions: ├── inner_h() @ q[0] └── inner_h() @ q[1] <BLANKLINE> >>> noisy_nested2 = mc.apply_noise_model(c_nested2, model) >>> noisy_nested2.decompose_step().decompose_step() 2-qubit circuit with 4 instructions: ├── H @ q[0] ├── AmplitudeDamping(0.01) @ q[0] ├── H @ q[1] └── AmplitudeDamping(0.01) @ q[1] <BLANKLINE> """ active_decls: Set[object] = set() noisy_instructions = _apply_noise_to_instructions( circuit.instructions, model, active_decls ) return mc.Circuit(noisy_instructions)
__all__ = [ "AbstractNoiseRule", "GlobalReadoutNoise", "ExactQubitReadoutNoise", "SetQubitReadoutNoise", "OperationInstanceNoise", "ExactOperationInstanceQubitNoise", "SetOperationInstanceQubitNoise", "IdleNoise", "SetIdleQubitNoise", "CustomNoiseRule", "NoiseModel", "apply_noise_model", ]