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/lifetime.py ADDED
@@ -0,0 +1,2058 @@
1
+ """Calculate, convert, and calibrate phasor coordinates of lifetimes.
2
+
3
+ The ``phasorpy.lifetime`` module provides functions to:
4
+
5
+ - synthesize time and frequency domain signals from fluorescence lifetimes:
6
+
7
+ - :py:func:`lifetime_to_signal`
8
+
9
+ - convert between phasor coordinates and single- or multi-component
10
+ fluorescence lifetimes:
11
+
12
+ - :py:func:`phasor_from_lifetime`
13
+ - :py:func:`phasor_from_apparent_lifetime`
14
+ - :py:func:`phasor_to_apparent_lifetime`
15
+ - :py:func:`phasor_to_normal_lifetime`
16
+ - :py:func:`phasor_to_lifetime_search`
17
+
18
+ - convert to and from polar coordinates (phase and modulation):
19
+
20
+ - :py:func:`polar_from_apparent_lifetime`
21
+ - :py:func:`polar_to_apparent_lifetime`
22
+
23
+ - calibrate phasor coordinates with a reference of known fluorescence
24
+ lifetime:
25
+
26
+ - :py:func:`phasor_calibrate`
27
+ - :py:func:`polar_from_reference`
28
+ - :py:func:`polar_from_reference_phasor`
29
+
30
+ - calculate phasor coordinates for FRET donor and acceptor channels:
31
+
32
+ - :py:func:`phasor_from_fret_donor`
33
+ - :py:func:`phasor_from_fret_acceptor`
34
+
35
+ - convert between single component lifetimes and optimal frequency:
36
+
37
+ - :py:func:`lifetime_to_frequency`
38
+ - :py:func:`lifetime_from_frequency`
39
+
40
+ - convert between fractional intensities and pre-exponential amplitudes:
41
+
42
+ - :py:func:`lifetime_fraction_from_amplitude`
43
+ - :py:func:`lifetime_fraction_to_amplitude`
44
+
45
+ - calculate phasor coordinates on the universal semicircle:
46
+
47
+ - :py:func:`phasor_semicircle`
48
+ - :py:func:`phasor_semicircle_intersect`
49
+ - :py:func:`phasor_at_harmonic`
50
+
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ __all__ = [
56
+ 'lifetime_fraction_from_amplitude',
57
+ 'lifetime_fraction_to_amplitude',
58
+ 'lifetime_from_frequency',
59
+ 'lifetime_to_frequency',
60
+ 'lifetime_to_signal',
61
+ 'phasor_at_harmonic',
62
+ 'phasor_calibrate',
63
+ 'phasor_from_apparent_lifetime',
64
+ 'phasor_from_fret_acceptor',
65
+ 'phasor_from_fret_donor',
66
+ 'phasor_from_lifetime',
67
+ 'phasor_semicircle',
68
+ 'phasor_semicircle_intersect',
69
+ 'phasor_to_apparent_lifetime',
70
+ 'phasor_to_lifetime_search',
71
+ 'phasor_to_normal_lifetime',
72
+ 'polar_from_apparent_lifetime',
73
+ 'polar_from_reference',
74
+ 'polar_from_reference_phasor',
75
+ 'polar_to_apparent_lifetime',
76
+ ]
77
+
78
+ import math
79
+ from collections.abc import Sequence
80
+ from typing import TYPE_CHECKING
81
+
82
+ if TYPE_CHECKING:
83
+ from ._typing import Any, NDArray, ArrayLike, DTypeLike, Literal
84
+
85
+ import numpy
86
+
87
+ from ._phasorpy import (
88
+ _gaussian_signal,
89
+ _intersect_semicircle_line,
90
+ _lifetime_search_2,
91
+ _phasor_at_harmonic,
92
+ _phasor_from_apparent_lifetime,
93
+ _phasor_from_fret_acceptor,
94
+ _phasor_from_fret_donor,
95
+ _phasor_from_lifetime,
96
+ _phasor_from_single_lifetime,
97
+ _phasor_to_apparent_lifetime,
98
+ _phasor_to_normal_lifetime,
99
+ _polar_from_apparent_lifetime,
100
+ _polar_from_reference,
101
+ _polar_from_reference_phasor,
102
+ _polar_from_single_lifetime,
103
+ _polar_to_apparent_lifetime,
104
+ )
105
+ from ._utils import parse_harmonic, parse_skip_axis
106
+ from .phasor import (
107
+ phasor_center,
108
+ phasor_from_signal,
109
+ phasor_multiply,
110
+ phasor_to_signal,
111
+ phasor_transform,
112
+ )
113
+ from .utils import number_threads
114
+
115
+
116
+ def phasor_from_lifetime(
117
+ frequency: ArrayLike,
118
+ lifetime: ArrayLike,
119
+ fraction: ArrayLike | None = None,
120
+ *,
121
+ preexponential: bool = False,
122
+ unit_conversion: float = 1e-3,
123
+ keepdims: bool = False,
124
+ ) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
125
+ r"""Return phasor coordinates from lifetime components.
126
+
127
+ Calculate phasor coordinates as a function of frequency, single or
128
+ multiple lifetime components, and the pre-exponential amplitudes
129
+ or fractional intensities of the components.
130
+
131
+ Parameters
132
+ ----------
133
+ frequency : array_like
134
+ Laser pulse or modulation frequency in MHz.
135
+ A scalar or one-dimensional sequence.
136
+ lifetime : array_like
137
+ Lifetime components in ns. See notes below for allowed dimensions.
138
+ fraction : array_like, optional
139
+ Fractional intensities or pre-exponential amplitudes of the lifetime
140
+ components. Fractions are normalized to sum to 1.
141
+ See notes below for allowed dimensions.
142
+ preexponential : bool, optional, default: False
143
+ If true, `fraction` values are pre-exponential amplitudes,
144
+ else fractional intensities.
145
+ unit_conversion : float, optional, default: 1e-3
146
+ Product of `frequency` and `lifetime` units' prefix factors.
147
+ The default is 1e-3 for MHz and ns, or Hz and ms.
148
+ Use 1.0 for Hz and s.
149
+ keepdims : bool, optional, default: False
150
+ If true, length-one dimensions are left in phasor coordinates.
151
+
152
+ Returns
153
+ -------
154
+ real : ndarray
155
+ Real component of phasor coordinates.
156
+ imag : ndarray
157
+ Imaginary component of phasor coordinates.
158
+
159
+ See notes below for dimensions of the returned arrays.
160
+
161
+ Raises
162
+ ------
163
+ ValueError
164
+ Input arrays exceed their allowed dimensionality or do not match.
165
+
166
+ See Also
167
+ --------
168
+ :ref:`sphx_glr_tutorials_api_phasorpy_phasor_from_lifetime.py`
169
+ :ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
170
+
171
+ Notes
172
+ -----
173
+ The phasor coordinates :math:`G` (`real`) and :math:`S` (`imag`) for
174
+ many lifetime components :math:`j` with lifetimes :math:`\tau` and
175
+ pre-exponential amplitudes :math:`\alpha` at frequency :math:`f` are:
176
+
177
+ .. math::
178
+
179
+ \omega &= 2 \pi f
180
+
181
+ g_{j} &= \alpha_{j} / (1 + (\omega \tau_{j})^2)
182
+
183
+ G &= \sum_{j} g_{j}
184
+
185
+ S &= \sum_{j} \omega \tau_{j} g_{j}
186
+
187
+ The relation between pre-exponential amplitudes :math:`a` and
188
+ fractional intensities :math:`\alpha` is:
189
+
190
+ .. math::
191
+ F_{DC} &= \sum_{j} a_{j} \tau_{j}
192
+
193
+ \alpha_{j} &= a_{j} \tau_{j} / F_{DC}
194
+
195
+ The following combinations of `lifetime` and `fraction` parameters are
196
+ supported:
197
+
198
+ - `lifetime` is scalar or one-dimensional, holding single component
199
+ lifetimes. `fraction` is None.
200
+ Return arrays of shape `(frequency.size, lifetime.size)`.
201
+
202
+ - `lifetime` is two-dimensional, `fraction` is one-dimensional.
203
+ The last dimensions match in size, holding lifetime components and
204
+ their fractions.
205
+ Return arrays of shape `(frequency.size, lifetime.shape[1])`.
206
+
207
+ - `lifetime` is one-dimensional, `fraction` is two-dimensional.
208
+ The last dimensions must match in size, holding lifetime components and
209
+ their fractions.
210
+ Return arrays of shape `(frequency.size, fraction.shape[1])`.
211
+
212
+ - `lifetime` and `fraction` are up to two-dimensional of same shape.
213
+ The last dimensions hold lifetime components and their fractions.
214
+ Return arrays of shape `(frequency.size, lifetime.shape[0])`.
215
+
216
+ Length-one dimensions are removed from returned arrays
217
+ if `keepdims` is false (default).
218
+
219
+ Examples
220
+ --------
221
+ Phasor coordinates of a single lifetime component (in ns) at a
222
+ frequency of 80 MHz:
223
+
224
+ >>> phasor_from_lifetime(80.0, 1.9894368) # doctest: +NUMBER
225
+ (0.5, 0.5)
226
+
227
+ Phasor coordinates of two lifetime components with equal fractional
228
+ intensities:
229
+
230
+ >>> phasor_from_lifetime(
231
+ ... 80.0, [3.9788735, 0.9947183], [0.5, 0.5]
232
+ ... ) # doctest: +NUMBER
233
+ (0.5, 0.4)
234
+
235
+ Phasor coordinates of two lifetime components with equal pre-exponential
236
+ amplitudes:
237
+
238
+ >>> phasor_from_lifetime(
239
+ ... 80.0, [3.9788735, 0.9947183], [0.5, 0.5], preexponential=True
240
+ ... ) # doctest: +NUMBER
241
+ (0.32, 0.4)
242
+
243
+ Phasor coordinates of many single-component lifetimes (fractions omitted):
244
+
245
+ >>> phasor_from_lifetime(
246
+ ... 80.0, [3.9788735, 1.9894368, 0.9947183]
247
+ ... ) # doctest: +NUMBER
248
+ (array([0.2, 0.5, 0.8]), array([0.4, 0.5, 0.4]))
249
+
250
+ Phasor coordinates of two lifetime components with varying fractions:
251
+
252
+ >>> phasor_from_lifetime(
253
+ ... 80.0, [3.9788735, 0.9947183], [[1, 0], [0.5, 0.5], [0, 1]]
254
+ ... ) # doctest: +NUMBER
255
+ (array([0.2, 0.5, 0.8]), array([0.4, 0.4, 0.4]))
256
+
257
+ Phasor coordinates of multiple two-component lifetimes with constant
258
+ fractions, keeping dimensions:
259
+
260
+ >>> phasor_from_lifetime(
261
+ ... 80.0, [[3.9788735, 0.9947183], [1.9894368, 1.9894368]], [0.5, 0.5]
262
+ ... ) # doctest: +NUMBER
263
+ (array([0.5, 0.5]), array([0.4, 0.5]))
264
+
265
+ Phasor coordinates of multiple two-component lifetimes with specific
266
+ fractions at multiple frequencies. Frequencies are in Hz, lifetimes in ns:
267
+
268
+ >>> phasor_from_lifetime(
269
+ ... [40e6, 80e6],
270
+ ... [[1e-9, 0.9947183e-9], [3.9788735e-9, 0.9947183e-9]],
271
+ ... [[0, 1], [0.5, 0.5]],
272
+ ... unit_conversion=1.0,
273
+ ... ) # doctest: +NUMBER
274
+ (array([[0.941, 0.721], [0.8, 0.5]]), array([[0.235, 0.368], [0.4, 0.4]]))
275
+
276
+ """
277
+ if unit_conversion < 1e-16:
278
+ raise ValueError(f'{unit_conversion=} < 1e-16')
279
+ frequency = numpy.array(
280
+ frequency, dtype=numpy.float64, ndmin=1, order='C', copy=None
281
+ )
282
+ if frequency.ndim != 1:
283
+ raise ValueError('frequency is not one-dimensional array')
284
+ lifetime = numpy.array(
285
+ lifetime, dtype=numpy.float64, ndmin=1, order='C', copy=None
286
+ )
287
+ if lifetime.ndim > 2:
288
+ raise ValueError('lifetime must be one- or two-dimensional array')
289
+
290
+ if fraction is None:
291
+ # single-component lifetimes
292
+ if lifetime.ndim > 1:
293
+ raise ValueError(
294
+ 'lifetime must be one-dimensional array if fraction is None'
295
+ )
296
+ lifetime = lifetime.reshape(-1, 1) # move components to last axis
297
+ fraction = numpy.ones_like(lifetime) # not really used
298
+ else:
299
+ fraction = numpy.array(
300
+ fraction, dtype=numpy.float64, ndmin=1, order='C', copy=None
301
+ )
302
+ if fraction.ndim > 2:
303
+ raise ValueError('fraction must be one- or two-dimensional array')
304
+
305
+ if lifetime.ndim == 1 and fraction.ndim == 1:
306
+ # one multi-component lifetime
307
+ if lifetime.shape != fraction.shape:
308
+ raise ValueError(
309
+ f'{lifetime.shape=} does not match {fraction.shape=}'
310
+ )
311
+ lifetime = lifetime.reshape(1, -1)
312
+ fraction = fraction.reshape(1, -1)
313
+ nvar = 1
314
+ elif lifetime.ndim == 2 and fraction.ndim == 2:
315
+ # multiple, multi-component lifetimes
316
+ if lifetime.shape[1] != fraction.shape[1]:
317
+ raise ValueError(f'{lifetime.shape[1]=} != {fraction.shape[1]=}')
318
+ nvar = lifetime.shape[0]
319
+ elif lifetime.ndim == 2 and fraction.ndim == 1:
320
+ # variable components, same fractions
321
+ fraction = fraction.reshape(1, -1)
322
+ nvar = lifetime.shape[0]
323
+ elif lifetime.ndim == 1 and fraction.ndim == 2:
324
+ # same components, varying fractions
325
+ lifetime = lifetime.reshape(1, -1)
326
+ nvar = fraction.shape[0]
327
+ else:
328
+ # unreachable code
329
+ raise RuntimeError(f'{lifetime.shape=}, {fraction.shape=}')
330
+
331
+ phasor = numpy.empty((2, frequency.size, nvar), dtype=numpy.float64)
332
+
333
+ _phasor_from_lifetime(
334
+ phasor, frequency, lifetime, fraction, unit_conversion, preexponential
335
+ )
336
+
337
+ if not keepdims:
338
+ phasor = phasor.squeeze()
339
+ return phasor[0], phasor[1]
340
+
341
+
342
+ def lifetime_to_signal(
343
+ frequency: float,
344
+ lifetime: ArrayLike,
345
+ fraction: ArrayLike | None = None,
346
+ *,
347
+ mean: ArrayLike | None = None,
348
+ background: ArrayLike | None = None,
349
+ samples: int = 64,
350
+ harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
351
+ zero_phase: float | None = None,
352
+ zero_stdev: float | None = None,
353
+ preexponential: bool = False,
354
+ unit_conversion: float = 1e-3,
355
+ ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
356
+ r"""Return synthetic signal from lifetime components.
357
+
358
+ Return synthetic signal, instrument response function (IRF), and
359
+ time axis, sampled over one period of the fundamental frequency.
360
+ The signal is convolved with the IRF, which is approximated by a
361
+ normal distribution.
362
+
363
+ Parameters
364
+ ----------
365
+ frequency : float
366
+ Fundamental laser pulse or modulation frequency in MHz.
367
+ lifetime : array_like
368
+ Lifetime components in ns.
369
+ fraction : array_like, optional
370
+ Fractional intensities or pre-exponential amplitudes of the lifetime
371
+ components. Fractions are normalized to sum to 1.
372
+ Must be specified if `lifetime` is not a scalar.
373
+ mean : array_like, optional, default: 1.0
374
+ Average signal intensity (DC). Must be scalar for now.
375
+ background : array_like, optional, default: 0.0
376
+ Background signal intensity. Must be smaller than `mean`.
377
+ samples : int, default: 64
378
+ Number of signal samples to return. Must be at least 16.
379
+ harmonic : int, sequence of int, or 'all', optional, default: 'all'
380
+ Harmonics used to synthesize signal.
381
+ If `'all'`, all harmonics are used.
382
+ Else, harmonics must be at least one and no larger than half of
383
+ `samples`.
384
+ Use `'all'` to synthesize an exponential time-domain decay signal,
385
+ or `1` to synthesize a homodyne signal.
386
+ zero_phase : float, optional
387
+ Position of instrument response function in radians.
388
+ Must be in range [0, pi]. The default is the 8th sample.
389
+ zero_stdev : float, optional
390
+ Standard deviation of instrument response function in radians.
391
+ Must be at least 1.5 samples and no more than one tenth of samples
392
+ to allow for sufficient sampling of the function.
393
+ The default is 1.5 samples. Increase `samples` to narrow the IRF.
394
+ preexponential : bool, optional, default: False
395
+ If true, `fraction` values are pre-exponential amplitudes,
396
+ else fractional intensities.
397
+ unit_conversion : float, optional, default: 1e-3
398
+ Product of `frequency` and `lifetime` units' prefix factors.
399
+ The default is 1e-3 for MHz and ns, or Hz and ms.
400
+ Use 1.0 for Hz and s.
401
+
402
+ Returns
403
+ -------
404
+ signal : ndarray
405
+ Signal generated from lifetimes at frequency, convolved with
406
+ instrument response function.
407
+ zero : ndarray
408
+ Instrument response function.
409
+ time : ndarray
410
+ Time for each sample in signal in units of `lifetime`.
411
+
412
+ See Also
413
+ --------
414
+ phasorpy.lifetime.phasor_from_lifetime
415
+ phasorpy.phasor.phasor_to_signal
416
+ :ref:`sphx_glr_tutorials_api_phasorpy_lifetime_to_signal.py`
417
+
418
+ Notes
419
+ -----
420
+ This implementation is based on an inverse discrete Fourier transform
421
+ (DFT). Because DFT cannot be used on signals with discontinuities
422
+ (for example, an exponential decay starting at zero) without producing
423
+ strong artifacts (ripples), the signal is convolved with a continuous
424
+ instrument response function (IRF). The minimum width of the IRF is
425
+ limited due to sampling requirements.
426
+
427
+ Examples
428
+ --------
429
+ Synthesize a multi-exponential time-domain decay signal for two
430
+ lifetime components of 4.2 and 0.9 ns at 40 MHz:
431
+
432
+ >>> signal, zero, times = lifetime_to_signal(
433
+ ... 40, [4.2, 0.9], fraction=[0.8, 0.2], samples=16
434
+ ... )
435
+ >>> signal # doctest: +NUMBER
436
+ array([0.2846, 0.1961, 0.1354, ..., 0.8874, 0.6029, 0.4135])
437
+
438
+ Synthesize a homodyne frequency-domain waveform signal for
439
+ a single lifetime:
440
+
441
+ >>> signal, zero, times = lifetime_to_signal(
442
+ ... 40.0, 4.2, samples=16, harmonic=1
443
+ ... )
444
+ >>> signal # doctest: +NUMBER
445
+ array([0.2047, -0.05602, -0.156, ..., 1.471, 1.031, 0.5865])
446
+
447
+ """
448
+ if harmonic is None:
449
+ harmonic = 'all'
450
+ all_harmonics = harmonic == 'all'
451
+ harmonic, _ = parse_harmonic(harmonic, samples // 2)
452
+
453
+ if samples < 16:
454
+ raise ValueError(f'{samples=} < 16')
455
+
456
+ if background is None:
457
+ background = 0.0
458
+ background = numpy.asarray(background)
459
+
460
+ if mean is None:
461
+ mean = 1.0
462
+ mean = numpy.asarray(mean)
463
+ mean -= background
464
+ if numpy.any(mean < 0.0):
465
+ raise ValueError('mean - background must not be less than zero')
466
+
467
+ scale = samples / (2.0 * math.pi)
468
+ if zero_phase is None:
469
+ zero_phase = 8.0 / scale
470
+ phase = zero_phase * scale # in sample units
471
+ if zero_stdev is None:
472
+ zero_stdev = 1.5 / scale
473
+ stdev = zero_stdev * scale # in sample units
474
+
475
+ if zero_phase < 0 or zero_phase > 2.0 * math.pi:
476
+ raise ValueError(f'{zero_phase=} out of range [0, 2 pi]')
477
+ if stdev < 1.5:
478
+ raise ValueError(
479
+ f'{zero_stdev=} < {1.5 / scale} cannot be sampled sufficiently'
480
+ )
481
+ if stdev >= samples / 10:
482
+ raise ValueError(f'{zero_stdev=} > pi / 5 not supported')
483
+
484
+ frequencies = numpy.atleast_1d(frequency)
485
+ if frequencies.size > 1 or frequencies[0] <= 0.0:
486
+ raise ValueError('frequency must be scalar and positive')
487
+ frequencies = numpy.linspace(
488
+ frequency, samples // 2 * frequency, samples // 2
489
+ )
490
+ frequencies = frequencies[[h - 1 for h in harmonic]]
491
+
492
+ real, imag = phasor_from_lifetime(
493
+ frequencies,
494
+ lifetime,
495
+ fraction,
496
+ preexponential=preexponential,
497
+ unit_conversion=unit_conversion,
498
+ )
499
+ real, imag = numpy.atleast_1d(real, imag)
500
+
501
+ zero = numpy.zeros(samples, dtype=numpy.float64)
502
+ _gaussian_signal(zero, phase, stdev)
503
+ zero_mean, zero_real, zero_imag = phasor_from_signal(
504
+ zero, harmonic=harmonic
505
+ )
506
+ if real.ndim > 1:
507
+ # make broadcastable with real and imag
508
+ zero_real = zero_real[:, None]
509
+ zero_imag = zero_imag[:, None]
510
+ if not all_harmonics:
511
+ zero = phasor_to_signal(
512
+ zero_mean, zero_real, zero_imag, samples=samples, harmonic=harmonic
513
+ )
514
+
515
+ phasor_multiply(real, imag, zero_real, zero_imag, out=(real, imag))
516
+
517
+ if len(harmonic) == 1:
518
+ harmonic = harmonic[0]
519
+ signal = phasor_to_signal(
520
+ mean, real, imag, samples=samples, harmonic=harmonic
521
+ )
522
+ signal += numpy.asarray(background)
523
+
524
+ time = numpy.linspace(0, 1.0 / (unit_conversion * frequency), samples)
525
+
526
+ return signal.squeeze(), zero.squeeze(), time
527
+
528
+
529
+ def phasor_calibrate(
530
+ real: ArrayLike,
531
+ imag: ArrayLike,
532
+ reference_mean: ArrayLike,
533
+ reference_real: ArrayLike,
534
+ reference_imag: ArrayLike,
535
+ /,
536
+ frequency: ArrayLike,
537
+ lifetime: ArrayLike,
538
+ *,
539
+ harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
540
+ skip_axis: int | Sequence[int] | None = None,
541
+ fraction: ArrayLike | None = None,
542
+ preexponential: bool = False,
543
+ unit_conversion: float = 1e-3,
544
+ method: Literal['mean', 'median'] = 'mean',
545
+ nan_safe: bool = True,
546
+ reverse: bool = False,
547
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
548
+ """Return calibrated/referenced phasor coordinates.
549
+
550
+ Calibration of phasor coordinates from time-resolved measurements is
551
+ necessary to account for the instrument response function (IRF) and delays
552
+ in the electronics.
553
+
554
+ Parameters
555
+ ----------
556
+ real : array_like
557
+ Real component of phasor coordinates to be calibrated.
558
+ imag : array_like
559
+ Imaginary component of phasor coordinates to be calibrated.
560
+ reference_mean : array_like or None
561
+ Intensity of phasor coordinates from reference of known lifetime.
562
+ Used to re-normalize averaged phasor coordinates.
563
+ reference_real : array_like
564
+ Real component of phasor coordinates from reference of known lifetime.
565
+ Must be measured with the same instrument setting as the phasor
566
+ coordinates to be calibrated. Dimensions must be the same as `real`.
567
+ reference_imag : array_like
568
+ Imaginary component of phasor coordinates from reference of known
569
+ lifetime.
570
+ Must be measured with the same instrument setting as the phasor
571
+ coordinates to be calibrated.
572
+ frequency : array_like
573
+ Fundamental laser pulse or modulation frequency in MHz.
574
+ lifetime : array_like
575
+ Lifetime components in ns. Must be scalar or one-dimensional.
576
+ harmonic : int, sequence of int, or 'all', default: 1
577
+ Harmonics included in `real` and `imag`.
578
+ If an integer, the harmonics at which `real` and `imag` were acquired
579
+ or calculated.
580
+ If a sequence, the harmonics included in the first axis of `real` and
581
+ `imag`.
582
+ If `'all'`, the first axis of `real` and `imag` contains lower
583
+ harmonics.
584
+ The default is the first harmonic (fundamental frequency).
585
+ skip_axis : int or sequence of int, optional
586
+ Axes in `reference_mean` to exclude from reference center calculation.
587
+ By default, all axes except harmonics are included.
588
+ fraction : array_like, optional
589
+ Fractional intensities or pre-exponential amplitudes of the lifetime
590
+ components. Fractions are normalized to sum to 1.
591
+ Must be same size as `lifetime`.
592
+ preexponential : bool, optional
593
+ If true, `fraction` values are pre-exponential amplitudes,
594
+ else fractional intensities (default).
595
+ unit_conversion : float, optional
596
+ Product of `frequency` and `lifetime` units' prefix factors.
597
+ The default is 1e-3 for MHz and ns, or Hz and ms.
598
+ Use 1.0 for Hz and s.
599
+ method : str, optional
600
+ Method used for calculating center of reference phasor coordinates:
601
+
602
+ - ``'mean'``: Arithmetic mean.
603
+ - ``'median'``: Spatial median.
604
+
605
+ nan_safe : bool, optional
606
+ Ensure `method` is applied to same elements of reference arrays.
607
+ By default, distribute NaNs among reference arrays before applying
608
+ `method`.
609
+ reverse : bool, optional
610
+ Reverse calibration.
611
+
612
+ Returns
613
+ -------
614
+ real : ndarray
615
+ Calibrated real component of phasor coordinates.
616
+ imag : ndarray
617
+ Calibrated imaginary component of phasor coordinates.
618
+
619
+ Raises
620
+ ------
621
+ ValueError
622
+ The array shapes of `real` and `imag`, or `reference_real` and
623
+ `reference_imag` do not match.
624
+ Number of harmonics or frequencies does not match the first axis
625
+ of `real` and `imag`.
626
+
627
+ See Also
628
+ --------
629
+ phasorpy.phasor.phasor_transform
630
+ phasorpy.phasor.phasor_center
631
+ phasorpy.lifetime.polar_from_reference_phasor
632
+ phasorpy.lifetime.phasor_from_lifetime
633
+
634
+ Notes
635
+ -----
636
+ This function is a convenience wrapper for the following operations:
637
+
638
+ .. code-block:: python
639
+
640
+ phasor_transform(
641
+ real,
642
+ imag,
643
+ *polar_from_reference_phasor(
644
+ *phasor_center(
645
+ reference_mean,
646
+ reference_real,
647
+ reference_imag,
648
+ skip_axis,
649
+ method,
650
+ nan_safe,
651
+ )[1:],
652
+ *phasor_from_lifetime(
653
+ frequency,
654
+ lifetime,
655
+ fraction,
656
+ preexponential,
657
+ unit_conversion,
658
+ ),
659
+ ),
660
+ )
661
+
662
+ Calibration can be reversed such that
663
+
664
+ .. code-block:: python
665
+
666
+ real, imag == phasor_calibrate(
667
+ *phasor_calibrate(real, imag, *args, **kwargs),
668
+ *args,
669
+ reverse=True,
670
+ **kwargs
671
+ )
672
+
673
+ Examples
674
+ --------
675
+ >>> phasor_calibrate(
676
+ ... [0.1, 0.2, 0.3],
677
+ ... [0.4, 0.5, 0.6],
678
+ ... [1.0, 1.0, 1.0],
679
+ ... [0.2, 0.3, 0.4],
680
+ ... [0.5, 0.6, 0.7],
681
+ ... frequency=80,
682
+ ... lifetime=4,
683
+ ... ) # doctest: +NUMBER
684
+ (array([0.0658, 0.132, 0.198]), array([0.2657, 0.332, 0.399]))
685
+
686
+ Undo the previous calibration:
687
+
688
+ >>> phasor_calibrate(
689
+ ... [0.0658, 0.132, 0.198],
690
+ ... [0.2657, 0.332, 0.399],
691
+ ... [1.0, 1.0, 1.0],
692
+ ... [0.2, 0.3, 0.4],
693
+ ... [0.5, 0.6, 0.7],
694
+ ... frequency=80,
695
+ ... lifetime=4,
696
+ ... reverse=True,
697
+ ... ) # doctest: +NUMBER
698
+ (array([0.1, 0.2, 0.3]), array([0.4, 0.5, 0.6]))
699
+
700
+ """
701
+ real = numpy.asarray(real)
702
+ imag = numpy.asarray(imag)
703
+ reference_mean = numpy.asarray(reference_mean)
704
+ reference_real = numpy.asarray(reference_real)
705
+ reference_imag = numpy.asarray(reference_imag)
706
+
707
+ if real.shape != imag.shape:
708
+ raise ValueError(f'{real.shape=} != {imag.shape=}')
709
+ if reference_real.shape != reference_imag.shape:
710
+ raise ValueError(f'{reference_real.shape=} != {reference_imag.shape=}')
711
+
712
+ has_harmonic_axis = reference_mean.ndim + 1 == reference_real.ndim
713
+ harmonic, _ = parse_harmonic(
714
+ harmonic,
715
+ (
716
+ reference_real.shape[0]
717
+ if has_harmonic_axis
718
+ and isinstance(harmonic, str)
719
+ and harmonic == 'all'
720
+ else None
721
+ ),
722
+ )
723
+
724
+ frequency = numpy.asarray(frequency)
725
+ frequency = frequency * harmonic
726
+
727
+ if has_harmonic_axis:
728
+ if real.ndim == 0:
729
+ raise ValueError(
730
+ f'{real.shape=} != {len(frequency)} frequencies or harmonics'
731
+ )
732
+ if real.shape[0] != len(frequency):
733
+ raise ValueError(
734
+ f'{real.shape[0]=} != {len(frequency)} '
735
+ 'frequencies or harmonics'
736
+ )
737
+ if reference_real.shape[0] != len(frequency):
738
+ raise ValueError(
739
+ f'{reference_real.shape[0]=} != {len(frequency)} '
740
+ 'frequencies or harmonics'
741
+ )
742
+ if reference_mean.shape != reference_real.shape[1:]:
743
+ raise ValueError(
744
+ f'{reference_mean.shape=} != {reference_real.shape[1:]=}'
745
+ )
746
+ elif reference_mean.shape != reference_real.shape:
747
+ raise ValueError(f'{reference_mean.shape=} != {reference_real.shape=}')
748
+ elif len(harmonic) > 1:
749
+ raise ValueError(
750
+ f'{reference_mean.shape=} does not have harmonic axis'
751
+ )
752
+
753
+ _, measured_re, measured_im = phasor_center(
754
+ reference_mean,
755
+ reference_real,
756
+ reference_imag,
757
+ skip_axis=skip_axis,
758
+ method=method,
759
+ nan_safe=nan_safe,
760
+ )
761
+
762
+ known_re, known_im = phasor_from_lifetime(
763
+ frequency,
764
+ lifetime,
765
+ fraction,
766
+ preexponential=preexponential,
767
+ unit_conversion=unit_conversion,
768
+ )
769
+
770
+ skip_axis, axis = parse_skip_axis(
771
+ skip_axis, real.ndim - int(has_harmonic_axis), has_harmonic_axis
772
+ )
773
+
774
+ if has_harmonic_axis and any(skip_axis):
775
+ known_re = numpy.expand_dims(
776
+ known_re, tuple(range(1, measured_re.ndim))
777
+ )
778
+ known_re = numpy.broadcast_to(
779
+ known_re, (len(frequency), *measured_re.shape[1:])
780
+ )
781
+ known_im = numpy.expand_dims(
782
+ known_im, tuple(range(1, measured_im.ndim))
783
+ )
784
+ known_im = numpy.broadcast_to(
785
+ known_im, (len(frequency), *measured_im.shape[1:])
786
+ )
787
+
788
+ phi_zero, mod_zero = polar_from_reference_phasor(
789
+ measured_re, measured_im, known_re, known_im
790
+ )
791
+
792
+ if numpy.ndim(phi_zero) > 0:
793
+ if reverse:
794
+ numpy.negative(phi_zero, out=phi_zero)
795
+ numpy.reciprocal(mod_zero, out=mod_zero)
796
+ if axis is not None:
797
+ phi_zero = numpy.expand_dims(phi_zero, axis=axis)
798
+ mod_zero = numpy.expand_dims(mod_zero, axis=axis)
799
+ elif reverse:
800
+ phi_zero = -phi_zero
801
+ mod_zero = 1.0 / mod_zero
802
+
803
+ return phasor_transform(real, imag, phi_zero, mod_zero)
804
+
805
+
806
+ def polar_from_reference_phasor(
807
+ measured_real: ArrayLike,
808
+ measured_imag: ArrayLike,
809
+ known_real: ArrayLike,
810
+ known_imag: ArrayLike,
811
+ /,
812
+ **kwargs: Any,
813
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
814
+ r"""Return polar coordinates for calibration from reference phasor.
815
+
816
+ Return rotation angle and scale factor for calibrating phasor coordinates
817
+ from measured and known phasor coordinates of a reference, for example,
818
+ a sample of known lifetime.
819
+
820
+ Parameters
821
+ ----------
822
+ measured_real : array_like
823
+ Real component of measured phasor coordinates.
824
+ measured_imag : array_like
825
+ Imaginary component of measured phasor coordinates.
826
+ known_real : array_like
827
+ Real component of reference phasor coordinates.
828
+ known_imag : array_like
829
+ Imaginary component of reference phasor coordinates.
830
+ **kwargs
831
+ Optional `arguments passed to numpy universal functions
832
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
833
+
834
+ Returns
835
+ -------
836
+ phase_zero : ndarray
837
+ Angular component of polar coordinates for calibration in radians.
838
+ modulation_zero : ndarray
839
+ Radial component of polar coordinates for calibration.
840
+
841
+ See Also
842
+ --------
843
+ phasorpy.lifetime.polar_from_reference
844
+
845
+ Notes
846
+ -----
847
+ This function performs the following operations:
848
+
849
+ .. code-block:: python
850
+
851
+ polar_from_reference(
852
+ *phasor_to_polar(measured_real, measured_imag),
853
+ *phasor_to_polar(known_real, known_imag),
854
+ )
855
+
856
+ Examples
857
+ --------
858
+ >>> polar_from_reference_phasor(0.5, 0.0, 1.0, 0.0)
859
+ (0.0, 2.0)
860
+
861
+ """
862
+ return _polar_from_reference_phasor( # type: ignore[no-any-return]
863
+ measured_real, measured_imag, known_real, known_imag, **kwargs
864
+ )
865
+
866
+
867
+ def polar_from_reference(
868
+ measured_phase: ArrayLike,
869
+ measured_modulation: ArrayLike,
870
+ known_phase: ArrayLike,
871
+ known_modulation: ArrayLike,
872
+ /,
873
+ **kwargs: Any,
874
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
875
+ r"""Return polar coordinates for calibration from reference coordinates.
876
+
877
+ Return rotation angle and scale factor for calibrating phasor coordinates
878
+ from measured and known polar coordinates of a reference, for example,
879
+ a sample of known lifetime.
880
+
881
+ Parameters
882
+ ----------
883
+ measured_phase : array_like
884
+ Angular component of measured polar coordinates in radians.
885
+ measured_modulation : array_like
886
+ Radial component of measured polar coordinates.
887
+ known_phase : array_like
888
+ Angular component of reference polar coordinates in radians.
889
+ known_modulation : array_like
890
+ Radial component of reference polar coordinates.
891
+ **kwargs
892
+ Optional `arguments passed to numpy universal functions
893
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
894
+
895
+ Returns
896
+ -------
897
+ phase_zero : ndarray
898
+ Angular component of polar coordinates for calibration in radians.
899
+ modulation_zero : ndarray
900
+ Radial component of polar coordinates for calibration.
901
+
902
+ See Also
903
+ --------
904
+ phasorpy.lifetime.polar_from_reference_phasor
905
+
906
+ Examples
907
+ --------
908
+ >>> polar_from_reference(0.2, 0.4, 0.4, 1.3)
909
+ (0.2, 3.25)
910
+
911
+ """
912
+ return _polar_from_reference( # type: ignore[no-any-return]
913
+ measured_phase,
914
+ measured_modulation,
915
+ known_phase,
916
+ known_modulation,
917
+ **kwargs,
918
+ )
919
+
920
+
921
+ def phasor_to_lifetime_search(
922
+ real: ArrayLike,
923
+ imag: ArrayLike,
924
+ /,
925
+ frequency: float,
926
+ *,
927
+ lifetime_range: tuple[float, float, float] | None = None,
928
+ unit_conversion: float = 1e-3,
929
+ dtype: DTypeLike = None,
930
+ num_threads: int | None = None,
931
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
932
+ """Return two lifetime components from multi-harmonic phasor coordinates.
933
+
934
+ Return estimated lifetimes and fractional intensities of two
935
+ single-exponential components from a set of multi-harmonic
936
+ phasor coordinates using the graphical approach described
937
+ in [1]_.
938
+
939
+ Return NaN for coordinates outside the universal semicircle.
940
+
941
+ Parameters
942
+ ----------
943
+ real : array_like
944
+ Real component of phasor coordinates.
945
+ Must contain at least two linearly increasing harmonics
946
+ in the first dimension.
947
+ imag : array_like
948
+ Imaginary component of phasor coordinates.
949
+ Must have same shape as `real`.
950
+ frequency : float
951
+ Laser pulse or modulation frequency in MHz.
952
+ lifetime_range : tuple of float, optional
953
+ Start, stop, and step of lifetime range in ns to search for components.
954
+ Defines the search range for the first lifetime component.
955
+ The default is ``(0.0, 20.0, 0.1)``.
956
+ unit_conversion : float, optional, default: 1e-3
957
+ Product of `frequency` and `lifetime` units' prefix factors.
958
+ The default is 1e-3 for MHz and ns, or Hz and ms.
959
+ Use 1.0 for Hz and s.
960
+ dtype : dtype_like, optional
961
+ Floating point data type used for calculation and output values.
962
+ Either `float32` or `float64`. The default is `float64`.
963
+ num_threads : int, optional
964
+ Number of OpenMP threads to use for parallelization.
965
+ By default, multi-threading is disabled.
966
+ If zero, up to half of logical CPUs are used.
967
+ OpenMP may not be available on all platforms.
968
+
969
+ Returns
970
+ -------
971
+ lifetime : ndarray
972
+ Lifetime components, shaped ``(2, *real.shape[1:])``.
973
+ fraction : ndarray
974
+ Fractional intensities of resolved lifetime components.
975
+
976
+ Raises
977
+ ------
978
+ ValueError
979
+ The shapes of real and imaginary coordinates do not match.
980
+ The number of harmonics is less than the number of components.
981
+ The lifetime range is invalid.
982
+
983
+ References
984
+ ----------
985
+ .. [1] Vallmitjana A, Torrado B, Dvornikov A, Ranjit S, and Gratton E.
986
+ `Blind resolution of lifetime components in individual pixels of
987
+ fluorescence lifetime images using the phasor approach
988
+ <https://doi.org/10.1021/acs.jpcb.0c06946>`_.
989
+ *J Phys Chem B*, 124(45): 10126-10137 (2020)
990
+
991
+ Notes
992
+ -----
993
+ This function currently supports only two lifetime components.
994
+
995
+ Examples
996
+ --------
997
+ Resolve two lifetime components from the phasor coordinates of a mixture
998
+ of 4.2 and 0.9 ns lifetimes with 70/30% fractions at 80 and 160 MHz:
999
+
1000
+ >>> phasor_to_lifetime_search(
1001
+ ... [0.3773104, 0.20213886], [0.3834715, 0.30623315], frequency=80.0
1002
+ ... )
1003
+ (array([0.9, 4.2]), array([0.3, 0.7]))
1004
+
1005
+ """
1006
+ num_components = 2
1007
+
1008
+ if lifetime_range is None:
1009
+ lifetime_range = (0.0, 20.0, 0.1)
1010
+ elif (
1011
+ lifetime_range[0] < 0.0
1012
+ or lifetime_range[1] <= lifetime_range[0]
1013
+ or lifetime_range[2] <= 0.0
1014
+ or lifetime_range[2] >= lifetime_range[1] - lifetime_range[0]
1015
+ ):
1016
+ raise ValueError(f'invalid {lifetime_range=}')
1017
+
1018
+ num_threads = number_threads(num_threads)
1019
+
1020
+ dtype = numpy.dtype(dtype)
1021
+ if dtype.char not in {'f', 'd'}:
1022
+ raise ValueError(f'{dtype=} is not a floating point type')
1023
+
1024
+ real = numpy.ascontiguousarray(real, dtype=dtype)
1025
+ imag = numpy.ascontiguousarray(imag, dtype=dtype)
1026
+
1027
+ if real.shape != imag.shape:
1028
+ raise ValueError(f'{real.shape=} != {imag.shape=}')
1029
+ if real.shape[0] < num_components:
1030
+ raise ValueError(f'{real.shape[0]=} < {num_components=}')
1031
+
1032
+ shape = real.shape[1:]
1033
+ real = real[:num_components].reshape((num_components, -1))
1034
+ imag = imag[:num_components].reshape((num_components, -1))
1035
+ size = real.shape[-1]
1036
+
1037
+ lifetime = numpy.zeros((num_components, size), dtype=dtype)
1038
+ fraction = numpy.zeros((num_components, size), dtype=dtype)
1039
+
1040
+ candidate = phasor_from_lifetime(
1041
+ frequency,
1042
+ numpy.arange(*lifetime_range, dtype=dtype),
1043
+ unit_conversion=unit_conversion,
1044
+ )[0]
1045
+
1046
+ omega = frequency * math.pi * 2.0 * unit_conversion
1047
+ omega *= omega
1048
+
1049
+ _lifetime_search_2(
1050
+ lifetime, fraction, real, imag, candidate, omega, num_threads
1051
+ )
1052
+
1053
+ lifetime = lifetime.reshape(num_components, *shape)
1054
+ fraction = fraction.reshape(num_components, *shape)
1055
+
1056
+ return lifetime, fraction
1057
+
1058
+
1059
+ def phasor_to_apparent_lifetime(
1060
+ real: ArrayLike,
1061
+ imag: ArrayLike,
1062
+ /,
1063
+ frequency: ArrayLike,
1064
+ *,
1065
+ unit_conversion: float = 1e-3,
1066
+ **kwargs: Any,
1067
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
1068
+ r"""Return apparent single lifetimes from phasor coordinates.
1069
+
1070
+ Parameters
1071
+ ----------
1072
+ real : array_like
1073
+ Real component of phasor coordinates.
1074
+ imag : array_like
1075
+ Imaginary component of phasor coordinates.
1076
+ frequency : array_like
1077
+ Laser pulse or modulation frequency in MHz.
1078
+ unit_conversion : float, optional
1079
+ Product of `frequency` and returned `lifetime` units' prefix factors.
1080
+ The default is 1e-3 for MHz and ns, or Hz and ms.
1081
+ Use 1.0 for Hz and s.
1082
+ **kwargs
1083
+ Optional `arguments passed to numpy universal functions
1084
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1085
+
1086
+ Returns
1087
+ -------
1088
+ phase_lifetime : ndarray
1089
+ Apparent single lifetime from angular component of phasor coordinates.
1090
+ modulation_lifetime : ndarray
1091
+ Apparent single lifetime from radial component of phasor coordinates.
1092
+
1093
+ See Also
1094
+ --------
1095
+ phasorpy.lifetime.phasor_from_apparent_lifetime
1096
+ :ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
1097
+
1098
+ Notes
1099
+ -----
1100
+ The phasor coordinates `real` (:math:`G`) and `imag` (:math:`S`)
1101
+ are converted to apparent single lifetimes
1102
+ `phase_lifetime` (:math:`\tau_{\phi}`) and
1103
+ `modulation_lifetime` (:math:`\tau_{M}`) at frequency :math:`f`
1104
+ according to:
1105
+
1106
+ .. math::
1107
+
1108
+ \omega &= 2 \pi f
1109
+
1110
+ \tau_{\phi} &= \omega^{-1} \cdot S / G
1111
+
1112
+ \tau_{M} &= \omega^{-1} \cdot \sqrt{1 / (S^2 + G^2) - 1}
1113
+
1114
+ Examples
1115
+ --------
1116
+ The apparent single lifetimes from phase and modulation are equal
1117
+ only if the phasor coordinates lie on the universal semicircle:
1118
+
1119
+ >>> phasor_to_apparent_lifetime(
1120
+ ... 0.5, [0.5, 0.45], frequency=80
1121
+ ... ) # doctest: +NUMBER
1122
+ (array([1.989, 1.79]), array([1.989, 2.188]))
1123
+
1124
+ Apparent single lifetimes of phasor coordinates outside the universal
1125
+ semicircle are undefined:
1126
+
1127
+ >>> phasor_to_apparent_lifetime(-0.1, 1.1, 80) # doctest: +NUMBER
1128
+ (-21.8, 0.0)
1129
+
1130
+ Apparent single lifetimes at the universal semicircle endpoints are
1131
+ infinite and zero:
1132
+
1133
+ >>> phasor_to_apparent_lifetime([0, 1], [0, 0], 80) # doctest: +NUMBER
1134
+ (array([inf, 0]), array([inf, 0]))
1135
+
1136
+ """
1137
+ omega = numpy.asarray(frequency, dtype=numpy.float64, copy=True)
1138
+ omega *= math.pi * 2.0 * unit_conversion
1139
+ return _phasor_to_apparent_lifetime( # type: ignore[no-any-return]
1140
+ real, imag, omega, **kwargs
1141
+ )
1142
+
1143
+
1144
+ def phasor_from_apparent_lifetime(
1145
+ phase_lifetime: ArrayLike,
1146
+ modulation_lifetime: ArrayLike | None,
1147
+ /,
1148
+ frequency: ArrayLike,
1149
+ *,
1150
+ unit_conversion: float = 1e-3,
1151
+ **kwargs: Any,
1152
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
1153
+ r"""Return phasor coordinates from apparent single lifetimes.
1154
+
1155
+ Parameters
1156
+ ----------
1157
+ phase_lifetime : ndarray
1158
+ Apparent single lifetime from phase.
1159
+ modulation_lifetime : ndarray, optional
1160
+ Apparent single lifetime from modulation.
1161
+ If None, `modulation_lifetime` is same as `phase_lifetime`.
1162
+ frequency : array_like
1163
+ Laser pulse or modulation frequency in MHz.
1164
+ unit_conversion : float, optional
1165
+ Product of `frequency` and `lifetime` units' prefix factors.
1166
+ The default is 1e-3 for MHz and ns, or Hz and ms.
1167
+ Use 1.0 for Hz and s.
1168
+ **kwargs
1169
+ Optional `arguments passed to numpy universal functions
1170
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1171
+
1172
+ Returns
1173
+ -------
1174
+ real : ndarray
1175
+ Real component of phasor coordinates.
1176
+ imag : ndarray
1177
+ Imaginary component of phasor coordinates.
1178
+
1179
+ See Also
1180
+ --------
1181
+ phasorpy.lifetime.phasor_to_apparent_lifetime
1182
+
1183
+ Notes
1184
+ -----
1185
+ The apparent single lifetimes `phase_lifetime` (:math:`\tau_{\phi}`)
1186
+ and `modulation_lifetime` (:math:`\tau_{M}`) are converted to phasor
1187
+ coordinates `real` (:math:`G`) and `imag` (:math:`S`) at
1188
+ frequency :math:`f` according to:
1189
+
1190
+ .. math::
1191
+
1192
+ \omega &= 2 \pi f
1193
+
1194
+ \phi & = \arctan(\omega \tau_{\phi})
1195
+
1196
+ M &= 1 / \sqrt{1 + (\omega \tau_{M})^2}
1197
+
1198
+ G &= M \cdot \cos{\phi}
1199
+
1200
+ S &= M \cdot \sin{\phi}
1201
+
1202
+ Examples
1203
+ --------
1204
+ If the apparent single lifetimes from phase and modulation are equal,
1205
+ the phasor coordinates lie on the universal semicircle, else inside:
1206
+
1207
+ >>> phasor_from_apparent_lifetime(
1208
+ ... 1.9894, [1.9894, 2.4113], frequency=80.0
1209
+ ... ) # doctest: +NUMBER
1210
+ (array([0.5, 0.45]), array([0.5, 0.45]))
1211
+
1212
+ Zero and infinite apparent single lifetimes define the endpoints of the
1213
+ universal semicircle:
1214
+
1215
+ >>> phasor_from_apparent_lifetime(
1216
+ ... [0.0, 1e9], [0.0, 1e9], frequency=80
1217
+ ... ) # doctest: +NUMBER
1218
+ (array([1, 0.0]), array([0, 0.0]))
1219
+
1220
+ """
1221
+ omega = numpy.asarray(frequency, dtype=numpy.float64, copy=True)
1222
+ omega *= math.pi * 2.0 * unit_conversion
1223
+ if modulation_lifetime is None:
1224
+ return _phasor_from_single_lifetime( # type: ignore[no-any-return]
1225
+ phase_lifetime, omega, **kwargs
1226
+ )
1227
+ return _phasor_from_apparent_lifetime( # type: ignore[no-any-return]
1228
+ phase_lifetime, modulation_lifetime, omega, **kwargs
1229
+ )
1230
+
1231
+
1232
+ def phasor_to_normal_lifetime(
1233
+ real: ArrayLike,
1234
+ imag: ArrayLike,
1235
+ /,
1236
+ frequency: ArrayLike,
1237
+ *,
1238
+ unit_conversion: float = 1e-3,
1239
+ **kwargs: Any,
1240
+ ) -> NDArray[Any]:
1241
+ r"""Return normal lifetimes from phasor coordinates.
1242
+
1243
+ The normal lifetime of phasor coordinates represents the single lifetime
1244
+ equivalent corresponding to the perpendicular projection of the coordinates
1245
+ onto the universal semicircle, as defined in [2]_.
1246
+
1247
+ Parameters
1248
+ ----------
1249
+ real : array_like
1250
+ Real component of phasor coordinates.
1251
+ imag : array_like
1252
+ Imaginary component of phasor coordinates.
1253
+ frequency : array_like
1254
+ Laser pulse or modulation frequency in MHz.
1255
+ unit_conversion : float, optional
1256
+ Product of `frequency` and returned `lifetime` units' prefix factors.
1257
+ The default is 1e-3 for MHz and ns, or Hz and ms.
1258
+ Use 1.0 for Hz and s.
1259
+ **kwargs
1260
+ Optional `arguments passed to numpy universal functions
1261
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1262
+
1263
+ Returns
1264
+ -------
1265
+ normal_lifetime : ndarray
1266
+ Normal lifetime of phasor coordinates.
1267
+
1268
+ See Also
1269
+ --------
1270
+ :ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
1271
+
1272
+ Notes
1273
+ -----
1274
+ The phasor coordinates `real` (:math:`G`) and `imag` (:math:`S`)
1275
+ are converted to normal lifetimes `normal_lifetime` (:math:`\tau_{N}`)
1276
+ at frequency :math:`f` according to:
1277
+
1278
+ .. math::
1279
+
1280
+ \omega &= 2 \pi f
1281
+
1282
+ G_{N} &= 0.5 \cdot (1 + \cos{\arctan{\frac{S}{G - 0.5}}})
1283
+
1284
+ \tau_{N} &= \sqrt{\frac{1 - G_{N}}{\omega^{2} \cdot G_{N}}}
1285
+
1286
+ References
1287
+ ----------
1288
+ .. [2] Silberberg M, and Grecco H.
1289
+ `pawFLIM: reducing bias and uncertainty to enable lower photon
1290
+ count in FLIM experiments
1291
+ <https://doi.org/10.1088/2050-6120/aa72ab>`_.
1292
+ *Methods Appl Fluoresc*, 5(2): 024016 (2017)
1293
+
1294
+ Examples
1295
+ --------
1296
+ The normal lifetimes of phasor coordinates with a real component of 0.5
1297
+ are independent of the imaginary component:
1298
+
1299
+ >>> phasor_to_normal_lifetime(
1300
+ ... 0.5, [0.5, 0.45], frequency=80
1301
+ ... ) # doctest: +NUMBER
1302
+ array([1.989, 1.989])
1303
+
1304
+ """
1305
+ omega = numpy.asarray(frequency, dtype=numpy.float64, copy=True)
1306
+ omega *= math.pi * 2.0 * unit_conversion
1307
+ return _phasor_to_normal_lifetime( # type: ignore[no-any-return]
1308
+ real, imag, omega, **kwargs
1309
+ )
1310
+
1311
+
1312
+ def lifetime_to_frequency(
1313
+ lifetime: ArrayLike,
1314
+ *,
1315
+ unit_conversion: float = 1e-3,
1316
+ ) -> NDArray[numpy.float64]:
1317
+ r"""Return optimal frequency for resolving single component lifetime.
1318
+
1319
+ Parameters
1320
+ ----------
1321
+ lifetime : array_like
1322
+ Single component lifetime.
1323
+ unit_conversion : float, optional, default: 1e-3
1324
+ Product of `frequency` and `lifetime` units' prefix factors.
1325
+ The default is 1e-3 for MHz and ns, or Hz and ms.
1326
+ Use 1.0 for Hz and s.
1327
+
1328
+ Returns
1329
+ -------
1330
+ frequency : ndarray
1331
+ Optimal laser pulse or modulation frequency for resolving `lifetime`.
1332
+
1333
+ Notes
1334
+ -----
1335
+ The optimal frequency :math:`f` to resolve a single component lifetime
1336
+ :math:`\tau` is
1337
+ (:ref:`Redford & Clegg 2005 <redford-clegg-2005>`. Eq. B.6):
1338
+
1339
+ .. math::
1340
+
1341
+ \omega &= 2 \pi f
1342
+
1343
+ \omega^2 &= \frac{1 + \sqrt{3}}{2 \tau^2}
1344
+
1345
+ Examples
1346
+ --------
1347
+ Measurements of a lifetime near 4 ns should be made at 47 MHz,
1348
+ near 1 ns at 186 MHz:
1349
+
1350
+ >>> lifetime_to_frequency([4.0, 1.0]) # doctest: +NUMBER
1351
+ array([46.5, 186])
1352
+
1353
+ """
1354
+ t = numpy.reciprocal(lifetime, dtype=numpy.float64)
1355
+ t *= 0.18601566519848653 / unit_conversion
1356
+ return t
1357
+
1358
+
1359
+ def lifetime_from_frequency(
1360
+ frequency: ArrayLike,
1361
+ *,
1362
+ unit_conversion: float = 1e-3,
1363
+ ) -> NDArray[numpy.float64]:
1364
+ r"""Return single component lifetime best resolved at frequency.
1365
+
1366
+ Parameters
1367
+ ----------
1368
+ frequency : array_like
1369
+ Laser pulse or modulation frequency.
1370
+ unit_conversion : float, optional, default: 1e-3
1371
+ Product of `frequency` and `lifetime` units' prefix factors.
1372
+ The default is 1e-3 for MHz and ns, or Hz and ms.
1373
+ Use 1.0 for Hz and s.
1374
+
1375
+ Returns
1376
+ -------
1377
+ lifetime : ndarray
1378
+ Single component lifetime best resolved at `frequency`.
1379
+
1380
+ Notes
1381
+ -----
1382
+ The lifetime :math:`\tau` that is best resolved at frequency :math:`f` is
1383
+ (:ref:`Redford & Clegg 2005 <redford-clegg-2005>`. Eq. B.6):
1384
+
1385
+ .. math::
1386
+
1387
+ \omega &= 2 \pi f
1388
+
1389
+ \tau^2 &= \frac{1 + \sqrt{3}}{2 \omega^2}
1390
+
1391
+ Examples
1392
+ --------
1393
+ Measurements at frequencies of 47 and 186 MHz are best for measuring
1394
+ lifetimes near 4 and 1 ns respectively:
1395
+
1396
+ >>> lifetime_from_frequency([46.5, 186]) # doctest: +NUMBER
1397
+ array([4, 1])
1398
+
1399
+ """
1400
+ t = numpy.reciprocal(frequency, dtype=numpy.float64)
1401
+ t *= 0.18601566519848653 / unit_conversion
1402
+ return t
1403
+
1404
+
1405
+ def lifetime_fraction_to_amplitude(
1406
+ lifetime: ArrayLike, fraction: ArrayLike, *, axis: int = -1
1407
+ ) -> NDArray[numpy.float64]:
1408
+ r"""Return pre-exponential amplitude from fractional intensity.
1409
+
1410
+ Parameters
1411
+ ----------
1412
+ lifetime : array_like
1413
+ Lifetime components.
1414
+ fraction : array_like
1415
+ Fractional intensities of lifetime components.
1416
+ Fractions are normalized to sum to 1.
1417
+ axis : int, optional
1418
+ Axis over which to compute pre-exponential amplitudes.
1419
+ The default is the last axis (-1).
1420
+
1421
+ Returns
1422
+ -------
1423
+ amplitude : ndarray
1424
+ Pre-exponential amplitudes.
1425
+ The product of `amplitude` and `lifetime` sums to 1 along `axis`.
1426
+
1427
+ See Also
1428
+ --------
1429
+ phasorpy.lifetime.lifetime_fraction_from_amplitude
1430
+
1431
+ Notes
1432
+ -----
1433
+ The pre-exponential amplitude :math:`a` of component :math:`j` with
1434
+ lifetime :math:`\tau` and fractional intensity :math:`\alpha` is:
1435
+
1436
+ .. math::
1437
+
1438
+ a_{j} = \frac{\alpha_{j}}{\tau_{j} \cdot \sum_{j} \alpha_{j}}
1439
+
1440
+ Examples
1441
+ --------
1442
+ >>> lifetime_fraction_to_amplitude(
1443
+ ... [4.0, 1.0], [1.6, 0.4]
1444
+ ... ) # doctest: +NUMBER
1445
+ array([0.2, 0.2])
1446
+
1447
+ """
1448
+ t = numpy.asarray(fraction, dtype=numpy.float64, copy=True)
1449
+ t /= numpy.sum(t, axis=axis, keepdims=True)
1450
+ numpy.true_divide(t, lifetime, out=t)
1451
+ return t
1452
+
1453
+
1454
+ def lifetime_fraction_from_amplitude(
1455
+ lifetime: ArrayLike, amplitude: ArrayLike, *, axis: int = -1
1456
+ ) -> NDArray[numpy.float64]:
1457
+ r"""Return fractional intensity from pre-exponential amplitude.
1458
+
1459
+ Parameters
1460
+ ----------
1461
+ lifetime : array_like
1462
+ Lifetime of components.
1463
+ amplitude : array_like
1464
+ Pre-exponential amplitudes of lifetime components.
1465
+ axis : int, optional
1466
+ Axis over which to compute fractional intensities.
1467
+ The default is the last axis (-1).
1468
+
1469
+ Returns
1470
+ -------
1471
+ fraction : ndarray
1472
+ Fractional intensities, normalized to sum to 1 along `axis`.
1473
+
1474
+ See Also
1475
+ --------
1476
+ phasorpy.lifetime.lifetime_fraction_to_amplitude
1477
+
1478
+ Notes
1479
+ -----
1480
+ The fractional intensity :math:`\alpha` of component :math:`j` with
1481
+ lifetime :math:`\tau` and pre-exponential amplitude :math:`a` is:
1482
+
1483
+ .. math::
1484
+
1485
+ \alpha_{j} = \frac{a_{j} \tau_{j}}{\sum_{j} a_{j} \tau_{j}}
1486
+
1487
+ Examples
1488
+ --------
1489
+ >>> lifetime_fraction_from_amplitude(
1490
+ ... [4.0, 1.0], [1.0, 1.0]
1491
+ ... ) # doctest: +NUMBER
1492
+ array([0.8, 0.2])
1493
+
1494
+ """
1495
+ t: NDArray[numpy.float64]
1496
+ t = numpy.multiply(amplitude, lifetime, dtype=numpy.float64)
1497
+ t /= numpy.sum(t, axis=axis, keepdims=True)
1498
+ return t
1499
+
1500
+
1501
+ def phasor_semicircle(
1502
+ samples: int = 101, /
1503
+ ) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
1504
+ r"""Return equally spaced phasor coordinates on universal semicircle.
1505
+
1506
+ Parameters
1507
+ ----------
1508
+ samples : int, optional, default: 101
1509
+ Number of coordinates to return.
1510
+
1511
+ Returns
1512
+ -------
1513
+ real : ndarray
1514
+ Real component of phasor coordinates on universal semicircle.
1515
+ imag : ndarray
1516
+ Imaginary component of phasor coordinates on universal semicircle.
1517
+
1518
+ Raises
1519
+ ------
1520
+ ValueError
1521
+ The number of `samples` is smaller than 1.
1522
+
1523
+ Notes
1524
+ -----
1525
+ If more than one sample, the first and last phasor coordinates returned
1526
+ are ``(0, 0)`` and ``(1, 0)``.
1527
+ The center coordinate, if any, is ``(0.5, 0.5)``.
1528
+
1529
+ The universal semicircle is composed of the phasor coordinates of
1530
+ single lifetime components, where the relation of polar coordinates
1531
+ (phase :math:`\phi` and modulation :math:`M`) is:
1532
+
1533
+ .. math::
1534
+
1535
+ M = \cos{\phi}
1536
+
1537
+ Examples
1538
+ --------
1539
+ Calculate three phasor coordinates on universal semicircle:
1540
+
1541
+ >>> phasor_semicircle(3) # doctest: +NUMBER
1542
+ (array([0, 0.5, 1]), array([0.0, 0.5, 0]))
1543
+
1544
+ """
1545
+ if samples < 1:
1546
+ raise ValueError(f'{samples=} < 1')
1547
+ arange = numpy.linspace(math.pi, 0.0, samples)
1548
+ real = numpy.cos(arange)
1549
+ real += 1.0
1550
+ real *= 0.5
1551
+ imag = numpy.sin(arange)
1552
+ imag *= 0.5
1553
+ return real, imag
1554
+
1555
+
1556
+ def phasor_semicircle_intersect(
1557
+ real0: ArrayLike,
1558
+ imag0: ArrayLike,
1559
+ real1: ArrayLike,
1560
+ imag1: ArrayLike,
1561
+ /,
1562
+ **kwargs: Any,
1563
+ ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], NDArray[Any]]:
1564
+ """Return intersection of line through phasors with universal semicircle.
1565
+
1566
+ Return the phasor coordinates of the two intersections of the universal
1567
+ semicircle with the line between two phasor coordinates.
1568
+ Return NaN if the line does not intersect the semicircle.
1569
+
1570
+ Parameters
1571
+ ----------
1572
+ real0 : array_like
1573
+ Real component of first set of phasor coordinates.
1574
+ imag0 : array_like
1575
+ Imaginary component of first set of phasor coordinates.
1576
+ real1 : array_like
1577
+ Real component of second set of phasor coordinates.
1578
+ imag1 : array_like
1579
+ Imaginary component of second set of phasor 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
+ real0 : ndarray
1587
+ Real component of first intersect of phasors with semicircle.
1588
+ imag0 : ndarray
1589
+ Imaginary component of first intersect of phasors with semicircle.
1590
+ real1 : ndarray
1591
+ Real component of second intersect of phasors with semicircle.
1592
+ imag1 : ndarray
1593
+ Imaginary component of second intersect of phasors with semicircle.
1594
+
1595
+ Examples
1596
+ --------
1597
+ Calculate two intersects of a line through two phasor coordinates
1598
+ with the universal semicircle:
1599
+
1600
+ >>> phasor_semicircle_intersect(0.2, 0.25, 0.6, 0.25) # doctest: +NUMBER
1601
+ (0.066, 0.25, 0.933, 0.25)
1602
+
1603
+ The line between two phasor coordinates may not intersect the semicircle
1604
+ at two points:
1605
+
1606
+ >>> phasor_semicircle_intersect(0.2, 0.0, 0.6, 0.25) # doctest: +NUMBER
1607
+ (nan, nan, 0.817, 0.386)
1608
+
1609
+ """
1610
+ return _intersect_semicircle_line( # type: ignore[no-any-return]
1611
+ real0, imag0, real1, imag1, **kwargs
1612
+ )
1613
+
1614
+
1615
+ def phasor_at_harmonic(
1616
+ real: ArrayLike,
1617
+ harmonic: ArrayLike,
1618
+ other_harmonic: ArrayLike,
1619
+ /,
1620
+ **kwargs: Any,
1621
+ ) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
1622
+ r"""Return phasor coordinates on universal semicircle at other harmonics.
1623
+
1624
+ Return phasor coordinates at any harmonic from the real component of
1625
+ phasor coordinates of a single exponential lifetime at a certain harmonic.
1626
+ The input and output phasor coordinates lie on the universal semicircle.
1627
+
1628
+ Parameters
1629
+ ----------
1630
+ real : array_like
1631
+ Real component of phasor coordinates of single exponential lifetime
1632
+ at `harmonic`.
1633
+ harmonic : array_like
1634
+ Harmonic of `real` coordinate. Must be integer >= 1.
1635
+ other_harmonic : array_like
1636
+ Harmonic for which to return phasor coordinates. Must be integer >= 1.
1637
+ **kwargs
1638
+ Optional `arguments passed to numpy universal functions
1639
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1640
+
1641
+ Returns
1642
+ -------
1643
+ real_other : ndarray
1644
+ Real component of phasor coordinates at `other_harmonic`.
1645
+ imag_other : ndarray
1646
+ Imaginary component of phasor coordinates at `other_harmonic`.
1647
+
1648
+ Notes
1649
+ -----
1650
+ The phasor coordinates
1651
+ :math:`g_{n}` (`real_other`) and :math:`s_{n}` (`imag_other`)
1652
+ of a single exponential lifetime at harmonic :math:`n` (`other_harmonic`)
1653
+ is calculated from the real part of the phasor coordinates
1654
+ :math:`g_{m}` (`real`) at harmonic :math:`m` (`harmonic`) according to
1655
+ (:ref:`Torrado, Malacrida, & Ranjit. 2022 <torrado-2022>`. Eq. 25):
1656
+
1657
+ .. math::
1658
+
1659
+ g_{n} &= \frac{m^2 \cdot g_{m}}{n^2 + (m^2-n^2) \cdot g_{m}}
1660
+
1661
+ s_{n} &= \sqrt{G_{n} - g_{n}^2}
1662
+
1663
+ This function is equivalent to the following operations:
1664
+
1665
+ .. code-block:: python
1666
+
1667
+ phasor_from_lifetime(
1668
+ frequency=other_harmonic,
1669
+ lifetime=phasor_to_apparent_lifetime(
1670
+ real, sqrt(real - real * real), frequency=harmonic
1671
+ )[0],
1672
+ )
1673
+
1674
+ Examples
1675
+ --------
1676
+ The phasor coordinates at higher harmonics are approaching the origin:
1677
+
1678
+ >>> phasor_at_harmonic(0.5, 1, [1, 2, 4, 8]) # doctest: +NUMBER
1679
+ (array([0.5, 0.2, 0.05882, 0.01538]), array([0.5, 0.4, 0.2353, 0.1231]))
1680
+
1681
+ """
1682
+ harmonic = numpy.asarray(harmonic, dtype=numpy.int32)
1683
+ if numpy.any(harmonic < 1):
1684
+ raise ValueError('invalid harmonic')
1685
+
1686
+ other_harmonic = numpy.asarray(other_harmonic, dtype=numpy.int32)
1687
+ if numpy.any(other_harmonic < 1):
1688
+ raise ValueError('invalid other_harmonic')
1689
+
1690
+ return _phasor_at_harmonic( # type: ignore[no-any-return]
1691
+ real, harmonic, other_harmonic, **kwargs
1692
+ )
1693
+
1694
+
1695
+ def polar_to_apparent_lifetime(
1696
+ phase: ArrayLike,
1697
+ modulation: ArrayLike,
1698
+ /,
1699
+ frequency: ArrayLike,
1700
+ *,
1701
+ unit_conversion: float = 1e-3,
1702
+ **kwargs: Any,
1703
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
1704
+ r"""Return apparent single lifetimes from polar coordinates.
1705
+
1706
+ Parameters
1707
+ ----------
1708
+ phase : array_like
1709
+ Angular component of polar coordinates.
1710
+ modulation : array_like
1711
+ Radial component of polar coordinates.
1712
+ frequency : array_like
1713
+ Laser pulse or modulation frequency in MHz.
1714
+ unit_conversion : float, optional
1715
+ Product of `frequency` and returned `lifetime` units' prefix factors.
1716
+ The default is 1e-3 for MHz and ns, or Hz and ms.
1717
+ Use 1.0 for Hz and s.
1718
+ **kwargs
1719
+ Optional `arguments passed to numpy universal functions
1720
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1721
+
1722
+ Returns
1723
+ -------
1724
+ phase_lifetime : ndarray
1725
+ Apparent single lifetime from `phase`.
1726
+ modulation_lifetime : ndarray
1727
+ Apparent single lifetime from `modulation`.
1728
+
1729
+ See Also
1730
+ --------
1731
+ phasorpy.lifetime.polar_from_apparent_lifetime
1732
+
1733
+ Notes
1734
+ -----
1735
+ The polar coordinates `phase` (:math:`\phi`) and `modulation` (:math:`M`)
1736
+ are converted to apparent single lifetimes
1737
+ `phase_lifetime` (:math:`\tau_{\phi}`) and
1738
+ `modulation_lifetime` (:math:`\tau_{M}`) at frequency :math:`f`
1739
+ according to:
1740
+
1741
+ .. math::
1742
+
1743
+ \omega &= 2 \pi f
1744
+
1745
+ \tau_{\phi} &= \omega^{-1} \cdot \tan{\phi}
1746
+
1747
+ \tau_{M} &= \omega^{-1} \cdot \sqrt{1 / M^2 - 1}
1748
+
1749
+ Examples
1750
+ --------
1751
+ The apparent single lifetimes from phase and modulation are equal
1752
+ only if the polar coordinates lie on the universal semicircle:
1753
+
1754
+ >>> polar_to_apparent_lifetime(
1755
+ ... math.pi / 4, numpy.hypot([0.5, 0.45], [0.5, 0.45]), frequency=80
1756
+ ... ) # doctest: +NUMBER
1757
+ (array([1.989, 1.989]), array([1.989, 2.411]))
1758
+
1759
+ """
1760
+ omega = numpy.asarray(frequency, dtype=numpy.float64, copy=True)
1761
+ omega *= math.pi * 2.0 * unit_conversion
1762
+ return _polar_to_apparent_lifetime( # type: ignore[no-any-return]
1763
+ phase, modulation, omega, **kwargs
1764
+ )
1765
+
1766
+
1767
+ def polar_from_apparent_lifetime(
1768
+ phase_lifetime: ArrayLike,
1769
+ modulation_lifetime: ArrayLike | None,
1770
+ /,
1771
+ frequency: ArrayLike,
1772
+ *,
1773
+ unit_conversion: float = 1e-3,
1774
+ **kwargs: Any,
1775
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
1776
+ r"""Return polar coordinates from apparent single lifetimes.
1777
+
1778
+ Parameters
1779
+ ----------
1780
+ phase_lifetime : ndarray
1781
+ Apparent single lifetime from phase.
1782
+ modulation_lifetime : ndarray, optional
1783
+ Apparent single lifetime from modulation.
1784
+ If None, `modulation_lifetime` is same as `phase_lifetime`.
1785
+ frequency : array_like
1786
+ Laser pulse or modulation frequency in MHz.
1787
+ unit_conversion : float, optional
1788
+ Product of `frequency` and `lifetime` units' prefix factors.
1789
+ The default is 1e-3 for MHz and ns, or Hz and ms.
1790
+ Use 1.0 for Hz and s.
1791
+ **kwargs
1792
+ Optional `arguments passed to numpy universal functions
1793
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1794
+
1795
+ Returns
1796
+ -------
1797
+ phase : ndarray
1798
+ Angular component of polar coordinates.
1799
+ modulation : ndarray
1800
+ Radial component of polar coordinates.
1801
+
1802
+ See Also
1803
+ --------
1804
+ phasorpy.lifetime.polar_to_apparent_lifetime
1805
+
1806
+ Notes
1807
+ -----
1808
+ The apparent single lifetimes `phase_lifetime` (:math:`\tau_{\phi}`)
1809
+ and `modulation_lifetime` (:math:`\tau_{M}`) are converted to polar
1810
+ coordinates `phase` (:math:`\phi`) and `modulation` (:math:`M`) at
1811
+ frequency :math:`f` according to:
1812
+
1813
+ .. math::
1814
+
1815
+ \omega &= 2 \pi f
1816
+
1817
+ \phi & = \arctan(\omega \tau_{\phi})
1818
+
1819
+ M &= 1 / \sqrt{1 + (\omega \tau_{M})^2}
1820
+
1821
+ Examples
1822
+ --------
1823
+ If the apparent single lifetimes from phase and modulation are equal,
1824
+ the polar coordinates lie on the universal semicircle, else inside:
1825
+
1826
+ >>> polar_from_apparent_lifetime(
1827
+ ... 1.9894, [1.9894, 2.4113], frequency=80.0
1828
+ ... ) # doctest: +NUMBER
1829
+ (array([0.7854, 0.7854]), array([0.7071, 0.6364]))
1830
+
1831
+ """
1832
+ omega = numpy.asarray(frequency, dtype=numpy.float64, copy=True)
1833
+ omega *= math.pi * 2.0 * unit_conversion
1834
+ if modulation_lifetime is None:
1835
+ return _polar_from_single_lifetime( # type: ignore[no-any-return]
1836
+ phase_lifetime, omega, **kwargs
1837
+ )
1838
+ return _polar_from_apparent_lifetime( # type: ignore[no-any-return]
1839
+ phase_lifetime, modulation_lifetime, omega, **kwargs
1840
+ )
1841
+
1842
+
1843
+ def phasor_from_fret_donor(
1844
+ frequency: ArrayLike,
1845
+ donor_lifetime: ArrayLike,
1846
+ *,
1847
+ fret_efficiency: ArrayLike = 0.0,
1848
+ donor_fretting: ArrayLike = 1.0,
1849
+ donor_background: ArrayLike = 0.0,
1850
+ background_real: ArrayLike = 0.0,
1851
+ background_imag: ArrayLike = 0.0,
1852
+ unit_conversion: float = 1e-3,
1853
+ **kwargs: Any,
1854
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
1855
+ """Return phasor coordinates of FRET donor channel.
1856
+
1857
+ Calculate phasor coordinates of a FRET (Förster Resonance Energy Transfer)
1858
+ donor channel as a function of frequency, donor lifetime, FRET efficiency,
1859
+ fraction of donors undergoing FRET, and background fluorescence.
1860
+
1861
+ The phasor coordinates of the donor channel contain fractions of:
1862
+
1863
+ - donor not undergoing energy transfer
1864
+ - donor quenched by energy transfer
1865
+ - background fluorescence
1866
+
1867
+ Parameters
1868
+ ----------
1869
+ frequency : array_like
1870
+ Laser pulse or modulation frequency in MHz.
1871
+ donor_lifetime : array_like
1872
+ Lifetime of donor without FRET in ns.
1873
+ fret_efficiency : array_like, optional, default 0
1874
+ FRET efficiency in range [0, 1].
1875
+ donor_fretting : array_like, optional, default 1
1876
+ Fraction of donors participating in FRET. Range [0, 1].
1877
+ donor_background : array_like, optional, default 0
1878
+ Weight of background fluorescence in donor channel
1879
+ relative to fluorescence of donor without FRET.
1880
+ A weight of 1 means the fluorescence of background and donor
1881
+ without FRET are equal.
1882
+ background_real : array_like, optional, default 0
1883
+ Real component of background fluorescence phasor coordinate
1884
+ at `frequency`.
1885
+ background_imag : array_like, optional, default 0
1886
+ Imaginary component of background fluorescence phasor coordinate
1887
+ at `frequency`.
1888
+ unit_conversion : float, optional
1889
+ Product of `frequency` and `lifetime` units' prefix factors.
1890
+ The default is 1e-3 for MHz and ns, or Hz and ms.
1891
+ Use 1.0 for Hz and s.
1892
+ **kwargs
1893
+ Optional `arguments passed to numpy universal functions
1894
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1895
+
1896
+ Returns
1897
+ -------
1898
+ real : ndarray
1899
+ Real component of donor channel phasor coordinates.
1900
+ imag : ndarray
1901
+ Imaginary component of donor channel phasor coordinates.
1902
+
1903
+ See Also
1904
+ --------
1905
+ phasorpy.lifetime.phasor_from_fret_acceptor
1906
+ :ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
1907
+ :ref:`sphx_glr_tutorials_applications_phasorpy_fret_efficiency.py`
1908
+
1909
+ Examples
1910
+ --------
1911
+ Compute the phasor coordinates of a FRET donor channel at three
1912
+ FRET efficiencies:
1913
+
1914
+ >>> phasor_from_fret_donor(
1915
+ ... frequency=80,
1916
+ ... donor_lifetime=4.2,
1917
+ ... fret_efficiency=[0.0, 0.3, 1.0],
1918
+ ... donor_fretting=0.9,
1919
+ ... donor_background=0.1,
1920
+ ... background_real=0.11,
1921
+ ... background_imag=0.12,
1922
+ ... ) # doctest: +NUMBER
1923
+ (array([0.1766, 0.2737, 0.1466]), array([0.3626, 0.4134, 0.2534]))
1924
+
1925
+ """
1926
+ omega = numpy.asarray(frequency, dtype=numpy.float64, copy=True)
1927
+ omega *= math.pi * 2.0 * unit_conversion
1928
+ return _phasor_from_fret_donor( # type: ignore[no-any-return]
1929
+ omega,
1930
+ donor_lifetime,
1931
+ fret_efficiency,
1932
+ donor_fretting,
1933
+ donor_background,
1934
+ background_real,
1935
+ background_imag,
1936
+ **kwargs,
1937
+ )
1938
+
1939
+
1940
+ def phasor_from_fret_acceptor(
1941
+ frequency: ArrayLike,
1942
+ donor_lifetime: ArrayLike,
1943
+ acceptor_lifetime: ArrayLike,
1944
+ *,
1945
+ fret_efficiency: ArrayLike = 0.0,
1946
+ donor_fretting: ArrayLike = 1.0,
1947
+ donor_bleedthrough: ArrayLike = 0.0,
1948
+ acceptor_bleedthrough: ArrayLike = 0.0,
1949
+ acceptor_background: ArrayLike = 0.0,
1950
+ background_real: ArrayLike = 0.0,
1951
+ background_imag: ArrayLike = 0.0,
1952
+ unit_conversion: float = 1e-3,
1953
+ **kwargs: Any,
1954
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
1955
+ """Return phasor coordinates of FRET acceptor channel.
1956
+
1957
+ Calculate phasor coordinates of a FRET (Förster Resonance Energy Transfer)
1958
+ acceptor channel as a function of frequency, donor and acceptor lifetimes,
1959
+ FRET efficiency, fraction of donors undergoing FRET, fraction of directly
1960
+ excited acceptors, fraction of donor fluorescence in acceptor channel,
1961
+ and background fluorescence.
1962
+
1963
+ The phasor coordinates of the acceptor channel contain fractions of:
1964
+
1965
+ - acceptor sensitized by energy transfer
1966
+ - directly excited acceptor
1967
+ - donor bleedthrough
1968
+ - background fluorescence
1969
+
1970
+ Parameters
1971
+ ----------
1972
+ frequency : array_like
1973
+ Laser pulse or modulation frequency in MHz.
1974
+ donor_lifetime : array_like
1975
+ Lifetime of donor without FRET in ns.
1976
+ acceptor_lifetime : array_like
1977
+ Lifetime of acceptor in ns.
1978
+ fret_efficiency : array_like, optional, default 0
1979
+ FRET efficiency in range [0, 1].
1980
+ donor_fretting : array_like, optional, default 1
1981
+ Fraction of donors participating in FRET. Range [0, 1].
1982
+ donor_bleedthrough : array_like, optional, default 0
1983
+ Weight of donor fluorescence in acceptor channel
1984
+ relative to fluorescence of fully sensitized acceptor.
1985
+ A weight of 1 means the fluorescence from donor and fully sensitized
1986
+ acceptor are equal.
1987
+ The background in the donor channel does not bleed through.
1988
+ acceptor_bleedthrough : array_like, optional, default 0
1989
+ Weight of fluorescence from directly excited acceptor
1990
+ relative to fluorescence of fully sensitized acceptor.
1991
+ A weight of 1 means the fluorescence from directly excited acceptor
1992
+ and fully sensitized acceptor are equal.
1993
+ acceptor_background : array_like, optional, default 0
1994
+ Weight of background fluorescence in acceptor channel
1995
+ relative to fluorescence of fully sensitized acceptor.
1996
+ A weight of 1 means the fluorescence of background and fully
1997
+ sensitized acceptor are equal.
1998
+ background_real : array_like, optional, default 0
1999
+ Real component of background fluorescence phasor coordinate
2000
+ at `frequency`.
2001
+ background_imag : array_like, optional, default 0
2002
+ Imaginary component of background fluorescence phasor coordinate
2003
+ at `frequency`.
2004
+ unit_conversion : float, optional
2005
+ Product of `frequency` and `lifetime` units' prefix factors.
2006
+ The default is 1e-3 for MHz and ns, or Hz and ms.
2007
+ Use 1.0 for Hz and s.
2008
+ **kwargs
2009
+ Optional `arguments passed to numpy universal functions
2010
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
2011
+
2012
+ Returns
2013
+ -------
2014
+ real : ndarray
2015
+ Real component of acceptor channel phasor coordinates.
2016
+ imag : ndarray
2017
+ Imaginary component of acceptor channel phasor coordinates.
2018
+
2019
+ See Also
2020
+ --------
2021
+ phasorpy.lifetime.phasor_from_fret_donor
2022
+ :ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
2023
+
2024
+ Examples
2025
+ --------
2026
+ Compute the phasor coordinates of a FRET acceptor channel at three
2027
+ FRET efficiencies:
2028
+
2029
+ >>> phasor_from_fret_acceptor(
2030
+ ... frequency=80,
2031
+ ... donor_lifetime=4.2,
2032
+ ... acceptor_lifetime=3.0,
2033
+ ... fret_efficiency=[0.0, 0.3, 1.0],
2034
+ ... donor_fretting=0.9,
2035
+ ... donor_bleedthrough=0.1,
2036
+ ... acceptor_bleedthrough=0.1,
2037
+ ... acceptor_background=0.1,
2038
+ ... background_real=0.11,
2039
+ ... background_imag=0.12,
2040
+ ... ) # doctest: +NUMBER
2041
+ (array([0.1996, 0.05772, 0.2867]), array([0.3225, 0.3103, 0.4292]))
2042
+
2043
+ """
2044
+ omega = numpy.asarray(frequency, dtype=numpy.float64, copy=True)
2045
+ omega *= math.pi * 2.0 * unit_conversion
2046
+ return _phasor_from_fret_acceptor( # type: ignore[no-any-return]
2047
+ omega,
2048
+ donor_lifetime,
2049
+ acceptor_lifetime,
2050
+ fret_efficiency,
2051
+ donor_fretting,
2052
+ donor_bleedthrough,
2053
+ acceptor_bleedthrough,
2054
+ acceptor_background,
2055
+ background_real,
2056
+ background_imag,
2057
+ **kwargs,
2058
+ )