phasorpy 0.7__cp314-cp314-win_arm64.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.
- phasorpy/__init__.py +9 -0
- phasorpy/__main__.py +7 -0
- phasorpy/_phasorpy.cp314-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +2688 -0
- phasorpy/_typing.py +77 -0
- phasorpy/_utils.py +786 -0
- phasorpy/cli.py +160 -0
- phasorpy/cluster.py +200 -0
- phasorpy/color.py +589 -0
- phasorpy/component.py +707 -0
- phasorpy/conftest.py +38 -0
- phasorpy/cursor.py +500 -0
- phasorpy/datasets.py +722 -0
- phasorpy/experimental.py +310 -0
- phasorpy/io/__init__.py +138 -0
- phasorpy/io/_flimlabs.py +360 -0
- phasorpy/io/_leica.py +331 -0
- phasorpy/io/_ometiff.py +444 -0
- phasorpy/io/_other.py +890 -0
- phasorpy/io/_simfcs.py +652 -0
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +2018 -0
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +723 -0
- phasorpy/plot/_lifetime_plots.py +563 -0
- phasorpy/plot/_phasorplot.py +1507 -0
- phasorpy/plot/_phasorplot_fret.py +561 -0
- phasorpy/py.typed +0 -0
- phasorpy/utils.py +172 -0
- phasorpy-0.7.dist-info/METADATA +74 -0
- phasorpy-0.7.dist-info/RECORD +35 -0
- phasorpy-0.7.dist-info/WHEEL +5 -0
- phasorpy-0.7.dist-info/entry_points.txt +2 -0
- phasorpy-0.7.dist-info/licenses/LICENSE.txt +21 -0
- phasorpy-0.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,563 @@
|
|
1
|
+
"""LifetimePlots class."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
__all__ = ['LifetimePlots']
|
6
|
+
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from .._typing import Any, NDArray, ArrayLike
|
11
|
+
|
12
|
+
from matplotlib.axes import Axes
|
13
|
+
|
14
|
+
import numpy
|
15
|
+
from matplotlib import pyplot
|
16
|
+
from matplotlib.lines import Line2D
|
17
|
+
from matplotlib.widgets import Slider
|
18
|
+
|
19
|
+
from .._utils import update_kwargs
|
20
|
+
from ..lifetime import (
|
21
|
+
lifetime_to_frequency,
|
22
|
+
lifetime_to_signal,
|
23
|
+
phasor_from_lifetime,
|
24
|
+
)
|
25
|
+
from ..phasor import (
|
26
|
+
phasor_to_polar,
|
27
|
+
phasor_transform,
|
28
|
+
)
|
29
|
+
from ..plot._phasorplot import (
|
30
|
+
CircleTicks,
|
31
|
+
PhasorPlot,
|
32
|
+
_semicircle_ticks,
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
class LifetimePlots:
|
37
|
+
"""Plot lifetimes in time domain, frequency domain, and phasor plot.
|
38
|
+
|
39
|
+
Plot the time domain signals, phasor coordinates, and multi-frequency
|
40
|
+
phase and modulation curves for a set of lifetime components and their
|
41
|
+
mixture at given frequency and fractional intensities.
|
42
|
+
|
43
|
+
Parameters
|
44
|
+
----------
|
45
|
+
frequency : float
|
46
|
+
Fundamental laser pulse or modulation frequency in MHz.
|
47
|
+
If None, an optimal frequency is calculated from the mean of the
|
48
|
+
lifetime components.
|
49
|
+
lifetime : array_like
|
50
|
+
Lifetime components in ns. Up to 6 components are supported.
|
51
|
+
fraction : array_like, optional
|
52
|
+
Fractional intensities of lifetime components.
|
53
|
+
Fractions are normalized to sum to 1.
|
54
|
+
If not given, all components are assumed to have equal fractions.
|
55
|
+
frequency_range : tuple[float, float, float], optional
|
56
|
+
Range of frequencies in MHz for frequency slider.
|
57
|
+
Default is (10.0, 200.0, 1.0).
|
58
|
+
lifetime_range : tuple[float, float, float], optional
|
59
|
+
Range of lifetimes in ns for lifetime sliders.
|
60
|
+
Default is (0.0, 20.0, 0.1).
|
61
|
+
interactive: bool
|
62
|
+
If True, add sliders to change frequency and lifetimes interactively.
|
63
|
+
Default is False.
|
64
|
+
**kwargs:
|
65
|
+
Additional arguments passed to matplotlib figure.
|
66
|
+
|
67
|
+
"""
|
68
|
+
|
69
|
+
_samples: int = 256 # number of frequencies and samples in signal
|
70
|
+
_frequency: float # current frequency in MHz
|
71
|
+
_zero_phase: float | None = None # location of IRF peak in the phase
|
72
|
+
_zero_stdev: float | None = None # standard deviation of IRF in radians
|
73
|
+
_frequencies: NDArray[Any] # for frequency domain plot
|
74
|
+
|
75
|
+
_time_plot: Axes
|
76
|
+
_phasor_plot: Axes
|
77
|
+
_phase_plot: Axes
|
78
|
+
_modulation_plot: Axes
|
79
|
+
|
80
|
+
_frequency_slider: Slider
|
81
|
+
_lifetime_sliders: list[Slider]
|
82
|
+
_fraction_sliders: list[Slider]
|
83
|
+
|
84
|
+
_signal_line: Line2D
|
85
|
+
_frequency_line: Line2D
|
86
|
+
_phase_point: Line2D
|
87
|
+
_modulation_point: Line2D
|
88
|
+
|
89
|
+
_signal_lines: list[Line2D]
|
90
|
+
_phasor_lines: list[Line2D]
|
91
|
+
_phasor_points: list[Line2D]
|
92
|
+
_phase_lines: list[Line2D]
|
93
|
+
_modulation_lines: list[Line2D]
|
94
|
+
|
95
|
+
_semicircle_line: Line2D
|
96
|
+
_semicircle_ticks: CircleTicks | None
|
97
|
+
|
98
|
+
_component_colors = (
|
99
|
+
# 'tab:blue', # main
|
100
|
+
# 'tab:red', # modulation
|
101
|
+
# 'tab:gray', # irf
|
102
|
+
'tab:orange',
|
103
|
+
'tab:green',
|
104
|
+
'tab:purple',
|
105
|
+
'tab:pink',
|
106
|
+
'tab:olive',
|
107
|
+
'tab:cyan',
|
108
|
+
'tab:brown',
|
109
|
+
)
|
110
|
+
|
111
|
+
def __init__(
|
112
|
+
self,
|
113
|
+
frequency: float | None,
|
114
|
+
lifetime: ArrayLike,
|
115
|
+
fraction: ArrayLike | None = None,
|
116
|
+
*,
|
117
|
+
frequency_range: tuple[float, float, float] = (10.0, 200.0, 1.0),
|
118
|
+
lifetime_range: tuple[float, float, float] = (0.0, 20.0, 0.1),
|
119
|
+
interactive: bool = False,
|
120
|
+
**kwargs: Any,
|
121
|
+
) -> None:
|
122
|
+
self._frequencies = numpy.logspace(-1, 4, self._samples)
|
123
|
+
|
124
|
+
(
|
125
|
+
frequency,
|
126
|
+
lifetimes,
|
127
|
+
fractions,
|
128
|
+
signal,
|
129
|
+
irf,
|
130
|
+
times,
|
131
|
+
real,
|
132
|
+
imag,
|
133
|
+
phase,
|
134
|
+
modulation,
|
135
|
+
phase_,
|
136
|
+
modulation_,
|
137
|
+
component_signal,
|
138
|
+
component_real,
|
139
|
+
component_imag,
|
140
|
+
component_phase_,
|
141
|
+
component_modulation_,
|
142
|
+
) = self._calculate(frequency, lifetime, fraction)
|
143
|
+
|
144
|
+
self._frequency = frequency
|
145
|
+
|
146
|
+
num_components = max(lifetimes.size, 1)
|
147
|
+
if num_components > 6:
|
148
|
+
raise ValueError(f'too many components {num_components} > 6')
|
149
|
+
|
150
|
+
# create plots
|
151
|
+
update_kwargs(kwargs, figsize=(10.24, 7.68))
|
152
|
+
fig, ((time_plot, phasor_plot), (phase_plot, ax4)) = pyplot.subplots(
|
153
|
+
2, 2, **kwargs
|
154
|
+
)
|
155
|
+
|
156
|
+
if interactive:
|
157
|
+
fcm = fig.canvas.manager
|
158
|
+
if fcm is not None:
|
159
|
+
fcm.set_window_title('PhasorPy lifetime plots')
|
160
|
+
|
161
|
+
self._signal_lines = []
|
162
|
+
self._phasor_lines = []
|
163
|
+
self._phasor_points = []
|
164
|
+
self._phase_lines = []
|
165
|
+
self._modulation_lines = []
|
166
|
+
|
167
|
+
# time domain plot
|
168
|
+
time_plot.set_title('Time domain')
|
169
|
+
time_plot.set_xlabel('Time [ns]')
|
170
|
+
time_plot.set_ylabel('Intensity [normalized]')
|
171
|
+
lines = time_plot.plot(
|
172
|
+
times,
|
173
|
+
signal,
|
174
|
+
label='Signal',
|
175
|
+
color='tab:blue',
|
176
|
+
linewidth=2,
|
177
|
+
zorder=10,
|
178
|
+
)
|
179
|
+
self._signal_lines.append(lines[0])
|
180
|
+
if num_components > 1:
|
181
|
+
for i in range(num_components):
|
182
|
+
lines = time_plot.plot(
|
183
|
+
times,
|
184
|
+
component_signal[i],
|
185
|
+
label=f'Lifetime {i}',
|
186
|
+
color=self._component_colors[i],
|
187
|
+
linewidth=0.8,
|
188
|
+
alpha=0.5,
|
189
|
+
)
|
190
|
+
self._signal_lines.append(lines[0])
|
191
|
+
lines = time_plot.plot(
|
192
|
+
times,
|
193
|
+
irf,
|
194
|
+
label='Instrument response',
|
195
|
+
color='tab:grey',
|
196
|
+
linewidth=0.8,
|
197
|
+
alpha=0.5,
|
198
|
+
)
|
199
|
+
self._signal_lines.append(lines[0])
|
200
|
+
time_plot.legend()
|
201
|
+
|
202
|
+
# phasor plot
|
203
|
+
phasorplot = PhasorPlot(ax=phasor_plot)
|
204
|
+
lines = phasorplot.semicircle(frequency)
|
205
|
+
self._semicircle_line = lines[0]
|
206
|
+
self._semicircle_ticks = phasorplot._semicircle_ticks
|
207
|
+
lines = phasorplot.plot(
|
208
|
+
real, imag, 'o', color='tab:blue', markersize=10, zorder=10
|
209
|
+
)
|
210
|
+
self._phasor_points.append(lines[0])
|
211
|
+
if num_components > 1:
|
212
|
+
for i in range(num_components):
|
213
|
+
lines = phasorplot.plot(
|
214
|
+
(real, component_real[i]),
|
215
|
+
(imag, component_imag[i]),
|
216
|
+
color=self._component_colors[i],
|
217
|
+
linestyle='-',
|
218
|
+
linewidth=0.8,
|
219
|
+
alpha=0.5,
|
220
|
+
)
|
221
|
+
self._phasor_lines.append(lines[0])
|
222
|
+
lines = phasorplot.plot(
|
223
|
+
component_real[i],
|
224
|
+
component_imag[i],
|
225
|
+
'o',
|
226
|
+
color=self._component_colors[i],
|
227
|
+
)
|
228
|
+
self._phasor_points.append(lines[0])
|
229
|
+
|
230
|
+
# frequency domain plot
|
231
|
+
phase_plot.set_title('Frequency domain')
|
232
|
+
phase_plot.set_xscale('log', base=10)
|
233
|
+
phase_plot.set_xlabel('Frequency (MHz)')
|
234
|
+
phase_plot.set_ylabel('Phase (°)', color='tab:blue')
|
235
|
+
phase_plot.set_yticks((0.0, 30.0, 60.0, 90.0))
|
236
|
+
phase_plot.plot((1, 1), (0.0, 90.0), alpha=0.0) # set autoscale
|
237
|
+
lines = phase_plot.plot(
|
238
|
+
(frequency, frequency),
|
239
|
+
(0, 90),
|
240
|
+
'--',
|
241
|
+
color='gray',
|
242
|
+
linewidth=0.8,
|
243
|
+
alpha=0.5,
|
244
|
+
)
|
245
|
+
self._frequency_line = lines[0]
|
246
|
+
lines = phase_plot.plot(
|
247
|
+
frequency, phase, 'o', color='tab:blue', markersize=8, zorder=2
|
248
|
+
)
|
249
|
+
self._phase_point = lines[0]
|
250
|
+
lines = phase_plot.plot(
|
251
|
+
self._frequencies, phase_, color='tab:blue', linewidth=2, zorder=2
|
252
|
+
)
|
253
|
+
self._phase_lines.append(lines[0])
|
254
|
+
if num_components > 1:
|
255
|
+
for i in range(num_components):
|
256
|
+
lines = phase_plot.plot(
|
257
|
+
self._frequencies,
|
258
|
+
component_phase_[i],
|
259
|
+
color=self._component_colors[i],
|
260
|
+
linewidth=0.5,
|
261
|
+
alpha=0.5,
|
262
|
+
)
|
263
|
+
self._phase_lines.append(lines[0])
|
264
|
+
# phase_plot.text(0.1, 1, 'Phase', ha='left', va='bottom')
|
265
|
+
|
266
|
+
# TODO: zorder doesn't work.
|
267
|
+
# twinx modulation_plot is always plotted on top of phase_plot
|
268
|
+
modulation_plot = phase_plot.twinx()
|
269
|
+
modulation_plot.set_ylabel('Modulation (%)', color='tab:red')
|
270
|
+
modulation_plot.set_yticks((0.0, 25.0, 50.0, 75.0, 100.0))
|
271
|
+
modulation_plot.plot((1, 1), (0.0, 100.0), alpha=0.0) # set autoscale
|
272
|
+
lines = modulation_plot.plot(
|
273
|
+
frequency, modulation, 'o', color='tab:red', markersize=8, zorder=2
|
274
|
+
)
|
275
|
+
self._modulation_point = lines[0]
|
276
|
+
lines = modulation_plot.plot(
|
277
|
+
self._frequencies,
|
278
|
+
modulation_,
|
279
|
+
color='tab:red',
|
280
|
+
linewidth=2,
|
281
|
+
zorder=2,
|
282
|
+
)
|
283
|
+
self._modulation_lines.append(lines[0])
|
284
|
+
if num_components > 1:
|
285
|
+
for i in range(num_components):
|
286
|
+
lines = modulation_plot.plot(
|
287
|
+
self._frequencies,
|
288
|
+
component_modulation_[i],
|
289
|
+
color=self._component_colors[i],
|
290
|
+
linewidth=0.5,
|
291
|
+
alpha=0.5,
|
292
|
+
)
|
293
|
+
self._modulation_lines.append(lines[0])
|
294
|
+
# modulation_plot.text(0.1, 98, 'Modulation', ha='left', va='top')
|
295
|
+
|
296
|
+
ax4.axis('off')
|
297
|
+
self._time_plot = time_plot
|
298
|
+
self._phasor_plot = phasor_plot
|
299
|
+
self._phase_plot = phase_plot
|
300
|
+
self._modulation_plot = modulation_plot
|
301
|
+
|
302
|
+
fig.tight_layout()
|
303
|
+
|
304
|
+
if not interactive:
|
305
|
+
return
|
306
|
+
|
307
|
+
# add sliders
|
308
|
+
axes = (
|
309
|
+
fig.add_axes((0.65, 0.45 - i * 0.035, 0.25, 0.01))
|
310
|
+
for i in range(1 + 2 * num_components)
|
311
|
+
)
|
312
|
+
|
313
|
+
self._frequency_slider = Slider(
|
314
|
+
ax=next(axes),
|
315
|
+
label='Frequency ',
|
316
|
+
valfmt=' %.0f MHz',
|
317
|
+
valmin=frequency_range[0],
|
318
|
+
valmax=frequency_range[1],
|
319
|
+
valstep=frequency_range[2],
|
320
|
+
valinit=frequency,
|
321
|
+
)
|
322
|
+
self._frequency_slider.on_changed(self._on_changed)
|
323
|
+
|
324
|
+
self._lifetime_sliders = []
|
325
|
+
for i, (lifetime, color) in enumerate(
|
326
|
+
zip(numpy.atleast_1d(lifetimes), self._component_colors)
|
327
|
+
):
|
328
|
+
slider = Slider(
|
329
|
+
ax=next(axes),
|
330
|
+
label=f'Lifetime {i} ',
|
331
|
+
valfmt=' %.2f ns',
|
332
|
+
valmin=lifetime_range[0],
|
333
|
+
valmax=lifetime_range[1],
|
334
|
+
valstep=lifetime_range[2],
|
335
|
+
valinit=lifetime, # type: ignore[arg-type]
|
336
|
+
facecolor=color,
|
337
|
+
)
|
338
|
+
slider.on_changed(self._on_changed)
|
339
|
+
self._lifetime_sliders.append(slider)
|
340
|
+
|
341
|
+
self._fraction_sliders = []
|
342
|
+
for i, (fraction, color) in enumerate(
|
343
|
+
zip(numpy.atleast_1d(fractions), self._component_colors)
|
344
|
+
):
|
345
|
+
if num_components == 1 or (i == 1 and num_components == 2):
|
346
|
+
break
|
347
|
+
slider = Slider(
|
348
|
+
ax=next(axes),
|
349
|
+
label=f'Fraction {i} ',
|
350
|
+
valfmt=' %.2f',
|
351
|
+
valmin=0.0,
|
352
|
+
valmax=1.0,
|
353
|
+
valstep=0.01,
|
354
|
+
valinit=fraction, # type: ignore[arg-type]
|
355
|
+
facecolor=color,
|
356
|
+
)
|
357
|
+
slider.on_changed(self._on_changed)
|
358
|
+
self._fraction_sliders.append(slider)
|
359
|
+
|
360
|
+
def _calculate(
|
361
|
+
self,
|
362
|
+
frequency: float | None,
|
363
|
+
lifetimes: ArrayLike,
|
364
|
+
fractions: ArrayLike | None,
|
365
|
+
/,
|
366
|
+
) -> tuple[
|
367
|
+
float, # frequency
|
368
|
+
NDArray[Any], # lifetimes
|
369
|
+
NDArray[Any], # fractions
|
370
|
+
NDArray[Any], # signal
|
371
|
+
NDArray[Any], # irf
|
372
|
+
NDArray[Any], # times
|
373
|
+
float, # real
|
374
|
+
float, # imag
|
375
|
+
float, # phase
|
376
|
+
float, # modulation
|
377
|
+
NDArray[Any], # phase_
|
378
|
+
NDArray[Any], # modulation_
|
379
|
+
NDArray[Any], # component_signal
|
380
|
+
NDArray[Any], # component_real
|
381
|
+
NDArray[Any], # component_imag
|
382
|
+
NDArray[Any], # component_phase_
|
383
|
+
NDArray[Any], # component_modulation_
|
384
|
+
]:
|
385
|
+
"""Return values for plotting."""
|
386
|
+
lifetimes = numpy.asarray(lifetimes)
|
387
|
+
num_components = max(lifetimes.size, 1)
|
388
|
+
|
389
|
+
if fractions is None:
|
390
|
+
fractions = numpy.ones(num_components) / num_components
|
391
|
+
else:
|
392
|
+
fractions = numpy.asarray(fractions)
|
393
|
+
num_fractions = max(fractions.size, 1)
|
394
|
+
if num_fractions != num_components:
|
395
|
+
raise ValueError(f'{num_fractions=} != {num_components=}')
|
396
|
+
s = fractions.sum()
|
397
|
+
if s > 0.0:
|
398
|
+
fractions = numpy.clip(fractions / s, 0.0, 1.0)
|
399
|
+
else:
|
400
|
+
fractions = numpy.ones(num_components) / num_components
|
401
|
+
|
402
|
+
if frequency is None:
|
403
|
+
frequency = float(
|
404
|
+
# lifetime_to_frequency(numpy.atleast_1d(lifetimes)[0])
|
405
|
+
# lifetime_to_frequency(numpy.mean(lifetimes * fractions))
|
406
|
+
lifetime_to_frequency(numpy.mean(lifetimes))
|
407
|
+
)
|
408
|
+
|
409
|
+
signal, irf, times = lifetime_to_signal(
|
410
|
+
frequency,
|
411
|
+
lifetimes,
|
412
|
+
fractions,
|
413
|
+
mean=1.0,
|
414
|
+
samples=self._samples,
|
415
|
+
zero_phase=self._zero_phase,
|
416
|
+
zero_stdev=self._zero_stdev,
|
417
|
+
)
|
418
|
+
signal_max = signal.max()
|
419
|
+
signal /= signal_max
|
420
|
+
irf /= signal_max
|
421
|
+
|
422
|
+
component_signal = lifetime_to_signal(
|
423
|
+
frequency,
|
424
|
+
lifetimes,
|
425
|
+
mean=fractions,
|
426
|
+
samples=self._samples,
|
427
|
+
zero_phase=self._zero_phase,
|
428
|
+
zero_stdev=self._zero_stdev,
|
429
|
+
)[0]
|
430
|
+
component_signal /= signal_max
|
431
|
+
|
432
|
+
real, imag = phasor_from_lifetime(frequency, lifetimes, fractions)
|
433
|
+
component_real, component_imag = phasor_from_lifetime(
|
434
|
+
frequency, lifetimes
|
435
|
+
)
|
436
|
+
|
437
|
+
phase, modulation = _degpct(*phasor_to_polar(real, imag))
|
438
|
+
phase_, modulation_ = _degpct(
|
439
|
+
*phasor_to_polar(
|
440
|
+
*phasor_from_lifetime(self._frequencies, lifetimes, fractions)
|
441
|
+
)
|
442
|
+
)
|
443
|
+
component_phase_, component_modulation_ = phasor_to_polar(
|
444
|
+
*phasor_from_lifetime(self._frequencies, lifetimes)
|
445
|
+
)
|
446
|
+
component_phase_, component_modulation_ = _degpct(
|
447
|
+
component_phase_.T, component_modulation_.T
|
448
|
+
)
|
449
|
+
|
450
|
+
return (
|
451
|
+
frequency,
|
452
|
+
lifetimes,
|
453
|
+
fractions,
|
454
|
+
signal,
|
455
|
+
irf,
|
456
|
+
times,
|
457
|
+
float(real),
|
458
|
+
float(imag),
|
459
|
+
float(phase),
|
460
|
+
float(modulation),
|
461
|
+
phase_,
|
462
|
+
modulation_,
|
463
|
+
component_signal,
|
464
|
+
component_real,
|
465
|
+
component_imag,
|
466
|
+
component_phase_,
|
467
|
+
component_modulation_,
|
468
|
+
) # type: ignore[return-value]
|
469
|
+
|
470
|
+
def _on_changed(self, value: Any) -> None:
|
471
|
+
"""Callback function to update plot with current slider values."""
|
472
|
+
frequency = self._frequency_slider.val
|
473
|
+
|
474
|
+
if frequency != self._frequency:
|
475
|
+
if self._semicircle_ticks is not None:
|
476
|
+
lifetime, labels = _semicircle_ticks(frequency)
|
477
|
+
self._semicircle_ticks.labels = labels
|
478
|
+
self._semicircle_line.set_data(
|
479
|
+
*phasor_transform(
|
480
|
+
*phasor_from_lifetime(frequency, lifetime)
|
481
|
+
)
|
482
|
+
)
|
483
|
+
self._frequency_line.set_data([frequency, frequency], [0, 90])
|
484
|
+
# self._time_plot.set_title(f'Time domain ({frequency:.0f} MHz)')
|
485
|
+
# self._phasor_plot.set_title(f'Phasor plot ({frequency:.0f} MHz)')
|
486
|
+
|
487
|
+
lifetimes = numpy.asarray([s.val for s in self._lifetime_sliders])
|
488
|
+
fractions = numpy.asarray([s.val for s in self._fraction_sliders])
|
489
|
+
|
490
|
+
num_components = len(lifetimes)
|
491
|
+
if num_components == 1:
|
492
|
+
fractions = numpy.asarray([1.0])
|
493
|
+
elif num_components == 2:
|
494
|
+
fractions = numpy.asarray([fractions[0], 1.0 - fractions[0]])
|
495
|
+
|
496
|
+
(
|
497
|
+
frequency,
|
498
|
+
lifetimes,
|
499
|
+
fractions,
|
500
|
+
signal,
|
501
|
+
irf,
|
502
|
+
times,
|
503
|
+
real,
|
504
|
+
imag,
|
505
|
+
phase,
|
506
|
+
modulation,
|
507
|
+
phase_,
|
508
|
+
modulation_,
|
509
|
+
component_signal,
|
510
|
+
component_real,
|
511
|
+
component_imag,
|
512
|
+
component_phase_,
|
513
|
+
component_modulation_,
|
514
|
+
) = self._calculate(frequency, lifetimes, fractions)
|
515
|
+
|
516
|
+
# time domain plot
|
517
|
+
self._signal_lines[0].set_data(times, signal)
|
518
|
+
if num_components > 1:
|
519
|
+
for i in range(num_components):
|
520
|
+
self._signal_lines[i + 1].set_data(times, component_signal[i])
|
521
|
+
self._signal_lines[-1].set_data(times, irf)
|
522
|
+
if frequency != self._frequency:
|
523
|
+
self._time_plot.relim()
|
524
|
+
self._time_plot.autoscale_view()
|
525
|
+
|
526
|
+
# phasor plot
|
527
|
+
self._phasor_points[0].set_data([real], [imag])
|
528
|
+
if num_components > 1:
|
529
|
+
for i in range(num_components):
|
530
|
+
self._phasor_lines[i].set_data(
|
531
|
+
[real, component_real[i]], [imag, component_imag[i]]
|
532
|
+
)
|
533
|
+
self._phasor_points[i + 1].set_data(
|
534
|
+
[component_real[i]], [component_imag[i]]
|
535
|
+
)
|
536
|
+
|
537
|
+
# frequency domain plot
|
538
|
+
self._frequency_line.set_data([frequency, frequency], [0, 90])
|
539
|
+
self._phase_point.set_data([frequency], [phase])
|
540
|
+
self._modulation_point.set_data([frequency], [modulation])
|
541
|
+
self._phase_lines[0].set_data(self._frequencies, phase_)
|
542
|
+
self._modulation_lines[0].set_data(self._frequencies, modulation_)
|
543
|
+
if num_components > 1:
|
544
|
+
for i in range(num_components):
|
545
|
+
self._phase_lines[i + 1].set_data(
|
546
|
+
self._frequencies, component_phase_[i]
|
547
|
+
)
|
548
|
+
self._modulation_lines[i + 1].set_data(
|
549
|
+
self._frequencies, component_modulation_[i]
|
550
|
+
)
|
551
|
+
|
552
|
+
self._frequency = frequency
|
553
|
+
|
554
|
+
def show(self) -> None:
|
555
|
+
"""Display all open figures. Call :py:func:`matplotlib.pyplot.show`."""
|
556
|
+
pyplot.show()
|
557
|
+
|
558
|
+
|
559
|
+
def _degpct(
|
560
|
+
phase: ArrayLike, modulation: ArrayLike, /
|
561
|
+
) -> tuple[NDArray[Any], NDArray[Any]]:
|
562
|
+
"""Return phase in degrees and modulation in percent."""
|
563
|
+
return numpy.rad2deg(phase), numpy.multiply(modulation, 100.0)
|