#
# 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.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))
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 _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:
"""Abstract base class for all noise rules."""
[docs]
def priority(self):
"""Lower = higher priority."""
return 100
[docs]
def before(self):
"""If True, apply noise before the operation."""
return False
[docs]
def replaces(self):
"""If True, noise instruction replaces the original instruction."""
return False
[docs]
def matches(self, inst: mc.Instruction):
raise NotImplementedError
[docs]
def apply_rule(self, inst: mc.Instruction):
raise NotImplementedError
[docs]
@dataclass(frozen=True)
class GlobalReadoutNoise(AbstractNoiseRule):
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):
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):
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):
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):
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):
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):
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):
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):
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.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()
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()
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()
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()
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()
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().decompose()
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()
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().decompose()
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",
]