Source code for pyfli.phasor.phasorSEPL.locus

"""
locus.py
========
Build Single-Exponential Phasor Locus (SEPL) arrays — the parametric curve
traced in the (g, s) plane as τ sweeps from 0 to ∞.

The term "SEPL" (pronounced *sepal*) was introduced by Michalet 2021 for all
such curves, whether they are the canonical universal semicircle or deformed
variants arising from gating / binning / truncation.
"""

from typing import Any

import numpy as np
from numpy.typing import NDArray

from .config import AcquisitionConfig
from .phasors import phasor_from_config

# ──────────────────────────────────────────────────────────────────────────────
# tau grid
# ──────────────────────────────────────────────────────────────────────────────


[docs] def tau_grid(cfg: AcquisitionConfig, power: float = 1.5) -> NDArray[np.float64]: """ Return a non-uniform τ grid in [tau_min_ns, tau_max_ns]. The grid is denser at small τ (where the locus curves sharply near (1,0)) using a power-law spacing. Parameters ---------- cfg : AcquisitionConfig power : float Exponent for the nonlinear spacing. 1.0 = linear; >1 = finer at small τ. Returns ------- tau : 1-D ndarray (length = cfg.n_tau_pts) """ u = np.linspace(0.0, 1.0, cfg.n_tau_pts) return cfg.tau_min_ns + (cfg.tau_max_ns - cfg.tau_min_ns) * u**power
# ────────────────────────────────────────────────────────────────────────────── # main builder # ──────────────────────────────────────────────────────────────────────────────
[docs] def build_locus( cfg: AcquisitionConfig, *, tau: NDArray[np.float64] | None = None, filter_finite: bool = True, ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: """ Compute the full SEPL for the given acquisition configuration. Parameters ---------- cfg : AcquisitionConfig Fully describes the experiment (mode, T, harmonic, gates, …). tau : 1-D array, optional Custom τ grid in ns. If *None*, ``tau_grid(cfg)`` is used. filter_finite : bool If True (default), remove any (g, s) pairs where either component is non-finite (NaN / ±Inf). Useful for modes that become undefined at τ → 0 for certain parameter sets. Returns ------- g : 1-D ndarray Real phasor coordinate. s : 1-D ndarray Imaginary phasor coordinate. tau : 1-D ndarray Corresponding lifetime values in ns. """ if tau is None: tau = tau_grid(cfg) g, s = phasor_from_config(tau, cfg) if filter_finite: mask = np.isfinite(g) & np.isfinite(s) g, s, tau = g[mask], s[mask], tau[mask] return g, s, tau
# ────────────────────────────────────────────────────────────────────────────── # multi-config convenience # ──────────────────────────────────────────────────────────────────────────────
[docs] def build_loci( cfgs: list[AcquisitionConfig], **kwargs: Any, ) -> list[tuple[NDArray, NDArray, NDArray]]: """ Compute SEPL curves for a list of configs in one call. Useful for overlaying several acquisition scenarios on the same plot. Parameters ---------- cfgs : list[AcquisitionConfig] kwargs : passed verbatim to ``build_locus``. Returns ------- list of (g, s, tau) tuples, same order as *cfgs*. """ return [build_locus(c, **kwargs) for c in cfgs]
# ────────────────────────────────────────────────────────────────────────────── # circle geometry helpers (universal semicircle reference) # ──────────────────────────────────────────────────────────────────────────────
[docs] def universal_semicircle(n_pts: int = 300) -> tuple[NDArray, NDArray]: """ Return (g, s) coordinates of the universal semicircle. The universal semicircle is defined by (g − ½)² + s² = (½)², s ≥ 0 and is parameterised by the angle θ ∈ [0, π]: g = ½ + ½·cos(θ), s = ½·sin(θ) Parameters ---------- n_pts : int Number of samples (default 300). Returns ------- g, s : 1-D ndarray """ theta = np.linspace(0.0, np.pi, n_pts) g = 0.5 + 0.5 * np.cos(theta) s = 0.5 * np.sin(theta) return g, s
[docs] def sepl_center_radius_discrete( cfg: AcquisitionConfig, ) -> tuple[float, float, float]: """ Analytically compute the circle centre (gc, sc) and radius r of the discrete SEPL L_N (Michalet 2021, Sec. III C 1). For a binned PSED with N bins and harmonic n, the phasor traces: z_N(τ) = (1 − x) / (1 − x·e^{iφ}) where x = e^{−T/(N·τ)} and φ = 2π·n/N. This Möbius transform of the real interval (0, 1) traces an arc of a circle with parameters (derived analytically by equating coefficients in the expanded circle equation): gc = 1/2 sc = −(1/2) · tan(φ/2) r = √(gc² + sc²) = (1/2) / cos(φ/2) The arc converges to the universal semicircle as N → ∞ (φ → 0). Parameters ---------- cfg : AcquisitionConfig Requires: N_bins, harmonic. Returns ------- gc, sc, r : float Circle centre coordinates and radius. """ phi = 2.0 * np.pi * cfg.harmonic / cfg.N_bins half_phi = phi / 2.0 if abs(np.cos(half_phi)) < 1e-12: # Degenerate case (e.g. N=2, n=1): the arc degenerates return 0.5, 0.0, 0.5 gc = 0.5 sc = -0.5 * np.tan(half_phi) r = np.sqrt(gc**2 + sc**2) # = 0.5 / cos(phi/2) return gc, sc, r