Author: Mykyta Kizilov
Affiliation: Advanced Spectroscopy Laboratory, Texas A&M University
This repository contains Python scripts for performing reconvolution curve fitting using both custom NNLS-based and lmfit-based methods. These scripts are designed to analyze time-resolved fluorescence decay data by fitting exponential decay models convolved with an instrument response function (IRF).
- Parse single FLIM
.ascfiles and ordered sets (e.g., time-series cycles). - Baseline removal and optional Savitzky–Golay smoothing.
- Consistent trimming, time-zero alignment, and IRF normalization.
- Fast FFT-based convolution and sub-channel IRF shifting.
- NNLS amplitude estimation with non-negative constraint and DC offset.
- Global search of decay constants (taus) + IRF shift via Differential Evolution.
- Diagnostics: χ² (reduced), residuals, component curves, IRF overlay.
- Batch histograms of metrics (peak, FWHM, integral) with “bad-bin” interpolation.
- Publication-ready plots (log-scale photons).
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -U numpy pandas matplotlib scipyOptional: pin versions for reproducibility (e.g.,
numpy>=1.24,scipy>=1.11,matplotlib>=3.7,pandas>=2.0).
- File format: ASCII
.ascwith tab separator and header line containing
*BLOCK 1 ( Time[ns] No_of_photons ). - Columns after parsing:
Time(ns),Photons(counts). - Series naming: If using
parse_FLIM_set, filenames should end with cycle tag like...c12.asc(the code sorts by the trailing number matched withr'c(\d+)\.asc$').
import os
from pathlib import Path
# 1) Load a signal/IRF pair
signal = parse_FLIM("data/my_sample_signal.asc")
irf = parse_FLIM("data/my_sample_irf.asc")
# 2) Prepare (trim zero regions, align time-zero, match lengths, normalize IRF)
sig_prep, irf_prep = prepare_data(signal, irf, window_length=7, polyorder=2)
# 3) Fit a bi-exponential with bounded taus and an IRF shift search
taus, amps, irf_shift, offset, chi2_red = reconvolution_fit(
FLIM_data=[sig_prep, irf_prep],
exp_num=2,
tau_bounds=[(0.02, 0.5), (0.5, 5.0)], # ns
irf_shift_bounds=[-50, 50], # in channels
smooth=None,
maxiter=1500,
workers=1,
plot_title="Bi-exponential reconvolution",
filename="fit_bi_exp"
)
print("taus (ns):", taus)
print("amps (photons):", amps)
print("IRF shift (channels):", irf_shift)
print("DC offset (photons):", offset)
print("reduced chi2:", chi2_red)Batch parsing & overview plots:
dfs = parse_FLIM_set(directory="data/time_series", file_startswith="sample_")
plot_FLIM_spectrum(dfs, min_scale=1, title="Time-series photon decays")
plot_histogram(dfs, what_to_plot='fwhm', min_scale=1, plot_name='FWHM over minutes')- Parse the raw
.ascfiles →parse_FLIM/parse_FLIM_set. - (Optional) Smooth for visualization →
apply_savgol. - Prepare signal/IRF (trim zeros, align time zero, equalize length, normalize IRF) →
prepare_data. - Fit reconvolution model (global DE for taus + IRF shift; NNLS for non-negative amplitudes + DC) →
reconvolution_fit. - Inspect plots & residuals →
plot_reconvolution. - Aggregate metrics over many cycles →
plot_histogram.
-
parse_FLIM(filename) -> pd.DataFrame
Reads a single.ascinto columnsTime,Photons. Drops non-numeric/infinite entries and sorts by time. -
parse_FLIM_set(directory, file_startswith) -> list[pd.DataFrame]
Loads and sorts multiple.ascfiles using the cycle number matched byc(\d+)before.asc. -
apply_savgol(df, window_length=5, polyorder=3, title=None) -> pd.DataFrame
Savitzky–Golay smooth for visualization; overlays original vs. smoothed on log-y plot. -
prepare_data(df_signal, df_irf, window_length=None, polyorder=None) -> (signal, irf)
Trims leading/trailing zeros, optional rolling mean smoothing, shifts both tot=0, matches lengths, normalizes IRF area to 1.
-
convolve(x, h)
Linear convolution via FFT, returns length ofx(signal-length crop). -
find_irf_shift(signal, irf) -> int
Coarse shift estimate from argmax indices. -
shift_irf(irf, irf_shift)
Fractional-channel circular shift with linear interpolation between neighboring samples. -
nnls_convol_irfexp(x_data, irf, p0, signal_values)
Builds design matrix[IRF⊗exp(-t/τ₁), …, IRF⊗exp(-t/τ_K), 1], solvesNNLSfor amplitudes and DC. -
residual_function(params, x_data, irf_values, y_data)
Givenparams = [irf_shift, τ₁..τ_K], computes reduced χ² using Poisson-like weighting1/√(fit+1). -
reconvolution_fit(FLIM_data, exp_num=1, tau_bounds=None, irf_shift_bounds=[-500,500], smooth=None, maxiter=1000, disp=False, workers=1, plot_title=None, filename=None)
Global optimization (Differential Evolution) over IRF shift and τs, then NNLS amplitudes + DC; returns
(taus_sorted, amplitudes_sorted, irf_shift_opt, dc_offset, reduced_chi2)and optionally plots.
-
plot_FLIM_spectrum(dataframes, min_scale=1, title="Spectra Plot")
Overlay one or many decays (log-y), with a colorbar for series. -
plot_histogram(dataframes, what_to_plot='max_photons'|'fwhm'|'integral', min_scale=1, bad_bins=None, window_length=11, polyorder=2, plot_name='Plot')
Batch compute metric per file and bar-plot across cycles; interpolate specifiedbad_bins. -
plot_reconvolution(times, signal, fit, irf, irf_shift, taus, amplitudes, plot_title, filename=None)
Two-panel figure: log-scale fits with IRF overlays + residuals; reports χ²_ν and parameters. Saves toplots/filename.pngif provided.
-
Model:
$y(t) \approx \big(\sum_k a_k e^{-t/\tau_k}\big) \ast \text{IRF}(t - \Delta) + c$ , with$a_k \ge 0$ ,$c \ge 0$ . - Why NNLS? Enforces physical non-negativity of components and DC background; avoids sign-flip artifacts.
-
Why Differential Evolution? The objective is non-convex in
$\tau_k$ and IRF shift; DE provides robust global search before amplitude estimation. -
Weights: Residuals are scaled by
$\sqrt{\text{fit}+1}$ to approximate Poisson statistics in photon counting.
- Taus & bounds: Provide problem-specific bounds in ns (e.g.,
[ (0.05,0.5), (0.5,5.0) ]). Too-wide bounds slow convergence. - IRF shift bounds: Expressed in channels/samples, not ns. Convert using your instrument’s bin width.
- Smoothing: Use only for visualization (
apply_savgol) or gentle rolling means duringprepare_data. Do not over-smooth prior to fitting. - Baseline/DC: The NNLS model includes a constant offset; no external baseline subtraction required for fitting.
- Data trimming:
prepare_dataauto-trims zero regions (leading/trailing). Ensure the peak is retained. - Parallelism:
workersinreconvolution_fitpasses through to SciPy DE; chooseworkers=-1for all cores (SciPy ≥1.9). - Reproducibility: Set
np.random.seed(...)before calling DE if you need repeatable runs (SciPy accepts aseedviadifferential_evolution(..., seed=...)—add if needed).
taus, amps, shift, dc, chi2 = reconvolution_fit(
[signal, irf],
exp_num=3,
tau_bounds=[(0.05,0.5), (0.5,3.0), (3.0,12.0)],
irf_shift_bounds=[-40, 40],
maxiter=2000,
workers=-1,
plot_title="Tri-exponential reconvolution"
)- “ValueError: Unknown plot option” → Use one of
max_photons | fwhm | integral. - Flat lines / NaNs after parsing → Check that the file contains the required
*BLOCK 1...header and tab separators. - Exploding χ² or noisy residuals → Narrow
tau_bounds; verify IRF is from the same setup and not saturated; considersmooth=Noneduring fitting. - Negative or zero times → Allowed; plots and fitting use time differences post-alignment.
- IRF much shorter than signal →
prepare_datamatches lengths by truncation; supply equally sampled inputs where possible.
project/
├─ data/
│ ├─ my_sample_signal.asc
│ └─ my_sample_irf.asc
├─ plots/ # created on save
└─ analysis.ipynb # your scripts/notebooks
- Add per-component fractional contributions:
f_k = a_k τ_k / Σ_j a_j τ_j. - Add multi-ROI or pixelwise fitting: vectorize
nnls_convol_irfexpand cache IRF⊗exp design matrices. - Expose
seedarg toreconvolution_fitfor reproducibility. - Support time-bin width (Δt) to convert channel shift ↔ ns.
See LICENSE.
- I/O:
parse_FLIM,parse_FLIM_set,extract_cycle_number - Prep:
apply_savgol,prepare_data - Maths:
convolve,find_irf_shift,shift_irf,exp_decay,sci_notation - Core fit:
nnls_convol_irfexp,model_func,residual_function,reconvolution_fit - Metrics/plots:
calculate_fwhm,calculate_integral,plot_FLIM_spectrum,plot_histogram,plot_reconvolution
Ready-to-use outcome: Load your .asc signal and IRF, run prepare_data, call reconvolution_fit with realistic tau_bounds and irf_shift_bounds, and inspect the plot + χ²_ν. That’s the full reconvolution loop.