phasorpy 0.6__cp313-cp313-win_amd64.whl → 0.8__cp313-cp313-win_amd64.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,16 @@ 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:
25
+ - linearly combine two phasor coordinates:
38
26
 
39
- - :py:func:`phasor_calibrate`
40
- - :py:func:`polar_from_reference`
41
- - :py:func:`polar_from_reference_phasor`
27
+ - :py:func:`phasor_combine`
42
28
 
43
29
  - reduce dimensionality of arrays of phasor coordinates:
44
30
 
45
31
  - :py:func:`phasor_center`
46
32
  - :py:func:`phasor_to_principal_plane`
47
33
 
48
- - 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
- - filter phasor coordinates:
68
-
69
- - :py:func:`phasor_filter_median`
70
- - :py:func:`phasor_filter_pawflim`
71
- - :py:func:`phasor_threshold`
72
-
73
- - find nearest neighbor phasor coordinates from another set of phasor coordinates:
34
+ - find nearest neighbor phasor coordinates from other phasor coordinates:
74
35
 
75
36
  - :py:func:`phasor_nearest_neighbor`
76
37
 
@@ -79,40 +40,19 @@ The ``phasorpy.phasor`` module provides functions to:
79
40
  from __future__ import annotations
80
41
 
81
42
  __all__ = [
82
- 'lifetime_fraction_from_amplitude',
83
- 'lifetime_fraction_to_amplitude',
84
- 'lifetime_from_frequency',
85
- 'lifetime_to_frequency',
86
- 'lifetime_to_signal',
87
- 'phasor_at_harmonic',
88
- 'phasor_calibrate',
89
43
  'phasor_center',
90
44
  'phasor_divide',
91
- 'phasor_filter_median',
92
- 'phasor_filter_pawflim',
93
- 'phasor_from_apparent_lifetime',
94
- 'phasor_from_fret_acceptor',
95
- 'phasor_from_fret_donor',
96
- 'phasor_from_lifetime',
97
45
  'phasor_from_polar',
98
46
  'phasor_from_signal',
47
+ 'phasor_combine',
99
48
  'phasor_multiply',
100
49
  'phasor_nearest_neighbor',
101
50
  'phasor_normalize',
102
- 'phasor_semicircle',
103
- 'phasor_semicircle_intersect',
104
- 'phasor_threshold',
105
- 'phasor_to_apparent_lifetime',
106
51
  'phasor_to_complex',
107
- 'phasor_to_normal_lifetime',
108
52
  'phasor_to_polar',
109
53
  'phasor_to_principal_plane',
110
54
  'phasor_to_signal',
111
55
  'phasor_transform',
112
- 'polar_from_apparent_lifetime',
113
- 'polar_from_reference',
114
- 'polar_from_reference_phasor',
115
- 'polar_to_apparent_lifetime',
116
56
  ]
117
57
 
118
58
  import math
@@ -132,37 +72,18 @@ if TYPE_CHECKING:
132
72
  import numpy
133
73
 
134
74
  from ._phasorpy import (
135
- _gaussian_signal,
136
- _intersect_semicircle_line,
137
- _median_filter_2d,
138
75
  _nearest_neighbor_2d,
139
- _phasor_at_harmonic,
76
+ _phasor_combine,
140
77
  _phasor_divide,
141
- _phasor_from_apparent_lifetime,
142
- _phasor_from_fret_acceptor,
143
- _phasor_from_fret_donor,
144
- _phasor_from_lifetime,
145
78
  _phasor_from_polar,
146
79
  _phasor_from_signal,
147
- _phasor_from_single_lifetime,
148
80
  _phasor_multiply,
149
- _phasor_threshold_closed,
150
- _phasor_threshold_mean_closed,
151
- _phasor_threshold_mean_open,
152
- _phasor_threshold_nan,
153
- _phasor_threshold_open,
154
- _phasor_to_apparent_lifetime,
155
- _phasor_to_normal_lifetime,
156
81
  _phasor_to_polar,
157
82
  _phasor_transform,
158
83
  _phasor_transform_const,
159
- _polar_from_apparent_lifetime,
160
- _polar_from_reference,
161
- _polar_from_reference_phasor,
162
- _polar_from_single_lifetime,
163
- _polar_to_apparent_lifetime,
164
84
  )
165
85
  from ._utils import parse_harmonic, parse_signal_axis, parse_skip_axis
86
+ from .filter import phasor_threshold
166
87
  from .utils import number_threads
167
88
 
168
89
 
