Source code for qilisdk.noise.representations

# Copyright 2026 Qilimanjaro Quantum Tech
#
# 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.
from __future__ import annotations

from numbers import Real
from typing import TYPE_CHECKING, Callable, Self, Union, cast

import numpy as np
from loguru import logger

from .noise import Noise
from .protocols import AttachmentScope

if TYPE_CHECKING:
    from qilisdk.analog import Hamiltonian
    from qilisdk.core import QTensor

# A rate is either a constant value or a callable evaluated at the simulation time ``t``.
[docs] Rate = Union[float, Callable[[float], float]]
[docs] class KrausChannel(Noise): """Kraus operator representation of a quantum channel.""" def __init__(self, operators: list[QTensor]) -> None: """Args: operators (list[QTensor]): Kraus operators defining the channel.""" self._operators: list[QTensor] = operators logger.debug("[NoiseRepresentations] Built Kraus channel with {} operators", len(operators)) @property
[docs] def operators(self) -> list[QTensor]: """Return the Kraus operators defining the channel. Returns: list[QTensor]: The Kraus operators for this channel. """ return self._operators
[docs] def as_kraus(self) -> Self: return self
@classmethod
[docs] def allowed_scopes(cls) -> frozenset[AttachmentScope]: """ Return the allowed attachment scopes for this noise representation. Returns: frozenset[AttachmentScope]: Allowed attachment scopes. """ return frozenset( { AttachmentScope.GLOBAL, AttachmentScope.PER_QUBIT, AttachmentScope.PER_GATE_TYPE, AttachmentScope.PER_GATE_TYPE_PER_QUBIT, } )
def __repr__(self) -> str: return f"KrausChannel(operators={self._operators})"
[docs] class LindbladGenerator(Noise): """Lindblad generator representation for Markovian noise.""" def __init__( self, jump_operators: list[QTensor], rates: list[Rate] | None = None, hamiltonian: Hamiltonian | None = None, ) -> None: """ Args: jump_operators (list[QTensor]): Jump operators defining dissipation. rates (list[float | Callable[[float], float]] | None): Optional rates for each jump operator. Each rate is either a constant value or a callable ``rate(t)`` that is evaluated at the simulation time ``t`` of an analog evolution, allowing the dissipation strength to vary in time. Time-dependent (callable) rates are only supported by the analog evolution of :class:`~qilisdk.backends.QiliSim` (dense methods); see :attr:`is_time_dependent`. hamiltonian (Hamiltonian | None): Optional Hamiltonian term for coherent evolution. Raises: ValueError: If rates are provided and their length does not match jump_operators, or if any rate is neither a real number nor a callable. """ if rates is not None: if len(rates) != len(jump_operators): raise ValueError("Length of rates must match length of jump_operators.") for rate in rates: if not (isinstance(rate, Real) or callable(rate)): raise ValueError( f"Each rate must be a real number or a callable rate(t) -> float, got {type(rate).__name__}." ) self._jump_operators = jump_operators self._rates = rates self._hamiltonian = hamiltonian logger.debug( "[NoiseRepresentations] Built Lindblad generator with {} jump operators, rates set {}, hamiltonian set {}", len(jump_operators), rates is not None, hamiltonian is not None, ) @property
[docs] def is_time_dependent(self) -> bool: """Whether any rate is a callable evaluated at the simulation time. Returns: bool: ``True`` if at least one rate is a callable ``rate(t)``, ``False`` otherwise. """ if self._rates is None: return False return any(callable(rate) for rate in self._rates)
@property
[docs] def jump_operators(self) -> list[QTensor]: """Return the jump operators defining dissipation. Returns: list[QTensor]: Jump operators for this generator. """ return self._jump_operators
@property
[docs] def jump_operators_with_rates(self) -> list[QTensor]: """Return the jump operators defining dissipation, scaled by their (constant) rates. Raises: ValueError: If the rate list is provided but its length does not match jump_operators. ValueError: If any rate is time-dependent (a callable). Time-dependent rates cannot be statically folded into the operators; the analog evolution of :class:`~qilisdk.backends.QiliSim` consumes :attr:`jump_operators` and :attr:`rates` directly instead. Returns: list[QTensor]: Jump operators for this generator, each scaled by ``sqrt(rate)``. """ if self._rates is None: return self._jump_operators if len(self._rates) != len(self._jump_operators): raise ValueError("Length of rates must match length of jump_operators.") if self.is_time_dependent: raise ValueError("Cannot statically scale jump operators by time-dependent (callable) rates. ") # All rates are constant here (time-dependent rates are rejected above). logger.debug("[NoiseRepresentations] Scaling {} jump operators by their rates", len(self._jump_operators)) return [ self._jump_operators[i] * np.sqrt(cast("float", self._rates[i])) for i in range(len(self._jump_operators)) ]
@property
[docs] def rates(self) -> list[Rate] | None: """Return the rates for each jump operator, if provided. Returns: list[float | Callable[[float], float]] | None: Rates for each jump operator. Each rate is either a constant value or a callable ``rate(t)``. """ return self._rates
@property
[docs] def hamiltonian(self) -> Hamiltonian | None: """Return the optional coherent Hamiltonian term. Returns: QTensor | None: The Hamiltonian term if provided. """ return self._hamiltonian
[docs] def as_lindblad(self) -> LindbladGenerator: """Return this instance as a Lindblad generator representation. Returns: The current LindbladGenerator instance. """ return self
@classmethod
[docs] def allowed_scopes(cls) -> frozenset[AttachmentScope]: """ Return the allowed attachment scopes for this noise representation. Returns: frozenset[AttachmentScope]: Allowed attachment scopes. """ return frozenset({AttachmentScope.GLOBAL, AttachmentScope.PER_QUBIT})
def __repr__(self) -> str: return f"LindbladGenerator(jump_operators={self._jump_operators}, rates={self._rates}, hamiltonian={self._hamiltonian})"