Source code for pyfli.analysis.phasor_analysis

"""
Compute phasor coordinates from binned decay data and save phasor-derived lifetime maps.

This module belongs to :mod:`pyfli.analysis` and is part of PyFLI post-processing,
diagnostics, statistical comparison, and result-loading utilities for fitted FLI/FLIM
datasets. Public API includes functions :func:`compute_freq_axis`,
:func:`compute_phasor`, :func:`plot_phasor_figures`, and :func:`save_phasor_result`.
"""

import os
from typing import Any

import numpy as np

from pyfli import logging

from ..analyticalWorkflow import AnalyticalHelpers, PhasorAnalyzer
from ..data_vnp import ColorProcessor


[docs] def compute_freq_axis( binned_irf: np.ndarray, laser_period_ns: float = 12.5 ) -> tuple[Any, ...]: """Derive frequency and time axis from IRF array shape. Parameters ---------- binned_irf : np.ndarray (H, W, T) laser_period_ns : float default 12.5 ns → 80 MHz Returns ------- freq : tuple from AnalyticalHelpers.freq_computation() freq_hz : float time_axis_ns : np.ndarray length T """ num_gates = binned_irf.shape[2] gate_delay = laser_period_ns / num_gates freq = AnalyticalHelpers( laser_period=laser_period_ns, gate_delay=gate_delay, num_gate=num_gates ).freq_computation() freq_hz = freq[1] * 1e6 time_axis_ns = np.linspace(0, gate_delay * num_gates, num_gates) return freq, freq_hz, time_axis_ns
[docs] def compute_phasor( binned_decay: np.ndarray, binned_irf: np.ndarray, freq_hz: np.ndarray, time_axis_ns: np.ndarray, n_harmonics: int = 3, ) -> tuple[Any, ...]: """Compute and calibrate phasor coordinates. Parameters ---------- binned_decay : np.ndarray (H, W, T) binned_irf : np.ndarray (H, W, T) freq_hz : float time_axis_ns : np.ndarray n_harmonics : int Returns ------- phasor_obj : PhasorAnalyzer reuse this instance for all plotting calls Gc, Sc : np.ndarray (n_harmonics, H, W) calibrated phasor coordinates tau_map_ns : np.ndarray (H, W) apparent lifetime from 1st harmonic """ phasor_obj = PhasorAnalyzer( frequency_hz=freq_hz, time_axis_ns=time_axis_ns, n_harmonics=n_harmonics ) G, S = phasor_obj.create_phasor_gpu(binned_decay) Gc, Sc = phasor_obj.calibrate_pixelwise(G, S, binned_irf) tau_map_ns = phasor_obj.compute_lifetime(Gc[0], Sc[0]) return phasor_obj, Gc, Sc, tau_map_ns
[docs] def plot_phasor_figures( phasor_obj: np.ndarray, Gc: Any, Sc: Any, binned_decay: np.ndarray, mask: np.ndarray, colorset: str = "jet", saver: Any | None = None, ) -> np.ndarray: """Generate all standard phasor figures. Produces: phasor diagram, overlay subplots, harmonics plot (if ≥ 3 harmonics), pure phasor map, and traceable analysis. Parameters ---------- phasor_obj : PhasorAnalyzer returned by compute_phasor() Gc, Sc : np.ndarray (n_harmonics, H, W) binned_decay: np.ndarray (H, W, T) mask : np.ndarray (H, W) bool colorset : str colormap name for hexbin density plots saver : DataSaver or None Returns ------- figs : dict[str, Figure] keyed by figure name """ plasma_m = ColorProcessor().lowest_zero("plasma") viridis_m = ColorProcessor().lowest_zero("viridis") figs = {} figs["phasor_diagram"] = phasor_obj.plot_phasor_diagram( Gc[0], Sc[0], mask=mask, colors=None, hexbin_color=colorset, ax=None, figsize=(8, 3), half_circle=True, title="Phasor Diagram", xlim=(-0.1, 1.1), ylim=(0.0, 0.6), kdeplot=False, kde_color="white", kde_levels=5, kde_linewidths=1, kde_alpha=0.5, ) if saver: saver.save_plot("Phasor_Plot", fig=figs["phasor_diagram"], close=False) figs["phasor_subplots"] = phasor_obj.plot_overlay_subplots( binned_decay, Gc[0], Sc[0], mask=mask, colormaps=[plasma_m, viridis_m], hexbin_color="jet", noise_removed=True, figsize=(10, 8), ) if saver: saver.save_plot("Phasor_subplots", fig=figs["phasor_subplots"], close=False) n_harmonics = Gc.shape[0] if n_harmonics >= 2: figs["phasor_harmonics"] = phasor_obj.plot_phasor_harmonics( Gc, Sc, harmonics=tuple(range(1, n_harmonics + 1)), mask=mask, hexbin_color=colorset, figsize=(14, 4), ) if saver: saver.save_plot( "phasor_harmonics_Plot", fig=figs["phasor_harmonics"], close=False ) figs["phasor_pure_map"] = phasor_obj.plot_pure_phasor_map( Gc[0], Sc[0], binned_decay, noise_removed=True, colormap="viridis" ) figs["phasor_traceable"] = phasor_obj.plot_traceable_analysis( Gc[0], Sc[0], binned_decay, mask=mask, colormap="viridis" ) return figs
[docs] def save_phasor_result( save_dir: str, tau_map_ns: np.ndarray, saver: Any | None = None ) -> np.ndarray: """ Save phasor result. Parameters ---------- save_dir : str Directory where outputs are saved. tau_map_ns : np.ndarray Lifetime map in nanoseconds. saver : Any | None Optional saver used to persist messages or figures. Returns ------- np.ndarray Saved phasor result arrays or metadata. """ result = { "results": { "maps": { "tau_map": tau_map_ns, "mean_lifetime": tau_map_ns, }, "TR_maps": {}, } } path = os.path.join(save_dir, "phasor_tau_map.npy") np.save(path, result) if saver: saver.log("Phasor tau map saved as phasor_tau_map.npy") logging.info(f"Phasor result saved → {path}") return path