flimkit 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flimkit/FLIM/__init__.py +0 -0
- flimkit/FLIM/assemble.py +254 -0
- flimkit/FLIM/batch.py +681 -0
- flimkit/FLIM/bg_tools.py +51 -0
- flimkit/FLIM/fit_tools.py +244 -0
- flimkit/FLIM/fitters.py +1471 -0
- flimkit/FLIM/irf_tools.py +617 -0
- flimkit/FLIM/models.py +391 -0
- flimkit/GPU/__init__.py +85 -0
- flimkit/GPU/_base.py +391 -0
- flimkit/GPU/cuda.py +10 -0
- flimkit/GPU/mlx_backend.py +381 -0
- flimkit/GPU/mps.py +10 -0
- flimkit/GPU/rocm.py +10 -0
- flimkit/GPU/torch_backend.py +385 -0
- flimkit/UI/app_state.py +10 -0
- flimkit/UI/controller.py +139 -0
- flimkit/UI/expert_settings.py +248 -0
- flimkit/UI/fit_help.py +206 -0
- flimkit/UI/fov_preview.py +1085 -0
- flimkit/UI/gui.py +3919 -0
- flimkit/UI/icon.icns +0 -0
- flimkit/UI/icon.ico +0 -0
- flimkit/UI/icon.png +0 -0
- flimkit/UI/irf_widget.py +103 -0
- flimkit/UI/mode_controller.py +118 -0
- flimkit/UI/modes/__init__.py +0 -0
- flimkit/UI/modes/base.py +3 -0
- flimkit/UI/modes/batch_mode.py +312 -0
- flimkit/UI/modes/fov_mode.py +164 -0
- flimkit/UI/modes/irf_mode.py +80 -0
- flimkit/UI/modes/phasor_mode.py +131 -0
- flimkit/UI/modes/stitch_mode.py +254 -0
- flimkit/UI/phasor_panel.py +1087 -0
- flimkit/UI/progress_window.py +113 -0
- flimkit/UI/project_panel.py +262 -0
- flimkit/UI/results_panel.py +332 -0
- flimkit/UI/roi_tools.py +794 -0
- flimkit/UI/utils.py +217 -0
- flimkit/__init__.py +0 -0
- flimkit/_version.py +41 -0
- flimkit/cli.py +120 -0
- flimkit/configs.py +148 -0
- flimkit/dialogs.py +46 -0
- flimkit/formats/BH/__init__.py +0 -0
- flimkit/formats/BH/reader.py +296 -0
- flimkit/formats/BH/writer.py +86 -0
- flimkit/formats/ISS/__init__.py +0 -0
- flimkit/formats/ISS/fdflim.py +86 -0
- flimkit/formats/ISS/image.py +114 -0
- flimkit/formats/ISS/reader.py +223 -0
- flimkit/formats/PS/__init__.py +0 -0
- flimkit/formats/PS/reader.py +202 -0
- flimkit/formats/PTU/__init__.py +0 -0
- flimkit/formats/PTU/decode.py +27 -0
- flimkit/formats/PTU/phu.py +85 -0
- flimkit/formats/PTU/reader.py +235 -0
- flimkit/formats/PTU/series.py +258 -0
- flimkit/formats/PTU/stitch.py +1182 -0
- flimkit/formats/PTU/tools.py +94 -0
- flimkit/formats/__init__.py +2 -0
- flimkit/formats/flim_file.py +232 -0
- flimkit/formats/phasor.py +132 -0
- flimkit/formats/signal.py +170 -0
- flimkit/image/tools.py +124 -0
- flimkit/interactive.py +1857 -0
- flimkit/mpl_backend.py +22 -0
- flimkit/phasor/__init__.py +40 -0
- flimkit/phasor/filters.py +127 -0
- flimkit/phasor/fret.py +654 -0
- flimkit/phasor/interactive.py +556 -0
- flimkit/phasor/peaks.py +186 -0
- flimkit/phasor/signal.py +90 -0
- flimkit/phasor_launcher.py +314 -0
- flimkit/plugins/__init__.py +137 -0
- flimkit/plugins/bindings.py +116 -0
- flimkit/plugins/builtin/__init__.py +3 -0
- flimkit/plugins/builtin/core_tools.py +28 -0
- flimkit/plugins/loader.py +371 -0
- flimkit/plugins/registry.py +406 -0
- flimkit/project.py +197 -0
- flimkit/synth.py +145 -0
- flimkit/utils/__init__.py +0 -0
- flimkit/utils/batch_fit.py +301 -0
- flimkit/utils/config_manager.py +119 -0
- flimkit/utils/config_snapshot.py +30 -0
- flimkit/utils/crash_handler.py +183 -0
- flimkit/utils/display.py +197 -0
- flimkit/utils/enhanced_outputs.py +345 -0
- flimkit/utils/fancy.py +103 -0
- flimkit/utils/lifetime_image.py +243 -0
- flimkit/utils/misc.py +111 -0
- flimkit/utils/plotting.py +190 -0
- flimkit/utils/roi.py +370 -0
- flimkit/utils/session.py +51 -0
- flimkit/utils/update_check.py +198 -0
- flimkit/utils/xlsx_tools.py +97 -0
- flimkit/utils/xml_utils.py +219 -0
- flimkit-0.12.0.dist-info/METADATA +356 -0
- flimkit-0.12.0.dist-info/RECORD +104 -0
- flimkit-0.12.0.dist-info/WHEEL +5 -0
- flimkit-0.12.0.dist-info/entry_points.txt +2 -0
- flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
- flimkit-0.12.0.dist-info/top_level.txt +1 -0
flimkit/FLIM/batch.py
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import shutil
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib
|
|
5
|
+
matplotlib.use('Agg')
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from flimkit.formats import FLIMFile
|
|
9
|
+
from ..FLIM.fitters import fit_summed, fit_per_pixel
|
|
10
|
+
from ..FLIM.fit_tools import find_irf_peak_bin
|
|
11
|
+
from ..utils.lifetime_image import make_lifetime_image, make_component_rgb_tiff
|
|
12
|
+
from ..utils.plotting import plot_summed
|
|
13
|
+
from ..configs import (
|
|
14
|
+
MIN_PHOTONS_PERPIX, lm_restarts, de_population, de_maxiter, n_workers,
|
|
15
|
+
)
|
|
16
|
+
from ..utils.batch_fit import (
|
|
17
|
+
group_timelapse_files, group_zstack_files, zstack_group_label,
|
|
18
|
+
pool_decays, build_irf, make_synthetic_popt, compute_redox_metrics,
|
|
19
|
+
save_json, save_series_csv, resolve_tau_display_range,
|
|
20
|
+
save_tile_lifetime_txt, save_map_stacks, plot_metric_summary,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def _save_4d_stacks(frame_positions, group_dir, group_label):
|
|
24
|
+
sorted_t = sorted(frame_positions.keys())
|
|
25
|
+
all_s = sorted({s for pos in frame_positions.values() for s in pos})
|
|
26
|
+
for s in all_s:
|
|
27
|
+
save_map_stacks([group_dir / f't{t:04d}' / f's{s}' for t in sorted_t],
|
|
28
|
+
group_dir, f'{group_label}_s{s}')
|
|
29
|
+
|
|
30
|
+
def _iter_effective_groups(groups, pool_positions, output_dir):
|
|
31
|
+
for (region, z), frame_positions in groups.items():
|
|
32
|
+
if pool_positions:
|
|
33
|
+
group_label = f'{region}_z{z}'
|
|
34
|
+
group_dir = output_dir / group_label
|
|
35
|
+
yield group_label, group_dir, frame_positions
|
|
36
|
+
else:
|
|
37
|
+
all_s = sorted({s for pos in frame_positions.values() for s in pos})
|
|
38
|
+
for s in all_s:
|
|
39
|
+
group_label = f'{region}_z{z}_s{s}'
|
|
40
|
+
group_dir = output_dir / group_label
|
|
41
|
+
fp_single = {
|
|
42
|
+
t: {s: pos[s]}
|
|
43
|
+
for t, pos in frame_positions.items()
|
|
44
|
+
if s in pos
|
|
45
|
+
}
|
|
46
|
+
yield group_label, group_dir, fp_single
|
|
47
|
+
|
|
48
|
+
def fit_timelapse(ptu_dir, output_dir, args,
|
|
49
|
+
ref_tau1_ns=None, ref_tau2_ns=None, ref_tau3_ns=None,
|
|
50
|
+
channel=None,
|
|
51
|
+
pool_positions=False,
|
|
52
|
+
compute_bound_fraction=False,
|
|
53
|
+
progress_callback=None,
|
|
54
|
+
cancel_event=None):
|
|
55
|
+
ptu_dir = Path(ptu_dir)
|
|
56
|
+
output_dir = Path(output_dir)
|
|
57
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
groups = group_timelapse_files(ptu_dir)
|
|
59
|
+
if not groups:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f'No timelapse PTU files found in {ptu_dir}. '
|
|
62
|
+
'Expected pattern: region_tX[_sY][_zZ].ptu')
|
|
63
|
+
print(f'\nTimelapse groups found:')
|
|
64
|
+
for (region, z), frame_positions in groups.items():
|
|
65
|
+
n_t = len(frame_positions)
|
|
66
|
+
all_s = sorted({s for pos in frame_positions.values() for s in pos})
|
|
67
|
+
t_list = sorted(frame_positions)
|
|
68
|
+
print(f' region={region} z={z} → {n_t} timepoints '
|
|
69
|
+
f'(t={t_list[0]}…{t_list[-1]}) × {len(all_s)} position(s) '
|
|
70
|
+
f'(s={all_s})')
|
|
71
|
+
print(f' Position handling: {'pooled (shared τ)' if pool_positions else 'independent (per-position τ)'}')
|
|
72
|
+
total_frames = sum(
|
|
73
|
+
sum(len(pos) for pos in fp.values())
|
|
74
|
+
for fp in groups.values())
|
|
75
|
+
step = 0
|
|
76
|
+
all_results = {}
|
|
77
|
+
for group_label, group_dir, frame_positions in _iter_effective_groups(
|
|
78
|
+
groups, pool_positions, output_dir):
|
|
79
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
80
|
+
print(' Cancelled.')
|
|
81
|
+
break
|
|
82
|
+
group_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
all_s = sorted({s for pos in frame_positions.values() for s in pos})
|
|
84
|
+
n_t = len(frame_positions)
|
|
85
|
+
print(f'\n{'='*60}')
|
|
86
|
+
print(f' GROUP: {group_label} '
|
|
87
|
+
f'({n_t} timepoints × {len(all_s)} position(s))')
|
|
88
|
+
print(f'{'='*60}')
|
|
89
|
+
n_ts = sum(len(pos) for pos in frame_positions.values())
|
|
90
|
+
print(f'\n[1] Pooling photons across {n_ts} PTU files…')
|
|
91
|
+
pooled_decay, tcspc_res, n_bins = pool_decays(frame_positions, channel=channel)
|
|
92
|
+
print(f' Total photons: {pooled_decay.sum():,.0f}')
|
|
93
|
+
irf_prompt, has_tail, fit_bg, fit_sigma, sigma_max = build_irf(
|
|
94
|
+
pooled_decay, tcspc_res, n_bins, args)
|
|
95
|
+
ref_taus = [ref_tau1_ns, ref_tau2_ns, ref_tau3_ns][:args.nexp]
|
|
96
|
+
use_supplied = all(r is not None for r in ref_taus)
|
|
97
|
+
if use_supplied:
|
|
98
|
+
print(f'\n[2] Using user-supplied τ: '
|
|
99
|
+
+ ' '.join(f'τ{i+1}={t} ns' for i, t in enumerate(ref_taus)))
|
|
100
|
+
irf_peak_bin = int(find_irf_peak_bin(pooled_decay))
|
|
101
|
+
global_popt = make_synthetic_popt(
|
|
102
|
+
ref_taus, args.nexp, n_bins,
|
|
103
|
+
irf_peak_bin, fit_sigma, fit_bg, has_tail)
|
|
104
|
+
taus_ns = np.array(ref_taus)
|
|
105
|
+
global_summary = {
|
|
106
|
+
'taus_ns': taus_ns,
|
|
107
|
+
'amps': np.ones(args.nexp) / args.nexp,
|
|
108
|
+
'tau_mean_amp_ns': float(np.mean(taus_ns)),
|
|
109
|
+
}
|
|
110
|
+
else:
|
|
111
|
+
print(f'\n[2] Fitting reference τ from pooled decay ({args.nexp}-exp)…')
|
|
112
|
+
t0 = time.time()
|
|
113
|
+
global_popt, global_summary = fit_summed(
|
|
114
|
+
pooled_decay, tcspc_res, n_bins,
|
|
115
|
+
irf_prompt, has_tail, fit_bg, fit_sigma,
|
|
116
|
+
args.nexp, args.tau_min, args.tau_max,
|
|
117
|
+
optimizer=getattr(args, 'optimizer', 'de'),
|
|
118
|
+
n_restarts=getattr(args, 'restarts', lm_restarts),
|
|
119
|
+
de_popsize=getattr(args, 'de_population', de_population),
|
|
120
|
+
de_maxiter=getattr(args, 'de_maxiter', de_maxiter),
|
|
121
|
+
workers=getattr(args, 'workers', n_workers),
|
|
122
|
+
polish=not getattr(args, 'no_polish', False),
|
|
123
|
+
cost_function=getattr(args, 'cost_function', 'poisson'),
|
|
124
|
+
sigma_max=sigma_max,
|
|
125
|
+
)
|
|
126
|
+
print(f' Reference fit: {time.time() - t0:.1f} s')
|
|
127
|
+
taus_ns = global_summary.get('taus_ns', global_popt[:args.nexp] * 1e9)
|
|
128
|
+
for i, tau in enumerate(taus_ns):
|
|
129
|
+
print(f' τ{i+1} = {tau:.4f} ns (locked for all frames)')
|
|
130
|
+
tau_disp_min, tau_disp_max = resolve_tau_display_range(taus_ns, args)
|
|
131
|
+
print(f' Lifetime display range (fixed for all timepoints): '
|
|
132
|
+
f'{tau_disp_min:.3f}-{tau_disp_max:.3f} ns')
|
|
133
|
+
save_json(group_dir / f'{group_label}_reference_fit.json', {
|
|
134
|
+
'taus_ns': list(taus_ns),
|
|
135
|
+
'nexp': args.nexp,
|
|
136
|
+
'tau_min_ns': args.tau_min,
|
|
137
|
+
'tau_max_ns': args.tau_max,
|
|
138
|
+
'total_pooled_photons': float(pooled_decay.sum()),
|
|
139
|
+
'tcspc_res_s': float(tcspc_res),
|
|
140
|
+
'n_bins': int(n_bins),
|
|
141
|
+
'n_timepoints': n_t,
|
|
142
|
+
'positions': all_s,
|
|
143
|
+
'estimate_irf': getattr(args, 'estimate_irf', 'gaussian'),
|
|
144
|
+
'user_supplied_tau': use_supplied,
|
|
145
|
+
'calibrated_chi2_pearson': global_summary.get(
|
|
146
|
+
'calibrated_chi2_pearson'),
|
|
147
|
+
'calibrated_chi2_tail_pearson': global_summary.get(
|
|
148
|
+
'calibrated_chi2_tail_pearson'),
|
|
149
|
+
})
|
|
150
|
+
print(f'\n[3] Per-frame per-position fitting (α free, τ locked)…')
|
|
151
|
+
per_pos_series = {s: {} for s in all_s}
|
|
152
|
+
for t, positions in sorted(frame_positions.items()):
|
|
153
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
154
|
+
break
|
|
155
|
+
frame_dir = group_dir / f't{t:04d}'
|
|
156
|
+
frame_dir.mkdir(exist_ok=True)
|
|
157
|
+
for s, ptu_path in sorted(positions.items()):
|
|
158
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
159
|
+
break
|
|
160
|
+
step += 1
|
|
161
|
+
if progress_callback is not None:
|
|
162
|
+
progress_callback(step, total_frames)
|
|
163
|
+
print(f'\n t={t} s={s}: {ptu_path.name}')
|
|
164
|
+
t_start = time.time()
|
|
165
|
+
ptu = FLIMFile(str(ptu_path), verbose=False)
|
|
166
|
+
pixel_stack = ptu.raw_pixel_stack(channel=channel)
|
|
167
|
+
if pixel_stack.shape[2] != n_bins:
|
|
168
|
+
nb = pixel_stack.shape[2]
|
|
169
|
+
if nb > n_bins:
|
|
170
|
+
pixel_stack = pixel_stack[:, :, :n_bins]
|
|
171
|
+
else:
|
|
172
|
+
pixel_stack = np.pad(
|
|
173
|
+
pixel_stack, ((0, 0), (0, 0), (0, n_bins - nb)))
|
|
174
|
+
pixel_maps = fit_per_pixel(
|
|
175
|
+
pixel_stack.astype(np.float32),
|
|
176
|
+
tcspc_res, n_bins,
|
|
177
|
+
irf_prompt, has_tail, fit_bg, fit_sigma,
|
|
178
|
+
global_popt, args.nexp,
|
|
179
|
+
min_photons=getattr(args, 'min_photons', MIN_PHOTONS_PERPIX),
|
|
180
|
+
tau_min_ns=args.tau_min,
|
|
181
|
+
tau_max_ns=args.tau_max,
|
|
182
|
+
correct_pileup=getattr(args, 'correct_pileup', False),
|
|
183
|
+
n_sync=getattr(ptu, 'n_sync', None),
|
|
184
|
+
fit_idx=global_summary.get('fit_idx'),
|
|
185
|
+
progress_callback=None,
|
|
186
|
+
free_tau=False,
|
|
187
|
+
)
|
|
188
|
+
redox = compute_redox_metrics(pixel_maps, args.nexp,
|
|
189
|
+
compute_bound_fraction=compute_bound_fraction)
|
|
190
|
+
pos_dir = frame_dir / f's{s}'
|
|
191
|
+
pos_dir.mkdir(exist_ok=True)
|
|
192
|
+
intensity = pixel_maps.get('intensity', pixel_stack.sum(axis=2))
|
|
193
|
+
np.save(str(pos_dir / 'intensity.npy'), intensity.astype(np.float32))
|
|
194
|
+
for map_name in ('alpha_1', 'alpha_2', 'alpha_3', 'tau_mean_amp',
|
|
195
|
+
'tau_mean_int', 'chi2_r', 'calibrated_chi2_r'):
|
|
196
|
+
if pixel_maps.get(map_name) is not None:
|
|
197
|
+
np.save(str(pos_dir / f'{map_name}.npy'),
|
|
198
|
+
pixel_maps[map_name].astype(np.float32))
|
|
199
|
+
for map_name, arr in redox.items():
|
|
200
|
+
np.save(str(pos_dir / f'{map_name}.npy'), arr)
|
|
201
|
+
roi_name = f'{group_label}_t{t:04d}_s{s}'
|
|
202
|
+
if getattr(args, 'save_lifetime', True):
|
|
203
|
+
try:
|
|
204
|
+
make_lifetime_image(
|
|
205
|
+
canvas=pixel_maps, output_dir=pos_dir, roi_name=roi_name,
|
|
206
|
+
tau_min_ns=tau_disp_min, tau_max_ns=tau_disp_max,
|
|
207
|
+
intensity_percentile_hi=95, tau_key='tau_mean_int', verbose=False,
|
|
208
|
+
)
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
print(f' Warning: lifetime image export failed for {roi_name}: {exc}')
|
|
211
|
+
finally:
|
|
212
|
+
plt.close('all')
|
|
213
|
+
try:
|
|
214
|
+
save_tile_lifetime_txt(
|
|
215
|
+
pos_dir / f'{roi_name}_lifetime.txt', taus_ns, pixel_maps)
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
print(f' Warning: lifetime .txt export failed for {roi_name}: {exc}')
|
|
218
|
+
try:
|
|
219
|
+
tile_decay = pixel_stack.sum(axis=(0, 1)).astype(np.float64)
|
|
220
|
+
tile_popt, tile_summary = fit_summed(
|
|
221
|
+
tile_decay, tcspc_res, n_bins,
|
|
222
|
+
irf_prompt, has_tail, fit_bg, fit_sigma,
|
|
223
|
+
args.nexp, args.tau_min, args.tau_max,
|
|
224
|
+
optimizer=getattr(args, 'optimizer', 'de'),
|
|
225
|
+
n_restarts=getattr(args, 'restarts', lm_restarts),
|
|
226
|
+
de_popsize=getattr(args, 'de_population', de_population),
|
|
227
|
+
de_maxiter=getattr(args, 'de_maxiter', de_maxiter),
|
|
228
|
+
workers=getattr(args, 'workers', n_workers),
|
|
229
|
+
polish=not getattr(args, 'no_polish', False),
|
|
230
|
+
cost_function=getattr(args, 'cost_function', 'poisson'),
|
|
231
|
+
sigma_max=sigma_max,
|
|
232
|
+
)
|
|
233
|
+
plot_summed(
|
|
234
|
+
tile_decay, tile_summary, ptu, None,
|
|
235
|
+
args.nexp, getattr(args, 'estimate_irf', 'gaussian'),
|
|
236
|
+
str(pos_dir / roi_name), irf_prompt=irf_prompt,
|
|
237
|
+
)
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
print(f' Warning: per-tile detail fit plot failed for {roi_name}: {exc}')
|
|
240
|
+
finally:
|
|
241
|
+
plt.close('all')
|
|
242
|
+
if getattr(args, 'save_rgb', True):
|
|
243
|
+
try:
|
|
244
|
+
make_component_rgb_tiff(
|
|
245
|
+
canvas=pixel_maps, output_dir=pos_dir, roi_name=roi_name,
|
|
246
|
+
n_exp=args.nexp, intensity_percentile_hi=95, verbose=False,
|
|
247
|
+
)
|
|
248
|
+
except Exception as exc:
|
|
249
|
+
print(f' Warning: component RGB TIFF export failed for {roi_name}: {exc}')
|
|
250
|
+
if getattr(args, 'save_intensity', True):
|
|
251
|
+
try:
|
|
252
|
+
import tifffile as _tifffile
|
|
253
|
+
int_max_disp = getattr(args, 'intensity_display_max', None)
|
|
254
|
+
i_max = float(int_max_disp) if int_max_disp is not None \
|
|
255
|
+
else float(np.percentile(intensity[intensity > 0], 99.0)
|
|
256
|
+
if (intensity > 0).any() else 1.0)
|
|
257
|
+
i_max = max(i_max, 1e-6)
|
|
258
|
+
intensity_u16 = np.clip(
|
|
259
|
+
intensity.astype(np.float64) / i_max * 65535, 0, 65535
|
|
260
|
+
).astype(np.uint16)
|
|
261
|
+
_tifffile.imwrite(str(pos_dir / f'{roi_name}_intensity.tif'), intensity_u16)
|
|
262
|
+
except Exception as exc:
|
|
263
|
+
print(f' Warning: intensity TIFF export failed for {roi_name}: {exc}')
|
|
264
|
+
if getattr(args, 'save_ind', False):
|
|
265
|
+
try:
|
|
266
|
+
from ..utils.enhanced_outputs import save_individual_tau_maps
|
|
267
|
+
save_individual_tau_maps(
|
|
268
|
+
pixel_maps, pos_dir, roi_name=roi_name, n_exp=args.nexp)
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
print(f' Warning: individual component map export failed for {roi_name}: {exc}')
|
|
271
|
+
stats = {'t': t, 's': s, 'path': str(ptu_path)}
|
|
272
|
+
tau_map = redox.get('tau_mean')
|
|
273
|
+
if tau_map is None:
|
|
274
|
+
tau_map = pixel_maps.get('tau_mean_amp')
|
|
275
|
+
if tau_map is not None:
|
|
276
|
+
valid = tau_map[np.isfinite(tau_map) & (tau_map > 0)]
|
|
277
|
+
stats['tau_mean_mean'] = float(np.mean(valid)) if valid.size > 0 else float('nan')
|
|
278
|
+
stats['tau_mean_std'] = float(np.std(valid)) if valid.size > 0 else float('nan')
|
|
279
|
+
if 'bound_fraction' in redox:
|
|
280
|
+
bf = redox['bound_fraction']
|
|
281
|
+
valid_bf = bf[np.isfinite(bf)]
|
|
282
|
+
stats['bound_fraction_mean'] = (
|
|
283
|
+
float(np.mean(valid_bf)) if valid_bf.size > 0 else float('nan'))
|
|
284
|
+
stats['bound_fraction_std'] = (
|
|
285
|
+
float(np.std(valid_bf)) if valid_bf.size > 0 else float('nan'))
|
|
286
|
+
n_fitted = int(np.sum(
|
|
287
|
+
np.isfinite(pixel_maps.get('tau_mean_amp', np.array([np.nan])))))
|
|
288
|
+
stats['n_pixels_fitted'] = n_fitted
|
|
289
|
+
for i, tau in enumerate(taus_ns):
|
|
290
|
+
stats[f'tau{i+1}_ns'] = float(tau)
|
|
291
|
+
def _map_mean(name, require_positive=False):
|
|
292
|
+
m = pixel_maps.get(name)
|
|
293
|
+
if m is None:
|
|
294
|
+
return None
|
|
295
|
+
ok = np.isfinite(m)
|
|
296
|
+
if require_positive:
|
|
297
|
+
ok = ok & (m > 0)
|
|
298
|
+
vals = m[ok]
|
|
299
|
+
return float(np.mean(vals)) if vals.size > 0 else float('nan')
|
|
300
|
+
for i in range(args.nexp):
|
|
301
|
+
am = _map_mean(f'alpha_{i+1}')
|
|
302
|
+
if am is not None:
|
|
303
|
+
stats[f'alpha_{i+1}_mean'] = am
|
|
304
|
+
chi_mean = _map_mean('chi2_r', require_positive=True)
|
|
305
|
+
if chi_mean is not None:
|
|
306
|
+
stats['chi2_r_mean'] = chi_mean
|
|
307
|
+
elapsed = time.time() - t_start
|
|
308
|
+
print(f' τ_mean={stats.get('tau_mean_mean', float('nan')):.4f} ns '
|
|
309
|
+
f'bound_frac={stats.get('bound_fraction_mean', float('nan')):.4f} '
|
|
310
|
+
f'n_px={n_fitted:,} ({elapsed:.1f} s)')
|
|
311
|
+
per_pos_series[s][t] = stats
|
|
312
|
+
if getattr(args, 'save_stack', True):
|
|
313
|
+
print(f'\n[4] Saving 4D stacks…')
|
|
314
|
+
_save_4d_stacks(frame_positions, group_dir, group_label)
|
|
315
|
+
if not getattr(args, 'save_npy', True):
|
|
316
|
+
for t in frame_positions:
|
|
317
|
+
for s in frame_positions[t]:
|
|
318
|
+
pos_dir = group_dir / f't{t:04d}' / f's{s}'
|
|
319
|
+
for f_ in pos_dir.glob('*.npy'):
|
|
320
|
+
try:
|
|
321
|
+
f_.unlink(missing_ok=True)
|
|
322
|
+
except Exception as exc:
|
|
323
|
+
print(f' Warning: could not remove {f_}: {exc}')
|
|
324
|
+
if not getattr(args, 'no_plots', False):
|
|
325
|
+
print(f'\n[5] Saving summary plot…')
|
|
326
|
+
plot_metric_summary(
|
|
327
|
+
{f's{s}': v for s, v in per_pos_series.items()},
|
|
328
|
+
group_dir / f'{group_label}_timeseries.png',
|
|
329
|
+
group_label, 'Timepoint')
|
|
330
|
+
for s, s_series in per_pos_series.items():
|
|
331
|
+
save_series_csv(
|
|
332
|
+
s_series,
|
|
333
|
+
group_dir / f'{group_label}_s{s}_timeseries.csv',
|
|
334
|
+
index_name='t', drop_keys=('path',))
|
|
335
|
+
print(f' Saved CSV: {group_label}_s{s}_timeseries.csv')
|
|
336
|
+
_json_keys = (['tau_mean_mean', 'tau_mean_std',
|
|
337
|
+
'bound_fraction_mean', 'bound_fraction_std',
|
|
338
|
+
'n_pixels_fitted', 'chi2_r_mean']
|
|
339
|
+
+ [f'tau{i+1}_ns' for i in range(args.nexp)]
|
|
340
|
+
+ [f'alpha_{i+1}_mean' for i in range(args.nexp)])
|
|
341
|
+
save_json(group_dir / f'{group_label}_timeseries.json', {
|
|
342
|
+
'positions': all_s,
|
|
343
|
+
'timepoints': sorted(frame_positions.keys()),
|
|
344
|
+
'per_position': {
|
|
345
|
+
str(s): {
|
|
346
|
+
't': sorted(s_data.keys()),
|
|
347
|
+
**{k: [s_data.get(t, {}).get(k) for t in sorted(s_data)]
|
|
348
|
+
for k in _json_keys}
|
|
349
|
+
}
|
|
350
|
+
for s, s_data in per_pos_series.items()
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
all_results[(region, z)] = {
|
|
354
|
+
'group_dir': str(group_dir),
|
|
355
|
+
'n_timepoints': n_t,
|
|
356
|
+
'positions': all_s,
|
|
357
|
+
'taus_ns': list(taus_ns),
|
|
358
|
+
'per_position_series': per_pos_series,
|
|
359
|
+
}
|
|
360
|
+
print(f'\n Group {group_label} done.')
|
|
361
|
+
print(f'\n{'='*60}')
|
|
362
|
+
print(f' TIMELAPSE COMPLETE → {output_dir}')
|
|
363
|
+
print(f'{'='*60}\n')
|
|
364
|
+
return all_results
|
|
365
|
+
|
|
366
|
+
def fit_zstack(ptu_dir, output_dir, args,
|
|
367
|
+
ref_tau1_ns=None, ref_tau2_ns=None, ref_tau3_ns=None,
|
|
368
|
+
channel=None,
|
|
369
|
+
compute_bound_fraction=False,
|
|
370
|
+
progress_callback=None,
|
|
371
|
+
cancel_event=None):
|
|
372
|
+
ptu_dir = Path(ptu_dir)
|
|
373
|
+
output_dir = Path(output_dir)
|
|
374
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
375
|
+
groups = group_zstack_files(ptu_dir)
|
|
376
|
+
if not groups:
|
|
377
|
+
raise ValueError(
|
|
378
|
+
f'No z-stack PTU files found in {ptu_dir}. '
|
|
379
|
+
'Expected pattern: region_zX.ptu (optionally region_tX_sY_zX.ptu)')
|
|
380
|
+
print(f'\nZ-stack groups found:')
|
|
381
|
+
for (region, t, s), zslices in groups.items():
|
|
382
|
+
z_list = sorted(zslices)
|
|
383
|
+
print(f' region={region} t={t} s={s} → {len(z_list)} slices '
|
|
384
|
+
f'(z={z_list[0]}…{z_list[-1]})')
|
|
385
|
+
total_slices = sum(len(z) for z in groups.values())
|
|
386
|
+
step = 0
|
|
387
|
+
all_results = {}
|
|
388
|
+
for (region, t, s), zslices in groups.items():
|
|
389
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
390
|
+
print(' Cancelled.')
|
|
391
|
+
break
|
|
392
|
+
group_label = zstack_group_label(region, t, s)
|
|
393
|
+
group_dir = output_dir / group_label
|
|
394
|
+
group_dir.mkdir(parents=True, exist_ok=True)
|
|
395
|
+
preview_dir = group_dir / 'lifetime_preview'
|
|
396
|
+
preview_dir.mkdir(exist_ok=True)
|
|
397
|
+
n_z = len(zslices)
|
|
398
|
+
print(f'\n{'='*60}')
|
|
399
|
+
print(f' STACK: {group_label} ({n_z} z-slices)')
|
|
400
|
+
print(f'{'='*60}')
|
|
401
|
+
print(f'\n[1] Pooling photons across {n_z} z-slices (one FOV)…')
|
|
402
|
+
pool_input = {z: {0: p} for z, p in zslices.items()}
|
|
403
|
+
pooled_decay, tcspc_res, n_bins = pool_decays(pool_input, channel=channel)
|
|
404
|
+
print(f' Total photons: {pooled_decay.sum():,.0f}')
|
|
405
|
+
irf_prompt, has_tail, fit_bg, fit_sigma, sigma_max = build_irf(
|
|
406
|
+
pooled_decay, tcspc_res, n_bins, args)
|
|
407
|
+
ref_taus = [ref_tau1_ns, ref_tau2_ns, ref_tau3_ns][:args.nexp]
|
|
408
|
+
use_supplied = all(r is not None for r in ref_taus)
|
|
409
|
+
if use_supplied:
|
|
410
|
+
print(f'\n[2] Using user-supplied τ: '
|
|
411
|
+
+ ' '.join(f'τ{i+1}={t_} ns' for i, t_ in enumerate(ref_taus)))
|
|
412
|
+
irf_peak_bin = int(find_irf_peak_bin(pooled_decay))
|
|
413
|
+
global_popt = make_synthetic_popt(
|
|
414
|
+
ref_taus, args.nexp, n_bins,
|
|
415
|
+
irf_peak_bin, fit_sigma, fit_bg, has_tail)
|
|
416
|
+
taus_ns = np.array(ref_taus)
|
|
417
|
+
global_summary = {'taus_ns': taus_ns, 'n_exp': args.nexp}
|
|
418
|
+
else:
|
|
419
|
+
print(f'\n[2] Fitting reference τ from pooled z-stack ({args.nexp}-exp)…')
|
|
420
|
+
t0 = time.time()
|
|
421
|
+
global_popt, global_summary = fit_summed(
|
|
422
|
+
pooled_decay, tcspc_res, n_bins,
|
|
423
|
+
irf_prompt, has_tail, fit_bg, fit_sigma,
|
|
424
|
+
args.nexp, args.tau_min, args.tau_max,
|
|
425
|
+
optimizer=getattr(args, 'optimizer', 'de'),
|
|
426
|
+
n_restarts=getattr(args, 'restarts', lm_restarts),
|
|
427
|
+
de_popsize=getattr(args, 'de_population', de_population),
|
|
428
|
+
de_maxiter=getattr(args, 'de_maxiter', de_maxiter),
|
|
429
|
+
workers=getattr(args, 'workers', n_workers),
|
|
430
|
+
polish=not getattr(args, 'no_polish', False),
|
|
431
|
+
cost_function=getattr(args, 'cost_function', 'poisson'),
|
|
432
|
+
sigma_max=sigma_max,
|
|
433
|
+
)
|
|
434
|
+
print(f' Reference fit: {time.time() - t0:.1f} s')
|
|
435
|
+
taus_ns = global_summary.get('taus_ns', global_popt[:args.nexp] * 1e9)
|
|
436
|
+
for i, tau in enumerate(taus_ns):
|
|
437
|
+
print(f' τ{i+1} = {tau:.4f} ns (locked for whole stack)')
|
|
438
|
+
tau_disp_min, tau_disp_max = resolve_tau_display_range(taus_ns, args)
|
|
439
|
+
print(f' Lifetime display range (fixed for all slices): '
|
|
440
|
+
f'{tau_disp_min:.3f}-{tau_disp_max:.3f} ns')
|
|
441
|
+
save_json(group_dir / f'{group_label}_reference_fit.json', {
|
|
442
|
+
'taus_ns': list(taus_ns),
|
|
443
|
+
'nexp': args.nexp,
|
|
444
|
+
'tau_min_ns': args.tau_min,
|
|
445
|
+
'tau_max_ns': args.tau_max,
|
|
446
|
+
'total_pooled_photons': float(pooled_decay.sum()),
|
|
447
|
+
'tcspc_res_s': float(tcspc_res),
|
|
448
|
+
'n_bins': int(n_bins),
|
|
449
|
+
'n_slices': n_z,
|
|
450
|
+
'z_slices': sorted(zslices),
|
|
451
|
+
'estimate_irf': getattr(args, 'estimate_irf', 'gaussian'),
|
|
452
|
+
'user_supplied_tau': use_supplied,
|
|
453
|
+
'calibrated_chi2_pearson': global_summary.get(
|
|
454
|
+
'calibrated_chi2_pearson'),
|
|
455
|
+
'calibrated_chi2_tail_pearson': global_summary.get(
|
|
456
|
+
'calibrated_chi2_tail_pearson'),
|
|
457
|
+
})
|
|
458
|
+
n_ref = min(len(pooled_decay), n_bins)
|
|
459
|
+
ref_time_ns = (np.arange(n_ref) + 0.5) * tcspc_res * 1e9
|
|
460
|
+
ref_model = global_summary.get('model')
|
|
461
|
+
np.savez(
|
|
462
|
+
str(group_dir / 'reference_decay.npz'),
|
|
463
|
+
decay=np.asarray(pooled_decay[:n_ref], dtype=np.float64),
|
|
464
|
+
time_ns=ref_time_ns.astype(np.float64),
|
|
465
|
+
model=(np.asarray(ref_model[:n_ref], dtype=np.float64)
|
|
466
|
+
if ref_model is not None else np.array([], dtype=np.float64)),
|
|
467
|
+
irf_prompt=(np.asarray(irf_prompt[:n_ref], dtype=np.float64)
|
|
468
|
+
if irf_prompt is not None else np.array([], dtype=np.float64)),
|
|
469
|
+
taus_ns=np.asarray(list(taus_ns), dtype=np.float64),
|
|
470
|
+
reduced_chi2_tail=np.asarray(
|
|
471
|
+
[global_summary.get('reduced_chi2_tail', float('nan'))], dtype=np.float64),
|
|
472
|
+
calibrated_chi2_pearson=np.asarray(
|
|
473
|
+
[global_summary.get('calibrated_chi2_pearson', float('nan'))],
|
|
474
|
+
dtype=np.float64),
|
|
475
|
+
calibrated_chi2_tail_pearson=np.asarray(
|
|
476
|
+
[global_summary.get('calibrated_chi2_tail_pearson', float('nan'))],
|
|
477
|
+
dtype=np.float64),
|
|
478
|
+
)
|
|
479
|
+
print(f'\n[3] Per-slice per-pixel fitting (α free, τ locked)…')
|
|
480
|
+
z_series = {}
|
|
481
|
+
for z, ptu_path in sorted(zslices.items()):
|
|
482
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
483
|
+
break
|
|
484
|
+
step += 1
|
|
485
|
+
if progress_callback is not None:
|
|
486
|
+
progress_callback(step, total_slices)
|
|
487
|
+
print(f'\n z={z}: {ptu_path.name}')
|
|
488
|
+
t_start = time.time()
|
|
489
|
+
slice_dir = group_dir / f'z{z:04d}'
|
|
490
|
+
slice_dir.mkdir(exist_ok=True)
|
|
491
|
+
ptu = FLIMFile(str(ptu_path), verbose=False)
|
|
492
|
+
pixel_stack = ptu.raw_pixel_stack(channel=channel)
|
|
493
|
+
if pixel_stack.shape[2] != n_bins:
|
|
494
|
+
nb = pixel_stack.shape[2]
|
|
495
|
+
if nb > n_bins:
|
|
496
|
+
pixel_stack = pixel_stack[:, :, :n_bins]
|
|
497
|
+
else:
|
|
498
|
+
pixel_stack = np.pad(
|
|
499
|
+
pixel_stack, ((0, 0), (0, 0), (0, n_bins - nb)))
|
|
500
|
+
pixel_maps = fit_per_pixel(
|
|
501
|
+
pixel_stack.astype(np.float32),
|
|
502
|
+
tcspc_res, n_bins,
|
|
503
|
+
irf_prompt, has_tail, fit_bg, fit_sigma,
|
|
504
|
+
global_popt, args.nexp,
|
|
505
|
+
min_photons=getattr(args, 'min_photons', MIN_PHOTONS_PERPIX),
|
|
506
|
+
tau_min_ns=args.tau_min,
|
|
507
|
+
tau_max_ns=args.tau_max,
|
|
508
|
+
correct_pileup=getattr(args, 'correct_pileup', False),
|
|
509
|
+
n_sync=getattr(ptu, 'n_sync', None),
|
|
510
|
+
fit_idx=global_summary.get('fit_idx'),
|
|
511
|
+
progress_callback=None,
|
|
512
|
+
free_tau=False,
|
|
513
|
+
)
|
|
514
|
+
redox = compute_redox_metrics(pixel_maps, args.nexp,
|
|
515
|
+
compute_bound_fraction=compute_bound_fraction)
|
|
516
|
+
intensity = pixel_maps.get('intensity', pixel_stack.sum(axis=2))
|
|
517
|
+
np.save(str(slice_dir / 'intensity.npy'), intensity.astype(np.float32))
|
|
518
|
+
for map_name in ('alpha_1', 'alpha_2', 'alpha_3', 'tau_mean_amp',
|
|
519
|
+
'tau_mean_int', 'chi2_r', 'calibrated_chi2_r'):
|
|
520
|
+
if pixel_maps.get(map_name) is not None:
|
|
521
|
+
np.save(str(slice_dir / f'{map_name}.npy'),
|
|
522
|
+
pixel_maps[map_name].astype(np.float32))
|
|
523
|
+
for map_name, arr in redox.items():
|
|
524
|
+
np.save(str(slice_dir / f'{map_name}.npy'), arr)
|
|
525
|
+
roi_name = f'{group_label}_z{z:04d}'
|
|
526
|
+
if getattr(args, 'save_lifetime', True):
|
|
527
|
+
try:
|
|
528
|
+
png_path = make_lifetime_image(
|
|
529
|
+
canvas=pixel_maps, output_dir=slice_dir, roi_name=roi_name,
|
|
530
|
+
tau_min_ns=tau_disp_min, tau_max_ns=tau_disp_max,
|
|
531
|
+
intensity_percentile_hi=95, tau_key='tau_mean_int', verbose=False,
|
|
532
|
+
)
|
|
533
|
+
if png_path is not None and Path(png_path).exists():
|
|
534
|
+
shutil.copyfile(str(png_path), str(preview_dir / f'z{z:04d}.png'))
|
|
535
|
+
except Exception as exc:
|
|
536
|
+
print(f' Warning: lifetime image export failed for {roi_name}: {exc}')
|
|
537
|
+
finally:
|
|
538
|
+
plt.close('all')
|
|
539
|
+
try:
|
|
540
|
+
save_tile_lifetime_txt(
|
|
541
|
+
slice_dir / f'{roi_name}_lifetime.txt', taus_ns, pixel_maps)
|
|
542
|
+
except Exception as exc:
|
|
543
|
+
print(f' Warning: lifetime .txt export failed for {roi_name}: {exc}')
|
|
544
|
+
try:
|
|
545
|
+
tile_decay = pixel_stack.sum(axis=(0, 1)).astype(np.float64)
|
|
546
|
+
tile_popt, tile_summary = fit_summed(
|
|
547
|
+
tile_decay, tcspc_res, n_bins,
|
|
548
|
+
irf_prompt, has_tail, fit_bg, fit_sigma,
|
|
549
|
+
args.nexp, args.tau_min, args.tau_max,
|
|
550
|
+
optimizer=getattr(args, 'optimizer', 'de'),
|
|
551
|
+
n_restarts=getattr(args, 'restarts', lm_restarts),
|
|
552
|
+
de_popsize=getattr(args, 'de_population', de_population),
|
|
553
|
+
de_maxiter=getattr(args, 'de_maxiter', de_maxiter),
|
|
554
|
+
workers=getattr(args, 'workers', n_workers),
|
|
555
|
+
polish=not getattr(args, 'no_polish', False),
|
|
556
|
+
cost_function=getattr(args, 'cost_function', 'poisson'),
|
|
557
|
+
sigma_max=sigma_max,
|
|
558
|
+
)
|
|
559
|
+
plot_summed(
|
|
560
|
+
tile_decay, tile_summary, ptu, None,
|
|
561
|
+
args.nexp, getattr(args, 'estimate_irf', 'gaussian'),
|
|
562
|
+
str(slice_dir / roi_name), irf_prompt=irf_prompt,
|
|
563
|
+
)
|
|
564
|
+
except Exception as exc:
|
|
565
|
+
print(f' Warning: per-slice detail fit plot failed for {roi_name}: {exc}')
|
|
566
|
+
finally:
|
|
567
|
+
plt.close('all')
|
|
568
|
+
if getattr(args, 'save_rgb', True):
|
|
569
|
+
try:
|
|
570
|
+
make_component_rgb_tiff(
|
|
571
|
+
canvas=pixel_maps, output_dir=slice_dir, roi_name=roi_name,
|
|
572
|
+
n_exp=args.nexp, intensity_percentile_hi=95, verbose=False,
|
|
573
|
+
)
|
|
574
|
+
except Exception as exc:
|
|
575
|
+
print(f' Warning: component RGB TIFF export failed for {roi_name}: {exc}')
|
|
576
|
+
if getattr(args, 'save_intensity', True):
|
|
577
|
+
try:
|
|
578
|
+
import tifffile as _tifffile
|
|
579
|
+
int_max_disp = getattr(args, 'intensity_display_max', None)
|
|
580
|
+
i_max = float(int_max_disp) if int_max_disp is not None \
|
|
581
|
+
else float(np.percentile(intensity[intensity > 0], 99.0)
|
|
582
|
+
if (intensity > 0).any() else 1.0)
|
|
583
|
+
i_max = max(i_max, 1e-6)
|
|
584
|
+
intensity_u16 = np.clip(
|
|
585
|
+
intensity.astype(np.float64) / i_max * 65535, 0, 65535
|
|
586
|
+
).astype(np.uint16)
|
|
587
|
+
_tifffile.imwrite(str(slice_dir / f'{roi_name}_intensity.tif'), intensity_u16)
|
|
588
|
+
except Exception as exc:
|
|
589
|
+
print(f' Warning: intensity TIFF export failed for {roi_name}: {exc}')
|
|
590
|
+
if getattr(args, 'save_ind', False):
|
|
591
|
+
try:
|
|
592
|
+
from ..utils.enhanced_outputs import save_individual_tau_maps
|
|
593
|
+
save_individual_tau_maps(
|
|
594
|
+
pixel_maps, slice_dir, roi_name=roi_name, n_exp=args.nexp)
|
|
595
|
+
except Exception as exc:
|
|
596
|
+
print(f' Warning: individual component map export failed for {roi_name}: {exc}')
|
|
597
|
+
stats = {'z': z, 'path': str(ptu_path)}
|
|
598
|
+
tau_map = redox.get('tau_mean')
|
|
599
|
+
if tau_map is None:
|
|
600
|
+
tau_map = pixel_maps.get('tau_mean_amp')
|
|
601
|
+
if tau_map is not None:
|
|
602
|
+
valid = tau_map[np.isfinite(tau_map) & (tau_map > 0)]
|
|
603
|
+
stats['tau_mean_mean'] = float(np.mean(valid)) if valid.size > 0 else float('nan')
|
|
604
|
+
stats['tau_mean_std'] = float(np.std(valid)) if valid.size > 0 else float('nan')
|
|
605
|
+
if 'bound_fraction' in redox:
|
|
606
|
+
bf = redox['bound_fraction']
|
|
607
|
+
valid_bf = bf[np.isfinite(bf)]
|
|
608
|
+
stats['bound_fraction_mean'] = (
|
|
609
|
+
float(np.mean(valid_bf)) if valid_bf.size > 0 else float('nan'))
|
|
610
|
+
stats['bound_fraction_std'] = (
|
|
611
|
+
float(np.std(valid_bf)) if valid_bf.size > 0 else float('nan'))
|
|
612
|
+
n_fitted = int(np.sum(
|
|
613
|
+
np.isfinite(pixel_maps.get('tau_mean_amp', np.array([np.nan])))))
|
|
614
|
+
stats['n_pixels_fitted'] = n_fitted
|
|
615
|
+
for i, tau in enumerate(taus_ns):
|
|
616
|
+
stats[f'tau{i+1}_ns'] = float(tau)
|
|
617
|
+
def _map_mean(name, require_positive=False):
|
|
618
|
+
m = pixel_maps.get(name)
|
|
619
|
+
if m is None:
|
|
620
|
+
return None
|
|
621
|
+
ok = np.isfinite(m)
|
|
622
|
+
if require_positive:
|
|
623
|
+
ok = ok & (m > 0)
|
|
624
|
+
vals = m[ok]
|
|
625
|
+
return float(np.mean(vals)) if vals.size > 0 else float('nan')
|
|
626
|
+
for i in range(args.nexp):
|
|
627
|
+
am = _map_mean(f'alpha_{i+1}')
|
|
628
|
+
if am is not None:
|
|
629
|
+
stats[f'alpha_{i+1}_mean'] = am
|
|
630
|
+
chi_mean = _map_mean('chi2_r', require_positive=True)
|
|
631
|
+
if chi_mean is not None:
|
|
632
|
+
stats['chi2_r_mean'] = chi_mean
|
|
633
|
+
elapsed = time.time() - t_start
|
|
634
|
+
print(f' τ_mean={stats.get('tau_mean_mean', float('nan')):.4f} ns '
|
|
635
|
+
f'bound_frac={stats.get('bound_fraction_mean', float('nan')):.4f} '
|
|
636
|
+
f'n_px={n_fitted:,} ({elapsed:.1f} s)')
|
|
637
|
+
z_series[z] = stats
|
|
638
|
+
if getattr(args, 'save_stack', True):
|
|
639
|
+
print(f'\n[4] Saving (Z, H, W) stacks…')
|
|
640
|
+
save_map_stacks(
|
|
641
|
+
[group_dir / f'z{z:04d}' for z in sorted(zslices)],
|
|
642
|
+
group_dir, group_label)
|
|
643
|
+
if not getattr(args, 'save_npy', True):
|
|
644
|
+
for z in zslices:
|
|
645
|
+
slice_dir = group_dir / f'z{z:04d}'
|
|
646
|
+
for f_ in slice_dir.glob('*.npy'):
|
|
647
|
+
try:
|
|
648
|
+
f_.unlink(missing_ok=True)
|
|
649
|
+
except Exception as exc:
|
|
650
|
+
print(f' Warning: could not remove {f_}: {exc}')
|
|
651
|
+
if not getattr(args, 'no_plots', False):
|
|
652
|
+
print(f'\n[5] Saving z-series plot…')
|
|
653
|
+
plot_metric_summary(
|
|
654
|
+
{group_label: z_series},
|
|
655
|
+
group_dir / f'{group_label}_zseries.png', group_label, 'Z-slice')
|
|
656
|
+
save_series_csv(
|
|
657
|
+
z_series, group_dir / f'{group_label}_zseries.csv',
|
|
658
|
+
index_name='z', drop_keys=('path', 'z'))
|
|
659
|
+
print(f' Saved CSV: {group_label}_zseries.csv')
|
|
660
|
+
_json_keys = (['tau_mean_mean', 'tau_mean_std',
|
|
661
|
+
'bound_fraction_mean', 'bound_fraction_std',
|
|
662
|
+
'n_pixels_fitted', 'chi2_r_mean']
|
|
663
|
+
+ [f'tau{i+1}_ns' for i in range(args.nexp)]
|
|
664
|
+
+ [f'alpha_{i+1}_mean' for i in range(args.nexp)])
|
|
665
|
+
save_json(group_dir / f'{group_label}_zseries.json', {
|
|
666
|
+
'z_slices': sorted(z_series.keys()),
|
|
667
|
+
**{k: [z_series.get(z, {}).get(k) for z in sorted(z_series)]
|
|
668
|
+
for k in _json_keys},
|
|
669
|
+
})
|
|
670
|
+
all_results[group_label] = {
|
|
671
|
+
'group_dir': str(group_dir),
|
|
672
|
+
'preview_dir': str(preview_dir),
|
|
673
|
+
'n_slices': n_z,
|
|
674
|
+
'taus_ns': list(taus_ns),
|
|
675
|
+
'z_series': z_series,
|
|
676
|
+
}
|
|
677
|
+
print(f'\n Stack {group_label} done.')
|
|
678
|
+
print(f'\n{'='*60}')
|
|
679
|
+
print(f' Z-STACK COMPLETE → {output_dir}')
|
|
680
|
+
print(f'{'='*60}\n')
|
|
681
|
+
return all_results
|