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/utils.py ADDED
@@ -0,0 +1,217 @@
1
+ from __future__ import annotations
2
+ import re
3
+ import time
4
+ import threading
5
+ import tkinter as tk
6
+ from tkinter import ttk, filedialog, scrolledtext
7
+ from pathlib import Path
8
+ from typing import Optional
9
+ import numpy as np
10
+ from flimkit.UI.fit_help import help_button
11
+ from flimkit.utils.config_snapshot import _C
12
+ from flimkit.utils.session import (
13
+ _reconstruct_dict_from_session,
14
+ _safe_array_from_json,
15
+ _parse_summary,
16
+ )
17
+ try:
18
+ from tkinterdnd2 import DND_FILES, DND_TEXT
19
+ HAS_DND = True
20
+ except ImportError:
21
+ HAS_DND = False
22
+
23
+ def _enable_dnd(root):
24
+ if not HAS_DND:
25
+ return False
26
+ try:
27
+ from tkinterdnd2 import TkinterDnD
28
+ TkinterDnD._require(root)
29
+ return True
30
+ except Exception:
31
+ return False
32
+
33
+ class _Redirect:
34
+
35
+ def __init__(self, widget: scrolledtext.ScrolledText, buf: list, root=None, is_stderr=False):
36
+ self.widget = widget
37
+ self.buf = buf
38
+ self.root = root
39
+ self._is_stderr = is_stderr
40
+ self._batch = []
41
+ self._batch_size = 5000
42
+ self._last_flush = time.time()
43
+ self._flush_interval = 0.5
44
+
45
+ def write(self, text: str):
46
+ if not text:
47
+ return
48
+ self.buf.append(text)
49
+ self._batch.append(text)
50
+ if self._is_stderr:
51
+ try:
52
+ from flimkit.utils.crash_handler import log_event
53
+ log_event(f'STDERR: {text.rstrip()}', level='warning')
54
+ except Exception:
55
+ pass
56
+ should_flush = False
57
+ if len(''.join(self._batch)) >= self._batch_size:
58
+ should_flush = True
59
+ elif time.time() - self._last_flush >= self._flush_interval:
60
+ should_flush = True
61
+ if should_flush:
62
+ self._flush_batch()
63
+
64
+ def _flush_batch(self):
65
+ if not self._batch:
66
+ return
67
+ text = ''.join(self._batch)
68
+ self._batch.clear()
69
+ if self.root:
70
+ self.root.after(0, self._update_widget, text)
71
+ else:
72
+ self._update_widget(text)
73
+
74
+ def _update_widget(self, text):
75
+ try:
76
+ self.widget.configure(state='normal')
77
+ self.widget.insert(tk.END, text)
78
+ self.widget.see(tk.END)
79
+ self.widget.configure(state='disabled')
80
+ self.widget.update_idletasks()
81
+ except Exception:
82
+ pass
83
+ self._last_flush = time.time()
84
+
85
+ def flush(self):
86
+ self._flush_batch()
87
+
88
+ class _FileRedirect:
89
+
90
+ def __init__(self, filepath: str, buf: list):
91
+ self.filepath = filepath
92
+ self.buf = buf
93
+ self._file = None
94
+ try:
95
+ self._file = open(filepath, 'w', buffering=1)
96
+ except Exception:
97
+ pass
98
+
99
+ def write(self, text: str):
100
+ if not text:
101
+ return
102
+ self.buf.append(text)
103
+ if self._file:
104
+ try:
105
+ self._file.write(text)
106
+ except Exception:
107
+ pass
108
+
109
+ def flush(self):
110
+ if self._file:
111
+ try:
112
+ self._file.flush()
113
+ except Exception:
114
+ pass
115
+
116
+ def close(self):
117
+ if self._file:
118
+ try:
119
+ self._file.close()
120
+ except Exception:
121
+ pass
122
+ self._file = None
123
+
124
+ class _FileTailer:
125
+
126
+ def __init__(self, filepath: str, widget: scrolledtext.ScrolledText, update_interval_ms: int = 200):
127
+ self.filepath = filepath
128
+ self.widget = widget
129
+ self.update_interval_ms = update_interval_ms
130
+ self._file = None
131
+ self._last_pos = 0
132
+ self._running = False
133
+
134
+ def start(self, root):
135
+ self._running = True
136
+ self._poll_file(root)
137
+
138
+ def _poll_file(self, root):
139
+ if not self._running:
140
+ return
141
+ try:
142
+ if not Path(self.filepath).exists():
143
+ root.after(self.update_interval_ms, lambda: self._poll_file(root))
144
+ return
145
+ with open(self.filepath, 'r') as f:
146
+ f.seek(self._last_pos)
147
+ new_content = f.read()
148
+ self._last_pos = f.tell()
149
+ if new_content:
150
+ self.widget.configure(state='normal')
151
+ self.widget.insert(tk.END, new_content)
152
+ self.widget.see(tk.END)
153
+ self.widget.configure(state='disabled')
154
+ self.widget.update_idletasks()
155
+ except Exception:
156
+ pass
157
+ root.after(self.update_interval_ms, lambda: self._poll_file(root))
158
+
159
+ def stop(self):
160
+ self._running = False
161
+
162
+ PAD = dict(padx=8, pady=4)
163
+
164
+ from flimkit.formats import file_dialog_filetypes
165
+ FLIM_FILETYPES = file_dialog_filetypes() + [('All', '*.*')]
166
+
167
+ def _browse_file(var, title='Select file', filetypes=(('All', '*.*'),)):
168
+ p = filedialog.askopenfilename(title=title, filetypes=filetypes)
169
+ if p:
170
+ var.set(p)
171
+
172
+ def _browse_dir(var, title='Select directory'):
173
+ p = filedialog.askdirectory(title=title)
174
+ if p:
175
+ var.set(p)
176
+
177
+ def _row(parent, label, var, row, browse_fn, width=45, state='normal'):
178
+ ttk.Label(parent, text=label).grid(
179
+ row=row, column=0, sticky='e', padx=6, pady=3)
180
+ e = ttk.Entry(parent, textvariable=var, width=width, state=state)
181
+ e.grid(row=row, column=1, sticky='ew', padx=4, pady=3)
182
+ ttk.Button(parent, text='Browse...', command=browse_fn).grid(
183
+ row=row, column=2, padx=4, pady=3)
184
+ if HAS_DND:
185
+ try:
186
+ def _drop_handler(event):
187
+ data = event.data.strip()
188
+ if data.startswith('{') and data.endswith('}'):
189
+ data = data[1:-1]
190
+ var.set(data)
191
+ e.drop_target_register(DND_FILES, DND_TEXT)
192
+ e.dnd_bind('<<Drop>>', _drop_handler)
193
+ except Exception:
194
+ pass
195
+ return e
196
+
197
+ def _section(parent, text: str, help_topic: Optional[str] = None) -> ttk.LabelFrame:
198
+ lf = ttk.LabelFrame(parent, text=f' {text} ', padding=(10, 6))
199
+ if help_topic:
200
+ holder = ttk.Frame(lf)
201
+ ttk.Label(holder, text=f' {text} ').pack(side='left')
202
+ help_button(holder, help_topic).pack(side='left', padx=(0, 6))
203
+ lf.configure(labelwidget=holder)
204
+ return lf
205
+
206
+ def _tog(bvar: tk.BooleanVar, entry: ttk.Entry):
207
+ entry.configure(state='normal' if bvar.get() else 'disabled')
208
+
209
+ def _flt(sv: tk.StringVar) -> Optional[float]:
210
+ v = sv.get().strip()
211
+ return float(v) if v and v.lower() != 'none' else None
212
+
213
+ def _thresh(bvar: tk.BooleanVar, sv: tk.StringVar):
214
+ if not bvar.get():
215
+ return None
216
+ v = sv.get().strip()
217
+ return int(v) if v else None
flimkit/__init__.py ADDED
File without changes
flimkit/_version.py ADDED
@@ -0,0 +1,41 @@
1
+ __version__ = '0.12.0'
2
+ fitter_version = '19'
3
+
4
+ roadmap = '''Flim program roadmap:
5
+ Version history:
6
+ 0.1.0 -> Initial version with basic FLIM fitting functionality
7
+ 0.1.1 -> Added command line interface (CLI) for single FOV fitting
8
+ 0.1.2 -> Added support for IRF estimation from XLSX files
9
+ 0.1.3 -> Added some user prompts and created defaults
10
+ 0.1.4 -> Updated interactive prompts and added some error handling
11
+ Will be updated to 0.2.0 when ROI stitching works
12
+ 0.3.0 -> ROI stiching + fitting
13
+ 0.4.0 -> batch processing of multiple ROIs or FOVs
14
+ 0.5.0 -> tbd
15
+ Need to add Phasor once added to the codebase, and update version to 1.0. And added some tests.
16
+ 0.8.0 -> added per-tile machine IRF fitting workflow, updated docs and examples
17
+ 0.8.1 -> added some error handling and user prompts to per-tile machine IRF workflow
18
+ 0.8.2 -> added some more error handling and user prompts to per-tile machine IRF workflow, and updated example notebook
19
+ 0.9.0 -> made flim fitted image visible in GUI
20
+ 0.9.1 -> added session restoration in the GUI
21
+ 0.9.2 -> added some error handling and begin session restoration for roi analysis panel in the GUI
22
+ 0.9.3 -> complete session restoration for roi analysis panel
23
+ 0.9.5 -> selectable ROIs in FLIM image in GUI and export of region stats
24
+ 0.9.7 -> added FRET (using PhasorPy)
25
+ 0.9.8 -> added pile-up correction function
26
+ 0.9.9 -> UI fixes
27
+ 0.9.10 -> fixed GPU tests
28
+ 0.9.11 -> updated PhasorPy calls to match latest version (0.10.0) and added phasor filtering options
29
+ 0.9.12 -> changed masking to use cellpose-SAM
30
+ 0.9.13 -> added Gaussian and Lorentzian lifetime distribution fitting (Lakowicz §4.11.2)
31
+ 0.9.14 -> bug fixes galore
32
+ 0.9.15 -> new file format support: ISS (TD-FLIM .TAGTIME/.TAGCHANNEL/.TAGDECAY, FD-FLIM phasor .ifli; see issue #19), Becker & Hickl .sdt (SPC TCSPC FLIM, FIFO-image histograms; decoder written from B&H SPCM file-structure docs) and Photonscore .photons (LINCam D7 container; pure-Python, no native dependency, validated bit-exact vs the Photonscore SDK; see issue #20)
33
+ 0.9.16 -> fixed pile-up correction: Coates was being fed the photon/record count instead of the excitation-pulse count (N_sync), which destroyed the decay tail and mis-reported pile-up (0.07% read as 97%). N_sync is now taken from the true sync count per format, and Coates refuses any count that cannot be one.
34
+ 0.9.17 -> user-settable fit window + exclusion bands (--fit-start-ns/--fit-end-ns/--exclude-ns), so reflection peaks mid-decay can be dropped from the fit; the per-pixel path now honours the window at all (it previously projected over every bin, IRF rise and artefacts included). Pile-up can now go in the forward model (n_sync=) instead of correcting the data, which keeps the fit Poisson.
35
+ 0.9.18 -> added the n-exponential tail fit: bare exponentials fitted past the decay peak with no IRF and no reconvolution, y(t) = sum A[i] exp(-(t-t0)/tau[i]) + Bkgr. Reports the Leica quantities (per-component intensities I[k] = A[k]tau[k], I_sum, A_sum, t0) alongside the usual amplitude- and intensity-weighted mean lifetimes. t0 is pinned to the decay peak by default because it is strongly correlated with the amplitudes; free it from expert settings or --fit-t0. Available in FOV, batch and stitch modes, and as --fit-model tail on the CLI.
36
+ 0.9.19 -> multidimensional (z-stack and/or timelapse) tile stitching: a fourth Stitch pipeline that walks every (timepoint, z) plane, stitches the overlapping tiles for that plane and fits it, so overlapping positions are no longer treated as separate fields of view. One decay is pooled across the series and the lifetimes held fixed for every plane, so amplitudes stay comparable between timepoints. Tile positions come from the XLIF/LIF when one is given and are then refined against the tile overlap, which matters because the nominal stage coordinates can be out by tens of pixels; without metadata they are recovered from the overlap alone. Results browse with a plane slider in the FOV preview.
37
+ 0.10.0 -> add-on system: analysis tools, file formats, format sniffers and phasor filters register themselves through a flimkit.plugins registry instead of being hard-coded, and FLIMKit's own tools go through the same registry a third party would use. Plugins load from ~/.flimkit/plugins (off until you turn it on), from folders named in the config or FLIMKIT_PLUGIN_PATH, and from installed packages that declare a flimkit.plugins entry point. A plugin that raises on import has its registrations rolled back and cannot take the app down; Help > Plugins reports what loaded and what failed, and turns individual plugins off. FLIMKIT_NO_PLUGINS=1 or --no-plugins skips the lot for a reproducible baseline. The v1 hook signatures are frozen and a fixture plugin written against them is loaded by the test suite on every run. Adds pyproject.toml so an installed plugin can announce itself.
38
+ 0.10.1 -> the GPU per-pixel path honours a fit window, so dropping a reflection peak out of the fit no longer costs the GPU. Two fitting bugs found and fixed by Zhen Yuan: one-component free-tau gave different answers depending on whether a backend was selected, because the CPU grid-scanned it and the backend used the SciPy solver; and per-pixel distribution fits ignored the fit window that the summed fit honoured, so an excluded artefact stayed in every per-pixel map. Add-ons can now be installed into the compiled app as a wheel, and the GUI explains the fit settings in place.
39
+ 0.12.0 -> on PyPI, so pip install flimkit gets the analysis core without the desktop, with gui, torch, mlx, notebook, segmentation and test as extras. Plugins now load when a format is looked up, so a plugin-registered format works from a script or a CLI and not only from the GUI. Per-pixel fitting on the GPU works in pixel blocks, so a field larger than a gigabyte reaches the GPU instead of silently falling back to the CPU; a 1024 square field fits in 7.8s against 25.2s on the CPU. The per-pixel tau grid goes from 200 points to 1600, which cuts the error against known-truth synthetic data from 0.021 ns to 0.008 ns, so per-pixel lifetimes will differ slightly from earlier versions. Cancelling a tile fit is honoured through canvas assembly and saving rather than only during the fitting. Leica .xlef projects are read for tile positions.
40
+ 1.0.0 -> release version with all core features implemented and tested
41
+ 1.5.0 (or 2.0) -> validation of fitting results with known fluorophores.'''
flimkit/cli.py ADDED
@@ -0,0 +1,120 @@
1
+ import argparse
2
+ import os
3
+
4
+
5
+ def main(fast=False, cli=False, check_updates=False, no_plugins=False, plugin_path=None):
6
+ if no_plugins:
7
+ os.environ['FLIMKIT_NO_PLUGINS'] = '1'
8
+ if plugin_path:
9
+ existing = os.environ.get('FLIMKIT_PLUGIN_PATH', '')
10
+ os.environ['FLIMKIT_PLUGIN_PATH'] = (
11
+ existing + os.pathsep + os.pathsep.join(plugin_path) if existing
12
+ else os.pathsep.join(plugin_path))
13
+ if check_updates:
14
+ from flimkit.utils.update_check import (
15
+ check_installation_freshness,
16
+ format_update_report,
17
+ )
18
+ report = format_update_report(
19
+ check_installation_freshness(timeout=3.0, do_fetch=True)
20
+ )
21
+ print(report)
22
+ return
23
+ if not cli:
24
+ from flimkit.UI.gui import launch_gui
25
+ launch_gui()
26
+ return
27
+ from flimkit import plugins
28
+ plugins.ensure_loaded()
29
+ from flimkit.interactive import single_FOV_flim_fit, stitch_and_fit, stitch_tiles, timelapse_flim_fit, zstack_flim_fit
30
+ import inquirer
31
+ from flimkit._version import __version__, roadmap
32
+ from flimkit.utils.fancy import display_banner, flim_fitting_banner, banner_goodbye
33
+ if fast == False:
34
+ display_banner()
35
+ print('Welcome to the FLIM data processing tool!')
36
+ questions = [
37
+ inquirer.List(
38
+ 'process_option',
39
+ message='Choose a processing option',
40
+ choices=[
41
+ 'FLIM FIT a single FOV',
42
+ 'Phasor analysis',
43
+ 'Reconstruct a FOV and FLIM FIT',
44
+ 'Just stitch multiple tiles together',
45
+ 'Timelapse batch fit',
46
+ 'Z-stack batch fit',
47
+ 'About',
48
+ 'Exit'
49
+ ]
50
+ )
51
+ ]
52
+ answers = inquirer.prompt(questions)
53
+ if answers['process_option'] == 'FLIM FIT a single FOV':
54
+ if fast == False:
55
+ flim_fitting_banner()
56
+ print('FLIM FITting a single FOV...')
57
+ single_FOV_flim_fit(interactive=True)
58
+ elif answers['process_option'] == 'Phasor analysis':
59
+ from flimkit.phasor_launcher import phasor_inquire
60
+ phasor_inquire()
61
+ elif answers['process_option'] == 'Reconstruct a FOV and FLIM FIT':
62
+ print('Reconstructing a FOV and FLIM FITting...')
63
+ stitch_and_fit(interactive=True)
64
+ elif answers['process_option'] == 'Just stitch multiple tiles together':
65
+ print('Stitching multiple tiles together...')
66
+ stitch_tiles(interactive=True)
67
+ elif answers['process_option'] == 'Timelapse batch fit':
68
+ print('Timelapse batch FLIM fitting...')
69
+ timelapse_flim_fit(interactive=True)
70
+ elif answers['process_option'] == 'Z-stack batch fit':
71
+ print('Z-stack batch FLIM fitting...')
72
+ zstack_flim_fit(interactive=True)
73
+ elif answers['process_option'] == 'About':
74
+ print('Current version: ' + __version__)
75
+ try:
76
+ from flimkit.utils.update_check import (
77
+ check_installation_freshness,
78
+ format_update_report,
79
+ )
80
+ print()
81
+ print(format_update_report(
82
+ check_installation_freshness(timeout=2.5, do_fetch=False)
83
+ ))
84
+ except Exception as exc:
85
+ print(f'Update check unavailable: {exc}')
86
+ print(roadmap)
87
+ return
88
+ else:
89
+ banner_goodbye()
90
+ return
91
+
92
+ def run(argv=None):
93
+ import multiprocessing
94
+ multiprocessing.freeze_support()
95
+ parser = argparse.ArgumentParser(description='FLIMKit — FLIM data processing toolkit')
96
+ parser.add_argument('--cli', action='store_true', help='=Run in CLI mode')
97
+ parser.add_argument('--fast', action='store_true', help='Skip banner display')
98
+ parser.add_argument(
99
+ '--check-updates',
100
+ action='store_true',
101
+ help='Check whether git checkout and local version are up to date, then exit',
102
+ )
103
+ parser.add_argument(
104
+ '--no-plugins',
105
+ action='store_true',
106
+ help='Skip every plugin, built in and user, for a reproducible baseline',
107
+ )
108
+ parser.add_argument(
109
+ '--plugins',
110
+ action='append',
111
+ metavar='DIR',
112
+ help='Extra folder to load plugins from, repeatable',
113
+ )
114
+ args = parser.parse_args(argv)
115
+ main(fast=args.fast, cli=args.cli, check_updates=args.check_updates,
116
+ no_plugins=args.no_plugins, plugin_path=args.plugins)
117
+
118
+
119
+ if __name__ == '__main__':
120
+ run()
flimkit/configs.py ADDED
@@ -0,0 +1,148 @@
1
+ import sys
2
+ from matplotlib.colors import LinearSegmentedColormap
3
+ from pathlib import Path
4
+
5
+ FLIM_CMAP = LinearSegmentedColormap.from_list(
6
+ 'flim', ['#000080','#0000ff','#00ffff','#00ff00','#ffff00','#ff0000']
7
+ )
8
+
9
+ MIN_PHOTONS_PERPIX = 10
10
+
11
+ # Intensity threshold for masking low-signal pixels before fitting.
12
+ # Set to None (disabled) or an integer photon count.
13
+ # Use --intensity-threshold on the CLI, or 'interactive' to pick visually.
14
+ INTENSITY_THRESHOLD = None
15
+
16
+
17
+ # General fitting settings:
18
+ Tau_min = 0.145
19
+ Tau_max = 45.0
20
+
21
+ # Set default fitting mode. Options are "summed", "perPixel", and "both". Override with --mode when running the code.
22
+ D_mode = 'both'
23
+
24
+ # Default number of exponentials to fit. Options are 1, 2, or 3. Override with --nexp when running the code. 4+ exponentials are not supported as the fitting becomes unstable and unreliable.
25
+ n_exp = 3
26
+
27
+ # Default binning factor for per-pixel fitting. Set to 1 for no binning. Override with --binning when running the code.
28
+ binning_factor = 1
29
+ # Default optimizer for per-pixel fitting. Options are "lm_multistart" and "de".
30
+ Optimizer = 'de'
31
+
32
+ # Levenberg-Marquardt settins:
33
+ lm_restarts = 8
34
+
35
+ # Default settings for DE optimizer:
36
+ de_population = 30
37
+ de_maxiter = 5000
38
+ n_workers = -1
39
+
40
+ # IRF settings:
41
+ IRF_FWHM = None
42
+ # ns window (centred on the decay peak) the parametric IRF is fit over. Keep it
43
+ # tight: a wide window pulls in the fluorescence falling edge, so the fit traces
44
+ # the decay instead of the instrument pulse and yields an over-broad IRF.
45
+ IRF_FIT_WIDTH = 0.5
46
+ IRF_BINS = 21
47
+ Estimate_IRF = 'none'
48
+
49
+ # Machine IRF defaults (spreadsheet-free workflow)
50
+
51
+ # True when launched as a PyInstaller-compiled executable.
52
+ _is_frozen = getattr(sys, 'frozen', False)
53
+
54
+ # Bundled read-only resources - always accessible via __file__ (even when frozen,
55
+ # PyInstaller extracts them to a temp dir that __file__ still points into).
56
+ _BUNDLED_MACHINE_IRF_DIR = Path(__file__).resolve().parent / 'machine_irf'
57
+
58
+ # Writable location for user-generated IRF files.
59
+ # When frozen the package dir is inside a read-only bundle, so saves go to
60
+ # ~/.flimkit/machine_irf/ instead. When running from source the behaviour is
61
+ # unchanged (saves alongside the package).
62
+ USER_MACHINE_IRF_DIR = Path.home() / '.flimkit' / 'machine_irf'
63
+ MACHINE_IRF_DIR = USER_MACHINE_IRF_DIR if _is_frozen else _BUNDLED_MACHINE_IRF_DIR
64
+
65
+ # Default IRF path used when no explicit path is given.
66
+ # When frozen: prefer a user-saved copy (if it exists), else fall back to the
67
+ # bundled factory default so a fresh install still works out of the box.
68
+ _user_default = USER_MACHINE_IRF_DIR / 'machine_irf_default.npy'
69
+ MACHINE_IRF_DEFAULT_PATH = (
70
+ (_user_default if _user_default.exists() else _BUNDLED_MACHINE_IRF_DIR / 'machine_irf_default.npy')
71
+ if _is_frozen
72
+ else _BUNDLED_MACHINE_IRF_DIR / 'machine_irf_default.npy'
73
+ )
74
+ MACHINE_IRF_ALIGN_ANCHOR = 'peak'
75
+ MACHINE_IRF_REDUCER = 'median'
76
+ MACHINE_IRF_FIT_STRATEGY = 'fixed'
77
+ MACHINE_IRF_FIT_BG = True
78
+ MACHINE_IRF_FIT_SIGMA = False
79
+ MACHINE_IRF_FIT_TAIL = False
80
+ MACHINE_IRF_SIGMA_MAX_FULL = 3.0
81
+ MACHINE_IRF_SIGMA_MAX_HALF = 0.5
82
+ MACHINE_IRF_DE_POPULATION = 30
83
+ MACHINE_IRF_DE_MAXITER = 5000
84
+
85
+ # Cost function for summed fit. Options are "poisson" (recommended) and "chi2" (legacy).
86
+ # "poisson" uses Poisson deviance (C-statistic) on raw counts - statistically correct.
87
+ # "chi2" normalises by peak and uses Neyman weights - underweights the tail.
88
+ Cost_function = 'poisson'
89
+
90
+ # Display range for exported tau images (FLIM microscope clipping).
91
+ # Out-of-range pixels are clipped to the nearest boundary, matching FLIM microscope software behaviour.
92
+ # Set to None to keep the full fitted range (no clipping).
93
+ TAU_DISPLAY_MIN = None
94
+ TAU_DISPLAY_MAX = None
95
+
96
+ # Display range for exported intensity images (same clipping behaviour).
97
+ # Set to None to keep the full range.
98
+ INTENSITY_DISPLAY_MIN = None
99
+ INTENSITY_DISPLAY_MAX = None
100
+
101
+ # Phasor-domain spatial filtering applied after calibration.
102
+ # Set to None to disable. Options: None, 'gaussian', 'median', 'wavelet'.
103
+ PHASOR_FILTER = None
104
+ PHASOR_FILTER_SIGMA = 1.0
105
+ PHASOR_FILTER_SIZE = 3
106
+ PHASOR_FILTER_WAVELET = 'db4'
107
+ PHASOR_FILTER_LEVEL = 1
108
+
109
+ # Other specific settings:
110
+ channels = None
111
+ OUT_NAME = 'flim_out'
112
+
113
+
114
+ config_message = f"""Default settings:
115
+ Intensity threshold: {INTENSITY_THRESHOLD} photons (None = disabled)
116
+ Tau_min: {Tau_min} ns
117
+ Tau_max: {Tau_max} ns
118
+ Tau display min: {TAU_DISPLAY_MIN} ns (None = no clip)
119
+ Tau display max: {TAU_DISPLAY_MAX} ns (None = no clip)
120
+ Intensity display min: {INTENSITY_DISPLAY_MIN} (None = no clip)
121
+ Intensity display max: {INTENSITY_DISPLAY_MAX} (None = no clip)
122
+ Fitting mode: {D_mode}
123
+ Number of exponentials: {n_exp}
124
+ Cost function: {Cost_function}
125
+ Optimizer: {Optimizer}
126
+ Levenberg-Marquardt restarts: {lm_restarts}
127
+ Differential Evolution population size: {de_population}
128
+ Differential Evolution max iterations: {de_maxiter}
129
+ Number of workers for DE optimization: {n_workers}
130
+ IRF FWHM: {IRF_FWHM} ns
131
+ IRF fit width: {IRF_FIT_WIDTH} ns
132
+ IRF bins: {IRF_BINS}
133
+ IRF estimation method: {Estimate_IRF}
134
+ Machine IRF default path: {MACHINE_IRF_DEFAULT_PATH}
135
+ Machine IRF align anchor: {MACHINE_IRF_ALIGN_ANCHOR}
136
+ Machine IRF reducer: {MACHINE_IRF_REDUCER}
137
+ Machine IRF fit strategy: {MACHINE_IRF_FIT_STRATEGY}
138
+ Machine IRF fit bg/sigma/tail: {MACHINE_IRF_FIT_BG}/{MACHINE_IRF_FIT_SIGMA}/{MACHINE_IRF_FIT_TAIL}
139
+ Machine IRF DE population/maxiter: {MACHINE_IRF_DE_POPULATION}/{MACHINE_IRF_DE_MAXITER}
140
+ Channels to fit: {channels}
141
+ Output directory: {OUT_NAME}
142
+ Change any of these defaults by passing the corresponding argument when running the code.
143
+ Run `python -m flim_fitter --help` for more details on available arguments and their usage.
144
+ Change defaults in configs.py or override with command line arguments as needed for different datasets and systems.
145
+ """
146
+
147
+ if __name__ == '__main__':
148
+ print(config_message)
flimkit/dialogs.py ADDED
@@ -0,0 +1,46 @@
1
+ _provider = None
2
+
3
+ def set_dialog_provider(fn):
4
+ global _provider
5
+ _provider = fn
6
+
7
+ def get_dialog_provider():
8
+ return _provider
9
+
10
+ def _tk_save_path(title, default_name, defaultextension, filetypes):
11
+ import tkinter as tk
12
+ from tkinter import filedialog
13
+ existing = tk._default_root
14
+ if existing is not None:
15
+ parent = existing
16
+ need_destroy = False
17
+ else:
18
+ parent = tk.Tk()
19
+ parent.withdraw()
20
+ need_destroy = True
21
+ parent.attributes('-topmost', True)
22
+ parent.update()
23
+ path = filedialog.asksaveasfilename(
24
+ parent=parent,
25
+ title=title,
26
+ defaultextension=defaultextension,
27
+ initialfile=default_name,
28
+ filetypes=filetypes)
29
+ if need_destroy:
30
+ parent.destroy()
31
+ return path or None
32
+
33
+ def ask_save_path(title, default_name, defaultextension='', filetypes=None):
34
+ filetypes = filetypes or [('All files', '*')]
35
+ if _provider is not None:
36
+ try:
37
+ return _provider(title=title, default_name=default_name,
38
+ defaultextension=defaultextension,
39
+ filetypes=filetypes)
40
+ except Exception:
41
+ pass
42
+ try:
43
+ return _tk_save_path(title, default_name, defaultextension, filetypes)
44
+ except Exception:
45
+ path = input(f'Save path [{default_name}]: ').strip().strip('"')
46
+ return path or default_name
File without changes