@@ -213,7 +134,8 @@ def phasor_from_signal(
213
134
  calculated, or `rfft` is specified.
214
135
  rfft : callable, optional
215
136
  Drop-in replacement function for ``numpy.fft.rfft``.
216
- For example, ``scipy.fft.rfft`` or ``mkl_fft._numpy_fft.rfft``.
137
+ For example, ``scipy.fft.rfft`` or
138
+ ``mkl_fft.interfaces.numpy_fft.rfft``.
217
139
  Used to calculate the real forward FFT.
218
140
  dtype : dtype_like, optional
219
141
  Data type of output arrays. Either float32 or float64.
@@ -257,13 +179,13 @@ def phasor_from_signal(
257
179
  --------
258
180
  phasorpy.phasor.phasor_to_signal
259
181
  phasorpy.phasor.phasor_normalize
260
- :ref:`sphx_glr_tutorials_benchmarks_phasorpy_phasor_from_signal.py`
182
+ :ref:`sphx_glr_tutorials_misc_phasorpy_phasor_from_signal.py`
261
183
 
262
184
  Notes
263
185
  -----
264
186
  The normalized phasor coordinates `real` (:math:`G`), `imag` (:math:`S`),
265
187
  and average intensity `mean` (:math:`F_{DC}`) are calculated from
266
- :math:`K\ge3` samples of the signal :math:`F` af `harmonic` :math:`h`
188
+ :math:`K \ge 3` samples of the signal :math:`F` at `harmonic` :math:`h`
267
189
  according to:
268
190
 
269
191
  .. math::
@@ -277,8 +199,8 @@ def phasor_from_signal(
277
199
  \sin{\left (2 \pi h \frac{k}{K} \right )} \cdot \frac{1}{F_{DC}}
278
200
 
279
201
  If :math:`F_{DC} = 0`, the phasor coordinates are undefined
280
- (:math:`NaN` or :math:`\infty`).
281
- Use `NaN`-aware software to further process the phasor coordinates.
202
+ (resulting in NaN or infinity).
203
+ Use NaN-aware software to further process the phasor coordinates.
282
204
 
283
205
  The phasor coordinates may be zero, for example, in case of only constant
284
206
  background in time-resolved signals, or as the result of linear
@@ -325,8 +247,8 @@ def phasor_from_signal(
325
247
  raise ValueError('sample_phase cannot be used with FFT')
326
248
  if num_harmonics > 1 or harmonic[0] != 1:
327
249
  raise ValueError('sample_phase cannot be used with harmonic != 1')
328
- sample_phase = numpy.atleast_1d(
329
- numpy.asarray(sample_phase, dtype=numpy.float64)
250
+ sample_phase = numpy.array(
251
+ sample_phase, dtype=numpy.float64, ndmin=1, order='C', copy=None
330
252
  )
331
253
  if sample_phase.ndim != 1 or sample_phase.size != samples:
332
254
  raise ValueError(f'{sample_phase.shape=} != ({samples},)')
@@ -348,10 +270,10 @@ def phasor_from_signal(
348
270
 
349
271
  mean = fft.take(0, axis=axis).real
350
272
  if not mean.ndim == 0:
351
- mean = numpy.ascontiguousarray(mean, dtype)
273
+ mean = numpy.ascontiguousarray(mean, dtype=dtype)
352
274
  fft = fft.take(harmonic, axis=axis)
353
- real = numpy.ascontiguousarray(fft.real, dtype)
354
- imag = numpy.ascontiguousarray(fft.imag, dtype)
275
+ real = numpy.ascontiguousarray(fft.real, dtype=dtype)
276
+ imag = numpy.ascontiguousarray(fft.imag, dtype=dtype)
355
277
  del fft
356
278
 
357
279
  if not keepdims and real.shape[axis] == 1:
@@ -398,7 +320,7 @@ def phasor_from_signal(
398
320
  shape1 = signal.shape[axis + 1 :]
399
321
  size0 = math.prod(shape0)
400
322
  size1 = math.prod(shape1)
401
- phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype)
323
+ phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype=dtype)
402
324
  signal = signal.reshape((size0, samples, size1))
403
325
 
404
326
  _phasor_from_signal(phasor, signal, sincos, normalize, num_threads)
@@ -457,7 +379,8 @@ def phasor_to_signal(
457
379
  The default is the last axis (-1).
458
380
  irfft : callable, optional
459
381
  Drop-in replacement function for ``numpy.fft.irfft``.
460
- For example, ``scipy.fft.irfft`` or ``mkl_fft._numpy_fft.irfft``.
382
+ For example, ``scipy.fft.irfft`` or
383
+ ``mkl_fft.interfaces.numpy_fft.irfft``.
461
384
  Used to calculate the real inverse FFT.
462
385
 
463
386
  Returns
@@ -472,7 +395,7 @@ def phasor_to_signal(
472
395
  Notes
473
396
  -----
474
397
  The reconstructed signal may be undefined if the input phasor coordinates,
475
- or signal mean contain `NaN` values.
398
+ or signal mean contain NaN values.
476
399
 
477
400
  Examples
478
401
  --------
@@ -515,8 +438,9 @@ def phasor_to_signal(
515
438
  else:
516
439
  keepdims = mean.ndim > 0 or real.ndim > 0
517
440
 
518
- mean = numpy.asarray(numpy.atleast_1d(mean))
519
- real = numpy.asarray(numpy.atleast_1d(real))
441
+ # mean, real = numpy.atleast_1d(mean, real) not working with Mypy
442
+ mean = numpy.array(mean, ndmin=1, copy=False)
443
+ real = numpy.array(real, ndmin=1, copy=False)
520
444
 
521
445
  if real.dtype.kind != 'f' or imag.dtype.kind != 'f':
522
446
  raise ValueError(f'{real.dtype=} or {imag.dtype=} not floating point')
@@ -563,307 +487,6 @@ def phasor_to_signal(
563
487
  return signal
564
488
 
565
489
 
566
- def lifetime_to_signal(
567
- frequency: float,
568
- lifetime: ArrayLike,
569
- fraction: ArrayLike | None = None,
570
- *,
571
- mean: ArrayLike | None = None,
572
- background: ArrayLike | None = None,
573
- samples: int = 64,
574
- harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
575
- zero_phase: float | None = None,
576
- zero_stdev: float | None = None,
577
- preexponential: bool = False,
578
- unit_conversion: float = 1e-3,
579
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
580
- r"""Return synthetic signal from lifetime components.
581
-
582
- Return synthetic signal, instrument response function (IRF), and
583
- time axis, sampled over one period of the fundamental frequency.
584
- The signal is convoluted with the IRF, which is approximated by a
585
- normal distribution.
586
-
587
- Parameters
588
- ----------
589
- frequency : float
590
- Fundamental laser pulse or modulation frequency in MHz.
591
- lifetime : array_like
592
- Lifetime components in ns.
593
- fraction : array_like, optional
594
- Fractional intensities or pre-exponential amplitudes of the lifetime
595
- components. Fractions are normalized to sum to 1.
596
- Must be specified if `lifetime` is not a scalar.
597
- mean : array_like, optional, default: 1.0
598
- Average signal intensity (DC). Must be scalar for now.
599
- background : array_like, optional, default: 0.0
600
- Background signal intensity. Must be smaller than `mean`.
601
- samples : int, default: 64
602
- Number of signal samples to return. Must be at least 16.
603
- harmonic : int, sequence of int, or 'all', optional, default: 'all'
604
- Harmonics used to synthesize signal.
605
- If `'all'`, all harmonics are used.
606
- Else, harmonics must be at least one and no larger than half of
607
- `samples`.
608
- Use `'all'` to synthesize an exponential time-domain decay signal,
609
- or `1` to synthesize a homodyne signal.
610
- zero_phase : float, optional
611
- Position of instrument response function in radians.
612
- Must be in range [0, pi]. The default is the 8th sample.
613
- zero_stdev : float, optional
614
- Standard deviation of instrument response function in radians.
615
- Must be at least 1.5 samples and no more than one tenth of samples
616
- to allow for sufficient sampling of the function.
617
- The default is 1.5 samples. Increase `samples` to narrow the IRF.
618
- preexponential : bool, optional, default: False
619
- If true, `fraction` values are pre-exponential amplitudes,
620
- else fractional intensities.
621
- unit_conversion : float, optional, default: 1e-3
622
- Product of `frequency` and `lifetime` units' prefix factors.
623
- The default is 1e-3 for MHz and ns, or Hz and ms.
624
- Use 1.0 for Hz and s.
625
-
626
- Returns
627
- -------
628
- signal : ndarray
629
- Signal generated from lifetimes at frequency, convoluted with
630
- instrument response function.
631
- zero : ndarray
632
- Instrument response function.
633
- time : ndarray
634
- Time for each sample in signal in units of `lifetime`.
635
-
636
- See Also
637
- --------
638
- phasorpy.phasor.phasor_from_lifetime
639
- phasorpy.phasor.phasor_to_signal
640
- :ref:`sphx_glr_tutorials_api_phasorpy_lifetime_to_signal.py`
641
-
642
- Notes
643
- -----
644
- This implementation is based on an inverse digital Fourier transform (DFT).
645
- Because DFT cannot be used on signals with discontinuities
646
- (for example, an exponential decay starting at zero) without producing
647
- strong artifacts (ripples), the signal is convoluted with a continuous
648
- instrument response function (IRF). The minimum width of the IRF is
649
- limited due to sampling requirements.
650
-
651
- Examples
652
- --------
653
- Synthesize a multi-exponential time-domain decay signal for two
654
- lifetime components of 4.2 and 0.9 ns at 40 MHz:
655
-
656
- >>> signal, zero, times = lifetime_to_signal(
657
- ... 40, [4.2, 0.9], fraction=[0.8, 0.2], samples=16
658
- ... )
659
- >>> signal # doctest: +NUMBER
660
- array([0.2846, 0.1961, 0.1354, ..., 0.8874, 0.6029, 0.4135])
661
-
662
- Synthesize a homodyne frequency-domain waveform signal for
663
- a single lifetime:
664
-
665
- >>> signal, zero, times = lifetime_to_signal(
666
- ... 40.0, 4.2, samples=16, harmonic=1
667
- ... )
668
- >>> signal # doctest: +NUMBER
669
- array([0.2047, -0.05602, -0.156, ..., 1.471, 1.031, 0.5865])
670
-
671
- """
672
- if harmonic is None:
673
- harmonic = 'all'
674
- all_hamonics = harmonic == 'all'
675
- harmonic, _ = parse_harmonic(harmonic, samples // 2)
676
-
677
- if samples < 16:
678
- raise ValueError(f'{samples=} < 16')
679
-
680
- if background is None:
681
- background = 0.0
682
- background = numpy.asarray(background)
683
-
684
- if mean is None:
685
- mean = 1.0
686
- mean = numpy.asarray(mean)
687
- mean -= background
688
- if numpy.any(mean < 0.0):
689
- raise ValueError('mean - background must not be less than zero')
690
-
691
- scale = samples / (2.0 * math.pi)
692
- if zero_phase is None:
693
- zero_phase = 8.0 / scale
694
- phase = zero_phase * scale # in sample units
695
- if zero_stdev is None:
696
- zero_stdev = 1.5 / scale
697
- stdev = zero_stdev * scale # in sample units
698
-
699
- if zero_phase < 0 or zero_phase > 2.0 * math.pi:
700
- raise ValueError(f'{zero_phase=} out of range [0, 2 pi]')
701
- if stdev < 1.5:
702
- raise ValueError(
703
- f'{zero_stdev=} < {1.5 / scale} cannot be sampled sufficiently'
704
- )
705
- if stdev >= samples / 10:
706
- raise ValueError(f'{zero_stdev=} > pi / 5 not supported')
707
-
708
- frequencies = numpy.atleast_1d(frequency)
709
- if frequencies.size > 1 or frequencies[0] <= 0.0:
710
- raise ValueError('frequency must be scalar and positive')
711
- frequencies = numpy.linspace(
712
- frequency, samples // 2 * frequency, samples // 2
713
- )
714
- frequencies = frequencies[[h - 1 for h in harmonic]]
715
-
716
- real, imag = phasor_from_lifetime(
717
- frequencies,
718
- lifetime,
719
- fraction,
720
- preexponential=preexponential,
721
- unit_conversion=unit_conversion,
722
- )
723
- real, imag = numpy.atleast_1d(real, imag)
724
-
725
- zero = numpy.zeros(samples, dtype=numpy.float64)
726
- _gaussian_signal(zero, phase, stdev)
727
- zero_mean, zero_real, zero_imag = phasor_from_signal(
728
- zero, harmonic=harmonic
729
- )
730
- if real.ndim > 1:
731
- # make broadcastable with real and imag
732
- zero_real = zero_real[:, None]
733
- zero_imag = zero_imag[:, None]
734
- if not all_hamonics:
735
- zero = phasor_to_signal(
736
- zero_mean, zero_real, zero_imag, samples=samples, harmonic=harmonic
737
- )
738
-
739
- phasor_multiply(real, imag, zero_real, zero_imag, out=(real, imag))
740
-
741
- if len(harmonic) == 1:
742
- harmonic = harmonic[0]
743
- signal = phasor_to_signal(
744
- mean, real, imag, samples=samples, harmonic=harmonic
745
- )
746
- signal += numpy.asarray(background)
747
-
748
- time = numpy.linspace(0, 1.0 / (unit_conversion * frequency), samples)
749
-
750
- return signal.squeeze(), zero.squeeze(), time
751
-
752
-
753
- def phasor_semicircle(
754
- samples: int = 101, /
755
- ) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
756
- r"""Return equally spaced phasor coordinates on universal semicircle.
757
-
758
- Parameters
759
- ----------
760
- samples : int, optional, default: 101
761
- Number of coordinates to return.
762
-
763
- Returns
764
- -------
765
- real : ndarray
766
- Real component of phasor coordinates on universal semicircle.
767
- imag : ndarray
768
- Imaginary component of phasor coordinates on universal semicircle.
769
-
770
- Raises
771
- ------
772
- ValueError
773
- The number of `samples` is smaller than 1.
774
-
775
- Notes
776
- -----
777
- If more than one sample, the first and last phasor coordinates returned
778
- are ``(0, 0)`` and ``(1, 0)``.
779
- The center coordinate, if any, is ``(0.5, 0.5)``.
780
-
781
- The universal semicircle is composed of the phasor coordinates of
782
- single lifetime components, where the relation of polar coordinates
783
- (phase :math:`\phi` and modulation :math:`M`) is:
784
-
785
- .. math::
786
-
787
- M = \cos{\phi}
788
-
789
- Examples
790
- --------
791
- Calculate three phasor coordinates on universal semicircle:
792
-
793
- >>> phasor_semicircle(3) # doctest: +NUMBER
794
- (array([0, 0.5, 1]), array([0.0, 0.5, 0]))
795
-
796
- """
797
- if samples < 1:
798
- raise ValueError(f'{samples=} < 1')
799
- arange = numpy.linspace(math.pi, 0.0, samples)
800
- real = numpy.cos(arange)
801
- real += 1.0
802
- real *= 0.5
803
- imag = numpy.sin(arange)
804
- imag *= 0.5
805
- return real, imag
806
-
807
-
808
- def phasor_semicircle_intersect(
809
- real0: ArrayLike,
810
- imag0: ArrayLike,
811
- real1: ArrayLike,
812
- imag1: ArrayLike,
813
- /,
814
- **kwargs: Any,
815
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], NDArray[Any]]:
816
- """Return intersection of line through phasors with universal semicircle.
817
-
818
- Return the phasor coordinates of two intersections of the universal
819
- semicircle with the line between two phasor coordinates.
820
- Return NaN if the line does not intersect the semicircle.
821
-
822
- Parameters
823
- ----------
824
- real0 : array_like
825
- Real component of first set of phasor coordinates.
826
- imag0 : array_like
827
- Imaginary component of first set of phasor coordinates.
828
- real1 : array_like
829
- Real component of second set of phasor coordinates.
830
- imag1 : array_like
831
- Imaginary component of second set of phasor coordinates.
832
- **kwargs
833
- Optional `arguments passed to numpy universal functions
834
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
835
-
836
- Returns
837
- -------
838
- real0 : ndarray
839
- Real component of first intersect of phasors with semicircle.
840
- imag0 : ndarray
841
- Imaginary component of first intersect of phasors with semicircle.
842
- real1 : ndarray
843
- Real component of second intersect of phasors with semicircle.
844
- imag1 : ndarray
845
- Imaginary component of second intersect of phasors with semicircle.
846
-
847
- Examples
848
- --------
849
- Calculate two intersects of a line through two phasor coordinates
850
- with the universal semicircle:
851
-
852
- >>> phasor_semicircle_intersect(0.2, 0.25, 0.6, 0.25) # doctest: +NUMBER
853
- (0.066, 0.25, 0.933, 0.25)
854
-
855
- The line between two phasor coordinates may not intersect the semicircle
856
- at two points:
857
-
858
- >>> phasor_semicircle_intersect(0.2, 0.0, 0.6, 0.25) # doctest: +NUMBER
859
- (nan, nan, 0.817, 0.386)
860
-
861
- """
862
- return _intersect_semicircle_line( # type: ignore[no-any-return]
863
- real0, imag0, real1, imag1, **kwargs
864
- )
865
-
866
-
867
490
  def phasor_to_complex(
868
491
  real: ArrayLike,
869
492
  imag: ArrayLike,
@@ -909,7 +532,7 @@ def phasor_to_complex(
909
532
  if dtype.kind != 'c':
910
533
  raise ValueError(f'{dtype=} not a complex type')
911
534
 
912
- c = numpy.empty(numpy.broadcast(real, imag).shape, dtype)
535
+ c = numpy.empty(numpy.broadcast(real, imag).shape, dtype=dtype)
913
536
  c.real = real
914
537
  c.imag = imag
915
538
  return c
@@ -1096,7 +719,7 @@ def phasor_normalize(
1096
719
  S &= S' / F
1097
720
 
1098
721
  If :math:`F = 0`, the normalized phasor coordinates (:math:`G`)
1099
- and (:math:`S`) are undefined (:math:`NaN` or :math:`\infty`).
722
+ and (:math:`S`) are undefined (NaN or infinity).
1100
723
 
1101
724
  Examples
1102
725
  --------
@@ -1121,9 +744,9 @@ def phasor_normalize(
1121
744
  ):
1122
745
  real = real_unnormalized.copy()
1123
746
  else:
1124
- real = numpy.array(real_unnormalized, dtype, copy=True)
1125
- imag = numpy.array(imag_unnormalized, real.dtype, copy=True)
1126
- mean = numpy.array(mean_unnormalized, real.dtype, copy=True)
747
+ real = numpy.asarray(real_unnormalized, dtype=dtype, copy=True)
748
+ imag = numpy.asarray(imag_unnormalized, dtype=real.dtype, copy=True)
749
+ mean = numpy.asarray(mean_unnormalized, dtype=real.dtype, copy=True)
1127
750
 
1128
751
  with numpy.errstate(divide='ignore', invalid='ignore'):
1129
752
  numpy.divide(real, mean, out=real)
@@ -1134,282 +757,102 @@ def phasor_normalize(
1134
757
  return mean, real, imag
1135
758
 
1136
759
 
1137
- def 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.
760
+ def phasor_combine(
761
+ int0: ArrayLike,
762
+ real0: ArrayLike,
763
+ imag0: ArrayLike,
764
+ int1: ArrayLike,
765
+ real1: ArrayLike,
766
+ imag1: ArrayLike,
767
+ fraction0: ArrayLike,
768
+ fraction1: ArrayLike | None = None,
769
+ **kwargs: Any,
770
+ ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
771
+ r"""Return linear combination of two phasor coordinates.
1157
772
 
1158
- 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.
773
+ Combine two sets of phasor coordinates using intensity-weighted mixing.
774
+ This simulates the phasor coordinates that would result from a mixture
775
+ of two components with known individual phasor coordinates.
1161
776
 
1162
777
  Parameters
1163
778
  ----------
1164
- 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.
779
+ int0 : array_like
780
+ Intensity of first phasor coordinates.
781
+ real0 : array_like
782
+ Real component of first phasor coordinates.
783
+ imag0 : array_like
784
+ Imaginary component of first phasor coordinates.
785
+ int1 : array_like
786
+ Intensity of second phasor coordinates.
787
+ real1 : array_like
788
+ Real component of second phasor coordinates.
789
+ imag1 : array_like
790
+ Imaginary component of second phasor coordinates.
791
+ fraction0 : array_like
792
+ Fraction of first phasor coordinates.
793
+ fraction1 : array_like, optional
794
+ Fraction of second phasor coordinates.
795
+ The default is `1 - fraction0`.
1212
796
 
1213
- 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.
797
+ **kwargs
798
+ Optional `arguments passed to numpy universal functions
799
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1219
800
 
1220
801
  Returns
1221
802
  -------
803
+ intensity : ndarray
804
+ Intensity of the linearly combined phasor coordinates.
1222
805
  real : ndarray
1223
- Calibrated real component of phasor coordinates.
806
+ Real component of the linearly combined phasor coordinates.
1224
807
  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
808
+ Imaginary component of the linearly combined phasor coordinates.
1241
809
 
1242
810
  Notes
1243
811
  -----
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
- )
812
+ The phasor coordinates (:math:`I`, :math:`G`, :math:`S`) of the linear
813
+ combination of two phasor coordinates (:math:`I_{0}`, :math:`G_{0}`,
814
+ :math:`S_{0}`, and :math:`I_{1}`, :math:`G_{1}`, :math:`S_{1}`)
815
+ with fractions :math:`f_{0}` and :math:`f_{1}` of the first and second
816
+ coordinates are:
1269
817
 
1270
- Calibration can be reversed such that
1271
-
1272
- .. code-block:: python
818
+ .. math::
1273
819
 
1274
- real, imag == phasor_calibrate(
1275
- *phasor_calibrate(real, imag, *args, **kwargs),
1276
- *args,
1277
- reverse=True,
1278
- **kwargs
1279
- )
820
+ f'_{0} &= f_{0} / (f_{0} + f_{1})
1280
821
 
1281
- 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]))
822
+ f'_{1} &= 1 - f'_{0}
1307
823
 
1308
- """
1309
- real = numpy.asarray(real)
1310
- imag = numpy.asarray(imag)
1311
- reference_mean = numpy.asarray(reference_mean)
1312
- reference_real = numpy.asarray(reference_real)
1313
- reference_imag = numpy.asarray(reference_imag)
824
+ I &= I_{0} \cdot f'_{0} + I_{1} \cdot f'_{1}
1314
825
 
1315
- 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
- )
826
+ G &= (G_{0} \cdot I_{0} \cdot f'_{0}
827
+ + G_{1} \cdot I_{1} \cdot f'_{1}) / I
1331
828
 
1332
- frequency = numpy.asarray(frequency)
1333
- frequency = frequency * harmonic
829
+ S &= (S_{0} \cdot I_{0} \cdot f'_{0}
830
+ + S_{1} \cdot I_{1} \cdot f'_{1}) / I
1334
831
 
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
- )
832
+ If the intensity :math:`I` is zero, the linear combined phasor coordinates
833
+ are undefined (NaN).
1360
834
 
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
- )
835
+ See Also
836
+ --------
837
+ phasorpy.component.phasor_from_component
1369
838
 
1370
- known_re, known_im = phasor_from_lifetime(
1371
- frequency,
1372
- lifetime,
1373
- fraction,
1374
- preexponential=preexponential,
1375
- unit_conversion=unit_conversion,
1376
- )
839
+ Examples
840
+ --------
841
+ Calculate the linear combination of two phasor coordinates with equal
842
+ intensities at three fractional intensities:
1377
843
 
1378
- skip_axis, axis = parse_skip_axis(
1379
- skip_axis, real.ndim - int(has_harmonic_axis), has_harmonic_axis
1380
- )
844
+ >>> phasor_combine(1.0, 0.6, 0.3, 1.0, 0.4, 0.2, [1.0, 0.2, 0.9])
845
+ (array([1, 1, 1]), array([0.6, 0.44, 0.58]), array([0.3, 0.22, 0.29]))
1381
846
 
1382
- 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
- )
847
+ """
848
+ if fraction1 is None:
849
+ fraction1 = numpy.asarray(fraction0, copy=True)
850
+ numpy.subtract(1.0, fraction0, out=fraction1)
1395
851
 
1396
- phi_zero, mod_zero = polar_from_reference_phasor(
1397
- measured_re, measured_im, known_re, known_im
852
+ return _phasor_combine( # type: ignore[no-any-return]
853
+ int0, real0, imag0, int1, real1, imag1, fraction0, fraction1, **kwargs
1398
854
  )
1399
855
 
1400
- if numpy.ndim(phi_zero) > 0:
1401
- if reverse:
1402
- numpy.negative(phi_zero, out=phi_zero)
1403
- numpy.reciprocal(mod_zero, out=mod_zero)
1404
- if axis is not None:
1405
- phi_zero = numpy.expand_dims(phi_zero, axis=axis)
1406
- mod_zero = numpy.expand_dims(mod_zero, axis=axis)
1407
- elif reverse:
1408
- phi_zero = -phi_zero
1409
- mod_zero = 1.0 / mod_zero
1410
-
1411
- return phasor_transform(real, imag, phi_zero, mod_zero)
1412
-
1413
856
 
1414
857
  def phasor_transform(
1415
858
  real: ArrayLike,
@@ -1492,121 +935,6 @@ def phasor_transform(
1492
935
  )
1493
936
 
1494
937
 
1495
- def polar_from_reference_phasor(
1496
- measured_real: ArrayLike,
1497
- measured_imag: ArrayLike,
1498
- known_real: ArrayLike,
1499
- known_imag: ArrayLike,
1500
- /,
1501
- **kwargs: Any,
1502
- ) -> tuple[NDArray[Any], NDArray[Any]]:
1503
- r"""Return polar coordinates for calibration from reference phasor.
1504
-
1505
- Return rotation angle and scale factor for calibrating phasor coordinates
1506
- from measured and known phasor coordinates of a reference, for example,
1507
- a sample of known lifetime.
1508
-
1509
- Parameters
1510
- ----------
1511
- measured_real : array_like
1512
- Real component of measured phasor coordinates.
1513
- measured_imag : array_like
1514
- Imaginary component of measured phasor coordinates.
1515
- known_real : array_like
1516
- Real component of reference phasor coordinates.
1517
- known_imag : array_like
1518
- Imaginary component of reference phasor coordinates.
1519
- **kwargs
1520
- Optional `arguments passed to numpy universal functions
1521
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1522
-
1523
- Returns
1524
- -------
1525
- phase_zero : ndarray
1526
- Angular component of polar coordinates for calibration in radians.
1527
- modulation_zero : ndarray
1528
- Radial component of polar coordinates for calibration.
1529
-
1530
- See Also
1531
- --------
1532
- phasorpy.phasor.polar_from_reference
1533
-
1534
- Notes
1535
- -----
1536
- This function performs the following operations:
1537
-
1538
- .. code-block:: python
1539
-
1540
- polar_from_reference(
1541
- *phasor_to_polar(measured_real, measured_imag),
1542
- *phasor_to_polar(known_real, known_imag),
1543
- )
1544
-
1545
- Examples
1546
- --------
1547
- >>> polar_from_reference_phasor(0.5, 0.0, 1.0, 0.0)
1548
- (0.0, 2.0)
1549
-
1550
- """
1551
- return _polar_from_reference_phasor( # type: ignore[no-any-return]
1552
- measured_real, measured_imag, known_real, known_imag, **kwargs
1553
- )
1554
-
1555
-
1556
- def polar_from_reference(
1557
- measured_phase: ArrayLike,
1558
- measured_modulation: ArrayLike,
1559
- known_phase: ArrayLike,
1560
- known_modulation: ArrayLike,
1561
- /,
1562
- **kwargs: Any,
1563
- ) -> tuple[NDArray[Any], NDArray[Any]]:
1564
- r"""Return polar coordinates for calibration from reference coordinates.
1565
-
1566
- Return rotation angle and scale factor for calibrating phasor coordinates
1567
- from measured and known polar coordinates of a reference, for example,
1568
- a sample of known lifetime.
1569
-
1570
- Parameters
1571
- ----------
1572
- measured_phase : array_like
1573
- Angular component of measured polar coordinates in radians.
1574
- measured_modulation : array_like
1575
- Radial component of measured polar coordinates.
1576
- known_phase : array_like
1577
- Angular component of reference polar coordinates in radians.
1578
- known_modulation : array_like
1579
- Radial component of reference polar coordinates.
1580
- **kwargs
1581
- Optional `arguments passed to numpy universal functions
1582
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1583
-
1584
- Returns
1585
- -------
1586
- phase_zero : ndarray
1587
- Angular component of polar coordinates for calibration in radians.
1588
- modulation_zero : ndarray
1589
- Radial component of polar coordinates for calibration.
1590
-
1591
- See Also
1592
- --------
1593
- phasorpy.phasor.polar_from_reference_phasor
1594
-
1595
- Examples
1596
- --------
1597
- >>> polar_from_reference(0.2, 0.4, 0.4, 1.3)
1598
- (0.2, 3.25)
1599
-
1600
- """
1601
- return _polar_from_reference( # type: ignore[no-any-return]
1602
- measured_phase,
1603
- measured_modulation,
1604
- known_phase,
1605
- known_modulation,
1606
- **kwargs,
1607
- )
1608
-
1609
-
1610
938
  def phasor_to_polar(
1611
939
  real: ArrayLike,
1612
940
  imag: ArrayLike,
@@ -1718,1117 +1046,7 @@ def phasor_from_polar(
1718
1046
  )
1719
1047
 
1720
1048
 
1721
- def 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
2502
-
2503
- Notes
2504
- -----
2505
- The polar coordinates `phase` (:math:`\phi`) and `modulation` (:math:`M`)
2506
- are converted to apparent single lifetimes
2507
- `phase_lifetime` (:math:`\tau_{\phi}`) and
2508
- `modulation_lifetime` (:math:`\tau_{M}`) at frequency :math:`f`
2509
- according to:
2510
-
2511
- .. math::
2512
-
2513
- \omega &= 2 \pi f
2514
-
2515
- \tau_{\phi} &= \omega^{-1} \cdot \tan{\phi}
2516
-
2517
- \tau_{M} &= \omega^{-1} \cdot \sqrt{1 / M^2 - 1}
2518
-
2519
- Examples
2520
- --------
2521
- The apparent single lifetimes from phase and modulation are equal
2522
- only if the polar coordinates lie on the universal semicircle:
2523
-
2524
- >>> polar_to_apparent_lifetime(
2525
- ... math.pi / 4, numpy.hypot([0.5, 0.45], [0.5, 0.45]), frequency=80
2526
- ... ) # doctest: +NUMBER
2527
- (array([1.989, 1.989]), array([1.989, 2.411]))
2528
-
2529
- """
2530
- omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
2531
- omega *= math.pi * 2.0 * unit_conversion
2532
- return _polar_to_apparent_lifetime( # type: ignore[no-any-return]
2533
- phase, modulation, omega, **kwargs
2534
- )
2535
-
2536
-
2537
- def polar_from_apparent_lifetime(
2538
- phase_lifetime: ArrayLike,
2539
- modulation_lifetime: ArrayLike | None,
2540
- /,
2541
- frequency: ArrayLike,
2542
- *,
2543
- unit_conversion: float = 1e-3,
2544
- **kwargs: Any,
2545
- ) -> tuple[NDArray[Any], NDArray[Any]]:
2546
- r"""Return polar coordinates from apparent single lifetimes.
2547
-
2548
- Parameters
2549
- ----------
2550
- phase_lifetime : ndarray
2551
- Apparent single lifetime from phase.
2552
- modulation_lifetime : ndarray, optional
2553
- Apparent single lifetime from modulation.
2554
- If None, `modulation_lifetime` is same as `phase_lifetime`.
2555
- frequency : array_like
2556
- Laser pulse or modulation frequency in MHz.
2557
- unit_conversion : float, optional
2558
- Product of `frequency` and `lifetime` units' prefix factors.
2559
- The default is 1e-3 for MHz and ns, or Hz and ms.
2560
- Use 1.0 for Hz and s.
2561
- **kwargs
2562
- Optional `arguments passed to numpy universal functions
2563
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
2564
-
2565
- Returns
2566
- -------
2567
- phase : ndarray
2568
- Angular component of polar coordinates.
2569
- modulation : ndarray
2570
- Radial component of polar coordinates.
2571
-
2572
- See Also
2573
- --------
2574
- phasorpy.phasor.polar_to_apparent_lifetime
2575
-
2576
- Notes
2577
- -----
2578
- The apparent single lifetimes `phase_lifetime` (:math:`\tau_{\phi}`)
2579
- and `modulation_lifetime` (:math:`\tau_{M}`) are converted to polar
2580
- coordinates `phase` (:math:`\phi`) and `modulation` (:math:`M`) at
2581
- frequency :math:`f` according to:
2582
-
2583
- .. math::
2584
-
2585
- \omega &= 2 \pi f
2586
-
2587
- \phi & = \arctan(\omega \tau_{\phi})
2588
-
2589
- M &= 1 / \sqrt{1 + (\omega \tau_{M})^2}
2590
-
2591
- Examples
2592
- --------
2593
- If the apparent single lifetimes from phase and modulation are equal,
2594
- the polar coordinates lie on the universal semicircle, else inside:
2595
-
2596
- >>> polar_from_apparent_lifetime(
2597
- ... 1.9894, [1.9894, 2.4113], frequency=80.0
2598
- ... ) # doctest: +NUMBER
2599
- (array([0.7854, 0.7854]), array([0.7071, 0.6364]))
2600
-
2601
- """
2602
- omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
2603
- omega *= math.pi * 2.0 * unit_conversion
2604
- if modulation_lifetime is None:
2605
- return _polar_from_single_lifetime( # type: ignore[no-any-return]
2606
- phase_lifetime, omega, **kwargs
2607
- )
2608
- return _polar_from_apparent_lifetime( # type: ignore[no-any-return]
2609
- phase_lifetime, modulation_lifetime, omega, **kwargs
2610
- )
2611
-
2612
-
2613
- def phasor_from_fret_donor(
2614
- frequency: ArrayLike,
2615
- donor_lifetime: ArrayLike,
2616
- *,
2617
- fret_efficiency: ArrayLike = 0.0,
2618
- donor_fretting: ArrayLike = 1.0,
2619
- donor_background: ArrayLike = 0.0,
2620
- background_real: ArrayLike = 0.0,
2621
- background_imag: ArrayLike = 0.0,
2622
- unit_conversion: float = 1e-3,
2623
- **kwargs: Any,
2624
- ) -> tuple[NDArray[Any], NDArray[Any]]:
2625
- """Return phasor coordinates of FRET donor channel.
2626
-
2627
- Calculate phasor coordinates of a FRET (Förster Resonance Energy Transfer)
2628
- donor channel as a function of frequency, donor lifetime, FRET efficiency,
2629
- fraction of donors undergoing FRET, and background fluorescence.
2630
-
2631
- The phasor coordinates of the donor channel contain fractions of:
2632
-
2633
- - donor not undergoing energy transfer
2634
- - donor quenched by energy transfer
2635
- - background fluorescence
2636
-
2637
- Parameters
2638
- ----------
2639
- frequency : array_like
2640
- Laser pulse or modulation frequency in MHz.
2641
- donor_lifetime : array_like
2642
- Lifetime of donor without FRET in ns.
2643
- fret_efficiency : array_like, optional, default 0
2644
- FRET efficiency in range [0, 1].
2645
- donor_fretting : array_like, optional, default 1
2646
- Fraction of donors participating in FRET. Range [0, 1].
2647
- donor_background : array_like, optional, default 0
2648
- Weight of background fluorescence in donor channel
2649
- relative to fluorescence of donor without FRET.
2650
- A weight of 1 means the fluorescence of background and donor
2651
- without FRET are equal.
2652
- background_real : array_like, optional, default 0
2653
- Real component of background fluorescence phasor coordinate
2654
- at `frequency`.
2655
- background_imag : array_like, optional, default 0
2656
- Imaginary component of background fluorescence phasor coordinate
2657
- at `frequency`.
2658
- unit_conversion : float, optional
2659
- Product of `frequency` and `lifetime` units' prefix factors.
2660
- The default is 1e-3 for MHz and ns, or Hz and ms.
2661
- Use 1.0 for Hz and s.
2662
- **kwargs
2663
- Optional `arguments passed to numpy universal functions
2664
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
2665
-
2666
- Returns
2667
- -------
2668
- real : ndarray
2669
- Real component of donor channel phasor coordinates.
2670
- imag : ndarray
2671
- Imaginary component of donor channel phasor coordinates.
2672
-
2673
- See Also
2674
- --------
2675
- phasorpy.phasor.phasor_from_fret_acceptor
2676
- :ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
2677
- :ref:`sphx_glr_tutorials_applications_phasorpy_fret_efficiency.py`
2678
-
2679
- Examples
2680
- --------
2681
- Compute the phasor coordinates of a FRET donor channel at three
2682
- FRET efficiencies:
2683
-
2684
- >>> phasor_from_fret_donor(
2685
- ... frequency=80,
2686
- ... donor_lifetime=4.2,
2687
- ... fret_efficiency=[0.0, 0.3, 1.0],
2688
- ... donor_fretting=0.9,
2689
- ... donor_background=0.1,
2690
- ... background_real=0.11,
2691
- ... background_imag=0.12,
2692
- ... ) # doctest: +NUMBER
2693
- (array([0.1766, 0.2737, 0.1466]), array([0.3626, 0.4134, 0.2534]))
2694
-
2695
- """
2696
- omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
2697
- omega *= math.pi * 2.0 * unit_conversion
2698
- return _phasor_from_fret_donor( # type: ignore[no-any-return]
2699
- omega,
2700
- donor_lifetime,
2701
- fret_efficiency,
2702
- donor_fretting,
2703
- donor_background,
2704
- background_real,
2705
- background_imag,
2706
- **kwargs,
2707
- )
2708
-
2709
-
2710
- def phasor_from_fret_acceptor(
2711
- frequency: ArrayLike,
2712
- donor_lifetime: ArrayLike,
2713
- acceptor_lifetime: ArrayLike,
2714
- *,
2715
- fret_efficiency: ArrayLike = 0.0,
2716
- donor_fretting: ArrayLike = 1.0,
2717
- donor_bleedthrough: ArrayLike = 0.0,
2718
- acceptor_bleedthrough: ArrayLike = 0.0,
2719
- acceptor_background: ArrayLike = 0.0,
2720
- background_real: ArrayLike = 0.0,
2721
- background_imag: ArrayLike = 0.0,
2722
- unit_conversion: float = 1e-3,
2723
- **kwargs: Any,
2724
- ) -> tuple[NDArray[Any], NDArray[Any]]:
2725
- """Return phasor coordinates of FRET acceptor channel.
2726
-
2727
- Calculate phasor coordinates of a FRET (Förster Resonance Energy Transfer)
2728
- acceptor channel as a function of frequency, donor and acceptor lifetimes,
2729
- FRET efficiency, fraction of donors undergoing FRET, fraction of directly
2730
- excited acceptors, fraction of donor fluorescence in acceptor channel,
2731
- and background fluorescence.
2732
-
2733
- The phasor coordinates of the acceptor channel contain fractions of:
2734
-
2735
- - acceptor sensitized by energy transfer
2736
- - directly excited acceptor
2737
- - donor bleedthrough
2738
- - background fluorescence
2739
-
2740
- Parameters
2741
- ----------
2742
- frequency : array_like
2743
- Laser pulse or modulation frequency in MHz.
2744
- donor_lifetime : array_like
2745
- Lifetime of donor without FRET in ns.
2746
- acceptor_lifetime : array_like
2747
- Lifetime of acceptor in ns.
2748
- fret_efficiency : array_like, optional, default 0
2749
- FRET efficiency in range [0, 1].
2750
- donor_fretting : array_like, optional, default 1
2751
- Fraction of donors participating in FRET. Range [0, 1].
2752
- donor_bleedthrough : array_like, optional, default 0
2753
- Weight of donor fluorescence in acceptor channel
2754
- relative to fluorescence of fully sensitized acceptor.
2755
- A weight of 1 means the fluorescence from donor and fully sensitized
2756
- acceptor are equal.
2757
- The background in the donor channel does not bleed through.
2758
- acceptor_bleedthrough : array_like, optional, default 0
2759
- Weight of fluorescence from directly excited acceptor
2760
- relative to fluorescence of fully sensitized acceptor.
2761
- A weight of 1 means the fluorescence from directly excited acceptor
2762
- and fully sensitized acceptor are equal.
2763
- acceptor_background : array_like, optional, default 0
2764
- Weight of background fluorescence in acceptor channel
2765
- relative to fluorescence of fully sensitized acceptor.
2766
- A weight of 1 means the fluorescence of background and fully
2767
- sensitized acceptor are equal.
2768
- background_real : array_like, optional, default 0
2769
- Real component of background fluorescence phasor coordinate
2770
- at `frequency`.
2771
- background_imag : array_like, optional, default 0
2772
- Imaginary component of background fluorescence phasor coordinate
2773
- at `frequency`.
2774
- unit_conversion : float, optional
2775
- Product of `frequency` and `lifetime` units' prefix factors.
2776
- The default is 1e-3 for MHz and ns, or Hz and ms.
2777
- Use 1.0 for Hz and s.
2778
- **kwargs
2779
- Optional `arguments passed to numpy universal functions
2780
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
2781
-
2782
- Returns
2783
- -------
2784
- real : ndarray
2785
- Real component of acceptor channel phasor coordinates.
2786
- imag : ndarray
2787
- Imaginary component of acceptor channel phasor coordinates.
2788
-
2789
- See Also
2790
- --------
2791
- phasorpy.phasor.phasor_from_fret_donor
2792
- :ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
2793
-
2794
- Examples
2795
- --------
2796
- Compute the phasor coordinates of a FRET acceptor channel at three
2797
- FRET efficiencies:
2798
-
2799
- >>> phasor_from_fret_acceptor(
2800
- ... frequency=80,
2801
- ... donor_lifetime=4.2,
2802
- ... acceptor_lifetime=3.0,
2803
- ... fret_efficiency=[0.0, 0.3, 1.0],
2804
- ... donor_fretting=0.9,
2805
- ... donor_bleedthrough=0.1,
2806
- ... acceptor_bleedthrough=0.1,
2807
- ... acceptor_background=0.1,
2808
- ... background_real=0.11,
2809
- ... background_imag=0.12,
2810
- ... ) # doctest: +NUMBER
2811
- (array([0.1996, 0.05772, 0.2867]), array([0.3225, 0.3103, 0.4292]))
2812
-
2813
- """
2814
- omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
2815
- omega *= math.pi * 2.0 * unit_conversion
2816
- return _phasor_from_fret_acceptor( # type: ignore[no-any-return]
2817
- omega,
2818
- donor_lifetime,
2819
- acceptor_lifetime,
2820
- fret_efficiency,
2821
- donor_fretting,
2822
- donor_bleedthrough,
2823
- acceptor_bleedthrough,
2824
- acceptor_background,
2825
- background_real,
2826
- background_imag,
2827
- **kwargs,
2828
- )
2829
-
2830
-
2831
- def phasor_to_principal_plane(
1049
+ def phasor_to_principal_plane(
2832
1050
  real: ArrayLike,
2833
1051
  imag: ArrayLike,
2834
1052
  /,
@@ -2837,7 +1055,7 @@ def phasor_to_principal_plane(
2837
1055
  ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
2838
1056
  """Return multi-harmonic phasor coordinates projected onto principal plane.
2839
1057
 
2840
- Principal Component Analysis (PCA) is used to project
1058
+ Principal component analysis (PCA) is used to project
2841
1059
  multi-harmonic phasor coordinates onto a plane, along which
2842
1060
  coordinate axes the phasor coordinates have the largest variations.
2843
1061
 
@@ -2882,7 +1100,7 @@ def phasor_to_principal_plane(
2882
1100
  -----
2883
1101
 
2884
1102
  This implementation does not work with coordinates containing
2885
- undefined `NaN` values.
1103
+ undefined NaN values.
2886
1104
 
2887
1105
  The transformation matrix can be used to project multi-harmonic phasor
2888
1106
  coordinates, where the first axis is the frequency:
@@ -2902,7 +1120,6 @@ def phasor_to_principal_plane(
2902
1120
 
2903
1121
  References
2904
1122
  ----------
2905
-
2906
1123
  .. [1] Franssen WMJ, Vergeldt FJ, Bader AN, van Amerongen H, and Terenzi C.
2907
1124
  `Full-harmonics phasor analysis: unravelling multiexponential trends
2908
1125
  in magnetic resonance imaging data
@@ -2935,7 +1152,7 @@ def phasor_to_principal_plane(
2935
1152
  im = im.reshape(im.shape[0], -1)
2936
1153
 
2937
1154
  # vector of multi-frequency phasor coordinates
2938
- coordinates = numpy.vstack((re, im))
1155
+ coordinates = numpy.vstack([re, im])
2939
1156
 
2940
1157
  # vector of centered coordinates
2941
1158
  center = numpy.nanmean(coordinates, axis=1, keepdims=True)
@@ -2997,619 +1214,6 @@ def phasor_to_principal_plane(
2997
1214
  )
2998
1215
 
2999
1216
 
3000
- def phasor_filter_median(
3001
- mean: ArrayLike,
3002
- real: ArrayLike,
3003
- imag: ArrayLike,
3004
- /,
3005
- *,
3006
- repeat: int = 1,
3007
- size: int = 3,
3008
- skip_axis: int | Sequence[int] | None = None,
3009
- use_scipy: bool = False,
3010
- num_threads: int | None = None,
3011
- **kwargs: Any,
3012
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
3013
- """Return median-filtered phasor coordinates.
3014
-
3015
- By default, apply a NaN-aware median filter independently to the real
3016
- and imaginary components of phasor coordinates once with a kernel size of 3
3017
- multiplied by the number of dimensions of the input arrays. Return the
3018
- intensity unchanged.
3019
-
3020
- Parameters
3021
- ----------
3022
- mean : array_like
3023
- Intensity of phasor coordinates.
3024
- real : array_like
3025
- Real component of phasor coordinates to be filtered.
3026
- imag : array_like
3027
- Imaginary component of phasor coordinates to be filtered.
3028
- repeat : int, optional
3029
- Number of times to apply median filter. The default is 1.
3030
- size : int, optional
3031
- Size of median filter kernel. The default is 3.
3032
- skip_axis : int or sequence of int, optional
3033
- Axes in `mean` to exclude from filter.
3034
- By default, all axes except harmonics are included.
3035
- use_scipy : bool, optional
3036
- Use :py:func:`scipy.ndimage.median_filter`.
3037
- This function has undefined behavior if the input arrays contain
3038
- `NaN` values but is faster when filtering more than 2 dimensions.
3039
- See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
3040
- num_threads : int, optional
3041
- Number of OpenMP threads to use for parallelization.
3042
- Applies to filtering in two dimensions when not using scipy.
3043
- By default, multi-threading is disabled.
3044
- If zero, up to half of logical CPUs are used.
3045
- OpenMP may not be available on all platforms.
3046
- **kwargs
3047
- Optional arguments passed to :py:func:`scipy.ndimage.median_filter`.
3048
-
3049
- Returns
3050
- -------
3051
- mean : ndarray
3052
- Unchanged intensity of phasor coordinates.
3053
- real : ndarray
3054
- Filtered real component of phasor coordinates.
3055
- imag : ndarray
3056
- Filtered imaginary component of phasor coordinates.
3057
-
3058
- Raises
3059
- ------
3060
- ValueError
3061
- If `repeat` is less than 0.
3062
- If `size` is less than 1.
3063
- The array shapes of `mean`, `real`, and `imag` do not match.
3064
-
3065
- Examples
3066
- --------
3067
- Apply three times a median filter with a kernel size of three:
3068
-
3069
- >>> mean, real, imag = phasor_filter_median(
3070
- ... [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
3071
- ... [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5], [0.2, 0.2, 0.2]],
3072
- ... [[0.3, 0.3, 0.3], [0.6, math.nan, 0.6], [0.4, 0.4, 0.4]],
3073
- ... size=3,
3074
- ... repeat=3,
3075
- ... )
3076
- >>> mean
3077
- array([[1, 2, 3],
3078
- [4, 5, 6],
3079
- [7, 8, 9]])
3080
- >>> real
3081
- array([[0, 0, 0],
3082
- [0.2, 0.2, 0.2],
3083
- [0.2, 0.2, 0.2]])
3084
- >>> imag
3085
- array([[0.3, 0.3, 0.3],
3086
- [0.4, nan, 0.4],
3087
- [0.4, 0.4, 0.4]])
3088
-
3089
- """
3090
- if repeat < 0:
3091
- raise ValueError(f'{repeat=} < 0')
3092
- if size < 1:
3093
- raise ValueError(f'{size=} < 1')
3094
- if size == 1:
3095
- # no need to filter
3096
- repeat = 0
3097
-
3098
- mean = numpy.asarray(mean)
3099
- if use_scipy or repeat == 0: # or using nD numpy filter
3100
- real = numpy.asarray(real)
3101
- elif isinstance(real, numpy.ndarray) and real.dtype == numpy.float32:
3102
- real = real.copy()
3103
- else:
3104
- real = numpy.array(real, numpy.float64, copy=True)
3105
- if use_scipy or repeat == 0: # or using nD numpy filter
3106
- imag = numpy.asarray(imag)
3107
- elif isinstance(imag, numpy.ndarray) and imag.dtype == numpy.float32:
3108
- imag = imag.copy()
3109
- else:
3110
- imag = numpy.array(imag, numpy.float64, copy=True)
3111
-
3112
- if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
3113
- raise ValueError(f'{mean.shape=} != {real.shape=}')
3114
- if real.shape != imag.shape:
3115
- raise ValueError(f'{real.shape=} != {imag.shape=}')
3116
-
3117
- prepend_axis = mean.ndim + 1 == real.ndim
3118
- _, axes = parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
3119
-
3120
- # in case mean is also filtered
3121
- # if prepend_axis:
3122
- # mean = numpy.expand_dims(mean, axis=0)
3123
- # ...
3124
- # if prepend_axis:
3125
- # mean = numpy.asarray(mean[0])
3126
-
3127
- if repeat == 0:
3128
- # no need to call filter
3129
- return mean, real, imag
3130
-
3131
- if use_scipy:
3132
- # use scipy NaN-unaware fallback
3133
- from scipy.ndimage import median_filter
3134
-
3135
- kwargs.pop('axes', None)
3136
-
3137
- for _ in range(repeat):
3138
- real = median_filter(real, size=size, axes=axes, **kwargs)
3139
- imag = median_filter(imag, size=size, axes=axes, **kwargs)
3140
-
3141
- return mean, numpy.asarray(real), numpy.asarray(imag)
3142
-
3143
- if len(axes) != 2:
3144
- # n-dimensional median filter using numpy
3145
- from numpy.lib.stride_tricks import sliding_window_view
3146
-
3147
- kernel_shape = tuple(
3148
- size if i in axes else 1 for i in range(real.ndim)
3149
- )
3150
- pad_width = [
3151
- (s // 2, s // 2) if s > 1 else (0, 0) for s in kernel_shape
3152
- ]
3153
- axis = tuple(range(-real.ndim, 0))
3154
-
3155
- nan_mask = numpy.isnan(real)
3156
- for _ in range(repeat):
3157
- real = numpy.pad(real, pad_width, mode='edge')
3158
- real = sliding_window_view(real, kernel_shape)
3159
- real = numpy.nanmedian(real, axis=axis)
3160
- real = numpy.where(nan_mask, numpy.nan, real)
3161
-
3162
- nan_mask = numpy.isnan(imag)
3163
- for _ in range(repeat):
3164
- imag = numpy.pad(imag, pad_width, mode='edge')
3165
- imag = sliding_window_view(imag, kernel_shape)
3166
- imag = numpy.nanmedian(imag, axis=axis)
3167
- imag = numpy.where(nan_mask, numpy.nan, imag)
3168
-
3169
- return mean, real, imag
3170
-
3171
- # 2-dimensional median filter using optimized Cython implementation
3172
- num_threads = number_threads(num_threads)
3173
-
3174
- buffer = numpy.empty(
3175
- tuple(real.shape[axis] for axis in axes), dtype=real.dtype
3176
- )
3177
-
3178
- for index in numpy.ndindex(
3179
- *[real.shape[ax] for ax in range(real.ndim) if ax not in axes]
3180
- ):
3181
- index_list: list[int | slice] = list(index)
3182
- for ax in axes:
3183
- index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
3184
- full_index = tuple(index_list)
3185
-
3186
- _median_filter_2d(real[full_index], buffer, size, repeat, num_threads)
3187
- _median_filter_2d(imag[full_index], buffer, size, repeat, num_threads)
3188
-
3189
- return mean, real, imag
3190
-
3191
-
3192
- def phasor_filter_pawflim(
3193
- mean: ArrayLike,
3194
- real: ArrayLike,
3195
- imag: ArrayLike,
3196
- /,
3197
- *,
3198
- sigma: float = 2.0,
3199
- levels: int = 1,
3200
- harmonic: Sequence[int] | None = None,
3201
- skip_axis: int | Sequence[int] | None = None,
3202
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
3203
- """Return pawFLIM wavelet-filtered phasor coordinates.
3204
-
3205
- This function must only be used with calibrated, unprocessed phasor
3206
- coordinates obtained from FLIM data. The coordinates must not be filtered,
3207
- thresholded, or otherwise pre-processed.
3208
-
3209
- The pawFLIM wavelet filter is described in [2]_.
3210
-
3211
- Parameters
3212
- ----------
3213
- mean : array_like
3214
- Intensity of phasor coordinates.
3215
- real : array_like
3216
- Real component of phasor coordinates to be filtered.
3217
- Must have at least two harmonics in the first axis.
3218
- imag : array_like
3219
- Imaginary component of phasor coordinates to be filtered.
3220
- Must have at least two harmonics in the first axis.
3221
- sigma : float, optional
3222
- Significance level to test difference between two phasors.
3223
- Given in terms of the equivalent 1D standard deviations.
3224
- sigma=2 corresponds to ~95% (or 5%) significance.
3225
- levels : int, optional
3226
- Number of levels for wavelet decomposition.
3227
- Controls the maximum averaging area, which has a length of
3228
- :math:`2^level`.
3229
- harmonic : sequence of int or None, optional
3230
- Harmonics included in first axis of `real` and `imag`.
3231
- If None (default), the first axis of `real` and `imag` contains lower
3232
- harmonics starting at and increasing by one.
3233
- All harmonics must have a corresponding half or double harmonic.
3234
- skip_axis : int or sequence of int, optional
3235
- Axes in `mean` to exclude from filter.
3236
- By default, all axes except harmonics are included.
3237
-
3238
- Returns
3239
- -------
3240
- mean : ndarray
3241
- Unchanged intensity of phasor coordinates.
3242
- real : ndarray
3243
- Filtered real component of phasor coordinates.
3244
- imag : ndarray
3245
- Filtered imaginary component of phasor coordinates.
3246
-
3247
- Raises
3248
- ------
3249
- ValueError
3250
- If `level` is less than 0.
3251
- The array shapes of `mean`, `real`, and `imag` do not match.
3252
- If `real` and `imag` have no harmonic axis.
3253
- Number of harmonics in `harmonic` is less than 2 or does not match
3254
- the first axis of `real` and `imag`.
3255
- Not all harmonics in `harmonic` have a corresponding half
3256
- or double harmonic.
3257
-
3258
- References
3259
- ----------
3260
-
3261
- .. [2] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
3262
- uncertainty to enable lower photon count in FLIM experiments
3263
- <https://doi.org/10.1088/2050-6120/aa72ab>`_.
3264
- *Methods Appl Fluoresc*, 5(2): 024016 (2017)
3265
-
3266
- Examples
3267
- --------
3268
- Apply a pawFLIM wavelet filter with four significance levels (sigma)
3269
- and three decomposition levels:
3270
-
3271
- >>> mean, real, imag = phasor_filter_pawflim(
3272
- ... [[1, 1], [1, 1]],
3273
- ... [[[0.5, 0.8], [0.5, 0.8]], [[0.2, 0.4], [0.2, 0.4]]],
3274
- ... [[[0.5, 0.4], [0.5, 0.4]], [[0.4, 0.5], [0.4, 0.5]]],
3275
- ... sigma=4,
3276
- ... levels=3,
3277
- ... harmonic=[1, 2],
3278
- ... )
3279
- >>> mean
3280
- array([[1, 1],
3281
- [1, 1]])
3282
- >>> real
3283
- array([[[0.65, 0.65],
3284
- [0.65, 0.65]],
3285
- [[0.3, 0.3],
3286
- [0.3, 0.3]]])
3287
- >>> imag
3288
- array([[[0.45, 0.45],
3289
- [0.45, 0.45]],
3290
- [[0.45, 0.45],
3291
- [0.45, 0.45]]])
3292
-
3293
- """
3294
- from pawflim import pawflim # type: ignore[import-untyped]
3295
-
3296
- mean = numpy.asarray(mean)
3297
- real = numpy.asarray(real)
3298
- imag = numpy.asarray(imag)
3299
-
3300
- if levels < 0:
3301
- raise ValueError(f'{levels=} < 0')
3302
- if levels == 0:
3303
- return mean, real, imag
3304
-
3305
- if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
3306
- raise ValueError(f'{mean.shape=} != {real.shape=}')
3307
- if real.shape != imag.shape:
3308
- raise ValueError(f'{real.shape=} != {imag.shape=}')
3309
-
3310
- has_harmonic_axis = mean.ndim + 1 == real.ndim
3311
- if not has_harmonic_axis:
3312
- raise ValueError('no harmonic axis')
3313
- if harmonic is None:
3314
- harmonics, _ = parse_harmonic('all', real.shape[0])
3315
- else:
3316
- harmonics, _ = parse_harmonic(harmonic, None)
3317
- if len(harmonics) < 2:
3318
- raise ValueError(
3319
- 'at least two harmonics required, ' f'got {len(harmonics)}'
3320
- )
3321
- if len(harmonics) != real.shape[0]:
3322
- raise ValueError(
3323
- 'number of harmonics does not match first axis of real and imag'
3324
- )
3325
-
3326
- mean = numpy.asarray(numpy.nan_to_num(mean), dtype=float)
3327
- real = numpy.asarray(numpy.nan_to_num(real * mean), dtype=float)
3328
- imag = numpy.asarray(numpy.nan_to_num(imag * mean), dtype=float)
3329
-
3330
- mean_expanded = numpy.broadcast_to(mean, real.shape).copy()
3331
- original_mean_expanded = mean_expanded.copy()
3332
- real_filtered = real.copy()
3333
- imag_filtered = imag.copy()
3334
-
3335
- _, axes = parse_skip_axis(skip_axis, mean.ndim, True)
3336
-
3337
- for index in numpy.ndindex(
3338
- *(
3339
- real.shape[ax]
3340
- for ax in range(real.ndim)
3341
- if ax not in axes and ax != 0
3342
- )
3343
- ):
3344
- index_list: list[int | slice] = list(index)
3345
- for ax in axes:
3346
- index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
3347
- full_index = tuple(index_list)
3348
-
3349
- processed_harmonics = set()
3350
-
3351
- for h in harmonics:
3352
- if h in processed_harmonics and (
3353
- h * 4 in harmonics or h * 2 not in harmonics
3354
- ):
3355
- continue
3356
- if h * 2 not in harmonics:
3357
- raise ValueError(
3358
- f'harmonic {h} does not have a corresponding half '
3359
- f'or double harmonic in {harmonics}'
3360
- )
3361
- n = harmonics.index(h)
3362
- n2 = harmonics.index(h * 2)
3363
-
3364
- complex_phasor = numpy.empty(
3365
- (3, *original_mean_expanded[n][full_index].shape),
3366
- dtype=complex,
3367
- )
3368
- complex_phasor[0] = original_mean_expanded[n][full_index]
3369
- complex_phasor[1] = real[n][full_index] + 1j * imag[n][full_index]
3370
- complex_phasor[2] = (
3371
- real[n2][full_index] + 1j * imag[n2][full_index]
3372
- )
3373
-
3374
- complex_phasor = pawflim(
3375
- complex_phasor, n_sigmas=sigma, levels=levels
3376
- )
3377
-
3378
- for i, idx in enumerate([n, n2]):
3379
- if harmonics[idx] in processed_harmonics:
3380
- continue
3381
- mean_expanded[idx][full_index] = complex_phasor[0].real
3382
- real_filtered[idx][full_index] = complex_phasor[i + 1].real
3383
- imag_filtered[idx][full_index] = complex_phasor[i + 1].imag
3384
-
3385
- processed_harmonics.add(h)
3386
- processed_harmonics.add(h * 2)
3387
-
3388
- with numpy.errstate(divide='ignore', invalid='ignore'):
3389
- real = numpy.asarray(numpy.divide(real_filtered, mean_expanded))
3390
- imag = numpy.asarray(numpy.divide(imag_filtered, mean_expanded))
3391
-
3392
- return mean, real, imag
3393
-
3394
-
3395
- def phasor_threshold(
3396
- mean: ArrayLike,
3397
- real: ArrayLike,
3398
- imag: ArrayLike,
3399
- /,
3400
- mean_min: ArrayLike | None = None,
3401
- mean_max: ArrayLike | None = None,
3402
- *,
3403
- real_min: ArrayLike | None = None,
3404
- real_max: ArrayLike | None = None,
3405
- imag_min: ArrayLike | None = None,
3406
- imag_max: ArrayLike | None = None,
3407
- phase_min: ArrayLike | None = None,
3408
- phase_max: ArrayLike | None = None,
3409
- modulation_min: ArrayLike | None = None,
3410
- modulation_max: ArrayLike | None = None,
3411
- open_interval: bool = False,
3412
- detect_harmonics: bool = True,
3413
- **kwargs: Any,
3414
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
3415
- """Return phasor coordinates with values out of interval replaced by NaN.
3416
-
3417
- Interval thresholds can be set for mean intensity, real and imaginary
3418
- coordinates, and phase and modulation.
3419
- Phasor coordinates smaller than minimum thresholds or larger than maximum
3420
- thresholds are replaced NaN.
3421
- No threshold is applied by default.
3422
- NaNs in `mean` or any `real` and `imag` harmonic are propagated to
3423
- `mean` and all harmonics in `real` and `imag`.
3424
-
3425
- Parameters
3426
- ----------
3427
- mean : array_like
3428
- Intensity of phasor coordinates.
3429
- real : array_like
3430
- Real component of phasor coordinates.
3431
- imag : array_like
3432
- Imaginary component of phasor coordinates.
3433
- mean_min : array_like, optional
3434
- Lower threshold for mean intensity.
3435
- mean_max : array_like, optional
3436
- Upper threshold for mean intensity.
3437
- real_min : array_like, optional
3438
- Lower threshold for real coordinates.
3439
- real_max : array_like, optional
3440
- Upper threshold for real coordinates.
3441
- imag_min : array_like, optional
3442
- Lower threshold for imaginary coordinates.
3443
- imag_max : array_like, optional
3444
- Upper threshold for imaginary coordinates.
3445
- phase_min : array_like, optional
3446
- Lower threshold for phase angle.
3447
- phase_max : array_like, optional
3448
- Upper threshold for phase angle.
3449
- modulation_min : array_like, optional
3450
- Lower threshold for modulation.
3451
- modulation_max : array_like, optional
3452
- Upper threshold for modulation.
3453
- open_interval : bool, optional
3454
- If true, the interval is open, and the threshold values are
3455
- not included in the interval.
3456
- If false (default), the interval is closed, and the threshold values
3457
- are included in the interval.
3458
- detect_harmonics : bool, optional
3459
- By default, detect presence of multiple harmonics from array shapes.
3460
- If false, no harmonics are assumed to be present, and the function
3461
- behaves like a numpy universal function.
3462
- **kwargs
3463
- Optional `arguments passed to numpy universal functions
3464
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
3465
-
3466
- Returns
3467
- -------
3468
- mean : ndarray
3469
- Thresholded intensity of phasor coordinates.
3470
- real : ndarray
3471
- Thresholded real component of phasor coordinates.
3472
- imag : ndarray
3473
- Thresholded imaginary component of phasor coordinates.
3474
-
3475
- Examples
3476
- --------
3477
- Set phasor coordinates to NaN if mean intensity is smaller than 1.1:
3478
-
3479
- >>> phasor_threshold([1, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], 1.1)
3480
- (array([nan, 2, 3]), array([nan, 0.2, 0.3]), array([nan, 0.5, 0.6]))
3481
-
3482
- Set phasor coordinates to NaN if real component is smaller than 0.15 or
3483
- larger than 0.25:
3484
-
3485
- >>> phasor_threshold(
3486
- ... [1.0, 2.0, 3.0],
3487
- ... [0.1, 0.2, 0.3],
3488
- ... [0.4, 0.5, 0.6],
3489
- ... real_min=0.15,
3490
- ... real_max=0.25,
3491
- ... )
3492
- (array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
3493
-
3494
- Apply NaNs to other input arrays:
3495
-
3496
- >>> phasor_threshold(
3497
- ... [numpy.nan, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, numpy.nan]
3498
- ... )
3499
- (array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
3500
-
3501
- """
3502
- threshold_mean_only = None
3503
- if mean_min is None:
3504
- mean_min = numpy.nan
3505
- else:
3506
- threshold_mean_only = True
3507
- if mean_max is None:
3508
- mean_max = numpy.nan
3509
- else:
3510
- threshold_mean_only = True
3511
- if real_min is None:
3512
- real_min = numpy.nan
3513
- else:
3514
- threshold_mean_only = False
3515
- if real_max is None:
3516
- real_max = numpy.nan
3517
- else:
3518
- threshold_mean_only = False
3519
- if imag_min is None:
3520
- imag_min = numpy.nan
3521
- else:
3522
- threshold_mean_only = False
3523
- if imag_max is None:
3524
- imag_max = numpy.nan
3525
- else:
3526
- threshold_mean_only = False
3527
- if phase_min is None:
3528
- phase_min = numpy.nan
3529
- else:
3530
- threshold_mean_only = False
3531
- if phase_max is None:
3532
- phase_max = numpy.nan
3533
- else:
3534
- threshold_mean_only = False
3535
- if modulation_min is None:
3536
- modulation_min = numpy.nan
3537
- else:
3538
- threshold_mean_only = False
3539
- if modulation_max is None:
3540
- modulation_max = numpy.nan
3541
- else:
3542
- threshold_mean_only = False
3543
-
3544
- if detect_harmonics:
3545
- mean = numpy.asarray(mean)
3546
- real = numpy.asarray(real)
3547
- imag = numpy.asarray(imag)
3548
-
3549
- shape = numpy.broadcast_shapes(mean.shape, real.shape, imag.shape)
3550
- ndim = len(shape)
3551
-
3552
- has_harmonic_axis = (
3553
- # detect multi-harmonic in axis 0
3554
- mean.ndim + 1 == ndim
3555
- and real.shape == shape
3556
- and imag.shape == shape
3557
- and mean.shape == shape[-mean.ndim if mean.ndim else 1 :]
3558
- )
3559
- else:
3560
- has_harmonic_axis = False
3561
-
3562
- if threshold_mean_only is None:
3563
- mean, real, imag = _phasor_threshold_nan(mean, real, imag, **kwargs)
3564
-
3565
- elif threshold_mean_only:
3566
- mean_func = (
3567
- _phasor_threshold_mean_open
3568
- if open_interval
3569
- else _phasor_threshold_mean_closed
3570
- )
3571
- mean, real, imag = mean_func(
3572
- mean, real, imag, mean_min, mean_max, **kwargs
3573
- )
3574
-
3575
- else:
3576
- func = (
3577
- _phasor_threshold_open
3578
- if open_interval
3579
- else _phasor_threshold_closed
3580
- )
3581
- mean, real, imag = func(
3582
- mean,
3583
- real,
3584
- imag,
3585
- mean_min,
3586
- mean_max,
3587
- real_min,
3588
- real_max,
3589
- imag_min,
3590
- imag_max,
3591
- phase_min,
3592
- phase_max,
3593
- modulation_min,
3594
- modulation_max,
3595
- **kwargs,
3596
- )
3597
-
3598
- mean = numpy.asarray(mean)
3599
- real = numpy.asarray(real)
3600
- imag = numpy.asarray(imag)
3601
- if has_harmonic_axis and mean.ndim > 0:
3602
- # propagate NaN to all dimensions
3603
- mean = numpy.mean(mean, axis=0, keepdims=True)
3604
- mask = numpy.where(numpy.isnan(mean), numpy.nan, 1.0)
3605
- numpy.multiply(real, mask, out=real)
3606
- numpy.multiply(imag, mask, out=imag)
3607
- # remove harmonic dimension created by broadcasting
3608
- mean = numpy.asarray(numpy.asarray(mean)[0])
3609
-
3610
- return mean, real, imag
3611
-
3612
-
3613
1217
  def phasor_nearest_neighbor(
3614
1218
  real: ArrayLike,
3615
1219
  imag: ArrayLike,
@@ -3622,7 +1226,7 @@ def phasor_nearest_neighbor(
3622
1226
  distance_max: float | None = None,
3623
1227
  num_threads: int | None = None,
3624
1228
  ) -> NDArray[Any]:
3625
- """Return indices or values of nearest neighbor from other coordinates.
1229
+ """Return indices or values of nearest neighbors from other coordinates.
3626
1230
 
3627
1231
  For each phasor coordinate, find the nearest neighbor in another set of
3628
1232
  phasor coordinates and return its flat index. If more than one neighbor
@@ -3723,7 +1327,7 @@ def phasor_nearest_neighbor(
3723
1327
  neighbor_imag = neighbor_imag.ravel()
3724
1328
 
3725
1329
  indices = numpy.empty(
3726
- real.shape, numpy.min_scalar_type(-neighbor_real.size)
1330
+ real.shape, dtype=numpy.min_scalar_type(-neighbor_real.size)
3727
1331
  )
3728
1332
 
3729
1333
  if distance_max is None: