"""
Load saved PyFLI fitting sessions and inject derived analysis results.
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:`load_session_arrays`,
:func:`scan_session_results`, :func:`load_fitting_results`,
:func:`save_laguerre_result`, :func:`inject_phasor_result`, and
:func:`add_mean_lifetime`.
"""
import os
from typing import Any
import numpy as np
from pyfli import logging
# Files that are NOT fitting results and should be excluded from scan output
_NON_RESULT_FILES = {
"clean_decay.npy",
"clean_irf.npy",
"final_mask.npy",
"decay_raw.npy",
"irf_raw.npy",
"pixel_invariant_irf.npy",
"pf_mask.npy",
}
# Expected naming conventions for result files.
# Use these constants (or save_laguerre_result / compute_fbi_results) to ensure
# load_fitting_results() and scan_session_results() can find them.
RESULT_FILENAMES = {
"laguerre_mono": "Laguerre Results_mono-exponential.npy",
"laguerre_bi": "Laguerre Results_bi-exponential.npy",
"fbi_bi": "F-BI Output_bi-exponential.npy",
"phasor": "phasor_tau_map.npy",
}
[docs]
def load_session_arrays(save_dir: str) -> tuple[Any, ...]:
"""Load clean_decay, clean_irf, and final_mask from a pf_Analysis session directory.
Returns
-------
decay : np.ndarray (H, W, T)
irf : np.ndarray (H, W, T)
mask : np.ndarray (H, W) bool
"""
decay = np.load(os.path.join(save_dir, "clean_decay.npy"), allow_pickle=True)
irf = np.load(os.path.join(save_dir, "clean_irf.npy"), allow_pickle=True)
mask = np.load(os.path.join(save_dir, "final_mask.npy"), allow_pickle=True).astype(
bool
)
return decay, irf, mask
[docs]
def scan_session_results(save_dir: str) -> np.ndarray:
"""
Scan session results.
Parameters
----------
save_dir : str
Directory where outputs are saved.
Returns
-------
np.ndarray
Session result arrays discovered from the output folder.
"""
all_npy = sorted(f for f in os.listdir(save_dir) if f.endswith(".npy"))
result_files = [f for f in all_npy if f not in _NON_RESULT_FILES]
logging.info(f"Available fitting results in '{save_dir}':")
if result_files:
for i, fname in enumerate(result_files):
logging.info(f" [{i}] {fname}")
else:
logging.info(" (none found)")
return result_files
[docs]
def load_fitting_results(save_dir: str, experiments: np.ndarray) -> tuple[Any, ...]:
"""Load fitting results using a user-defined filename → label mapping.
Parameters
----------
save_dir : str
Path to the pf_Analysis session folder.
experiments : dict[str, str]
Maps each .npy filename to a short display label.
You control exactly which results are loaded and in what order.
Mix any model types freely (NLSF, MLE, Laguerre, FBI, etc.).
Example — mono-exponential, CPU only::
experiments = {
'CPU_NLSF_least_squares_mono-exponential.npy': 'NLSF',
'CPU_MLE_poisson_mono-exponential.npy': 'MLE',
'Laguerre Results_mono-exponential.npy': 'Laguerre',
}
Example — bi-exponential, selective::
experiments = {
'CPU_NLSF_least_squares_bi-exponential.npy': 'NLSF-bi',
'GPU_MLE_poisson_bi-exponential.npy': 'MLE-GPU-bi',
}
Returns
-------
all_datasets : list[dict] parameter maps (tau_map, alpha1_map, ...)
all_fitset : list[dict] TR maps (fit_map, residual_map)
names : list[str] labels matching each entry, in dict order
"""
all_datasets, all_fitset, names = [], [], []
for file_name, label in experiments.items():
file_path = os.path.join(save_dir, file_name)
if not os.path.exists(file_path):
logging.warning(
f"[load_fitting_results] Skipping missing file: {file_name}"
)
continue
var = np.load(file_path, allow_pickle=True).item()
all_datasets.append(var["results"]["maps"])
all_fitset.append(var["results"]["TR_maps"])
names.append(label)
if not all_datasets:
raise FileNotFoundError(
f"No fitting result files were found in '{save_dir}'. "
"Run scan_session_results(save_dir) to see what is available."
)
return all_datasets, all_fitset, names
[docs]
def save_laguerre_result(saver: Any, lag_results: np.ndarray, model_type: str) -> None:
"""
Save laguerre result.
Parameters
----------
saver : Any
Optional saver used to persist messages or figures.
lag_results : np.ndarray
Laguerre deconvolution results written into the saver.
model_type : str
FLI/FLIM model family, such as mono- or bi-exponential.
Returns
-------
None
No object is returned; the function save laguerre result.
"""
key = f"laguerre_{model_type.split('-')[0]}" # 'laguerre_mono' or 'laguerre_bi'
if key not in RESULT_FILENAMES:
raise ValueError(
f"model_type must be 'mono-exponential' or 'bi-exponential', got '{model_type}'"
)
fname = RESULT_FILENAMES[key].replace(".npy", "")
saver.save_npy(fname, lag_results)
saver.log(f"Laguerre {model_type} results saved as {fname}.npy")
[docs]
def inject_phasor_result(
tau_map_ns: np.ndarray,
all_datasets: np.ndarray,
all_fitset: np.ndarray,
names: Any,
label: str = "Phasor",
) -> None:
"""
Inject phasor result.
Parameters
----------
tau_map_ns : np.ndarray
Lifetime map in nanoseconds.
all_datasets : np.ndarray
Collection of fitted datasets to classify, compare, or summarize.
all_fitset : np.ndarray
Collection of fit-result dictionaries used for comparison or plotting.
names : Any
Dataset names used in summaries and plots.
label : str
Display label assigned to the data or plot element.
Returns
-------
None
No object is returned; the function inject phasor result.
"""
all_datasets.append(
{
"tau_map": tau_map_ns,
"mean_lifetime": tau_map_ns,
}
)
all_fitset.append({})
names.append(label)
[docs]
def add_mean_lifetime(all_datasets: np.ndarray) -> None:
"""
Add mean lifetime.
Parameters
----------
all_datasets : np.ndarray
Collection of fitted datasets to classify, compare, or summarize.
Returns
-------
None
No object is returned; the function add mean lifetime.
"""
for ds in all_datasets:
if "mean_lifetime" in ds:
continue
if not {"alpha1_map", "tau1_map", "tau2_map"}.issubset(ds):
continue
ds["mean_lifetime"] = (
ds["alpha1_map"] * ds["tau1_map"] + ds["alpha2_map"] * ds["tau2_map"]
if "alpha2_map" in ds
else ds["alpha1_map"] * ds["tau1_map"]
+ (1 - ds["alpha1_map"]) * ds["tau2_map"]
)