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,556 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from matplotlib.patches import Ellipse
4
+
5
+ from phasorpy.plot import PhasorPlot
6
+ from phasorpy.cursor import mask_from_elliptic_cursor, pseudo_color
7
+ from phasorpy.lifetime import (
8
+ phasor_to_apparent_lifetime,
9
+ phasor_semicircle_intersect,
10
+ )
11
+ from phasorpy.component import phasor_component_fraction
12
+
13
+ _COLORS = ['#d62728', '#1f77b4', '#2ca02c', '#ff7f0e', '#9467bd', '#8c564b']
14
+
15
+ def _in_notebook():
16
+ try:
17
+ from IPython import get_ipython
18
+ shell = get_ipython()
19
+ if shell is None:
20
+ return False
21
+ return shell.__class__.__name__ == 'ZMQInteractiveShell'
22
+ except Exception:
23
+ return False
24
+
25
+ def _ellipse_angle_rad(cg, cs, mode):
26
+ if mode == 'semicircle':
27
+ return np.arctan2(cs, cg - 0.5) + np.pi / 2.0
28
+ return np.arctan2(cs, cg)
29
+
30
+ def phasor_cursor_tool(
31
+ real_cal,
32
+ imag_cal,
33
+ mean,
34
+ frequency,
35
+ *,
36
+ display_image=None,
37
+ min_photons=0.01,
38
+ max_cursors=6,
39
+ figsize=(8, 5),
40
+ initial_cursors=None,
41
+ initial_params=None,
42
+ on_save=None,
43
+ ):
44
+ notebook = _in_notebook()
45
+
46
+ rc = np.asarray(real_cal).squeeze().astype(float)
47
+ ic = np.asarray(imag_cal).squeeze().astype(float)
48
+ mn = np.asarray(mean).squeeze().astype(float)
49
+
50
+ disp = (np.asarray(display_image).squeeze().astype(float)
51
+ if display_image is not None else mn)
52
+ valid = (mn >= min_photons) & ~np.isnan(rc)
53
+ g_all = rc[valid]
54
+ s_all = ic[valid]
55
+
56
+ state = {
57
+ 'cursors': [],
58
+ 'masks': None,
59
+ 'fig': None,
60
+ 'ax': None,
61
+ }
62
+ cursor_artists = []
63
+
64
+ params = dict(radius=0.05, radius_minor=0.03, angle_mode='semicircle')
65
+ if initial_params:
66
+ params.update({k: v for k, v in initial_params.items()
67
+ if k in params})
68
+
69
+ if notebook:
70
+ import ipywidgets as widgets
71
+ from IPython.display import display, clear_output
72
+ results_out = widgets.Output()
73
+
74
+ def _begin_output():
75
+ results_out.__enter__()
76
+ clear_output(wait=True)
77
+
78
+ def _end_output():
79
+ results_out.__exit__(None, None, None)
80
+ else:
81
+ def _begin_output():
82
+ pass
83
+
84
+ def _end_output():
85
+ pass
86
+
87
+ def _redraw():
88
+ ax = state['ax']
89
+ for art in cursor_artists:
90
+ try:
91
+ art.remove()
92
+ except Exception:
93
+ pass
94
+ cursor_artists.clear()
95
+
96
+ radius = params['radius']
97
+ r_minor = params['radius_minor']
98
+ mode = params['angle_mode']
99
+ cursors = state['cursors']
100
+
101
+ for i, cur in enumerate(cursors):
102
+ cg, cs, col = cur['center_g'], cur['center_s'], cur['color']
103
+ ang_deg = np.degrees(_ellipse_angle_rad(cg, cs, mode))
104
+
105
+ ell = Ellipse(
106
+ (cg, cs), 2 * radius, 2 * r_minor,
107
+ angle=ang_deg, facecolor=col, alpha=0.20,
108
+ edgecolor=col, linewidth=2, linestyle='--', zorder=8)
109
+ ax.add_patch(ell)
110
+ cursor_artists.append(ell)
111
+
112
+ (dot,) = ax.plot(cg, cs, 'o', color=col, ms=8, zorder=10)
113
+ cursor_artists.append(dot)
114
+ txt = ax.text(cg + 0.015, cs + 0.015, f'C{i+1}',
115
+ color=col, fontsize=10, fontweight='bold',
116
+ zorder=11)
117
+ cursor_artists.append(txt)
118
+
119
+ if len(cursors) >= 2:
120
+ g0, s0 = cursors[0]['center_g'], cursors[0]['center_s']
121
+ g1, s1 = cursors[1]['center_g'], cursors[1]['center_s']
122
+ gi0, si0, gi1, si1 = phasor_semicircle_intersect(
123
+ g0, s0, g1, s1)
124
+ if not np.isnan(gi0):
125
+ (ln,) = ax.plot(
126
+ [float(gi0), float(gi1)],
127
+ [float(si0), float(si1)],
128
+ 'r-', lw=1.5, alpha=0.6, zorder=7)
129
+ cursor_artists.append(ln)
130
+ (st1,) = ax.plot(float(gi0), float(si0),
131
+ 'g*', ms=12, zorder=12)
132
+ (st2,) = ax.plot(float(gi1), float(si1),
133
+ 'm*', ms=12, zorder=12)
134
+ cursor_artists.extend([st1, st2])
135
+
136
+ ax.figure.canvas.draw_idle()
137
+
138
+ def _analyse():
139
+ cursors = state['cursors']
140
+ if not cursors:
141
+ state['masks'] = None
142
+ _begin_output()
143
+ print('Click on the phasor plot to place elliptical cursors.')
144
+ _end_output()
145
+ return
146
+
147
+ radius = params['radius']
148
+ r_minor = params['radius_minor']
149
+ mode = params['angle_mode']
150
+ n_cursors = len(cursors)
151
+
152
+ disp_arr = np.asarray(disp)
153
+ if disp_arr.ndim != 2 or min(disp_arr.shape) < 2:
154
+
155
+ _begin_output()
156
+ g_pt = float(np.ravel(rc)[0])
157
+ s_pt = float(np.ravel(ic)[0])
158
+ tau_phi_pt = float(phasor_to_apparent_lifetime(g_pt, s_pt, frequency)[0])
159
+ fig2, ax_pt = plt.subplots(1, 1, figsize=(5, 4))
160
+ ax_pt.text(0.5, 0.5, 'No image available\n(point measurement)',
161
+ ha='center', va='center', fontsize=11)
162
+ ax_pt.set_xticks([])
163
+ ax_pt.set_yticks([])
164
+ ax_pt.set_title(f'τ_φ = {tau_phi_pt:.2f} ns (G={g_pt:.3f}, S={s_pt:.3f})')
165
+ fig2.tight_layout()
166
+ plt.show()
167
+ print(f'Point measurement: τ_φ = {tau_phi_pt:.2f} ns '
168
+ f'(G={g_pt:.3f}, S={s_pt:.3f})')
169
+ _end_output()
170
+ return
171
+
172
+ centers_g = np.array([c['center_g'] for c in cursors])
173
+ centers_s = np.array([c['center_s'] for c in cursors])
174
+
175
+ masks = mask_from_elliptic_cursor(
176
+ rc, ic, centers_g, centers_s,
177
+ radius=radius, radius_minor=r_minor, angle=mode,
178
+ )
179
+ if masks.ndim == rc.ndim:
180
+ masks = masks[np.newaxis]
181
+ masks = masks & valid[np.newaxis]
182
+ state['masks'] = masks
183
+
184
+ _begin_output()
185
+
186
+ has_decomp = n_cursors >= 2
187
+ n_cols = 1 + n_cursors + (1 if has_decomp else 0)
188
+ fig2, axes2 = plt.subplots(
189
+ 1, n_cols, figsize=(4.5 * n_cols, 4.5))
190
+ if n_cols == 1:
191
+ axes2 = [axes2]
192
+
193
+ ax_pc = axes2[0]
194
+ mask_list = [masks[i] for i in range(n_cursors)]
195
+ colors_rgb = np.array([
196
+ [int(c['color'][1:3], 16) / 255,
197
+ int(c['color'][3:5], 16) / 255,
198
+ int(c['color'][5:7], 16) / 255]
199
+ for c in cursors
200
+ ])
201
+ pc_img = pseudo_color(
202
+ *mask_list, intensity=disp, colors=colors_rgb)
203
+ ax_pc.imshow(pc_img, origin='upper')
204
+ ax_pc.set_title('Pseudo-color overlay')
205
+ for i, c in enumerate(cursors):
206
+ ax_pc.text(
207
+ 5, 15 + i * 15,
208
+ f'C{i+1}: {int(masks[i].sum())} px',
209
+ color=c['color'], fontsize=9, fontweight='bold')
210
+
211
+ for ci in range(n_cursors):
212
+ ax_cur = axes2[1 + ci]
213
+ mask_i = masks[ci]
214
+ n_px = int(mask_i.sum())
215
+ if n_px == 0:
216
+ ax_cur.set_title(f'C{ci+1}: no pixels')
217
+ continue
218
+
219
+ g_sel = rc[mask_i]
220
+ s_sel = ic[mask_i]
221
+ tau_phi, tau_mod = phasor_to_apparent_lifetime(
222
+ g_sel, s_sel, frequency)
223
+
224
+ valid_mod = tau_mod[np.isfinite(tau_mod)]
225
+
226
+ fmap = np.full_like(rc, np.nan)
227
+ fmap[mask_i] = tau_phi
228
+ vlo = np.nanpercentile(tau_phi, 2)
229
+ vhi = np.nanpercentile(tau_phi, 98)
230
+ im = ax_cur.imshow(
231
+ fmap, cmap='turbo', vmin=vlo, vmax=vhi,
232
+ origin='upper')
233
+ plt.colorbar(im, ax=ax_cur, label='τ_φ (ns)')
234
+
235
+ med = np.nanmedian(tau_phi)
236
+ med_mod = float(np.median(valid_mod)) if valid_mod.size > 0 else float('nan')
237
+ mod_str = f'{med_mod:.2f}' if np.isfinite(med_mod) else 'n/a'
238
+ ax_cur.set_title(
239
+ f'C{ci+1}: τ_φ med={med:.2f}, τ_m med={mod_str} ns (n={n_px})')
240
+ print(
241
+ f"C{ci+1}: {n_px} px "
242
+ f"τ_φ = {np.nanmin(tau_phi):.2f}"
243
+ f"-{np.nanmax(tau_phi):.2f} ns "
244
+ f"(med {med:.2f}) │ "
245
+ f"τ_m med = {mod_str} ns")
246
+
247
+ if has_decomp:
248
+ ax_dec = axes2[-1]
249
+ g0, s0 = centers_g[0], centers_s[0]
250
+ g1, s1 = centers_g[1], centers_s[1]
251
+ gi0, si0, gi1, si1 = phasor_semicircle_intersect(
252
+ g0, s0, g1, s1)
253
+
254
+ if not (np.isnan(gi0) or np.isnan(gi1)):
255
+ tau1 = float(phasor_to_apparent_lifetime(
256
+ gi0, si0, frequency)[0])
257
+ tau2 = float(phasor_to_apparent_lifetime(
258
+ gi1, si1, frequency)[0])
259
+
260
+ combined = np.any(masks, axis=0)
261
+ g_comb = rc[combined]
262
+ s_comb = ic[combined]
263
+
264
+ frac = phasor_component_fraction(
265
+ g_comb, s_comb,
266
+ [float(gi0), float(gi1)],
267
+ [float(si0), float(si1)],
268
+ )
269
+
270
+ theta = np.linspace(0, np.pi, 200)
271
+ ax_dec.plot(
272
+ 0.5 + 0.5 * np.cos(theta),
273
+ 0.5 * np.sin(theta),
274
+ 'k-', lw=1, alpha=0.6)
275
+ sc = ax_dec.scatter(
276
+ g_comb, s_comb, c=frac, s=2,
277
+ cmap='RdYlBu_r', vmin=0, vmax=1,
278
+ alpha=0.5, zorder=5)
279
+ plt.colorbar(sc, ax=ax_dec, label='fraction C1')
280
+ ax_dec.plot(
281
+ [float(gi0), float(gi1)],
282
+ [float(si0), float(si1)],
283
+ 'r-', lw=2, zorder=10)
284
+ ax_dec.plot(
285
+ float(gi0), float(si0), 'g*', ms=14,
286
+ zorder=15, label=f'τ₁={tau1:.2f} ns')
287
+ ax_dec.plot(
288
+ float(gi1), float(si1), 'm*', ms=14,
289
+ zorder=15, label=f'τ₂={tau2:.2f} ns')
290
+ ax_dec.set_xlabel('G')
291
+ ax_dec.set_ylabel('S')
292
+ ax_dec.set_xlim(0, 1.05)
293
+ ax_dec.set_ylim(-0.02, 0.55)
294
+ ax_dec.set_aspect('equal')
295
+ ax_dec.legend(fontsize=9, loc='upper left')
296
+ ax_dec.set_title('Two-component (C1↔C2)')
297
+
298
+ print(
299
+ f"\n═══ Two-component decomposition (C1-C2) ═══")
300
+ print(
301
+ f" τ₁ = {tau1:.3f} ns "
302
+ f"(G={float(gi0):.4f}, S={float(si0):.4f})")
303
+ print(
304
+ f" τ₂ = {tau2:.3f} ns "
305
+ f"(G={float(gi1):.4f}, S={float(si1):.4f})")
306
+ print(
307
+ f" Mean fraction(τ₁) = "
308
+ f"{float(np.mean(frac)):.3f}")
309
+ else:
310
+ ax_dec.text(
311
+ 0.5, 0.5,
312
+ 'C1↔C2 line does not\nintersect semicircle',
313
+ ha='center', va='center',
314
+ transform=ax_dec.transAxes)
315
+ ax_dec.set_title('Two-component (C1↔C2)')
316
+
317
+ fig2.tight_layout()
318
+ plt.show()
319
+ _end_output()
320
+
321
+ def _on_click(event):
322
+ if event.inaxes != state['ax'] or event.button != 1:
323
+ return
324
+ if event.xdata is None:
325
+ return
326
+ cursors = state['cursors']
327
+ if len(cursors) >= max_cursors:
328
+ return
329
+ idx = len(cursors) % len(_COLORS)
330
+ cursors.append(dict(
331
+ center_g=event.xdata,
332
+ center_s=event.ydata,
333
+ color=_COLORS[idx]))
334
+ _redraw()
335
+ _analyse()
336
+
337
+ def _on_clear(_ignored=None):
338
+ state['cursors'].clear()
339
+ state['masks'] = None
340
+ _redraw()
341
+ _begin_output()
342
+ print('Cleared. Click on the phasor plot to place cursors.')
343
+ _end_output()
344
+
345
+ def _on_undo(_ignored=None):
346
+ cursors = state['cursors']
347
+ if cursors:
348
+ cursors.pop()
349
+ _redraw()
350
+ if cursors:
351
+ _analyse()
352
+ else:
353
+ state['masks'] = None
354
+ _begin_output()
355
+ print('All cursors removed. Click to place new ones.')
356
+ _end_output()
357
+
358
+ def _on_param_change(_ignored=None):
359
+ _redraw()
360
+ if state['cursors']:
361
+ _analyse()
362
+
363
+ if notebook:
364
+ fig, ax = plt.subplots(figsize=figsize)
365
+ else:
366
+
367
+ fig, ax = plt.subplots(figsize=(figsize[0], figsize[1] + 1.8))
368
+ fig.subplots_adjust(bottom=0.30)
369
+
370
+ try:
371
+ fig.canvas.toolbar_visible = True
372
+ fig.canvas.header_visible = False
373
+ except Exception:
374
+ pass
375
+
376
+ pp = PhasorPlot(
377
+ frequency=frequency, ax=ax,
378
+ title=f'Click to place elliptical cursors (up to {max_cursors})')
379
+ pp.hist2d(g_all, s_all)
380
+
381
+ state['fig'] = fig
382
+ state['ax'] = ax
383
+
384
+ fig.canvas.mpl_connect('button_press_event', _on_click)
385
+
386
+ if initial_cursors:
387
+ for cur in initial_cursors:
388
+ state['cursors'].append(dict(
389
+ center_g=cur['center_g'],
390
+ center_s=cur['center_s'],
391
+ color=cur.get('color', _COLORS[
392
+ len(state['cursors']) % len(_COLORS)])))
393
+ _redraw()
394
+ _analyse()
395
+
396
+ def _on_save(_ignored=None):
397
+ if on_save is not None:
398
+ on_save(state, params)
399
+
400
+ def _on_export(_ignored=None):
401
+ from ..dialogs import ask_save_path
402
+ path = ask_save_path(
403
+ 'Export phasor figure', 'phasor_plot.png', defaultextension='.png',
404
+ filetypes=[('PNG', '*.png'), ('PDF', '*.pdf'),
405
+ ('SVG', '*.svg'), ('All files', '*')])
406
+ if path:
407
+ state['fig'].savefig(path, dpi=300, bbox_inches='tight')
408
+ print(f"Figure exported → {path}")
409
+
410
+ _peak_artists: list = []
411
+
412
+ def _on_peaks(_ignored=None):
413
+ from .peaks import find_phasor_peaks, print_peaks, _annotate_peaks
414
+
415
+ for art in _peak_artists:
416
+ try:
417
+ art.remove()
418
+ except Exception:
419
+ pass
420
+ _peak_artists.clear()
421
+
422
+ _begin_output()
423
+ peaks = find_phasor_peaks(rc, ic, mn, frequency,
424
+ min_photons=min_photons)
425
+ state['peaks'] = peaks
426
+ print_peaks(peaks)
427
+ arts = _annotate_peaks(
428
+ state['ax'], peaks['n_peaks'],
429
+ peaks['peak_g'], peaks['peak_s'], peaks['tau_phase'])
430
+ _peak_artists.extend(arts)
431
+ state['ax'].figure.canvas.draw_idle()
432
+ _end_output()
433
+
434
+ if notebook:
435
+ import ipywidgets as widgets
436
+ from IPython.display import display
437
+
438
+ w_radius = widgets.FloatSlider(
439
+ value=params['radius'], min=0.01, max=0.25, step=0.005,
440
+ description='Major radius:', continuous_update=False,
441
+ style={'description_width': 'initial'})
442
+ w_radius_minor = widgets.FloatSlider(
443
+ value=params['radius_minor'], min=0.005, max=0.15, step=0.005,
444
+ description='Minor radius:', continuous_update=False,
445
+ style={'description_width': 'initial'})
446
+ w_angle_mode = widgets.Dropdown(
447
+ options=['semicircle', 'phase'],
448
+ value=params['angle_mode'], description='Alignment:',
449
+ style={'description_width': 'initial'})
450
+ btn_clear = widgets.Button(description='Clear all',
451
+ button_style='warning')
452
+ btn_undo = widgets.Button(description='Undo last', button_style='')
453
+ btn_save = widgets.Button(description='💾 Save',
454
+ button_style='success') if on_save else None
455
+ btn_export = widgets.Button(description='Export',
456
+ button_style='info')
457
+ btn_peaks = widgets.Button(description='Peaks',
458
+ button_style='')
459
+
460
+ def _ipyw_radius(change):
461
+ params['radius'] = change['new']
462
+ _on_param_change()
463
+
464
+ def _ipyw_radius_minor(change):
465
+ params['radius_minor'] = change['new']
466
+ _on_param_change()
467
+
468
+ def _ipyw_angle(change):
469
+ params['angle_mode'] = change['new']
470
+ _on_param_change()
471
+
472
+ w_radius.observe(_ipyw_radius, names='value')
473
+ w_radius_minor.observe(_ipyw_radius_minor, names='value')
474
+ w_angle_mode.observe(_ipyw_angle, names='value')
475
+ btn_clear.on_click(_on_clear)
476
+ btn_undo.on_click(_on_undo)
477
+ btn_export.on_click(_on_export)
478
+ btn_peaks.on_click(_on_peaks)
479
+ if btn_save:
480
+ btn_save.on_click(_on_save)
481
+
482
+ btn_row = [btn_undo, btn_clear, btn_peaks, btn_export]
483
+ if btn_save:
484
+ btn_row.append(btn_save)
485
+ controls = widgets.HBox([
486
+ widgets.VBox([w_radius, w_radius_minor]),
487
+ widgets.VBox([w_angle_mode,
488
+ widgets.HBox(btn_row)]),
489
+ ])
490
+ display(controls)
491
+ display(results_out)
492
+
493
+ else:
494
+ from matplotlib.widgets import Slider, Button, RadioButtons
495
+
496
+ ax_rad = fig.add_axes([0.15, 0.17, 0.55, 0.03])
497
+ ax_rmin = fig.add_axes([0.15, 0.12, 0.55, 0.03])
498
+ sl_radius = Slider(ax_rad, 'Major R', 0.01, 0.25,
499
+ valinit=params['radius'], valstep=0.005)
500
+ sl_radius_minor = Slider(ax_rmin, 'Minor R', 0.005, 0.15,
501
+ valinit=params['radius_minor'],
502
+ valstep=0.005)
503
+
504
+ ax_angle = fig.add_axes([0.15, 0.02, 0.15, 0.08])
505
+ rb_angle = RadioButtons(ax_angle, ['semicircle', 'phase'],
506
+ active=0)
507
+
508
+ ax_undo = fig.add_axes([0.35, 0.02, 0.10, 0.05])
509
+ ax_clr = fig.add_axes([0.46, 0.02, 0.10, 0.05])
510
+ ax_peaks = fig.add_axes([0.57, 0.02, 0.10, 0.05])
511
+ ax_exp = fig.add_axes([0.68, 0.02, 0.10, 0.05])
512
+ mpl_btn_undo = Button(ax_undo, 'Undo')
513
+ mpl_btn_clear = Button(ax_clr, 'Clear')
514
+ mpl_btn_peaks = Button(ax_peaks, 'Peaks')
515
+ mpl_btn_export = Button(ax_exp, 'Export')
516
+ if on_save:
517
+ ax_save = fig.add_axes([0.79, 0.02, 0.10, 0.05])
518
+ mpl_btn_save = Button(ax_save, 'Save')
519
+ else:
520
+ mpl_btn_save = None
521
+
522
+ def _mpl_radius(val):
523
+ params['radius'] = val
524
+ _on_param_change()
525
+
526
+ def _mpl_rminor(val):
527
+ params['radius_minor'] = val
528
+ _on_param_change()
529
+
530
+ def _mpl_angle(label):
531
+ params['angle_mode'] = label
532
+ _on_param_change()
533
+
534
+ sl_radius.on_changed(_mpl_radius)
535
+ sl_radius_minor.on_changed(_mpl_rminor)
536
+ rb_angle.on_clicked(_mpl_angle)
537
+ mpl_btn_undo.on_clicked(lambda _: _on_undo())
538
+ mpl_btn_clear.on_clicked(lambda _: _on_clear())
539
+ mpl_btn_peaks.on_clicked(lambda _: _on_peaks())
540
+ mpl_btn_export.on_clicked(lambda _: _on_export())
541
+ if mpl_btn_save:
542
+ mpl_btn_save.on_clicked(lambda _: _on_save())
543
+
544
+ state['_mpl_widgets'] = (sl_radius, sl_radius_minor, rb_angle,
545
+ mpl_btn_undo, mpl_btn_clear,
546
+ mpl_btn_peaks, mpl_btn_export,
547
+ mpl_btn_save)
548
+
549
+ print(f"Plotting {valid.sum()} valid pixels")
550
+ print(' Click on the phasor to place elliptical cursors. '
551
+ 'First two define the decomposition line.')
552
+
553
+ if not notebook:
554
+ plt.show()
555
+
556
+ return state