#
# 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",
]