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/UI/icon.icns ADDED
Binary file
flimkit/UI/icon.ico ADDED
Binary file
flimkit/UI/icon.png ADDED
Binary file
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+ import tkinter as tk
3
+ from tkinter import ttk
4
+ from typing import Optional
5
+ from flimkit.UI.utils import _browse_file, _section
6
+ class IRFWidget:
7
+ _AUTO_FILL = object()
8
+ CHOICES = [
9
+ ('Analytical model (LAS X export)', 'irf_xlsx'),
10
+ ('Machine IRF (.npy pre-built)', 'machine_irf'),
11
+ ('Machine IRF + full σ broadening', 'machine_irf_sigma_full'),
12
+ ('Machine IRF + half σ broadening (σ≤0.5)', 'machine_irf_sigma_half'),
13
+ ('Measured IRF file (scatter PTU or .pck)', 'file'),
14
+ ('Estimate from decay - raw', 'raw'),
15
+ ('Estimate from decay - parametric', 'parametric'),
16
+ ('Gaussian (fallback)', 'gaussian'),
17
+ ]
18
+ def __init__(self, parent, default='irf_xlsx', xlsx_var=None, machine_irf_default: str = ''):
19
+ self.xlsx_var = xlsx_var
20
+ self._machine_irf_default = machine_irf_default
21
+ self.sv_method = tk.StringVar(value=default)
22
+ self.sv_path = tk.StringVar()
23
+ self.frame = _section(parent, 'Instrument Response Function (IRF)',
24
+ help_topic='irf')
25
+ self.frame.columnconfigure(1, weight=1)
26
+ for i, (lbl, val) in enumerate(self.CHOICES):
27
+ ttk.Radiobutton(self.frame, text=lbl, variable=self.sv_method,
28
+ value=val, command=self._update).grid(
29
+ row=i, column=0, columnspan=3, sticky='w', padx=4, pady=1)
30
+ r = len(self.CHOICES)
31
+ self._path_lbl = ttk.Label(self.frame, text='IRF file')
32
+ self._path_lbl.grid(row=r, column=0, sticky='e', padx=6, pady=3)
33
+ self._path_e = ttk.Entry(self.frame, textvariable=self.sv_path, width=45)
34
+ self._path_e.grid(row=r, column=1, sticky='ew', padx=4, pady=3)
35
+ self._path_btn = ttk.Button(
36
+ self.frame, text='Browse...',
37
+ command=self._browse_irf_path)
38
+ self._path_btn.grid(row=r, column=2, padx=4, pady=3)
39
+ self._note = ttk.Label(
40
+ self.frame,
41
+ text='Uses the LAS X export entered in Input Files above',
42
+ foreground='grey')
43
+ self._note.grid(row=r + 1, column=0, columnspan=3, sticky='w', padx=8, pady=3)
44
+ self._update()
45
+ def _browse_irf_path(self):
46
+ if self.sv_method.get().startswith('machine_irf'):
47
+ _browse_file(self.sv_path, 'Select machine IRF',
48
+ [('NumPy array', '*.npy'), ('All', '*.*')])
49
+ else:
50
+ from flimkit.formats import supported_extensions
51
+ _flim = ' '.join('*' + e for e in supported_extensions())
52
+ _browse_file(self.sv_path, 'Select IRF file',
53
+ [('FLIM / PCK / LAS X export',
54
+ _flim + ' *.pck *.xlsx *.csv *.tsv *.txt *.dat *.ascii *.asc'),
55
+ ('All', '*.*')])
56
+ def _show_browse(self):
57
+ method = self.sv_method.get()
58
+ self._path_lbl.config(
59
+ text='Machine IRF (.npy) path' if method.startswith('machine_irf') else 'IRF file')
60
+ if method.startswith('machine_irf') and not self.sv_path.get().endswith('.npy'):
61
+ self.sv_path.set(self._machine_irf_default)
62
+ self._path_lbl.grid()
63
+ self._path_e.grid()
64
+ self._path_btn.grid()
65
+ self._note.grid_remove()
66
+ def _show_note(self):
67
+ self._path_lbl.grid_remove()
68
+ self._path_e.grid_remove()
69
+ self._path_btn.grid_remove()
70
+ self._note.grid()
71
+ def _hide_all(self):
72
+ self._path_lbl.grid_remove()
73
+ self._path_e.grid_remove()
74
+ self._path_btn.grid_remove()
75
+ self._note.grid_remove()
76
+ def _update(self):
77
+ method = self.sv_method.get()
78
+ if method == 'irf_xlsx':
79
+ self._show_note() if self.xlsx_var is not None else self._show_browse()
80
+ elif method in ('file',) or method.startswith('machine_irf'):
81
+ self._show_browse()
82
+ else:
83
+ self._hide_all()
84
+ def grid(self, **kw):
85
+ self.frame.grid(**kw)
86
+
87
+ def grid_remove(self):
88
+ self.frame.grid_remove()
89
+ def get_args(self, xlsx_fallback: Optional[str] = None) -> dict:
90
+ method = self.sv_method.get()
91
+ path = self.sv_path.get().strip() or None
92
+ if method == 'irf_xlsx':
93
+ xlsx = (self.xlsx_var.get().strip() if self.xlsx_var else None) \
94
+ or xlsx_fallback or path
95
+ return dict(irf=None, irf_xlsx=xlsx, estimate_irf='none', no_xlsx_irf=False, machine_irf=None)
96
+ elif method.startswith('machine_irf'):
97
+ return dict(irf=None, irf_xlsx=None, estimate_irf=method, no_xlsx_irf=True, machine_irf=path)
98
+ elif method == 'file':
99
+ return dict(irf=path, irf_xlsx=None, estimate_irf='none', no_xlsx_irf=True, machine_irf=None)
100
+ elif method in ('raw', 'parametric'):
101
+ return dict(irf=None, irf_xlsx=None, estimate_irf=method, no_xlsx_irf=True, machine_irf=None)
102
+ else:
103
+ return dict(irf=None, irf_xlsx=None, estimate_irf='none', no_xlsx_irf=True, machine_irf=None)
@@ -0,0 +1,118 @@
1
+ from flimkit.UI.roi_tools import RoiAnalysisPanel
2
+
3
+
4
+ class ModeController:
5
+ def __init__(self, builder):
6
+ self.b = builder
7
+
8
+ def switch(self, form_id: str):
9
+ for btn in self.b._form_buttons.values():
10
+ btn.state(['!pressed'])
11
+
12
+ if form_id in self.b._form_inner_frames:
13
+ # batch and irf are menu-only - no sidebar button to highlight
14
+
15
+ self.b._current_form = form_id
16
+
17
+ if form_id == 'fov':
18
+ self.b._stitch_tabs.grid_remove()
19
+ if 'phasor' in self.b._form_inner_frames:
20
+ self.b._form_inner_frames['phasor'][0].grid_remove()
21
+ if hasattr(self.b, '_roi_analysis_panel'):
22
+ self.b._fov_preview._roi_analysis_panel = self.b._roi_analysis_panel
23
+
24
+ fov_frame = self.b._form_inner_frames['fov'][0]
25
+ fov_frame.grid(row=0, column=0, sticky='nsew')
26
+ fov_frame.lift()
27
+ fov_frame.tkraise()
28
+
29
+ self.b._analysis_tabs.grid(row=0, column=0, sticky='nsew')
30
+ self.b._analysis_tabs.lift()
31
+ self.b._analysis_tabs.tkraise()
32
+ self.b._analysis_tabs.select(0)
33
+
34
+ # update_idletasks avoids re-entrant event processing
35
+ self.b._fit_settings_tab.update_idletasks()
36
+ self.b._refresh_scrollable_frame(form_id)
37
+
38
+ elif form_id == 'stitch':
39
+ self.b._analysis_tabs.grid_remove()
40
+ if 'phasor' in self.b._form_inner_frames:
41
+ self.b._form_inner_frames['phasor'][0].grid_remove()
42
+
43
+ if 'fov' in self.b._form_inner_frames:
44
+ self.b._form_inner_frames['fov'][0].grid_remove()
45
+
46
+ stitch_frame = self.b._form_inner_frames['stitch'][0]
47
+ stitch_frame.grid(row=0, column=0, sticky='nsew')
48
+ stitch_frame.lift()
49
+ stitch_frame.tkraise()
50
+
51
+ self.b._stitch_tabs.grid(row=0, column=0, sticky='nsew')
52
+ self.b._stitch_tabs.lift()
53
+ self.b._stitch_tabs.tkraise()
54
+
55
+ if not hasattr(self.b, '_stitch_roi_panel'):
56
+ self.b._stitch_roi_panel = RoiAnalysisPanel(self.b._stitch_roi_analysis_frame)
57
+ self.b._stitch_roi_panel.app = self.b
58
+ self.b._stitch_roi_panel.grid(row=0, column=0, sticky='nsew')
59
+ self.b._stitch_roi_panel.fov_preview = self.b._fov_preview
60
+ self.b._fov_preview._roi_analysis_panel = self.b._stitch_roi_panel
61
+
62
+ self.b._stitch_tabs.select(0)
63
+
64
+ # update_idletasks avoids re-entrant event processing
65
+ self.b._stitch_settings_tab.update_idletasks()
66
+ self.b._refresh_scrollable_frame(form_id)
67
+
68
+ else:
69
+ self.b._analysis_tabs.grid_remove()
70
+ self.b._stitch_tabs.grid_remove()
71
+
72
+ for fid in ('phasor', 'fov', 'stitch', 'batch', 'irf'):
73
+ if fid != form_id and fid in self.b._form_inner_frames:
74
+ self.b._form_inner_frames[fid][0].grid_remove()
75
+
76
+ if form_id in self.b._form_inner_frames:
77
+ selected_frame = self.b._form_inner_frames[form_id][0]
78
+ selected_frame.grid(row=0, column=0, sticky='nsew')
79
+ selected_frame.lift()
80
+ selected_frame.tkraise()
81
+ self.b._refresh_scrollable_frame(form_id)
82
+
83
+ if form_id == 'phasor':
84
+ self.b._fov_preview.frame.grid_remove()
85
+ self.b._phasor_panel.frame.grid()
86
+ self.b._preview_frame_label.configure(text=' Phasor Analysis ')
87
+ if (hasattr(self.b, 'sv_ph_ptu') and hasattr(self.b, 'sv_ptu')
88
+ and not self.b.sv_ph_ptu.get().strip()):
89
+ fov_ptu = self.b.sv_ptu.get().strip()
90
+ if fov_ptu:
91
+ self.b.sv_ph_ptu.set(fov_ptu)
92
+ if hasattr(self.b, '_irf_fov'):
93
+ method = self.b._irf_fov.sv_method.get()
94
+ if method == 'irf_xlsx' and hasattr(self.b, 'sv_xlsx'):
95
+ xlsx = self.b.sv_xlsx.get().strip()
96
+ if xlsx and not self.b.sv_ph_irf.get().strip():
97
+ self.b.sv_ph_irf.set(xlsx)
98
+ elif method == 'machine_irf':
99
+ mirf = self.b._irf_fov.sv_path.get().strip()
100
+ if mirf and not self.b.sv_ph_mirf.get().strip():
101
+ self.b.sv_ph_mirf.set(mirf)
102
+ elif form_id in ('batch', 'irf'):
103
+ self.b._phasor_panel.frame.grid_remove()
104
+ self.b._fov_preview.frame.grid_remove()
105
+ label = ' Machine IRF Builder ' if form_id == 'irf' else ' Batch Processing '
106
+ self.b._preview_frame_label.configure(text=label)
107
+ # Show the IRF plot canvas if it exists (created after first build)
108
+ if form_id == 'irf' and hasattr(self.b, '_irf_plot_frame'):
109
+ self.b._irf_plot_frame.grid()
110
+ else:
111
+ self.b._phasor_panel.frame.grid_remove()
112
+ if hasattr(self.b, '_irf_plot_frame'):
113
+ self.b._irf_plot_frame.grid_remove()
114
+ self.b._fov_preview.frame.grid()
115
+
116
+ if hasattr(self.b, 'mode_status'):
117
+ self.b.mode_status.set(f"Current: {self.b._form_labels.get(form_id, form_id)}")
118
+ self.b._preview_frame_label.configure(text=' FOV Preview ')
File without changes
@@ -0,0 +1,3 @@
1
+ class BaseMode:
2
+ def __init__(self, builder):
3
+ self.b = builder
@@ -0,0 +1,312 @@
1
+ import tkinter as tk
2
+ from tkinter import ttk
3
+
4
+ from flimkit.UI.modes.base import BaseMode
5
+ from flimkit.UI.utils import PAD, _C, _section, _row, _browse_file, _browse_dir, _tog, FLIM_FILETYPES
6
+
7
+
8
+ class BatchMode(BaseMode):
9
+ def build(self):
10
+ outer, tab = self.b._form_inner_frames['batch']
11
+ tab.columnconfigure(0, weight=1)
12
+
13
+ self.b.state.sv_batch_mode = tk.StringVar(value='tiled')
14
+
15
+ self.b._batch_mode_label = ttk.Label(
16
+ tab, text='Mode: Multi-Tile ROI Fit',
17
+ font=('TkDefaultFont', 9, 'bold'), foreground='#555')
18
+ self.b._batch_mode_label.grid(row=0, column=0, sticky='w', padx=8, pady=(6, 2))
19
+
20
+ ff = _section(tab, 'Input / Output')
21
+ ff.grid(row=1, column=0, sticky='ew', pady=(0, 6))
22
+ ff.columnconfigure(1, weight=1)
23
+ self.b.state.sv_batch_xlif_dir = tk.StringVar()
24
+ self.b.state.sv_batch_ptu_dir = tk.StringVar()
25
+ self.b.state.sv_batch_out_dir = tk.StringVar()
26
+
27
+ self.b._batch_xlif_fr = ttk.Frame(ff)
28
+ self.b._batch_xlif_fr.grid(row=0, column=0, columnspan=3, sticky='ew')
29
+ self.b._batch_xlif_fr.columnconfigure(1, weight=1)
30
+ ttk.Label(self.b._batch_xlif_fr, text='XLIF folder *').grid(
31
+ row=0, column=0, sticky='e', padx=6, pady=3)
32
+ ttk.Entry(self.b._batch_xlif_fr, textvariable=self.b.sv_batch_xlif_dir, width=45).grid(
33
+ row=0, column=1, sticky='ew', padx=4, pady=3)
34
+ ttk.Button(self.b._batch_xlif_fr, text='Browse...',
35
+ command=lambda: _browse_dir(self.b.sv_batch_xlif_dir,
36
+ 'Folder of XLIF files')).grid(
37
+ row=0, column=2, padx=4, pady=3)
38
+
39
+ _row(ff, 'PTU folder *', self.b.sv_batch_ptu_dir, 1,
40
+ lambda: _browse_dir(self.b.sv_batch_ptu_dir, 'PTU tile directory'))
41
+
42
+ self.b._batch_out_fr = ttk.Frame(ff)
43
+ self.b._batch_out_fr.grid(row=2, column=0, columnspan=3, sticky='ew')
44
+ self.b._batch_out_fr.columnconfigure(1, weight=1)
45
+ ttk.Label(self.b._batch_out_fr, text='Output base dir *').grid(
46
+ row=0, column=0, sticky='e', padx=6, pady=3)
47
+ ttk.Entry(self.b._batch_out_fr, textvariable=self.b.sv_batch_out_dir, width=45).grid(
48
+ row=0, column=1, sticky='ew', padx=4, pady=3)
49
+ ttk.Button(self.b._batch_out_fr, text='Browse...',
50
+ command=lambda: _browse_dir(self.b.sv_batch_out_dir,
51
+ 'Base output directory')).grid(
52
+ row=0, column=2, padx=4, pady=3)
53
+
54
+ self.b.state.bv_batch_save_beside = tk.BooleanVar(value=False)
55
+ self.b.state.sv_batch_save_beside_preview = tk.StringVar(value='')
56
+
57
+ def _update_beside_preview(*_):
58
+ ptu = self.b.sv_batch_ptu_dir.get().strip()
59
+ preview = (ptu.rstrip('/\\') + '/save/') if ptu else '<ptu-dir>/save/'
60
+ self.b.sv_batch_save_beside_preview.set(preview)
61
+
62
+ def _on_save_beside(*_):
63
+ if self.b.bv_batch_save_beside.get():
64
+ self.b._batch_out_fr.grid_remove()
65
+ self.b._batch_beside_preview_lbl.pack(side='left', padx=2)
66
+ _update_beside_preview()
67
+ else:
68
+ self.b._batch_out_fr.grid()
69
+ self.b._batch_beside_preview_lbl.pack_forget()
70
+
71
+ self.b.sv_batch_ptu_dir.trace_add('write', _update_beside_preview)
72
+
73
+ beside_fr = ttk.Frame(ff)
74
+ beside_fr.grid(row=3, column=0, columnspan=3, sticky='w')
75
+ ttk.Checkbutton(beside_fr, text='Save beside input folder →',
76
+ variable=self.b.bv_batch_save_beside,
77
+ command=_on_save_beside).pack(side='left', padx=(4, 0))
78
+ self.b._batch_beside_preview_lbl = ttk.Label(
79
+ beside_fr, textvariable=self.b.sv_batch_save_beside_preview,
80
+ foreground='#5c9bd6')
81
+ self.b._batch_beside_preview_lbl.pack(side='left', padx=2)
82
+ self.b._batch_beside_preview_lbl.pack_forget()
83
+
84
+ self.b._batch_io_help = ttk.Label(
85
+ ff, text='One sub-folder per ROI created inside the output base dir.',
86
+ foreground='grey')
87
+ self.b._batch_io_help.grid(row=4, column=1, columnspan=2, sticky='w', padx=4)
88
+
89
+ fi = _section(tab, 'IRF')
90
+ fi.grid(row=2, column=0, sticky='ew', pady=(0, 6))
91
+ fi.columnconfigure(1, weight=1)
92
+ self.b.state.sv_batch_mirf = tk.StringVar(value=str(_C()['MACHINE_IRF_DEFAULT_PATH']))
93
+ _row(fi, 'Machine IRF (.npy) *', self.b.sv_batch_mirf, 0,
94
+ lambda: _browse_file(self.b.sv_batch_mirf, 'Machine IRF',
95
+ [('NumPy', '*.npy'), ('All', '*.*')]))
96
+
97
+ fp = _section(tab, 'Fitting Parameters', help_topic='fit_model')
98
+ fp.grid(row=3, column=0, sticky='ew', pady=(0, 6))
99
+ ttk.Label(fp, text='Fit model:').grid(row=0, column=0, sticky='w', **PAD)
100
+ self.b.state.sv_fit_model_batch = tk.StringVar(value='discrete')
101
+ ttk.Radiobutton(fp, text='n-exp', variable=self.b.sv_fit_model_batch,
102
+ value='discrete').grid(row=0, column=1, sticky='w', padx=1)
103
+ ttk.Radiobutton(fp, text='Gaussian dist.', variable=self.b.sv_fit_model_batch,
104
+ value='gaussian').grid(row=0, column=2, sticky='w', padx=1)
105
+ ttk.Radiobutton(fp, text='Lorentzian dist.', variable=self.b.sv_fit_model_batch,
106
+ value='lorentzian').grid(row=0, column=3, sticky='w', padx=1)
107
+ ttk.Radiobutton(fp, text='n-exp tail', variable=self.b.sv_fit_model_batch,
108
+ value='tail').grid(row=0, column=4, sticky='w', padx=1)
109
+
110
+ self.b.state.iv_nexp_batch = tk.IntVar(value=2)
111
+ self.b.state.iv_ncomp_dist_batch = tk.IntVar(value=1)
112
+
113
+ nexp_frame_batch = ttk.Frame(fp)
114
+ nexp_frame_batch.grid(row=1, column=0, columnspan=5, sticky='w')
115
+ ttk.Label(nexp_frame_batch, text='Components:').pack(side='left', padx=(4, 8))
116
+ for n in (1, 2, 3):
117
+ ttk.Radiobutton(nexp_frame_batch, text=str(n), variable=self.b.iv_nexp_batch,
118
+ value=n).pack(side='left', padx=4)
119
+
120
+ dist_frame_batch = ttk.Frame(fp)
121
+ ttk.Label(dist_frame_batch, text='Components:').pack(side='left', padx=(4, 8))
122
+ ttk.Radiobutton(dist_frame_batch, text='1 (unimodal)', variable=self.b.iv_ncomp_dist_batch,
123
+ value=1).pack(side='left', padx=4)
124
+ ttk.Radiobutton(dist_frame_batch, text='2 (bimodal)', variable=self.b.iv_ncomp_dist_batch,
125
+ value=2).pack(side='left', padx=4)
126
+
127
+ def _on_batch_model_change(*_):
128
+ if self.b.sv_fit_model_batch.get() in ('discrete', 'tail'):
129
+ dist_frame_batch.grid_remove()
130
+ nexp_frame_batch.grid(row=1, column=0, columnspan=5, sticky='w')
131
+ else:
132
+ nexp_frame_batch.grid_remove()
133
+ dist_frame_batch.grid(row=1, column=0, columnspan=5, sticky='w')
134
+ self.b.sv_fit_model_batch.trace_add('write', _on_batch_model_change)
135
+
136
+ ttk.Label(fp, text='Fit window (ns):').grid(row=2, column=0, sticky='w', **PAD)
137
+ self.b.state.sv_batch_tau_min = tk.StringVar(value=str(_C()['Tau_min']))
138
+ self.b.state.sv_batch_tau_max = tk.StringVar(value=str(_C()['Tau_max']))
139
+ ttk.Entry(fp, textvariable=self.b.sv_batch_tau_min, width=7).grid(row=2, column=1, padx=4)
140
+ ttk.Label(fp, text='to').grid(row=2, column=2)
141
+ ttk.Entry(fp, textvariable=self.b.sv_batch_tau_max, width=7).grid(row=2, column=3, padx=4)
142
+ ttk.Label(fp, text='ns', foreground='grey').grid(row=2, column=4, padx=4)
143
+
144
+ ttk.Label(fp, text='Colour scale (ns):').grid(row=3, column=0, sticky='w', **PAD)
145
+ self.b.state.sv_batch_tau_lo = tk.StringVar(
146
+ value='' if _C()['TAU_DISPLAY_MIN'] is None else str(_C()['TAU_DISPLAY_MIN']))
147
+ self.b.state.sv_batch_tau_hi = tk.StringVar(
148
+ value='' if _C()['TAU_DISPLAY_MAX'] is None else str(_C()['TAU_DISPLAY_MAX']))
149
+ ttk.Entry(fp, textvariable=self.b.sv_batch_tau_lo, width=7).grid(row=3, column=1, padx=4)
150
+ ttk.Label(fp, text='to').grid(row=3, column=2)
151
+ ttk.Entry(fp, textvariable=self.b.sv_batch_tau_hi, width=7).grid(row=3, column=3, padx=4)
152
+ ttk.Label(fp, text='ns (display only)', foreground='grey').grid(row=3, column=4, padx=4)
153
+
154
+ freg = _section(tab, 'Tile Registration')
155
+ freg.grid(row=4, column=0, sticky='ew', pady=(0, 6))
156
+ self.b._batch_freg = freg
157
+ self.b.state.bv_batch_register = tk.BooleanVar(value=True)
158
+ ttk.Checkbutton(freg, text='Phase-correlation registration (fixes stage Y/X drift)',
159
+ variable=self.b.bv_batch_register).grid(
160
+ row=0, column=0, columnspan=3, sticky='w', **PAD)
161
+ ttk.Label(freg, text='Max shift (px):').grid(row=1, column=0, sticky='w', **PAD)
162
+ self.b.state.sv_batch_reg_shift = tk.StringVar(value='120')
163
+ ttk.Entry(freg, textvariable=self.b.sv_batch_reg_shift, width=6).grid(
164
+ row=1, column=1, sticky='w', padx=4)
165
+ ttk.Label(freg, text='(increase if drift > 120px)',
166
+ foreground='grey').grid(row=1, column=2, sticky='w')
167
+
168
+ fm = _section(tab, 'Masking', help_topic='masking')
169
+ fm.grid(row=5, column=0, sticky='ew', pady=(0, 6))
170
+ self.b.state.bv_batch_thr = tk.BooleanVar(value=False)
171
+ self.b.state.sv_batch_thr = tk.StringVar()
172
+ ttk.Checkbutton(fm, text='Intensity threshold (min photons/px):',
173
+ variable=self.b.bv_batch_thr,
174
+ command=lambda: _tog(self.b.bv_batch_thr, self.b._batch_thr_e)).grid(
175
+ row=0, column=0, sticky='w', **PAD)
176
+ self.b._batch_thr_e = ttk.Entry(fm, textvariable=self.b.sv_batch_thr,
177
+ width=8, state='disabled')
178
+ self.b._batch_thr_e.grid(row=0, column=1, sticky='w', padx=4)
179
+ self.b.state.bv_batch_correct_pileup = tk.BooleanVar(value=False)
180
+ ttk.Checkbutton(fm, text='Apply Coates pile-up correction (recommended if pile-up > 5%)',
181
+ variable=self.b.bv_batch_correct_pileup).grid(
182
+ row=1, column=0, columnspan=3, sticky='w', **PAD)
183
+
184
+ ttk.Label(fm, text='Time-varying background PTU:').grid(row=2, column=0, sticky='w', **PAD)
185
+ self.b.state.sv_batch_tvb_ptu = tk.StringVar()
186
+ ttk.Entry(fm, textvariable=self.b.sv_batch_tvb_ptu, width=24).grid(
187
+ row=2, column=1, sticky='ew', padx=4)
188
+ ttk.Button(fm, text='Browse...',
189
+ command=lambda: _browse_file(self.b.sv_batch_tvb_ptu,
190
+ 'Background reference PTU',
191
+ FLIM_FILETYPES)).grid(
192
+ row=2, column=2, sticky='w', padx=4)
193
+ ttk.Label(fm, text='(optional: applied to every FOV in the batch)',
194
+ foreground='grey').grid(row=3, column=0, columnspan=3, sticky='w', padx=8)
195
+
196
+ fexp = _section(tab, 'Image Export')
197
+ fexp.grid(row=6, column=0, sticky='ew', pady=(0, 6))
198
+ self.b.state.bv_batch_save_lifetime = tk.BooleanVar(value=True)
199
+ self.b.state.bv_batch_save_rgb = tk.BooleanVar(value=True)
200
+ self.b.state.bv_batch_save_intensity = tk.BooleanVar(value=True)
201
+ self.b.state.bv_batch_save_npy = tk.BooleanVar(value=True)
202
+ self.b.state.bv_batch_save_ind = tk.BooleanVar(value=False)
203
+ ttk.Checkbutton(fexp, text='Lifetime image (uint16 TIFF)',
204
+ variable=self.b.bv_batch_save_lifetime).grid(row=0, column=0, sticky='w', **PAD)
205
+ ttk.Checkbutton(fexp, text='Component RGB TIFF',
206
+ variable=self.b.bv_batch_save_rgb).grid(row=0, column=1, sticky='w', **PAD)
207
+ ttk.Checkbutton(fexp, text='Intensity TIFF',
208
+ variable=self.b.bv_batch_save_intensity).grid(row=0, column=2, sticky='w', **PAD)
209
+ ttk.Checkbutton(fexp, text='Raw maps (.npy)',
210
+ variable=self.b.bv_batch_save_npy).grid(row=1, column=0, sticky='w', **PAD)
211
+ ttk.Checkbutton(fexp, text='Individual component maps (τ₁, a₁, τ₂...)',
212
+ variable=self.b.bv_batch_save_ind).grid(row=1, column=1, columnspan=2, sticky='w', **PAD)
213
+ self.b.state.bv_batch_save_stack = tk.BooleanVar(value=True)
214
+ ttk.Checkbutton(fexp, text='4D stacks - (T, H, W) .npy per map (timelapse only)',
215
+ variable=self.b.bv_batch_save_stack).grid(row=2, column=0, columnspan=3, sticky='w', **PAD)
216
+ ttk.Label(fexp, text='Lifetime colour scale (ns):').grid(row=3, column=0, sticky='w', **PAD)
217
+ ttk.Entry(fexp, textvariable=self.b.sv_batch_tau_lo, width=7).grid(row=3, column=1, sticky='w', padx=4)
218
+ ttk.Label(fexp, text='to').grid(row=3, column=2)
219
+ ttk.Entry(fexp, textvariable=self.b.sv_batch_tau_hi, width=7).grid(row=3, column=3, sticky='w', padx=4)
220
+ ttk.Label(fexp, text='ns (blank = auto)', foreground='grey').grid(row=3, column=4, sticky='w', padx=4)
221
+ ttk.Label(fexp, text='Gamma (lifetime image):').grid(row=4, column=0, sticky='w', **PAD)
222
+ self.b.state.sv_batch_gamma = tk.StringVar(value='0.4')
223
+ ttk.Entry(fexp, textvariable=self.b.sv_batch_gamma, width=5).grid(row=4, column=1, sticky='w', padx=4)
224
+ ttk.Label(fexp, text='(0.4 = boost dim tissue; 1.0 = linear)',
225
+ foreground='grey').grid(row=4, column=2, columnspan=3, sticky='w')
226
+ ttk.Label(fexp, text='Intensity display max:').grid(row=5, column=0, sticky='w', **PAD)
227
+ self.b.state.sv_batch_int_max = tk.StringVar()
228
+ ttk.Entry(fexp, textvariable=self.b.sv_batch_int_max, width=8).grid(row=5, column=1, sticky='w', padx=4)
229
+ ttk.Label(fexp, text='(blank = auto 99th percentile)',
230
+ foreground='grey').grid(row=5, column=2, columnspan=3, sticky='w')
231
+
232
+ ttk.Label(fexp, text='Lifetime weighting:').grid(row=6, column=0, sticky='w', **PAD)
233
+ self.b.state.sv_batch_tau_weighting = tk.StringVar(value='amplitude')
234
+ ttk.Combobox(fexp, textvariable=self.b.sv_batch_tau_weighting, state='readonly',
235
+ values=['amplitude', 'intensity'], width=11).grid(row=6, column=1, sticky='w', padx=4)
236
+ ttk.Label(fexp, text='(τ map / lifetime image; both raw .npy saved either way)',
237
+ foreground='grey').grid(row=6, column=2, columnspan=3, sticky='w')
238
+
239
+ ftl = _section(tab, 'Timelapse - Reference Lifetimes')
240
+ ftl.grid(row=7, column=0, sticky='ew', pady=(0, 6))
241
+ self.b._batch_tl_fr = ftl
242
+
243
+ self.b.state.bv_tl_fix_tau = tk.BooleanVar(value=False)
244
+ self.b.state.sv_tl_tau1 = tk.StringVar(value='')
245
+ self.b.state.sv_tl_tau2 = tk.StringVar(value='')
246
+ self.b.state.sv_tl_tau3 = tk.StringVar(value='')
247
+
248
+ ttk.Checkbutton(ftl,
249
+ text='Fix reference τ values (skip pooled-decay fit)',
250
+ variable=self.b.bv_tl_fix_tau,
251
+ command=lambda: _tog_tl_entries()).grid(
252
+ row=0, column=0, columnspan=6, sticky='w', **PAD)
253
+
254
+ self.b._tl_tau_fr = ttk.Frame(ftl)
255
+ self.b._tl_tau_fr.grid(row=1, column=0, columnspan=6, sticky='w')
256
+
257
+ def _make_tau_box(col, sym, var):
258
+ box = ttk.Frame(self.b._tl_tau_fr)
259
+ box.grid(row=0, column=col, sticky='w')
260
+ ttk.Label(box, text=sym).grid(row=0, column=0, sticky='e', padx=(8, 2))
261
+ e = ttk.Entry(box, textvariable=var, width=8, state='disabled')
262
+ e.grid(row=0, column=1, padx=4)
263
+ return box, e
264
+
265
+ self.b._tl_tau1_box, self.b._tl_tau1_e = _make_tau_box(0, 'τ₁ (ns):', self.b.sv_tl_tau1)
266
+ self.b._tl_tau2_box, self.b._tl_tau2_e = _make_tau_box(1, 'τ₂ (ns):', self.b.sv_tl_tau2)
267
+ self.b._tl_tau3_box, self.b._tl_tau3_e = _make_tau_box(2, 'τ₃ (ns):', self.b.sv_tl_tau3)
268
+ ttk.Label(self.b._tl_tau_fr, text='(leave blank to fit from pooled data)',
269
+ foreground='grey').grid(row=0, column=3, padx=6)
270
+
271
+ def _tog_tl_entries():
272
+ state = 'normal' if self.b.bv_tl_fix_tau.get() else 'disabled'
273
+ self.b._tl_tau1_e.config(state=state)
274
+ self.b._tl_tau2_e.config(state=state)
275
+ self.b._tl_tau3_e.config(state=state)
276
+
277
+ def _update_tl_tau_count(*_):
278
+ n = self.b.iv_nexp_batch.get()
279
+ (self.b._tl_tau2_box.grid() if n >= 2 else self.b._tl_tau2_box.grid_remove())
280
+ (self.b._tl_tau3_box.grid() if n >= 3 else self.b._tl_tau3_box.grid_remove())
281
+
282
+ self.b.iv_nexp_batch.trace_add('write', _update_tl_tau_count)
283
+ _update_tl_tau_count()
284
+ self.b.state.bv_tl_pool_positions = tk.BooleanVar(value=False)
285
+ ttk.Checkbutton(ftl,
286
+ text='Pool all positions for a shared reference τ '
287
+ '(default: each position gets its own τ)',
288
+ variable=self.b.bv_tl_pool_positions).grid(
289
+ row=2, column=0, columnspan=5, sticky='w', **PAD)
290
+
291
+ self.b.state.bv_tl_bound_fraction = tk.BooleanVar(value=False)
292
+ ttk.Checkbutton(ftl,
293
+ text='Compute bound fraction α₂/(α₁+α₂) '
294
+ '(off by default - verify component ordering)',
295
+ variable=self.b.bv_tl_bound_fraction).grid(
296
+ row=3, column=0, columnspan=5, sticky='w', **PAD)
297
+
298
+ ftl.grid_remove()
299
+
300
+ self.b._expert_banner_batch = ttk.Label(
301
+ tab, text='⚙ Custom expert settings active',
302
+ foreground='#e8a838', font=('TkDefaultFont', 9, 'bold'))
303
+ self.b._expert_banner_batch.grid(row=8, column=0, sticky='w', padx=8)
304
+ self.b._expert_banner_batch.grid_remove()
305
+
306
+ btn_row_batch = ttk.Frame(tab)
307
+ btn_row_batch.grid(row=9, column=0, pady=8)
308
+ ttk.Button(btn_row_batch, text='⚙ Expert Settings',
309
+ command=self.b._open_expert_settings).pack(side='left', padx=4)
310
+ self.b._btn_batch = ttk.Button(btn_row_batch, text='▶ Run Batch ROI Fit',
311
+ command=self.b._dispatch_batch)
312
+ self.b._btn_batch.pack(side='left', padx=4, ipadx=20, ipady=4)