"""
Copyright (c) Entropica Labs Pte Ltd 2025.
Use, distribution and reproduction of this program in its source or compiled
form is prohibited without the express written consent of Entropica Labs Pte
Ltd.
"""
from collections import defaultdict
from enum import Enum
import math
from typing import Any, Callable, Optional, Protocol, runtime_checkable
from pydantic import BaseModel, Field, field_validator, model_validator, PrivateAttr
from loom.eka import Circuit
# ====== Type / Aliases =====
# Callable type for error probability, return a list of float given a gate name and an optional
# time.
# Allow returning a list of floats for error probabilities. Some noise instructions
# may require multiple parameters (e.g., pauli_channel).
# pylint: disable=too-few-public-methods
[docs]
@runtime_checkable
class ErrorProbProtocol(Protocol):
"""Protocol for error probability functions."""
def __call__(
self, time_from_start: Optional[float], time_of_tick: Optional[float]
) -> list[float]:
"""Compute error probabilities for a given gate and time parameters.
Parameters
----------
time_from_start : Optional[float]
Cumulative time from the start of the circuit.
time_of_tick : Optional[float]
Duration or idle time within the current tick.
Returns
-------
list[float]
List of error probabilities.
"""
GateErrorProbProtocol = Callable[[Optional[float]], list[float]]
[docs]
class ApplicationMode(str, Enum):
"""
Enum-like class to define the application mode of the error model.
"""
BEFORE_GATE = "before_gate"
AFTER_GATE = "after_gate"
END_OF_TICK = "end_of_tick"
IDLE_END_OF_TICK = "idle_end_of_tick"
# Error model shall define the types of errors that can occur in a quantum circuit,
# In order to properly map to noise instructions in the different backends.
[docs]
class ErrorType(Enum):
"""
Provides a set of error types that can be used to define the error model for a quantum
circuit.
Each error type has a label and a number of parameters that it expects.
Also provides a method to validate the parameters for the error type by checking they
are a proper probability distribution (sum doesn't exceed 1 if multiple parameters)
and that they are in the range [0, 1].
"""
PAULI_X = ("pauli_x", 1)
PAULI_Y = ("pauli_y", 1)
PAULI_Z = ("pauli_z", 1)
PAULI_CHANNEL = ("pauli_channel", 3)
BIT_FLIP = ("bit-flip", 1)
PHASE_FLIP = ("phase-flip", 1)
DEPOLARIZING1 = ("depolarizing1", 1)
DEPOLARIZING2 = ("depolarizing2", 1)
def __init__(self, label: str, param_count: int):
self.label = label
self.param_count = param_count
[docs]
def validate_params(self, params: list[float]) -> None:
"""Raise ValueError if params are not valid for this error type."""
if len(params) != self.param_count:
raise ValueError(
f"{self.name} expects {self.param_count} parameter(s), got {len(params)}."
)
if not all(isinstance(p, (float, int)) for p in params):
raise TypeError(f"All parameters for {self.name} must be numbers.")
if not all(0 <= p <= 1 for p in params):
raise ValueError(f"All parameters for {self.name} must be in [0, 1].")
if self.param_count > 1:
if sum(params) > 1.0:
raise ValueError(f"{self.name} parameters sum must not exceed 1.0.")
# ====== CircuitErrorModel Class =====
[docs]
class CircuitErrorModel(BaseModel):
"""
Define a circuit error model that can be used to simulate errors in quantum circuits.
This model can be time-dependent or not, having error applied in different modes,
and can define error probabilities for each gate in the circuit or for each tick in
the circuit.
It is designed to be used with the Circuit class and its operations.
This class is very general and allows for very flexible error modeling, however, it
is a bit tedious to work with, so we recommend to define subclasses that
define specific error models for your use case.
Parameters
----------
circuit : Circuit
The quantum circuit to which the error model will be applied.
This is frozen after initialization, so it cannot be changed.
error_type : ErrorType
The type of error that the model will apply to the circuit.
This is frozen after initialization, so it cannot be changed.
is_time_dependent : bool
Whether the error model is time-dependent or not.
If True, the model will use gate_durations to compute error probabilities.
If False, the model will not use gate_durations.
This is frozen after initialization, so it cannot be changed.
application_mode : ApplicationMode
The mode in which the error is applied to the circuit.
It can be BEFORE_GATE, AFTER_GATE, or END_OF_TICK.
This is frozen after initialization, so it cannot be changed.
gate_durations : Optional[dict[str, float]]
A dictionary mapping gate names to their execution times.
This is only used if the model is time-dependent.
If the model is not time-dependent, this can be None.
If provided, it must assign a duration to each gate type present in the circuit.
gate_error_probabilities : Optional[dict[str, GateErrorProbProtocol]]
A dictionary mapping gate names to a callable that returns the error probability
for that gate.
If the model is time-dependent, the callable can take an optional time parameter.
If the model is not time-dependent, the callable should not take any parameters.
If one gate isn't in the dictionary, it will default to a callable that
returns 0.
global_time_error_probability : Callable[[Optional[float]], list[float]]
A callable that returns the error probability at a specific time in the circuit.
It can take an optional time parameter, which represents some information in the
current tick:
- If the application mode is END_OF_TICK, it represents the duration of the tick.
- If the application mode is IDLE_END_OF_TICK, it represents the idle times
of the channel in the tick.
The function must be well-defined at t = 0 (for both inputs).
"""
# Pydantic configuration, this is necessary to allow arbitrary types in the model.
# In particular, we need to allow Circuit and ErrorProbProtocol custom types.
model_config = {"arbitrary_types_allowed": True, "frozen": True}
# ====== Required Class Attributes =====
# We assume a single instance of error model has a single error type and a single
# application mode.
# No initial value given, Pydantic will enforce the constructor to provide them.
circuit: Circuit
error_type: ErrorType # Define the instruction type of the error model.
is_time_dependent: bool
application_mode: ApplicationMode
# ====== Attribute ======
# Dictionary of gate durations, mapping gate names to their execution times.
# It must assign a duration to each gate type used in the circuit.
# For time-independent models, this can be left undefined (None).
gate_durations: Optional[dict[str, float]] = defaultdict(lambda: lambda _: [0.0])
[docs]
def update_gate_durations(self, gate_durations: dict[str, float]):
"""Update the gate durations for the error model.
This will recompute the operation times and tick durations if the model is
time-dependent."""
new_gate_durations = self.validate_gate_duration(gate_durations)
return self.model_copy(
update={
"gate_durations": new_gate_durations,
}
)
# Dictionary of gate error probabilities, mapping gate names to a callable
# that returns the error probabilities for that gate, with optional time attribute.
gate_error_probabilities: Optional[dict[str, GateErrorProbProtocol]] = defaultdict(
lambda: lambda _: [0.0]
)
# If the application mode is END_OF_TICK or IDLE_END_OF_TICK,
# this callable defines the error probability
# for a given time in the circuit.
# It returns a list of floats according to the expected parameters of the
# noise instruction.
global_time_error_probability: ErrorProbProtocol = lambda _, __: [0.0]
# If the model is time-dependent, this will be computed after initialization.
# List the ticks duration, ordered according to occurance in the circuit.
# !! Note: This contains the duration, not the time at which the tick occurs.
# In order to compute the time at which the tick occurs,
# you must sum the tick durations up to that point.
_tick_durations: Optional[list[float]] = PrivateAttr(default=None)
# If the model is time-dependent, this will be computed after initialization.
# Dictionary mapping gate IDs to their execution times.
_op_time: Optional[dict[str, float]] = PrivateAttr(default=None)
# If the model depends on the idle time of channels during ticks,
# this will be computed after initialization.
# Dictionary mapping channel IDs to a list of idle times during each tick.
_idle_times: Optional[dict[str, list[float]]] = PrivateAttr(default=None)
def model_post_init(self, __context: Any) -> None:
"""
Post-initialization method to compute operation times and ticks duration
if the model is time-dependent.
This is called after the model is initialized and all validators have run.
If the model is time-dependent, it computes the operation times and tick durations
This bypass the frozen nature of the model to set the private attributes.
"""
if self.is_time_dependent and self.gate_durations is not None:
op_time, tick_durations, idle_durations = (
self._compute__op_times_and__tick_durations()
)
object.__setattr__(self, "_idle_times", idle_durations)
object.__setattr__(self, "_op_time", op_time)
object.__setattr__(self, "_tick_durations", tick_durations)
else:
object.__setattr__(self, "_op_time", {})
object.__setattr__(self, "_tick_durations", [])
object.__setattr__(self, "_idle_times", {})
# ====== Validators =====
# if time-dependent, gate_durations must be provided
[docs]
@model_validator(mode="after")
@classmethod
def check_duration_defined_if_time_dependent(cls, model):
"""Ensure that gate_durations is defined if the model is time-dependent."""
if not isinstance(model, CircuitErrorModel):
raise TypeError("model must be an instance of CircuitErrorModel")
if model.is_time_dependent and model.gate_durations is None:
raise ValueError(
"gate_durations must be provided for time-dependent error models."
)
return model
[docs]
@model_validator(mode="after")
@classmethod
def check_idle_application_is_time_dependent(cls, model):
"""Ensure that idle application mode is only used for time-dependent models."""
if (
model.application_mode == ApplicationMode.IDLE_END_OF_TICK
and not model.is_time_dependent
):
raise ValueError(
"Idle application mode can only be used with time-dependent error models."
)
return model
# if time-dependent, _op_time and tick_time must be computed
# pylint: disable=protected-access
[docs]
@model_validator(mode="after")
@classmethod
def check__op_time_defined_if_time_dependent(cls, model):
"""Ensure that _op_time is defined if the model is time-dependent."""
if model.is_time_dependent and model._op_time is None:
raise ValueError(
"_op_time must be computed for time-dependent error models."
)
if model.is_time_dependent and model._tick_durations is None:
raise ValueError(
"_tick_durations must be computed for time-dependent error models."
)
return model
[docs]
@model_validator(mode="after")
@classmethod
def validate_gate_duration(cls, model):
"""Ensure the gates duration are defined for all gate used in the circuit
and that each gate has a valid duration.
"""
if model.is_time_dependent:
unrolled = Circuit.unroll(model.circuit)
gate_set = {op.name for layer in unrolled for op in layer if op is not None}
v = model.gate_durations
if v is not None:
if not isinstance(v, dict):
raise TypeError("gate_duration must be a dictionary or None")
missing_gates = [gate for gate in gate_set if gate not in v]
if missing_gates:
raise ValueError(
f"Missing gate durations for: {missing_gates}. Must provide durations",
f"for all gates in {gate_set}",
)
return model
[docs]
@field_validator("gate_error_probabilities", mode="before")
@classmethod
def validate_gate_error_probabilities(
cls, v: dict
) -> dict[str, GateErrorProbProtocol]:
"""Validate the gate error probabilities dictionary.
Ensure it contains only gates in the gate_set
Also ensure that each value is a callable.
Parameters
----------
v : dict[str, GateErrorProbProtocol] | None
The dictionary of gate error probabilities, where keys are gate names
and values are callables that return a list of floats representing the
error probabilities for that gate.
If None, it defaults to a callable that returns 0.0 for all gates.
Returns
-------
dict[str, GateErrorProbProtocol]
A validated dictionary of gate error probabilities, where each key is a gate name
and each value is a callable that returns the
error probabilities for that gate.
Raises
------
TypeError: If v is not a dictionary or callable.
ValueError: If v contains invalid gate names or if a value is not callable.
"""
# if undefined, return a default callable that returns 0.0
# This can be allowed if end_of_tick application is used.
if v is None:
v = defaultdict(lambda: lambda _: [0.0])
if not isinstance(v, dict):
raise TypeError("gate_error_probabilities must be a dictionary")
# Check it only contains valid gate names
for key, func in v.items():
# Check that it's callable
if not callable(func):
raise TypeError(f"Value for '{key}' must be callable.")
invalid_values = [k for k, v in v.items() if not callable(v)]
if invalid_values:
raise TypeError(
"All values in gate_error_probabilities must be callable. "
f"Keys with invalid value: {invalid_values}"
)
# fill with 0 for missing gates
return defaultdict(lambda: lambda _: [0.0], v)
# Check that the global_time_error_probability is valid for t=0
[docs]
@field_validator("global_time_error_probability", mode="before")
@classmethod
def validate_global_time_error_probability(cls, v) -> ErrorProbProtocol:
"""Validate the tick error probabilities callable.
Ensure it returns a float or a list of floats for t=0."""
if v is None:
return lambda _, __: [
0.0
] # Default to single zero, will be reformatted by model validator if needed
if not callable(v):
raise TypeError("global_time_error_probability must be a callable")
try:
def is_valid_prob(p):
return isinstance(p, float) and 0.0 <= p <= 1.0
result = v(0.0, 0.0)
if isinstance(result, list):
if not all(is_valid_prob(p) for p in result):
raise ValueError(
"global_time_error_probability must return a list of"
"floats between 0 and 1"
)
else:
raise TypeError(
"global_time_error_probability must return a list of floats"
)
except Exception as e:
raise ValueError(
f"global_time_error_probability callable failed: {e}"
) from e
return v
# ====== Validators that run last (to reformat default values) =====
[docs]
@model_validator(mode="after")
@classmethod
def validate_gate_error_probabilities_output(cls, model):
"""
Validate that each lambda in gate_error_probabilities returns the correct
number of parameters. The previous validators will typically assign default
value of [0.0] to the gate error probabilities for each undefined gate, but
some error types may require multiple parameters (e.g., PAULI_CHANNEL).
This validator will reformat the default value to return a list of zeros with
the correct length.
"""
# Create a new dictionary to hold potentially modified functions
updated_gate_error_probabilities = {}
expected_count = model.error_type.param_count
for gate_name, error_func in model.gate_error_probabilities.items():
try:
# Test the function with a dummy time value
if model.is_time_dependent:
result = error_func(0.0)
else:
result = error_func(None)
# Check if this is a default value [0.0] that needs reformatting
if (
len(result) == 1
and result[0] == 0.0
and model.error_type.param_count > 1
):
# Create a new function that returns the correct number of zeros
if model.is_time_dependent:
updated_gate_error_probabilities[gate_name] = (
lambda t: [0.0] * expected_count
)
else:
updated_gate_error_probabilities[gate_name] = (
lambda _: [0.0] * expected_count
)
else:
# Validate using the error type's validation method
model.error_type.validate_params(result)
updated_gate_error_probabilities[gate_name] = error_func
except Exception as e:
raise ValueError(
f"Gate '{gate_name}' error probability function failed validation: {e}"
) from e
# Update the model with the potentially modified functions
if updated_gate_error_probabilities != model.gate_error_probabilities:
object.__setattr__(
model,
"gate_error_probabilities",
defaultdict(
lambda: lambda _: [0.0] * model.error_type.param_count,
updated_gate_error_probabilities,
),
)
return model
[docs]
@model_validator(mode="after")
@classmethod
def validate_global_time_error_probability_output(cls, model):
"""Validate that global_time_error_probability returns the correct number of
parameters. The previous validators will typically assign default value of [0.0]
to the global time error probability, but some error types may require multiple
parameters (e.g., PAULI_CHANNEL). This validator will reformat the default value
to return a list of zeros with the correct length."""
# Define a dummy zero value for tick_time and global_time in order to test the
# function's behavior.
tick_time = (
0.0
if model.application_mode
in {ApplicationMode.END_OF_TICK, ApplicationMode.IDLE_END_OF_TICK}
else None
)
global_time = 0.0 if model.is_time_dependent else None
try:
result = model.global_time_error_probability(global_time, tick_time)
# Check if this is a default value [0.0] that needs reformatting
if (
len(result) == 1
and result[0] == 0.0
and model.error_type.param_count > 1
):
# Create a new function that returns the correct number of zeros
expected_count = model.error_type.param_count
# pylint: disable=unnecessary-lambda-assignment
if model.is_time_dependent:
new_func = lambda t, _: [0.0] * expected_count
else:
new_func = lambda _, __: [0.0] * expected_count
object.__setattr__(model, "global_time_error_probability", new_func)
else:
# Validate using the error type's validation method
model.error_type.validate_params(result)
except Exception as e:
raise ValueError(
f"global_time_error_probability function failed validation: {e}"
) from e
return model
# ====== Methods =====
[docs]
def get_idle_tick_error_probability(
self, tick_index: int, channel_id: str
) -> list[float] | None:
"""
Get the error probability based on the time a specific channel was idle during a tick.
Parameters
----------
tick_index : int
The index of the tick for which to get the error probability.
channel_id : str
The ID of the channel for which to get the idle time error probability.
Returns
-------
list[float] | None
List of floats representing the error probabilities for the idle tick.
If the application mode is not IDLE_END_OF_TICK, returns None.
"""
if self.application_mode != ApplicationMode.IDLE_END_OF_TICK:
return None
if channel_id not in self._idle_times:
raise ValueError(f"Channel {channel_id} not found in idle times mapping.")
# time dependent, check the tick index
if tick_index < 0 or tick_index >= len(self._tick_durations):
raise IndexError("Tick index out of range.")
# compute the time at which the tick occurs (time at the end of tick)
idle_in_tick = self._idle_times[channel_id][tick_index]
time = sum(self._tick_durations[: tick_index + 1])
p = self.global_time_error_probability(time, idle_in_tick)
# if the error probability is 0, we return None so that it gets ignored instead
if not p or all(x == 0.0 for x in p):
return None
return p
[docs]
def get_tick_error_probability(self, tick_index: int = None) -> list[float] | None:
"""
Get the error probability for a specific tick in the circuit.
Parameters
----------
int
The index of the tick for which to get the error probability.
Returns
-------
list[float] | None
List of floats representing the error probabilities for the tick.
If the application mode is not END_OF_TICK, returns None.
"""
if self.application_mode != ApplicationMode.END_OF_TICK:
return None # Only applicable for END_OF_TICK application mode
if not self.is_time_dependent:
p = self.global_time_error_probability(None, None)
else:
if tick_index is None:
raise ValueError(
"tick_index must be provided for time-dependent models."
)
# time dependent, check the tick index
if tick_index < 0 or tick_index >= len(self._tick_durations):
raise IndexError("Tick index out of range.")
# compute the time at which the tick occurs (time at the end of tick)
time = sum(self._tick_durations[: tick_index + 1])
p = self.global_time_error_probability(
time, self._tick_durations[tick_index]
)
# if the error probability is 0, we return None so that it gets ignored instead
if not p or all(x == 0.0 for x in p):
return None
return p
[docs]
def get_gate_error_probability(self, gate: Circuit) -> list[float] | None:
"""
Get the error type (instruction) and probability for a given gate.
Parameters
----------
Circuit
The quantum gate for which to get the error type and probability.
Returns
-------
list[float] | None
List of floats representing the error probabilities parameters given
the gate name
if the error probability is 0, return None so that it gets ignored
if the application mode is END_OF_TICK, return None
"""
if not isinstance(gate, Circuit):
raise TypeError("gate must be an instance of Circuit")
if gate.circuit != ():
raise ValueError("gate must have empty children (no sub-circuit)")
if self.application_mode == ApplicationMode.END_OF_TICK:
# If the application mode is END_OF_TICK, return None
return None
if self.is_time_dependent:
if gate.id not in self._op_time:
raise ValueError(f"Gate {gate.id} not found in operation time mapping.")
time = self._op_time.get(gate.id)
if self.application_mode == ApplicationMode.AFTER_GATE:
# If the error is applied after the gate, we use the time of the gate.
time = self.gate_durations[gate.name] + time
error_probability = self.gate_error_probabilities[gate.name](time)
else:
error_probability = self.gate_error_probabilities[gate.name](None)
# if the error probability is 0, we return None so that it gets ignored instead
# of being converted to a noise instruction with 0 probability.
if not error_probability or all(x == 0.0 for x in error_probability):
return None
return error_probability
@property
def total_time(self) -> float:
"""
Total time of the circuit, computed as the sum of all tick durations.
Returns
-------
float
The total time of the circuit.
Raises
------
ValueError
If the circuit is not time-dependent or tick durations are not set.
"""
if not self.is_time_dependent or self._tick_durations is None:
raise ValueError(
"Circuit is not time-dependent or tick durations are not set."
)
return sum(self._tick_durations)
# ====== Utilities ======
def _compute__op_times_and__tick_durations(
self,
) -> tuple[dict[str, float], list[float], dict[str, list[float]]]:
"""
Retrieve for each operation, the time elapsed from the start of the circuit until
the start of the operation. Also computes the duration of each tick in the circuit
and the idle time for each channel during each tick.
Returns
-------
tuple[dict[str, float], list[float], dict[str, list[float]]]
A tuple containing:
- dict[str, float]: A dictionary mapping gate IDs to the execution times
(time elapsed before the gate's execution starts).
- list[float]: A list of tick durations for each layer in the circuit.
- dict[str, list[float]]: A dictionary mapping channel IDs to a list of
idle times during each tick.
"""
_op_time = {}
_tick_durations = []
_channel_idle_duration_in_tick = defaultdict(lambda: [])
time_stack = 0
unrolled = Circuit.unroll(self.circuit)
for tick in unrolled:
channel_usage_time = defaultdict(lambda: 0)
for operation in tick:
if operation.name not in self.gate_durations:
raise ValueError(
f"Gate {operation.name} not found in gate_duration dictionary."
)
_op_time[operation.id] = time_stack
for ch in operation.channels:
channel_usage_time[ch.id] += self.gate_durations[operation.name]
_tick_duration = max(
self.gate_durations[op.name] for op in tick if op is not None
)
_tick_durations.append(_tick_duration)
# Store the idle time for each channel in the tick by subtracting the
# total usage time from the tick duration.
for ch in self.circuit.channels:
if ch.is_quantum():
_channel_idle_duration_in_tick[ch.id].append(
_tick_duration - channel_usage_time[ch.id]
)
time_stack += _tick_duration
return _op_time, _tick_durations, _channel_idle_duration_in_tick
[docs]
class HomogeneousTimeIndependentCEM(CircuitErrorModel):
"""
A constant probability error model that applies a fixed error probability to all gates
in the circuit.
This model is not time-dependent, meaning the error probability does not change
over time.
Enforces the following properties:
- The error model is not time-dependent.
Parameters
----------
error_type : ErrorType
The type of error that the model will apply to the circuit.
application_mode : ApplicationMode
The mode in which the error is applied to the circuit.
error_probability : list[float]
The error probability parameter(s) for the error model. This will be assigned
to all target gates.
target_gates : list[str]
A list of gate names to which the error probability applies. Other gates will have
an error probability of 0.0 by default.
"""
is_time_dependent: bool = Field(
init=False, default=False
) # This model is not time-dependent.
# User need to define the error type and application mode
error_type: ErrorType
application_mode: ApplicationMode
error_probability: float | list[float]
global_time_error_probability: ErrorProbProtocol = Field(
default=lambda _, __: [0.0], init=False
)
gate_error_probabilities: dict[str, GateErrorProbProtocol] | None = Field(
default_factory=lambda: defaultdict(lambda: lambda _: [0]), init=False
)
[docs]
@field_validator("error_probability")
@classmethod
def validate_error_probability(cls, v: float | list[float]) -> list[float]:
"""Validate the error probability to ensure it is a list of floats."""
if isinstance(v, float):
return [v]
if isinstance(v, list) and all(isinstance(x, float) for x in v):
return v
raise ValueError("error_probability must be a float or a list of floats.")
# Define the target gates to which the error probability applies. For the rest of the
# gates, the error probability will be 0.0. By default, it applies to nothing
target_gates: list[str] = []
def model_post_init(self, __context):
"""Post-initialization for HomogeneousTimeIndependentCEM to set constant gate errors."""
object.__setattr__(
self,
"gate_error_probabilities",
self.validate_gate_error_probabilities(
{g: lambda _: self.error_probability for g in self.target_gates}
),
)
# set the global time error probability to a constant value.
object.__setattr__(
self,
"global_time_error_probability",
lambda _, __: self.error_probability, # error_probability is already a list
)
super().model_post_init(__context)
[docs]
class HomogeneousTimeDependentCEM(CircuitErrorModel):
"""
A constant probability error model that applies a fixed error probability function
to all gates in the circuit.
Enforces the following properties:
- The error model is time-dependent.
Parameters
----------
error_type : ErrorType
The type of error that the model will apply to the circuit.
application_mode : ApplicationMode
The mode in which the error is applied to the circuit.
error_probability : ErrorProbProtocol
The error probability parameter(s) for the error model. This will be assigned
to all target gates.
target_gates : list[str]
A list of gate names to which the error probability applies. Other gates will have
an error probability of 0.0 by default.
"""
is_time_dependent: bool = Field(
init=False, default=True
) # This model is time-dependent.
# Define the error type and application mode
error_type: ErrorType
application_mode: ApplicationMode
target_gates: list[str] = []
error_probability: ErrorProbProtocol
def model_post_init(self, __context):
"""
Post-initialization for HomogeneousTimeDependentCEM to set constant gate errors.
"""
object.__setattr__(
self,
"gate_error_probabilities",
self.validate_gate_error_probabilities(
{gate: self.error_probability for gate in self.target_gates}
),
)
# Set the global time error probability to a constant value according to the
# given parameters.
object.__setattr__(
self,
"global_time_error_probability",
self.error_probability,
)
super().model_post_init(__context)
[docs]
class AsymmetricDepolarizeCEM(CircuitErrorModel):
"""
Error model that applies asymmetric depolarizing noise to the circuit.
Enforce the following properties:
- The error model is time-dependent.
- The error type is PAULI_CHANNEL.
- The application mode is END_OF_TICK.
Parameters
----------
t1 : float
The time constant for the X and Y errors.
Must be positive.
t2 : float
The time constant for the Z error.
Must be positive.
"""
t1: float
t2: float
gate_error_probabilities: dict[str, GateErrorProbProtocol] | None = Field(
default_factory=lambda: defaultdict(lambda: lambda _: [0]), init=False
)
global_time_error_probability: ErrorProbProtocol = Field(
default=lambda _, __: [0], init=False
)
[docs]
@model_validator(mode="after")
@classmethod
def validate_time_constants(cls, model) -> float:
"""t2 must be smaller or equal 2*t1."""
if model.t2 > 2 * model.t1:
raise ValueError("t2 must be smaller or equal than 2*t1.")
return model
[docs]
@field_validator("t1", "t2")
@classmethod
def check_positive(cls, v, info):
"""Ensure that t1 and t2 are positive."""
if v < 0:
raise ValueError(f"{info.field_name} must be non-negative.")
return v
def _p(self, t: float) -> list[float]:
"""
Internal method to compute error probabilities given time t.
Using the following model:
p_x = p_y = (1 - exp(-t / t1)) / 4
p_z = (1 - exp(-t / t2)) / 2 - p_x
Parameters
----------
t : float
The time used to compute the error probabilities.
Returns
-------
List[float]: [p_x, p_y, p_z]
"""
if self.t1 == 0:
exp_t_t1 = 0
else:
exp_t_t1 = math.exp(-t / self.t1)
p_x = p_y = (1 - exp_t_t1) / 4
if self.t2 == 0:
exp_t_t2 = 0
else:
exp_t_t2 = math.exp(-t / self.t2)
p_z = (1 - exp_t_t2) / 2 - p_x
return [p_x, p_y, p_z]
is_time_dependent: bool = Field(
init=False, default=True
) # This model is time-dependent.
error_type: ErrorType = Field(init=False, default=ErrorType.PAULI_CHANNEL)
application_mode: ApplicationMode = Field(
init=False, default=ApplicationMode.END_OF_TICK
)
def model_post_init(self, __context):
"""
Post-initialization for AsymmetricDepolarizeCEM to set gate error probabilities.
"""
object.__setattr__(
self,
"global_time_error_probability",
lambda _, t: self._p(t),
)
super().model_post_init(__context)