Source code for pyfli.io.processed_data

"""
Load and plot processed outputs from AlliG, BH, and PyFLI pipelines.

This module belongs to :mod:`pyfli.io` and is part of PyFLI detector importers, file
readers, saving helpers, and processed-data loaders. Public API includes classes
:class:`DatasetPlotter`, :class:`AlliGprocessedImport`, :class:`BHprocessedImport`, and
:class:`PyFliprocessedImport`.
"""

import glob
import json
import os
from typing import Any

import matplotlib.pyplot as plt
import numpy as np

from pyfli import logging


[docs] class DatasetPlotter: """ Provide shared plotting helpers for processed dataset importers. It keeps plotting behavior close to the processed-data readers that use it. """ def _plotting_maps(self) -> np.ndarray: """ Run the plotting maps routine. Returns ------- np.ndarray Matplotlib figure or axes containing the rendered result maps. """ if ( not hasattr(self, "dataset") or not self.dataset or not self.dataset["results"]["maps"] ): logging.warning("No maps found to plot.") return map_results = self.dataset["results"]["maps"] name_keys = list(map_results.keys()) num_plots = len(name_keys) fig, axes = plt.subplots(1, num_plots, figsize=(4 * num_plots, 4)) if num_plots == 1: axes = [axes] for i, key in enumerate(name_keys): data = map_results[key] if data.ndim == 2 and data.shape[1] == 2: g_vals, s_vals = data[:, 0], data[:, 1] h = axes[i].hist2d( g_vals, s_vals, bins=256, cmap="jet", range=[[0, 1], [0, 0.6]], cmin=1, ) xc = np.linspace(0, 1, 100) yc = np.sqrt(0.25 - (xc - 0.5) ** 2) axes[i].plot( xc, yc, color="black", linestyle="--", linewidth=1, alpha=0.7 ) axes[i].set_title(f"{self.dataset['name']} - Phasor") plt.colorbar(h[3], ax=axes[i], fraction=0.046, pad=0.04) else: if data.ndim == 1: side = int(np.sqrt(len(data))) data = data[: side * side].reshape((side, side)) im = axes[i].imshow(data, cmap="magma") axes[i].set_title(f"{key}") axes[i].axis("off") plt.colorbar(im, ax=axes[i], fraction=0.046, pad=0.04) plt.tight_layout() plt.show() return fig
[docs] class AlliGprocessedImport(DatasetPlotter): """ Import processed AlliG outputs into PyFLI-style result structures. The class loads saved arrays and exposes them for plotting or downstream comparison. Parameters ---------- folder_path : str Filesystem path used by this workflow. """ def __init__(self, folder_path: str) -> None: self.folder_path = os.path.abspath(folder_path) self.dataset = { "name": os.path.basename(self.folder_path), "results": {"maps": {}, "decay": None, "irf": None}, } def _read_roi_files(self) -> None: """Processes .roiN files using the provided working logic.""" if not os.path.exists(self.folder_path): logging.error(f"Error: The path '{self.folder_path}' does not exist.") return None search_path = os.path.join(self.folder_path, "*.roiN") files = glob.glob(search_path) # Ensure we have a map to get dimensions from if not self.dataset["results"]["maps"]: logging.warning( "Warning: No maps loaded yet. ROI mask cannot determine dimensions. Load text files first." ) return # Get shape from the first available map first_map = next(iter(self.dataset["results"]["maps"].values())) a1, a2 = first_map.shape mask = np.zeros((a1, a2)) for f_n in files: # --- YOUR WORKING CODE --- with open(f_n) as fid: j_data = json.load(fid) n = len(j_data["Named ROI Descriptions"]) for i in range(n): a = j_data["Named ROI Descriptions"][i]["ROI Descriptor"]["Contours"][ 0 ]["Coordinates"] mask[a[1], a[0]] = 1 self.dataset["results"]["maps"]["mask"] = mask # ------------------------- def _detect_fit_type(self, files: np.ndarray) -> bool: """ Run the detect fit type routine. Parameters ---------- files : np.ndarray File list inspected to infer the processed fit type. Returns ------- bool Boolean result computed by detect fit type. """ for f in files: name_lower = os.path.basename(f).lower() if any( indicator in name_lower for indicator in ["tau_1", "tau_2", "f1_a", "f1_i", "a_1", "a_2"] ): return True return False def _read_text_files(self) -> Any: """ Read text files. Returns ------- Any Object produced by read text files. """ if not os.path.exists(self.folder_path): logging.error(f"Error: The path '{self.folder_path}' does not exist.") return None search_path = os.path.join(self.folder_path, "*Map.txt") files = glob.glob(search_path) if not files: logging.info(f"No *Map.txt files found in {self.folder_path}.") return None is_biexp = self._detect_fit_type(files) for file_path in files: full_name = os.path.basename(file_path) name_no_ext = os.path.splitext(full_name)[0] parts = name_no_ext.split(" ") var_name = parts[-2] if is_biexp: biexp_rename = { "A_1 Map": "A1_map", "A_2 Map": "A2_map", "Baseline Map": "baseline_map", "Chi^2 Map": "chi2_map", "f1_a Map": "f1a_map", "f1_i Map": "f1i_map", "f2_a Map": "f2a_map", "f2_i Map": "f2i_map", "Offset Map": "v_shift_map", "R^2 Map": "r2_map", "tau_1 Map": "tau1_map", "tau_2 Map": "tau2_map", "-tau-_a Map": "tau_mean_map", "-tau-_i Map": "taui_mean_map", } try: key_lookup = var_name + " Map" if key_lookup in biexp_rename: data = np.loadtxt(file_path, delimiter="\t") self.dataset["results"]["maps"][biexp_rename[key_lookup]] = data except Exception as e: logging.warning(f"Could not load {full_name}: {e}") else: monoexp_rename = { "A Map": "A_map", "Baseline Map": "baseline_map", "Chi^2 Map": "chi2_map", "Offset Map": "v_shift_map", "R^2 Map": "r2_map", "tau Map": "tau_map", } try: key_lookup = var_name + " Map" if key_lookup in monoexp_rename: data = np.loadtxt(file_path, delimiter="\t") self.dataset["results"]["maps"][monoexp_rename[key_lookup]] = ( data ) except Exception as e: logging.warning(f"Could not load {full_name}: {e}") # Automatically trigger ROI reading AFTER maps are established self._read_roi_files() # Summary Printout loaded_keys = list(self.dataset["results"]["maps"].keys()) logging.info("--- AlliG Import Summary ---") logging.info(f"Folder: {self.dataset['name']}") logging.info(f"Total Maps Loaded: {len(loaded_keys)}") logging.info(f"Map Names: {', '.join(loaded_keys)}") logging.info("----------------------------") return self.dataset
[docs] class BHprocessedImport(DatasetPlotter): """ Import Becker & Hickl processed results for comparison with PyFLI outputs. It adapts BH maps and traces into structures used by the analysis layer. Parameters ---------- folder_path : str Filesystem path used by this workflow. """ def __init__(self, folder_path: str) -> None: self.folder_path = os.path.abspath(folder_path) self.dataset = { "name": os.path.basename(self.folder_path), "results": {"maps": {}, "decay": None, "irf": None}, } def _detect_fit_type(self, files: np.ndarray) -> bool: """ Run the detect fit type routine. Parameters ---------- files : np.ndarray File list inspected to infer the processed fit type. Returns ------- bool Boolean result computed by detect fit type. """ for f in files: name_lower = os.path.basename(f).lower() if "biexp" in name_lower or any( x in name_lower for x in ["_a1", "_a2", "_t1", "_t2"] ): return True return False def _read_ascfiles(self) -> Any: """ Read ascfiles. Returns ------- Any Object produced by read ascfiles. """ if not os.path.exists(self.folder_path): logging.error(f"Error: The path '{self.folder_path}' does not exist.") return None search_path = os.path.join(self.folder_path, "*.asc") files = glob.glob(search_path) if not files: logging.info(f"No .asc files found in {self.folder_path}. Aborting import.") return None is_biexp = self._detect_fit_type(files) biexp_rename = { "a1": "A1_map", "a1[%]": "A1_perc_map", "a2": "A2_map", "a2[%]": "A2_perc_map", "t1": "tau1_map", "t2": "tau2_map", "tm": "tau_mean_map", "offset": "v_shift_map", "chi": "chi2_map", "r2": "r2_map", "phasor": "phasor_map", } mono_rename = { "a": "A_map", "a[%]": "A_perc_map", "t": "tau_map", "offset": "v_shift_map", "chi": "chi2_map", "r2": "r2_map", "phasor": "phasor_map", } special_cases = ["phasor", "irf"] for file_path in files: full_name = os.path.basename(file_path) root_name, _ = os.path.splitext(full_name) name_no_ext = root_name.lower() parts = name_no_ext.split("_") var_suffix = parts[-1] try: if "binned_raw_data" in name_no_ext: raw_cube = np.genfromtxt( file_path, skip_header=11, max_rows=512 * 512, encoding="latin1" ) self.dataset["results"]["decay"] = raw_cube.reshape((512, 512, 256)) continue current_rename = biexp_rename if is_biexp else mono_rename map_names = [ "a1", "a1[%]", "a2", "a2[%]", "chi", "offset", "t1", "t2", "a", "t", ] if var_suffix in map_names: data = np.genfromtxt( file_path, skip_header=10, max_rows=512, encoding="latin1" ) final_name = current_rename.get(var_suffix, var_suffix) self.dataset["results"]["maps"][final_name] = data elif var_suffix in special_cases: if var_suffix == "phasor": phasor_read = np.genfromtxt(file_path, invalid_raise=False) self.dataset["results"]["maps"]["phasor_map"] = phasor_read elif var_suffix == "irf": irf_read = np.genfromtxt(file_path, invalid_raise=False) irf_1d = ( irf_read[:, 1] if irf_read.ndim == 2 else irf_read.flatten() ) self.dataset["results"]["irf"] = np.tile(irf_1d, (512, 512, 1)) except Exception as e: logging.warning(f"Skipping BH file {full_name} due to error: {e}") # Summary Printout loaded_keys = list(self.dataset["results"]["maps"].keys()) logging.info("--- BH Import Summary ---") logging.info(f"Folder: {self.dataset['name']}") logging.info(f"Total Maps Loaded: {len(loaded_keys)}") logging.info(f"Map Names: {', '.join(loaded_keys)}") logging.info("-------------------------") return self.dataset
[docs] class PyFliprocessedImport(DatasetPlotter): """ Reload processed PyFLI outputs from disk. The importer supports downstream plotting and comparison without rerunning the original fitting workflow. """