Source code for pyfli.simulator.noise_models

# simulator/noise_models.py

"""
Apply Poisson, dark-count, read-noise, jitter, and pile-up models to simulated decays.

This module belongs to :mod:`pyfli.simulator` and is part of PyFLI synthetic FLI/FLIM
data generation, hardware noise modeling, calibration, and validation tools. Public API
includes classes :class:`NoiseEngine`.
"""

from typing import Any

import numpy as np


[docs] class NoiseEngine: """ Apply detector noise transformations to clean simulated decays. Static methods model Poisson counting, dark counts, read noise, temporal jitter, and simple TCSPC pile-up filtering. """
[docs] @staticmethod def apply_poisson(clean_signal: np.ndarray) -> Any: """ Apply poisson. Parameters ---------- clean_signal : np.ndarray Noise-free simulated signal before detector noise is applied. Returns ------- Any Object produced by apply poisson. """ return np.random.poisson(np.clip(clean_signal, 0, None)).astype(np.float64)
[docs] @staticmethod def apply_dcr(decay: np.ndarray, dcr_level: float = 0.5) -> Any: """ Simulates Dark Count Rate (thermal noise). dcr_level: average dark photons per bin per measurement. """ dark_noise = np.random.poisson(dcr_level, size=decay.shape) return decay + dark_noise
[docs] @staticmethod def apply_read_noise(decay: np.ndarray, sigma_read: float = 1.5) -> Any: """ Simulates electronic read noise (Gaussian). Common in ICCD sensors during CCD readout. """ read_noise = np.random.normal(0, sigma_read, size=decay.shape) return decay + read_noise
[docs] @staticmethod def apply_jitter(decay: np.ndarray, max_shift: int = 2) -> np.ndarray: """ Apply jitter. Parameters ---------- decay : np.ndarray Time-resolved decay signal or decay cube. max_shift : int Maximum random temporal jitter shift in bins. Returns ------- np.ndarray Decay array after timing jitter has been applied. """ n = len(decay) shift = np.random.randint(-max_shift, max_shift + 1) if shift == 0: return decay if shift > 0: return np.concatenate([np.zeros(shift), decay[: n - shift]]) return np.concatenate([decay[-shift:], np.zeros(-shift)])
[docs] @staticmethod def tcspc_pileup_filter( arrival_times: np.ndarray, t_rep: float, mode: str = "wrap" ) -> Any: # Simplistic pile-up: real systems might only take the first photon per cycle """ Run the TCSPC pileup filter routine. Parameters ---------- arrival_times : np.ndarray Photon arrival times passed to the TCSPC pile-up filter. t_rep : float Laser repetition period used for TCSPC pile-up filtering. mode : str 'wrap' (default) folds photons back via modulo; 'truncate' drops them. Returns ------- Any Object produced by TCSPC pileup filter. """ if mode == "truncate": return arrival_times[arrival_times < t_rep] if mode == "wrap": return arrival_times % t_rep raise ValueError(f"mode must be 'truncate' or 'wrap', got {mode!r}")