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.
Files changed (104) hide show
  1. flimkit/FLIM/__init__.py +0 -0
  2. flimkit/FLIM/assemble.py +254 -0
  3. flimkit/FLIM/batch.py +681 -0
  4. flimkit/FLIM/bg_tools.py +51 -0
  5. flimkit/FLIM/fit_tools.py +244 -0
  6. flimkit/FLIM/fitters.py +1471 -0
  7. flimkit/FLIM/irf_tools.py +617 -0
  8. flimkit/FLIM/models.py +391 -0
  9. flimkit/GPU/__init__.py +85 -0
  10. flimkit/GPU/_base.py +391 -0
  11. flimkit/GPU/cuda.py +10 -0
  12. flimkit/GPU/mlx_backend.py +381 -0
  13. flimkit/GPU/mps.py +10 -0
  14. flimkit/GPU/rocm.py +10 -0
  15. flimkit/GPU/torch_backend.py +385 -0
  16. flimkit/UI/app_state.py +10 -0
  17. flimkit/UI/controller.py +139 -0
  18. flimkit/UI/expert_settings.py +248 -0
  19. flimkit/UI/fit_help.py +206 -0
  20. flimkit/UI/fov_preview.py +1085 -0
  21. flimkit/UI/gui.py +3919 -0
  22. flimkit/UI/icon.icns +0 -0
  23. flimkit/UI/icon.ico +0 -0
  24. flimkit/UI/icon.png +0 -0
  25. flimkit/UI/irf_widget.py +103 -0
  26. flimkit/UI/mode_controller.py +118 -0
  27. flimkit/UI/modes/__init__.py +0 -0
  28. flimkit/UI/modes/base.py +3 -0
  29. flimkit/UI/modes/batch_mode.py +312 -0
  30. flimkit/UI/modes/fov_mode.py +164 -0
  31. flimkit/UI/modes/irf_mode.py +80 -0
  32. flimkit/UI/modes/phasor_mode.py +131 -0
  33. flimkit/UI/modes/stitch_mode.py +254 -0
  34. flimkit/UI/phasor_panel.py +1087 -0
  35. flimkit/UI/progress_window.py +113 -0
  36. flimkit/UI/project_panel.py +262 -0
  37. flimkit/UI/results_panel.py +332 -0
  38. flimkit/UI/roi_tools.py +794 -0
  39. flimkit/UI/utils.py +217 -0
  40. flimkit/__init__.py +0 -0
  41. flimkit/_version.py +41 -0
  42. flimkit/cli.py +120 -0
  43. flimkit/configs.py +148 -0
  44. flimkit/dialogs.py +46 -0
  45. flimkit/formats/BH/__init__.py +0 -0
  46. flimkit/formats/BH/reader.py +296 -0
  47. flimkit/formats/BH/writer.py +86 -0
  48. flimkit/formats/ISS/__init__.py +0 -0
  49. flimkit/formats/ISS/fdflim.py +86 -0
  50. flimkit/formats/ISS/image.py +114 -0
  51. flimkit/formats/ISS/reader.py +223 -0
  52. flimkit/formats/PS/__init__.py +0 -0
  53. flimkit/formats/PS/reader.py +202 -0
  54. flimkit/formats/PTU/__init__.py +0 -0
  55. flimkit/formats/PTU/decode.py +27 -0
  56. flimkit/formats/PTU/phu.py +85 -0
  57. flimkit/formats/PTU/reader.py +235 -0
  58. flimkit/formats/PTU/series.py +258 -0
  59. flimkit/formats/PTU/stitch.py +1182 -0
  60. flimkit/formats/PTU/tools.py +94 -0
  61. flimkit/formats/__init__.py +2 -0
  62. flimkit/formats/flim_file.py +232 -0
  63. flimkit/formats/phasor.py +132 -0
  64. flimkit/formats/signal.py +170 -0
  65. flimkit/image/tools.py +124 -0
  66. flimkit/interactive.py +1857 -0
  67. flimkit/mpl_backend.py +22 -0
  68. flimkit/phasor/__init__.py +40 -0
  69. flimkit/phasor/filters.py +127 -0
  70. flimkit/phasor/fret.py +654 -0
  71. flimkit/phasor/interactive.py +556 -0
  72. flimkit/phasor/peaks.py +186 -0
  73. flimkit/phasor/signal.py +90 -0
  74. flimkit/phasor_launcher.py +314 -0
  75. flimkit/plugins/__init__.py +137 -0
  76. flimkit/plugins/bindings.py +116 -0
  77. flimkit/plugins/builtin/__init__.py +3 -0
  78. flimkit/plugins/builtin/core_tools.py +28 -0
  79. flimkit/plugins/loader.py +371 -0
  80. flimkit/plugins/registry.py +406 -0
  81. flimkit/project.py +197 -0
  82. flimkit/synth.py +145 -0
  83. flimkit/utils/__init__.py +0 -0
  84. flimkit/utils/batch_fit.py +301 -0
  85. flimkit/utils/config_manager.py +119 -0
  86. flimkit/utils/config_snapshot.py +30 -0
  87. flimkit/utils/crash_handler.py +183 -0
  88. flimkit/utils/display.py +197 -0
  89. flimkit/utils/enhanced_outputs.py +345 -0
  90. flimkit/utils/fancy.py +103 -0
  91. flimkit/utils/lifetime_image.py +243 -0
  92. flimkit/utils/misc.py +111 -0
  93. flimkit/utils/plotting.py +190 -0
  94. flimkit/utils/roi.py +370 -0
  95. flimkit/utils/session.py +51 -0
  96. flimkit/utils/update_check.py +198 -0
  97. flimkit/utils/xlsx_tools.py +97 -0
  98. flimkit/utils/xml_utils.py +219 -0
  99. flimkit-0.12.0.dist-info/METADATA +356 -0
  100. flimkit-0.12.0.dist-info/RECORD +104 -0
  101. flimkit-0.12.0.dist-info/WHEEL +5 -0
  102. flimkit-0.12.0.dist-info/entry_points.txt +2 -0
  103. flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
  104. flimkit-0.12.0.dist-info/top_level.txt +1 -0
