phasorpy 0.6__cp312-cp312-win_arm64.whl → 0.8__cp312-cp312-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 +1 -1
- phasorpy/_phasorpy.cp312-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +320 -10
- phasorpy/_utils.py +114 -33
- phasorpy/cli.py +19 -1
- phasorpy/cluster.py +12 -18
- phasorpy/color.py +11 -7
- phasorpy/{components.py → component.py} +263 -36
- phasorpy/{cursors.py → cursor.py} +31 -33
- phasorpy/datasets.py +118 -8
- phasorpy/experimental.py +4 -168
- phasorpy/filter.py +966 -0
- phasorpy/io/__init__.py +3 -1
- phasorpy/io/_flimlabs.py +26 -16
- phasorpy/io/_leica.py +38 -34
- phasorpy/io/_ometiff.py +10 -9
- phasorpy/io/_other.py +116 -8
- phasorpy/io/_simfcs.py +52 -24
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +106 -2502
- phasorpy/plot/_functions.py +13 -7
- phasorpy/plot/_lifetime_plots.py +34 -24
- phasorpy/plot/_phasorplot.py +561 -176
- phasorpy/plot/_phasorplot_fret.py +12 -10
- phasorpy/utils.py +22 -10
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/METADATA +8 -7
- phasorpy-0.8.dist-info/RECORD +36 -0
- phasorpy-0.6.dist-info/RECORD +0 -34
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/WHEEL +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/top_level.txt +0 -0
phasorpy/phasor.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""Calculate, convert,
|
1
|
+
"""Calculate, convert, and reduce phasor coordinates.
|
2
2
|
|
3
3
|
The ``phasorpy.phasor`` module provides functions to:
|
4
4
|
|
@@ -6,25 +6,14 @@ The ``phasorpy.phasor`` module provides functions to:
|
|
6
6
|
|
7
7
|
- :py:func:`phasor_from_signal`
|
8
8
|
|
9
|
-
- synthesize signals from phasor coordinates
|
9
|
+
- synthesize signals from phasor coordinates:
|
10
10
|
|
11
11
|
- :py:func:`phasor_to_signal`
|
12
|
-
- :py:func:`lifetime_to_signal`
|
13
|
-
|
14
|
-
- convert between phasor coordinates and single- or multi-component
|
15
|
-
fluorescence lifetimes:
|
16
|
-
|
17
|
-
- :py:func:`phasor_from_lifetime`
|
18
|
-
- :py:func:`phasor_from_apparent_lifetime`
|
19
|
-
- :py:func:`phasor_to_apparent_lifetime`
|
20
|
-
- :py:func:`phasor_to_normal_lifetime`
|
21
12
|
|
22
13
|
- convert to and from polar coordinates (phase and modulation):
|
23
14
|
|
24
15
|
- :py:func:`phasor_from_polar`
|
25
16
|
- :py:func:`phasor_to_polar`
|
26
|
-
- :py:func:`polar_from_apparent_lifetime`
|
27
|
-
- :py:func:`polar_to_apparent_lifetime`
|
28
17
|
|
29
18
|
- transform phasor coordinates:
|
30
19
|
|
@@ -33,44 +22,16 @@ The ``phasorpy.phasor`` module provides functions to:
|
|
33
22
|
- :py:func:`phasor_divide`
|
34
23
|
- :py:func:`phasor_normalize`
|
35
24
|
|
36
|
-
-
|
37
|
-
lifetime:
|
25
|
+
- linearly combine two phasor coordinates:
|
38
26
|
|
39
|
-
- :py:func:`
|
40
|
-
- :py:func:`polar_from_reference`
|
41
|
-
- :py:func:`polar_from_reference_phasor`
|
27
|
+
- :py:func:`phasor_combine`
|
42
28
|
|
43
29
|
- reduce dimensionality of arrays of phasor coordinates:
|
44
30
|
|
45
31
|
- :py:func:`phasor_center`
|
46
32
|
- :py:func:`phasor_to_principal_plane`
|
47
33
|
|
48
|
-
-
|
49
|
-
|
50
|
-
- :py:func:`phasor_from_fret_donor`
|
51
|
-
- :py:func:`phasor_from_fret_acceptor`
|
52
|
-
|
53
|
-
- convert between single component lifetimes and optimal frequency:
|
54
|
-
|
55
|
-
- :py:func:`lifetime_to_frequency`
|
56
|
-
- :py:func:`lifetime_from_frequency`
|
57
|
-
|
58
|
-
- convert between fractional intensities and pre-exponential amplitudes:
|
59
|
-
|
60
|
-
- :py:func:`lifetime_fraction_from_amplitude`
|
61
|
-
- :py:func:`lifetime_fraction_to_amplitude`
|
62
|
-
|
63
|
-
- calculate phasor coordinates on universal semicircle at other harmonics:
|
64
|
-
|
65
|
-
- :py:func:`phasor_at_harmonic`
|
66
|
-
|
67
|
-
- filter phasor coordinates:
|
68
|
-
|
69
|
-
- :py:func:`phasor_filter_median`
|
70
|
-
- :py:func:`phasor_filter_pawflim`
|
71
|
-
- :py:func:`phasor_threshold`
|
72
|
-
|
73
|
-
- find nearest neighbor phasor coordinates from another set of phasor coordinates:
|
34
|
+
- find nearest neighbor phasor coordinates from other phasor coordinates:
|
74
35
|
|
75
36
|
- :py:func:`phasor_nearest_neighbor`
|
76
37
|
|
@@ -79,40 +40,19 @@ The ``phasorpy.phasor`` module provides functions to:
|
|
79
40
|
from __future__ import annotations
|
80
41
|
|
81
42
|
__all__ = [
|
82
|
-
'lifetime_fraction_from_amplitude',
|
83
|
-
'lifetime_fraction_to_amplitude',
|
84
|
-
'lifetime_from_frequency',
|
85
|
-
'lifetime_to_frequency',
|
86
|
-
'lifetime_to_signal',
|
87
|
-
'phasor_at_harmonic',
|
88
|
-
'phasor_calibrate',
|
89
43
|
'phasor_center',
|
90
44
|
'phasor_divide',
|
91
|
-
'phasor_filter_median',
|
92
|
-
'phasor_filter_pawflim',
|
93
|
-
'phasor_from_apparent_lifetime',
|
94
|
-
'phasor_from_fret_acceptor',
|
95
|
-
'phasor_from_fret_donor',
|
96
|
-
'phasor_from_lifetime',
|
97
45
|
'phasor_from_polar',
|
98
46
|
'phasor_from_signal',
|
47
|
+
'phasor_combine',
|
99
48
|
'phasor_multiply',
|
100
49
|
'phasor_nearest_neighbor',
|
101
50
|
'phasor_normalize',
|
102
|
-
'phasor_semicircle',
|
103
|
-
'phasor_semicircle_intersect',
|
104
|
-
'phasor_threshold',
|
105
|
-
'phasor_to_apparent_lifetime',
|
106
51
|
'phasor_to_complex',
|
107
|
-
'phasor_to_normal_lifetime',
|
108
52
|
'phasor_to_polar',
|
109
53
|
'phasor_to_principal_plane',
|
110
54
|
'phasor_to_signal',
|
111
55
|
'phasor_transform',
|
112
|
-
'polar_from_apparent_lifetime',
|
113
|
-
'polar_from_reference',
|
114
|
-
'polar_from_reference_phasor',
|
115
|
-
'polar_to_apparent_lifetime',
|
116
56
|
]
|
117
57
|
|
118
58
|
import math
|
@@ -132,37 +72,18 @@ if TYPE_CHECKING:
|
|
132
72
|
import numpy
|
133
73
|
|
134
74
|
from ._phasorpy import (
|
135
|
-
_gaussian_signal,
|
136
|
-
_intersect_semicircle_line,
|
137
|
-
_median_filter_2d,
|
138
75
|
_nearest_neighbor_2d,
|
139
|
-
|
76
|
+
_phasor_combine,
|
140
77
|
_phasor_divide,
|
141
|
-
_phasor_from_apparent_lifetime,
|
142
|
-
_phasor_from_fret_acceptor,
|
143
|
-
_phasor_from_fret_donor,
|
144
|
-
_phasor_from_lifetime,
|
145
78
|
_phasor_from_polar,
|
146
79
|
_phasor_from_signal,
|
147
|
-
_phasor_from_single_lifetime,
|
148
80
|
_phasor_multiply,
|
149
|
-
_phasor_threshold_closed,
|
150
|
-
_phasor_threshold_mean_closed,
|
151
|
-
_phasor_threshold_mean_open,
|
152
|
-
_phasor_threshold_nan,
|
153
|
-
_phasor_threshold_open,
|
154
|
-
_phasor_to_apparent_lifetime,
|
155
|
-
_phasor_to_normal_lifetime,
|
156
81
|
_phasor_to_polar,
|
157
82
|
_phasor_transform,
|
158
83
|
_phasor_transform_const,
|
159
|
-
_polar_from_apparent_lifetime,
|
160
|
-
_polar_from_reference,
|
161
|
-
_polar_from_reference_phasor,
|
162
|
-
_polar_from_single_lifetime,
|
163
|
-
_polar_to_apparent_lifetime,
|
164
84
|
)
|
165
85
|
from ._utils import parse_harmonic, parse_signal_axis, parse_skip_axis
|
86
|
+
from .filter import phasor_threshold
|
166
87
|
from .utils import number_threads
|
167
88
|
|
168
89
|
|
@@ -213,7 +134,8 @@ def phasor_from_signal(
|
|
213
134
|
calculated, or `rfft` is specified.
|
214
135
|
rfft : callable, optional
|
215
136
|
Drop-in replacement function for ``numpy.fft.rfft``.
|
216
|
-
For example, ``scipy.fft.rfft`` or
|
137
|
+
For example, ``scipy.fft.rfft`` or
|
138
|
+
``mkl_fft.interfaces.numpy_fft.rfft``.
|
217
139
|
Used to calculate the real forward FFT.
|
218
140
|
dtype : dtype_like, optional
|
219
141
|
Data type of output arrays. Either float32 or float64.
|
@@ -257,13 +179,13 @@ def phasor_from_signal(
|
|
257
179
|
--------
|
258
180
|
phasorpy.phasor.phasor_to_signal
|
259
181
|
phasorpy.phasor.phasor_normalize
|
260
|
-
:ref:`
|
182
|
+
:ref:`sphx_glr_tutorials_misc_phasorpy_phasor_from_signal.py`
|
261
183
|
|
262
184
|
Notes
|
263
185
|
-----
|
264
186
|
The normalized phasor coordinates `real` (:math:`G`), `imag` (:math:`S`),
|
265
187
|
and average intensity `mean` (:math:`F_{DC}`) are calculated from
|
266
|
-
:math:`K\
|
188
|
+
:math:`K \ge 3` samples of the signal :math:`F` at `harmonic` :math:`h`
|
267
189
|
according to:
|
268
190
|
|
269
191
|
.. math::
|
@@ -277,8 +199,8 @@ def phasor_from_signal(
|
|
277
199
|
\sin{\left (2 \pi h \frac{k}{K} \right )} \cdot \frac{1}{F_{DC}}
|
278
200
|
|
279
201
|
If :math:`F_{DC} = 0`, the phasor coordinates are undefined
|
280
|
-
(
|
281
|
-
Use
|
202
|
+
(resulting in NaN or infinity).
|
203
|
+
Use NaN-aware software to further process the phasor coordinates.
|
282
204
|
|
283
205
|
The phasor coordinates may be zero, for example, in case of only constant
|
284
206
|
background in time-resolved signals, or as the result of linear
|
@@ -325,8 +247,8 @@ def phasor_from_signal(
|
|
325
247
|
raise ValueError('sample_phase cannot be used with FFT')
|
326
248
|
if num_harmonics > 1 or harmonic[0] != 1:
|
327
249
|
raise ValueError('sample_phase cannot be used with harmonic != 1')
|
328
|
-
sample_phase = numpy.
|
329
|
-
|
250
|
+
sample_phase = numpy.array(
|
251
|
+
sample_phase, dtype=numpy.float64, ndmin=1, order='C', copy=None
|
330
252
|
)
|
331
253
|
if sample_phase.ndim != 1 or sample_phase.size != samples:
|
332
254
|
raise ValueError(f'{sample_phase.shape=} != ({samples},)')
|
@@ -348,10 +270,10 @@ def phasor_from_signal(
|
|
348
270
|
|
349
271
|
mean = fft.take(0, axis=axis).real
|
350
272
|
if not mean.ndim == 0:
|
351
|
-
mean = numpy.ascontiguousarray(mean, dtype)
|
273
|
+
mean = numpy.ascontiguousarray(mean, dtype=dtype)
|
352
274
|
fft = fft.take(harmonic, axis=axis)
|
353
|
-
real = numpy.ascontiguousarray(fft.real, dtype)
|
354
|
-
imag = numpy.ascontiguousarray(fft.imag, dtype)
|
275
|
+
real = numpy.ascontiguousarray(fft.real, dtype=dtype)
|
276
|
+
imag = numpy.ascontiguousarray(fft.imag, dtype=dtype)
|
355
277
|
del fft
|
356
278
|
|
357
279
|
if not keepdims and real.shape[axis] == 1:
|
@@ -398,7 +320,7 @@ def phasor_from_signal(
|
|
398
320
|
shape1 = signal.shape[axis + 1 :]
|
399
321
|
size0 = math.prod(shape0)
|
400
322
|
size1 = math.prod(shape1)
|
401
|
-
phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype)
|
323
|
+
phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype=dtype)
|
402
324
|
signal = signal.reshape((size0, samples, size1))
|
403
325
|
|
404
326
|
_phasor_from_signal(phasor, signal, sincos, normalize, num_threads)
|
@@ -457,7 +379,8 @@ def phasor_to_signal(
|
|
457
379
|
The default is the last axis (-1).
|
458
380
|
irfft : callable, optional
|
459
381
|
Drop-in replacement function for ``numpy.fft.irfft``.
|
460
|
-
For example, ``scipy.fft.irfft`` or
|
382
|
+
For example, ``scipy.fft.irfft`` or
|
383
|
+
``mkl_fft.interfaces.numpy_fft.irfft``.
|
461
384
|
Used to calculate the real inverse FFT.
|
462
385
|
|
463
386
|
Returns
|
@@ -472,7 +395,7 @@ def phasor_to_signal(
|
|
472
395
|
Notes
|
473
396
|
-----
|
474
397
|
The reconstructed signal may be undefined if the input phasor coordinates,
|
475
|
-
or signal mean contain
|
398
|
+
or signal mean contain NaN values.
|
476
399
|
|
477
400
|
Examples
|
478
401
|
--------
|
@@ -515,8 +438,9 @@ def phasor_to_signal(
|
|
515
438
|
else:
|
516
439
|
keepdims = mean.ndim > 0 or real.ndim > 0
|
517
440
|
|
518
|
-
mean = numpy.
|
519
|
-
|
441
|
+
# mean, real = numpy.atleast_1d(mean, real) not working with Mypy
|
442
|
+
mean = numpy.array(mean, ndmin=1, copy=False)
|
443
|
+
real = numpy.array(real, ndmin=1, copy=False)
|
520
444
|
|
521
445
|
if real.dtype.kind != 'f' or imag.dtype.kind != 'f':
|
522
446
|
raise ValueError(f'{real.dtype=} or {imag.dtype=} not floating point')
|
@@ -563,307 +487,6 @@ def phasor_to_signal(
|
|
563
487
|
return signal
|
564
488
|
|
565
489
|
|
566
|
-
def lifetime_to_signal(
|
567
|
-
frequency: float,
|
568
|
-
lifetime: ArrayLike,
|
569
|
-
fraction: ArrayLike | None = None,
|
570
|
-
*,
|
571
|
-
mean: ArrayLike | None = None,
|
572
|
-
background: ArrayLike | None = None,
|
573
|
-
samples: int = 64,
|
574
|
-
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
575
|
-
zero_phase: float | None = None,
|
576
|
-
zero_stdev: float | None = None,
|
577
|
-
preexponential: bool = False,
|
578
|
-
unit_conversion: float = 1e-3,
|
579
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
580
|
-
r"""Return synthetic signal from lifetime components.
|
581
|
-
|
582
|
-
Return synthetic signal, instrument response function (IRF), and
|
583
|
-
time axis, sampled over one period of the fundamental frequency.
|
584
|
-
The signal is convoluted with the IRF, which is approximated by a
|
585
|
-
normal distribution.
|
586
|
-
|
587
|
-
Parameters
|
588
|
-
----------
|
589
|
-
frequency : float
|
590
|
-
Fundamental laser pulse or modulation frequency in MHz.
|
591
|
-
lifetime : array_like
|
592
|
-
Lifetime components in ns.
|
593
|
-
fraction : array_like, optional
|
594
|
-
Fractional intensities or pre-exponential amplitudes of the lifetime
|
595
|
-
components. Fractions are normalized to sum to 1.
|
596
|
-
Must be specified if `lifetime` is not a scalar.
|
597
|
-
mean : array_like, optional, default: 1.0
|
598
|
-
Average signal intensity (DC). Must be scalar for now.
|
599
|
-
background : array_like, optional, default: 0.0
|
600
|
-
Background signal intensity. Must be smaller than `mean`.
|
601
|
-
samples : int, default: 64
|
602
|
-
Number of signal samples to return. Must be at least 16.
|
603
|
-
harmonic : int, sequence of int, or 'all', optional, default: 'all'
|
604
|
-
Harmonics used to synthesize signal.
|
605
|
-
If `'all'`, all harmonics are used.
|
606
|
-
Else, harmonics must be at least one and no larger than half of
|
607
|
-
`samples`.
|
608
|
-
Use `'all'` to synthesize an exponential time-domain decay signal,
|
609
|
-
or `1` to synthesize a homodyne signal.
|
610
|
-
zero_phase : float, optional
|
611
|
-
Position of instrument response function in radians.
|
612
|
-
Must be in range [0, pi]. The default is the 8th sample.
|
613
|
-
zero_stdev : float, optional
|
614
|
-
Standard deviation of instrument response function in radians.
|
615
|
-
Must be at least 1.5 samples and no more than one tenth of samples
|
616
|
-
to allow for sufficient sampling of the function.
|
617
|
-
The default is 1.5 samples. Increase `samples` to narrow the IRF.
|
618
|
-
preexponential : bool, optional, default: False
|
619
|
-
If true, `fraction` values are pre-exponential amplitudes,
|
620
|
-
else fractional intensities.
|
621
|
-
unit_conversion : float, optional, default: 1e-3
|
622
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
623
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
624
|
-
Use 1.0 for Hz and s.
|
625
|
-
|
626
|
-
Returns
|
627
|
-
-------
|
628
|
-
signal : ndarray
|
629
|
-
Signal generated from lifetimes at frequency, convoluted with
|
630
|
-
instrument response function.
|
631
|
-
zero : ndarray
|
632
|
-
Instrument response function.
|
633
|
-
time : ndarray
|
634
|
-
Time for each sample in signal in units of `lifetime`.
|
635
|
-
|
636
|
-
See Also
|
637
|
-
--------
|
638
|
-
phasorpy.phasor.phasor_from_lifetime
|
639
|
-
phasorpy.phasor.phasor_to_signal
|
640
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_lifetime_to_signal.py`
|
641
|
-
|
642
|
-
Notes
|
643
|
-
-----
|
644
|
-
This implementation is based on an inverse digital Fourier transform (DFT).
|
645
|
-
Because DFT cannot be used on signals with discontinuities
|
646
|
-
(for example, an exponential decay starting at zero) without producing
|
647
|
-
strong artifacts (ripples), the signal is convoluted with a continuous
|
648
|
-
instrument response function (IRF). The minimum width of the IRF is
|
649
|
-
limited due to sampling requirements.
|
650
|
-
|
651
|
-
Examples
|
652
|
-
--------
|
653
|
-
Synthesize a multi-exponential time-domain decay signal for two
|
654
|
-
lifetime components of 4.2 and 0.9 ns at 40 MHz:
|
655
|
-
|
656
|
-
>>> signal, zero, times = lifetime_to_signal(
|
657
|
-
... 40, [4.2, 0.9], fraction=[0.8, 0.2], samples=16
|
658
|
-
... )
|
659
|
-
>>> signal # doctest: +NUMBER
|
660
|
-
array([0.2846, 0.1961, 0.1354, ..., 0.8874, 0.6029, 0.4135])
|
661
|
-
|
662
|
-
Synthesize a homodyne frequency-domain waveform signal for
|
663
|
-
a single lifetime:
|
664
|
-
|
665
|
-
>>> signal, zero, times = lifetime_to_signal(
|
666
|
-
... 40.0, 4.2, samples=16, harmonic=1
|
667
|
-
... )
|
668
|
-
>>> signal # doctest: +NUMBER
|
669
|
-
array([0.2047, -0.05602, -0.156, ..., 1.471, 1.031, 0.5865])
|
670
|
-
|
671
|
-
"""
|
672
|
-
if harmonic is None:
|
673
|
-
harmonic = 'all'
|
674
|
-
all_hamonics = harmonic == 'all'
|
675
|
-
harmonic, _ = parse_harmonic(harmonic, samples // 2)
|
676
|
-
|
677
|
-
if samples < 16:
|
678
|
-
raise ValueError(f'{samples=} < 16')
|
679
|
-
|
680
|
-
if background is None:
|
681
|
-
background = 0.0
|
682
|
-
background = numpy.asarray(background)
|
683
|
-
|
684
|
-
if mean is None:
|
685
|
-
mean = 1.0
|
686
|
-
mean = numpy.asarray(mean)
|
687
|
-
mean -= background
|
688
|
-
if numpy.any(mean < 0.0):
|
689
|
-
raise ValueError('mean - background must not be less than zero')
|
690
|
-
|
691
|
-
scale = samples / (2.0 * math.pi)
|
692
|
-
if zero_phase is None:
|
693
|
-
zero_phase = 8.0 / scale
|
694
|
-
phase = zero_phase * scale # in sample units
|
695
|
-
if zero_stdev is None:
|
696
|
-
zero_stdev = 1.5 / scale
|
697
|
-
stdev = zero_stdev * scale # in sample units
|
698
|
-
|
699
|
-
if zero_phase < 0 or zero_phase > 2.0 * math.pi:
|
700
|
-
raise ValueError(f'{zero_phase=} out of range [0, 2 pi]')
|
701
|
-
if stdev < 1.5:
|
702
|
-
raise ValueError(
|
703
|
-
f'{zero_stdev=} < {1.5 / scale} cannot be sampled sufficiently'
|
704
|
-
)
|
705
|
-
if stdev >= samples / 10:
|
706
|
-
raise ValueError(f'{zero_stdev=} > pi / 5 not supported')
|
707
|
-
|
708
|
-
frequencies = numpy.atleast_1d(frequency)
|
709
|
-
if frequencies.size > 1 or frequencies[0] <= 0.0:
|
710
|
-
raise ValueError('frequency must be scalar and positive')
|
711
|
-
frequencies = numpy.linspace(
|
712
|
-
frequency, samples // 2 * frequency, samples // 2
|
713
|
-
)
|
714
|
-
frequencies = frequencies[[h - 1 for h in harmonic]]
|
715
|
-
|
716
|
-
real, imag = phasor_from_lifetime(
|
717
|
-
frequencies,
|
718
|
-
lifetime,
|
719
|
-
fraction,
|
720
|
-
preexponential=preexponential,
|
721
|
-
unit_conversion=unit_conversion,
|
722
|
-
)
|
723
|
-
real, imag = numpy.atleast_1d(real, imag)
|
724
|
-
|
725
|
-
zero = numpy.zeros(samples, dtype=numpy.float64)
|
726
|
-
_gaussian_signal(zero, phase, stdev)
|
727
|
-
zero_mean, zero_real, zero_imag = phasor_from_signal(
|
728
|
-
zero, harmonic=harmonic
|
729
|
-
)
|
730
|
-
if real.ndim > 1:
|
731
|
-
# make broadcastable with real and imag
|
732
|
-
zero_real = zero_real[:, None]
|
733
|
-
zero_imag = zero_imag[:, None]
|
734
|
-
if not all_hamonics:
|
735
|
-
zero = phasor_to_signal(
|
736
|
-
zero_mean, zero_real, zero_imag, samples=samples, harmonic=harmonic
|
737
|
-
)
|
738
|
-
|
739
|
-
phasor_multiply(real, imag, zero_real, zero_imag, out=(real, imag))
|
740
|
-
|
741
|
-
if len(harmonic) == 1:
|
742
|
-
harmonic = harmonic[0]
|
743
|
-
signal = phasor_to_signal(
|
744
|
-
mean, real, imag, samples=samples, harmonic=harmonic
|
745
|
-
)
|
746
|
-
signal += numpy.asarray(background)
|
747
|
-
|
748
|
-
time = numpy.linspace(0, 1.0 / (unit_conversion * frequency), samples)
|
749
|
-
|
750
|
-
return signal.squeeze(), zero.squeeze(), time
|
751
|
-
|
752
|
-
|
753
|
-
def phasor_semicircle(
|
754
|
-
samples: int = 101, /
|
755
|
-
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
756
|
-
r"""Return equally spaced phasor coordinates on universal semicircle.
|
757
|
-
|
758
|
-
Parameters
|
759
|
-
----------
|
760
|
-
samples : int, optional, default: 101
|
761
|
-
Number of coordinates to return.
|
762
|
-
|
763
|
-
Returns
|
764
|
-
-------
|
765
|
-
real : ndarray
|
766
|
-
Real component of phasor coordinates on universal semicircle.
|
767
|
-
imag : ndarray
|
768
|
-
Imaginary component of phasor coordinates on universal semicircle.
|
769
|
-
|
770
|
-
Raises
|
771
|
-
------
|
772
|
-
ValueError
|
773
|
-
The number of `samples` is smaller than 1.
|
774
|
-
|
775
|
-
Notes
|
776
|
-
-----
|
777
|
-
If more than one sample, the first and last phasor coordinates returned
|
778
|
-
are ``(0, 0)`` and ``(1, 0)``.
|
779
|
-
The center coordinate, if any, is ``(0.5, 0.5)``.
|
780
|
-
|
781
|
-
The universal semicircle is composed of the phasor coordinates of
|
782
|
-
single lifetime components, where the relation of polar coordinates
|
783
|
-
(phase :math:`\phi` and modulation :math:`M`) is:
|
784
|
-
|
785
|
-
.. math::
|
786
|
-
|
787
|
-
M = \cos{\phi}
|
788
|
-
|
789
|
-
Examples
|
790
|
-
--------
|
791
|
-
Calculate three phasor coordinates on universal semicircle:
|
792
|
-
|
793
|
-
>>> phasor_semicircle(3) # doctest: +NUMBER
|
794
|
-
(array([0, 0.5, 1]), array([0.0, 0.5, 0]))
|
795
|
-
|
796
|
-
"""
|
797
|
-
if samples < 1:
|
798
|
-
raise ValueError(f'{samples=} < 1')
|
799
|
-
arange = numpy.linspace(math.pi, 0.0, samples)
|
800
|
-
real = numpy.cos(arange)
|
801
|
-
real += 1.0
|
802
|
-
real *= 0.5
|
803
|
-
imag = numpy.sin(arange)
|
804
|
-
imag *= 0.5
|
805
|
-
return real, imag
|
806
|
-
|
807
|
-
|
808
|
-
def phasor_semicircle_intersect(
|
809
|
-
real0: ArrayLike,
|
810
|
-
imag0: ArrayLike,
|
811
|
-
real1: ArrayLike,
|
812
|
-
imag1: ArrayLike,
|
813
|
-
/,
|
814
|
-
**kwargs: Any,
|
815
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], NDArray[Any]]:
|
816
|
-
"""Return intersection of line through phasors with universal semicircle.
|
817
|
-
|
818
|
-
Return the phasor coordinates of two intersections of the universal
|
819
|
-
semicircle with the line between two phasor coordinates.
|
820
|
-
Return NaN if the line does not intersect the semicircle.
|
821
|
-
|
822
|
-
Parameters
|
823
|
-
----------
|
824
|
-
real0 : array_like
|
825
|
-
Real component of first set of phasor coordinates.
|
826
|
-
imag0 : array_like
|
827
|
-
Imaginary component of first set of phasor coordinates.
|
828
|
-
real1 : array_like
|
829
|
-
Real component of second set of phasor coordinates.
|
830
|
-
imag1 : array_like
|
831
|
-
Imaginary component of second set of phasor coordinates.
|
832
|
-
**kwargs
|
833
|
-
Optional `arguments passed to numpy universal functions
|
834
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
835
|
-
|
836
|
-
Returns
|
837
|
-
-------
|
838
|
-
real0 : ndarray
|
839
|
-
Real component of first intersect of phasors with semicircle.
|
840
|
-
imag0 : ndarray
|
841
|
-
Imaginary component of first intersect of phasors with semicircle.
|
842
|
-
real1 : ndarray
|
843
|
-
Real component of second intersect of phasors with semicircle.
|
844
|
-
imag1 : ndarray
|
845
|
-
Imaginary component of second intersect of phasors with semicircle.
|
846
|
-
|
847
|
-
Examples
|
848
|
-
--------
|
849
|
-
Calculate two intersects of a line through two phasor coordinates
|
850
|
-
with the universal semicircle:
|
851
|
-
|
852
|
-
>>> phasor_semicircle_intersect(0.2, 0.25, 0.6, 0.25) # doctest: +NUMBER
|
853
|
-
(0.066, 0.25, 0.933, 0.25)
|
854
|
-
|
855
|
-
The line between two phasor coordinates may not intersect the semicircle
|
856
|
-
at two points:
|
857
|
-
|
858
|
-
>>> phasor_semicircle_intersect(0.2, 0.0, 0.6, 0.25) # doctest: +NUMBER
|
859
|
-
(nan, nan, 0.817, 0.386)
|
860
|
-
|
861
|
-
"""
|
862
|
-
return _intersect_semicircle_line( # type: ignore[no-any-return]
|
863
|
-
real0, imag0, real1, imag1, **kwargs
|
864
|
-
)
|
865
|
-
|
866
|
-
|
867
490
|
def phasor_to_complex(
|
868
491
|
real: ArrayLike,
|
869
492
|
imag: ArrayLike,
|
@@ -909,7 +532,7 @@ def phasor_to_complex(
|
|
909
532
|
if dtype.kind != 'c':
|
910
533
|
raise ValueError(f'{dtype=} not a complex type')
|
911
534
|
|
912
|
-
c = numpy.empty(numpy.broadcast(real, imag).shape, dtype)
|
535
|
+
c = numpy.empty(numpy.broadcast(real, imag).shape, dtype=dtype)
|
913
536
|
c.real = real
|
914
537
|
c.imag = imag
|
915
538
|
return c
|
@@ -1096,7 +719,7 @@ def phasor_normalize(
|
|
1096
719
|
S &= S' / F
|
1097
720
|
|
1098
721
|
If :math:`F = 0`, the normalized phasor coordinates (:math:`G`)
|
1099
|
-
and (:math:`S`) are undefined (
|
722
|
+
and (:math:`S`) are undefined (NaN or infinity).
|
1100
723
|
|
1101
724
|
Examples
|
1102
725
|
--------
|
@@ -1121,9 +744,9 @@ def phasor_normalize(
|
|
1121
744
|
):
|
1122
745
|
real = real_unnormalized.copy()
|
1123
746
|
else:
|
1124
|
-
real = numpy.
|
1125
|
-
imag = numpy.
|
1126
|
-
mean = numpy.
|
747
|
+
real = numpy.asarray(real_unnormalized, dtype=dtype, copy=True)
|
748
|
+
imag = numpy.asarray(imag_unnormalized, dtype=real.dtype, copy=True)
|
749
|
+
mean = numpy.asarray(mean_unnormalized, dtype=real.dtype, copy=True)
|
1127
750
|
|
1128
751
|
with numpy.errstate(divide='ignore', invalid='ignore'):
|
1129
752
|
numpy.divide(real, mean, out=real)
|
@@ -1134,282 +757,102 @@ def phasor_normalize(
|
|
1134
757
|
return mean, real, imag
|
1135
758
|
|
1136
759
|
|
1137
|
-
def
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
fraction: ArrayLike | None = None,
|
1150
|
-
preexponential: bool = False,
|
1151
|
-
unit_conversion: float = 1e-3,
|
1152
|
-
method: Literal['mean', 'median'] = 'mean',
|
1153
|
-
nan_safe: bool = True,
|
1154
|
-
reverse: bool = False,
|
1155
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
1156
|
-
"""Return calibrated/referenced phasor coordinates.
|
760
|
+
def phasor_combine(
|
761
|
+
int0: ArrayLike,
|
762
|
+
real0: ArrayLike,
|
763
|
+
imag0: ArrayLike,
|
764
|
+
int1: ArrayLike,
|
765
|
+
real1: ArrayLike,
|
766
|
+
imag1: ArrayLike,
|
767
|
+
fraction0: ArrayLike,
|
768
|
+
fraction1: ArrayLike | None = None,
|
769
|
+
**kwargs: Any,
|
770
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
771
|
+
r"""Return linear combination of two phasor coordinates.
|
1157
772
|
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
773
|
+
Combine two sets of phasor coordinates using intensity-weighted mixing.
|
774
|
+
This simulates the phasor coordinates that would result from a mixture
|
775
|
+
of two components with known individual phasor coordinates.
|
1161
776
|
|
1162
777
|
Parameters
|
1163
778
|
----------
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
Fundamental laser pulse or modulation frequency in MHz.
|
1182
|
-
lifetime : array_like
|
1183
|
-
Lifetime components in ns. Must be scalar or one-dimensional.
|
1184
|
-
harmonic : int, sequence of int, or 'all', default: 1
|
1185
|
-
Harmonics included in `real` and `imag`.
|
1186
|
-
If an integer, the harmonics at which `real` and `imag` were acquired
|
1187
|
-
or calculated.
|
1188
|
-
If a sequence, the harmonics included in the first axis of `real` and
|
1189
|
-
`imag`.
|
1190
|
-
If `'all'`, the first axis of `real` and `imag` contains lower
|
1191
|
-
harmonics.
|
1192
|
-
The default is the first harmonic (fundamental frequency).
|
1193
|
-
skip_axis : int or sequence of int, optional
|
1194
|
-
Axes in `reference_mean` to exclude from reference center calculation.
|
1195
|
-
By default, all axes except harmonics are included.
|
1196
|
-
fraction : array_like, optional
|
1197
|
-
Fractional intensities or pre-exponential amplitudes of the lifetime
|
1198
|
-
components. Fractions are normalized to sum to 1.
|
1199
|
-
Must be same size as `lifetime`.
|
1200
|
-
preexponential : bool, optional
|
1201
|
-
If true, `fraction` values are pre-exponential amplitudes,
|
1202
|
-
else fractional intensities (default).
|
1203
|
-
unit_conversion : float, optional
|
1204
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
1205
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1206
|
-
Use 1.0 for Hz and s.
|
1207
|
-
method : str, optional
|
1208
|
-
Method used for calculating center of reference phasor coordinates:
|
1209
|
-
|
1210
|
-
- ``'mean'``: Arithmetic mean.
|
1211
|
-
- ``'median'``: Spatial median.
|
779
|
+
int0 : array_like
|
780
|
+
Intensity of first phasor coordinates.
|
781
|
+
real0 : array_like
|
782
|
+
Real component of first phasor coordinates.
|
783
|
+
imag0 : array_like
|
784
|
+
Imaginary component of first phasor coordinates.
|
785
|
+
int1 : array_like
|
786
|
+
Intensity of second phasor coordinates.
|
787
|
+
real1 : array_like
|
788
|
+
Real component of second phasor coordinates.
|
789
|
+
imag1 : array_like
|
790
|
+
Imaginary component of second phasor coordinates.
|
791
|
+
fraction0 : array_like
|
792
|
+
Fraction of first phasor coordinates.
|
793
|
+
fraction1 : array_like, optional
|
794
|
+
Fraction of second phasor coordinates.
|
795
|
+
The default is `1 - fraction0`.
|
1212
796
|
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
`method`.
|
1217
|
-
reverse : bool, optional
|
1218
|
-
Reverse calibration.
|
797
|
+
**kwargs
|
798
|
+
Optional `arguments passed to numpy universal functions
|
799
|
+
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1219
800
|
|
1220
801
|
Returns
|
1221
802
|
-------
|
803
|
+
intensity : ndarray
|
804
|
+
Intensity of the linearly combined phasor coordinates.
|
1222
805
|
real : ndarray
|
1223
|
-
|
806
|
+
Real component of the linearly combined phasor coordinates.
|
1224
807
|
imag : ndarray
|
1225
|
-
|
1226
|
-
|
1227
|
-
Raises
|
1228
|
-
------
|
1229
|
-
ValueError
|
1230
|
-
The array shapes of `real` and `imag`, or `reference_real` and
|
1231
|
-
`reference_imag` do not match.
|
1232
|
-
Number of harmonics or frequencies does not match the first axis
|
1233
|
-
of `real` and `imag`.
|
1234
|
-
|
1235
|
-
See Also
|
1236
|
-
--------
|
1237
|
-
phasorpy.phasor.phasor_transform
|
1238
|
-
phasorpy.phasor.polar_from_reference_phasor
|
1239
|
-
phasorpy.phasor.phasor_center
|
1240
|
-
phasorpy.phasor.phasor_from_lifetime
|
808
|
+
Imaginary component of the linearly combined phasor coordinates.
|
1241
809
|
|
1242
810
|
Notes
|
1243
811
|
-----
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
real,
|
1250
|
-
imag,
|
1251
|
-
*polar_from_reference_phasor(
|
1252
|
-
*phasor_center(
|
1253
|
-
reference_mean,
|
1254
|
-
reference_real,
|
1255
|
-
reference_imag,
|
1256
|
-
skip_axis,
|
1257
|
-
method,
|
1258
|
-
nan_safe,
|
1259
|
-
)[1:],
|
1260
|
-
*phasor_from_lifetime(
|
1261
|
-
frequency,
|
1262
|
-
lifetime,
|
1263
|
-
fraction,
|
1264
|
-
preexponential,
|
1265
|
-
unit_conversion,
|
1266
|
-
),
|
1267
|
-
),
|
1268
|
-
)
|
812
|
+
The phasor coordinates (:math:`I`, :math:`G`, :math:`S`) of the linear
|
813
|
+
combination of two phasor coordinates (:math:`I_{0}`, :math:`G_{0}`,
|
814
|
+
:math:`S_{0}`, and :math:`I_{1}`, :math:`G_{1}`, :math:`S_{1}`)
|
815
|
+
with fractions :math:`f_{0}` and :math:`f_{1}` of the first and second
|
816
|
+
coordinates are:
|
1269
817
|
|
1270
|
-
|
1271
|
-
|
1272
|
-
.. code-block:: python
|
818
|
+
.. math::
|
1273
819
|
|
1274
|
-
|
1275
|
-
*phasor_calibrate(real, imag, *args, **kwargs),
|
1276
|
-
*args,
|
1277
|
-
reverse=True,
|
1278
|
-
**kwargs
|
1279
|
-
)
|
820
|
+
f'_{0} &= f_{0} / (f_{0} + f_{1})
|
1280
821
|
|
1281
|
-
|
1282
|
-
--------
|
1283
|
-
>>> phasor_calibrate(
|
1284
|
-
... [0.1, 0.2, 0.3],
|
1285
|
-
... [0.4, 0.5, 0.6],
|
1286
|
-
... [1.0, 1.0, 1.0],
|
1287
|
-
... [0.2, 0.3, 0.4],
|
1288
|
-
... [0.5, 0.6, 0.7],
|
1289
|
-
... frequency=80,
|
1290
|
-
... lifetime=4,
|
1291
|
-
... ) # doctest: +NUMBER
|
1292
|
-
(array([0.0658, 0.132, 0.198]), array([0.2657, 0.332, 0.399]))
|
1293
|
-
|
1294
|
-
Undo the previous calibration:
|
1295
|
-
|
1296
|
-
>>> phasor_calibrate(
|
1297
|
-
... [0.0658, 0.132, 0.198],
|
1298
|
-
... [0.2657, 0.332, 0.399],
|
1299
|
-
... [1.0, 1.0, 1.0],
|
1300
|
-
... [0.2, 0.3, 0.4],
|
1301
|
-
... [0.5, 0.6, 0.7],
|
1302
|
-
... frequency=80,
|
1303
|
-
... lifetime=4,
|
1304
|
-
... reverse=True,
|
1305
|
-
... ) # doctest: +NUMBER
|
1306
|
-
(array([0.1, 0.2, 0.3]), array([0.4, 0.5, 0.6]))
|
822
|
+
f'_{1} &= 1 - f'_{0}
|
1307
823
|
|
1308
|
-
|
1309
|
-
real = numpy.asarray(real)
|
1310
|
-
imag = numpy.asarray(imag)
|
1311
|
-
reference_mean = numpy.asarray(reference_mean)
|
1312
|
-
reference_real = numpy.asarray(reference_real)
|
1313
|
-
reference_imag = numpy.asarray(reference_imag)
|
824
|
+
I &= I_{0} \cdot f'_{0} + I_{1} \cdot f'_{1}
|
1314
825
|
|
1315
|
-
|
1316
|
-
|
1317
|
-
if reference_real.shape != reference_imag.shape:
|
1318
|
-
raise ValueError(f'{reference_real.shape=} != {reference_imag.shape=}')
|
1319
|
-
|
1320
|
-
has_harmonic_axis = reference_mean.ndim + 1 == reference_real.ndim
|
1321
|
-
harmonic, _ = parse_harmonic(
|
1322
|
-
harmonic,
|
1323
|
-
(
|
1324
|
-
reference_real.shape[0]
|
1325
|
-
if has_harmonic_axis
|
1326
|
-
and isinstance(harmonic, str)
|
1327
|
-
and harmonic == 'all'
|
1328
|
-
else None
|
1329
|
-
),
|
1330
|
-
)
|
826
|
+
G &= (G_{0} \cdot I_{0} \cdot f'_{0}
|
827
|
+
+ G_{1} \cdot I_{1} \cdot f'_{1}) / I
|
1331
828
|
|
1332
|
-
|
1333
|
-
|
829
|
+
S &= (S_{0} \cdot I_{0} \cdot f'_{0}
|
830
|
+
+ S_{1} \cdot I_{1} \cdot f'_{1}) / I
|
1334
831
|
|
1335
|
-
|
1336
|
-
|
1337
|
-
raise ValueError(
|
1338
|
-
f'{real.shape=} != {len(frequency)} frequencies or harmonics'
|
1339
|
-
)
|
1340
|
-
if real.shape[0] != len(frequency):
|
1341
|
-
raise ValueError(
|
1342
|
-
f'{real.shape[0]=} != {len(frequency)} '
|
1343
|
-
'frequencies or harmonics'
|
1344
|
-
)
|
1345
|
-
if reference_real.shape[0] != len(frequency):
|
1346
|
-
raise ValueError(
|
1347
|
-
f'{reference_real.shape[0]=} != {len(frequency)} '
|
1348
|
-
'frequencies or harmonics'
|
1349
|
-
)
|
1350
|
-
if reference_mean.shape != reference_real.shape[1:]:
|
1351
|
-
raise ValueError(
|
1352
|
-
f'{reference_mean.shape=} != {reference_real.shape[1:]=}'
|
1353
|
-
)
|
1354
|
-
elif reference_mean.shape != reference_real.shape:
|
1355
|
-
raise ValueError(f'{reference_mean.shape=} != {reference_real.shape=}')
|
1356
|
-
elif len(harmonic) > 1:
|
1357
|
-
raise ValueError(
|
1358
|
-
f'{reference_mean.shape=} does not have harmonic axis'
|
1359
|
-
)
|
832
|
+
If the intensity :math:`I` is zero, the linear combined phasor coordinates
|
833
|
+
are undefined (NaN).
|
1360
834
|
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
reference_imag,
|
1365
|
-
skip_axis=skip_axis,
|
1366
|
-
method=method,
|
1367
|
-
nan_safe=nan_safe,
|
1368
|
-
)
|
835
|
+
See Also
|
836
|
+
--------
|
837
|
+
phasorpy.component.phasor_from_component
|
1369
838
|
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
preexponential=preexponential,
|
1375
|
-
unit_conversion=unit_conversion,
|
1376
|
-
)
|
839
|
+
Examples
|
840
|
+
--------
|
841
|
+
Calculate the linear combination of two phasor coordinates with equal
|
842
|
+
intensities at three fractional intensities:
|
1377
843
|
|
1378
|
-
|
1379
|
-
|
1380
|
-
)
|
844
|
+
>>> phasor_combine(1.0, 0.6, 0.3, 1.0, 0.4, 0.2, [1.0, 0.2, 0.9])
|
845
|
+
(array([1, 1, 1]), array([0.6, 0.44, 0.58]), array([0.3, 0.22, 0.29]))
|
1381
846
|
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
)
|
1386
|
-
known_re = numpy.broadcast_to(
|
1387
|
-
known_re, (len(frequency), *measured_re.shape[1:])
|
1388
|
-
)
|
1389
|
-
known_im = numpy.expand_dims(
|
1390
|
-
known_im, tuple(range(1, measured_im.ndim))
|
1391
|
-
)
|
1392
|
-
known_im = numpy.broadcast_to(
|
1393
|
-
known_im, (len(frequency), *measured_im.shape[1:])
|
1394
|
-
)
|
847
|
+
"""
|
848
|
+
if fraction1 is None:
|
849
|
+
fraction1 = numpy.asarray(fraction0, copy=True)
|
850
|
+
numpy.subtract(1.0, fraction0, out=fraction1)
|
1395
851
|
|
1396
|
-
|
1397
|
-
|
852
|
+
return _phasor_combine( # type: ignore[no-any-return]
|
853
|
+
int0, real0, imag0, int1, real1, imag1, fraction0, fraction1, **kwargs
|
1398
854
|
)
|
1399
855
|
|
1400
|
-
if numpy.ndim(phi_zero) > 0:
|
1401
|
-
if reverse:
|
1402
|
-
numpy.negative(phi_zero, out=phi_zero)
|
1403
|
-
numpy.reciprocal(mod_zero, out=mod_zero)
|
1404
|
-
if axis is not None:
|
1405
|
-
phi_zero = numpy.expand_dims(phi_zero, axis=axis)
|
1406
|
-
mod_zero = numpy.expand_dims(mod_zero, axis=axis)
|
1407
|
-
elif reverse:
|
1408
|
-
phi_zero = -phi_zero
|
1409
|
-
mod_zero = 1.0 / mod_zero
|
1410
|
-
|
1411
|
-
return phasor_transform(real, imag, phi_zero, mod_zero)
|
1412
|
-
|
1413
856
|
|
1414
857
|
def phasor_transform(
|
1415
858
|
real: ArrayLike,
|
@@ -1492,121 +935,6 @@ def phasor_transform(
|
|
1492
935
|
)
|
1493
936
|
|
1494
937
|
|
1495
|
-
def polar_from_reference_phasor(
|
1496
|
-
measured_real: ArrayLike,
|
1497
|
-
measured_imag: ArrayLike,
|
1498
|
-
known_real: ArrayLike,
|
1499
|
-
known_imag: ArrayLike,
|
1500
|
-
/,
|
1501
|
-
**kwargs: Any,
|
1502
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
1503
|
-
r"""Return polar coordinates for calibration from reference phasor.
|
1504
|
-
|
1505
|
-
Return rotation angle and scale factor for calibrating phasor coordinates
|
1506
|
-
from measured and known phasor coordinates of a reference, for example,
|
1507
|
-
a sample of known lifetime.
|
1508
|
-
|
1509
|
-
Parameters
|
1510
|
-
----------
|
1511
|
-
measured_real : array_like
|
1512
|
-
Real component of measured phasor coordinates.
|
1513
|
-
measured_imag : array_like
|
1514
|
-
Imaginary component of measured phasor coordinates.
|
1515
|
-
known_real : array_like
|
1516
|
-
Real component of reference phasor coordinates.
|
1517
|
-
known_imag : array_like
|
1518
|
-
Imaginary component of reference phasor coordinates.
|
1519
|
-
**kwargs
|
1520
|
-
Optional `arguments passed to numpy universal functions
|
1521
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1522
|
-
|
1523
|
-
Returns
|
1524
|
-
-------
|
1525
|
-
phase_zero : ndarray
|
1526
|
-
Angular component of polar coordinates for calibration in radians.
|
1527
|
-
modulation_zero : ndarray
|
1528
|
-
Radial component of polar coordinates for calibration.
|
1529
|
-
|
1530
|
-
See Also
|
1531
|
-
--------
|
1532
|
-
phasorpy.phasor.polar_from_reference
|
1533
|
-
|
1534
|
-
Notes
|
1535
|
-
-----
|
1536
|
-
This function performs the following operations:
|
1537
|
-
|
1538
|
-
.. code-block:: python
|
1539
|
-
|
1540
|
-
polar_from_reference(
|
1541
|
-
*phasor_to_polar(measured_real, measured_imag),
|
1542
|
-
*phasor_to_polar(known_real, known_imag),
|
1543
|
-
)
|
1544
|
-
|
1545
|
-
Examples
|
1546
|
-
--------
|
1547
|
-
>>> polar_from_reference_phasor(0.5, 0.0, 1.0, 0.0)
|
1548
|
-
(0.0, 2.0)
|
1549
|
-
|
1550
|
-
"""
|
1551
|
-
return _polar_from_reference_phasor( # type: ignore[no-any-return]
|
1552
|
-
measured_real, measured_imag, known_real, known_imag, **kwargs
|
1553
|
-
)
|
1554
|
-
|
1555
|
-
|
1556
|
-
def polar_from_reference(
|
1557
|
-
measured_phase: ArrayLike,
|
1558
|
-
measured_modulation: ArrayLike,
|
1559
|
-
known_phase: ArrayLike,
|
1560
|
-
known_modulation: ArrayLike,
|
1561
|
-
/,
|
1562
|
-
**kwargs: Any,
|
1563
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
1564
|
-
r"""Return polar coordinates for calibration from reference coordinates.
|
1565
|
-
|
1566
|
-
Return rotation angle and scale factor for calibrating phasor coordinates
|
1567
|
-
from measured and known polar coordinates of a reference, for example,
|
1568
|
-
a sample of known lifetime.
|
1569
|
-
|
1570
|
-
Parameters
|
1571
|
-
----------
|
1572
|
-
measured_phase : array_like
|
1573
|
-
Angular component of measured polar coordinates in radians.
|
1574
|
-
measured_modulation : array_like
|
1575
|
-
Radial component of measured polar coordinates.
|
1576
|
-
known_phase : array_like
|
1577
|
-
Angular component of reference polar coordinates in radians.
|
1578
|
-
known_modulation : array_like
|
1579
|
-
Radial component of reference polar coordinates.
|
1580
|
-
**kwargs
|
1581
|
-
Optional `arguments passed to numpy universal functions
|
1582
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1583
|
-
|
1584
|
-
Returns
|
1585
|
-
-------
|
1586
|
-
phase_zero : ndarray
|
1587
|
-
Angular component of polar coordinates for calibration in radians.
|
1588
|
-
modulation_zero : ndarray
|
1589
|
-
Radial component of polar coordinates for calibration.
|
1590
|
-
|
1591
|
-
See Also
|
1592
|
-
--------
|
1593
|
-
phasorpy.phasor.polar_from_reference_phasor
|
1594
|
-
|
1595
|
-
Examples
|
1596
|
-
--------
|
1597
|
-
>>> polar_from_reference(0.2, 0.4, 0.4, 1.3)
|
1598
|
-
(0.2, 3.25)
|
1599
|
-
|
1600
|
-
"""
|
1601
|
-
return _polar_from_reference( # type: ignore[no-any-return]
|
1602
|
-
measured_phase,
|
1603
|
-
measured_modulation,
|
1604
|
-
known_phase,
|
1605
|
-
known_modulation,
|
1606
|
-
**kwargs,
|
1607
|
-
)
|
1608
|
-
|
1609
|
-
|
1610
938
|
def phasor_to_polar(
|
1611
939
|
real: ArrayLike,
|
1612
940
|
imag: ArrayLike,
|
@@ -1718,1117 +1046,7 @@ def phasor_from_polar(
|
|
1718
1046
|
)
|
1719
1047
|
|
1720
1048
|
|
1721
|
-
def
|
1722
|
-
real: ArrayLike,
|
1723
|
-
imag: ArrayLike,
|
1724
|
-
/,
|
1725
|
-
frequency: ArrayLike,
|
1726
|
-
*,
|
1727
|
-
unit_conversion: float = 1e-3,
|
1728
|
-
**kwargs: Any,
|
1729
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
1730
|
-
r"""Return apparent single lifetimes from phasor coordinates.
|
1731
|
-
|
1732
|
-
Parameters
|
1733
|
-
----------
|
1734
|
-
real : array_like
|
1735
|
-
Real component of phasor coordinates.
|
1736
|
-
imag : array_like
|
1737
|
-
Imaginary component of phasor coordinates.
|
1738
|
-
frequency : array_like
|
1739
|
-
Laser pulse or modulation frequency in MHz.
|
1740
|
-
unit_conversion : float, optional
|
1741
|
-
Product of `frequency` and returned `lifetime` units' prefix factors.
|
1742
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1743
|
-
Use 1.0 for Hz and s.
|
1744
|
-
**kwargs
|
1745
|
-
Optional `arguments passed to numpy universal functions
|
1746
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1747
|
-
|
1748
|
-
Returns
|
1749
|
-
-------
|
1750
|
-
phase_lifetime : ndarray
|
1751
|
-
Apparent single lifetime from angular component of phasor coordinates.
|
1752
|
-
modulation_lifetime : ndarray
|
1753
|
-
Apparent single lifetime from radial component of phasor coordinates.
|
1754
|
-
|
1755
|
-
See Also
|
1756
|
-
--------
|
1757
|
-
phasorpy.phasor.phasor_from_apparent_lifetime
|
1758
|
-
:ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
|
1759
|
-
|
1760
|
-
Notes
|
1761
|
-
-----
|
1762
|
-
The phasor coordinates `real` (:math:`G`) and `imag` (:math:`S`)
|
1763
|
-
are converted to apparent single lifetimes
|
1764
|
-
`phase_lifetime` (:math:`\tau_{\phi}`) and
|
1765
|
-
`modulation_lifetime` (:math:`\tau_{M}`) at frequency :math:`f`
|
1766
|
-
according to:
|
1767
|
-
|
1768
|
-
.. math::
|
1769
|
-
|
1770
|
-
\omega &= 2 \pi f
|
1771
|
-
|
1772
|
-
\tau_{\phi} &= \omega^{-1} \cdot S / G
|
1773
|
-
|
1774
|
-
\tau_{M} &= \omega^{-1} \cdot \sqrt{1 / (S^2 + G^2) - 1}
|
1775
|
-
|
1776
|
-
Examples
|
1777
|
-
--------
|
1778
|
-
The apparent single lifetimes from phase and modulation are equal
|
1779
|
-
only if the phasor coordinates lie on the universal semicircle:
|
1780
|
-
|
1781
|
-
>>> phasor_to_apparent_lifetime(
|
1782
|
-
... 0.5, [0.5, 0.45], frequency=80
|
1783
|
-
... ) # doctest: +NUMBER
|
1784
|
-
(array([1.989, 1.79]), array([1.989, 2.188]))
|
1785
|
-
|
1786
|
-
Apparent single lifetimes of phasor coordinates outside the universal
|
1787
|
-
semicircle are undefined:
|
1788
|
-
|
1789
|
-
>>> phasor_to_apparent_lifetime(-0.1, 1.1, 80) # doctest: +NUMBER
|
1790
|
-
(-21.8, 0.0)
|
1791
|
-
|
1792
|
-
Apparent single lifetimes at the universal semicircle endpoints are
|
1793
|
-
infinite and zero:
|
1794
|
-
|
1795
|
-
>>> phasor_to_apparent_lifetime([0, 1], [0, 0], 80) # doctest: +NUMBER
|
1796
|
-
(array([inf, 0]), array([inf, 0]))
|
1797
|
-
|
1798
|
-
"""
|
1799
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
1800
|
-
omega *= math.pi * 2.0 * unit_conversion
|
1801
|
-
return _phasor_to_apparent_lifetime( # type: ignore[no-any-return]
|
1802
|
-
real, imag, omega, **kwargs
|
1803
|
-
)
|
1804
|
-
|
1805
|
-
|
1806
|
-
def phasor_from_apparent_lifetime(
|
1807
|
-
phase_lifetime: ArrayLike,
|
1808
|
-
modulation_lifetime: ArrayLike | None,
|
1809
|
-
/,
|
1810
|
-
frequency: ArrayLike,
|
1811
|
-
*,
|
1812
|
-
unit_conversion: float = 1e-3,
|
1813
|
-
**kwargs: Any,
|
1814
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
1815
|
-
r"""Return phasor coordinates from apparent single lifetimes.
|
1816
|
-
|
1817
|
-
Parameters
|
1818
|
-
----------
|
1819
|
-
phase_lifetime : ndarray
|
1820
|
-
Apparent single lifetime from phase.
|
1821
|
-
modulation_lifetime : ndarray, optional
|
1822
|
-
Apparent single lifetime from modulation.
|
1823
|
-
If None, `modulation_lifetime` is same as `phase_lifetime`.
|
1824
|
-
frequency : array_like
|
1825
|
-
Laser pulse or modulation frequency in MHz.
|
1826
|
-
unit_conversion : float, optional
|
1827
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
1828
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1829
|
-
Use 1.0 for Hz and s.
|
1830
|
-
**kwargs
|
1831
|
-
Optional `arguments passed to numpy universal functions
|
1832
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1833
|
-
|
1834
|
-
Returns
|
1835
|
-
-------
|
1836
|
-
real : ndarray
|
1837
|
-
Real component of phasor coordinates.
|
1838
|
-
imag : ndarray
|
1839
|
-
Imaginary component of phasor coordinates.
|
1840
|
-
|
1841
|
-
See Also
|
1842
|
-
--------
|
1843
|
-
phasorpy.phasor.phasor_to_apparent_lifetime
|
1844
|
-
|
1845
|
-
Notes
|
1846
|
-
-----
|
1847
|
-
The apparent single lifetimes `phase_lifetime` (:math:`\tau_{\phi}`)
|
1848
|
-
and `modulation_lifetime` (:math:`\tau_{M}`) are converted to phasor
|
1849
|
-
coordinates `real` (:math:`G`) and `imag` (:math:`S`) at
|
1850
|
-
frequency :math:`f` according to:
|
1851
|
-
|
1852
|
-
.. math::
|
1853
|
-
|
1854
|
-
\omega &= 2 \pi f
|
1855
|
-
|
1856
|
-
\phi & = \arctan(\omega \tau_{\phi})
|
1857
|
-
|
1858
|
-
M &= 1 / \sqrt{1 + (\omega \tau_{M})^2}
|
1859
|
-
|
1860
|
-
G &= M \cdot \cos{\phi}
|
1861
|
-
|
1862
|
-
S &= M \cdot \sin{\phi}
|
1863
|
-
|
1864
|
-
Examples
|
1865
|
-
--------
|
1866
|
-
If the apparent single lifetimes from phase and modulation are equal,
|
1867
|
-
the phasor coordinates lie on the universal semicircle, else inside:
|
1868
|
-
|
1869
|
-
>>> phasor_from_apparent_lifetime(
|
1870
|
-
... 1.9894, [1.9894, 2.4113], frequency=80.0
|
1871
|
-
... ) # doctest: +NUMBER
|
1872
|
-
(array([0.5, 0.45]), array([0.5, 0.45]))
|
1873
|
-
|
1874
|
-
Zero and infinite apparent single lifetimes define the endpoints of the
|
1875
|
-
universal semicircle:
|
1876
|
-
|
1877
|
-
>>> phasor_from_apparent_lifetime(
|
1878
|
-
... [0.0, 1e9], [0.0, 1e9], frequency=80
|
1879
|
-
... ) # doctest: +NUMBER
|
1880
|
-
(array([1, 0.0]), array([0, 0.0]))
|
1881
|
-
|
1882
|
-
"""
|
1883
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
1884
|
-
omega *= math.pi * 2.0 * unit_conversion
|
1885
|
-
if modulation_lifetime is None:
|
1886
|
-
return _phasor_from_single_lifetime( # type: ignore[no-any-return]
|
1887
|
-
phase_lifetime, omega, **kwargs
|
1888
|
-
)
|
1889
|
-
return _phasor_from_apparent_lifetime( # type: ignore[no-any-return]
|
1890
|
-
phase_lifetime, modulation_lifetime, omega, **kwargs
|
1891
|
-
)
|
1892
|
-
|
1893
|
-
|
1894
|
-
def phasor_to_normal_lifetime(
|
1895
|
-
real: ArrayLike,
|
1896
|
-
imag: ArrayLike,
|
1897
|
-
/,
|
1898
|
-
frequency: ArrayLike,
|
1899
|
-
*,
|
1900
|
-
unit_conversion: float = 1e-3,
|
1901
|
-
**kwargs: Any,
|
1902
|
-
) -> NDArray[Any]:
|
1903
|
-
r"""Return normal lifetimes from phasor coordinates.
|
1904
|
-
|
1905
|
-
The normal lifetime of phasor coordinates represents the single lifetime
|
1906
|
-
equivalent corresponding to the perpendicular projection of the coordinates
|
1907
|
-
onto the universal semicircle, as defined in [3]_.
|
1908
|
-
|
1909
|
-
Parameters
|
1910
|
-
----------
|
1911
|
-
real : array_like
|
1912
|
-
Real component of phasor coordinates.
|
1913
|
-
imag : array_like
|
1914
|
-
Imaginary component of phasor coordinates.
|
1915
|
-
frequency : array_like
|
1916
|
-
Laser pulse or modulation frequency in MHz.
|
1917
|
-
unit_conversion : float, optional
|
1918
|
-
Product of `frequency` and returned `lifetime` units' prefix factors.
|
1919
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1920
|
-
Use 1.0 for Hz and s.
|
1921
|
-
**kwargs
|
1922
|
-
Optional `arguments passed to numpy universal functions
|
1923
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1924
|
-
|
1925
|
-
Returns
|
1926
|
-
-------
|
1927
|
-
normal_lifetime : ndarray
|
1928
|
-
Normal lifetime from of phasor coordinates.
|
1929
|
-
|
1930
|
-
See Also
|
1931
|
-
--------
|
1932
|
-
:ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
|
1933
|
-
|
1934
|
-
Notes
|
1935
|
-
-----
|
1936
|
-
The phasor coordinates `real` (:math:`G`) and `imag` (:math:`S`)
|
1937
|
-
are converted to normal lifetimes `normal_lifetime` (:math:`\tau_{N}`)
|
1938
|
-
at frequency :math:`f` according to:
|
1939
|
-
|
1940
|
-
.. math::
|
1941
|
-
|
1942
|
-
\omega &= 2 \pi f
|
1943
|
-
|
1944
|
-
G_{N} &= 0.5 \cdot (1 + \cos{\arctan{\frac{S}{G - 0.5}}})
|
1945
|
-
|
1946
|
-
\tau_{N} &= \sqrt{\frac{1 - G_{N}}{\omega^{2} \cdot G_{N}}}
|
1947
|
-
|
1948
|
-
References
|
1949
|
-
----------
|
1950
|
-
|
1951
|
-
.. [3] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
|
1952
|
-
uncertainty to enable lower photon count in FLIM experiments
|
1953
|
-
<https://doi.org/10.1088/2050-6120/aa72ab>`_.
|
1954
|
-
*Methods Appl Fluoresc*, 5(2): 024016 (2017)
|
1955
|
-
|
1956
|
-
Examples
|
1957
|
-
--------
|
1958
|
-
The normal lifetimes of phasor coordinates with a real component of 0.5
|
1959
|
-
are independent of the imaginary component:
|
1960
|
-
|
1961
|
-
>>> phasor_to_normal_lifetime(
|
1962
|
-
... 0.5, [0.5, 0.45], frequency=80
|
1963
|
-
... ) # doctest: +NUMBER
|
1964
|
-
array([1.989, 1.989])
|
1965
|
-
|
1966
|
-
"""
|
1967
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
1968
|
-
omega *= math.pi * 2.0 * unit_conversion
|
1969
|
-
return _phasor_to_normal_lifetime( # type: ignore[no-any-return]
|
1970
|
-
real, imag, omega, **kwargs
|
1971
|
-
)
|
1972
|
-
|
1973
|
-
|
1974
|
-
def lifetime_to_frequency(
|
1975
|
-
lifetime: ArrayLike,
|
1976
|
-
*,
|
1977
|
-
unit_conversion: float = 1e-3,
|
1978
|
-
) -> NDArray[numpy.float64]:
|
1979
|
-
r"""Return optimal frequency for resolving single component lifetime.
|
1980
|
-
|
1981
|
-
Parameters
|
1982
|
-
----------
|
1983
|
-
lifetime : array_like
|
1984
|
-
Single component lifetime.
|
1985
|
-
unit_conversion : float, optional, default: 1e-3
|
1986
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
1987
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1988
|
-
Use 1.0 for Hz and s.
|
1989
|
-
|
1990
|
-
Returns
|
1991
|
-
-------
|
1992
|
-
frequency : ndarray
|
1993
|
-
Optimal laser pulse or modulation frequency for resolving `lifetime`.
|
1994
|
-
|
1995
|
-
Notes
|
1996
|
-
-----
|
1997
|
-
The optimal frequency :math:`f` to resolve a single component lifetime
|
1998
|
-
:math:`\tau` is
|
1999
|
-
(:ref:`Redford & Clegg 2005 <redford-clegg-2005>`. Eq. B.6):
|
2000
|
-
|
2001
|
-
.. math::
|
2002
|
-
|
2003
|
-
\omega &= 2 \pi f
|
2004
|
-
|
2005
|
-
\omega^2 &= \frac{1 + \sqrt{3}}{2 \tau^2}
|
2006
|
-
|
2007
|
-
Examples
|
2008
|
-
--------
|
2009
|
-
Measurements of a lifetime near 4 ns should be made at 47 MHz,
|
2010
|
-
near 1 ns at 186 MHz:
|
2011
|
-
|
2012
|
-
>>> lifetime_to_frequency([4.0, 1.0]) # doctest: +NUMBER
|
2013
|
-
array([46.5, 186])
|
2014
|
-
|
2015
|
-
"""
|
2016
|
-
t = numpy.reciprocal(lifetime, dtype=numpy.float64)
|
2017
|
-
t *= 0.18601566519848653 / unit_conversion
|
2018
|
-
return t
|
2019
|
-
|
2020
|
-
|
2021
|
-
def lifetime_from_frequency(
|
2022
|
-
frequency: ArrayLike,
|
2023
|
-
*,
|
2024
|
-
unit_conversion: float = 1e-3,
|
2025
|
-
) -> NDArray[numpy.float64]:
|
2026
|
-
r"""Return single component lifetime best resolved at frequency.
|
2027
|
-
|
2028
|
-
Parameters
|
2029
|
-
----------
|
2030
|
-
frequency : array_like
|
2031
|
-
Laser pulse or modulation frequency.
|
2032
|
-
unit_conversion : float, optional, default: 1e-3
|
2033
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
2034
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2035
|
-
Use 1.0 for Hz and s.
|
2036
|
-
|
2037
|
-
Returns
|
2038
|
-
-------
|
2039
|
-
lifetime : ndarray
|
2040
|
-
Single component lifetime best resolved at `frequency`.
|
2041
|
-
|
2042
|
-
Notes
|
2043
|
-
-----
|
2044
|
-
The lifetime :math:`\tau` that is best resolved at frequency :math:`f` is
|
2045
|
-
(:ref:`Redford & Clegg 2005 <redford-clegg-2005>`. Eq. B.6):
|
2046
|
-
|
2047
|
-
.. math::
|
2048
|
-
|
2049
|
-
\omega &= 2 \pi f
|
2050
|
-
|
2051
|
-
\tau^2 &= \frac{1 + \sqrt{3}}{2 \omega^2}
|
2052
|
-
|
2053
|
-
Examples
|
2054
|
-
--------
|
2055
|
-
Measurements at frequencies of 47 and 186 MHz are best for measuring
|
2056
|
-
lifetimes near 4 and 1 ns respectively:
|
2057
|
-
|
2058
|
-
>>> lifetime_from_frequency([46.5, 186]) # doctest: +NUMBER
|
2059
|
-
array([4, 1])
|
2060
|
-
|
2061
|
-
"""
|
2062
|
-
t = numpy.reciprocal(frequency, dtype=numpy.float64)
|
2063
|
-
t *= 0.18601566519848653 / unit_conversion
|
2064
|
-
return t
|
2065
|
-
|
2066
|
-
|
2067
|
-
def lifetime_fraction_to_amplitude(
|
2068
|
-
lifetime: ArrayLike, fraction: ArrayLike, *, axis: int = -1
|
2069
|
-
) -> NDArray[numpy.float64]:
|
2070
|
-
r"""Return pre-exponential amplitude from fractional intensity.
|
2071
|
-
|
2072
|
-
Parameters
|
2073
|
-
----------
|
2074
|
-
lifetime : array_like
|
2075
|
-
Lifetime components.
|
2076
|
-
fraction : array_like
|
2077
|
-
Fractional intensities of lifetime components.
|
2078
|
-
Fractions are normalized to sum to 1.
|
2079
|
-
axis : int, optional
|
2080
|
-
Axis over which to compute pre-exponential amplitudes.
|
2081
|
-
The default is the last axis (-1).
|
2082
|
-
|
2083
|
-
Returns
|
2084
|
-
-------
|
2085
|
-
amplitude : ndarray
|
2086
|
-
Pre-exponential amplitudes.
|
2087
|
-
The product of `amplitude` and `lifetime` sums to 1 along `axis`.
|
2088
|
-
|
2089
|
-
See Also
|
2090
|
-
--------
|
2091
|
-
phasorpy.phasor.lifetime_fraction_from_amplitude
|
2092
|
-
|
2093
|
-
Notes
|
2094
|
-
-----
|
2095
|
-
The pre-exponential amplitude :math:`a` of component :math:`j` with
|
2096
|
-
lifetime :math:`\tau` and fractional intensity :math:`\alpha` is:
|
2097
|
-
|
2098
|
-
.. math::
|
2099
|
-
|
2100
|
-
a_{j} = \frac{\alpha_{j}}{\tau_{j} \cdot \sum_{j} \alpha_{j}}
|
2101
|
-
|
2102
|
-
Examples
|
2103
|
-
--------
|
2104
|
-
>>> lifetime_fraction_to_amplitude(
|
2105
|
-
... [4.0, 1.0], [1.6, 0.4]
|
2106
|
-
... ) # doctest: +NUMBER
|
2107
|
-
array([0.2, 0.2])
|
2108
|
-
|
2109
|
-
"""
|
2110
|
-
t = numpy.array(fraction, dtype=numpy.float64) # makes copy
|
2111
|
-
t /= numpy.sum(t, axis=axis, keepdims=True)
|
2112
|
-
numpy.true_divide(t, lifetime, out=t)
|
2113
|
-
return t
|
2114
|
-
|
2115
|
-
|
2116
|
-
def lifetime_fraction_from_amplitude(
|
2117
|
-
lifetime: ArrayLike, amplitude: ArrayLike, *, axis: int = -1
|
2118
|
-
) -> NDArray[numpy.float64]:
|
2119
|
-
r"""Return fractional intensity from pre-exponential amplitude.
|
2120
|
-
|
2121
|
-
Parameters
|
2122
|
-
----------
|
2123
|
-
lifetime : array_like
|
2124
|
-
Lifetime of components.
|
2125
|
-
amplitude : array_like
|
2126
|
-
Pre-exponential amplitudes of lifetime components.
|
2127
|
-
axis : int, optional
|
2128
|
-
Axis over which to compute fractional intensities.
|
2129
|
-
The default is the last axis (-1).
|
2130
|
-
|
2131
|
-
Returns
|
2132
|
-
-------
|
2133
|
-
fraction : ndarray
|
2134
|
-
Fractional intensities, normalized to sum to 1 along `axis`.
|
2135
|
-
|
2136
|
-
See Also
|
2137
|
-
--------
|
2138
|
-
phasorpy.phasor.lifetime_fraction_to_amplitude
|
2139
|
-
|
2140
|
-
Notes
|
2141
|
-
-----
|
2142
|
-
The fractional intensity :math:`\alpha` of component :math:`j` with
|
2143
|
-
lifetime :math:`\tau` and pre-exponential amplitude :math:`a` is:
|
2144
|
-
|
2145
|
-
.. math::
|
2146
|
-
|
2147
|
-
\alpha_{j} = \frac{a_{j} \tau_{j}}{\sum_{j} a_{j} \tau_{j}}
|
2148
|
-
|
2149
|
-
Examples
|
2150
|
-
--------
|
2151
|
-
>>> lifetime_fraction_from_amplitude(
|
2152
|
-
... [4.0, 1.0], [1.0, 1.0]
|
2153
|
-
... ) # doctest: +NUMBER
|
2154
|
-
array([0.8, 0.2])
|
2155
|
-
|
2156
|
-
"""
|
2157
|
-
t: NDArray[numpy.float64]
|
2158
|
-
t = numpy.multiply(amplitude, lifetime, dtype=numpy.float64)
|
2159
|
-
t /= numpy.sum(t, axis=axis, keepdims=True)
|
2160
|
-
return t
|
2161
|
-
|
2162
|
-
|
2163
|
-
def phasor_at_harmonic(
|
2164
|
-
real: ArrayLike,
|
2165
|
-
harmonic: ArrayLike,
|
2166
|
-
other_harmonic: ArrayLike,
|
2167
|
-
/,
|
2168
|
-
**kwargs: Any,
|
2169
|
-
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
2170
|
-
r"""Return phasor coordinates on universal semicircle at other harmonics.
|
2171
|
-
|
2172
|
-
Return phasor coordinates at any harmonic, given the real component of
|
2173
|
-
phasor coordinates of a single exponential lifetime at a certain harmonic.
|
2174
|
-
The input and output phasor coordinates lie on the universal semicircle.
|
2175
|
-
|
2176
|
-
Parameters
|
2177
|
-
----------
|
2178
|
-
real : array_like
|
2179
|
-
Real component of phasor coordinates of single exponential lifetime
|
2180
|
-
at `harmonic`.
|
2181
|
-
harmonic : array_like
|
2182
|
-
Harmonic of `real` coordinate. Must be integer >= 1.
|
2183
|
-
other_harmonic : array_like
|
2184
|
-
Harmonic for which to return phasor coordinates. Must be integer >= 1.
|
2185
|
-
**kwargs
|
2186
|
-
Optional `arguments passed to numpy universal functions
|
2187
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2188
|
-
|
2189
|
-
Returns
|
2190
|
-
-------
|
2191
|
-
real_other : ndarray
|
2192
|
-
Real component of phasor coordinates at `other_harmonic`.
|
2193
|
-
imag_other : ndarray
|
2194
|
-
Imaginary component of phasor coordinates at `other_harmonic`.
|
2195
|
-
|
2196
|
-
Notes
|
2197
|
-
-----
|
2198
|
-
The phasor coordinates
|
2199
|
-
:math:`g_{n}` (`real_other`) and :math:`s_{n}` (`imag_other`)
|
2200
|
-
of a single exponential lifetime at harmonic :math:`n` (`other_harmonic`)
|
2201
|
-
is calculated from the real part of the phasor coordinates
|
2202
|
-
:math:`g_{m}` (`real`) at harmonic :math:`m` (`harmonic`) according to
|
2203
|
-
(:ref:`Torrado, Malacrida, & Ranjit. 2022 <torrado-2022>`. Eq. 25):
|
2204
|
-
|
2205
|
-
.. math::
|
2206
|
-
|
2207
|
-
g_{n} &= \frac{m^2 \cdot g_{m}}{n^2 + (m^2-n^2) \cdot g_{m}}
|
2208
|
-
|
2209
|
-
s_{n} &= \sqrt{G_{n} - g_{n}^2}
|
2210
|
-
|
2211
|
-
This function is equivalent to the following operations:
|
2212
|
-
|
2213
|
-
.. code-block:: python
|
2214
|
-
|
2215
|
-
phasor_from_lifetime(
|
2216
|
-
frequency=other_harmonic,
|
2217
|
-
lifetime=phasor_to_apparent_lifetime(
|
2218
|
-
real, sqrt(real - real * real), frequency=harmonic
|
2219
|
-
)[0],
|
2220
|
-
)
|
2221
|
-
|
2222
|
-
Examples
|
2223
|
-
--------
|
2224
|
-
The phasor coordinates at higher harmonics are approaching the origin:
|
2225
|
-
|
2226
|
-
>>> phasor_at_harmonic(0.5, 1, [1, 2, 4, 8]) # doctest: +NUMBER
|
2227
|
-
(array([0.5, 0.2, 0.05882, 0.01538]), array([0.5, 0.4, 0.2353, 0.1231]))
|
2228
|
-
|
2229
|
-
"""
|
2230
|
-
harmonic = numpy.asarray(harmonic, dtype=numpy.int32)
|
2231
|
-
if numpy.any(harmonic < 1):
|
2232
|
-
raise ValueError('invalid harmonic')
|
2233
|
-
|
2234
|
-
other_harmonic = numpy.asarray(other_harmonic, dtype=numpy.int32)
|
2235
|
-
if numpy.any(other_harmonic < 1):
|
2236
|
-
raise ValueError('invalid other_harmonic')
|
2237
|
-
|
2238
|
-
return _phasor_at_harmonic( # type: ignore[no-any-return]
|
2239
|
-
real, harmonic, other_harmonic, **kwargs
|
2240
|
-
)
|
2241
|
-
|
2242
|
-
|
2243
|
-
def phasor_from_lifetime(
|
2244
|
-
frequency: ArrayLike,
|
2245
|
-
lifetime: ArrayLike,
|
2246
|
-
fraction: ArrayLike | None = None,
|
2247
|
-
*,
|
2248
|
-
preexponential: bool = False,
|
2249
|
-
unit_conversion: float = 1e-3,
|
2250
|
-
keepdims: bool = False,
|
2251
|
-
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
2252
|
-
r"""Return phasor coordinates from lifetime components.
|
2253
|
-
|
2254
|
-
Calculate phasor coordinates as a function of frequency, single or
|
2255
|
-
multiple lifetime components, and the pre-exponential amplitudes
|
2256
|
-
or fractional intensities of the components.
|
2257
|
-
|
2258
|
-
Parameters
|
2259
|
-
----------
|
2260
|
-
frequency : array_like
|
2261
|
-
Laser pulse or modulation frequency in MHz.
|
2262
|
-
A scalar or one-dimensional sequence.
|
2263
|
-
lifetime : array_like
|
2264
|
-
Lifetime components in ns. See notes below for allowed dimensions.
|
2265
|
-
fraction : array_like, optional
|
2266
|
-
Fractional intensities or pre-exponential amplitudes of the lifetime
|
2267
|
-
components. Fractions are normalized to sum to 1.
|
2268
|
-
See notes below for allowed dimensions.
|
2269
|
-
preexponential : bool, optional, default: False
|
2270
|
-
If true, `fraction` values are pre-exponential amplitudes,
|
2271
|
-
else fractional intensities.
|
2272
|
-
unit_conversion : float, optional, default: 1e-3
|
2273
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
2274
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2275
|
-
Use 1.0 for Hz and s.
|
2276
|
-
keepdims : bool, optional, default: False
|
2277
|
-
If true, length-one dimensions are left in phasor coordinates.
|
2278
|
-
|
2279
|
-
Returns
|
2280
|
-
-------
|
2281
|
-
real : ndarray
|
2282
|
-
Real component of phasor coordinates.
|
2283
|
-
imag : ndarray
|
2284
|
-
Imaginary component of phasor coordinates.
|
2285
|
-
|
2286
|
-
See notes below for dimensions of the returned arrays.
|
2287
|
-
|
2288
|
-
Raises
|
2289
|
-
------
|
2290
|
-
ValueError
|
2291
|
-
Input arrays exceed their allowed dimensionality or do not match.
|
2292
|
-
|
2293
|
-
See Also
|
2294
|
-
--------
|
2295
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_phasor_from_lifetime.py`
|
2296
|
-
:ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
|
2297
|
-
|
2298
|
-
Notes
|
2299
|
-
-----
|
2300
|
-
The phasor coordinates :math:`G` (`real`) and :math:`S` (`imag`) for
|
2301
|
-
many lifetime components :math:`j` with lifetimes :math:`\tau` and
|
2302
|
-
pre-exponential amplitudes :math:`\alpha` at frequency :math:`f` are:
|
2303
|
-
|
2304
|
-
.. math::
|
2305
|
-
|
2306
|
-
\omega &= 2 \pi f
|
2307
|
-
|
2308
|
-
g_{j} &= \alpha_{j} / (1 + (\omega \tau_{j})^2)
|
2309
|
-
|
2310
|
-
G &= \sum_{j} g_{j}
|
2311
|
-
|
2312
|
-
S &= \sum_{j} \omega \tau_{j} g_{j}
|
2313
|
-
|
2314
|
-
The relation between pre-exponential amplitudes :math:`a` and
|
2315
|
-
fractional intensities :math:`\alpha` is:
|
2316
|
-
|
2317
|
-
.. math::
|
2318
|
-
F_{DC} &= \sum_{j} a_{j} \tau_{j}
|
2319
|
-
|
2320
|
-
\alpha_{j} &= a_{j} \tau_{j} / F_{DC}
|
2321
|
-
|
2322
|
-
The following combinations of `lifetime` and `fraction` parameters are
|
2323
|
-
supported:
|
2324
|
-
|
2325
|
-
- `lifetime` is scalar or one-dimensional, holding single component
|
2326
|
-
lifetimes. `fraction` is None.
|
2327
|
-
Return arrays of shape `(frequency.size, lifetime.size)`.
|
2328
|
-
|
2329
|
-
- `lifetime` is two-dimensional, `fraction` is one-dimensional.
|
2330
|
-
The last dimensions match in size, holding lifetime components and
|
2331
|
-
their fractions.
|
2332
|
-
Return arrays of shape `(frequency.size, lifetime.shape[1])`.
|
2333
|
-
|
2334
|
-
- `lifetime` is one-dimensional, `fraction` is two-dimensional.
|
2335
|
-
The last dimensions must match in size, holding lifetime components and
|
2336
|
-
their fractions.
|
2337
|
-
Return arrays of shape `(frequency.size, fraction.shape[1])`.
|
2338
|
-
|
2339
|
-
- `lifetime` and `fraction` are up to two-dimensional of same shape.
|
2340
|
-
The last dimensions hold lifetime components and their fractions.
|
2341
|
-
Return arrays of shape `(frequency.size, lifetime.shape[0])`.
|
2342
|
-
|
2343
|
-
Length-one dimensions are removed from returned arrays
|
2344
|
-
if `keepdims` is false (default).
|
2345
|
-
|
2346
|
-
Examples
|
2347
|
-
--------
|
2348
|
-
Phasor coordinates of a single lifetime component (in ns) at a
|
2349
|
-
frequency of 80 MHz:
|
2350
|
-
|
2351
|
-
>>> phasor_from_lifetime(80.0, 1.9894368) # doctest: +NUMBER
|
2352
|
-
(0.5, 0.5)
|
2353
|
-
|
2354
|
-
Phasor coordinates of two lifetime components with equal fractional
|
2355
|
-
intensities:
|
2356
|
-
|
2357
|
-
>>> phasor_from_lifetime(
|
2358
|
-
... 80.0, [3.9788735, 0.9947183], [0.5, 0.5]
|
2359
|
-
... ) # doctest: +NUMBER
|
2360
|
-
(0.5, 0.4)
|
2361
|
-
|
2362
|
-
Phasor coordinates of two lifetime components with equal pre-exponential
|
2363
|
-
amplitudes:
|
2364
|
-
|
2365
|
-
>>> phasor_from_lifetime(
|
2366
|
-
... 80.0, [3.9788735, 0.9947183], [0.5, 0.5], preexponential=True
|
2367
|
-
... ) # doctest: +NUMBER
|
2368
|
-
(0.32, 0.4)
|
2369
|
-
|
2370
|
-
Phasor coordinates of many single-component lifetimes (fractions omitted):
|
2371
|
-
|
2372
|
-
>>> phasor_from_lifetime(
|
2373
|
-
... 80.0, [3.9788735, 1.9894368, 0.9947183]
|
2374
|
-
... ) # doctest: +NUMBER
|
2375
|
-
(array([0.2, 0.5, 0.8]), array([0.4, 0.5, 0.4]))
|
2376
|
-
|
2377
|
-
Phasor coordinates of two lifetime components with varying fractions:
|
2378
|
-
|
2379
|
-
>>> phasor_from_lifetime(
|
2380
|
-
... 80.0, [3.9788735, 0.9947183], [[1, 0], [0.5, 0.5], [0, 1]]
|
2381
|
-
... ) # doctest: +NUMBER
|
2382
|
-
(array([0.2, 0.5, 0.8]), array([0.4, 0.4, 0.4]))
|
2383
|
-
|
2384
|
-
Phasor coordinates of multiple two-component lifetimes with constant
|
2385
|
-
fractions, keeping dimensions:
|
2386
|
-
|
2387
|
-
>>> phasor_from_lifetime(
|
2388
|
-
... 80.0, [[3.9788735, 0.9947183], [1.9894368, 1.9894368]], [0.5, 0.5]
|
2389
|
-
... ) # doctest: +NUMBER
|
2390
|
-
(array([0.5, 0.5]), array([0.4, 0.5]))
|
2391
|
-
|
2392
|
-
Phasor coordinates of multiple two-component lifetimes with specific
|
2393
|
-
fractions at multiple frequencies. Frequencies are in Hz, lifetimes in ns:
|
2394
|
-
|
2395
|
-
>>> phasor_from_lifetime(
|
2396
|
-
... [40e6, 80e6],
|
2397
|
-
... [[1e-9, 0.9947183e-9], [3.9788735e-9, 0.9947183e-9]],
|
2398
|
-
... [[0, 1], [0.5, 0.5]],
|
2399
|
-
... unit_conversion=1.0,
|
2400
|
-
... ) # doctest: +NUMBER
|
2401
|
-
(array([[0.941, 0.721], [0.8, 0.5]]), array([[0.235, 0.368], [0.4, 0.4]]))
|
2402
|
-
|
2403
|
-
"""
|
2404
|
-
if unit_conversion < 1e-16:
|
2405
|
-
raise ValueError(f'{unit_conversion=} < 1e-16')
|
2406
|
-
frequency = numpy.atleast_1d(numpy.asarray(frequency, dtype=numpy.float64))
|
2407
|
-
if frequency.ndim != 1:
|
2408
|
-
raise ValueError('frequency is not one-dimensional array')
|
2409
|
-
lifetime = numpy.atleast_1d(numpy.asarray(lifetime, dtype=numpy.float64))
|
2410
|
-
if lifetime.ndim > 2:
|
2411
|
-
raise ValueError('lifetime must be one- or two-dimensional array')
|
2412
|
-
|
2413
|
-
if fraction is None:
|
2414
|
-
# single-component lifetimes
|
2415
|
-
if lifetime.ndim > 1:
|
2416
|
-
raise ValueError(
|
2417
|
-
'lifetime must be one-dimensional array if fraction is None'
|
2418
|
-
)
|
2419
|
-
lifetime = lifetime.reshape(-1, 1) # move components to last axis
|
2420
|
-
fraction = numpy.ones_like(lifetime) # not really used
|
2421
|
-
else:
|
2422
|
-
fraction = numpy.atleast_1d(
|
2423
|
-
numpy.asarray(fraction, dtype=numpy.float64)
|
2424
|
-
)
|
2425
|
-
if fraction.ndim > 2:
|
2426
|
-
raise ValueError('fraction must be one- or two-dimensional array')
|
2427
|
-
|
2428
|
-
if lifetime.ndim == 1 and fraction.ndim == 1:
|
2429
|
-
# one multi-component lifetime
|
2430
|
-
if lifetime.shape != fraction.shape:
|
2431
|
-
raise ValueError(
|
2432
|
-
f'{lifetime.shape=} does not match {fraction.shape=}'
|
2433
|
-
)
|
2434
|
-
lifetime = lifetime.reshape(1, -1)
|
2435
|
-
fraction = fraction.reshape(1, -1)
|
2436
|
-
nvar = 1
|
2437
|
-
elif lifetime.ndim == 2 and fraction.ndim == 2:
|
2438
|
-
# multiple, multi-component lifetimes
|
2439
|
-
if lifetime.shape[1] != fraction.shape[1]:
|
2440
|
-
raise ValueError(f'{lifetime.shape[1]=} != {fraction.shape[1]=}')
|
2441
|
-
nvar = lifetime.shape[0]
|
2442
|
-
elif lifetime.ndim == 2 and fraction.ndim == 1:
|
2443
|
-
# variable components, same fractions
|
2444
|
-
fraction = fraction.reshape(1, -1)
|
2445
|
-
nvar = lifetime.shape[0]
|
2446
|
-
elif lifetime.ndim == 1 and fraction.ndim == 2:
|
2447
|
-
# same components, varying fractions
|
2448
|
-
lifetime = lifetime.reshape(1, -1)
|
2449
|
-
nvar = fraction.shape[0]
|
2450
|
-
else:
|
2451
|
-
# unreachable code
|
2452
|
-
raise RuntimeError(f'{lifetime.shape=}, {fraction.shape=}')
|
2453
|
-
|
2454
|
-
phasor = numpy.empty((2, frequency.size, nvar), dtype=numpy.float64)
|
2455
|
-
|
2456
|
-
_phasor_from_lifetime(
|
2457
|
-
phasor, frequency, lifetime, fraction, unit_conversion, preexponential
|
2458
|
-
)
|
2459
|
-
|
2460
|
-
if not keepdims:
|
2461
|
-
phasor = phasor.squeeze()
|
2462
|
-
return phasor[0], phasor[1]
|
2463
|
-
|
2464
|
-
|
2465
|
-
def polar_to_apparent_lifetime(
|
2466
|
-
phase: ArrayLike,
|
2467
|
-
modulation: ArrayLike,
|
2468
|
-
/,
|
2469
|
-
frequency: ArrayLike,
|
2470
|
-
*,
|
2471
|
-
unit_conversion: float = 1e-3,
|
2472
|
-
**kwargs: Any,
|
2473
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2474
|
-
r"""Return apparent single lifetimes from polar coordinates.
|
2475
|
-
|
2476
|
-
Parameters
|
2477
|
-
----------
|
2478
|
-
phase : array_like
|
2479
|
-
Angular component of polar coordinates.
|
2480
|
-
imag : array_like
|
2481
|
-
Radial component of polar coordinates.
|
2482
|
-
frequency : array_like
|
2483
|
-
Laser pulse or modulation frequency in MHz.
|
2484
|
-
unit_conversion : float, optional
|
2485
|
-
Product of `frequency` and returned `lifetime` units' prefix factors.
|
2486
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2487
|
-
Use 1.0 for Hz and s.
|
2488
|
-
**kwargs
|
2489
|
-
Optional `arguments passed to numpy universal functions
|
2490
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2491
|
-
|
2492
|
-
Returns
|
2493
|
-
-------
|
2494
|
-
phase_lifetime : ndarray
|
2495
|
-
Apparent single lifetime from `phase`.
|
2496
|
-
modulation_lifetime : ndarray
|
2497
|
-
Apparent single lifetime from `modulation`.
|
2498
|
-
|
2499
|
-
See Also
|
2500
|
-
--------
|
2501
|
-
phasorpy.phasor.polar_from_apparent_lifetime
|
2502
|
-
|
2503
|
-
Notes
|
2504
|
-
-----
|
2505
|
-
The polar coordinates `phase` (:math:`\phi`) and `modulation` (:math:`M`)
|
2506
|
-
are converted to apparent single lifetimes
|
2507
|
-
`phase_lifetime` (:math:`\tau_{\phi}`) and
|
2508
|
-
`modulation_lifetime` (:math:`\tau_{M}`) at frequency :math:`f`
|
2509
|
-
according to:
|
2510
|
-
|
2511
|
-
.. math::
|
2512
|
-
|
2513
|
-
\omega &= 2 \pi f
|
2514
|
-
|
2515
|
-
\tau_{\phi} &= \omega^{-1} \cdot \tan{\phi}
|
2516
|
-
|
2517
|
-
\tau_{M} &= \omega^{-1} \cdot \sqrt{1 / M^2 - 1}
|
2518
|
-
|
2519
|
-
Examples
|
2520
|
-
--------
|
2521
|
-
The apparent single lifetimes from phase and modulation are equal
|
2522
|
-
only if the polar coordinates lie on the universal semicircle:
|
2523
|
-
|
2524
|
-
>>> polar_to_apparent_lifetime(
|
2525
|
-
... math.pi / 4, numpy.hypot([0.5, 0.45], [0.5, 0.45]), frequency=80
|
2526
|
-
... ) # doctest: +NUMBER
|
2527
|
-
(array([1.989, 1.989]), array([1.989, 2.411]))
|
2528
|
-
|
2529
|
-
"""
|
2530
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
2531
|
-
omega *= math.pi * 2.0 * unit_conversion
|
2532
|
-
return _polar_to_apparent_lifetime( # type: ignore[no-any-return]
|
2533
|
-
phase, modulation, omega, **kwargs
|
2534
|
-
)
|
2535
|
-
|
2536
|
-
|
2537
|
-
def polar_from_apparent_lifetime(
|
2538
|
-
phase_lifetime: ArrayLike,
|
2539
|
-
modulation_lifetime: ArrayLike | None,
|
2540
|
-
/,
|
2541
|
-
frequency: ArrayLike,
|
2542
|
-
*,
|
2543
|
-
unit_conversion: float = 1e-3,
|
2544
|
-
**kwargs: Any,
|
2545
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2546
|
-
r"""Return polar coordinates from apparent single lifetimes.
|
2547
|
-
|
2548
|
-
Parameters
|
2549
|
-
----------
|
2550
|
-
phase_lifetime : ndarray
|
2551
|
-
Apparent single lifetime from phase.
|
2552
|
-
modulation_lifetime : ndarray, optional
|
2553
|
-
Apparent single lifetime from modulation.
|
2554
|
-
If None, `modulation_lifetime` is same as `phase_lifetime`.
|
2555
|
-
frequency : array_like
|
2556
|
-
Laser pulse or modulation frequency in MHz.
|
2557
|
-
unit_conversion : float, optional
|
2558
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
2559
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2560
|
-
Use 1.0 for Hz and s.
|
2561
|
-
**kwargs
|
2562
|
-
Optional `arguments passed to numpy universal functions
|
2563
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2564
|
-
|
2565
|
-
Returns
|
2566
|
-
-------
|
2567
|
-
phase : ndarray
|
2568
|
-
Angular component of polar coordinates.
|
2569
|
-
modulation : ndarray
|
2570
|
-
Radial component of polar coordinates.
|
2571
|
-
|
2572
|
-
See Also
|
2573
|
-
--------
|
2574
|
-
phasorpy.phasor.polar_to_apparent_lifetime
|
2575
|
-
|
2576
|
-
Notes
|
2577
|
-
-----
|
2578
|
-
The apparent single lifetimes `phase_lifetime` (:math:`\tau_{\phi}`)
|
2579
|
-
and `modulation_lifetime` (:math:`\tau_{M}`) are converted to polar
|
2580
|
-
coordinates `phase` (:math:`\phi`) and `modulation` (:math:`M`) at
|
2581
|
-
frequency :math:`f` according to:
|
2582
|
-
|
2583
|
-
.. math::
|
2584
|
-
|
2585
|
-
\omega &= 2 \pi f
|
2586
|
-
|
2587
|
-
\phi & = \arctan(\omega \tau_{\phi})
|
2588
|
-
|
2589
|
-
M &= 1 / \sqrt{1 + (\omega \tau_{M})^2}
|
2590
|
-
|
2591
|
-
Examples
|
2592
|
-
--------
|
2593
|
-
If the apparent single lifetimes from phase and modulation are equal,
|
2594
|
-
the polar coordinates lie on the universal semicircle, else inside:
|
2595
|
-
|
2596
|
-
>>> polar_from_apparent_lifetime(
|
2597
|
-
... 1.9894, [1.9894, 2.4113], frequency=80.0
|
2598
|
-
... ) # doctest: +NUMBER
|
2599
|
-
(array([0.7854, 0.7854]), array([0.7071, 0.6364]))
|
2600
|
-
|
2601
|
-
"""
|
2602
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
2603
|
-
omega *= math.pi * 2.0 * unit_conversion
|
2604
|
-
if modulation_lifetime is None:
|
2605
|
-
return _polar_from_single_lifetime( # type: ignore[no-any-return]
|
2606
|
-
phase_lifetime, omega, **kwargs
|
2607
|
-
)
|
2608
|
-
return _polar_from_apparent_lifetime( # type: ignore[no-any-return]
|
2609
|
-
phase_lifetime, modulation_lifetime, omega, **kwargs
|
2610
|
-
)
|
2611
|
-
|
2612
|
-
|
2613
|
-
def phasor_from_fret_donor(
|
2614
|
-
frequency: ArrayLike,
|
2615
|
-
donor_lifetime: ArrayLike,
|
2616
|
-
*,
|
2617
|
-
fret_efficiency: ArrayLike = 0.0,
|
2618
|
-
donor_fretting: ArrayLike = 1.0,
|
2619
|
-
donor_background: ArrayLike = 0.0,
|
2620
|
-
background_real: ArrayLike = 0.0,
|
2621
|
-
background_imag: ArrayLike = 0.0,
|
2622
|
-
unit_conversion: float = 1e-3,
|
2623
|
-
**kwargs: Any,
|
2624
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2625
|
-
"""Return phasor coordinates of FRET donor channel.
|
2626
|
-
|
2627
|
-
Calculate phasor coordinates of a FRET (Förster Resonance Energy Transfer)
|
2628
|
-
donor channel as a function of frequency, donor lifetime, FRET efficiency,
|
2629
|
-
fraction of donors undergoing FRET, and background fluorescence.
|
2630
|
-
|
2631
|
-
The phasor coordinates of the donor channel contain fractions of:
|
2632
|
-
|
2633
|
-
- donor not undergoing energy transfer
|
2634
|
-
- donor quenched by energy transfer
|
2635
|
-
- background fluorescence
|
2636
|
-
|
2637
|
-
Parameters
|
2638
|
-
----------
|
2639
|
-
frequency : array_like
|
2640
|
-
Laser pulse or modulation frequency in MHz.
|
2641
|
-
donor_lifetime : array_like
|
2642
|
-
Lifetime of donor without FRET in ns.
|
2643
|
-
fret_efficiency : array_like, optional, default 0
|
2644
|
-
FRET efficiency in range [0, 1].
|
2645
|
-
donor_fretting : array_like, optional, default 1
|
2646
|
-
Fraction of donors participating in FRET. Range [0, 1].
|
2647
|
-
donor_background : array_like, optional, default 0
|
2648
|
-
Weight of background fluorescence in donor channel
|
2649
|
-
relative to fluorescence of donor without FRET.
|
2650
|
-
A weight of 1 means the fluorescence of background and donor
|
2651
|
-
without FRET are equal.
|
2652
|
-
background_real : array_like, optional, default 0
|
2653
|
-
Real component of background fluorescence phasor coordinate
|
2654
|
-
at `frequency`.
|
2655
|
-
background_imag : array_like, optional, default 0
|
2656
|
-
Imaginary component of background fluorescence phasor coordinate
|
2657
|
-
at `frequency`.
|
2658
|
-
unit_conversion : float, optional
|
2659
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
2660
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2661
|
-
Use 1.0 for Hz and s.
|
2662
|
-
**kwargs
|
2663
|
-
Optional `arguments passed to numpy universal functions
|
2664
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2665
|
-
|
2666
|
-
Returns
|
2667
|
-
-------
|
2668
|
-
real : ndarray
|
2669
|
-
Real component of donor channel phasor coordinates.
|
2670
|
-
imag : ndarray
|
2671
|
-
Imaginary component of donor channel phasor coordinates.
|
2672
|
-
|
2673
|
-
See Also
|
2674
|
-
--------
|
2675
|
-
phasorpy.phasor.phasor_from_fret_acceptor
|
2676
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
|
2677
|
-
:ref:`sphx_glr_tutorials_applications_phasorpy_fret_efficiency.py`
|
2678
|
-
|
2679
|
-
Examples
|
2680
|
-
--------
|
2681
|
-
Compute the phasor coordinates of a FRET donor channel at three
|
2682
|
-
FRET efficiencies:
|
2683
|
-
|
2684
|
-
>>> phasor_from_fret_donor(
|
2685
|
-
... frequency=80,
|
2686
|
-
... donor_lifetime=4.2,
|
2687
|
-
... fret_efficiency=[0.0, 0.3, 1.0],
|
2688
|
-
... donor_fretting=0.9,
|
2689
|
-
... donor_background=0.1,
|
2690
|
-
... background_real=0.11,
|
2691
|
-
... background_imag=0.12,
|
2692
|
-
... ) # doctest: +NUMBER
|
2693
|
-
(array([0.1766, 0.2737, 0.1466]), array([0.3626, 0.4134, 0.2534]))
|
2694
|
-
|
2695
|
-
"""
|
2696
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
2697
|
-
omega *= math.pi * 2.0 * unit_conversion
|
2698
|
-
return _phasor_from_fret_donor( # type: ignore[no-any-return]
|
2699
|
-
omega,
|
2700
|
-
donor_lifetime,
|
2701
|
-
fret_efficiency,
|
2702
|
-
donor_fretting,
|
2703
|
-
donor_background,
|
2704
|
-
background_real,
|
2705
|
-
background_imag,
|
2706
|
-
**kwargs,
|
2707
|
-
)
|
2708
|
-
|
2709
|
-
|
2710
|
-
def phasor_from_fret_acceptor(
|
2711
|
-
frequency: ArrayLike,
|
2712
|
-
donor_lifetime: ArrayLike,
|
2713
|
-
acceptor_lifetime: ArrayLike,
|
2714
|
-
*,
|
2715
|
-
fret_efficiency: ArrayLike = 0.0,
|
2716
|
-
donor_fretting: ArrayLike = 1.0,
|
2717
|
-
donor_bleedthrough: ArrayLike = 0.0,
|
2718
|
-
acceptor_bleedthrough: ArrayLike = 0.0,
|
2719
|
-
acceptor_background: ArrayLike = 0.0,
|
2720
|
-
background_real: ArrayLike = 0.0,
|
2721
|
-
background_imag: ArrayLike = 0.0,
|
2722
|
-
unit_conversion: float = 1e-3,
|
2723
|
-
**kwargs: Any,
|
2724
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2725
|
-
"""Return phasor coordinates of FRET acceptor channel.
|
2726
|
-
|
2727
|
-
Calculate phasor coordinates of a FRET (Förster Resonance Energy Transfer)
|
2728
|
-
acceptor channel as a function of frequency, donor and acceptor lifetimes,
|
2729
|
-
FRET efficiency, fraction of donors undergoing FRET, fraction of directly
|
2730
|
-
excited acceptors, fraction of donor fluorescence in acceptor channel,
|
2731
|
-
and background fluorescence.
|
2732
|
-
|
2733
|
-
The phasor coordinates of the acceptor channel contain fractions of:
|
2734
|
-
|
2735
|
-
- acceptor sensitized by energy transfer
|
2736
|
-
- directly excited acceptor
|
2737
|
-
- donor bleedthrough
|
2738
|
-
- background fluorescence
|
2739
|
-
|
2740
|
-
Parameters
|
2741
|
-
----------
|
2742
|
-
frequency : array_like
|
2743
|
-
Laser pulse or modulation frequency in MHz.
|
2744
|
-
donor_lifetime : array_like
|
2745
|
-
Lifetime of donor without FRET in ns.
|
2746
|
-
acceptor_lifetime : array_like
|
2747
|
-
Lifetime of acceptor in ns.
|
2748
|
-
fret_efficiency : array_like, optional, default 0
|
2749
|
-
FRET efficiency in range [0, 1].
|
2750
|
-
donor_fretting : array_like, optional, default 1
|
2751
|
-
Fraction of donors participating in FRET. Range [0, 1].
|
2752
|
-
donor_bleedthrough : array_like, optional, default 0
|
2753
|
-
Weight of donor fluorescence in acceptor channel
|
2754
|
-
relative to fluorescence of fully sensitized acceptor.
|
2755
|
-
A weight of 1 means the fluorescence from donor and fully sensitized
|
2756
|
-
acceptor are equal.
|
2757
|
-
The background in the donor channel does not bleed through.
|
2758
|
-
acceptor_bleedthrough : array_like, optional, default 0
|
2759
|
-
Weight of fluorescence from directly excited acceptor
|
2760
|
-
relative to fluorescence of fully sensitized acceptor.
|
2761
|
-
A weight of 1 means the fluorescence from directly excited acceptor
|
2762
|
-
and fully sensitized acceptor are equal.
|
2763
|
-
acceptor_background : array_like, optional, default 0
|
2764
|
-
Weight of background fluorescence in acceptor channel
|
2765
|
-
relative to fluorescence of fully sensitized acceptor.
|
2766
|
-
A weight of 1 means the fluorescence of background and fully
|
2767
|
-
sensitized acceptor are equal.
|
2768
|
-
background_real : array_like, optional, default 0
|
2769
|
-
Real component of background fluorescence phasor coordinate
|
2770
|
-
at `frequency`.
|
2771
|
-
background_imag : array_like, optional, default 0
|
2772
|
-
Imaginary component of background fluorescence phasor coordinate
|
2773
|
-
at `frequency`.
|
2774
|
-
unit_conversion : float, optional
|
2775
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
2776
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2777
|
-
Use 1.0 for Hz and s.
|
2778
|
-
**kwargs
|
2779
|
-
Optional `arguments passed to numpy universal functions
|
2780
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2781
|
-
|
2782
|
-
Returns
|
2783
|
-
-------
|
2784
|
-
real : ndarray
|
2785
|
-
Real component of acceptor channel phasor coordinates.
|
2786
|
-
imag : ndarray
|
2787
|
-
Imaginary component of acceptor channel phasor coordinates.
|
2788
|
-
|
2789
|
-
See Also
|
2790
|
-
--------
|
2791
|
-
phasorpy.phasor.phasor_from_fret_donor
|
2792
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
|
2793
|
-
|
2794
|
-
Examples
|
2795
|
-
--------
|
2796
|
-
Compute the phasor coordinates of a FRET acceptor channel at three
|
2797
|
-
FRET efficiencies:
|
2798
|
-
|
2799
|
-
>>> phasor_from_fret_acceptor(
|
2800
|
-
... frequency=80,
|
2801
|
-
... donor_lifetime=4.2,
|
2802
|
-
... acceptor_lifetime=3.0,
|
2803
|
-
... fret_efficiency=[0.0, 0.3, 1.0],
|
2804
|
-
... donor_fretting=0.9,
|
2805
|
-
... donor_bleedthrough=0.1,
|
2806
|
-
... acceptor_bleedthrough=0.1,
|
2807
|
-
... acceptor_background=0.1,
|
2808
|
-
... background_real=0.11,
|
2809
|
-
... background_imag=0.12,
|
2810
|
-
... ) # doctest: +NUMBER
|
2811
|
-
(array([0.1996, 0.05772, 0.2867]), array([0.3225, 0.3103, 0.4292]))
|
2812
|
-
|
2813
|
-
"""
|
2814
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
2815
|
-
omega *= math.pi * 2.0 * unit_conversion
|
2816
|
-
return _phasor_from_fret_acceptor( # type: ignore[no-any-return]
|
2817
|
-
omega,
|
2818
|
-
donor_lifetime,
|
2819
|
-
acceptor_lifetime,
|
2820
|
-
fret_efficiency,
|
2821
|
-
donor_fretting,
|
2822
|
-
donor_bleedthrough,
|
2823
|
-
acceptor_bleedthrough,
|
2824
|
-
acceptor_background,
|
2825
|
-
background_real,
|
2826
|
-
background_imag,
|
2827
|
-
**kwargs,
|
2828
|
-
)
|
2829
|
-
|
2830
|
-
|
2831
|
-
def phasor_to_principal_plane(
|
1049
|
+
def phasor_to_principal_plane(
|
2832
1050
|
real: ArrayLike,
|
2833
1051
|
imag: ArrayLike,
|
2834
1052
|
/,
|
@@ -2837,7 +1055,7 @@ def phasor_to_principal_plane(
|
|
2837
1055
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2838
1056
|
"""Return multi-harmonic phasor coordinates projected onto principal plane.
|
2839
1057
|
|
2840
|
-
Principal
|
1058
|
+
Principal component analysis (PCA) is used to project
|
2841
1059
|
multi-harmonic phasor coordinates onto a plane, along which
|
2842
1060
|
coordinate axes the phasor coordinates have the largest variations.
|
2843
1061
|
|
@@ -2882,7 +1100,7 @@ def phasor_to_principal_plane(
|
|
2882
1100
|
-----
|
2883
1101
|
|
2884
1102
|
This implementation does not work with coordinates containing
|
2885
|
-
undefined
|
1103
|
+
undefined NaN values.
|
2886
1104
|
|
2887
1105
|
The transformation matrix can be used to project multi-harmonic phasor
|
2888
1106
|
coordinates, where the first axis is the frequency:
|
@@ -2902,7 +1120,6 @@ def phasor_to_principal_plane(
|
|
2902
1120
|
|
2903
1121
|
References
|
2904
1122
|
----------
|
2905
|
-
|
2906
1123
|
.. [1] Franssen WMJ, Vergeldt FJ, Bader AN, van Amerongen H, and Terenzi C.
|
2907
1124
|
`Full-harmonics phasor analysis: unravelling multiexponential trends
|
2908
1125
|
in magnetic resonance imaging data
|
@@ -2935,7 +1152,7 @@ def phasor_to_principal_plane(
|
|
2935
1152
|
im = im.reshape(im.shape[0], -1)
|
2936
1153
|
|
2937
1154
|
# vector of multi-frequency phasor coordinates
|
2938
|
-
coordinates = numpy.vstack(
|
1155
|
+
coordinates = numpy.vstack([re, im])
|
2939
1156
|
|
2940
1157
|
# vector of centered coordinates
|
2941
1158
|
center = numpy.nanmean(coordinates, axis=1, keepdims=True)
|
@@ -2997,619 +1214,6 @@ def phasor_to_principal_plane(
|
|
2997
1214
|
)
|
2998
1215
|
|
2999
1216
|
|
3000
|
-
def phasor_filter_median(
|
3001
|
-
mean: ArrayLike,
|
3002
|
-
real: ArrayLike,
|
3003
|
-
imag: ArrayLike,
|
3004
|
-
/,
|
3005
|
-
*,
|
3006
|
-
repeat: int = 1,
|
3007
|
-
size: int = 3,
|
3008
|
-
skip_axis: int | Sequence[int] | None = None,
|
3009
|
-
use_scipy: bool = False,
|
3010
|
-
num_threads: int | None = None,
|
3011
|
-
**kwargs: Any,
|
3012
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
3013
|
-
"""Return median-filtered phasor coordinates.
|
3014
|
-
|
3015
|
-
By default, apply a NaN-aware median filter independently to the real
|
3016
|
-
and imaginary components of phasor coordinates once with a kernel size of 3
|
3017
|
-
multiplied by the number of dimensions of the input arrays. Return the
|
3018
|
-
intensity unchanged.
|
3019
|
-
|
3020
|
-
Parameters
|
3021
|
-
----------
|
3022
|
-
mean : array_like
|
3023
|
-
Intensity of phasor coordinates.
|
3024
|
-
real : array_like
|
3025
|
-
Real component of phasor coordinates to be filtered.
|
3026
|
-
imag : array_like
|
3027
|
-
Imaginary component of phasor coordinates to be filtered.
|
3028
|
-
repeat : int, optional
|
3029
|
-
Number of times to apply median filter. The default is 1.
|
3030
|
-
size : int, optional
|
3031
|
-
Size of median filter kernel. The default is 3.
|
3032
|
-
skip_axis : int or sequence of int, optional
|
3033
|
-
Axes in `mean` to exclude from filter.
|
3034
|
-
By default, all axes except harmonics are included.
|
3035
|
-
use_scipy : bool, optional
|
3036
|
-
Use :py:func:`scipy.ndimage.median_filter`.
|
3037
|
-
This function has undefined behavior if the input arrays contain
|
3038
|
-
`NaN` values but is faster when filtering more than 2 dimensions.
|
3039
|
-
See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
|
3040
|
-
num_threads : int, optional
|
3041
|
-
Number of OpenMP threads to use for parallelization.
|
3042
|
-
Applies to filtering in two dimensions when not using scipy.
|
3043
|
-
By default, multi-threading is disabled.
|
3044
|
-
If zero, up to half of logical CPUs are used.
|
3045
|
-
OpenMP may not be available on all platforms.
|
3046
|
-
**kwargs
|
3047
|
-
Optional arguments passed to :py:func:`scipy.ndimage.median_filter`.
|
3048
|
-
|
3049
|
-
Returns
|
3050
|
-
-------
|
3051
|
-
mean : ndarray
|
3052
|
-
Unchanged intensity of phasor coordinates.
|
3053
|
-
real : ndarray
|
3054
|
-
Filtered real component of phasor coordinates.
|
3055
|
-
imag : ndarray
|
3056
|
-
Filtered imaginary component of phasor coordinates.
|
3057
|
-
|
3058
|
-
Raises
|
3059
|
-
------
|
3060
|
-
ValueError
|
3061
|
-
If `repeat` is less than 0.
|
3062
|
-
If `size` is less than 1.
|
3063
|
-
The array shapes of `mean`, `real`, and `imag` do not match.
|
3064
|
-
|
3065
|
-
Examples
|
3066
|
-
--------
|
3067
|
-
Apply three times a median filter with a kernel size of three:
|
3068
|
-
|
3069
|
-
>>> mean, real, imag = phasor_filter_median(
|
3070
|
-
... [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
|
3071
|
-
... [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5], [0.2, 0.2, 0.2]],
|
3072
|
-
... [[0.3, 0.3, 0.3], [0.6, math.nan, 0.6], [0.4, 0.4, 0.4]],
|
3073
|
-
... size=3,
|
3074
|
-
... repeat=3,
|
3075
|
-
... )
|
3076
|
-
>>> mean
|
3077
|
-
array([[1, 2, 3],
|
3078
|
-
[4, 5, 6],
|
3079
|
-
[7, 8, 9]])
|
3080
|
-
>>> real
|
3081
|
-
array([[0, 0, 0],
|
3082
|
-
[0.2, 0.2, 0.2],
|
3083
|
-
[0.2, 0.2, 0.2]])
|
3084
|
-
>>> imag
|
3085
|
-
array([[0.3, 0.3, 0.3],
|
3086
|
-
[0.4, nan, 0.4],
|
3087
|
-
[0.4, 0.4, 0.4]])
|
3088
|
-
|
3089
|
-
"""
|
3090
|
-
if repeat < 0:
|
3091
|
-
raise ValueError(f'{repeat=} < 0')
|
3092
|
-
if size < 1:
|
3093
|
-
raise ValueError(f'{size=} < 1')
|
3094
|
-
if size == 1:
|
3095
|
-
# no need to filter
|
3096
|
-
repeat = 0
|
3097
|
-
|
3098
|
-
mean = numpy.asarray(mean)
|
3099
|
-
if use_scipy or repeat == 0: # or using nD numpy filter
|
3100
|
-
real = numpy.asarray(real)
|
3101
|
-
elif isinstance(real, numpy.ndarray) and real.dtype == numpy.float32:
|
3102
|
-
real = real.copy()
|
3103
|
-
else:
|
3104
|
-
real = numpy.array(real, numpy.float64, copy=True)
|
3105
|
-
if use_scipy or repeat == 0: # or using nD numpy filter
|
3106
|
-
imag = numpy.asarray(imag)
|
3107
|
-
elif isinstance(imag, numpy.ndarray) and imag.dtype == numpy.float32:
|
3108
|
-
imag = imag.copy()
|
3109
|
-
else:
|
3110
|
-
imag = numpy.array(imag, numpy.float64, copy=True)
|
3111
|
-
|
3112
|
-
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
3113
|
-
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
3114
|
-
if real.shape != imag.shape:
|
3115
|
-
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
3116
|
-
|
3117
|
-
prepend_axis = mean.ndim + 1 == real.ndim
|
3118
|
-
_, axes = parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
|
3119
|
-
|
3120
|
-
# in case mean is also filtered
|
3121
|
-
# if prepend_axis:
|
3122
|
-
# mean = numpy.expand_dims(mean, axis=0)
|
3123
|
-
# ...
|
3124
|
-
# if prepend_axis:
|
3125
|
-
# mean = numpy.asarray(mean[0])
|
3126
|
-
|
3127
|
-
if repeat == 0:
|
3128
|
-
# no need to call filter
|
3129
|
-
return mean, real, imag
|
3130
|
-
|
3131
|
-
if use_scipy:
|
3132
|
-
# use scipy NaN-unaware fallback
|
3133
|
-
from scipy.ndimage import median_filter
|
3134
|
-
|
3135
|
-
kwargs.pop('axes', None)
|
3136
|
-
|
3137
|
-
for _ in range(repeat):
|
3138
|
-
real = median_filter(real, size=size, axes=axes, **kwargs)
|
3139
|
-
imag = median_filter(imag, size=size, axes=axes, **kwargs)
|
3140
|
-
|
3141
|
-
return mean, numpy.asarray(real), numpy.asarray(imag)
|
3142
|
-
|
3143
|
-
if len(axes) != 2:
|
3144
|
-
# n-dimensional median filter using numpy
|
3145
|
-
from numpy.lib.stride_tricks import sliding_window_view
|
3146
|
-
|
3147
|
-
kernel_shape = tuple(
|
3148
|
-
size if i in axes else 1 for i in range(real.ndim)
|
3149
|
-
)
|
3150
|
-
pad_width = [
|
3151
|
-
(s // 2, s // 2) if s > 1 else (0, 0) for s in kernel_shape
|
3152
|
-
]
|
3153
|
-
axis = tuple(range(-real.ndim, 0))
|
3154
|
-
|
3155
|
-
nan_mask = numpy.isnan(real)
|
3156
|
-
for _ in range(repeat):
|
3157
|
-
real = numpy.pad(real, pad_width, mode='edge')
|
3158
|
-
real = sliding_window_view(real, kernel_shape)
|
3159
|
-
real = numpy.nanmedian(real, axis=axis)
|
3160
|
-
real = numpy.where(nan_mask, numpy.nan, real)
|
3161
|
-
|
3162
|
-
nan_mask = numpy.isnan(imag)
|
3163
|
-
for _ in range(repeat):
|
3164
|
-
imag = numpy.pad(imag, pad_width, mode='edge')
|
3165
|
-
imag = sliding_window_view(imag, kernel_shape)
|
3166
|
-
imag = numpy.nanmedian(imag, axis=axis)
|
3167
|
-
imag = numpy.where(nan_mask, numpy.nan, imag)
|
3168
|
-
|
3169
|
-
return mean, real, imag
|
3170
|
-
|
3171
|
-
# 2-dimensional median filter using optimized Cython implementation
|
3172
|
-
num_threads = number_threads(num_threads)
|
3173
|
-
|
3174
|
-
buffer = numpy.empty(
|
3175
|
-
tuple(real.shape[axis] for axis in axes), dtype=real.dtype
|
3176
|
-
)
|
3177
|
-
|
3178
|
-
for index in numpy.ndindex(
|
3179
|
-
*[real.shape[ax] for ax in range(real.ndim) if ax not in axes]
|
3180
|
-
):
|
3181
|
-
index_list: list[int | slice] = list(index)
|
3182
|
-
for ax in axes:
|
3183
|
-
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
3184
|
-
full_index = tuple(index_list)
|
3185
|
-
|
3186
|
-
_median_filter_2d(real[full_index], buffer, size, repeat, num_threads)
|
3187
|
-
_median_filter_2d(imag[full_index], buffer, size, repeat, num_threads)
|
3188
|
-
|
3189
|
-
return mean, real, imag
|
3190
|
-
|
3191
|
-
|
3192
|
-
def phasor_filter_pawflim(
|
3193
|
-
mean: ArrayLike,
|
3194
|
-
real: ArrayLike,
|
3195
|
-
imag: ArrayLike,
|
3196
|
-
/,
|
3197
|
-
*,
|
3198
|
-
sigma: float = 2.0,
|
3199
|
-
levels: int = 1,
|
3200
|
-
harmonic: Sequence[int] | None = None,
|
3201
|
-
skip_axis: int | Sequence[int] | None = None,
|
3202
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
3203
|
-
"""Return pawFLIM wavelet-filtered phasor coordinates.
|
3204
|
-
|
3205
|
-
This function must only be used with calibrated, unprocessed phasor
|
3206
|
-
coordinates obtained from FLIM data. The coordinates must not be filtered,
|
3207
|
-
thresholded, or otherwise pre-processed.
|
3208
|
-
|
3209
|
-
The pawFLIM wavelet filter is described in [2]_.
|
3210
|
-
|
3211
|
-
Parameters
|
3212
|
-
----------
|
3213
|
-
mean : array_like
|
3214
|
-
Intensity of phasor coordinates.
|
3215
|
-
real : array_like
|
3216
|
-
Real component of phasor coordinates to be filtered.
|
3217
|
-
Must have at least two harmonics in the first axis.
|
3218
|
-
imag : array_like
|
3219
|
-
Imaginary component of phasor coordinates to be filtered.
|
3220
|
-
Must have at least two harmonics in the first axis.
|
3221
|
-
sigma : float, optional
|
3222
|
-
Significance level to test difference between two phasors.
|
3223
|
-
Given in terms of the equivalent 1D standard deviations.
|
3224
|
-
sigma=2 corresponds to ~95% (or 5%) significance.
|
3225
|
-
levels : int, optional
|
3226
|
-
Number of levels for wavelet decomposition.
|
3227
|
-
Controls the maximum averaging area, which has a length of
|
3228
|
-
:math:`2^level`.
|
3229
|
-
harmonic : sequence of int or None, optional
|
3230
|
-
Harmonics included in first axis of `real` and `imag`.
|
3231
|
-
If None (default), the first axis of `real` and `imag` contains lower
|
3232
|
-
harmonics starting at and increasing by one.
|
3233
|
-
All harmonics must have a corresponding half or double harmonic.
|
3234
|
-
skip_axis : int or sequence of int, optional
|
3235
|
-
Axes in `mean` to exclude from filter.
|
3236
|
-
By default, all axes except harmonics are included.
|
3237
|
-
|
3238
|
-
Returns
|
3239
|
-
-------
|
3240
|
-
mean : ndarray
|
3241
|
-
Unchanged intensity of phasor coordinates.
|
3242
|
-
real : ndarray
|
3243
|
-
Filtered real component of phasor coordinates.
|
3244
|
-
imag : ndarray
|
3245
|
-
Filtered imaginary component of phasor coordinates.
|
3246
|
-
|
3247
|
-
Raises
|
3248
|
-
------
|
3249
|
-
ValueError
|
3250
|
-
If `level` is less than 0.
|
3251
|
-
The array shapes of `mean`, `real`, and `imag` do not match.
|
3252
|
-
If `real` and `imag` have no harmonic axis.
|
3253
|
-
Number of harmonics in `harmonic` is less than 2 or does not match
|
3254
|
-
the first axis of `real` and `imag`.
|
3255
|
-
Not all harmonics in `harmonic` have a corresponding half
|
3256
|
-
or double harmonic.
|
3257
|
-
|
3258
|
-
References
|
3259
|
-
----------
|
3260
|
-
|
3261
|
-
.. [2] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
|
3262
|
-
uncertainty to enable lower photon count in FLIM experiments
|
3263
|
-
<https://doi.org/10.1088/2050-6120/aa72ab>`_.
|
3264
|
-
*Methods Appl Fluoresc*, 5(2): 024016 (2017)
|
3265
|
-
|
3266
|
-
Examples
|
3267
|
-
--------
|
3268
|
-
Apply a pawFLIM wavelet filter with four significance levels (sigma)
|
3269
|
-
and three decomposition levels:
|
3270
|
-
|
3271
|
-
>>> mean, real, imag = phasor_filter_pawflim(
|
3272
|
-
... [[1, 1], [1, 1]],
|
3273
|
-
... [[[0.5, 0.8], [0.5, 0.8]], [[0.2, 0.4], [0.2, 0.4]]],
|
3274
|
-
... [[[0.5, 0.4], [0.5, 0.4]], [[0.4, 0.5], [0.4, 0.5]]],
|
3275
|
-
... sigma=4,
|
3276
|
-
... levels=3,
|
3277
|
-
... harmonic=[1, 2],
|
3278
|
-
... )
|
3279
|
-
>>> mean
|
3280
|
-
array([[1, 1],
|
3281
|
-
[1, 1]])
|
3282
|
-
>>> real
|
3283
|
-
array([[[0.65, 0.65],
|
3284
|
-
[0.65, 0.65]],
|
3285
|
-
[[0.3, 0.3],
|
3286
|
-
[0.3, 0.3]]])
|
3287
|
-
>>> imag
|
3288
|
-
array([[[0.45, 0.45],
|
3289
|
-
[0.45, 0.45]],
|
3290
|
-
[[0.45, 0.45],
|
3291
|
-
[0.45, 0.45]]])
|
3292
|
-
|
3293
|
-
"""
|
3294
|
-
from pawflim import pawflim # type: ignore[import-untyped]
|
3295
|
-
|
3296
|
-
mean = numpy.asarray(mean)
|
3297
|
-
real = numpy.asarray(real)
|
3298
|
-
imag = numpy.asarray(imag)
|
3299
|
-
|
3300
|
-
if levels < 0:
|
3301
|
-
raise ValueError(f'{levels=} < 0')
|
3302
|
-
if levels == 0:
|
3303
|
-
return mean, real, imag
|
3304
|
-
|
3305
|
-
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
3306
|
-
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
3307
|
-
if real.shape != imag.shape:
|
3308
|
-
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
3309
|
-
|
3310
|
-
has_harmonic_axis = mean.ndim + 1 == real.ndim
|
3311
|
-
if not has_harmonic_axis:
|
3312
|
-
raise ValueError('no harmonic axis')
|
3313
|
-
if harmonic is None:
|
3314
|
-
harmonics, _ = parse_harmonic('all', real.shape[0])
|
3315
|
-
else:
|
3316
|
-
harmonics, _ = parse_harmonic(harmonic, None)
|
3317
|
-
if len(harmonics) < 2:
|
3318
|
-
raise ValueError(
|
3319
|
-
'at least two harmonics required, ' f'got {len(harmonics)}'
|
3320
|
-
)
|
3321
|
-
if len(harmonics) != real.shape[0]:
|
3322
|
-
raise ValueError(
|
3323
|
-
'number of harmonics does not match first axis of real and imag'
|
3324
|
-
)
|
3325
|
-
|
3326
|
-
mean = numpy.asarray(numpy.nan_to_num(mean), dtype=float)
|
3327
|
-
real = numpy.asarray(numpy.nan_to_num(real * mean), dtype=float)
|
3328
|
-
imag = numpy.asarray(numpy.nan_to_num(imag * mean), dtype=float)
|
3329
|
-
|
3330
|
-
mean_expanded = numpy.broadcast_to(mean, real.shape).copy()
|
3331
|
-
original_mean_expanded = mean_expanded.copy()
|
3332
|
-
real_filtered = real.copy()
|
3333
|
-
imag_filtered = imag.copy()
|
3334
|
-
|
3335
|
-
_, axes = parse_skip_axis(skip_axis, mean.ndim, True)
|
3336
|
-
|
3337
|
-
for index in numpy.ndindex(
|
3338
|
-
*(
|
3339
|
-
real.shape[ax]
|
3340
|
-
for ax in range(real.ndim)
|
3341
|
-
if ax not in axes and ax != 0
|
3342
|
-
)
|
3343
|
-
):
|
3344
|
-
index_list: list[int | slice] = list(index)
|
3345
|
-
for ax in axes:
|
3346
|
-
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
3347
|
-
full_index = tuple(index_list)
|
3348
|
-
|
3349
|
-
processed_harmonics = set()
|
3350
|
-
|
3351
|
-
for h in harmonics:
|
3352
|
-
if h in processed_harmonics and (
|
3353
|
-
h * 4 in harmonics or h * 2 not in harmonics
|
3354
|
-
):
|
3355
|
-
continue
|
3356
|
-
if h * 2 not in harmonics:
|
3357
|
-
raise ValueError(
|
3358
|
-
f'harmonic {h} does not have a corresponding half '
|
3359
|
-
f'or double harmonic in {harmonics}'
|
3360
|
-
)
|
3361
|
-
n = harmonics.index(h)
|
3362
|
-
n2 = harmonics.index(h * 2)
|
3363
|
-
|
3364
|
-
complex_phasor = numpy.empty(
|
3365
|
-
(3, *original_mean_expanded[n][full_index].shape),
|
3366
|
-
dtype=complex,
|
3367
|
-
)
|
3368
|
-
complex_phasor[0] = original_mean_expanded[n][full_index]
|
3369
|
-
complex_phasor[1] = real[n][full_index] + 1j * imag[n][full_index]
|
3370
|
-
complex_phasor[2] = (
|
3371
|
-
real[n2][full_index] + 1j * imag[n2][full_index]
|
3372
|
-
)
|
3373
|
-
|
3374
|
-
complex_phasor = pawflim(
|
3375
|
-
complex_phasor, n_sigmas=sigma, levels=levels
|
3376
|
-
)
|
3377
|
-
|
3378
|
-
for i, idx in enumerate([n, n2]):
|
3379
|
-
if harmonics[idx] in processed_harmonics:
|
3380
|
-
continue
|
3381
|
-
mean_expanded[idx][full_index] = complex_phasor[0].real
|
3382
|
-
real_filtered[idx][full_index] = complex_phasor[i + 1].real
|
3383
|
-
imag_filtered[idx][full_index] = complex_phasor[i + 1].imag
|
3384
|
-
|
3385
|
-
processed_harmonics.add(h)
|
3386
|
-
processed_harmonics.add(h * 2)
|
3387
|
-
|
3388
|
-
with numpy.errstate(divide='ignore', invalid='ignore'):
|
3389
|
-
real = numpy.asarray(numpy.divide(real_filtered, mean_expanded))
|
3390
|
-
imag = numpy.asarray(numpy.divide(imag_filtered, mean_expanded))
|
3391
|
-
|
3392
|
-
return mean, real, imag
|
3393
|
-
|
3394
|
-
|
3395
|
-
def phasor_threshold(
|
3396
|
-
mean: ArrayLike,
|
3397
|
-
real: ArrayLike,
|
3398
|
-
imag: ArrayLike,
|
3399
|
-
/,
|
3400
|
-
mean_min: ArrayLike | None = None,
|
3401
|
-
mean_max: ArrayLike | None = None,
|
3402
|
-
*,
|
3403
|
-
real_min: ArrayLike | None = None,
|
3404
|
-
real_max: ArrayLike | None = None,
|
3405
|
-
imag_min: ArrayLike | None = None,
|
3406
|
-
imag_max: ArrayLike | None = None,
|
3407
|
-
phase_min: ArrayLike | None = None,
|
3408
|
-
phase_max: ArrayLike | None = None,
|
3409
|
-
modulation_min: ArrayLike | None = None,
|
3410
|
-
modulation_max: ArrayLike | None = None,
|
3411
|
-
open_interval: bool = False,
|
3412
|
-
detect_harmonics: bool = True,
|
3413
|
-
**kwargs: Any,
|
3414
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
3415
|
-
"""Return phasor coordinates with values out of interval replaced by NaN.
|
3416
|
-
|
3417
|
-
Interval thresholds can be set for mean intensity, real and imaginary
|
3418
|
-
coordinates, and phase and modulation.
|
3419
|
-
Phasor coordinates smaller than minimum thresholds or larger than maximum
|
3420
|
-
thresholds are replaced NaN.
|
3421
|
-
No threshold is applied by default.
|
3422
|
-
NaNs in `mean` or any `real` and `imag` harmonic are propagated to
|
3423
|
-
`mean` and all harmonics in `real` and `imag`.
|
3424
|
-
|
3425
|
-
Parameters
|
3426
|
-
----------
|
3427
|
-
mean : array_like
|
3428
|
-
Intensity of phasor coordinates.
|
3429
|
-
real : array_like
|
3430
|
-
Real component of phasor coordinates.
|
3431
|
-
imag : array_like
|
3432
|
-
Imaginary component of phasor coordinates.
|
3433
|
-
mean_min : array_like, optional
|
3434
|
-
Lower threshold for mean intensity.
|
3435
|
-
mean_max : array_like, optional
|
3436
|
-
Upper threshold for mean intensity.
|
3437
|
-
real_min : array_like, optional
|
3438
|
-
Lower threshold for real coordinates.
|
3439
|
-
real_max : array_like, optional
|
3440
|
-
Upper threshold for real coordinates.
|
3441
|
-
imag_min : array_like, optional
|
3442
|
-
Lower threshold for imaginary coordinates.
|
3443
|
-
imag_max : array_like, optional
|
3444
|
-
Upper threshold for imaginary coordinates.
|
3445
|
-
phase_min : array_like, optional
|
3446
|
-
Lower threshold for phase angle.
|
3447
|
-
phase_max : array_like, optional
|
3448
|
-
Upper threshold for phase angle.
|
3449
|
-
modulation_min : array_like, optional
|
3450
|
-
Lower threshold for modulation.
|
3451
|
-
modulation_max : array_like, optional
|
3452
|
-
Upper threshold for modulation.
|
3453
|
-
open_interval : bool, optional
|
3454
|
-
If true, the interval is open, and the threshold values are
|
3455
|
-
not included in the interval.
|
3456
|
-
If false (default), the interval is closed, and the threshold values
|
3457
|
-
are included in the interval.
|
3458
|
-
detect_harmonics : bool, optional
|
3459
|
-
By default, detect presence of multiple harmonics from array shapes.
|
3460
|
-
If false, no harmonics are assumed to be present, and the function
|
3461
|
-
behaves like a numpy universal function.
|
3462
|
-
**kwargs
|
3463
|
-
Optional `arguments passed to numpy universal functions
|
3464
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
3465
|
-
|
3466
|
-
Returns
|
3467
|
-
-------
|
3468
|
-
mean : ndarray
|
3469
|
-
Thresholded intensity of phasor coordinates.
|
3470
|
-
real : ndarray
|
3471
|
-
Thresholded real component of phasor coordinates.
|
3472
|
-
imag : ndarray
|
3473
|
-
Thresholded imaginary component of phasor coordinates.
|
3474
|
-
|
3475
|
-
Examples
|
3476
|
-
--------
|
3477
|
-
Set phasor coordinates to NaN if mean intensity is smaller than 1.1:
|
3478
|
-
|
3479
|
-
>>> phasor_threshold([1, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], 1.1)
|
3480
|
-
(array([nan, 2, 3]), array([nan, 0.2, 0.3]), array([nan, 0.5, 0.6]))
|
3481
|
-
|
3482
|
-
Set phasor coordinates to NaN if real component is smaller than 0.15 or
|
3483
|
-
larger than 0.25:
|
3484
|
-
|
3485
|
-
>>> phasor_threshold(
|
3486
|
-
... [1.0, 2.0, 3.0],
|
3487
|
-
... [0.1, 0.2, 0.3],
|
3488
|
-
... [0.4, 0.5, 0.6],
|
3489
|
-
... real_min=0.15,
|
3490
|
-
... real_max=0.25,
|
3491
|
-
... )
|
3492
|
-
(array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
|
3493
|
-
|
3494
|
-
Apply NaNs to other input arrays:
|
3495
|
-
|
3496
|
-
>>> phasor_threshold(
|
3497
|
-
... [numpy.nan, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, numpy.nan]
|
3498
|
-
... )
|
3499
|
-
(array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
|
3500
|
-
|
3501
|
-
"""
|
3502
|
-
threshold_mean_only = None
|
3503
|
-
if mean_min is None:
|
3504
|
-
mean_min = numpy.nan
|
3505
|
-
else:
|
3506
|
-
threshold_mean_only = True
|
3507
|
-
if mean_max is None:
|
3508
|
-
mean_max = numpy.nan
|
3509
|
-
else:
|
3510
|
-
threshold_mean_only = True
|
3511
|
-
if real_min is None:
|
3512
|
-
real_min = numpy.nan
|
3513
|
-
else:
|
3514
|
-
threshold_mean_only = False
|
3515
|
-
if real_max is None:
|
3516
|
-
real_max = numpy.nan
|
3517
|
-
else:
|
3518
|
-
threshold_mean_only = False
|
3519
|
-
if imag_min is None:
|
3520
|
-
imag_min = numpy.nan
|
3521
|
-
else:
|
3522
|
-
threshold_mean_only = False
|
3523
|
-
if imag_max is None:
|
3524
|
-
imag_max = numpy.nan
|
3525
|
-
else:
|
3526
|
-
threshold_mean_only = False
|
3527
|
-
if phase_min is None:
|
3528
|
-
phase_min = numpy.nan
|
3529
|
-
else:
|
3530
|
-
threshold_mean_only = False
|
3531
|
-
if phase_max is None:
|
3532
|
-
phase_max = numpy.nan
|
3533
|
-
else:
|
3534
|
-
threshold_mean_only = False
|
3535
|
-
if modulation_min is None:
|
3536
|
-
modulation_min = numpy.nan
|
3537
|
-
else:
|
3538
|
-
threshold_mean_only = False
|
3539
|
-
if modulation_max is None:
|
3540
|
-
modulation_max = numpy.nan
|
3541
|
-
else:
|
3542
|
-
threshold_mean_only = False
|
3543
|
-
|
3544
|
-
if detect_harmonics:
|
3545
|
-
mean = numpy.asarray(mean)
|
3546
|
-
real = numpy.asarray(real)
|
3547
|
-
imag = numpy.asarray(imag)
|
3548
|
-
|
3549
|
-
shape = numpy.broadcast_shapes(mean.shape, real.shape, imag.shape)
|
3550
|
-
ndim = len(shape)
|
3551
|
-
|
3552
|
-
has_harmonic_axis = (
|
3553
|
-
# detect multi-harmonic in axis 0
|
3554
|
-
mean.ndim + 1 == ndim
|
3555
|
-
and real.shape == shape
|
3556
|
-
and imag.shape == shape
|
3557
|
-
and mean.shape == shape[-mean.ndim if mean.ndim else 1 :]
|
3558
|
-
)
|
3559
|
-
else:
|
3560
|
-
has_harmonic_axis = False
|
3561
|
-
|
3562
|
-
if threshold_mean_only is None:
|
3563
|
-
mean, real, imag = _phasor_threshold_nan(mean, real, imag, **kwargs)
|
3564
|
-
|
3565
|
-
elif threshold_mean_only:
|
3566
|
-
mean_func = (
|
3567
|
-
_phasor_threshold_mean_open
|
3568
|
-
if open_interval
|
3569
|
-
else _phasor_threshold_mean_closed
|
3570
|
-
)
|
3571
|
-
mean, real, imag = mean_func(
|
3572
|
-
mean, real, imag, mean_min, mean_max, **kwargs
|
3573
|
-
)
|
3574
|
-
|
3575
|
-
else:
|
3576
|
-
func = (
|
3577
|
-
_phasor_threshold_open
|
3578
|
-
if open_interval
|
3579
|
-
else _phasor_threshold_closed
|
3580
|
-
)
|
3581
|
-
mean, real, imag = func(
|
3582
|
-
mean,
|
3583
|
-
real,
|
3584
|
-
imag,
|
3585
|
-
mean_min,
|
3586
|
-
mean_max,
|
3587
|
-
real_min,
|
3588
|
-
real_max,
|
3589
|
-
imag_min,
|
3590
|
-
imag_max,
|
3591
|
-
phase_min,
|
3592
|
-
phase_max,
|
3593
|
-
modulation_min,
|
3594
|
-
modulation_max,
|
3595
|
-
**kwargs,
|
3596
|
-
)
|
3597
|
-
|
3598
|
-
mean = numpy.asarray(mean)
|
3599
|
-
real = numpy.asarray(real)
|
3600
|
-
imag = numpy.asarray(imag)
|
3601
|
-
if has_harmonic_axis and mean.ndim > 0:
|
3602
|
-
# propagate NaN to all dimensions
|
3603
|
-
mean = numpy.mean(mean, axis=0, keepdims=True)
|
3604
|
-
mask = numpy.where(numpy.isnan(mean), numpy.nan, 1.0)
|
3605
|
-
numpy.multiply(real, mask, out=real)
|
3606
|
-
numpy.multiply(imag, mask, out=imag)
|
3607
|
-
# remove harmonic dimension created by broadcasting
|
3608
|
-
mean = numpy.asarray(numpy.asarray(mean)[0])
|
3609
|
-
|
3610
|
-
return mean, real, imag
|
3611
|
-
|
3612
|
-
|
3613
1217
|
def phasor_nearest_neighbor(
|
3614
1218
|
real: ArrayLike,
|
3615
1219
|
imag: ArrayLike,
|
@@ -3622,7 +1226,7 @@ def phasor_nearest_neighbor(
|
|
3622
1226
|
distance_max: float | None = None,
|
3623
1227
|
num_threads: int | None = None,
|
3624
1228
|
) -> NDArray[Any]:
|
3625
|
-
"""Return indices or values of nearest
|
1229
|
+
"""Return indices or values of nearest neighbors from other coordinates.
|
3626
1230
|
|
3627
1231
|
For each phasor coordinate, find the nearest neighbor in another set of
|
3628
1232
|
phasor coordinates and return its flat index. If more than one neighbor
|
@@ -3723,7 +1327,7 @@ def phasor_nearest_neighbor(
|
|
3723
1327
|
neighbor_imag = neighbor_imag.ravel()
|
3724
1328
|
|
3725
1329
|
indices = numpy.empty(
|
3726
|
-
real.shape, numpy.min_scalar_type(-neighbor_real.size)
|
1330
|
+
real.shape, dtype=numpy.min_scalar_type(-neighbor_real.size)
|
3727
1331
|
)
|
3728
1332
|
|
3729
1333
|
if distance_max is None:
|