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/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,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 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
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
- _phasor_at_harmonic,
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:`sphx_glr_tutorials_benchmarks_phasorpy_phasor_from_signal.py`
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\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`
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
- (:math:`NaN` or :math:`\infty`).
270
- 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.
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 `NaN` values.
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 (:math:`NaN` or :math:`\infty`).
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 phasor_to_apparent_lifetime(
957
+ def phasor_to_principal_plane(
1651
958
  real: ArrayLike,
1652
959
  imag: ArrayLike,
1653
960
  /,
1654
- frequency: ArrayLike,
1655
961
  *,
1656
- unit_conversion: float = 1e-3,
1657
- **kwargs: Any,
1658
- ) -> tuple[NDArray[Any], NDArray[Any]]:
1659
- r"""Return apparent single lifetimes from phasor coordinates.
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
- imag : array_like
1666
- Imaginary component of phasor coordinates.
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 `NaN` values.
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((re, im))
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
- `NaN` values but is faster when filtering more than 2 dimensions.
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 out of interval replaced by NaN.
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,