phasorpy 0.5__cp313-cp313-win_arm64.whl → 0.7__cp313-cp313-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 +2 -3
- phasorpy/_phasorpy.cp313-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +466 -11
- phasorpy/_utils.py +222 -37
- phasorpy/cli.py +74 -3
- phasorpy/cluster.py +51 -21
- phasorpy/color.py +11 -7
- phasorpy/component.py +707 -0
- phasorpy/{cursors.py → cursor.py} +31 -33
- phasorpy/datasets.py +117 -7
- phasorpy/experimental.py +310 -0
- phasorpy/io/__init__.py +138 -0
- phasorpy/io/_flimlabs.py +360 -0
- phasorpy/io/_leica.py +331 -0
- phasorpy/io/_ometiff.py +444 -0
- phasorpy/io/_other.py +890 -0
- phasorpy/io/_simfcs.py +652 -0
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +184 -1754
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +723 -0
- phasorpy/plot/_lifetime_plots.py +563 -0
- phasorpy/plot/_phasorplot.py +1507 -0
- phasorpy/plot/_phasorplot_fret.py +561 -0
- phasorpy/utils.py +89 -290
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/METADATA +3 -3
- phasorpy-0.7.dist-info/RECORD +35 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/WHEEL +1 -1
- phasorpy/_io.py +0 -2655
- phasorpy/components.py +0 -313
- phasorpy/io.py +0 -9
- phasorpy/plot.py +0 -2318
- phasorpy/version.py +0 -80
- phasorpy-0.5.dist-info/RECORD +0 -26
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.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,24 +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
12
|
|
21
13
|
- convert to and from polar coordinates (phase and modulation):
|
22
14
|
|
23
15
|
- :py:func:`phasor_from_polar`
|
24
16
|
- :py:func:`phasor_to_polar`
|
25
|
-
- :py:func:`polar_from_apparent_lifetime`
|
26
|
-
- :py:func:`polar_to_apparent_lifetime`
|
27
17
|
|
28
18
|
- transform phasor coordinates:
|
29
19
|
|
@@ -32,79 +22,41 @@ The ``phasorpy.phasor`` module provides functions to:
|
|
32
22
|
- :py:func:`phasor_divide`
|
33
23
|
- :py:func:`phasor_normalize`
|
34
24
|
|
35
|
-
- calibrate phasor coordinates with reference of known fluorescence
|
36
|
-
lifetime:
|
37
|
-
|
38
|
-
- :py:func:`phasor_calibrate`
|
39
|
-
- :py:func:`polar_from_reference`
|
40
|
-
- :py:func:`polar_from_reference_phasor`
|
41
|
-
|
42
25
|
- reduce dimensionality of arrays of phasor coordinates:
|
43
26
|
|
44
27
|
- :py:func:`phasor_center`
|
45
28
|
- :py:func:`phasor_to_principal_plane`
|
46
29
|
|
47
|
-
- calculate phasor coordinates for FRET donor and acceptor channels:
|
48
|
-
|
49
|
-
- :py:func:`phasor_from_fret_donor`
|
50
|
-
- :py:func:`phasor_from_fret_acceptor`
|
51
|
-
|
52
|
-
- convert between single component lifetimes and optimal frequency:
|
53
|
-
|
54
|
-
- :py:func:`lifetime_to_frequency`
|
55
|
-
- :py:func:`lifetime_from_frequency`
|
56
|
-
|
57
|
-
- convert between fractional intensities and pre-exponential amplitudes:
|
58
|
-
|
59
|
-
- :py:func:`lifetime_fraction_from_amplitude`
|
60
|
-
- :py:func:`lifetime_fraction_to_amplitude`
|
61
|
-
|
62
|
-
- calculate phasor coordinates on universal semicircle at other harmonics:
|
63
|
-
|
64
|
-
- :py:func:`phasor_at_harmonic`
|
65
|
-
|
66
30
|
- filter phasor coordinates:
|
67
31
|
|
68
32
|
- :py:func:`phasor_filter_median`
|
69
33
|
- :py:func:`phasor_filter_pawflim`
|
70
34
|
- :py:func:`phasor_threshold`
|
71
35
|
|
36
|
+
- find nearest neighbor phasor coordinates from other phasor coordinates:
|
37
|
+
|
38
|
+
- :py:func:`phasor_nearest_neighbor`
|
39
|
+
|
72
40
|
"""
|
73
41
|
|
74
42
|
from __future__ import annotations
|
75
43
|
|
76
44
|
__all__ = [
|
77
|
-
'lifetime_fraction_from_amplitude',
|
78
|
-
'lifetime_fraction_to_amplitude',
|
79
|
-
'lifetime_from_frequency',
|
80
|
-
'lifetime_to_frequency',
|
81
|
-
'lifetime_to_signal',
|
82
|
-
'phasor_at_harmonic',
|
83
|
-
'phasor_calibrate',
|
84
45
|
'phasor_center',
|
85
46
|
'phasor_divide',
|
86
47
|
'phasor_filter_median',
|
87
48
|
'phasor_filter_pawflim',
|
88
|
-
'phasor_from_apparent_lifetime',
|
89
|
-
'phasor_from_fret_acceptor',
|
90
|
-
'phasor_from_fret_donor',
|
91
|
-
'phasor_from_lifetime',
|
92
49
|
'phasor_from_polar',
|
93
50
|
'phasor_from_signal',
|
94
51
|
'phasor_multiply',
|
52
|
+
'phasor_nearest_neighbor',
|
95
53
|
'phasor_normalize',
|
96
|
-
'phasor_semicircle',
|
97
54
|
'phasor_threshold',
|
98
|
-
'phasor_to_apparent_lifetime',
|
99
55
|
'phasor_to_complex',
|
100
56
|
'phasor_to_polar',
|
101
57
|
'phasor_to_principal_plane',
|
102
58
|
'phasor_to_signal',
|
103
59
|
'phasor_transform',
|
104
|
-
'polar_from_apparent_lifetime',
|
105
|
-
'polar_from_reference',
|
106
|
-
'polar_from_reference_phasor',
|
107
|
-
'polar_to_apparent_lifetime',
|
108
60
|
]
|
109
61
|
|
110
62
|
import math
|
@@ -124,32 +76,20 @@ if TYPE_CHECKING:
|
|
124
76
|
import numpy
|
125
77
|
|
126
78
|
from ._phasorpy import (
|
127
|
-
_gaussian_signal,
|
128
79
|
_median_filter_2d,
|
129
|
-
|
80
|
+
_nearest_neighbor_2d,
|
130
81
|
_phasor_divide,
|
131
|
-
_phasor_from_apparent_lifetime,
|
132
|
-
_phasor_from_fret_acceptor,
|
133
|
-
_phasor_from_fret_donor,
|
134
|
-
_phasor_from_lifetime,
|
135
82
|
_phasor_from_polar,
|
136
83
|
_phasor_from_signal,
|
137
|
-
_phasor_from_single_lifetime,
|
138
84
|
_phasor_multiply,
|
139
85
|
_phasor_threshold_closed,
|
140
86
|
_phasor_threshold_mean_closed,
|
141
87
|
_phasor_threshold_mean_open,
|
142
88
|
_phasor_threshold_nan,
|
143
89
|
_phasor_threshold_open,
|
144
|
-
_phasor_to_apparent_lifetime,
|
145
90
|
_phasor_to_polar,
|
146
91
|
_phasor_transform,
|
147
92
|
_phasor_transform_const,
|
148
|
-
_polar_from_apparent_lifetime,
|
149
|
-
_polar_from_reference,
|
150
|
-
_polar_from_reference_phasor,
|
151
|
-
_polar_from_single_lifetime,
|
152
|
-
_polar_to_apparent_lifetime,
|
153
93
|
)
|
154
94
|
from ._utils import parse_harmonic, parse_signal_axis, parse_skip_axis
|
155
95
|
from .utils import number_threads
|
@@ -246,13 +186,13 @@ def phasor_from_signal(
|
|
246
186
|
--------
|
247
187
|
phasorpy.phasor.phasor_to_signal
|
248
188
|
phasorpy.phasor.phasor_normalize
|
249
|
-
:ref:`
|
189
|
+
:ref:`sphx_glr_tutorials_misc_phasorpy_phasor_from_signal.py`
|
250
190
|
|
251
191
|
Notes
|
252
192
|
-----
|
253
193
|
The normalized phasor coordinates `real` (:math:`G`), `imag` (:math:`S`),
|
254
194
|
and average intensity `mean` (:math:`F_{DC}`) are calculated from
|
255
|
-
:math:`K\
|
195
|
+
:math:`K \ge 3` samples of the signal :math:`F` at `harmonic` :math:`h`
|
256
196
|
according to:
|
257
197
|
|
258
198
|
.. math::
|
@@ -266,8 +206,8 @@ def phasor_from_signal(
|
|
266
206
|
\sin{\left (2 \pi h \frac{k}{K} \right )} \cdot \frac{1}{F_{DC}}
|
267
207
|
|
268
208
|
If :math:`F_{DC} = 0`, the phasor coordinates are undefined
|
269
|
-
(
|
270
|
-
Use
|
209
|
+
(resulting in NaN or infinity).
|
210
|
+
Use NaN-aware software to further process the phasor coordinates.
|
271
211
|
|
272
212
|
The phasor coordinates may be zero, for example, in case of only constant
|
273
213
|
background in time-resolved signals, or as the result of linear
|
@@ -461,7 +401,7 @@ def phasor_to_signal(
|
|
461
401
|
Notes
|
462
402
|
-----
|
463
403
|
The reconstructed signal may be undefined if the input phasor coordinates,
|
464
|
-
or signal mean contain
|
404
|
+
or signal mean contain NaN values.
|
465
405
|
|
466
406
|
Examples
|
467
407
|
--------
|
@@ -552,248 +492,6 @@ def phasor_to_signal(
|
|
552
492
|
return signal
|
553
493
|
|
554
494
|
|
555
|
-
def lifetime_to_signal(
|
556
|
-
frequency: float,
|
557
|
-
lifetime: ArrayLike,
|
558
|
-
fraction: ArrayLike | None = None,
|
559
|
-
*,
|
560
|
-
mean: ArrayLike | None = None,
|
561
|
-
background: ArrayLike | None = None,
|
562
|
-
samples: int = 64,
|
563
|
-
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
564
|
-
zero_phase: float | None = None,
|
565
|
-
zero_stdev: float | None = None,
|
566
|
-
preexponential: bool = False,
|
567
|
-
unit_conversion: float = 1e-3,
|
568
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
569
|
-
r"""Return synthetic signal from lifetime components.
|
570
|
-
|
571
|
-
Return synthetic signal, instrument response function (IRF), and
|
572
|
-
time axis, sampled over one period of the fundamental frequency.
|
573
|
-
The signal is convoluted with the IRF, which is approximated by a
|
574
|
-
normal distribution.
|
575
|
-
|
576
|
-
Parameters
|
577
|
-
----------
|
578
|
-
frequency : float
|
579
|
-
Fundamental laser pulse or modulation frequency in MHz.
|
580
|
-
lifetime : array_like
|
581
|
-
Lifetime components in ns.
|
582
|
-
fraction : array_like, optional
|
583
|
-
Fractional intensities or pre-exponential amplitudes of the lifetime
|
584
|
-
components. Fractions are normalized to sum to 1.
|
585
|
-
Must be specified if `lifetime` is not a scalar.
|
586
|
-
mean : array_like, optional, default: 1.0
|
587
|
-
Average signal intensity (DC). Must be scalar for now.
|
588
|
-
background : array_like, optional, default: 0.0
|
589
|
-
Background signal intensity. Must be smaller than `mean`.
|
590
|
-
samples : int, default: 64
|
591
|
-
Number of signal samples to return. Must be at least 16.
|
592
|
-
harmonic : int, sequence of int, or 'all', optional, default: 'all'
|
593
|
-
Harmonics used to synthesize signal.
|
594
|
-
If `'all'`, all harmonics are used.
|
595
|
-
Else, harmonics must be at least one and no larger than half of
|
596
|
-
`samples`.
|
597
|
-
Use `'all'` to synthesize an exponential time-domain decay signal,
|
598
|
-
or `1` to synthesize a homodyne signal.
|
599
|
-
zero_phase : float, optional
|
600
|
-
Position of instrument response function in radians.
|
601
|
-
Must be in range [0, pi]. The default is the 8th sample.
|
602
|
-
zero_stdev : float, optional
|
603
|
-
Standard deviation of instrument response function in radians.
|
604
|
-
Must be at least 1.5 samples and no more than one tenth of samples
|
605
|
-
to allow for sufficient sampling of the function.
|
606
|
-
The default is 1.5 samples. Increase `samples` to narrow the IRF.
|
607
|
-
preexponential : bool, optional, default: False
|
608
|
-
If true, `fraction` values are pre-exponential amplitudes,
|
609
|
-
else fractional intensities.
|
610
|
-
unit_conversion : float, optional, default: 1e-3
|
611
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
612
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
613
|
-
Use 1.0 for Hz and s.
|
614
|
-
|
615
|
-
Returns
|
616
|
-
-------
|
617
|
-
signal : ndarray
|
618
|
-
Signal generated from lifetimes at frequency, convoluted with
|
619
|
-
instrument response function.
|
620
|
-
zero : ndarray
|
621
|
-
Instrument response function.
|
622
|
-
time : ndarray
|
623
|
-
Time for each sample in signal in units of `lifetime`.
|
624
|
-
|
625
|
-
See Also
|
626
|
-
--------
|
627
|
-
phasorpy.phasor.phasor_from_lifetime
|
628
|
-
phasorpy.phasor.phasor_to_signal
|
629
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_lifetime_to_signal.py`
|
630
|
-
|
631
|
-
Notes
|
632
|
-
-----
|
633
|
-
This implementation is based on an inverse digital Fourier transform (DFT).
|
634
|
-
Because DFT cannot be used on signals with discontinuities
|
635
|
-
(for example, an exponential decay starting at zero) without producing
|
636
|
-
strong artifacts (ripples), the signal is convoluted with a continuous
|
637
|
-
instrument response function (IRF). The minimum width of the IRF is
|
638
|
-
limited due to sampling requirements.
|
639
|
-
|
640
|
-
Examples
|
641
|
-
--------
|
642
|
-
Synthesize a multi-exponential time-domain decay signal for two
|
643
|
-
lifetime components of 4.2 and 0.9 ns at 40 MHz:
|
644
|
-
|
645
|
-
>>> signal, zero, times = lifetime_to_signal(
|
646
|
-
... 40, [4.2, 0.9], fraction=[0.8, 0.2], samples=16
|
647
|
-
... )
|
648
|
-
>>> signal # doctest: +NUMBER
|
649
|
-
array([0.2846, 0.1961, 0.1354, ..., 0.8874, 0.6029, 0.4135])
|
650
|
-
|
651
|
-
Synthesize a homodyne frequency-domain waveform signal for
|
652
|
-
a single lifetime:
|
653
|
-
|
654
|
-
>>> signal, zero, times = lifetime_to_signal(
|
655
|
-
... 40.0, 4.2, samples=16, harmonic=1
|
656
|
-
... )
|
657
|
-
>>> signal # doctest: +NUMBER
|
658
|
-
array([0.2047, -0.05602, -0.156, ..., 1.471, 1.031, 0.5865])
|
659
|
-
|
660
|
-
"""
|
661
|
-
if harmonic is None:
|
662
|
-
harmonic = 'all'
|
663
|
-
all_hamonics = harmonic == 'all'
|
664
|
-
harmonic, _ = parse_harmonic(harmonic, samples // 2)
|
665
|
-
|
666
|
-
if samples < 16:
|
667
|
-
raise ValueError(f'{samples=} < 16')
|
668
|
-
|
669
|
-
if background is None:
|
670
|
-
background = 0.0
|
671
|
-
background = numpy.asarray(background)
|
672
|
-
|
673
|
-
if mean is None:
|
674
|
-
mean = 1.0
|
675
|
-
mean = numpy.asarray(mean)
|
676
|
-
mean -= background
|
677
|
-
if numpy.any(mean <= 0.0):
|
678
|
-
raise ValueError('mean - background must not be less than zero')
|
679
|
-
|
680
|
-
scale = samples / (2.0 * math.pi)
|
681
|
-
if zero_phase is None:
|
682
|
-
zero_phase = 8.0 / scale
|
683
|
-
phase = zero_phase * scale # in sample units
|
684
|
-
if zero_stdev is None:
|
685
|
-
zero_stdev = 1.5 / scale
|
686
|
-
stdev = zero_stdev * scale # in sample units
|
687
|
-
|
688
|
-
if zero_phase < 0 or zero_phase > 2.0 * math.pi:
|
689
|
-
raise ValueError(f'{zero_phase=} out of range [0, 2 pi]')
|
690
|
-
if stdev < 1.5:
|
691
|
-
raise ValueError(
|
692
|
-
f'{zero_stdev=} < {1.5 / scale} cannot be sampled sufficiently'
|
693
|
-
)
|
694
|
-
if stdev >= samples / 10:
|
695
|
-
raise ValueError(f'{zero_stdev=} > pi / 5 not supported')
|
696
|
-
|
697
|
-
frequencies = numpy.atleast_1d(frequency)
|
698
|
-
if frequencies.size > 1 or frequencies[0] <= 0.0:
|
699
|
-
raise ValueError('frequency must be scalar and positive')
|
700
|
-
frequencies = numpy.linspace(
|
701
|
-
frequency, samples // 2 * frequency, samples // 2
|
702
|
-
)
|
703
|
-
frequencies = frequencies[[h - 1 for h in harmonic]]
|
704
|
-
|
705
|
-
real, imag = phasor_from_lifetime(
|
706
|
-
frequencies,
|
707
|
-
lifetime,
|
708
|
-
fraction,
|
709
|
-
preexponential=preexponential,
|
710
|
-
unit_conversion=unit_conversion,
|
711
|
-
)
|
712
|
-
real, imag = numpy.atleast_1d(real, imag)
|
713
|
-
|
714
|
-
zero = numpy.zeros(samples, dtype=numpy.float64)
|
715
|
-
_gaussian_signal(zero, phase, stdev)
|
716
|
-
zero_mean, zero_real, zero_imag = phasor_from_signal(
|
717
|
-
zero, harmonic=harmonic
|
718
|
-
)
|
719
|
-
if real.ndim > 1:
|
720
|
-
# make broadcastable with real and imag
|
721
|
-
zero_real = zero_real[:, None]
|
722
|
-
zero_imag = zero_imag[:, None]
|
723
|
-
if not all_hamonics:
|
724
|
-
zero = phasor_to_signal(
|
725
|
-
zero_mean, zero_real, zero_imag, samples=samples, harmonic=harmonic
|
726
|
-
)
|
727
|
-
|
728
|
-
phasor_multiply(real, imag, zero_real, zero_imag, out=(real, imag))
|
729
|
-
|
730
|
-
if len(harmonic) == 1:
|
731
|
-
harmonic = harmonic[0]
|
732
|
-
signal = phasor_to_signal(
|
733
|
-
mean, real, imag, samples=samples, harmonic=harmonic
|
734
|
-
)
|
735
|
-
signal += numpy.asarray(background)
|
736
|
-
|
737
|
-
time = numpy.linspace(0, 1.0 / (unit_conversion * frequency), samples)
|
738
|
-
|
739
|
-
return signal.squeeze(), zero.squeeze(), time
|
740
|
-
|
741
|
-
|
742
|
-
def phasor_semicircle(
|
743
|
-
samples: int = 101, /
|
744
|
-
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
745
|
-
r"""Return equally spaced phasor coordinates on universal semicircle.
|
746
|
-
|
747
|
-
Parameters
|
748
|
-
----------
|
749
|
-
samples : int, optional, default: 101
|
750
|
-
Number of coordinates to return.
|
751
|
-
|
752
|
-
Returns
|
753
|
-
-------
|
754
|
-
real : ndarray
|
755
|
-
Real component of phasor coordinates on universal semicircle.
|
756
|
-
imag : ndarray
|
757
|
-
Imaginary component of phasor coordinates on universal semicircle.
|
758
|
-
|
759
|
-
Raises
|
760
|
-
------
|
761
|
-
ValueError
|
762
|
-
The number of `samples` is smaller than 1.
|
763
|
-
|
764
|
-
Notes
|
765
|
-
-----
|
766
|
-
If more than one sample, the first and last phasor coordinates returned
|
767
|
-
are ``(0, 0)`` and ``(1, 0)``.
|
768
|
-
The center coordinate, if any, is ``(0.5, 0.5)``.
|
769
|
-
|
770
|
-
The universal semicircle is composed of the phasor coordinates of
|
771
|
-
single lifetime components, where the relation of polar coordinates
|
772
|
-
(phase :math:`\phi` and modulation :math:`M`) is:
|
773
|
-
|
774
|
-
.. math::
|
775
|
-
|
776
|
-
M = \cos{\phi}
|
777
|
-
|
778
|
-
Examples
|
779
|
-
--------
|
780
|
-
Calculate three phasor coordinates on universal semicircle:
|
781
|
-
|
782
|
-
>>> phasor_semicircle(3) # doctest: +NUMBER
|
783
|
-
(array([0, 0.5, 1]), array([0.0, 0.5, 0]))
|
784
|
-
|
785
|
-
"""
|
786
|
-
if samples < 1:
|
787
|
-
raise ValueError(f'{samples=} < 1')
|
788
|
-
arange = numpy.linspace(math.pi, 0.0, samples)
|
789
|
-
real = numpy.cos(arange)
|
790
|
-
real += 1.0
|
791
|
-
real *= 0.5
|
792
|
-
imag = numpy.sin(arange)
|
793
|
-
imag *= 0.5
|
794
|
-
return real, imag
|
795
|
-
|
796
|
-
|
797
495
|
def phasor_to_complex(
|
798
496
|
real: ArrayLike,
|
799
497
|
imag: ArrayLike,
|
@@ -1026,7 +724,7 @@ def phasor_normalize(
|
|
1026
724
|
S &= S' / F
|
1027
725
|
|
1028
726
|
If :math:`F = 0`, the normalized phasor coordinates (:math:`G`)
|
1029
|
-
and (:math:`S`) are undefined (
|
727
|
+
and (:math:`S`) are undefined (NaN or infinity).
|
1030
728
|
|
1031
729
|
Examples
|
1032
730
|
--------
|
@@ -1064,283 +762,6 @@ def phasor_normalize(
|
|
1064
762
|
return mean, real, imag
|
1065
763
|
|
1066
764
|
|
1067
|
-
def phasor_calibrate(
|
1068
|
-
real: ArrayLike,
|
1069
|
-
imag: ArrayLike,
|
1070
|
-
reference_mean: ArrayLike,
|
1071
|
-
reference_real: ArrayLike,
|
1072
|
-
reference_imag: ArrayLike,
|
1073
|
-
/,
|
1074
|
-
frequency: ArrayLike,
|
1075
|
-
lifetime: ArrayLike,
|
1076
|
-
*,
|
1077
|
-
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
1078
|
-
skip_axis: int | Sequence[int] | None = None,
|
1079
|
-
fraction: ArrayLike | None = None,
|
1080
|
-
preexponential: bool = False,
|
1081
|
-
unit_conversion: float = 1e-3,
|
1082
|
-
method: Literal['mean', 'median'] = 'mean',
|
1083
|
-
nan_safe: bool = True,
|
1084
|
-
reverse: bool = False,
|
1085
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
1086
|
-
"""Return calibrated/referenced phasor coordinates.
|
1087
|
-
|
1088
|
-
Calibration of phasor coordinates from time-resolved measurements is
|
1089
|
-
necessary to account for the instrument response function (IRF) and delays
|
1090
|
-
in the electronics.
|
1091
|
-
|
1092
|
-
Parameters
|
1093
|
-
----------
|
1094
|
-
real : array_like
|
1095
|
-
Real component of phasor coordinates to be calibrated.
|
1096
|
-
imag : array_like
|
1097
|
-
Imaginary component of phasor coordinates to be calibrated.
|
1098
|
-
reference_mean : array_like or None
|
1099
|
-
Intensity of phasor coordinates from reference of known lifetime.
|
1100
|
-
Used to re-normalize averaged phasor coordinates.
|
1101
|
-
reference_real : array_like
|
1102
|
-
Real component of phasor coordinates from reference of known lifetime.
|
1103
|
-
Must be measured with the same instrument setting as the phasor
|
1104
|
-
coordinates to be calibrated. Dimensions must be the same as `real`.
|
1105
|
-
reference_imag : array_like
|
1106
|
-
Imaginary component of phasor coordinates from reference of known
|
1107
|
-
lifetime.
|
1108
|
-
Must be measured with the same instrument setting as the phasor
|
1109
|
-
coordinates to be calibrated.
|
1110
|
-
frequency : array_like
|
1111
|
-
Fundamental laser pulse or modulation frequency in MHz.
|
1112
|
-
lifetime : array_like
|
1113
|
-
Lifetime components in ns. Must be scalar or one-dimensional.
|
1114
|
-
harmonic : int, sequence of int, or 'all', default: 1
|
1115
|
-
Harmonics included in `real` and `imag`.
|
1116
|
-
If an integer, the harmonics at which `real` and `imag` were acquired
|
1117
|
-
or calculated.
|
1118
|
-
If a sequence, the harmonics included in the first axis of `real` and
|
1119
|
-
`imag`.
|
1120
|
-
If `'all'`, the first axis of `real` and `imag` contains lower
|
1121
|
-
harmonics.
|
1122
|
-
The default is the first harmonic (fundamental frequency).
|
1123
|
-
skip_axis : int or sequence of int, optional
|
1124
|
-
Axes in `reference_mean` to exclude from reference center calculation.
|
1125
|
-
By default, all axes except harmonics are included.
|
1126
|
-
fraction : array_like, optional
|
1127
|
-
Fractional intensities or pre-exponential amplitudes of the lifetime
|
1128
|
-
components. Fractions are normalized to sum to 1.
|
1129
|
-
Must be same size as `lifetime`.
|
1130
|
-
preexponential : bool, optional
|
1131
|
-
If true, `fraction` values are pre-exponential amplitudes,
|
1132
|
-
else fractional intensities (default).
|
1133
|
-
unit_conversion : float, optional
|
1134
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
1135
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1136
|
-
Use 1.0 for Hz and s.
|
1137
|
-
method : str, optional
|
1138
|
-
Method used for calculating center of reference phasor coordinates:
|
1139
|
-
|
1140
|
-
- ``'mean'``: Arithmetic mean.
|
1141
|
-
- ``'median'``: Spatial median.
|
1142
|
-
|
1143
|
-
nan_safe : bool, optional
|
1144
|
-
Ensure `method` is applied to same elements of reference arrays.
|
1145
|
-
By default, distribute NaNs among reference arrays before applying
|
1146
|
-
`method`.
|
1147
|
-
reverse : bool, optional
|
1148
|
-
Reverse calibration.
|
1149
|
-
|
1150
|
-
Returns
|
1151
|
-
-------
|
1152
|
-
real : ndarray
|
1153
|
-
Calibrated real component of phasor coordinates.
|
1154
|
-
imag : ndarray
|
1155
|
-
Calibrated imaginary component of phasor coordinates.
|
1156
|
-
|
1157
|
-
Raises
|
1158
|
-
------
|
1159
|
-
ValueError
|
1160
|
-
The array shapes of `real` and `imag`, or `reference_real` and
|
1161
|
-
`reference_imag` do not match.
|
1162
|
-
Number of harmonics or frequencies does not match the first axis
|
1163
|
-
of `real` and `imag`.
|
1164
|
-
|
1165
|
-
See Also
|
1166
|
-
--------
|
1167
|
-
phasorpy.phasor.phasor_transform
|
1168
|
-
phasorpy.phasor.polar_from_reference_phasor
|
1169
|
-
phasorpy.phasor.phasor_center
|
1170
|
-
phasorpy.phasor.phasor_from_lifetime
|
1171
|
-
|
1172
|
-
Notes
|
1173
|
-
-----
|
1174
|
-
This function is a convenience wrapper for the following operations:
|
1175
|
-
|
1176
|
-
.. code-block:: python
|
1177
|
-
|
1178
|
-
phasor_transform(
|
1179
|
-
real,
|
1180
|
-
imag,
|
1181
|
-
*polar_from_reference_phasor(
|
1182
|
-
*phasor_center(
|
1183
|
-
reference_mean,
|
1184
|
-
reference_real,
|
1185
|
-
reference_imag,
|
1186
|
-
skip_axis,
|
1187
|
-
method,
|
1188
|
-
nan_safe,
|
1189
|
-
)[1:],
|
1190
|
-
*phasor_from_lifetime(
|
1191
|
-
frequency,
|
1192
|
-
lifetime,
|
1193
|
-
fraction,
|
1194
|
-
preexponential,
|
1195
|
-
unit_conversion,
|
1196
|
-
),
|
1197
|
-
),
|
1198
|
-
)
|
1199
|
-
|
1200
|
-
Calibration can be reversed such that
|
1201
|
-
|
1202
|
-
.. code-block:: python
|
1203
|
-
|
1204
|
-
real, imag == phasor_calibrate(
|
1205
|
-
*phasor_calibrate(real, imag, *args, **kwargs),
|
1206
|
-
*args,
|
1207
|
-
reverse=True,
|
1208
|
-
**kwargs
|
1209
|
-
)
|
1210
|
-
|
1211
|
-
Examples
|
1212
|
-
--------
|
1213
|
-
>>> phasor_calibrate(
|
1214
|
-
... [0.1, 0.2, 0.3],
|
1215
|
-
... [0.4, 0.5, 0.6],
|
1216
|
-
... [1.0, 1.0, 1.0],
|
1217
|
-
... [0.2, 0.3, 0.4],
|
1218
|
-
... [0.5, 0.6, 0.7],
|
1219
|
-
... frequency=80,
|
1220
|
-
... lifetime=4,
|
1221
|
-
... ) # doctest: +NUMBER
|
1222
|
-
(array([0.0658, 0.132, 0.198]), array([0.2657, 0.332, 0.399]))
|
1223
|
-
|
1224
|
-
Undo the previous calibration:
|
1225
|
-
|
1226
|
-
>>> phasor_calibrate(
|
1227
|
-
... [0.0658, 0.132, 0.198],
|
1228
|
-
... [0.2657, 0.332, 0.399],
|
1229
|
-
... [1.0, 1.0, 1.0],
|
1230
|
-
... [0.2, 0.3, 0.4],
|
1231
|
-
... [0.5, 0.6, 0.7],
|
1232
|
-
... frequency=80,
|
1233
|
-
... lifetime=4,
|
1234
|
-
... reverse=True,
|
1235
|
-
... ) # doctest: +NUMBER
|
1236
|
-
(array([0.1, 0.2, 0.3]), array([0.4, 0.5, 0.6]))
|
1237
|
-
|
1238
|
-
"""
|
1239
|
-
real = numpy.asarray(real)
|
1240
|
-
imag = numpy.asarray(imag)
|
1241
|
-
reference_mean = numpy.asarray(reference_mean)
|
1242
|
-
reference_real = numpy.asarray(reference_real)
|
1243
|
-
reference_imag = numpy.asarray(reference_imag)
|
1244
|
-
|
1245
|
-
if real.shape != imag.shape:
|
1246
|
-
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
1247
|
-
if reference_real.shape != reference_imag.shape:
|
1248
|
-
raise ValueError(f'{reference_real.shape=} != {reference_imag.shape=}')
|
1249
|
-
|
1250
|
-
has_harmonic_axis = reference_mean.ndim + 1 == reference_real.ndim
|
1251
|
-
harmonic, _ = parse_harmonic(
|
1252
|
-
harmonic,
|
1253
|
-
(
|
1254
|
-
reference_real.shape[0]
|
1255
|
-
if has_harmonic_axis
|
1256
|
-
and isinstance(harmonic, str)
|
1257
|
-
and harmonic == 'all'
|
1258
|
-
else None
|
1259
|
-
),
|
1260
|
-
)
|
1261
|
-
|
1262
|
-
frequency = numpy.asarray(frequency)
|
1263
|
-
frequency = frequency * harmonic
|
1264
|
-
|
1265
|
-
if has_harmonic_axis:
|
1266
|
-
if real.ndim == 0:
|
1267
|
-
raise ValueError(
|
1268
|
-
f'{real.shape=} != {len(frequency)} frequencies or harmonics'
|
1269
|
-
)
|
1270
|
-
if real.shape[0] != len(frequency):
|
1271
|
-
raise ValueError(
|
1272
|
-
f'{real.shape[0]=} != {len(frequency)} '
|
1273
|
-
'frequencies or harmonics'
|
1274
|
-
)
|
1275
|
-
if reference_real.shape[0] != len(frequency):
|
1276
|
-
raise ValueError(
|
1277
|
-
f'{reference_real.shape[0]=} != {len(frequency)} '
|
1278
|
-
'frequencies or harmonics'
|
1279
|
-
)
|
1280
|
-
if reference_mean.shape != reference_real.shape[1:]:
|
1281
|
-
raise ValueError(
|
1282
|
-
f'{reference_mean.shape=} != {reference_real.shape[1:]=}'
|
1283
|
-
)
|
1284
|
-
elif reference_mean.shape != reference_real.shape:
|
1285
|
-
raise ValueError(f'{reference_mean.shape=} != {reference_real.shape=}')
|
1286
|
-
elif len(harmonic) > 1:
|
1287
|
-
raise ValueError(
|
1288
|
-
f'{reference_mean.shape=} does not have harmonic axis'
|
1289
|
-
)
|
1290
|
-
|
1291
|
-
_, measured_re, measured_im = phasor_center(
|
1292
|
-
reference_mean,
|
1293
|
-
reference_real,
|
1294
|
-
reference_imag,
|
1295
|
-
skip_axis=skip_axis,
|
1296
|
-
method=method,
|
1297
|
-
nan_safe=nan_safe,
|
1298
|
-
)
|
1299
|
-
|
1300
|
-
known_re, known_im = phasor_from_lifetime(
|
1301
|
-
frequency,
|
1302
|
-
lifetime,
|
1303
|
-
fraction,
|
1304
|
-
preexponential=preexponential,
|
1305
|
-
unit_conversion=unit_conversion,
|
1306
|
-
)
|
1307
|
-
|
1308
|
-
skip_axis, axis = parse_skip_axis(
|
1309
|
-
skip_axis, real.ndim - int(has_harmonic_axis), has_harmonic_axis
|
1310
|
-
)
|
1311
|
-
|
1312
|
-
if has_harmonic_axis and any(skip_axis):
|
1313
|
-
known_re = numpy.expand_dims(
|
1314
|
-
known_re, tuple(range(1, measured_re.ndim))
|
1315
|
-
)
|
1316
|
-
known_re = numpy.broadcast_to(
|
1317
|
-
known_re, (len(frequency), *measured_re.shape[1:])
|
1318
|
-
)
|
1319
|
-
known_im = numpy.expand_dims(
|
1320
|
-
known_im, tuple(range(1, measured_im.ndim))
|
1321
|
-
)
|
1322
|
-
known_im = numpy.broadcast_to(
|
1323
|
-
known_im, (len(frequency), *measured_im.shape[1:])
|
1324
|
-
)
|
1325
|
-
|
1326
|
-
phi_zero, mod_zero = polar_from_reference_phasor(
|
1327
|
-
measured_re, measured_im, known_re, known_im
|
1328
|
-
)
|
1329
|
-
|
1330
|
-
if numpy.ndim(phi_zero) > 0:
|
1331
|
-
if reverse:
|
1332
|
-
numpy.negative(phi_zero, out=phi_zero)
|
1333
|
-
numpy.reciprocal(mod_zero, out=mod_zero)
|
1334
|
-
if axis is not None:
|
1335
|
-
phi_zero = numpy.expand_dims(phi_zero, axis=axis)
|
1336
|
-
mod_zero = numpy.expand_dims(mod_zero, axis=axis)
|
1337
|
-
elif reverse:
|
1338
|
-
phi_zero = -phi_zero
|
1339
|
-
mod_zero = 1.0 / mod_zero
|
1340
|
-
|
1341
|
-
return phasor_transform(real, imag, phi_zero, mod_zero)
|
1342
|
-
|
1343
|
-
|
1344
765
|
def phasor_transform(
|
1345
766
|
real: ArrayLike,
|
1346
767
|
imag: ArrayLike,
|
@@ -1422,121 +843,6 @@ def phasor_transform(
|
|
1422
843
|
)
|
1423
844
|
|
1424
845
|
|
1425
|
-
def polar_from_reference_phasor(
|
1426
|
-
measured_real: ArrayLike,
|
1427
|
-
measured_imag: ArrayLike,
|
1428
|
-
known_real: ArrayLike,
|
1429
|
-
known_imag: ArrayLike,
|
1430
|
-
/,
|
1431
|
-
**kwargs: Any,
|
1432
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
1433
|
-
r"""Return polar coordinates for calibration from reference phasor.
|
1434
|
-
|
1435
|
-
Return rotation angle and scale factor for calibrating phasor coordinates
|
1436
|
-
from measured and known phasor coordinates of a reference, for example,
|
1437
|
-
a sample of known lifetime.
|
1438
|
-
|
1439
|
-
Parameters
|
1440
|
-
----------
|
1441
|
-
measured_real : array_like
|
1442
|
-
Real component of measured phasor coordinates.
|
1443
|
-
measured_imag : array_like
|
1444
|
-
Imaginary component of measured phasor coordinates.
|
1445
|
-
known_real : array_like
|
1446
|
-
Real component of reference phasor coordinates.
|
1447
|
-
known_imag : array_like
|
1448
|
-
Imaginary component of reference phasor coordinates.
|
1449
|
-
**kwargs
|
1450
|
-
Optional `arguments passed to numpy universal functions
|
1451
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1452
|
-
|
1453
|
-
Returns
|
1454
|
-
-------
|
1455
|
-
phase_zero : ndarray
|
1456
|
-
Angular component of polar coordinates for calibration in radians.
|
1457
|
-
modulation_zero : ndarray
|
1458
|
-
Radial component of polar coordinates for calibration.
|
1459
|
-
|
1460
|
-
See Also
|
1461
|
-
--------
|
1462
|
-
phasorpy.phasor.polar_from_reference
|
1463
|
-
|
1464
|
-
Notes
|
1465
|
-
-----
|
1466
|
-
This function performs the following operations:
|
1467
|
-
|
1468
|
-
.. code-block:: python
|
1469
|
-
|
1470
|
-
polar_from_reference(
|
1471
|
-
*phasor_to_polar(measured_real, measured_imag),
|
1472
|
-
*phasor_to_polar(known_real, known_imag),
|
1473
|
-
)
|
1474
|
-
|
1475
|
-
Examples
|
1476
|
-
--------
|
1477
|
-
>>> polar_from_reference_phasor(0.5, 0.0, 1.0, 0.0)
|
1478
|
-
(0.0, 2.0)
|
1479
|
-
|
1480
|
-
"""
|
1481
|
-
return _polar_from_reference_phasor( # type: ignore[no-any-return]
|
1482
|
-
measured_real, measured_imag, known_real, known_imag, **kwargs
|
1483
|
-
)
|
1484
|
-
|
1485
|
-
|
1486
|
-
def polar_from_reference(
|
1487
|
-
measured_phase: ArrayLike,
|
1488
|
-
measured_modulation: ArrayLike,
|
1489
|
-
known_phase: ArrayLike,
|
1490
|
-
known_modulation: ArrayLike,
|
1491
|
-
/,
|
1492
|
-
**kwargs: Any,
|
1493
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
1494
|
-
r"""Return polar coordinates for calibration from reference coordinates.
|
1495
|
-
|
1496
|
-
Return rotation angle and scale factor for calibrating phasor coordinates
|
1497
|
-
from measured and known polar coordinates of a reference, for example,
|
1498
|
-
a sample of known lifetime.
|
1499
|
-
|
1500
|
-
Parameters
|
1501
|
-
----------
|
1502
|
-
measured_phase : array_like
|
1503
|
-
Angular component of measured polar coordinates in radians.
|
1504
|
-
measured_modulation : array_like
|
1505
|
-
Radial component of measured polar coordinates.
|
1506
|
-
known_phase : array_like
|
1507
|
-
Angular component of reference polar coordinates in radians.
|
1508
|
-
known_modulation : array_like
|
1509
|
-
Radial component of reference polar coordinates.
|
1510
|
-
**kwargs
|
1511
|
-
Optional `arguments passed to numpy universal functions
|
1512
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1513
|
-
|
1514
|
-
Returns
|
1515
|
-
-------
|
1516
|
-
phase_zero : ndarray
|
1517
|
-
Angular component of polar coordinates for calibration in radians.
|
1518
|
-
modulation_zero : ndarray
|
1519
|
-
Radial component of polar coordinates for calibration.
|
1520
|
-
|
1521
|
-
See Also
|
1522
|
-
--------
|
1523
|
-
phasorpy.phasor.polar_from_reference_phasor
|
1524
|
-
|
1525
|
-
Examples
|
1526
|
-
--------
|
1527
|
-
>>> polar_from_reference(0.2, 0.4, 0.4, 1.3)
|
1528
|
-
(0.2, 3.25)
|
1529
|
-
|
1530
|
-
"""
|
1531
|
-
return _polar_from_reference( # type: ignore[no-any-return]
|
1532
|
-
measured_phase,
|
1533
|
-
measured_modulation,
|
1534
|
-
known_phase,
|
1535
|
-
known_modulation,
|
1536
|
-
**kwargs,
|
1537
|
-
)
|
1538
|
-
|
1539
|
-
|
1540
846
|
def phasor_to_polar(
|
1541
847
|
real: ArrayLike,
|
1542
848
|
imag: ArrayLike,
|
@@ -1577,6 +883,7 @@ def phasor_to_polar(
|
|
1577
883
|
See Also
|
1578
884
|
--------
|
1579
885
|
phasorpy.phasor.phasor_from_polar
|
886
|
+
:ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
|
1580
887
|
|
1581
888
|
Examples
|
1582
889
|
--------
|
@@ -1647,1053 +954,30 @@ def phasor_from_polar(
|
|
1647
954
|
)
|
1648
955
|
|
1649
956
|
|
1650
|
-
def
|
957
|
+
def phasor_to_principal_plane(
|
1651
958
|
real: ArrayLike,
|
1652
959
|
imag: ArrayLike,
|
1653
960
|
/,
|
1654
|
-
frequency: ArrayLike,
|
1655
961
|
*,
|
1656
|
-
|
1657
|
-
|
1658
|
-
|
1659
|
-
|
962
|
+
reorient: bool = True,
|
963
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
964
|
+
"""Return multi-harmonic phasor coordinates projected onto principal plane.
|
965
|
+
|
966
|
+
Principal component analysis (PCA) is used to project
|
967
|
+
multi-harmonic phasor coordinates onto a plane, along which
|
968
|
+
coordinate axes the phasor coordinates have the largest variations.
|
969
|
+
|
970
|
+
The transformed coordinates are not phasor coordinates. However, the
|
971
|
+
coordinates can be used in visualization and cursor analysis since
|
972
|
+
the transformation is affine (preserving collinearity and ratios
|
973
|
+
of distances).
|
1660
974
|
|
1661
975
|
Parameters
|
1662
976
|
----------
|
1663
977
|
real : array_like
|
1664
|
-
Real component of phasor coordinates.
|
1665
|
-
|
1666
|
-
|
1667
|
-
frequency : array_like
|
1668
|
-
Laser pulse or modulation frequency in MHz.
|
1669
|
-
unit_conversion : float, optional
|
1670
|
-
Product of `frequency` and returned `lifetime` units' prefix factors.
|
1671
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1672
|
-
Use 1.0 for Hz and s.
|
1673
|
-
**kwargs
|
1674
|
-
Optional `arguments passed to numpy universal functions
|
1675
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1676
|
-
|
1677
|
-
Returns
|
1678
|
-
-------
|
1679
|
-
phase_lifetime : ndarray
|
1680
|
-
Apparent single lifetime from angular component of phasor coordinates.
|
1681
|
-
modulation_lifetime : ndarray
|
1682
|
-
Apparent single lifetime from radial component of phasor coordinates.
|
1683
|
-
|
1684
|
-
See Also
|
1685
|
-
--------
|
1686
|
-
phasorpy.phasor.phasor_from_apparent_lifetime
|
1687
|
-
|
1688
|
-
Notes
|
1689
|
-
-----
|
1690
|
-
The phasor coordinates `real` (:math:`G`) and `imag` (:math:`S`)
|
1691
|
-
are converted to apparent single lifetimes
|
1692
|
-
`phase_lifetime` (:math:`\tau_{\phi}`) and
|
1693
|
-
`modulation_lifetime` (:math:`\tau_{M}`) at frequency :math:`f`
|
1694
|
-
according to:
|
1695
|
-
|
1696
|
-
.. math::
|
1697
|
-
|
1698
|
-
\omega &= 2 \pi f
|
1699
|
-
|
1700
|
-
\tau_{\phi} &= \omega^{-1} \cdot S / G
|
1701
|
-
|
1702
|
-
\tau_{M} &= \omega^{-1} \cdot \sqrt{1 / (S^2 + G^2) - 1}
|
1703
|
-
|
1704
|
-
Examples
|
1705
|
-
--------
|
1706
|
-
The apparent single lifetimes from phase and modulation are equal
|
1707
|
-
only if the phasor coordinates lie on the universal semicircle:
|
1708
|
-
|
1709
|
-
>>> phasor_to_apparent_lifetime(
|
1710
|
-
... 0.5, [0.5, 0.45], frequency=80
|
1711
|
-
... ) # doctest: +NUMBER
|
1712
|
-
(array([1.989, 1.79]), array([1.989, 2.188]))
|
1713
|
-
|
1714
|
-
Apparent single lifetimes of phasor coordinates outside the universal
|
1715
|
-
semicircle are undefined:
|
1716
|
-
|
1717
|
-
>>> phasor_to_apparent_lifetime(-0.1, 1.1, 80) # doctest: +NUMBER
|
1718
|
-
(-21.8, 0.0)
|
1719
|
-
|
1720
|
-
Apparent single lifetimes at the universal semicircle endpoints are
|
1721
|
-
infinite and zero:
|
1722
|
-
|
1723
|
-
>>> phasor_to_apparent_lifetime([0, 1], [0, 0], 80) # doctest: +NUMBER
|
1724
|
-
(array([inf, 0]), array([inf, 0]))
|
1725
|
-
|
1726
|
-
"""
|
1727
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
1728
|
-
omega *= math.pi * 2.0 * unit_conversion
|
1729
|
-
return _phasor_to_apparent_lifetime( # type: ignore[no-any-return]
|
1730
|
-
real, imag, omega, **kwargs
|
1731
|
-
)
|
1732
|
-
|
1733
|
-
|
1734
|
-
def phasor_from_apparent_lifetime(
|
1735
|
-
phase_lifetime: ArrayLike,
|
1736
|
-
modulation_lifetime: ArrayLike | None,
|
1737
|
-
/,
|
1738
|
-
frequency: ArrayLike,
|
1739
|
-
*,
|
1740
|
-
unit_conversion: float = 1e-3,
|
1741
|
-
**kwargs: Any,
|
1742
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
1743
|
-
r"""Return phasor coordinates from apparent single lifetimes.
|
1744
|
-
|
1745
|
-
Parameters
|
1746
|
-
----------
|
1747
|
-
phase_lifetime : ndarray
|
1748
|
-
Apparent single lifetime from phase.
|
1749
|
-
modulation_lifetime : ndarray, optional
|
1750
|
-
Apparent single lifetime from modulation.
|
1751
|
-
If None, `modulation_lifetime` is same as `phase_lifetime`.
|
1752
|
-
frequency : array_like
|
1753
|
-
Laser pulse or modulation frequency in MHz.
|
1754
|
-
unit_conversion : float, optional
|
1755
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
1756
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1757
|
-
Use 1.0 for Hz and s.
|
1758
|
-
**kwargs
|
1759
|
-
Optional `arguments passed to numpy universal functions
|
1760
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1761
|
-
|
1762
|
-
Returns
|
1763
|
-
-------
|
1764
|
-
real : ndarray
|
1765
|
-
Real component of phasor coordinates.
|
1766
|
-
imag : ndarray
|
1767
|
-
Imaginary component of phasor coordinates.
|
1768
|
-
|
1769
|
-
See Also
|
1770
|
-
--------
|
1771
|
-
phasorpy.phasor.phasor_to_apparent_lifetime
|
1772
|
-
|
1773
|
-
Notes
|
1774
|
-
-----
|
1775
|
-
The apparent single lifetimes `phase_lifetime` (:math:`\tau_{\phi}`)
|
1776
|
-
and `modulation_lifetime` (:math:`\tau_{M}`) are converted to phasor
|
1777
|
-
coordinates `real` (:math:`G`) and `imag` (:math:`S`) at
|
1778
|
-
frequency :math:`f` according to:
|
1779
|
-
|
1780
|
-
.. math::
|
1781
|
-
|
1782
|
-
\omega &= 2 \pi f
|
1783
|
-
|
1784
|
-
\phi & = \arctan(\omega \tau_{\phi})
|
1785
|
-
|
1786
|
-
M &= 1 / \sqrt{1 + (\omega \tau_{M})^2}
|
1787
|
-
|
1788
|
-
G &= M \cdot \cos{\phi}
|
1789
|
-
|
1790
|
-
S &= M \cdot \sin{\phi}
|
1791
|
-
|
1792
|
-
Examples
|
1793
|
-
--------
|
1794
|
-
If the apparent single lifetimes from phase and modulation are equal,
|
1795
|
-
the phasor coordinates lie on the universal semicircle, else inside:
|
1796
|
-
|
1797
|
-
>>> phasor_from_apparent_lifetime(
|
1798
|
-
... 1.9894, [1.9894, 2.4113], frequency=80.0
|
1799
|
-
... ) # doctest: +NUMBER
|
1800
|
-
(array([0.5, 0.45]), array([0.5, 0.45]))
|
1801
|
-
|
1802
|
-
Zero and infinite apparent single lifetimes define the endpoints of the
|
1803
|
-
universal semicircle:
|
1804
|
-
|
1805
|
-
>>> phasor_from_apparent_lifetime(
|
1806
|
-
... [0.0, 1e9], [0.0, 1e9], frequency=80
|
1807
|
-
... ) # doctest: +NUMBER
|
1808
|
-
(array([1, 0.0]), array([0, 0.0]))
|
1809
|
-
|
1810
|
-
"""
|
1811
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
1812
|
-
omega *= math.pi * 2.0 * unit_conversion
|
1813
|
-
if modulation_lifetime is None:
|
1814
|
-
return _phasor_from_single_lifetime( # type: ignore[no-any-return]
|
1815
|
-
phase_lifetime, omega, **kwargs
|
1816
|
-
)
|
1817
|
-
return _phasor_from_apparent_lifetime( # type: ignore[no-any-return]
|
1818
|
-
phase_lifetime, modulation_lifetime, omega, **kwargs
|
1819
|
-
)
|
1820
|
-
|
1821
|
-
|
1822
|
-
def lifetime_to_frequency(
|
1823
|
-
lifetime: ArrayLike,
|
1824
|
-
*,
|
1825
|
-
unit_conversion: float = 1e-3,
|
1826
|
-
) -> NDArray[numpy.float64]:
|
1827
|
-
r"""Return optimal frequency for resolving single component lifetime.
|
1828
|
-
|
1829
|
-
Parameters
|
1830
|
-
----------
|
1831
|
-
lifetime : array_like
|
1832
|
-
Single component lifetime.
|
1833
|
-
unit_conversion : float, optional, default: 1e-3
|
1834
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
1835
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1836
|
-
Use 1.0 for Hz and s.
|
1837
|
-
|
1838
|
-
Returns
|
1839
|
-
-------
|
1840
|
-
frequency : ndarray
|
1841
|
-
Optimal laser pulse or modulation frequency for resolving `lifetime`.
|
1842
|
-
|
1843
|
-
Notes
|
1844
|
-
-----
|
1845
|
-
The optimal frequency :math:`f` to resolve a single component lifetime
|
1846
|
-
:math:`\tau` is
|
1847
|
-
(:ref:`Redford & Clegg 2005 <redford-clegg-2005>`. Eq. B.6):
|
1848
|
-
|
1849
|
-
.. math::
|
1850
|
-
|
1851
|
-
\omega &= 2 \pi f
|
1852
|
-
|
1853
|
-
\omega^2 &= \frac{1 + \sqrt{3}}{2 \tau^2}
|
1854
|
-
|
1855
|
-
Examples
|
1856
|
-
--------
|
1857
|
-
Measurements of a lifetime near 4 ns should be made at 47 MHz,
|
1858
|
-
near 1 ns at 186 MHz:
|
1859
|
-
|
1860
|
-
>>> lifetime_to_frequency([4.0, 1.0]) # doctest: +NUMBER
|
1861
|
-
array([46.5, 186])
|
1862
|
-
|
1863
|
-
"""
|
1864
|
-
t = numpy.reciprocal(lifetime, dtype=numpy.float64)
|
1865
|
-
t *= 0.18601566519848653 / unit_conversion
|
1866
|
-
return t
|
1867
|
-
|
1868
|
-
|
1869
|
-
def lifetime_from_frequency(
|
1870
|
-
frequency: ArrayLike,
|
1871
|
-
*,
|
1872
|
-
unit_conversion: float = 1e-3,
|
1873
|
-
) -> NDArray[numpy.float64]:
|
1874
|
-
r"""Return single component lifetime best resolved at frequency.
|
1875
|
-
|
1876
|
-
Parameters
|
1877
|
-
----------
|
1878
|
-
frequency : array_like
|
1879
|
-
Laser pulse or modulation frequency.
|
1880
|
-
unit_conversion : float, optional, default: 1e-3
|
1881
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
1882
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1883
|
-
Use 1.0 for Hz and s.
|
1884
|
-
|
1885
|
-
Returns
|
1886
|
-
-------
|
1887
|
-
lifetime : ndarray
|
1888
|
-
Single component lifetime best resolved at `frequency`.
|
1889
|
-
|
1890
|
-
Notes
|
1891
|
-
-----
|
1892
|
-
The lifetime :math:`\tau` that is best resolved at frequency :math:`f` is
|
1893
|
-
(:ref:`Redford & Clegg 2005 <redford-clegg-2005>`. Eq. B.6):
|
1894
|
-
|
1895
|
-
.. math::
|
1896
|
-
|
1897
|
-
\omega &= 2 \pi f
|
1898
|
-
|
1899
|
-
\tau^2 &= \frac{1 + \sqrt{3}}{2 \omega^2}
|
1900
|
-
|
1901
|
-
Examples
|
1902
|
-
--------
|
1903
|
-
Measurements at frequencies of 47 and 186 MHz are best for measuring
|
1904
|
-
lifetimes near 4 and 1 ns respectively:
|
1905
|
-
|
1906
|
-
>>> lifetime_from_frequency([46.5, 186]) # doctest: +NUMBER
|
1907
|
-
array([4, 1])
|
1908
|
-
|
1909
|
-
"""
|
1910
|
-
t = numpy.reciprocal(frequency, dtype=numpy.float64)
|
1911
|
-
t *= 0.18601566519848653 / unit_conversion
|
1912
|
-
return t
|
1913
|
-
|
1914
|
-
|
1915
|
-
def lifetime_fraction_to_amplitude(
|
1916
|
-
lifetime: ArrayLike, fraction: ArrayLike, *, axis: int = -1
|
1917
|
-
) -> NDArray[numpy.float64]:
|
1918
|
-
r"""Return pre-exponential amplitude from fractional intensity.
|
1919
|
-
|
1920
|
-
Parameters
|
1921
|
-
----------
|
1922
|
-
lifetime : array_like
|
1923
|
-
Lifetime components.
|
1924
|
-
fraction : array_like
|
1925
|
-
Fractional intensities of lifetime components.
|
1926
|
-
Fractions are normalized to sum to 1.
|
1927
|
-
axis : int, optional
|
1928
|
-
Axis over which to compute pre-exponential amplitudes.
|
1929
|
-
The default is the last axis (-1).
|
1930
|
-
|
1931
|
-
Returns
|
1932
|
-
-------
|
1933
|
-
amplitude : ndarray
|
1934
|
-
Pre-exponential amplitudes.
|
1935
|
-
The product of `amplitude` and `lifetime` sums to 1 along `axis`.
|
1936
|
-
|
1937
|
-
See Also
|
1938
|
-
--------
|
1939
|
-
phasorpy.phasor.lifetime_fraction_from_amplitude
|
1940
|
-
|
1941
|
-
Notes
|
1942
|
-
-----
|
1943
|
-
The pre-exponential amplitude :math:`a` of component :math:`j` with
|
1944
|
-
lifetime :math:`\tau` and fractional intensity :math:`\alpha` is:
|
1945
|
-
|
1946
|
-
.. math::
|
1947
|
-
|
1948
|
-
a_{j} = \frac{\alpha_{j}}{\tau_{j} \cdot \sum_{j} \alpha_{j}}
|
1949
|
-
|
1950
|
-
Examples
|
1951
|
-
--------
|
1952
|
-
>>> lifetime_fraction_to_amplitude(
|
1953
|
-
... [4.0, 1.0], [1.6, 0.4]
|
1954
|
-
... ) # doctest: +NUMBER
|
1955
|
-
array([0.2, 0.2])
|
1956
|
-
|
1957
|
-
"""
|
1958
|
-
t = numpy.array(fraction, dtype=numpy.float64) # makes copy
|
1959
|
-
t /= numpy.sum(t, axis=axis, keepdims=True)
|
1960
|
-
numpy.true_divide(t, lifetime, out=t)
|
1961
|
-
return t
|
1962
|
-
|
1963
|
-
|
1964
|
-
def lifetime_fraction_from_amplitude(
|
1965
|
-
lifetime: ArrayLike, amplitude: ArrayLike, *, axis: int = -1
|
1966
|
-
) -> NDArray[numpy.float64]:
|
1967
|
-
r"""Return fractional intensity from pre-exponential amplitude.
|
1968
|
-
|
1969
|
-
Parameters
|
1970
|
-
----------
|
1971
|
-
lifetime : array_like
|
1972
|
-
Lifetime of components.
|
1973
|
-
amplitude : array_like
|
1974
|
-
Pre-exponential amplitudes of lifetime components.
|
1975
|
-
axis : int, optional
|
1976
|
-
Axis over which to compute fractional intensities.
|
1977
|
-
The default is the last axis (-1).
|
1978
|
-
|
1979
|
-
Returns
|
1980
|
-
-------
|
1981
|
-
fraction : ndarray
|
1982
|
-
Fractional intensities, normalized to sum to 1 along `axis`.
|
1983
|
-
|
1984
|
-
See Also
|
1985
|
-
--------
|
1986
|
-
phasorpy.phasor.lifetime_fraction_to_amplitude
|
1987
|
-
|
1988
|
-
Notes
|
1989
|
-
-----
|
1990
|
-
The fractional intensity :math:`\alpha` of component :math:`j` with
|
1991
|
-
lifetime :math:`\tau` and pre-exponential amplitude :math:`a` is:
|
1992
|
-
|
1993
|
-
.. math::
|
1994
|
-
|
1995
|
-
\alpha_{j} = \frac{a_{j} \tau_{j}}{\sum_{j} a_{j} \tau_{j}}
|
1996
|
-
|
1997
|
-
Examples
|
1998
|
-
--------
|
1999
|
-
>>> lifetime_fraction_from_amplitude(
|
2000
|
-
... [4.0, 1.0], [1.0, 1.0]
|
2001
|
-
... ) # doctest: +NUMBER
|
2002
|
-
array([0.8, 0.2])
|
2003
|
-
|
2004
|
-
"""
|
2005
|
-
t: NDArray[numpy.float64]
|
2006
|
-
t = numpy.multiply(amplitude, lifetime, dtype=numpy.float64)
|
2007
|
-
t /= numpy.sum(t, axis=axis, keepdims=True)
|
2008
|
-
return t
|
2009
|
-
|
2010
|
-
|
2011
|
-
def phasor_at_harmonic(
|
2012
|
-
real: ArrayLike,
|
2013
|
-
harmonic: ArrayLike,
|
2014
|
-
other_harmonic: ArrayLike,
|
2015
|
-
/,
|
2016
|
-
**kwargs: Any,
|
2017
|
-
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
2018
|
-
r"""Return phasor coordinates on universal semicircle at other harmonics.
|
2019
|
-
|
2020
|
-
Return phasor coordinates at any harmonic, given the real component of
|
2021
|
-
phasor coordinates of a single exponential lifetime at a certain harmonic.
|
2022
|
-
The input and output phasor coordinates lie on the universal semicircle.
|
2023
|
-
|
2024
|
-
Parameters
|
2025
|
-
----------
|
2026
|
-
real : array_like
|
2027
|
-
Real component of phasor coordinates of single exponential lifetime
|
2028
|
-
at `harmonic`.
|
2029
|
-
harmonic : array_like
|
2030
|
-
Harmonic of `real` coordinate. Must be integer >= 1.
|
2031
|
-
other_harmonic : array_like
|
2032
|
-
Harmonic for which to return phasor coordinates. Must be integer >= 1.
|
2033
|
-
**kwargs
|
2034
|
-
Optional `arguments passed to numpy universal functions
|
2035
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2036
|
-
|
2037
|
-
Returns
|
2038
|
-
-------
|
2039
|
-
real_other : ndarray
|
2040
|
-
Real component of phasor coordinates at `other_harmonic`.
|
2041
|
-
imag_other : ndarray
|
2042
|
-
Imaginary component of phasor coordinates at `other_harmonic`.
|
2043
|
-
|
2044
|
-
Notes
|
2045
|
-
-----
|
2046
|
-
The phasor coordinates
|
2047
|
-
:math:`g_{n}` (`real_other`) and :math:`s_{n}` (`imag_other`)
|
2048
|
-
of a single exponential lifetime at harmonic :math:`n` (`other_harmonic`)
|
2049
|
-
is calculated from the real part of the phasor coordinates
|
2050
|
-
:math:`g_{m}` (`real`) at harmonic :math:`m` (`harmonic`) according to
|
2051
|
-
(:ref:`Torrado, Malacrida, & Ranjit. 2022 <torrado-2022>`. Eq. 25):
|
2052
|
-
|
2053
|
-
.. math::
|
2054
|
-
|
2055
|
-
g_{n} &= \frac{m^2 \cdot g_{m}}{n^2 + (m^2-n^2) \cdot g_{m}}
|
2056
|
-
|
2057
|
-
s_{n} &= \sqrt{G_{n} - g_{n}^2}
|
2058
|
-
|
2059
|
-
This function is equivalent to the following operations:
|
2060
|
-
|
2061
|
-
.. code-block:: python
|
2062
|
-
|
2063
|
-
phasor_from_lifetime(
|
2064
|
-
frequency=other_harmonic,
|
2065
|
-
lifetime=phasor_to_apparent_lifetime(
|
2066
|
-
real, sqrt(real - real * real), frequency=harmonic
|
2067
|
-
)[0],
|
2068
|
-
)
|
2069
|
-
|
2070
|
-
Examples
|
2071
|
-
--------
|
2072
|
-
The phasor coordinates at higher harmonics are approaching the origin:
|
2073
|
-
|
2074
|
-
>>> phasor_at_harmonic(0.5, 1, [1, 2, 4, 8]) # doctest: +NUMBER
|
2075
|
-
(array([0.5, 0.2, 0.05882, 0.01538]), array([0.5, 0.4, 0.2353, 0.1231]))
|
2076
|
-
|
2077
|
-
"""
|
2078
|
-
harmonic = numpy.asarray(harmonic, dtype=numpy.int32)
|
2079
|
-
if numpy.any(harmonic < 1):
|
2080
|
-
raise ValueError('invalid harmonic')
|
2081
|
-
|
2082
|
-
other_harmonic = numpy.asarray(other_harmonic, dtype=numpy.int32)
|
2083
|
-
if numpy.any(other_harmonic < 1):
|
2084
|
-
raise ValueError('invalid other_harmonic')
|
2085
|
-
|
2086
|
-
return _phasor_at_harmonic( # type: ignore[no-any-return]
|
2087
|
-
real, harmonic, other_harmonic, **kwargs
|
2088
|
-
)
|
2089
|
-
|
2090
|
-
|
2091
|
-
def phasor_from_lifetime(
|
2092
|
-
frequency: ArrayLike,
|
2093
|
-
lifetime: ArrayLike,
|
2094
|
-
fraction: ArrayLike | None = None,
|
2095
|
-
*,
|
2096
|
-
preexponential: bool = False,
|
2097
|
-
unit_conversion: float = 1e-3,
|
2098
|
-
keepdims: bool = False,
|
2099
|
-
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
2100
|
-
r"""Return phasor coordinates from lifetime components.
|
2101
|
-
|
2102
|
-
Calculate phasor coordinates as a function of frequency, single or
|
2103
|
-
multiple lifetime components, and the pre-exponential amplitudes
|
2104
|
-
or fractional intensities of the components.
|
2105
|
-
|
2106
|
-
Parameters
|
2107
|
-
----------
|
2108
|
-
frequency : array_like
|
2109
|
-
Laser pulse or modulation frequency in MHz.
|
2110
|
-
A scalar or one-dimensional sequence.
|
2111
|
-
lifetime : array_like
|
2112
|
-
Lifetime components in ns. See notes below for allowed dimensions.
|
2113
|
-
fraction : array_like, optional
|
2114
|
-
Fractional intensities or pre-exponential amplitudes of the lifetime
|
2115
|
-
components. Fractions are normalized to sum to 1.
|
2116
|
-
See notes below for allowed dimensions.
|
2117
|
-
preexponential : bool, optional, default: False
|
2118
|
-
If true, `fraction` values are pre-exponential amplitudes,
|
2119
|
-
else fractional intensities.
|
2120
|
-
unit_conversion : float, optional, default: 1e-3
|
2121
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
2122
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2123
|
-
Use 1.0 for Hz and s.
|
2124
|
-
keepdims : bool, optional, default: False
|
2125
|
-
If true, length-one dimensions are left in phasor coordinates.
|
2126
|
-
|
2127
|
-
Returns
|
2128
|
-
-------
|
2129
|
-
real : ndarray
|
2130
|
-
Real component of phasor coordinates.
|
2131
|
-
imag : ndarray
|
2132
|
-
Imaginary component of phasor coordinates.
|
2133
|
-
|
2134
|
-
See notes below for dimensions of the returned arrays.
|
2135
|
-
|
2136
|
-
Raises
|
2137
|
-
------
|
2138
|
-
ValueError
|
2139
|
-
Input arrays exceed their allowed dimensionality or do not match.
|
2140
|
-
|
2141
|
-
Notes
|
2142
|
-
-----
|
2143
|
-
The phasor coordinates :math:`G` (`real`) and :math:`S` (`imag`) for
|
2144
|
-
many lifetime components :math:`j` with lifetimes :math:`\tau` and
|
2145
|
-
pre-exponential amplitudes :math:`\alpha` at frequency :math:`f` are:
|
2146
|
-
|
2147
|
-
.. math::
|
2148
|
-
|
2149
|
-
\omega &= 2 \pi f
|
2150
|
-
|
2151
|
-
g_{j} &= \alpha_{j} / (1 + (\omega \tau_{j})^2)
|
2152
|
-
|
2153
|
-
G &= \sum_{j} g_{j}
|
2154
|
-
|
2155
|
-
S &= \sum_{j} \omega \tau_{j} g_{j}
|
2156
|
-
|
2157
|
-
The relation between pre-exponential amplitudes :math:`a` and
|
2158
|
-
fractional intensities :math:`\alpha` is:
|
2159
|
-
|
2160
|
-
.. math::
|
2161
|
-
F_{DC} &= \sum_{j} a_{j} \tau_{j}
|
2162
|
-
|
2163
|
-
\alpha_{j} &= a_{j} \tau_{j} / F_{DC}
|
2164
|
-
|
2165
|
-
The following combinations of `lifetime` and `fraction` parameters are
|
2166
|
-
supported:
|
2167
|
-
|
2168
|
-
- `lifetime` is scalar or one-dimensional, holding single component
|
2169
|
-
lifetimes. `fraction` is None.
|
2170
|
-
Return arrays of shape `(frequency.size, lifetime.size)`.
|
2171
|
-
|
2172
|
-
- `lifetime` is two-dimensional, `fraction` is one-dimensional.
|
2173
|
-
The last dimensions match in size, holding lifetime components and
|
2174
|
-
their fractions.
|
2175
|
-
Return arrays of shape `(frequency.size, lifetime.shape[1])`.
|
2176
|
-
|
2177
|
-
- `lifetime` is one-dimensional, `fraction` is two-dimensional.
|
2178
|
-
The last dimensions must match in size, holding lifetime components and
|
2179
|
-
their fractions.
|
2180
|
-
Return arrays of shape `(frequency.size, fraction.shape[1])`.
|
2181
|
-
|
2182
|
-
- `lifetime` and `fraction` are up to two-dimensional of same shape.
|
2183
|
-
The last dimensions hold lifetime components and their fractions.
|
2184
|
-
Return arrays of shape `(frequency.size, lifetime.shape[0])`.
|
2185
|
-
|
2186
|
-
Length-one dimensions are removed from returned arrays
|
2187
|
-
if `keepdims` is false (default).
|
2188
|
-
|
2189
|
-
Examples
|
2190
|
-
--------
|
2191
|
-
Phasor coordinates of a single lifetime component (in ns) at a
|
2192
|
-
frequency of 80 MHz:
|
2193
|
-
|
2194
|
-
>>> phasor_from_lifetime(80.0, 1.9894368) # doctest: +NUMBER
|
2195
|
-
(0.5, 0.5)
|
2196
|
-
|
2197
|
-
Phasor coordinates of two lifetime components with equal fractional
|
2198
|
-
intensities:
|
2199
|
-
|
2200
|
-
>>> phasor_from_lifetime(
|
2201
|
-
... 80.0, [3.9788735, 0.9947183], [0.5, 0.5]
|
2202
|
-
... ) # doctest: +NUMBER
|
2203
|
-
(0.5, 0.4)
|
2204
|
-
|
2205
|
-
Phasor coordinates of two lifetime components with equal pre-exponential
|
2206
|
-
amplitudes:
|
2207
|
-
|
2208
|
-
>>> phasor_from_lifetime(
|
2209
|
-
... 80.0, [3.9788735, 0.9947183], [0.5, 0.5], preexponential=True
|
2210
|
-
... ) # doctest: +NUMBER
|
2211
|
-
(0.32, 0.4)
|
2212
|
-
|
2213
|
-
Phasor coordinates of many single-component lifetimes (fractions omitted):
|
2214
|
-
|
2215
|
-
>>> phasor_from_lifetime(
|
2216
|
-
... 80.0, [3.9788735, 1.9894368, 0.9947183]
|
2217
|
-
... ) # doctest: +NUMBER
|
2218
|
-
(array([0.2, 0.5, 0.8]), array([0.4, 0.5, 0.4]))
|
2219
|
-
|
2220
|
-
Phasor coordinates of two lifetime components with varying fractions:
|
2221
|
-
|
2222
|
-
>>> phasor_from_lifetime(
|
2223
|
-
... 80.0, [3.9788735, 0.9947183], [[1, 0], [0.5, 0.5], [0, 1]]
|
2224
|
-
... ) # doctest: +NUMBER
|
2225
|
-
(array([0.2, 0.5, 0.8]), array([0.4, 0.4, 0.4]))
|
2226
|
-
|
2227
|
-
Phasor coordinates of multiple two-component lifetimes with constant
|
2228
|
-
fractions, keeping dimensions:
|
2229
|
-
|
2230
|
-
>>> phasor_from_lifetime(
|
2231
|
-
... 80.0, [[3.9788735, 0.9947183], [1.9894368, 1.9894368]], [0.5, 0.5]
|
2232
|
-
... ) # doctest: +NUMBER
|
2233
|
-
(array([0.5, 0.5]), array([0.4, 0.5]))
|
2234
|
-
|
2235
|
-
Phasor coordinates of multiple two-component lifetimes with specific
|
2236
|
-
fractions at multiple frequencies. Frequencies are in Hz, lifetimes in ns:
|
2237
|
-
|
2238
|
-
>>> phasor_from_lifetime(
|
2239
|
-
... [40e6, 80e6],
|
2240
|
-
... [[1e-9, 0.9947183e-9], [3.9788735e-9, 0.9947183e-9]],
|
2241
|
-
... [[0, 1], [0.5, 0.5]],
|
2242
|
-
... unit_conversion=1.0,
|
2243
|
-
... ) # doctest: +NUMBER
|
2244
|
-
(array([[0.941, 0.721], [0.8, 0.5]]), array([[0.235, 0.368], [0.4, 0.4]]))
|
2245
|
-
|
2246
|
-
"""
|
2247
|
-
if unit_conversion < 1e-16:
|
2248
|
-
raise ValueError(f'{unit_conversion=} < 1e-16')
|
2249
|
-
frequency = numpy.atleast_1d(numpy.asarray(frequency, dtype=numpy.float64))
|
2250
|
-
if frequency.ndim != 1:
|
2251
|
-
raise ValueError('frequency is not one-dimensional array')
|
2252
|
-
lifetime = numpy.atleast_1d(numpy.asarray(lifetime, dtype=numpy.float64))
|
2253
|
-
if lifetime.ndim > 2:
|
2254
|
-
raise ValueError('lifetime must be one- or two-dimensional array')
|
2255
|
-
|
2256
|
-
if fraction is None:
|
2257
|
-
# single-component lifetimes
|
2258
|
-
if lifetime.ndim > 1:
|
2259
|
-
raise ValueError(
|
2260
|
-
'lifetime must be one-dimensional array if fraction is None'
|
2261
|
-
)
|
2262
|
-
lifetime = lifetime.reshape(-1, 1) # move components to last axis
|
2263
|
-
fraction = numpy.ones_like(lifetime) # not really used
|
2264
|
-
else:
|
2265
|
-
fraction = numpy.atleast_1d(
|
2266
|
-
numpy.asarray(fraction, dtype=numpy.float64)
|
2267
|
-
)
|
2268
|
-
if fraction.ndim > 2:
|
2269
|
-
raise ValueError('fraction must be one- or two-dimensional array')
|
2270
|
-
|
2271
|
-
if lifetime.ndim == 1 and fraction.ndim == 1:
|
2272
|
-
# one multi-component lifetime
|
2273
|
-
if lifetime.shape != fraction.shape:
|
2274
|
-
raise ValueError(
|
2275
|
-
f'{lifetime.shape=} does not match {fraction.shape=}'
|
2276
|
-
)
|
2277
|
-
lifetime = lifetime.reshape(1, -1)
|
2278
|
-
fraction = fraction.reshape(1, -1)
|
2279
|
-
nvar = 1
|
2280
|
-
elif lifetime.ndim == 2 and fraction.ndim == 2:
|
2281
|
-
# multiple, multi-component lifetimes
|
2282
|
-
if lifetime.shape[1] != fraction.shape[1]:
|
2283
|
-
raise ValueError(f'{lifetime.shape[1]=} != {fraction.shape[1]=}')
|
2284
|
-
nvar = lifetime.shape[0]
|
2285
|
-
elif lifetime.ndim == 2 and fraction.ndim == 1:
|
2286
|
-
# variable components, same fractions
|
2287
|
-
fraction = fraction.reshape(1, -1)
|
2288
|
-
nvar = lifetime.shape[0]
|
2289
|
-
elif lifetime.ndim == 1 and fraction.ndim == 2:
|
2290
|
-
# same components, varying fractions
|
2291
|
-
lifetime = lifetime.reshape(1, -1)
|
2292
|
-
nvar = fraction.shape[0]
|
2293
|
-
else:
|
2294
|
-
# unreachable code
|
2295
|
-
raise RuntimeError(f'{lifetime.shape=}, {fraction.shape=}')
|
2296
|
-
|
2297
|
-
phasor = numpy.empty((2, frequency.size, nvar), dtype=numpy.float64)
|
2298
|
-
|
2299
|
-
_phasor_from_lifetime(
|
2300
|
-
phasor, frequency, lifetime, fraction, unit_conversion, preexponential
|
2301
|
-
)
|
2302
|
-
|
2303
|
-
if not keepdims:
|
2304
|
-
phasor = phasor.squeeze()
|
2305
|
-
return phasor[0], phasor[1]
|
2306
|
-
|
2307
|
-
|
2308
|
-
def polar_to_apparent_lifetime(
|
2309
|
-
phase: ArrayLike,
|
2310
|
-
modulation: ArrayLike,
|
2311
|
-
/,
|
2312
|
-
frequency: ArrayLike,
|
2313
|
-
*,
|
2314
|
-
unit_conversion: float = 1e-3,
|
2315
|
-
**kwargs: Any,
|
2316
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2317
|
-
r"""Return apparent single lifetimes from polar coordinates.
|
2318
|
-
|
2319
|
-
Parameters
|
2320
|
-
----------
|
2321
|
-
phase : array_like
|
2322
|
-
Angular component of polar coordinates.
|
2323
|
-
imag : array_like
|
2324
|
-
Radial component of polar coordinates.
|
2325
|
-
frequency : array_like
|
2326
|
-
Laser pulse or modulation frequency in MHz.
|
2327
|
-
unit_conversion : float, optional
|
2328
|
-
Product of `frequency` and returned `lifetime` units' prefix factors.
|
2329
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2330
|
-
Use 1.0 for Hz and s.
|
2331
|
-
**kwargs
|
2332
|
-
Optional `arguments passed to numpy universal functions
|
2333
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2334
|
-
|
2335
|
-
Returns
|
2336
|
-
-------
|
2337
|
-
phase_lifetime : ndarray
|
2338
|
-
Apparent single lifetime from `phase`.
|
2339
|
-
modulation_lifetime : ndarray
|
2340
|
-
Apparent single lifetime from `modulation`.
|
2341
|
-
|
2342
|
-
See Also
|
2343
|
-
--------
|
2344
|
-
phasorpy.phasor.polar_from_apparent_lifetime
|
2345
|
-
|
2346
|
-
Notes
|
2347
|
-
-----
|
2348
|
-
The polar coordinates `phase` (:math:`\phi`) and `modulation` (:math:`M`)
|
2349
|
-
are converted to apparent single lifetimes
|
2350
|
-
`phase_lifetime` (:math:`\tau_{\phi}`) and
|
2351
|
-
`modulation_lifetime` (:math:`\tau_{M}`) at frequency :math:`f`
|
2352
|
-
according to:
|
2353
|
-
|
2354
|
-
.. math::
|
2355
|
-
|
2356
|
-
\omega &= 2 \pi f
|
2357
|
-
|
2358
|
-
\tau_{\phi} &= \omega^{-1} \cdot \tan{\phi}
|
2359
|
-
|
2360
|
-
\tau_{M} &= \omega^{-1} \cdot \sqrt{1 / M^2 - 1}
|
2361
|
-
|
2362
|
-
Examples
|
2363
|
-
--------
|
2364
|
-
The apparent single lifetimes from phase and modulation are equal
|
2365
|
-
only if the polar coordinates lie on the universal semicircle:
|
2366
|
-
|
2367
|
-
>>> polar_to_apparent_lifetime(
|
2368
|
-
... math.pi / 4, numpy.hypot([0.5, 0.45], [0.5, 0.45]), frequency=80
|
2369
|
-
... ) # doctest: +NUMBER
|
2370
|
-
(array([1.989, 1.989]), array([1.989, 2.411]))
|
2371
|
-
|
2372
|
-
"""
|
2373
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
2374
|
-
omega *= math.pi * 2.0 * unit_conversion
|
2375
|
-
return _polar_to_apparent_lifetime( # type: ignore[no-any-return]
|
2376
|
-
phase, modulation, omega, **kwargs
|
2377
|
-
)
|
2378
|
-
|
2379
|
-
|
2380
|
-
def polar_from_apparent_lifetime(
|
2381
|
-
phase_lifetime: ArrayLike,
|
2382
|
-
modulation_lifetime: ArrayLike | None,
|
2383
|
-
/,
|
2384
|
-
frequency: ArrayLike,
|
2385
|
-
*,
|
2386
|
-
unit_conversion: float = 1e-3,
|
2387
|
-
**kwargs: Any,
|
2388
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2389
|
-
r"""Return polar coordinates from apparent single lifetimes.
|
2390
|
-
|
2391
|
-
Parameters
|
2392
|
-
----------
|
2393
|
-
phase_lifetime : ndarray
|
2394
|
-
Apparent single lifetime from phase.
|
2395
|
-
modulation_lifetime : ndarray, optional
|
2396
|
-
Apparent single lifetime from modulation.
|
2397
|
-
If None, `modulation_lifetime` is same as `phase_lifetime`.
|
2398
|
-
frequency : array_like
|
2399
|
-
Laser pulse or modulation frequency in MHz.
|
2400
|
-
unit_conversion : float, optional
|
2401
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
2402
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2403
|
-
Use 1.0 for Hz and s.
|
2404
|
-
**kwargs
|
2405
|
-
Optional `arguments passed to numpy universal functions
|
2406
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2407
|
-
|
2408
|
-
Returns
|
2409
|
-
-------
|
2410
|
-
phase : ndarray
|
2411
|
-
Angular component of polar coordinates.
|
2412
|
-
modulation : ndarray
|
2413
|
-
Radial component of polar coordinates.
|
2414
|
-
|
2415
|
-
See Also
|
2416
|
-
--------
|
2417
|
-
phasorpy.phasor.polar_to_apparent_lifetime
|
2418
|
-
|
2419
|
-
Notes
|
2420
|
-
-----
|
2421
|
-
The apparent single lifetimes `phase_lifetime` (:math:`\tau_{\phi}`)
|
2422
|
-
and `modulation_lifetime` (:math:`\tau_{M}`) are converted to polar
|
2423
|
-
coordinates `phase` (:math:`\phi`) and `modulation` (:math:`M`) at
|
2424
|
-
frequency :math:`f` according to:
|
2425
|
-
|
2426
|
-
.. math::
|
2427
|
-
|
2428
|
-
\omega &= 2 \pi f
|
2429
|
-
|
2430
|
-
\phi & = \arctan(\omega \tau_{\phi})
|
2431
|
-
|
2432
|
-
M &= 1 / \sqrt{1 + (\omega \tau_{M})^2}
|
2433
|
-
|
2434
|
-
Examples
|
2435
|
-
--------
|
2436
|
-
If the apparent single lifetimes from phase and modulation are equal,
|
2437
|
-
the polar coordinates lie on the universal semicircle, else inside:
|
2438
|
-
|
2439
|
-
>>> polar_from_apparent_lifetime(
|
2440
|
-
... 1.9894, [1.9894, 2.4113], frequency=80.0
|
2441
|
-
... ) # doctest: +NUMBER
|
2442
|
-
(array([0.7854, 0.7854]), array([0.7071, 0.6364]))
|
2443
|
-
|
2444
|
-
"""
|
2445
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
2446
|
-
omega *= math.pi * 2.0 * unit_conversion
|
2447
|
-
if modulation_lifetime is None:
|
2448
|
-
return _polar_from_single_lifetime( # type: ignore[no-any-return]
|
2449
|
-
phase_lifetime, omega, **kwargs
|
2450
|
-
)
|
2451
|
-
return _polar_from_apparent_lifetime( # type: ignore[no-any-return]
|
2452
|
-
phase_lifetime, modulation_lifetime, omega, **kwargs
|
2453
|
-
)
|
2454
|
-
|
2455
|
-
|
2456
|
-
def phasor_from_fret_donor(
|
2457
|
-
frequency: ArrayLike,
|
2458
|
-
donor_lifetime: ArrayLike,
|
2459
|
-
*,
|
2460
|
-
fret_efficiency: ArrayLike = 0.0,
|
2461
|
-
donor_fretting: ArrayLike = 1.0,
|
2462
|
-
donor_background: ArrayLike = 0.0,
|
2463
|
-
background_real: ArrayLike = 0.0,
|
2464
|
-
background_imag: ArrayLike = 0.0,
|
2465
|
-
unit_conversion: float = 1e-3,
|
2466
|
-
**kwargs: Any,
|
2467
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2468
|
-
"""Return phasor coordinates of FRET donor channel.
|
2469
|
-
|
2470
|
-
Calculate phasor coordinates of a FRET (Förster Resonance Energy Transfer)
|
2471
|
-
donor channel as a function of frequency, donor lifetime, FRET efficiency,
|
2472
|
-
fraction of donors undergoing FRET, and background fluorescence.
|
2473
|
-
|
2474
|
-
The phasor coordinates of the donor channel contain fractions of:
|
2475
|
-
|
2476
|
-
- donor not undergoing energy transfer
|
2477
|
-
- donor quenched by energy transfer
|
2478
|
-
- background fluorescence
|
2479
|
-
|
2480
|
-
Parameters
|
2481
|
-
----------
|
2482
|
-
frequency : array_like
|
2483
|
-
Laser pulse or modulation frequency in MHz.
|
2484
|
-
donor_lifetime : array_like
|
2485
|
-
Lifetime of donor without FRET in ns.
|
2486
|
-
fret_efficiency : array_like, optional, default 0
|
2487
|
-
FRET efficiency in range [0, 1].
|
2488
|
-
donor_fretting : array_like, optional, default 1
|
2489
|
-
Fraction of donors participating in FRET. Range [0, 1].
|
2490
|
-
donor_background : array_like, optional, default 0
|
2491
|
-
Weight of background fluorescence in donor channel
|
2492
|
-
relative to fluorescence of donor without FRET.
|
2493
|
-
A weight of 1 means the fluorescence of background and donor
|
2494
|
-
without FRET are equal.
|
2495
|
-
background_real : array_like, optional, default 0
|
2496
|
-
Real component of background fluorescence phasor coordinate
|
2497
|
-
at `frequency`.
|
2498
|
-
background_imag : array_like, optional, default 0
|
2499
|
-
Imaginary component of background fluorescence phasor coordinate
|
2500
|
-
at `frequency`.
|
2501
|
-
unit_conversion : float, optional
|
2502
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
2503
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2504
|
-
Use 1.0 for Hz and s.
|
2505
|
-
**kwargs
|
2506
|
-
Optional `arguments passed to numpy universal functions
|
2507
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2508
|
-
|
2509
|
-
Returns
|
2510
|
-
-------
|
2511
|
-
real : ndarray
|
2512
|
-
Real component of donor channel phasor coordinates.
|
2513
|
-
imag : ndarray
|
2514
|
-
Imaginary component of donor channel phasor coordinates.
|
2515
|
-
|
2516
|
-
See Also
|
2517
|
-
--------
|
2518
|
-
phasorpy.phasor.phasor_from_fret_acceptor
|
2519
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
|
2520
|
-
|
2521
|
-
Examples
|
2522
|
-
--------
|
2523
|
-
Compute the phasor coordinates of a FRET donor channel at three
|
2524
|
-
FRET efficiencies:
|
2525
|
-
|
2526
|
-
>>> phasor_from_fret_donor(
|
2527
|
-
... frequency=80,
|
2528
|
-
... donor_lifetime=4.2,
|
2529
|
-
... fret_efficiency=[0.0, 0.3, 1.0],
|
2530
|
-
... donor_fretting=0.9,
|
2531
|
-
... donor_background=0.1,
|
2532
|
-
... background_real=0.11,
|
2533
|
-
... background_imag=0.12,
|
2534
|
-
... ) # doctest: +NUMBER
|
2535
|
-
(array([0.1766, 0.2737, 0.1466]), array([0.3626, 0.4134, 0.2534]))
|
2536
|
-
|
2537
|
-
"""
|
2538
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
2539
|
-
omega *= math.pi * 2.0 * unit_conversion
|
2540
|
-
return _phasor_from_fret_donor( # type: ignore[no-any-return]
|
2541
|
-
omega,
|
2542
|
-
donor_lifetime,
|
2543
|
-
fret_efficiency,
|
2544
|
-
donor_fretting,
|
2545
|
-
donor_background,
|
2546
|
-
background_real,
|
2547
|
-
background_imag,
|
2548
|
-
**kwargs,
|
2549
|
-
)
|
2550
|
-
|
2551
|
-
|
2552
|
-
def phasor_from_fret_acceptor(
|
2553
|
-
frequency: ArrayLike,
|
2554
|
-
donor_lifetime: ArrayLike,
|
2555
|
-
acceptor_lifetime: ArrayLike,
|
2556
|
-
*,
|
2557
|
-
fret_efficiency: ArrayLike = 0.0,
|
2558
|
-
donor_fretting: ArrayLike = 1.0,
|
2559
|
-
donor_bleedthrough: ArrayLike = 0.0,
|
2560
|
-
acceptor_bleedthrough: ArrayLike = 0.0,
|
2561
|
-
acceptor_background: ArrayLike = 0.0,
|
2562
|
-
background_real: ArrayLike = 0.0,
|
2563
|
-
background_imag: ArrayLike = 0.0,
|
2564
|
-
unit_conversion: float = 1e-3,
|
2565
|
-
**kwargs: Any,
|
2566
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2567
|
-
"""Return phasor coordinates of FRET acceptor channel.
|
2568
|
-
|
2569
|
-
Calculate phasor coordinates of a FRET (Förster Resonance Energy Transfer)
|
2570
|
-
acceptor channel as a function of frequency, donor and acceptor lifetimes,
|
2571
|
-
FRET efficiency, fraction of donors undergoing FRET, fraction of directly
|
2572
|
-
excited acceptors, fraction of donor fluorescence in acceptor channel,
|
2573
|
-
and background fluorescence.
|
2574
|
-
|
2575
|
-
The phasor coordinates of the acceptor channel contain fractions of:
|
2576
|
-
|
2577
|
-
- acceptor sensitized by energy transfer
|
2578
|
-
- directly excited acceptor
|
2579
|
-
- donor bleedthrough
|
2580
|
-
- background fluorescence
|
2581
|
-
|
2582
|
-
Parameters
|
2583
|
-
----------
|
2584
|
-
frequency : array_like
|
2585
|
-
Laser pulse or modulation frequency in MHz.
|
2586
|
-
donor_lifetime : array_like
|
2587
|
-
Lifetime of donor without FRET in ns.
|
2588
|
-
acceptor_lifetime : array_like
|
2589
|
-
Lifetime of acceptor in ns.
|
2590
|
-
fret_efficiency : array_like, optional, default 0
|
2591
|
-
FRET efficiency in range [0, 1].
|
2592
|
-
donor_fretting : array_like, optional, default 1
|
2593
|
-
Fraction of donors participating in FRET. Range [0, 1].
|
2594
|
-
donor_bleedthrough : array_like, optional, default 0
|
2595
|
-
Weight of donor fluorescence in acceptor channel
|
2596
|
-
relative to fluorescence of fully sensitized acceptor.
|
2597
|
-
A weight of 1 means the fluorescence from donor and fully sensitized
|
2598
|
-
acceptor are equal.
|
2599
|
-
The background in the donor channel does not bleed through.
|
2600
|
-
acceptor_bleedthrough : array_like, optional, default 0
|
2601
|
-
Weight of fluorescence from directly excited acceptor
|
2602
|
-
relative to fluorescence of fully sensitized acceptor.
|
2603
|
-
A weight of 1 means the fluorescence from directly excited acceptor
|
2604
|
-
and fully sensitized acceptor are equal.
|
2605
|
-
acceptor_background : array_like, optional, default 0
|
2606
|
-
Weight of background fluorescence in acceptor channel
|
2607
|
-
relative to fluorescence of fully sensitized acceptor.
|
2608
|
-
A weight of 1 means the fluorescence of background and fully
|
2609
|
-
sensitized acceptor are equal.
|
2610
|
-
background_real : array_like, optional, default 0
|
2611
|
-
Real component of background fluorescence phasor coordinate
|
2612
|
-
at `frequency`.
|
2613
|
-
background_imag : array_like, optional, default 0
|
2614
|
-
Imaginary component of background fluorescence phasor coordinate
|
2615
|
-
at `frequency`.
|
2616
|
-
unit_conversion : float, optional
|
2617
|
-
Product of `frequency` and `lifetime` units' prefix factors.
|
2618
|
-
The default is 1e-3 for MHz and ns, or Hz and ms.
|
2619
|
-
Use 1.0 for Hz and s.
|
2620
|
-
**kwargs
|
2621
|
-
Optional `arguments passed to numpy universal functions
|
2622
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
2623
|
-
|
2624
|
-
Returns
|
2625
|
-
-------
|
2626
|
-
real : ndarray
|
2627
|
-
Real component of acceptor channel phasor coordinates.
|
2628
|
-
imag : ndarray
|
2629
|
-
Imaginary component of acceptor channel phasor coordinates.
|
2630
|
-
|
2631
|
-
See Also
|
2632
|
-
--------
|
2633
|
-
phasorpy.phasor.phasor_from_fret_donor
|
2634
|
-
:ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
|
2635
|
-
|
2636
|
-
Examples
|
2637
|
-
--------
|
2638
|
-
Compute the phasor coordinates of a FRET acceptor channel at three
|
2639
|
-
FRET efficiencies:
|
2640
|
-
|
2641
|
-
>>> phasor_from_fret_acceptor(
|
2642
|
-
... frequency=80,
|
2643
|
-
... donor_lifetime=4.2,
|
2644
|
-
... acceptor_lifetime=3.0,
|
2645
|
-
... fret_efficiency=[0.0, 0.3, 1.0],
|
2646
|
-
... donor_fretting=0.9,
|
2647
|
-
... donor_bleedthrough=0.1,
|
2648
|
-
... acceptor_bleedthrough=0.1,
|
2649
|
-
... acceptor_background=0.1,
|
2650
|
-
... background_real=0.11,
|
2651
|
-
... background_imag=0.12,
|
2652
|
-
... ) # doctest: +NUMBER
|
2653
|
-
(array([0.1996, 0.05772, 0.2867]), array([0.3225, 0.3103, 0.4292]))
|
2654
|
-
|
2655
|
-
"""
|
2656
|
-
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
|
2657
|
-
omega *= math.pi * 2.0 * unit_conversion
|
2658
|
-
return _phasor_from_fret_acceptor( # type: ignore[no-any-return]
|
2659
|
-
omega,
|
2660
|
-
donor_lifetime,
|
2661
|
-
acceptor_lifetime,
|
2662
|
-
fret_efficiency,
|
2663
|
-
donor_fretting,
|
2664
|
-
donor_bleedthrough,
|
2665
|
-
acceptor_bleedthrough,
|
2666
|
-
acceptor_background,
|
2667
|
-
background_real,
|
2668
|
-
background_imag,
|
2669
|
-
**kwargs,
|
2670
|
-
)
|
2671
|
-
|
2672
|
-
|
2673
|
-
def phasor_to_principal_plane(
|
2674
|
-
real: ArrayLike,
|
2675
|
-
imag: ArrayLike,
|
2676
|
-
/,
|
2677
|
-
*,
|
2678
|
-
reorient: bool = True,
|
2679
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2680
|
-
"""Return multi-harmonic phasor coordinates projected onto principal plane.
|
2681
|
-
|
2682
|
-
Principal Component Analysis (PCA) is used to project
|
2683
|
-
multi-harmonic phasor coordinates onto a plane, along which
|
2684
|
-
coordinate axes the phasor coordinates have the largest variations.
|
2685
|
-
|
2686
|
-
The transformed coordinates are not phasor coordinates. However, the
|
2687
|
-
coordinates can be used in visualization and cursor analysis since
|
2688
|
-
the transformation is affine (preserving collinearity and ratios
|
2689
|
-
of distances).
|
2690
|
-
|
2691
|
-
Parameters
|
2692
|
-
----------
|
2693
|
-
real : array_like
|
2694
|
-
Real component of multi-harmonic phasor coordinates.
|
2695
|
-
The first axis is the frequency dimension.
|
2696
|
-
If less than 2-dimensional, size-1 dimensions are prepended.
|
978
|
+
Real component of multi-harmonic phasor coordinates.
|
979
|
+
The first axis is the frequency dimension.
|
980
|
+
If less than 2-dimensional, size-1 dimensions are prepended.
|
2697
981
|
imag : array_like
|
2698
982
|
Imaginary component of multi-harmonic phasor coordinates.
|
2699
983
|
Must be of same shape as `real`.
|
@@ -2724,7 +1008,7 @@ def phasor_to_principal_plane(
|
|
2724
1008
|
-----
|
2725
1009
|
|
2726
1010
|
This implementation does not work with coordinates containing
|
2727
|
-
undefined
|
1011
|
+
undefined NaN values.
|
2728
1012
|
|
2729
1013
|
The transformation matrix can be used to project multi-harmonic phasor
|
2730
1014
|
coordinates, where the first axis is the frequency:
|
@@ -2744,7 +1028,6 @@ def phasor_to_principal_plane(
|
|
2744
1028
|
|
2745
1029
|
References
|
2746
1030
|
----------
|
2747
|
-
|
2748
1031
|
.. [1] Franssen WMJ, Vergeldt FJ, Bader AN, van Amerongen H, and Terenzi C.
|
2749
1032
|
`Full-harmonics phasor analysis: unravelling multiexponential trends
|
2750
1033
|
in magnetic resonance imaging data
|
@@ -2777,7 +1060,7 @@ def phasor_to_principal_plane(
|
|
2777
1060
|
im = im.reshape(im.shape[0], -1)
|
2778
1061
|
|
2779
1062
|
# vector of multi-frequency phasor coordinates
|
2780
|
-
coordinates = numpy.vstack(
|
1063
|
+
coordinates = numpy.vstack([re, im])
|
2781
1064
|
|
2782
1065
|
# vector of centered coordinates
|
2783
1066
|
center = numpy.nanmean(coordinates, axis=1, keepdims=True)
|
@@ -2877,7 +1160,7 @@ def phasor_filter_median(
|
|
2877
1160
|
use_scipy : bool, optional
|
2878
1161
|
Use :py:func:`scipy.ndimage.median_filter`.
|
2879
1162
|
This function has undefined behavior if the input arrays contain
|
2880
|
-
|
1163
|
+
NaN values but is faster when filtering more than 2 dimensions.
|
2881
1164
|
See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
|
2882
1165
|
num_threads : int, optional
|
2883
1166
|
Number of OpenMP threads to use for parallelization.
|
@@ -3099,7 +1382,6 @@ def phasor_filter_pawflim(
|
|
3099
1382
|
|
3100
1383
|
References
|
3101
1384
|
----------
|
3102
|
-
|
3103
1385
|
.. [2] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
|
3104
1386
|
uncertainty to enable lower photon count in FLIM experiments
|
3105
1387
|
<https://doi.org/10.1088/2050-6120/aa72ab>`_.
|
@@ -3254,12 +1536,12 @@ def phasor_threshold(
|
|
3254
1536
|
detect_harmonics: bool = True,
|
3255
1537
|
**kwargs: Any,
|
3256
1538
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
3257
|
-
"""Return phasor coordinates with values
|
1539
|
+
"""Return phasor coordinates with values outside interval replaced by NaN.
|
3258
1540
|
|
3259
1541
|
Interval thresholds can be set for mean intensity, real and imaginary
|
3260
1542
|
coordinates, and phase and modulation.
|
3261
1543
|
Phasor coordinates smaller than minimum thresholds or larger than maximum
|
3262
|
-
thresholds are replaced NaN.
|
1544
|
+
thresholds are replaced with NaN.
|
3263
1545
|
No threshold is applied by default.
|
3264
1546
|
NaNs in `mean` or any `real` and `imag` harmonic are propagated to
|
3265
1547
|
`mean` and all harmonics in `real` and `imag`.
|
@@ -3452,6 +1734,154 @@ def phasor_threshold(
|
|
3452
1734
|
return mean, real, imag
|
3453
1735
|
|
3454
1736
|
|
1737
|
+
def phasor_nearest_neighbor(
|
1738
|
+
real: ArrayLike,
|
1739
|
+
imag: ArrayLike,
|
1740
|
+
neighbor_real: ArrayLike,
|
1741
|
+
neighbor_imag: ArrayLike,
|
1742
|
+
/,
|
1743
|
+
*,
|
1744
|
+
values: ArrayLike | None = None,
|
1745
|
+
dtype: DTypeLike | None = None,
|
1746
|
+
distance_max: float | None = None,
|
1747
|
+
num_threads: int | None = None,
|
1748
|
+
) -> NDArray[Any]:
|
1749
|
+
"""Return indices or values of nearest neighbors from other coordinates.
|
1750
|
+
|
1751
|
+
For each phasor coordinate, find the nearest neighbor in another set of
|
1752
|
+
phasor coordinates and return its flat index. If more than one neighbor
|
1753
|
+
has the same distance, return the smallest index.
|
1754
|
+
|
1755
|
+
For phasor coordinates that are NaN, or have a distance to the nearest
|
1756
|
+
neighbor that is larger than `distance_max`, return an index of -1.
|
1757
|
+
|
1758
|
+
If `values` are provided, return the values corresponding to the nearest
|
1759
|
+
neighbor coordinates instead of indices. Return NaN values for indices
|
1760
|
+
that are -1.
|
1761
|
+
|
1762
|
+
This function does not support multi-harmonic, multi-channel, or
|
1763
|
+
multi-frequency phasor coordinates.
|
1764
|
+
|
1765
|
+
Parameters
|
1766
|
+
----------
|
1767
|
+
real : array_like
|
1768
|
+
Real component of phasor coordinates.
|
1769
|
+
imag : array_like
|
1770
|
+
Imaginary component of phasor coordinates.
|
1771
|
+
neighbor_real : array_like
|
1772
|
+
Real component of neighbor phasor coordinates.
|
1773
|
+
neighbor_imag : array_like
|
1774
|
+
Imaginary component of neighbor phasor coordinates.
|
1775
|
+
values : array_like, optional
|
1776
|
+
Array of values corresponding to neighbor coordinates.
|
1777
|
+
If provided, return the values corresponding to the nearest
|
1778
|
+
neighbor coordinates.
|
1779
|
+
distance_max : float, optional
|
1780
|
+
Maximum Euclidean distance to consider a neighbor valid.
|
1781
|
+
By default, all neighbors are considered.
|
1782
|
+
dtype : dtype_like, optional
|
1783
|
+
Floating point data type used for calculation and output values.
|
1784
|
+
Either `float32` or `float64`. The default is `float64`.
|
1785
|
+
num_threads : int, optional
|
1786
|
+
Number of OpenMP threads to use for parallelization.
|
1787
|
+
By default, multi-threading is disabled.
|
1788
|
+
If zero, up to half of logical CPUs are used.
|
1789
|
+
OpenMP may not be available on all platforms.
|
1790
|
+
|
1791
|
+
Returns
|
1792
|
+
-------
|
1793
|
+
nearest : ndarray
|
1794
|
+
Flat indices (or the corresponding values if provided) of the nearest
|
1795
|
+
neighbor coordinates.
|
1796
|
+
|
1797
|
+
Raises
|
1798
|
+
------
|
1799
|
+
ValueError
|
1800
|
+
If the shapes of `real`, and `imag` do not match.
|
1801
|
+
If the shapes of `neighbor_real` and `neighbor_imag` do not match.
|
1802
|
+
If the shapes of `values` and `neighbor_real` do not match.
|
1803
|
+
If `distance_max` is less than or equal to zero.
|
1804
|
+
|
1805
|
+
See Also
|
1806
|
+
--------
|
1807
|
+
:ref:`sphx_glr_tutorials_applications_phasorpy_fret_efficiency.py`
|
1808
|
+
|
1809
|
+
Notes
|
1810
|
+
-----
|
1811
|
+
This function uses linear search, which is inefficient for large
|
1812
|
+
number of coordinates or neighbors.
|
1813
|
+
``scipy.spatial.KDTree.query()`` would be more efficient in those cases.
|
1814
|
+
However, KDTree is known to return non-deterministic results in case of
|
1815
|
+
multiple neighbors with the same distance.
|
1816
|
+
|
1817
|
+
Examples
|
1818
|
+
--------
|
1819
|
+
>>> phasor_nearest_neighbor(
|
1820
|
+
... [0.1, 0.5, numpy.nan],
|
1821
|
+
... [0.1, 0.5, numpy.nan],
|
1822
|
+
... [0, 0.4],
|
1823
|
+
... [0, 0.4],
|
1824
|
+
... values=[10, 20],
|
1825
|
+
... )
|
1826
|
+
array([10, 20, nan])
|
1827
|
+
|
1828
|
+
"""
|
1829
|
+
dtype = numpy.dtype(dtype)
|
1830
|
+
if dtype.char not in {'f', 'd'}:
|
1831
|
+
raise ValueError(f'{dtype=} is not a floating point type')
|
1832
|
+
|
1833
|
+
real = numpy.ascontiguousarray(real, dtype=dtype)
|
1834
|
+
imag = numpy.ascontiguousarray(imag, dtype=dtype)
|
1835
|
+
neighbor_real = numpy.ascontiguousarray(neighbor_real, dtype=dtype)
|
1836
|
+
neighbor_imag = numpy.ascontiguousarray(neighbor_imag, dtype=dtype)
|
1837
|
+
|
1838
|
+
if real.shape != imag.shape:
|
1839
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
1840
|
+
if neighbor_real.shape != neighbor_imag.shape:
|
1841
|
+
raise ValueError(f'{neighbor_real.shape=} != {neighbor_imag.shape=}')
|
1842
|
+
|
1843
|
+
shape = real.shape
|
1844
|
+
real = real.ravel()
|
1845
|
+
imag = imag.ravel()
|
1846
|
+
neighbor_real = neighbor_real.ravel()
|
1847
|
+
neighbor_imag = neighbor_imag.ravel()
|
1848
|
+
|
1849
|
+
indices = numpy.empty(
|
1850
|
+
real.shape, numpy.min_scalar_type(-neighbor_real.size)
|
1851
|
+
)
|
1852
|
+
|
1853
|
+
if distance_max is None:
|
1854
|
+
distance_max = numpy.inf
|
1855
|
+
else:
|
1856
|
+
distance_max = float(distance_max)
|
1857
|
+
if distance_max <= 0:
|
1858
|
+
raise ValueError(f'{distance_max=} <= 0')
|
1859
|
+
|
1860
|
+
num_threads = number_threads(num_threads)
|
1861
|
+
|
1862
|
+
_nearest_neighbor_2d(
|
1863
|
+
indices,
|
1864
|
+
real,
|
1865
|
+
imag,
|
1866
|
+
neighbor_real,
|
1867
|
+
neighbor_imag,
|
1868
|
+
distance_max,
|
1869
|
+
num_threads,
|
1870
|
+
)
|
1871
|
+
|
1872
|
+
if values is None:
|
1873
|
+
return numpy.asarray(indices.reshape(shape))
|
1874
|
+
|
1875
|
+
values = numpy.ascontiguousarray(values, dtype=dtype).ravel()
|
1876
|
+
if values.shape != neighbor_real.shape:
|
1877
|
+
raise ValueError(f'{values.shape=} != {neighbor_real.shape=}')
|
1878
|
+
|
1879
|
+
nearest_values = values[indices]
|
1880
|
+
nearest_values[indices == -1] = numpy.nan
|
1881
|
+
|
1882
|
+
return numpy.asarray(nearest_values.reshape(shape))
|
1883
|
+
|
1884
|
+
|
3455
1885
|
def phasor_center(
|
3456
1886
|
mean: ArrayLike,
|
3457
1887
|
real: ArrayLike,
|