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
@@ -0,0 +1,183 @@
1
+ import atexit
2
+ import datetime
3
+ import logging
4
+ import os
5
+ import platform
6
+ import sys
7
+ import traceback
8
+
9
+ _LOG_DIR = os.path.join(os.path.expanduser('~'), '.flimkit', 'logs')
10
+ _session_log_path: str | None = None
11
+ _logger: logging.Logger | None = None
12
+ _original_excepthook = None
13
+
14
+
15
+ def _get_system_info() -> str:
16
+ try:
17
+ from flimkit._version import __version__
18
+ except Exception:
19
+ __version__ = 'unknown'
20
+
21
+ lines = [
22
+ f"FLIMKit version : {__version__}",
23
+ f"Python : {sys.version}",
24
+ f"Platform : {platform.platform()}",
25
+ f"OS : {platform.system()} {platform.release()}",
26
+ f"Machine : {platform.machine()}",
27
+ f"Processor : {platform.processor() or 'N/A'}",
28
+ ]
29
+
30
+ # NumPy / Matplotlib versions if available
31
+ for pkg_name in ('numpy', 'matplotlib', 'scipy'):
32
+ try:
33
+ mod = __import__(pkg_name)
34
+ lines.append(f"{pkg_name:16s}: {mod.__version__}")
35
+ except Exception:
36
+ pass
37
+
38
+ return '\n'.join(lines)
39
+
40
+
41
+ def _crash_excepthook(exc_type, exc_value, exc_tb):
42
+ if _logger is not None:
43
+ tb_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
44
+ _logger.critical(
45
+ 'UNCAUGHT EXCEPTION (main thread)\n%s\n%s',
46
+ tb_text,
47
+ _get_system_info(),
48
+ )
49
+ # Force flush so log is written even if the process is killed
50
+ for handler in _logger.handlers:
51
+ handler.flush()
52
+
53
+ # Call the original hook so the traceback still prints to stderr
54
+ if _original_excepthook is not None:
55
+ _original_excepthook(exc_type, exc_value, exc_tb)
56
+
57
+
58
+ def _thread_excepthook(args):
59
+ if _logger is not None:
60
+ tb_text = ''.join(
61
+ traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback)
62
+ )
63
+ thread_name = args.thread.name if args.thread else 'unknown'
64
+ _logger.critical(
65
+ 'UNCAUGHT EXCEPTION (thread: %s)\n%s',
66
+ thread_name,
67
+ tb_text,
68
+ )
69
+ for handler in _logger.handlers:
70
+ handler.flush()
71
+
72
+
73
+ def install_tk_error_handler(root):
74
+ def _tk_error_handler(exc_type, exc_value, exc_tb):
75
+ if _logger is not None:
76
+ tb_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
77
+ _logger.error('UNCAUGHT EXCEPTION (Tk callback)\n%s', tb_text)
78
+ for handler in _logger.handlers:
79
+ handler.flush()
80
+ # Still print to stderr so the user sees it
81
+ sys.__stderr__.write(''.join(traceback.format_exception(exc_type, exc_value, exc_tb)))
82
+
83
+ root.report_callback_exception = _tk_error_handler
84
+
85
+
86
+ def init_crash_handler() -> str:
87
+ global _session_log_path, _logger, _original_excepthook
88
+
89
+ os.makedirs(_LOG_DIR, exist_ok=True)
90
+
91
+ # Create a timestamped log file for this session
92
+ timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
93
+ _session_log_path = os.path.join(_LOG_DIR, f"session_{timestamp}.log")
94
+
95
+ # Configure the logger
96
+ _logger = logging.getLogger('flimkit.crash')
97
+ _logger.setLevel(logging.DEBUG)
98
+ _logger.handlers.clear()
99
+
100
+ fh = logging.FileHandler(_session_log_path, encoding='utf-8')
101
+ fh.setLevel(logging.DEBUG)
102
+ fh.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
103
+ _logger.addHandler(fh)
104
+
105
+ # Write session header
106
+ _logger.info('SESSION START')
107
+ _logger.info('System info:\n%s', _get_system_info())
108
+
109
+ # Install the global exception hook
110
+ _original_excepthook = sys.excepthook
111
+ sys.excepthook = _crash_excepthook
112
+
113
+ # Install the threading exception hook (catches errors in background threads)
114
+ import threading
115
+ threading.excepthook = _thread_excepthook
116
+
117
+ # Log a clean shutdown if we exit normally
118
+ atexit.register(lambda: _logger.info('SESSION END (clean exit)') if _logger else None)
119
+
120
+ return _session_log_path
121
+
122
+
123
+ def log_event(message: str, level: str = 'info'):
124
+ if _logger is None:
125
+ return
126
+ getattr(_logger, level, _logger.info)(message)
127
+
128
+
129
+ def log_exception(context: str = ''):
130
+ if _logger is None:
131
+ return
132
+ _logger.error(
133
+ 'CAUGHT EXCEPTION%s\n%s',
134
+ f" ({context})" if context else '',
135
+ traceback.format_exc(),
136
+ )
137
+ for handler in _logger.handlers:
138
+ handler.flush()
139
+
140
+
141
+ def get_log_dir() -> str:
142
+ return _LOG_DIR
143
+
144
+
145
+ def get_session_log_path() -> str | None:
146
+ return _session_log_path
147
+
148
+
149
+ def build_export_report(include_all_sessions: bool = False) -> str:
150
+ import glob
151
+
152
+ sections = []
153
+ sections.append('=' * 60)
154
+ sections.append('FLIMKit Error Report')
155
+ sections.append(f"Generated: {datetime.datetime.now().isoformat()}")
156
+ sections.append('=' * 60)
157
+ sections.append('')
158
+ sections.append('SYSTEM INFO')
159
+ sections.append('-' * 40)
160
+ sections.append(_get_system_info())
161
+ sections.append('')
162
+
163
+ log_files = sorted(
164
+ glob.glob(os.path.join(_LOG_DIR, '*.log')),
165
+ key=os.path.getmtime,
166
+ reverse=True,
167
+ )
168
+
169
+ if not include_all_sessions and _session_log_path:
170
+ log_files = [f for f in log_files if f == _session_log_path]
171
+
172
+ for log_file in log_files:
173
+ sections.append('=' * 60)
174
+ sections.append(f"LOG: {os.path.basename(log_file)}")
175
+ sections.append('=' * 60)
176
+ try:
177
+ with open(log_file, 'r', encoding='utf-8') as f:
178
+ sections.append(f.read())
179
+ except Exception as e:
180
+ sections.append(f"(could not read: {e})")
181
+ sections.append('')
182
+
183
+ return '\n'.join(sections)
@@ -0,0 +1,197 @@
1
+ import json
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from pathlib import Path
5
+ from matplotlib.colors import Normalize, PowerNorm
6
+
7
+ def load_zstack_display_slices(group_dir, ptu_dir=None, region=None):
8
+ from flimkit.utils.batch_fit import group_zstack_files
9
+ group_dir = Path(group_dir)
10
+ ref = {}
11
+ ref_files = list(group_dir.glob('*_reference_fit.json'))
12
+ if ref_files:
13
+ try:
14
+ ref = json.loads(ref_files[0].read_text())
15
+ except Exception:
16
+ ref = {}
17
+ taus_ns = ref.get('taus_ns', [])
18
+ nexp = ref.get('nexp', len(taus_ns))
19
+ ref_decay = ref_time = ref_model = ref_irf = None
20
+ ref_chi2 = None
21
+ ref_calibrated = ref.get('calibrated_chi2_pearson')
22
+ ref_calibrated_tail = ref.get('calibrated_chi2_tail_pearson')
23
+ npz_path = group_dir / 'reference_decay.npz'
24
+ if npz_path.exists():
25
+ try:
26
+ with np.load(str(npz_path)) as zf:
27
+ ref_decay = zf['decay']
28
+ ref_time = zf['time_ns']
29
+ ref_model = zf['model'] if zf['model'].size > 0 else None
30
+ ref_irf = zf['irf_prompt'] if zf['irf_prompt'].size > 0 else None
31
+ ref_chi2 = float(zf['reduced_chi2_tail'][0]) if zf['reduced_chi2_tail'].size > 0 else None
32
+ if ref_calibrated is None and 'calibrated_chi2_pearson' in zf:
33
+ ref_calibrated = float(zf['calibrated_chi2_pearson'][0])
34
+ if ref_calibrated_tail is None and 'calibrated_chi2_tail_pearson' in zf:
35
+ ref_calibrated_tail = float(zf['calibrated_chi2_tail_pearson'][0])
36
+ except Exception:
37
+ pass
38
+ z_to_ptu = {}
39
+ if ptu_dir is not None and Path(ptu_dir).is_dir():
40
+ groups = group_zstack_files(ptu_dir)
41
+ for (r, _t, _s), zsl in groups.items():
42
+ if region is None or r == region:
43
+ z_to_ptu = {z: str(p) for z, p in zsl.items()}
44
+ break
45
+ slices = []
46
+ for slice_dir in sorted(group_dir.glob('z[0-9]*')):
47
+ if not slice_dir.is_dir():
48
+ continue
49
+ try:
50
+ z = int(slice_dir.name[1:])
51
+ except ValueError:
52
+ continue
53
+ def _load(name):
54
+ p = slice_dir / f'{name}.npy'
55
+ return np.load(str(p)) if p.exists() else None
56
+ pixel_maps = {}
57
+ for k in ('tau_mean_int', 'tau_mean_amp', 'alpha_1', 'alpha_2', 'alpha_3',
58
+ 'chi2_r', 'calibrated_chi2_r'):
59
+ m = _load(k)
60
+ if m is not None:
61
+ pixel_maps[k] = m
62
+ global_summary = {'taus_ns': list(taus_ns), 'n_exp': nexp}
63
+ if ref_model is not None:
64
+ global_summary['model'] = ref_model
65
+ if ref_chi2 is not None and ref_chi2 == ref_chi2:
66
+ global_summary['reduced_chi2_tail'] = ref_chi2
67
+ if ref_calibrated is not None and ref_calibrated == ref_calibrated:
68
+ global_summary['calibrated_chi2_pearson'] = ref_calibrated
69
+ if ref_calibrated_tail is not None and ref_calibrated_tail == ref_calibrated_tail:
70
+ global_summary['calibrated_chi2_tail_pearson'] = ref_calibrated_tail
71
+ fit_result = {
72
+ 'pixel_maps': pixel_maps,
73
+ 'intensity': _load('intensity'),
74
+ 'global_summary': global_summary,
75
+ }
76
+ if ref_decay is not None and ref_time is not None:
77
+ fit_result['decay'] = ref_decay
78
+ fit_result['time_ns'] = ref_time
79
+ if ref_irf is not None:
80
+ fit_result['irf_prompt'] = ref_irf
81
+ slices.append({'z': z, 'ptu_path': z_to_ptu.get(z), 'fit_result': fit_result})
82
+ slices.sort(key=lambda d: d['z'])
83
+ return slices
84
+
85
+ COLORMAPS = {
86
+ 'hsv': 'hsv',
87
+ 'viridis': 'viridis',
88
+ 'cool': 'cool',
89
+ 'hot': 'hot',
90
+ 'twilight': 'twilight',
91
+ }
92
+
93
+ def compute_weighted_lifetime(
94
+ pixel_maps,
95
+ intensity,
96
+ n_exp=2,
97
+ weighting='amplitude',
98
+ ) -> np.ndarray:
99
+ primary = 'tau_mean_int' if weighting == 'intensity' else 'tau_mean_amp'
100
+ fallback = 'tau_mean_amp' if weighting == 'intensity' else 'tau_mean_int'
101
+ for key in (primary, fallback):
102
+ if key in pixel_maps:
103
+ arr = np.asarray(pixel_maps[key], dtype=np.float32).copy()
104
+ arr[arr == 0] = np.nan
105
+ return arr
106
+ shape = intensity.shape
107
+ num = np.zeros(shape, dtype=np.float64)
108
+ den = np.zeros(shape, dtype=np.float64)
109
+ for i in range(1, n_exp + 1):
110
+ tau_key = f'tau{i}'
111
+ amp_key = f'a{i}'
112
+ if tau_key in pixel_maps and amp_key in pixel_maps:
113
+ tau = np.asarray(pixel_maps[tau_key], dtype=np.float64)
114
+ amp = np.asarray(pixel_maps[amp_key], dtype=np.float64)
115
+ valid = np.isfinite(tau) & np.isfinite(amp)
116
+ if weighting == 'intensity':
117
+ num[valid] += amp[valid] * tau[valid] ** 2
118
+ den[valid] += amp[valid] * tau[valid]
119
+ else:
120
+ num[valid] += amp[valid] * tau[valid]
121
+ den[valid] += amp[valid]
122
+ result = np.full(shape, np.nan, dtype=np.float32)
123
+ mask = den > 0
124
+ result[mask] = (num[mask] / den[mask]).astype(np.float32)
125
+ return result
126
+
127
+ def apply_color_scale(
128
+ image,
129
+ vmin=None,
130
+ vmax=None,
131
+ gamma=1.0,
132
+ percentile_auto=(2, 98),
133
+ ):
134
+ valid_mask = ~np.isnan(image)
135
+ valid_pixels = image[valid_mask]
136
+ if vmin is None:
137
+ vmin = np.percentile(valid_pixels, percentile_auto[0]) if valid_pixels.size > 0 else 0
138
+ if vmax is None:
139
+ vmax = np.percentile(valid_pixels, percentile_auto[1]) if valid_pixels.size > 0 else 1
140
+ clipped = np.clip(image, vmin, vmax)
141
+ if vmax > vmin:
142
+ normalized = (clipped - vmin) / (vmax - vmin)
143
+ else:
144
+ normalized = np.zeros_like(clipped)
145
+ if gamma != 1.0:
146
+ normalized = np.power(normalized, 1.0 / gamma)
147
+ normalized[~valid_mask] = np.nan
148
+ return normalized
149
+
150
+ def get_colormap(name='viridis'):
151
+ cmap_name = COLORMAPS.get(name, name)
152
+ return plt.cm.get_cmap(cmap_name)
153
+
154
+ def compute_region_stats(
155
+ lifetime_map,
156
+ intensity_map,
157
+ region_mask,
158
+ full_stats=False,
159
+ ):
160
+ region_lifetime = lifetime_map[region_mask]
161
+ region_intensity = intensity_map[region_mask]
162
+ valid_mask = ~np.isnan(region_lifetime)
163
+ valid_lifetime = region_lifetime[valid_mask]
164
+ valid_intensity = region_intensity[valid_mask]
165
+ stats = {
166
+ 'median_tau': float(np.nanmedian(valid_lifetime)) if valid_lifetime.size > 0 else np.nan,
167
+ 'mean_amplitude': float(np.mean(valid_intensity)) if valid_intensity.size > 0 else np.nan,
168
+ 'photon_count': int(np.sum(valid_intensity)),
169
+ 'n_pixels': int(np.sum(valid_mask)),
170
+ }
171
+ if full_stats and valid_lifetime.size > 0:
172
+ stats.update({
173
+ 'min_tau': float(np.nanmin(valid_lifetime)),
174
+ 'max_tau': float(np.nanmax(valid_lifetime)),
175
+ 'std_tau': float(np.nanstd(valid_lifetime)),
176
+ 'mean_tau': float(np.nanmean(valid_lifetime)),
177
+ 'percentiles': {
178
+ 'p25': float(np.nanpercentile(valid_lifetime, 25)),
179
+ 'p50': float(np.nanpercentile(valid_lifetime, 50)),
180
+ 'p75': float(np.nanpercentile(valid_lifetime, 75)),
181
+ 'p90': float(np.nanpercentile(valid_lifetime, 90)),
182
+ 'p95': float(np.nanpercentile(valid_lifetime, 95)),
183
+ },
184
+ })
185
+ return stats
186
+
187
+ def mask_to_rgba(
188
+ mask,
189
+ color=(1.0, 1.0, 1.0),
190
+ alpha=0.3,
191
+ ):
192
+ rgba = np.zeros((*mask.shape, 4), dtype=np.float32)
193
+ rgba[mask, 0] = color[0]
194
+ rgba[mask, 1] = color[1]
195
+ rgba[mask, 2] = color[2]
196
+ rgba[mask, 3] = alpha
197
+ return rgba