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/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
|