Source code for pyfli.simulator.distributions

#  simulator/distributions.py

"""
Sample detector, noise, beta, and truncated-normal parameters for simulations.

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:`ParameterSampler`.
"""

from typing import Any

import numpy as np
from scipy.stats import truncnorm


[docs] class ParameterSampler: """ Sample physically plausible detector and lifetime parameters for simulation workflows. Static methods cover quantum efficiency, noise parameters, beta draws, truncated normals, and interval stretching. """
[docs] @staticmethod def sample_qe(sensor_type: str = "continuous", rng: Any | None = None) -> Any: """Samples QE based on sensor type.""" _r = rng or np.random if sensor_type.upper() == "CONTINUOUS": return _r.uniform(0.15, 0.35) # Typical continuous (ICCD) QE return _r.uniform(0.70, 0.90) # Typical discrete (photon-counter) QE
[docs] @staticmethod def sample_noise_params( bit_depth: int, sensor_type: str = "continuous", rng: Any | None = None ) -> dict[Any, Any]: """Centralized control for hardware noise levels.""" _r = rng or np.random if sensor_type.upper() == "CONTINUOUS": # Read noise is a fixed electronic property (electrons RMS), independent of bit depth read_sigma = _r.uniform(1.0, 3.0) return {"read_sigma": read_sigma} return { "read_sigma": 0.0 } # discrete (photon-counter) sensors effectively have zero read noise
[docs] @staticmethod def sample_beta( alpha: float, beta: float, scale: float = 1.0, offset: float = 0.0, rng: Any | None = None, ) -> Any: """Standard beta sampling with scale and offset.""" _r = rng or np.random val = _r.beta(alpha, beta) return (val * scale) + offset
[docs] @staticmethod def beta_sample( alpha: float, beta: float, scale: float = 1.0, clip_eps: float = 1e-4, rng: Any | None = None, ) -> np.ndarray: """Your specific photon-count beta sampling logic.""" _r = rng or np.random val = _r.beta(alpha, beta) return np.clip(val * scale, clip_eps, scale - clip_eps)
[docs] @staticmethod def truncated_normal( mu: float, sigma: float, lower: float = 0.01, upper: float = 5.0 ) -> Any: """Fixed: Now takes mu and sigma as separate arguments.""" a, b = (lower - mu) / sigma, (upper - mu) / sigma return truncnorm.rvs(a, b, loc=mu, scale=sigma)
[docs] @staticmethod def stretch_squeeze(sample: Any, epsilon: Any) -> Any: """Maps [0,1] to [epsilon, 1-epsilon].""" return sample * (1.0 - 2.0 * epsilon) + epsilon