flimkit/synth.py ADDED
@@ -0,0 +1,145 @@
1
+ import json
2
+ import numpy as np
3
+ from pathlib import Path
4
+ from flimkit.FLIM.models import apply_pileup
5
+
6
+ def gaussian_irf(n_bins, center_bin, fwhm_bins):
7
+ sigma = fwhm_bins / 2.3548
8
+ b = np.arange(n_bins, dtype=float)
9
+ irf = np.exp(-0.5 * ((b - center_bin) / sigma) ** 2)
10
+ s = irf.sum()
11
+ return irf / s if s > 0 else irf
12
+
13
+ def build_decay(tau_ns, amps=None, n_bins=2000, tcspc_res_ns=0.025,
14
+ irf_fwhm_ns=0.15, irf_center_ns=2.0, n_photons=1e5,
15
+ background_frac=0.0, reflection=None, pileup_pp=None):
16
+ taus = np.atleast_1d(np.asarray(tau_ns, dtype=float))
17
+ if amps is None:
18
+ amps = np.ones_like(taus)
19
+ amps = np.atleast_1d(np.asarray(amps, dtype=float))
20
+ amps = amps / amps.sum()
21
+ t = np.arange(n_bins) * tcspc_res_ns
22
+ kernel = np.zeros(n_bins)
23
+ for a, tau in zip(amps, taus):
24
+ kernel += a * np.exp(-t / tau)
25
+ center_bin = irf_center_ns / tcspc_res_ns
26
+ fwhm_bins = irf_fwhm_ns / tcspc_res_ns
27
+ irf = gaussian_irf(n_bins, center_bin, fwhm_bins)
28
+ model = np.real(np.fft.ifft(np.fft.fft(kernel) * np.fft.fft(irf)))
29
+ model = np.maximum(model, 0.0)
30
+ model = model / model.sum()
31
+ refl_truth = None
32
+ if reflection is not None:
33
+ rc_bin = reflection['center_ns'] / tcspc_res_ns
34
+ rw_bin = max(reflection.get('width_ns', 0.15) / tcspc_res_ns, 0.5)
35
+ band = np.exp(-0.5 * ((np.arange(n_bins) - rc_bin) / (rw_bin / 2.3548)) ** 2)
36
+ band = band / band.sum()
37
+ frac = float(reflection['frac'])
38
+ model = (1.0 - frac) * model + frac * band
39
+ refl_truth = dict(center_ns=reflection['center_ns'],
40
+ width_ns=reflection.get('width_ns', 0.15), frac=frac)
41
+ if background_frac > 0:
42
+ model = (1.0 - background_frac) * model + background_frac / n_bins
43
+ expected = model * float(n_photons)
44
+ pileup_truth = None
45
+ if pileup_pp is not None and pileup_pp > 0:
46
+ n_sync = float(n_photons) / float(pileup_pp)
47
+ expected = apply_pileup(expected, n_sync)
48
+ pileup_truth = dict(photons_per_pulse=float(pileup_pp), n_sync=n_sync)
49
+ truth = dict(
50
+ tau_ns=taus.tolist(),
51
+ amps=amps.tolist(),
52
+ n_bins=int(n_bins),
53
+ tcspc_res_ns=float(tcspc_res_ns),
54
+ period_ns=float(n_bins * tcspc_res_ns),
55
+ irf_fwhm_ns=float(irf_fwhm_ns),
56
+ irf_center_ns=float(irf_center_ns),
57
+ n_photons_target=float(n_photons),
58
+ background_frac=float(background_frac),
59
+ reflection=refl_truth,
60
+ pileup=pileup_truth,
61
+ wrap_residual=float(kernel[-1] / kernel.max()),
62
+ )
63
+ return expected, irf, truth
64
+
65
+ def sample_cube(expected, ny, nx, seed=0):
66
+ rng = np.random.default_rng(seed)
67
+ per_px = expected / float(ny * nx)
68
+ cube = rng.poisson(per_px[None, None, :] * np.ones((ny, nx, 1)))
69
+ return cube.astype(np.uint32)
70
+
71
+ def write_ptu(path, cube, period_ns, tcspc_res_ns, pixel_margin=10.0, n_sync=None):
72
+ import ptufile
73
+ cube = np.ascontiguousarray(cube, dtype=np.uint32)
74
+ period_s = period_ns * 1e-9
75
+ res_s = tcspc_res_ns * 1e-9
76
+ max_px = int(cube.sum(axis=2).max()) if cube.size else 0
77
+ pixel_time = max(max_px, 1) * period_s * pixel_margin
78
+ if n_sync:
79
+ n_px = max(int(cube.shape[0] * cube.shape[1]), 1)
80
+ pixel_time = max(float(n_sync) * period_s / n_px, max(max_px, 1) * period_s)
81
+ w = ptufile.PtuWriter(str(path), shape=cube.shape,
82
+ global_resolution=period_s,
83
+ tcspc_resolution=res_s, pixel_time=pixel_time, mode='w')
84
+ w.write(cube)
85
+ w.close()
86
+ return str(path)
87
+
88
+ def irf_cube(truth, n_photons=2e5, ny=8, nx=8, seed=1):
89
+ nb = truth['n_bins']
90
+ res = truth['tcspc_res_ns']
91
+ center_bin = truth['irf_center_ns'] / res
92
+ fwhm_bins = truth['irf_fwhm_ns'] / res
93
+ irf = gaussian_irf(nb, center_bin, fwhm_bins) * float(n_photons)
94
+ return sample_cube(irf, ny, nx, seed=seed)
95
+
96
+ def write_irf_ptu(path, truth, n_photons=2e5, ny=8, nx=8, seed=1):
97
+ cube = irf_cube(truth, n_photons, ny, nx, seed)
98
+ return write_ptu(path, cube, truth['period_ns'], truth['tcspc_res_ns'])
99
+
100
+ def write_sdt(path, cube, period_ns, tcspc_res_ns):
101
+ from flimkit.formats.BH.writer import write_sdt as _write_sdt
102
+ return _write_sdt(path, cube, period_ns, tcspc_res_ns)
103
+
104
+ def generate(out_dir, name='synth', ny=16, nx=16, with_irf=True, seed=0, sdt=False, **kwargs):
105
+ out_dir = Path(out_dir)
106
+ out_dir.mkdir(parents=True, exist_ok=True)
107
+ expected, irf, truth = build_decay(**kwargs)
108
+ cube = sample_cube(expected, ny, nx, seed=seed)
109
+ truth['image_shape'] = [ny, nx]
110
+ truth['n_photons_written'] = int(cube.sum())
111
+ sample_path = out_dir / f'{name}.ptu'
112
+ pileup_sync = (truth.get('pileup') or {}).get('n_sync')
113
+ write_ptu(sample_path, cube, truth['period_ns'], truth['tcspc_res_ns'],
114
+ n_sync=pileup_sync)
115
+ truth['sample_ptu'] = sample_path.name
116
+ if sdt:
117
+ sdt_path = out_dir / f'{name}.sdt'
118
+ write_sdt(sdt_path, cube, truth['period_ns'], truth['tcspc_res_ns'])
119
+ truth['sample_sdt'] = sdt_path.name
120
+ if with_irf:
121
+ irf_c = irf_cube(truth)
122
+ irf_path = out_dir / f'{name}_IRF.ptu'
123
+ write_ptu(irf_path, irf_c, truth['period_ns'], truth['tcspc_res_ns'])
124
+ truth['irf_ptu'] = irf_path.name
125
+ if sdt:
126
+ irf_sdt = out_dir / f'{name}_IRF.sdt'
127
+ write_sdt(irf_sdt, irf_c, truth['period_ns'], truth['tcspc_res_ns'])
128
+ truth['irf_sdt'] = irf_sdt.name
129
+ truth_path = out_dir / f'{name}_truth.json'
130
+ truth_path.write_text(json.dumps(truth, indent=2))
131
+ return dict(sample=str(sample_path), truth=truth, truth_json=str(truth_path))
132
+
133
+ def generate_series(out_dir, photon_counts, name='synth', with_reflection=True,
134
+ reflection=None, **kwargs):
135
+ out_dir = Path(out_dir)
136
+ if reflection is None:
137
+ reflection = dict(center_ns=8.0, width_ns=0.15, frac=0.02)
138
+ results = []
139
+ for i, n in enumerate(photon_counts):
140
+ refl = reflection if with_reflection else None
141
+ tag = f'{name}_{int(n):d}ph'
142
+ res = generate(out_dir, name=tag, with_irf=(i == 0), seed=i,
143
+ n_photons=n, reflection=refl, **kwargs)
144
+ results.append(res)
145
+ return results
File without changes
@@ -0,0 +1,301 @@
1
+ import re
2
+ import json
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 collections import defaultdict
9
+ from flimkit.formats import FLIMFile
10
+ from ..FLIM.fit_tools import find_irf_peak_bin
11
+ from ..FLIM.irf_tools import (
12
+ gaussian_irf_from_fwhm, estimate_irf_from_decay_parametric, machine_irf_prompt,
13
+ )
14
+ from ..configs import (
15
+ MACHINE_IRF_SIGMA_MAX_FULL,
16
+ IRF_FWHM, IRF_BINS, IRF_FIT_WIDTH,
17
+ TAU_DISPLAY_MIN, TAU_DISPLAY_MAX,
18
+ )
19
+
20
+ _STACK_MAPS = ['intensity', 'tau_mean_amp', 'alpha_1', 'alpha_2',
21
+ 'bound_fraction', 'chi2_r', 'calibrated_chi2_r']
22
+
23
+ _TL_FILENAME_RE = re.compile(
24
+ r'^(?P<region>.+?)_t(?P<t>\d+)(?:_s(?P<s>\d+))?(?:_z(?P<z>\d+))?\.ptu$',
25
+ re.IGNORECASE
26
+ )
27
+ _ZS_FILENAME_RE = re.compile(
28
+ r'^(?P<region>.+?)(?:_t(?P<t>\d+))?(?:_s(?P<s>\d+))?_z(?P<z>\d+)\.ptu$',
29
+ re.IGNORECASE
30
+ )
31
+
32
+ def parse_timelapse_filename(fname):
33
+ m = _TL_FILENAME_RE.match(Path(fname).name)
34
+ if not m:
35
+ return None
36
+ region = m.group('region')
37
+ t = int(m.group('t'))
38
+ s = int(m.group('s')) if m.group('s') is not None else 0
39
+ z = int(m.group('z')) if m.group('z') is not None else 0
40
+ return region, t, s, z
41
+
42
+ def group_timelapse_files(ptu_dir):
43
+ groups = defaultdict(lambda: defaultdict(dict))
44
+ ptu_dir = Path(ptu_dir)
45
+ for p in sorted(ptu_dir.glob('*.ptu')):
46
+ parsed = parse_timelapse_filename(p.name)
47
+ if parsed is None:
48
+ continue
49
+ region, t, s, z = parsed
50
+ groups[(region, z)][t][s] = p
51
+ return {k: dict(v) for k, v in groups.items()}
52
+
53
+ def parse_zstack_filename(fname):
54
+ m = _ZS_FILENAME_RE.match(Path(fname).name)
55
+ if not m:
56
+ return None
57
+ region = m.group('region')
58
+ t = int(m.group('t')) if m.group('t') is not None else 0
59
+ s = int(m.group('s')) if m.group('s') is not None else 0
60
+ z = int(m.group('z'))
61
+ return region, t, s, z
62
+
63
+ def group_zstack_files(ptu_dir):
64
+ groups = defaultdict(dict)
65
+ ptu_dir = Path(ptu_dir)
66
+ for p in sorted(ptu_dir.glob('*.ptu')):
67
+ parsed = parse_zstack_filename(p.name)
68
+ if parsed is None:
69
+ continue
70
+ region, t, s, z = parsed
71
+ groups[(region, t, s)][z] = p
72
+ return {k: dict(v) for k, v in groups.items()}
73
+
74
+ def zstack_group_label(region, t, s):
75
+ if t == 0 and s == 0:
76
+ return region
77
+ return f'{region}_t{t:04d}_s{s}'
78
+
79
+ def pool_decays(frame_positions, channel=None):
80
+ pooled = None
81
+ tcspc_res = None
82
+ n_bins = None
83
+ for _t, positions in sorted(frame_positions.items()):
84
+ for _s, ptu_path in sorted(positions.items()):
85
+ ptu = FLIMFile(str(ptu_path), verbose=False)
86
+ d = ptu.summed_decay(channel=channel).astype(np.float64)
87
+ if pooled is None:
88
+ pooled = d.copy()
89
+ tcspc_res = ptu.tcspc_res
90
+ n_bins = ptu.n_bins
91
+ else:
92
+ n = min(d.size, pooled.size)
93
+ pooled = pooled[:n] + d[:n]
94
+ n_bins = n
95
+ return pooled, tcspc_res, n_bins
96
+
97
+ def build_irf(decay, tcspc_res, n_bins, args):
98
+ from ..FLIM.irf_tools import estimate_irf_from_decay_raw
99
+ irf_peak_bin = find_irf_peak_bin(decay)
100
+ decay_peak_bin = int(np.argmax(decay))
101
+ sigma_max = MACHINE_IRF_SIGMA_MAX_FULL
102
+ estimate_irf = getattr(args, 'estimate_irf', 'gaussian')
103
+ if estimate_irf in ('machine_irf', 'machine_irf_sigma_full', 'machine_irf_sigma_half'):
104
+ irf_prompt, _strategy, has_tail, fit_bg, fit_sigma, sigma_max = machine_irf_prompt(
105
+ getattr(args, 'machine_irf', None), n_bins, irf_peak_bin, estimate_irf)
106
+ elif estimate_irf == 'raw':
107
+ irf_prompt = estimate_irf_from_decay_raw(
108
+ decay, tcspc_res, n_bins,
109
+ n_irf_bins=getattr(args, 'irf_bins', IRF_BINS))
110
+ has_tail = True
111
+ fit_sigma = True
112
+ fit_bg = True
113
+ elif estimate_irf == 'parametric':
114
+ irf_prompt = estimate_irf_from_decay_parametric(
115
+ decay, tcspc_res, n_bins,
116
+ fit_window_width_ns=getattr(args, 'irf_fit_width', IRF_FIT_WIDTH))
117
+ has_tail = True
118
+ fit_sigma = True
119
+ fit_bg = True
120
+ else:
121
+ fwhm_ns = getattr(args, 'irf_fwhm', None) or IRF_FWHM or (tcspc_res * 1e9)
122
+ irf_prompt = gaussian_irf_from_fwhm(n_bins, tcspc_res, fwhm_ns, decay_peak_bin)
123
+ has_tail = False
124
+ fit_sigma = False
125
+ fit_bg = True
126
+ return irf_prompt, has_tail, fit_bg, fit_sigma, sigma_max
127
+
128
+ def compute_redox_metrics(pixel_maps, n_exp, compute_bound_fraction=False):
129
+ out = {}
130
+ a1 = pixel_maps.get('a1') if pixel_maps.get('a1') is not None else pixel_maps.get('alpha_1')
131
+ a2 = pixel_maps.get('a2') if pixel_maps.get('a2') is not None else pixel_maps.get('alpha_2')
132
+ if compute_bound_fraction and n_exp >= 2 and a1 is not None and a2 is not None:
133
+ total = a1 + a2
134
+ with np.errstate(invalid='ignore', divide='ignore'):
135
+ out['bound_fraction'] = np.where(total > 0, a2 / total, np.nan).astype(np.float32)
136
+ tau_mean = pixel_maps.get('tau_mean_amp')
137
+ if tau_mean is not None:
138
+ out['tau_mean'] = tau_mean.astype(np.float32)
139
+ return out
140
+
141
+ def compute_intensity_redox_ratio(intensity_s0, intensity_s1):
142
+ total = intensity_s0.astype(float) + intensity_s1.astype(float)
143
+ return np.where(total > 0, intensity_s1.astype(float) / total, np.nan).astype(np.float32)
144
+
145
+ def make_synthetic_popt(ref_taus_ns, n_exp, n_bins, irf_peak_bin,
146
+ fit_sigma, fit_bg, has_tail):
147
+ taus = [t * 1e-9 for t in ref_taus_ns if t is not None]
148
+ while len(taus) < n_exp:
149
+ taus.append((taus[-1] if taus else 1e-9) * 2.0)
150
+ taus = taus[:n_exp]
151
+ amps = [1.0 / n_exp] * n_exp
152
+ parts = taus + amps + [float(irf_peak_bin)]
153
+ if fit_sigma:
154
+ parts.append(0.0)
155
+ if fit_bg:
156
+ parts.append(0.0)
157
+ if has_tail:
158
+ parts += [0.0, 1.0]
159
+ return np.array(parts, dtype=float)
160
+
161
+ def _json_default(obj):
162
+ if isinstance(obj, (np.floating, float)):
163
+ v = float(obj)
164
+ return None if (v != v) else v
165
+ if isinstance(obj, np.integer):
166
+ return int(obj)
167
+ if isinstance(obj, np.ndarray):
168
+ return obj.tolist()
169
+ raise TypeError(f'Not serializable: {type(obj)}')
170
+
171
+ def save_json(path, data):
172
+ with open(str(path), 'w') as f:
173
+ json.dump(data, f, indent=2, default=_json_default)
174
+
175
+ def save_series_csv(series, path, index_name='t', drop_keys=('path',)):
176
+ indices = sorted(series.keys())
177
+ if not indices:
178
+ return
179
+ drop = set(drop_keys)
180
+ scalar_keys = [k for k in sorted({k for row in series.values() for k in row})
181
+ if k not in drop]
182
+ lines = [','.join([index_name] + scalar_keys)]
183
+ for idx in indices:
184
+ row = series[idx]
185
+ vals = [str(idx)] + [str(row.get(k, '')) for k in scalar_keys]
186
+ lines.append(','.join(vals))
187
+ Path(path).write_text('\n'.join(lines))
188
+
189
+ def resolve_tau_display_range(taus_ns, args):
190
+ tau_lo = getattr(args, 'tau_display_min', None)
191
+ tau_hi = getattr(args, 'tau_display_max', None)
192
+ if tau_lo is None:
193
+ tau_lo = TAU_DISPLAY_MIN
194
+ if tau_hi is None:
195
+ tau_hi = TAU_DISPLAY_MAX
196
+ if tau_lo is not None and tau_hi is not None:
197
+ return float(tau_lo), float(tau_hi)
198
+ taus_valid = [t for t in taus_ns if t == t]
199
+ if taus_valid:
200
+ pad = 0.25 * (max(taus_valid) - min(taus_valid) + 1e-9)
201
+ auto_lo = max(0.0, min(taus_valid) - pad)
202
+ auto_hi = max(taus_valid) + pad
203
+ else:
204
+ auto_lo = getattr(args, 'tau_min', 0.0)
205
+ auto_hi = getattr(args, 'tau_max', 5.0)
206
+ return (float(tau_lo) if tau_lo is not None else float(auto_lo),
207
+ float(tau_hi) if tau_hi is not None else float(auto_hi))
208
+
209
+ def save_tile_lifetime_txt(path, taus_ns, pixel_maps):
210
+ tau_map = pixel_maps.get('tau_mean_int')
211
+ valid = (np.isfinite(tau_map) & (tau_map > 0)) if tau_map is not None else None
212
+ chi_map = pixel_maps.get('chi2_r')
213
+ chi_valid = (np.isfinite(chi_map) & (chi_map > 0)) if chi_map is not None else None
214
+ lines = ['Per-tile lifetime export']
215
+ for i, tau in enumerate(taus_ns):
216
+ lines.append(f'tau{i+1}_ns = {float(tau):.6f}')
217
+ if valid is not None and valid.any():
218
+ tau_vals = tau_map[valid]
219
+ lines.append(f'tau_mean_int_mean_ns = {float(np.mean(tau_vals)):.6f}')
220
+ lines.append(f'tau_mean_int_median_ns = {float(np.median(tau_vals)):.6f}')
221
+ lines.append(f'tau_mean_int_std_ns = {float(np.std(tau_vals)):.6f}')
222
+ n_pixels_fitted = int(valid.sum())
223
+ else:
224
+ lines.append('tau_mean_int_mean_ns = nan')
225
+ lines.append('tau_mean_int_median_ns = nan')
226
+ lines.append('tau_mean_int_std_ns = nan')
227
+ n_pixels_fitted = 0
228
+ lines.append(f'n_pixels_fitted = {n_pixels_fitted}')
229
+ if chi_valid is not None and chi_valid.any():
230
+ lines.append(f'chi2_r_mean = {float(np.mean(chi_map[chi_valid])):.6f}')
231
+ else:
232
+ lines.append('chi2_r_mean = nan')
233
+ Path(path).write_text('\n'.join(lines) + '\n')
234
+
235
+ def save_map_stacks(slice_dirs, out_dir, out_prefix, maps=None):
236
+ maps = maps or _STACK_MAPS
237
+ out_dir = Path(out_dir)
238
+ for map_name in maps:
239
+ frames = []
240
+ for d in slice_dirs:
241
+ p = Path(d) / f'{map_name}.npy'
242
+ if p.exists():
243
+ frames.append(np.load(str(p)))
244
+ else:
245
+ frames = []
246
+ break
247
+ if frames:
248
+ arr = np.stack(frames, axis=0)
249
+ out_path = out_dir / f'{out_prefix}_{map_name}_stack.npy'
250
+ np.save(str(out_path), arr)
251
+ print(f' Saved stack: {out_prefix} {map_name} {arr.shape} → {out_path.name}')
252
+
253
+ def plot_metric_summary(series_map, output_path, group_label, x_label):
254
+ labels = sorted(series_map.keys())
255
+ all_x = sorted({x for s in series_map.values() for x in s})
256
+ if not all_x:
257
+ return
258
+ tau_colors = ['#2196F3', '#64B5F6', '#0D47A1', '#42A5F5']
259
+ bound_colors = ['#E91E63', '#F48FB1', '#880E4F', '#EC407A']
260
+ def _vals(series, key):
261
+ return [series.get(x, {}).get(key, np.nan) for x in all_x]
262
+ has_tau = any(v == v
263
+ for s in series_map.values() for v in _vals(s, 'tau_mean_mean'))
264
+ has_bound = any(v == v
265
+ for s in series_map.values() for v in _vals(s, 'bound_fraction_mean'))
266
+ n_plots = int(has_tau) + int(has_bound)
267
+ if n_plots == 0:
268
+ return
269
+ from flimkit.utils.plotting import _EXPORT_RC
270
+ plt.rcParams.update(_EXPORT_RC)
271
+ fig, axes = plt.subplots(1, n_plots, figsize=(5 * n_plots, 4))
272
+ if n_plots == 1:
273
+ axes = [axes]
274
+ multi = len(labels) > 1
275
+ def _plot_metric(ax, key, ylabel, colors):
276
+ for i, lab in enumerate(labels):
277
+ vals = _vals(series_map[lab], key)
278
+ c = colors[i % len(colors)]
279
+ lw = 1.0 if multi else 1.5
280
+ ax.plot(all_x, vals, 'o-', color=c, linewidth=lw, label=str(lab), alpha=0.8)
281
+ if multi:
282
+ mean_vals = [float(np.nanmean([series_map[lab].get(x, {}).get(key, np.nan)
283
+ for lab in labels])) for x in all_x]
284
+ ax.plot(all_x, mean_vals, 'o-', color='black', linewidth=2.0,
285
+ label='mean', zorder=5)
286
+ ax.legend(fontsize=8)
287
+ ax.set_xlabel(x_label)
288
+ ax.set_ylabel(ylabel)
289
+ ax.set_title(f'{group_label}')
290
+ ax.grid(True, alpha=0.3)
291
+ idx = 0
292
+ if has_tau:
293
+ _plot_metric(axes[idx], 'tau_mean_mean', 'Mean τ (ns)', tau_colors)
294
+ idx += 1
295
+ if has_bound:
296
+ _plot_metric(axes[idx], 'bound_fraction_mean',
297
+ 'Bound fraction α₂/(α₁+α₂)', bound_colors)
298
+ plt.tight_layout()
299
+ plt.savefig(str(output_path), dpi=150, bbox_inches='tight')
300
+ plt.close(fig)
301
+ print(f' Saved summary plot: {Path(output_path).name}')
@@ -0,0 +1,119 @@
1
+ import json
2
+ import os
3
+ import copy
4
+
5
+ _CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.flimkit')
6
+ _CONFIG_FILE = os.path.join(_CONFIG_DIR, 'config.json')
7
+
8
+ _DEFAULTS = {
9
+ 'expert': {
10
+ 'binning_factor': 1,
11
+ 'optimizer': 'de',
12
+ 'lm_restarts': 8,
13
+ 'de_population': 30,
14
+ 'de_maxiter': 5000,
15
+ 'n_workers': -1,
16
+ 'cost_function': 'poisson',
17
+ 'channels': None,
18
+ 'min_photons': 10,
19
+ },
20
+ 'preferences': {
21
+ 'colormap': 'viridis',
22
+ 'font_size': 9,
23
+ 'default_nexp': 2,
24
+ 'export_format': 'CSV',
25
+ 'output_directory': '',
26
+ 'auto_save_npz': True,
27
+ },
28
+ }
29
+
30
+
31
+ class ConfigManager:
32
+
33
+ def __init__(self):
34
+ self._global: dict = {}
35
+ self._project: dict = {}
36
+ self._load()
37
+
38
+
39
+ def get(self, dotted_key: str, default=None):
40
+ section, _, key = dotted_key.partition('.')
41
+ if not key:
42
+ # whole-section request
43
+ merged = copy.deepcopy(_DEFAULTS.get(section, {}))
44
+ merged.update(self._global.get(section, {}))
45
+ merged.update(self._project.get(section, {}))
46
+ return merged
47
+
48
+ # Per-key lookup with layered fallback
49
+ if section in self._project and key in self._project[section]:
50
+ return self._project[section][key]
51
+ if section in self._global and key in self._global[section]:
52
+ return self._global[section][key]
53
+ if section in _DEFAULTS and key in _DEFAULTS[section]:
54
+ return _DEFAULTS[section][key]
55
+ return default
56
+
57
+ def get_section(self, section: str) -> dict:
58
+ return self.get(section)
59
+
60
+ def set(self, dotted_key: str, value):
61
+ section, _, key = dotted_key.partition('.')
62
+ if section not in self._global:
63
+ self._global[section] = {}
64
+ if key:
65
+ self._global[section][key] = value
66
+ else:
67
+ self._global[section] = value
68
+
69
+ def update_section(self, section: str, data: dict):
70
+ if section not in self._global:
71
+ self._global[section] = {}
72
+ self._global[section].update(data)
73
+ self.save()
74
+
75
+ def load_project_overrides(self, overrides: dict):
76
+ self._project = overrides or {}
77
+
78
+ def clear_project_overrides(self):
79
+ self._project = {}
80
+
81
+ def get_project_overrides(self) -> dict:
82
+ return copy.deepcopy(self._project)
83
+
84
+
85
+ def _load(self):
86
+ # Also migrate old YAML config if it exists
87
+ yaml_path = os.path.join(_CONFIG_DIR, 'config.yaml')
88
+ if not os.path.exists(_CONFIG_FILE) and os.path.exists(yaml_path):
89
+ try:
90
+ import yaml
91
+ with open(yaml_path, 'r') as f:
92
+ data = yaml.safe_load(f)
93
+ if isinstance(data, dict):
94
+ self._global = data
95
+ self.save()
96
+ return
97
+ except Exception:
98
+ pass
99
+ if not os.path.exists(_CONFIG_FILE):
100
+ self._global = {}
101
+ return
102
+ try:
103
+ with open(_CONFIG_FILE, 'r') as f:
104
+ data = json.load(f)
105
+ self._global = data if isinstance(data, dict) else {}
106
+ except Exception as exc:
107
+ print(f"[Config] Warning: could not read {_CONFIG_FILE}: {exc}")
108
+ self._global = {}
109
+
110
+ def save(self):
111
+ os.makedirs(_CONFIG_DIR, exist_ok=True)
112
+ with open(_CONFIG_FILE, 'w') as f:
113
+ json.dump(self._global, f, indent=2)
114
+
115
+ def reload(self):
116
+ self._load()
117
+
118
+
119
+ cfg = ConfigManager()
@@ -0,0 +1,30 @@
1
+ _cfg: dict = {}
2
+
3
+ def _C() -> dict:
4
+ if not _cfg:
5
+ from flimkit.configs import (
6
+ n_exp, Tau_min, Tau_max, D_mode, binning_factor,
7
+ MIN_PHOTONS_PERPIX, Optimizer, lm_restarts, de_population,
8
+ de_maxiter, n_workers, OUT_NAME, IRF_BINS, IRF_FIT_WIDTH,
9
+ IRF_FWHM, channels, TAU_DISPLAY_MIN, TAU_DISPLAY_MAX,
10
+ INTENSITY_DISPLAY_MIN, INTENSITY_DISPLAY_MAX,
11
+ MACHINE_IRF_DIR, MACHINE_IRF_DEFAULT_PATH,
12
+ MACHINE_IRF_ALIGN_ANCHOR, MACHINE_IRF_REDUCER,
13
+ )
14
+ _cfg.update(
15
+ n_exp=n_exp, Tau_min=Tau_min, Tau_max=Tau_max, D_mode=D_mode,
16
+ binning_factor=binning_factor, MIN_PHOTONS_PERPIX=MIN_PHOTONS_PERPIX,
17
+ Optimizer=Optimizer, lm_restarts=lm_restarts,
18
+ de_population=de_population, de_maxiter=de_maxiter,
19
+ n_workers=n_workers, OUT_NAME=OUT_NAME,
20
+ IRF_BINS=IRF_BINS, IRF_FIT_WIDTH=IRF_FIT_WIDTH, IRF_FWHM=IRF_FWHM,
21
+ channels=channels,
22
+ TAU_DISPLAY_MIN=TAU_DISPLAY_MIN, TAU_DISPLAY_MAX=TAU_DISPLAY_MAX,
23
+ INTENSITY_DISPLAY_MIN=INTENSITY_DISPLAY_MIN,
24
+ INTENSITY_DISPLAY_MAX=INTENSITY_DISPLAY_MAX,
25
+ MACHINE_IRF_DIR=MACHINE_IRF_DIR,
26
+ MACHINE_IRF_DEFAULT_PATH=MACHINE_IRF_DEFAULT_PATH,
27
+ MACHINE_IRF_ALIGN_ANCHOR=MACHINE_IRF_ALIGN_ANCHOR,
28
+ MACHINE_IRF_REDUCER=MACHINE_IRF_REDUCER,
29
+ )
30
+ return _cfg