"""Abstract interface for noise models.
This module defines :class:`NoiseModel`, the base class used by
:class:`graphix.simulator.PatternSimulator` when running noisy
simulations. Child classes implement concrete noise processes by
overriding the abstract methods defined here.
"""
from __future__ import annotations
import dataclasses
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, ClassVar, Literal
# override introduced in Python 3.12
from typing_extensions import override
from graphix.command import BaseM, Command, CommandKind, Node, _KindChecker
if TYPE_CHECKING:
from collections.abc import Iterable
from numpy.random import Generator
from graphix.channels import KrausChannel
from graphix.measurements import Outcome
[docs]
class Noise(ABC):
"""Abstract base class for noise."""
@property
@abstractmethod
def nqubits(self) -> int:
"""Return the number of qubits targetted by the noise."""
[docs]
@abstractmethod
def to_kraus_channel(self) -> KrausChannel:
"""Return the Kraus channel describing the noise."""
[docs]
@dataclass
class ApplyNoise(_KindChecker):
"""Apply noise command.
Parameters
----------
noise : Noise
noise to be applied
nodes : list[Node]
list of node indices on which to apply noise
domain: set[Node] | None = None
Optional domain for conditional noise.
If ``None``, the noise is applied unconditionally.
Otherwise, the noise is applied if there is an odd number of nodes among ``domain`` that have been measured with outcome 1 (as for ``X`` and ``Z`` commands).
Note that the noise is never applied if ``domain`` is the empty set.
"""
kind: ClassVar[Literal[CommandKind.ApplyNoise]] = dataclasses.field(default=CommandKind.ApplyNoise, init=False)
noise: Noise
nodes: list[Node]
domain: set[Node] | None = None
CommandOrNoise = Command | ApplyNoise
[docs]
class NoiseModel(ABC):
"""Abstract base class for all noise models."""
[docs]
@abstractmethod
def command(
self, cmd: CommandOrNoise, rng: Generator | None = None, *, stacklevel: int = 1
) -> list[CommandOrNoise]:
"""Return the noise to apply to the command ``cmd``."""
[docs]
@abstractmethod
def confuse_result(
self, cmd: BaseM, result: Outcome, rng: Generator | None = None, *, stacklevel: int = 1
) -> Outcome:
"""Return a possibly flipped measurement outcome.
Parameters
----------
result : Outcome
Ideal measurement result.
cmd : BaseM
The measurement command that produced the given outcome.
Returns
-------
Outcome
Possibly corrupted result.
"""
[docs]
def transpile(
self, sequence: Iterable[CommandOrNoise], rng: Generator | None = None, *, stacklevel: int = 1
) -> list[CommandOrNoise]:
"""Apply the noise to a sequence of commands and return the resulting sequence."""
return [n_cmd for cmd in sequence for n_cmd in self.command(cmd, rng=rng, stacklevel=stacklevel + 1)]
[docs]
class NoiselessNoiseModel(NoiseModel):
"""Noise model that performs no operation."""
[docs]
@override
def command(
self, cmd: CommandOrNoise, rng: Generator | None = None, *, stacklevel: int = 1
) -> list[CommandOrNoise]:
"""Return the noise to apply to the command ``cmd``."""
return [cmd]
[docs]
@override
def confuse_result(
self, cmd: BaseM, result: Outcome, rng: Generator | None = None, *, stacklevel: int = 1
) -> Outcome:
"""Assign wrong measurement result."""
return result
[docs]
@dataclass(frozen=True)
class ComposeNoiseModel(NoiseModel):
"""Compose noise models."""
models: list[NoiseModel]
[docs]
@override
def command(
self, cmd: CommandOrNoise, rng: Generator | None = None, *, stacklevel: int = 1
) -> list[CommandOrNoise]:
"""Return the noise to apply to the command ``cmd``."""
sequence = [cmd]
for model in self.models:
sequence = model.transpile(sequence, rng=rng, stacklevel=stacklevel + 1)
return sequence
[docs]
@override
def confuse_result(
self, cmd: BaseM, result: Outcome, rng: Generator | None = None, *, stacklevel: int = 1
) -> Outcome:
"""Assign wrong measurement result."""
for m in self.models:
result = m.confuse_result(cmd, result, rng=rng, stacklevel=stacklevel + 1)
return result