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,94 @@
1
+ import numpy as np
2
+ from .reader import PTUFile
3
+
4
+ def signal_from_PTUFile(
5
+ filename,
6
+ /,
7
+ *,
8
+ dtype=None,
9
+ frame=None,
10
+ channel=0,
11
+ dtime=0,
12
+ binning=1,
13
+ keepdims=False,
14
+ ):
15
+ from xarray import DataArray
16
+
17
+ ptu = PTUFile(str(filename), verbose=False)
18
+ is_image = ptu.n_x > 0 and ptu.n_y > 0
19
+ _active_channels = np.asarray(ptu._active)
20
+
21
+ if is_image:
22
+ cubes = [ptu.raw_pixel_stack(channel=int(c), binning=binning)
23
+ for c in _active_channels]
24
+ stack = np.stack(cubes, axis=0)
25
+ data = np.transpose(stack, (1, 2, 0, 3))[np.newaxis, ...]
26
+ else:
27
+ hists = [ptu.summed_decay(channel=int(c)) for c in _active_channels]
28
+ data = np.stack(hists, axis=0)[np.newaxis, np.newaxis, np.newaxis, :, :]
29
+
30
+ if dtype is None:
31
+ dtype = np.uint16
32
+ data = data.astype(dtype)
33
+
34
+ n_bins_full = data.shape[-1]
35
+
36
+ if dtime is not None:
37
+ if dtime < 0:
38
+ data = data.sum(axis=-1, keepdims=keepdims)
39
+ elif dtime > 0:
40
+ data = data[..., :dtime]
41
+
42
+ if channel is not None:
43
+ if channel < 0:
44
+ data = data.sum(axis=3, keepdims=keepdims)
45
+ else:
46
+ ch_positions = np.where(_active_channels == channel)[0]
47
+ if len(ch_positions) == 0:
48
+ raise ValueError(
49
+ f"Channel {channel} not found in PTU file. "
50
+ f"Available hardware channels: {_active_channels.tolist()}"
51
+ )
52
+ channel_idx = int(ch_positions[0])
53
+ if keepdims:
54
+ data = data[:, :, :, channel_idx : channel_idx + 1]
55
+ else:
56
+ data = data[:, :, :, channel_idx]
57
+
58
+ if frame is not None:
59
+ if frame < 0:
60
+ data = data.sum(axis=0, keepdims=keepdims)
61
+ else:
62
+ if keepdims:
63
+ data = data[frame : frame + 1]
64
+ else:
65
+ data = data[frame]
66
+
67
+ all_dims = ['T', 'Y', 'X', 'C', 'H']
68
+ removed = []
69
+ if dtime is not None and dtime < 0 and not keepdims:
70
+ removed.append('H')
71
+ if channel is not None and channel >= 0 and not keepdims:
72
+ removed.append('C')
73
+ if frame is not None and not keepdims:
74
+ removed.append('T')
75
+ dims = [d for d in all_dims if d not in removed]
76
+
77
+ coords = {}
78
+ h_bins = dtime if (dtime is not None and dtime > 0) else n_bins_full
79
+ time_ns = ptu.time_ns[:h_bins]
80
+ if 'H' in dims:
81
+ coords['H'] = ('H', time_ns)
82
+
83
+ frequency_mhz = ptu.sync_rate * 1e-6
84
+
85
+ da = DataArray(
86
+ data,
87
+ dims=dims,
88
+ coords=coords,
89
+ attrs={
90
+ 'frequency': frequency_mhz,
91
+ 'ptu_tags': ptu.tags,
92
+ },
93
+ )
94
+ return da
@@ -0,0 +1,2 @@
1
+ from .flim_file import (FLIMFile, detect_format, file_modality,
2
+ supported_formats, supported_extensions, file_dialog_filetypes)
@@ -0,0 +1,232 @@
1
+ from pathlib import Path
2
+ import importlib
3
+
4
+ _FORMATS = [
5
+ {'id': 'ptu', 'label': 'PicoQuant PTU', 'exts': ('.ptu',),
6
+ 'modality': 'time', 'reader': 'flimkit.formats.PTU.reader:PTUFile'},
7
+ {'id': 'bh_sdt', 'label': 'Becker & Hickl SDT', 'exts': ('.sdt',),
8
+ 'modality': 'time', 'reader': 'flimkit.formats.BH.reader:BHFile'},
9
+ {'id': 'iss_tdflim', 'label': 'ISS time-tag',
10
+ 'exts': ('.tagtime', '.tagchannel', '.tagdecay'),
11
+ 'modality': 'time', 'reader': 'flimkit.formats.ISS.reader:ISSFile'},
12
+ {'id': 'iss_image', 'label': 'ISS image', 'exts': ('.ifi',),
13
+ 'modality': 'intensity', 'reader': 'flimkit.formats.ISS.image:ISSImage'},
14
+ {'id': 'iss_fdflim', 'label': 'ISS FD-FLIM (phasor)', 'exts': ('.ifli',),
15
+ 'modality': 'frequency', 'reader': 'flimkit.formats.ISS.fdflim:ISSFdFlim'},
16
+ {'id': 'ps', 'label': 'Photonscore .photons', 'exts': ('.photons',),
17
+ 'modality': 'time', 'reader': 'flimkit.formats.PS.reader:PSFile'},
18
+ {'id': 'pq_phu', 'label': 'PicoQuant PHU (histogram)', 'exts': ('.phu',),
19
+ 'modality': 'time', 'reader': 'flimkit.formats.PTU.phu:PHUFile'},
20
+ {'id': 'pq_bin', 'label': 'PicoQuant BIN', 'exts': ('.bin',),
21
+ 'modality': 'time', 'reader': 'flimkit.formats.signal:PQBinFile'},
22
+ {'id': 'simfcs_bh', 'label': 'SimFCS B&H', 'exts': ('.b&h',),
23
+ 'modality': 'time', 'reader': 'flimkit.formats.signal:SimfcsBHFile'},
24
+ {'id': 'simfcs_bhz', 'label': 'SimFCS BHZ', 'exts': ('.bhz',),
25
+ 'modality': 'time', 'reader': 'flimkit.formats.signal:SimfcsBHZFile'},
26
+ {'id': 'imspector_tiff', 'label': 'ImSpector FLIM TIFF', 'exts': ('.tif', '.tiff'),
27
+ 'modality': 'time', 'reader': 'flimkit.formats.signal:ImspectorTIFFFile'},
28
+ {'id': 'iss_vista_tdflim', 'label': 'ISS Vista TDFLIM', 'exts': ('.iss-tdflim', '.tdflim'),
29
+ 'modality': 'time', 'reader': 'flimkit.formats.signal:VistaTdflimFile'},
30
+ {'id': 'simfcs_referenced', 'label': 'SimFCS referenced (phasor)', 'exts': ('.ref', '.r64'),
31
+ 'modality': 'frequency', 'reader': 'flimkit.formats.phasor:SimfcsReferencedFile'},
32
+ {'id': 'ometiff_phasor', 'label': 'PhasorPy OME-TIFF (phasor)', 'exts': ('.ome.tif',),
33
+ 'modality': 'frequency', 'reader': 'flimkit.formats.phasor:OmeTiffPhasorFile'},
34
+ {'id': 'flimlabs_phasor', 'label': 'FLIM LABS phasor (JSON)', 'exts': ('.json',),
35
+ 'modality': 'frequency', 'reader': 'flimkit.formats.phasor:FlimLabsPhasorFile'},
36
+ {'id': 'flimlabs_signal', 'label': 'FLIM LABS imaging (JSON)', 'exts': (),
37
+ 'modality': 'time', 'reader': 'flimkit.formats.signal:FlimLabsSignalFile'},
38
+ ]
39
+
40
+ _cache = {'version': None, 'formats': None, 'ext_to_id': None, 'modality_by_id': None}
41
+
42
+ def _registry():
43
+ from flimkit import plugins
44
+ plugins.ensure_loaded()
45
+ return plugins.registry
46
+
47
+ def _all_formats():
48
+ reg = _registry()
49
+ if _cache['version'] != reg.version():
50
+ merged = [dict(f) for f in _FORMATS]
51
+ known = {f['id'] for f in merged}
52
+ for f in reg.formats():
53
+ if f.id in known:
54
+ continue
55
+ merged.append({'id': f.id, 'label': f.label, 'exts': f.exts,
56
+ 'modality': f.modality, 'reader': f})
57
+ ext_to_id = {}
58
+ for f in merged:
59
+ for ext in f['exts']:
60
+ ext_to_id.setdefault(ext, f['id'])
61
+ _cache['formats'] = merged
62
+ _cache['ext_to_id'] = ext_to_id
63
+ _cache['modality_by_id'] = {f['id']: f['modality'] for f in merged}
64
+ _cache['version'] = reg.version()
65
+ return _cache['formats']
66
+
67
+ def _ext_to_id():
68
+ _all_formats()
69
+ return _cache['ext_to_id']
70
+
71
+ def _modality_by_id():
72
+ _all_formats()
73
+ return _cache['modality_by_id']
74
+
75
+ def _find_sibling(stem, ext_lower):
76
+ direct = Path(str(stem) + ext_lower)
77
+ if direct.exists():
78
+ return direct
79
+ parent = stem.parent if str(stem.parent) else Path('.')
80
+ target = stem.name.lower() + ext_lower
81
+ if parent.exists():
82
+ for f in parent.iterdir():
83
+ if f.name.lower() == target:
84
+ return f
85
+ return None
86
+
87
+ def _has_iss_triplet(p):
88
+ stem = p.with_suffix('') if p.suffix else p
89
+ return _find_sibling(stem, '.tagtime') is not None
90
+
91
+ def _sniff_tiff(p):
92
+ try:
93
+ with open(p, 'rb') as fh:
94
+ head = fh.read(4)
95
+ except OSError:
96
+ return 'unknown'
97
+ if head not in (b'II*\x00', b'MM\x00*'):
98
+ return 'unknown'
99
+ try:
100
+ import tifffile
101
+ with tifffile.TiffFile(str(p)) as tif:
102
+ make = tif.pages.first.tags.valueof(271, '')
103
+ if make == 'ImSpector':
104
+ return 'imspector_tiff'
105
+ names = [s.name for s in tif.series[:3]]
106
+ if tif.is_ome and names == ['Phasor mean', 'Phasor real', 'Phasor imag']:
107
+ return 'ometiff_phasor'
108
+ except Exception:
109
+ pass
110
+ return 'unknown'
111
+
112
+ def _scan_bytes(p, needle, chunk=1 << 20):
113
+ try:
114
+ with open(p, 'rb') as fh:
115
+ tail = b''
116
+ while True:
117
+ buf = fh.read(chunk)
118
+ if not buf:
119
+ return False
120
+ if needle in tail + buf:
121
+ return True
122
+ tail = buf[-len(needle):]
123
+ except OSError:
124
+ return False
125
+
126
+ def _sniff_json(p):
127
+ try:
128
+ with open(p, 'rb') as fh:
129
+ head = fh.read(8192)
130
+ except OSError:
131
+ return 'unknown'
132
+ if b'"file_id"' not in head or b'"laser_period_ns"' not in head:
133
+ return 'unknown'
134
+ if _scan_bytes(p, b'"phasors_data"'):
135
+ return 'flimlabs_phasor'
136
+ if _scan_bytes(p, b'"intensities_data"') or _scan_bytes(p, b'"data"'):
137
+ return 'flimlabs_signal'
138
+ return 'unknown'
139
+
140
+ def _sniff_magic(p):
141
+ try:
142
+ with open(p, 'rb') as fh:
143
+ head = fh.read(64)
144
+ except OSError:
145
+ return 'unknown'
146
+ if b'PQHISTO' in head:
147
+ return 'pq_phu'
148
+ if b'PQTTTR' in head or b'PTU' in head:
149
+ return 'ptu'
150
+ if head[:12] == b'VistaFLImage':
151
+ return 'iss_fdflim'
152
+ if head[:10] == b'VISTAIMAGE':
153
+ return 'iss_image'
154
+ if b'D7 Photons Data' in head:
155
+ return 'ps'
156
+ return 'unknown'
157
+
158
+ def _clean_path(path):
159
+ s = str(path).strip()
160
+ if len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"):
161
+ s = s[1:-1].strip()
162
+ return s
163
+
164
+ def _run_sniffers(tier, p):
165
+ for sniffer in _registry().sniffers(tier):
166
+ try:
167
+ found = sniffer(p)
168
+ except Exception as exc:
169
+ print(f'[Plugins] sniffer from {sniffer.source} raised {exc}')
170
+ continue
171
+ if found and found != 'unknown':
172
+ return found
173
+ return None
174
+
175
+ def detect_format(path):
176
+ p = Path(_clean_path(path))
177
+ ext = p.suffix.lower()
178
+ if ext in ('.tif', '.tiff'):
179
+ return _sniff_tiff(p)
180
+ if ext == '.json':
181
+ return _sniff_json(p)
182
+ found = _run_sniffers('extension', p)
183
+ if found is not None:
184
+ return found
185
+ ext_to_id = _ext_to_id()
186
+ if ext in ext_to_id:
187
+ return ext_to_id[ext]
188
+ if _has_iss_triplet(p):
189
+ return 'iss_tdflim'
190
+ found = _sniff_magic(p)
191
+ if found != 'unknown':
192
+ return found
193
+ found = _run_sniffers('magic', p)
194
+ if found is not None:
195
+ return found
196
+ return 'unknown'
197
+
198
+ def file_modality(path):
199
+ return _modality_by_id().get(detect_format(path), 'unknown')
200
+
201
+ def supported_formats():
202
+ return [dict(f) for f in _all_formats()]
203
+
204
+ def supported_extensions():
205
+ return [ext for f in _all_formats() for ext in f['exts']]
206
+
207
+ def file_dialog_filetypes():
208
+ all_globs = ' '.join('*' + ext for ext in supported_extensions())
209
+ types = [('FLIM files', all_globs)]
210
+ for f in _all_formats():
211
+ globs = ' '.join('*' + ext for ext in f['exts'])
212
+ types.append((f['label'], globs))
213
+ return types
214
+
215
+ def _load_reader(fmt):
216
+ for f in _all_formats():
217
+ if f['id'] == fmt:
218
+ spec = f['reader']
219
+ if isinstance(spec, str):
220
+ mod_name, cls_name = spec.split(':')
221
+ return getattr(importlib.import_module(mod_name), cls_name)
222
+ return spec.reader()
223
+ return None
224
+
225
+ class FLIMFile:
226
+ def __new__(cls, path, **kwargs):
227
+ path = _clean_path(path)
228
+ fmt = detect_format(path)
229
+ reader = _load_reader(fmt)
230
+ if reader is None:
231
+ raise ValueError(f'Unrecognised FLIM file format: {path}')
232
+ return reader(str(path), **kwargs)
@@ -0,0 +1,132 @@
1
+ import numpy as np
2
+ from pathlib import Path
3
+
4
+ def _import_io():
5
+ try:
6
+ import phasorpy.io as pio
7
+ except ImportError:
8
+ raise ImportError('phasorpy is required to read this format '
9
+ '(pip install phasorpy)')
10
+ return pio
11
+
12
+ class PhasorReader:
13
+ modality = 'frequency'
14
+
15
+ def __init__(self, path, mean, real, imag, attrs, label='phasor',
16
+ verbose=True, channel=None, frequency_mhz=None):
17
+ self.path = str(path)
18
+ self.verbose = verbose
19
+ self.label = label
20
+ self.channel = channel
21
+ self._mean = np.asarray(mean)
22
+ self._real = np.asarray(real)
23
+ self._imag = np.asarray(imag)
24
+ attrs = dict(attrs or {})
25
+ self._dims = tuple(attrs.get('dims') or ())
26
+ self._harmonics = attrs.get('harmonic')
27
+ freq = frequency_mhz if frequency_mhz is not None else attrs.get('frequency')
28
+ self.frequency_mhz = float(freq) if freq else 0.0
29
+ self.tags = {f'phasorpy_{k}': v for k, v in attrs.items()}
30
+ dims = self._dims
31
+ self.n_channels = int(self._mean.shape[dims.index('C')]) if 'C' in dims else 1
32
+ self.n_y = int(self._mean.shape[dims.index('Y')]) if 'Y' in dims else 0
33
+ self.n_x = int(self._mean.shape[dims.index('X')]) if 'X' in dims else 0
34
+ self.is_image = self.n_x > 0 and self.n_y > 0
35
+ self.n_harmonics = len(self._harmonics) if isinstance(self._harmonics, (list, tuple)) else 1
36
+ if self.verbose:
37
+ print(f' {label} : {Path(self.path).name}')
38
+ if self.is_image:
39
+ print(f' Size : {self.n_x} x {self.n_y} px, {self.n_channels} ch, '
40
+ f'{self.n_harmonics} harmonic(s) dims={"".join(dims)}')
41
+ else:
42
+ print(f' Point : no image (no Y/X axes), {self.n_channels} ch, '
43
+ f'{self.n_harmonics} harmonic(s) dims={"".join(dims)}')
44
+ if self.frequency_mhz:
45
+ print(f' Frequency: {self.frequency_mhz:.3f} MHz')
46
+ else:
47
+ print(' WARNING : file carries no frequency; pass frequency_mhz=<MHz> '
48
+ 'for the universal circle / lifetime overlay')
49
+ print(' Frequency-domain data: loads into phasor analysis, no lifetime fitting')
50
+ print(' ')
51
+
52
+ def _ch_index(self, channel):
53
+ if channel is None:
54
+ return None
55
+ c = int(channel)
56
+ idx = c - 1 if c >= 1 else c
57
+ return idx if 0 <= idx < self.n_channels else 0
58
+
59
+ def _auto_channel(self):
60
+ dims = list(self._dims)
61
+ if 'C' not in dims:
62
+ return 0
63
+ ci = dims.index('C')
64
+ sums = self._mean.sum(axis=tuple(i for i in range(self._mean.ndim) if i != ci))
65
+ return int(np.argmax(sums))
66
+
67
+ def _reduce(self, arr, dims, ch_idx):
68
+ dims = list(dims)
69
+ if arr.ndim != len(dims):
70
+ dims = dims[-arr.ndim:]
71
+ if 'C' in dims:
72
+ ci = dims.index('C')
73
+ arr = np.take(arr, ch_idx, axis=ci)
74
+ dims = [d for i, d in enumerate(dims) if i != ci]
75
+ while len(dims) > 2:
76
+ extra = next(i for i, d in enumerate(dims) if d not in ('Y', 'X'))
77
+ arr = np.take(arr, 0, axis=extra)
78
+ dims = [d for i, d in enumerate(dims) if i != extra]
79
+ order = [dims.index(d) for d in ('Y', 'X') if d in dims]
80
+ return np.transpose(arr, order) if len(order) == 2 else arr
81
+
82
+ def phasor(self, channel=None, harmonic=0, calibrate=True, **kwargs):
83
+ ch = self.channel if channel is None else channel
84
+ ch_idx = self._ch_index(ch)
85
+ if ch_idx is None:
86
+ ch_idx = self._auto_channel()
87
+ real, imag = self._real, self._imag
88
+ if isinstance(self._harmonics, (list, tuple)):
89
+ hi = int(harmonic) if 0 <= int(harmonic) < len(self._harmonics) else 0
90
+ real, imag = real[hi], imag[hi]
91
+ mean2 = self._reduce(self._mean, self._dims, ch_idx)
92
+ real2 = self._reduce(real, self._dims, ch_idx)
93
+ imag2 = self._reduce(imag, self._dims, ch_idx)
94
+ return (mean2.astype(float), real2.astype(float), imag2.astype(float),
95
+ self.frequency_mhz)
96
+
97
+ def pixel_stack(self, *args, **kwargs):
98
+ raise ValueError(f'{self.label} is frequency-domain phasor data: there is no '
99
+ 'time-domain decay to fit. Load it into phasor analysis instead.')
100
+
101
+ def summed_decay(self, *args, **kwargs):
102
+ return self.pixel_stack()
103
+
104
+ def raw_pixel_stack(self, *args, **kwargs):
105
+ return self.pixel_stack()
106
+
107
+ class _GohlkePhasorFile(PhasorReader):
108
+ _reader_name = None
109
+ _label = None
110
+ _reader_kwargs = {}
111
+
112
+ def __init__(self, path, verbose=True, channel=None, **kwargs):
113
+ pio = _import_io()
114
+ fn = getattr(pio, self._reader_name, None)
115
+ if fn is None:
116
+ raise ImportError(f'{self._reader_name} is not available in this '
117
+ f'phasorpy version; upgrade phasorpy')
118
+ mean, real, imag, attrs = fn(str(path), **self._reader_kwargs)
119
+ super().__init__(path, mean, real, imag, attrs, label=self._label,
120
+ verbose=verbose, channel=channel, **kwargs)
121
+
122
+ class SimfcsReferencedFile(_GohlkePhasorFile):
123
+ _reader_name = 'phasor_from_simfcs_referenced'
124
+ _label = 'SimFCS referenced'
125
+
126
+ class OmeTiffPhasorFile(_GohlkePhasorFile):
127
+ _reader_name = 'phasor_from_ometiff'
128
+ _label = 'PhasorPy OME-TIFF'
129
+
130
+ class FlimLabsPhasorFile(_GohlkePhasorFile):
131
+ _reader_name = 'phasor_from_flimlabs_json'
132
+ _label = 'FLIM LABS phasor'
@@ -0,0 +1,170 @@
1
+ import numpy as np
2
+ from pathlib import Path
3
+
4
+ def _import_io():
5
+ try:
6
+ import phasorpy.io as pio
7
+ except ImportError:
8
+ raise ImportError('phasorpy is required to read this format '
9
+ '(pip install phasorpy)')
10
+ return pio
11
+
12
+ def _bin_cube(cube, binning):
13
+ if binning <= 1:
14
+ return cube
15
+ ny, nx, nh = cube.shape
16
+ ny2, nx2 = (ny // binning) * binning, (nx // binning) * binning
17
+ cube = cube[:ny2, :nx2, :]
18
+ return cube.reshape(ny2 // binning, binning, nx2 // binning, binning, nh).sum(axis=(1, 3))
19
+
20
+ class SignalReader:
21
+ modality = 'time'
22
+
23
+ def __init__(self, path, signal, label='signal', verbose=True, channel=None,
24
+ bin_width_ns=None, frequency_mhz=None):
25
+ self.path = str(path)
26
+ self.verbose = verbose
27
+ self.label = label
28
+ self.channel = channel
29
+ dims = list(signal.dims)
30
+ vals = np.asarray(signal.values)
31
+ attrs = dict(getattr(signal, 'attrs', {}) or {})
32
+ coords = getattr(signal, 'coords', {})
33
+ self._dims = dims
34
+ self._vals = vals
35
+ self.tags = {f'phasorpy_{k}': v for k, v in attrs.items()}
36
+ c_ax = dims.index('C') if 'C' in dims else None
37
+ self.n_channels = int(vals.shape[c_ax]) if c_ax is not None else 1
38
+ self._c_ax = c_ax
39
+ h_ax = dims.index('H') if 'H' in dims else None
40
+ if h_ax is None:
41
+ raise ValueError(f'{label}: no TCSPC histogram (H) axis in {dims}')
42
+ self.n_bins = int(vals.shape[h_ax])
43
+ self.n_y = int(vals.shape[dims.index('Y')]) if 'Y' in dims else 0
44
+ self.n_x = int(vals.shape[dims.index('X')]) if 'X' in dims else 0
45
+ self.is_image = self.n_x > 0 and self.n_y > 0
46
+ h_ns = None
47
+ if 'H' in coords:
48
+ h_ns = np.asarray(coords['H'].values, dtype=float)
49
+ elif bin_width_ns is not None:
50
+ h_ns = np.arange(self.n_bins) * float(bin_width_ns)
51
+ if h_ns is not None and h_ns.size > 1:
52
+ self.tcspc_res = float(np.median(np.diff(h_ns))) * 1e-9
53
+ self.time_ns = h_ns
54
+ else:
55
+ self.tcspc_res = float(bin_width_ns) * 1e-9 if bin_width_ns else 0.0
56
+ self.time_ns = np.arange(self.n_bins) * (self.tcspc_res * 1e9)
57
+ freq_mhz = frequency_mhz if frequency_mhz is not None else attrs.get('frequency')
58
+ self.sync_rate = float(freq_mhz) * 1e6 if freq_mhz else 0.0
59
+ self.period_ns = (1e9 / self.sync_rate) if self.sync_rate else 0.0
60
+ self.photon_channel = None
61
+ self._total_photons = None
62
+ self._cube_cache = {}
63
+ if self.verbose:
64
+ print(f' {label} : {Path(self.path).name}')
65
+ print(f' TCSPC : {self.n_bins} bins x {self.tcspc_res*1e12:.2f} ps')
66
+ if self.sync_rate:
67
+ print(f' Laser : {self.sync_rate/1e6:.3f} MHz ({self.period_ns:.3f} ns)')
68
+ if self.is_image:
69
+ print(f' Image : {self.n_x} x {self.n_y} px, {self.n_channels} channel(s)')
70
+ else:
71
+ print(f' Point : no image (no Y/X axes), {self.n_channels} channel(s)')
72
+ if self.tcspc_res <= 0:
73
+ print(f' WARNING : {label} carries no time axis; pass bin_width_ns='
74
+ f'<ns> (and frequency_mhz=<MHz>) or fits will be meaningless')
75
+ print(' ')
76
+
77
+ def _ch_index(self, channel):
78
+ if channel is None:
79
+ return None
80
+ c = int(channel)
81
+ idx = c - 1 if c >= 1 else c
82
+ return idx if 0 <= idx < self.n_channels else 0
83
+
84
+ def _cube_for(self, channel):
85
+ key = channel
86
+ if key in self._cube_cache:
87
+ return self._cube_cache[key]
88
+ arr = self._vals
89
+ dims = list(self._dims)
90
+ if self._c_ax is not None:
91
+ idx = self._ch_index(channel)
92
+ if idx is None:
93
+ sums = arr.sum(axis=tuple(i for i in range(arr.ndim) if i != self._c_ax))
94
+ idx = int(np.argmax(sums))
95
+ self.photon_channel = idx + 1
96
+ arr = np.take(arr, idx, axis=self._c_ax)
97
+ dims = [d for i, d in enumerate(dims) if i != self._c_ax]
98
+ drop = tuple(i for i, d in enumerate(dims) if d not in ('Y', 'X', 'H'))
99
+ if drop:
100
+ arr = arr.sum(axis=drop)
101
+ dims = [d for d in dims if d in ('Y', 'X', 'H')]
102
+ order = [dims.index(d) for d in ('Y', 'X', 'H') if d in dims]
103
+ cube = np.transpose(arr, order)
104
+ self._cube_cache[key] = cube
105
+ return cube
106
+
107
+ def summed_decay(self, channel=None):
108
+ cube = self._cube_for(self.channel if channel is None else channel)
109
+ decay = cube.reshape(-1, self.n_bins).sum(axis=0).astype(float)
110
+ self._total_photons = int(decay.sum())
111
+ return decay
112
+
113
+ def raw_pixel_stack(self, channel=None, binning=1):
114
+ cube = self._cube_for(self.channel if channel is None else channel)
115
+ if cube.ndim != 3:
116
+ raise RuntimeError(f'{self.label} has no scan image; use summed_decay')
117
+ if binning > 1:
118
+ cube = _bin_cube(cube, binning)
119
+ self._total_photons = int(cube.sum())
120
+ return np.ascontiguousarray(cube).astype(np.uint32)
121
+
122
+ def pixel_stack(self, channel=None, binning=1):
123
+ return self.raw_pixel_stack(channel=channel, binning=binning).astype(float)
124
+
125
+ def intensity_image(self, channel=None, binning=1):
126
+ return self.raw_pixel_stack(channel=channel, binning=binning).sum(axis=2)
127
+
128
+ @property
129
+ def photons_per_pulse(self):
130
+ return None
131
+
132
+ class _GohlkeSignalFile(SignalReader):
133
+ _reader_name = None
134
+ _label = None
135
+ _reader_kwargs = {}
136
+
137
+ def __init__(self, path, verbose=True, channel=None, **kwargs):
138
+ pio = _import_io()
139
+ fn = getattr(pio, self._reader_name, None)
140
+ if fn is None:
141
+ raise ImportError(f'{self._reader_name} is not available in this '
142
+ f'phasorpy version; upgrade phasorpy')
143
+ signal = fn(str(path), **self._reader_kwargs)
144
+ super().__init__(path, signal, label=self._label, verbose=verbose,
145
+ channel=channel, **kwargs)
146
+
147
+ class PQBinFile(_GohlkeSignalFile):
148
+ _reader_name = 'signal_from_pqbin'
149
+ _label = 'PicoQuant BIN'
150
+
151
+ class SimfcsBHFile(_GohlkeSignalFile):
152
+ _reader_name = 'signal_from_bh'
153
+ _label = 'SimFCS B&H'
154
+
155
+ class SimfcsBHZFile(_GohlkeSignalFile):
156
+ _reader_name = 'signal_from_bhz'
157
+ _label = 'SimFCS BHZ'
158
+
159
+ class ImspectorTIFFFile(_GohlkeSignalFile):
160
+ _reader_name = 'signal_from_imspector_tiff'
161
+ _label = 'ImSpector FLIM TIFF'
162
+
163
+ class VistaTdflimFile(_GohlkeSignalFile):
164
+ _reader_name = 'signal_from_tdflim'
165
+ _label = 'ISS Vista TDFLIM'
166
+ _reader_kwargs = {'channel': None}
167
+
168
+ class FlimLabsSignalFile(_GohlkeSignalFile):
169
+ _reader_name = 'signal_from_flimlabs_json'
170
+ _label = 'FLIM LABS imaging'