phasorpy 0.6__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/phasor.py CHANGED
@@ -1,4 +1,4 @@
1
- """Calculate, convert, calibrate, and reduce phasor coordinates.
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 or lifetimes:
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,18 @@ The ``phasorpy.phasor`` module provides functions to:
33
22
  - :py:func:`phasor_divide`
34
23
  - :py:func:`phasor_normalize`
35
24
 
36
- - calibrate phasor coordinates with reference of known fluorescence
37
- lifetime:
38
-
39
- - :py:func:`phasor_calibrate`
40
- - :py:func:`polar_from_reference`
41
- - :py:func:`polar_from_reference_phasor`
42
-
43
25
  - reduce dimensionality of arrays of phasor coordinates:
44
26
 
45
27
  - :py:func:`phasor_center`
46
28
  - :py:func:`phasor_to_principal_plane`
47
29
 
48
- - calculate phasor coordinates for FRET donor and acceptor channels:
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
30
  - filter phasor coordinates:
68
31
 
69
32
  - :py:func:`phasor_filter_median`
70
33
  - :py:func:`phasor_filter_pawflim`
71
34
  - :py:func:`phasor_threshold`
72
35
 
73
- - find nearest neighbor phasor coordinates from another set of phasor coordinates:
36
+ - find nearest neighbor phasor coordinates from other phasor coordinates:
74
37
 
75
38
  - :py:func:`phasor_nearest_neighbor`
76
39
 
@@ -79,40 +42,21 @@ The ``phasorpy.phasor`` module provides functions to:
79
42
  from __future__ import annotations
80
43
 
81
44
  __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
45
  'phasor_center',
90
46
  'phasor_divide',
91
47
  'phasor_filter_median',
92
48
  'phasor_filter_pawflim',
93
- 'phasor_from_apparent_lifetime',
94
- 'phasor_from_fret_acceptor',
95
- 'phasor_from_fret_donor',
96
- 'phasor_from_lifetime',
97
49
  'phasor_from_polar',
98
50
  'phasor_from_signal',
99
51
  'phasor_multiply',
100
52
  'phasor_nearest_neighbor',
101
53
  'phasor_normalize',
102
- 'phasor_semicircle',
103
- 'phasor_semicircle_intersect',
104
54
  'phasor_threshold',
105
- 'phasor_to_apparent_lifetime',
106
55
  'phasor_to_complex',
107
- 'phasor_to_normal_lifetime',
108
56
  'phasor_to_polar',
109
57
  'phasor_to_principal_plane',
110
58
  'phasor_to_signal',
111
59
  'phasor_transform',
112
- 'polar_from_apparent_lifetime',
113
- 'polar_from_reference',
114
- 'polar_from_reference_phasor',
115
- 'polar_to_apparent_lifetime',
116
60
  ]
117
61
 
118
62
  import math
@@ -132,35 +76,20 @@ if TYPE_CHECKING:
132
76
  import numpy
133
77
 
134
78
  from ._phasorpy import (
135
- _gaussian_signal,
136
- _intersect_semicircle_line,
137
79
  _median_filter_2d,
138
80
  _nearest_neighbor_2d,
139
- _phasor_at_harmonic,
140
81
  _phasor_divide,
141
- _phasor_from_apparent_lifetime,
142
- _phasor_from_fret_acceptor,
143
- _phasor_from_fret_donor,
144
- _phasor_from_lifetime,
145
82
  _phasor_from_polar,
146
83
  _phasor_from_signal,
147
- _phasor_from_single_lifetime,
148
84
  _phasor_multiply,
149
85
  _phasor_threshold_closed,
150
86
  _phasor_threshold_mean_closed,
151
87
  _phasor_threshold_mean_open,
152
88
  _phasor_threshold_nan,
153
89
  _phasor_threshold_open,
154
- _phasor_to_apparent_lifetime,
155
- _phasor_to_normal_lifetime,
156
90
  _phasor_to_polar,
157
91
  _phasor_transform,
158
92
  _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
93
  )
165
94
  from ._utils import parse_harmonic, parse_signal_axis, parse_skip_axis
166
95
  from .utils import number_threads
@@ -257,13 +186,13 @@ def phasor_from_signal(
257
186
  --------
258
187
  phasorpy.phasor.phasor_to_signal
259
188
  phasorpy.phasor.phasor_normalize
260
- :ref:`sphx_glr_tutorials_benchmarks_phasorpy_phasor_from_signal.py`
189
+ :ref:`sphx_glr_tutorials_misc_phasorpy_phasor_from_signal.py`
261
190
 
262
191
  Notes
263
192
  -----
264
193
  The normalized phasor coordinates `real` (:math:`G`), `imag` (:math:`S`),
265
194
  and average intensity `mean` (:math:`F_{DC}`) are calculated from
266
- :math:`K\ge3` samples of the signal :math:`F` af `harmonic` :math:`h`
195
+ :math:`K \ge 3` samples of the signal :math:`F` at `harmonic` :math:`h`
267
196
  according to:
268
197
 
269
198
  .. math::
@@ -277,8 +206,8 @@ def phasor_from_signal(
277
206
  \sin{\left (2 \pi h \frac{k}{K} \right )} \cdot \frac{1}{F_{DC}}
278
207
 
279
208
  If :math:`F_{DC} = 0`, the phasor coordinates are undefined
280
- (:math:`NaN` or :math:`\infty`).
281
- Use `NaN`-aware software to further process the phasor coordinates.
209
+ (resulting in NaN or infinity).
210
+ Use NaN-aware software to further process the phasor coordinates.
282
211
 
283
212
  The phasor coordinates may be zero, for example, in case of only constant
284
213
  background in time-resolved signals, or as the result of linear
@@ -472,7 +401,7 @@ def phasor_to_signal(
472
401
  Notes
473
402
  -----
474
403
  The reconstructed signal may be undefined if the input phasor coordinates,
475
- or signal mean contain `NaN` values.
404
+ or signal mean contain NaN values.
476
405
 
477
406
  Examples
478
407
  --------
@@ -563,307 +492,6 @@ def phasor_to_signal(
563
492
  return signal
564
493
 
565
494
 
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
495
  def phasor_to_complex(
868
496
  real: ArrayLike,
869
497
  imag: ArrayLike,
@@ -1096,7 +724,7 @@ def phasor_normalize(
1096
724
  S &= S' / F
1097
725
 
1098
726
  If :math:`F = 0`, the normalized phasor coordinates (:math:`G`)
1099
- and (:math:`S`) are undefined (:math:`NaN` or :math:`\infty`).
727
+ and (:math:`S`) are undefined (NaN or infinity).
1100
728
 
1101
729
  Examples
1102
730
  --------
@@ -1134,283 +762,6 @@ def phasor_normalize(
1134
762
  return mean, real, imag
1135
763
 
1136
764
 
1137
- def phasor_calibrate(
1138
- real: ArrayLike,
1139
- imag: ArrayLike,
1140
- reference_mean: ArrayLike,
1141
- reference_real: ArrayLike,
1142
- reference_imag: ArrayLike,
1143
- /,
1144
- frequency: ArrayLike,
1145
- lifetime: ArrayLike,
1146
- *,
1147
- harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
1148
- skip_axis: int | Sequence[int] | None = None,
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.
1157
-
1158
- Calibration of phasor coordinates from time-resolved measurements is
1159
- necessary to account for the instrument response function (IRF) and delays
1160
- in the electronics.
1161
-
1162
- Parameters
1163
- ----------
1164
- real : array_like
1165
- Real component of phasor coordinates to be calibrated.
1166
- imag : array_like
1167
- Imaginary component of phasor coordinates to be calibrated.
1168
- reference_mean : array_like or None
1169
- Intensity of phasor coordinates from reference of known lifetime.
1170
- Used to re-normalize averaged phasor coordinates.
1171
- reference_real : array_like
1172
- Real component of phasor coordinates from reference of known lifetime.
1173
- Must be measured with the same instrument setting as the phasor
1174
- coordinates to be calibrated. Dimensions must be the same as `real`.
1175
- reference_imag : array_like
1176
- Imaginary component of phasor coordinates from reference of known
1177
- lifetime.
1178
- Must be measured with the same instrument setting as the phasor
1179
- coordinates to be calibrated.
1180
- frequency : array_like
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.
1212
-
1213
- nan_safe : bool, optional
1214
- Ensure `method` is applied to same elements of reference arrays.
1215
- By default, distribute NaNs among reference arrays before applying
1216
- `method`.
1217
- reverse : bool, optional
1218
- Reverse calibration.
1219
-
1220
- Returns
1221
- -------
1222
- real : ndarray
1223
- Calibrated real component of phasor coordinates.
1224
- imag : ndarray
1225
- Calibrated imaginary component of phasor coordinates.
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
1241
-
1242
- Notes
1243
- -----
1244
- This function is a convenience wrapper for the following operations:
1245
-
1246
- .. code-block:: python
1247
-
1248
- phasor_transform(
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
- )
1269
-
1270
- Calibration can be reversed such that
1271
-
1272
- .. code-block:: python
1273
-
1274
- real, imag == phasor_calibrate(
1275
- *phasor_calibrate(real, imag, *args, **kwargs),
1276
- *args,
1277
- reverse=True,
1278
- **kwargs
1279
- )
1280
-
1281
- Examples
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]))
1307
-
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)
1314
-
1315
- if real.shape != imag.shape:
1316
- raise ValueError(f'{real.shape=} != {imag.shape=}')
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
- )
1331
-
1332
- frequency = numpy.asarray(frequency)
1333
- frequency = frequency * harmonic
1334
-
1335
- if has_harmonic_axis:
1336
- if real.ndim == 0:
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
- )
1360
-
1361
- _, measured_re, measured_im = phasor_center(
1362
- reference_mean,
1363
- reference_real,
1364
- reference_imag,
1365
- skip_axis=skip_axis,
1366
- method=method,
1367
- nan_safe=nan_safe,
1368
- )
1369
-
1370
- known_re, known_im = phasor_from_lifetime(
1371
- frequency,
1372
- lifetime,
1373
- fraction,
1374
- preexponential=preexponential,
1375
- unit_conversion=unit_conversion,
1376
- )
1377
-
1378
- skip_axis, axis = parse_skip_axis(
1379
- skip_axis, real.ndim - int(has_harmonic_axis), has_harmonic_axis
1380
- )
1381
-
1382
- if has_harmonic_axis and any(skip_axis):
1383
- known_re = numpy.expand_dims(
1384
- known_re, tuple(range(1, measured_re.ndim))
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
- )
1395
-
1396
- phi_zero, mod_zero = polar_from_reference_phasor(
1397
- measured_re, measured_im, known_re, known_im
1398
- )
1399
-
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
-
1414
765
  def phasor_transform(
1415
766
  real: ArrayLike,
1416
767
  imag: ArrayLike,
@@ -1492,1339 +843,114 @@ def phasor_transform(
1492
843
  )
1493
844
 
1494
845
 
1495
- def polar_from_reference_phasor(
1496
- measured_real: ArrayLike,
1497
- measured_imag: ArrayLike,
1498
- known_real: ArrayLike,
1499
- known_imag: ArrayLike,
846
+ def phasor_to_polar(
847
+ real: ArrayLike,
848
+ imag: ArrayLike,
1500
849
  /,
1501
850
  **kwargs: Any,
1502
851
  ) -> 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.
852
+ r"""Return polar coordinates from phasor coordinates.
1508
853
 
1509
854
  Parameters
1510
855
  ----------
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.
856
+ real : array_like
857
+ Real component of phasor coordinates.
858
+ imag : array_like
859
+ Imaginary component of phasor coordinates.
1519
860
  **kwargs
1520
861
  Optional `arguments passed to numpy universal functions
1521
862
  <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1522
863
 
864
+ Notes
865
+ -----
866
+ The phasor coordinates `real` (:math:`G`) and `imag` (:math:`S`)
867
+ are converted to polar coordinates `phase` (:math:`\phi`) and
868
+ `modulation` (:math:`M`) according to:
869
+
870
+ .. math::
871
+
872
+ \phi &= \arctan(S / G)
873
+
874
+ M &= \sqrt{G^2 + S^2}
875
+
1523
876
  Returns
1524
877
  -------
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.
878
+ phase : ndarray
879
+ Angular component of polar coordinates in radians.
880
+ modulation : ndarray
881
+ Radial component of polar coordinates.
1529
882
 
1530
883
  See Also
1531
884
  --------
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
- )
885
+ phasorpy.phasor.phasor_from_polar
886
+ :ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
1544
887
 
1545
888
  Examples
1546
889
  --------
1547
- >>> polar_from_reference_phasor(0.5, 0.0, 1.0, 0.0)
1548
- (0.0, 2.0)
890
+ Calculate polar coordinates from three phasor coordinates:
891
+
892
+ >>> phasor_to_polar([1.0, 0.5, 0.0], [0.0, 0.5, 1.0]) # doctest: +NUMBER
893
+ (array([0, 0.7854, 1.571]), array([1, 0.7071, 1]))
1549
894
 
1550
895
  """
1551
- return _polar_from_reference_phasor( # type: ignore[no-any-return]
1552
- measured_real, measured_imag, known_real, known_imag, **kwargs
896
+ return _phasor_to_polar( # type: ignore[no-any-return]
897
+ real, imag, **kwargs
1553
898
  )
1554
899
 
1555
900
 
1556
- def polar_from_reference(
1557
- measured_phase: ArrayLike,
1558
- measured_modulation: ArrayLike,
1559
- known_phase: ArrayLike,
1560
- known_modulation: ArrayLike,
901
+ def phasor_from_polar(
902
+ phase: ArrayLike,
903
+ modulation: ArrayLike,
1561
904
  /,
1562
905
  **kwargs: Any,
1563
906
  ) -> 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.
907
+ r"""Return phasor coordinates from polar coordinates.
1569
908
 
1570
909
  Parameters
1571
910
  ----------
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.
911
+ phase : array_like
912
+ Angular component of polar coordinates in radians.
913
+ modulation : array_like
914
+ Radial component of polar coordinates.
1580
915
  **kwargs
1581
916
  Optional `arguments passed to numpy universal functions
1582
917
  <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1583
918
 
1584
919
  Returns
1585
920
  -------
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.
921
+ real : ndarray
922
+ Real component of phasor coordinates.
923
+ imag : ndarray
924
+ Imaginary component of phasor coordinates.
1590
925
 
1591
926
  See Also
1592
927
  --------
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
- def phasor_to_polar(
1611
- real: ArrayLike,
1612
- imag: ArrayLike,
1613
- /,
1614
- **kwargs: Any,
1615
- ) -> tuple[NDArray[Any], NDArray[Any]]:
1616
- r"""Return polar coordinates from phasor coordinates.
1617
-
1618
- Parameters
1619
- ----------
1620
- real : array_like
1621
- Real component of phasor coordinates.
1622
- imag : array_like
1623
- Imaginary component of phasor coordinates.
1624
- **kwargs
1625
- Optional `arguments passed to numpy universal functions
1626
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1627
-
1628
- Notes
1629
- -----
1630
- The phasor coordinates `real` (:math:`G`) and `imag` (:math:`S`)
1631
- are converted to polar coordinates `phase` (:math:`\phi`) and
1632
- `modulation` (:math:`M`) according to:
1633
-
1634
- .. math::
1635
-
1636
- \phi &= \arctan(S / G)
1637
-
1638
- M &= \sqrt{G^2 + S^2}
1639
-
1640
- Returns
1641
- -------
1642
- phase : ndarray
1643
- Angular component of polar coordinates in radians.
1644
- modulation : ndarray
1645
- Radial component of polar coordinates.
1646
-
1647
- See Also
1648
- --------
1649
- phasorpy.phasor.phasor_from_polar
1650
- :ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
1651
-
1652
- Examples
1653
- --------
1654
- Calculate polar coordinates from three phasor coordinates:
1655
-
1656
- >>> phasor_to_polar([1.0, 0.5, 0.0], [0.0, 0.5, 1.0]) # doctest: +NUMBER
1657
- (array([0, 0.7854, 1.571]), array([1, 0.7071, 1]))
1658
-
1659
- """
1660
- return _phasor_to_polar( # type: ignore[no-any-return]
1661
- real, imag, **kwargs
1662
- )
1663
-
1664
-
1665
- def phasor_from_polar(
1666
- phase: ArrayLike,
1667
- modulation: ArrayLike,
1668
- /,
1669
- **kwargs: Any,
1670
- ) -> tuple[NDArray[Any], NDArray[Any]]:
1671
- r"""Return phasor coordinates from polar coordinates.
1672
-
1673
- Parameters
1674
- ----------
1675
- phase : array_like
1676
- Angular component of polar coordinates in radians.
1677
- modulation : array_like
1678
- Radial component of polar coordinates.
1679
- **kwargs
1680
- Optional `arguments passed to numpy universal functions
1681
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1682
-
1683
- Returns
1684
- -------
1685
- real : ndarray
1686
- Real component of phasor coordinates.
1687
- imag : ndarray
1688
- Imaginary component of phasor coordinates.
1689
-
1690
- See Also
1691
- --------
1692
- phasorpy.phasor.phasor_to_polar
1693
-
1694
- Notes
1695
- -----
1696
- The polar coordinates `phase` (:math:`\phi`) and `modulation` (:math:`M`)
1697
- are converted to phasor coordinates `real` (:math:`G`) and
1698
- `imag` (:math:`S`) according to:
1699
-
1700
- .. math::
1701
-
1702
- G &= M \cdot \cos{\phi}
1703
-
1704
- S &= M \cdot \sin{\phi}
1705
-
1706
- Examples
1707
- --------
1708
- Calculate phasor coordinates from three polar coordinates:
1709
-
1710
- >>> phasor_from_polar(
1711
- ... [0.0, math.pi / 4, math.pi / 2], [1.0, math.sqrt(0.5), 1.0]
1712
- ... ) # doctest: +NUMBER
1713
- (array([1, 0.5, 0.0]), array([0, 0.5, 1]))
1714
-
1715
- """
1716
- return _phasor_from_polar( # type: ignore[no-any-return]
1717
- phase, modulation, **kwargs
1718
- )
1719
-
1720
-
1721
- def phasor_to_apparent_lifetime(
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
928
+ phasorpy.phasor.phasor_to_polar
2502
929
 
2503
930
  Notes
2504
931
  -----
2505
932
  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:
933
+ are converted to phasor coordinates `real` (:math:`G`) and
934
+ `imag` (:math:`S`) according to:
2582
935
 
2583
936
  .. math::
2584
937
 
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.
938
+ G &= M \cdot \cos{\phi}
2672
939
 
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`
940
+ S &= M \cdot \sin{\phi}
2678
941
 
2679
942
  Examples
2680
943
  --------
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`
944
+ Calculate phasor coordinates from three polar coordinates:
2793
945
 
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,
946
+ >>> phasor_from_polar(
947
+ ... [0.0, math.pi / 4, math.pi / 2], [1.0, math.sqrt(0.5), 1.0]
2810
948
  ... ) # doctest: +NUMBER
2811
- (array([0.1996, 0.05772, 0.2867]), array([0.3225, 0.3103, 0.4292]))
949
+ (array([1, 0.5, 0.0]), array([0, 0.5, 1]))
2812
950
 
2813
951
  """
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,
952
+ return _phasor_from_polar( # type: ignore[no-any-return]
953
+ phase, modulation, **kwargs
2828
954
  )
2829
955
 
2830
956
 
@@ -2837,7 +963,7 @@ def phasor_to_principal_plane(
2837
963
  ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
2838
964
  """Return multi-harmonic phasor coordinates projected onto principal plane.
2839
965
 
2840
- Principal Component Analysis (PCA) is used to project
966
+ Principal component analysis (PCA) is used to project
2841
967
  multi-harmonic phasor coordinates onto a plane, along which
2842
968
  coordinate axes the phasor coordinates have the largest variations.
2843
969
 
@@ -2882,7 +1008,7 @@ def phasor_to_principal_plane(
2882
1008
  -----
2883
1009
 
2884
1010
  This implementation does not work with coordinates containing
2885
- undefined `NaN` values.
1011
+ undefined NaN values.
2886
1012
 
2887
1013
  The transformation matrix can be used to project multi-harmonic phasor
2888
1014
  coordinates, where the first axis is the frequency:
@@ -2902,7 +1028,6 @@ def phasor_to_principal_plane(
2902
1028
 
2903
1029
  References
2904
1030
  ----------
2905
-
2906
1031
  .. [1] Franssen WMJ, Vergeldt FJ, Bader AN, van Amerongen H, and Terenzi C.
2907
1032
  `Full-harmonics phasor analysis: unravelling multiexponential trends
2908
1033
  in magnetic resonance imaging data
@@ -2935,7 +1060,7 @@ def phasor_to_principal_plane(
2935
1060
  im = im.reshape(im.shape[0], -1)
2936
1061
 
2937
1062
  # vector of multi-frequency phasor coordinates
2938
- coordinates = numpy.vstack((re, im))
1063
+ coordinates = numpy.vstack([re, im])
2939
1064
 
2940
1065
  # vector of centered coordinates
2941
1066
  center = numpy.nanmean(coordinates, axis=1, keepdims=True)
@@ -3035,7 +1160,7 @@ def phasor_filter_median(
3035
1160
  use_scipy : bool, optional
3036
1161
  Use :py:func:`scipy.ndimage.median_filter`.
3037
1162
  This function has undefined behavior if the input arrays contain
3038
- `NaN` values but is faster when filtering more than 2 dimensions.
1163
+ NaN values but is faster when filtering more than 2 dimensions.
3039
1164
  See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
3040
1165
  num_threads : int, optional
3041
1166
  Number of OpenMP threads to use for parallelization.
@@ -3257,7 +1382,6 @@ def phasor_filter_pawflim(
3257
1382
 
3258
1383
  References
3259
1384
  ----------
3260
-
3261
1385
  .. [2] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
3262
1386
  uncertainty to enable lower photon count in FLIM experiments
3263
1387
  <https://doi.org/10.1088/2050-6120/aa72ab>`_.
@@ -3412,12 +1536,12 @@ def phasor_threshold(
3412
1536
  detect_harmonics: bool = True,
3413
1537
  **kwargs: Any,
3414
1538
  ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
3415
- """Return phasor coordinates with values out of interval replaced by NaN.
1539
+ """Return phasor coordinates with values outside interval replaced by NaN.
3416
1540
 
3417
1541
  Interval thresholds can be set for mean intensity, real and imaginary
3418
1542
  coordinates, and phase and modulation.
3419
1543
  Phasor coordinates smaller than minimum thresholds or larger than maximum
3420
- thresholds are replaced NaN.
1544
+ thresholds are replaced with NaN.
3421
1545
  No threshold is applied by default.
3422
1546
  NaNs in `mean` or any `real` and `imag` harmonic are propagated to
3423
1547
  `mean` and all harmonics in `real` and `imag`.
@@ -3622,7 +1746,7 @@ def phasor_nearest_neighbor(
3622
1746
  distance_max: float | None = None,
3623
1747
  num_threads: int | None = None,
3624
1748
  ) -> NDArray[Any]:
3625
- """Return indices or values of nearest neighbor from other coordinates.
1749
+ """Return indices or values of nearest neighbors from other coordinates.
3626
1750
 
3627
1751
  For each phasor coordinate, find the nearest neighbor in another set of
3628
1752
  phasor coordinates and return its flat index. If more than one neighbor