phasorpy 0.1__cp312-cp312-win_amd64.whl → 0.3__cp312-cp312-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/_phasorpy.cp312-win_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +388 -38
- phasorpy/_utils.py +27 -14
- phasorpy/datasets.py +20 -0
- phasorpy/io.py +156 -16
- phasorpy/phasor.py +521 -249
- phasorpy/plot.py +6 -2
- phasorpy/utils.py +301 -1
- phasorpy/version.py +1 -1
- {phasorpy-0.1.dist-info → phasorpy-0.3.dist-info}/METADATA +22 -22
- phasorpy-0.3.dist-info/RECORD +24 -0
- {phasorpy-0.1.dist-info → phasorpy-0.3.dist-info}/WHEEL +1 -1
- phasorpy-0.1.dist-info/RECORD +0 -24
- {phasorpy-0.1.dist-info → phasorpy-0.3.dist-info}/LICENSE.txt +0 -0
- {phasorpy-0.1.dist-info → phasorpy-0.3.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.1.dist-info → phasorpy-0.3.dist-info}/top_level.txt +0 -0
phasorpy/phasor.py
CHANGED
@@ -30,6 +30,7 @@ The ``phasorpy.phasor`` module provides functions to:
|
|
30
30
|
- :py:func:`phasor_transform`
|
31
31
|
- :py:func:`phasor_multiply`
|
32
32
|
- :py:func:`phasor_divide`
|
33
|
+
- :py:func:`phasor_normalize`
|
33
34
|
|
34
35
|
- calibrate phasor coordinates with reference of known fluorescence
|
35
36
|
lifetime:
|
@@ -64,7 +65,7 @@ The ``phasorpy.phasor`` module provides functions to:
|
|
64
65
|
|
65
66
|
- filter phasor coordinates:
|
66
67
|
|
67
|
-
- :py:func:`
|
68
|
+
- :py:func:`phasor_filter_median`
|
68
69
|
- :py:func:`phasor_threshold`
|
69
70
|
|
70
71
|
"""
|
@@ -81,7 +82,7 @@ __all__ = [
|
|
81
82
|
'phasor_calibrate',
|
82
83
|
'phasor_center',
|
83
84
|
'phasor_divide',
|
84
|
-
'
|
85
|
+
'phasor_filter_median',
|
85
86
|
'phasor_from_apparent_lifetime',
|
86
87
|
'phasor_from_fret_acceptor',
|
87
88
|
'phasor_from_fret_donor',
|
@@ -89,6 +90,7 @@ __all__ = [
|
|
89
90
|
'phasor_from_polar',
|
90
91
|
'phasor_from_signal',
|
91
92
|
'phasor_multiply',
|
93
|
+
'phasor_normalize',
|
92
94
|
'phasor_semicircle',
|
93
95
|
'phasor_threshold',
|
94
96
|
'phasor_to_apparent_lifetime',
|
@@ -104,7 +106,6 @@ __all__ = [
|
|
104
106
|
]
|
105
107
|
|
106
108
|
import math
|
107
|
-
import numbers
|
108
109
|
from collections.abc import Sequence
|
109
110
|
from typing import TYPE_CHECKING
|
110
111
|
|
@@ -122,6 +123,7 @@ import numpy
|
|
122
123
|
|
123
124
|
from ._phasorpy import (
|
124
125
|
_gaussian_signal,
|
126
|
+
_median_filter_2d,
|
125
127
|
_phasor_at_harmonic,
|
126
128
|
_phasor_divide,
|
127
129
|
_phasor_from_apparent_lifetime,
|
@@ -161,6 +163,7 @@ def phasor_from_signal(
|
|
161
163
|
use_fft: bool | None = None,
|
162
164
|
rfft: Callable[..., NDArray[Any]] | None = None,
|
163
165
|
dtype: DTypeLike = None,
|
166
|
+
normalize: bool = True,
|
164
167
|
num_threads: int | None = None,
|
165
168
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
166
169
|
r"""Return phasor coordinates from signal.
|
@@ -180,6 +183,8 @@ def phasor_from_signal(
|
|
180
183
|
Else, harmonics must be at least one and no larger than half the
|
181
184
|
number of `signal` samples along `axis`.
|
182
185
|
The default is the first harmonic (fundamental frequency).
|
186
|
+
A minimum of `harmonic * 2 + 1` samples are required along `axis`
|
187
|
+
to calculate correct phasor coordinates at `harmonic`.
|
183
188
|
sample_phase : array_like, optional
|
184
189
|
Phase values (in radians) of `signal` samples along `axis`.
|
185
190
|
If None (default), samples are assumed to be uniformly spaced along
|
@@ -199,6 +204,14 @@ def phasor_from_signal(
|
|
199
204
|
dtype : dtype_like, optional
|
200
205
|
Data type of output arrays. Either float32 or float64.
|
201
206
|
The default is float64 unless the `signal` is float32.
|
207
|
+
normalize : bool, optional
|
208
|
+
Return normalized phasor coordinates.
|
209
|
+
If true (default), return average of `signal` along `axis` and
|
210
|
+
Fourier coefficients divided by sum of `signal` along `axis`.
|
211
|
+
Else, return sum of `signal` along `axis` and unscaled Fourier
|
212
|
+
coefficients.
|
213
|
+
Un-normalized phasor coordinates cannot be used with most of PhasorPy's
|
214
|
+
functions but may be required for intermediate processing.
|
202
215
|
num_threads : int, optional
|
203
216
|
Number of OpenMP threads to use for parallelization when not using FFT.
|
204
217
|
By default, multi-threading is disabled.
|
@@ -229,13 +242,15 @@ def phasor_from_signal(
|
|
229
242
|
See Also
|
230
243
|
--------
|
231
244
|
phasorpy.phasor.phasor_to_signal
|
245
|
+
phasorpy.phasor.phasor_normalize
|
232
246
|
:ref:`sphx_glr_tutorials_benchmarks_phasorpy_phasor_from_signal.py`
|
233
247
|
|
234
248
|
Notes
|
235
249
|
-----
|
236
|
-
The phasor coordinates `real` (:math:`G`), `imag` (:math:`S`),
|
237
|
-
`mean` (:math:`F_{DC}`) are calculated from
|
238
|
-
signal :math:`F` af `harmonic` :math:`h`
|
250
|
+
The normalized phasor coordinates `real` (:math:`G`), `imag` (:math:`S`),
|
251
|
+
and average intensity `mean` (:math:`F_{DC}`) are calculated from
|
252
|
+
:math:`K\ge3` samples of the signal :math:`F` af `harmonic` :math:`h`
|
253
|
+
according to:
|
239
254
|
|
240
255
|
.. math::
|
241
256
|
|
@@ -285,7 +300,7 @@ def phasor_from_signal(
|
|
285
300
|
if dtype.kind != 'f':
|
286
301
|
raise TypeError(f'{dtype=} not supported')
|
287
302
|
|
288
|
-
harmonic, keepdims = parse_harmonic(harmonic, samples)
|
303
|
+
harmonic, keepdims = parse_harmonic(harmonic, samples // 2)
|
289
304
|
num_harmonics = len(harmonic)
|
290
305
|
|
291
306
|
if sample_phase is not None:
|
@@ -303,14 +318,16 @@ def phasor_from_signal(
|
|
303
318
|
use_fft = sample_phase is None and (
|
304
319
|
rfft is not None
|
305
320
|
or num_harmonics > 7
|
306
|
-
or num_harmonics
|
321
|
+
or num_harmonics >= samples // 2
|
307
322
|
)
|
308
323
|
|
309
324
|
if use_fft:
|
310
325
|
if rfft is None:
|
311
326
|
rfft = numpy.fft.rfft
|
312
327
|
|
313
|
-
fft: NDArray[Any] = rfft(
|
328
|
+
fft: NDArray[Any] = rfft(
|
329
|
+
signal, axis=axis, norm='forward' if normalize else 'backward'
|
330
|
+
)
|
314
331
|
|
315
332
|
mean = fft.take(0, axis=axis).real
|
316
333
|
if not mean.ndim == 0:
|
@@ -330,10 +347,10 @@ def phasor_from_signal(
|
|
330
347
|
real = numpy.moveaxis(real, axis, 0)
|
331
348
|
imag = numpy.moveaxis(imag, axis, 0)
|
332
349
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
350
|
+
if normalize:
|
351
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
352
|
+
real /= dc
|
353
|
+
imag /= dc
|
337
354
|
numpy.negative(imag, out=imag)
|
338
355
|
|
339
356
|
if not keepdims and real.ndim == 0:
|
@@ -367,7 +384,7 @@ def phasor_from_signal(
|
|
367
384
|
phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype)
|
368
385
|
signal = signal.reshape((size0, samples, size1))
|
369
386
|
|
370
|
-
_phasor_from_signal(phasor, signal, sincos, num_threads)
|
387
|
+
_phasor_from_signal(phasor, signal, sincos, normalize, num_threads)
|
371
388
|
|
372
389
|
# restore original shape
|
373
390
|
shape = shape0 + shape1
|
@@ -398,7 +415,7 @@ def phasor_to_signal(
|
|
398
415
|
----------
|
399
416
|
mean : array_like
|
400
417
|
Average signal intensity (DC).
|
401
|
-
If not scalar, shape must match the last
|
418
|
+
If not scalar, shape must match the last dimensions of `real`.
|
402
419
|
real : array_like
|
403
420
|
Real component of phasor coordinates.
|
404
421
|
Multiple harmonics, if any, must be in the first axis.
|
@@ -413,7 +430,7 @@ def phasor_to_signal(
|
|
413
430
|
coordinates (most commonly, lower harmonics are present if the number
|
414
431
|
of dimensions of `mean` is one less than `real`).
|
415
432
|
If `'all'`, the harmonics in the first axis of phasor coordinates are
|
416
|
-
the lower harmonics
|
433
|
+
the lower harmonics necessary to synthesize `samples`.
|
417
434
|
Else, harmonics must be at least one and no larger than half of
|
418
435
|
`samples`.
|
419
436
|
The phasor coordinates of missing harmonics are zeroed
|
@@ -462,18 +479,15 @@ def phasor_to_signal(
|
|
462
479
|
array([2.2, 2.486, 0.8566, -0.4365, 0.3938])
|
463
480
|
|
464
481
|
"""
|
482
|
+
if samples < 3:
|
483
|
+
raise ValueError(f'{samples=} < 3')
|
484
|
+
|
465
485
|
mean = numpy.array(mean, ndmin=0, copy=True)
|
466
486
|
real = numpy.array(real, ndmin=0, copy=True)
|
467
487
|
imag = numpy.array(imag, ndmin=1, copy=True)
|
468
488
|
|
469
|
-
if isinstance(harmonic, (int, numbers.Integral)) and harmonic == 0:
|
470
|
-
# harmonics are expected in the first axes of real and imag
|
471
|
-
samples_ = 2 * imag.shape[0]
|
472
|
-
else:
|
473
|
-
samples_ = samples
|
474
|
-
|
475
489
|
harmonic_ = harmonic
|
476
|
-
harmonic, has_harmonic_axis = parse_harmonic(harmonic,
|
490
|
+
harmonic, has_harmonic_axis = parse_harmonic(harmonic, samples // 2)
|
477
491
|
|
478
492
|
if real.ndim == 1 and len(harmonic) > 1 and real.shape[0] == len(harmonic):
|
479
493
|
# single axis contains harmonic
|
@@ -508,7 +522,6 @@ def phasor_to_signal(
|
|
508
522
|
if len(harmonic) != real.shape[0]:
|
509
523
|
raise ValueError(f'{len(harmonic)=} != {real.shape[0]=}')
|
510
524
|
|
511
|
-
# complex multiplication by mean signal
|
512
525
|
real *= mean
|
513
526
|
imag *= mean
|
514
527
|
numpy.negative(imag, out=imag)
|
@@ -641,7 +654,7 @@ def lifetime_to_signal(
|
|
641
654
|
if harmonic is None:
|
642
655
|
harmonic = 'all'
|
643
656
|
all_hamonics = harmonic == 'all'
|
644
|
-
harmonic, _ = parse_harmonic(harmonic, samples)
|
657
|
+
harmonic, _ = parse_harmonic(harmonic, samples // 2)
|
645
658
|
|
646
659
|
if samples < 16:
|
647
660
|
raise ValueError(f'{samples=} < 16')
|
@@ -953,24 +966,119 @@ def phasor_divide(
|
|
953
966
|
)
|
954
967
|
|
955
968
|
|
969
|
+
def phasor_normalize(
|
970
|
+
mean_unnormalized: ArrayLike,
|
971
|
+
real_unnormalized: ArrayLike,
|
972
|
+
imag_unnormalized: ArrayLike,
|
973
|
+
/,
|
974
|
+
samples: int = 1,
|
975
|
+
dtype: DTypeLike = None,
|
976
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
977
|
+
r"""Return normalized phasor coordinates.
|
978
|
+
|
979
|
+
Use to normalize the phasor coordinates returned by
|
980
|
+
``phasor_from_signal(..., normalize=False)``.
|
981
|
+
|
982
|
+
Parameters
|
983
|
+
----------
|
984
|
+
mean_unnormalized : array_like
|
985
|
+
Unnormalized intensity of phasor coordinates.
|
986
|
+
real_unnormalized : array_like
|
987
|
+
Unnormalized real component of phasor coordinates.
|
988
|
+
imag_unnormalized : array_like
|
989
|
+
Unnormalized imaginary component of phasor coordinates.
|
990
|
+
samples : int, default: 1
|
991
|
+
Number of signal samples over which `mean` was integrated.
|
992
|
+
dtype : dtype_like, optional
|
993
|
+
Data type of output arrays. Either float32 or float64.
|
994
|
+
The default is float64 unless the `real` is float32.
|
995
|
+
|
996
|
+
Returns
|
997
|
+
-------
|
998
|
+
mean : ndarray
|
999
|
+
Normalized intensity.
|
1000
|
+
real : ndarray
|
1001
|
+
Normalized real component.
|
1002
|
+
imag : ndarray
|
1003
|
+
Normalized imaginary component.
|
1004
|
+
|
1005
|
+
Notes
|
1006
|
+
-----
|
1007
|
+
The average intensity `mean` (:math:`F_{DC}`) and normalized phasor
|
1008
|
+
coordinates `real` (:math:`G`) and `imag` (:math:`S`) are calculated from
|
1009
|
+
the signal `intensity` (:math:`F`), the number of `samples` (:math:`K`),
|
1010
|
+
`real_unnormalized` (:math:`G'`), and `imag_unnormalized` (:math:`S'`)
|
1011
|
+
according to:
|
1012
|
+
|
1013
|
+
.. math::
|
1014
|
+
|
1015
|
+
F_{DC} &= F / K
|
1016
|
+
|
1017
|
+
G &= G' / F
|
1018
|
+
|
1019
|
+
S &= S' / F
|
1020
|
+
|
1021
|
+
If :math:`F = 0`, the normalized phasor coordinates (:math:`G`)
|
1022
|
+
and (:math:`S`) are undefined (:math:`NaN` or :math:`\infty`).
|
1023
|
+
|
1024
|
+
Examples
|
1025
|
+
--------
|
1026
|
+
Normalize phasor coordinates with intensity integrated over 10 samples:
|
1027
|
+
|
1028
|
+
>>> phasor_normalize([0.0, 0.1], [0.0, 0.3], [0.4, 0.5], samples=10)
|
1029
|
+
(array([0, 0.01]), array([nan, 3]), array([inf, 5]))
|
1030
|
+
|
1031
|
+
Normalize multi-harmonic phasor coordinates:
|
1032
|
+
|
1033
|
+
>>> phasor_normalize(0.1, [0.0, 0.3], [0.4, 0.5], samples=10)
|
1034
|
+
(array(0.01), array([0, 3]), array([4, 5]))
|
1035
|
+
|
1036
|
+
"""
|
1037
|
+
if samples < 1:
|
1038
|
+
raise ValueError(f'{samples=} < 1')
|
1039
|
+
|
1040
|
+
if (
|
1041
|
+
dtype is None
|
1042
|
+
and isinstance(real_unnormalized, numpy.ndarray)
|
1043
|
+
and real_unnormalized.dtype == numpy.float32
|
1044
|
+
):
|
1045
|
+
real = real_unnormalized.copy()
|
1046
|
+
else:
|
1047
|
+
real = numpy.array(real_unnormalized, dtype, copy=True)
|
1048
|
+
imag = numpy.array(imag_unnormalized, real.dtype, copy=True)
|
1049
|
+
mean = numpy.array(
|
1050
|
+
mean_unnormalized, real.dtype, copy=None if samples == 1 else True
|
1051
|
+
)
|
1052
|
+
|
1053
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
1054
|
+
numpy.divide(real, mean, out=real)
|
1055
|
+
numpy.divide(imag, mean, out=imag)
|
1056
|
+
if samples > 1:
|
1057
|
+
numpy.divide(mean, samples, out=mean)
|
1058
|
+
|
1059
|
+
return mean, real, imag
|
1060
|
+
|
1061
|
+
|
956
1062
|
def phasor_calibrate(
|
957
1063
|
real: ArrayLike,
|
958
1064
|
imag: ArrayLike,
|
1065
|
+
reference_mean: ArrayLike,
|
959
1066
|
reference_real: ArrayLike,
|
960
1067
|
reference_imag: ArrayLike,
|
961
1068
|
/,
|
962
1069
|
frequency: ArrayLike,
|
963
1070
|
lifetime: ArrayLike,
|
964
1071
|
*,
|
1072
|
+
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
1073
|
+
skip_axis: int | Sequence[int] | None = None,
|
965
1074
|
fraction: ArrayLike | None = None,
|
966
1075
|
preexponential: bool = False,
|
967
1076
|
unit_conversion: float = 1e-3,
|
968
|
-
reverse: bool = False,
|
969
1077
|
method: Literal['mean', 'median'] = 'mean',
|
970
|
-
|
1078
|
+
nan_safe: bool = True,
|
1079
|
+
reverse: bool = False,
|
971
1080
|
) -> tuple[NDArray[Any], NDArray[Any]]:
|
972
|
-
"""
|
973
|
-
Return calibrated/referenced phasor coordinates.
|
1081
|
+
"""Return calibrated/referenced phasor coordinates.
|
974
1082
|
|
975
1083
|
Calibration of phasor coordinates from time-resolved measurements is
|
976
1084
|
necessary to account for the instrument response function (IRF) and delays
|
@@ -982,20 +1090,34 @@ def phasor_calibrate(
|
|
982
1090
|
Real component of phasor coordinates to be calibrated.
|
983
1091
|
imag : array_like
|
984
1092
|
Imaginary component of phasor coordinates to be calibrated.
|
1093
|
+
reference_mean : array_like or None
|
1094
|
+
Intensity of phasor coordinates from reference of known lifetime.
|
1095
|
+
Used to re-normalize averaged phasor coordinates.
|
985
1096
|
reference_real : array_like
|
986
1097
|
Real component of phasor coordinates from reference of known lifetime.
|
987
1098
|
Must be measured with the same instrument setting as the phasor
|
988
|
-
coordinates to be calibrated.
|
1099
|
+
coordinates to be calibrated. Dimensions must be the same as `real`.
|
989
1100
|
reference_imag : array_like
|
990
1101
|
Imaginary component of phasor coordinates from reference of known
|
991
1102
|
lifetime.
|
992
1103
|
Must be measured with the same instrument setting as the phasor
|
993
1104
|
coordinates to be calibrated.
|
994
1105
|
frequency : array_like
|
995
|
-
|
996
|
-
A scalar or one-dimensional sequence.
|
1106
|
+
Fundamental laser pulse or modulation frequency in MHz.
|
997
1107
|
lifetime : array_like
|
998
|
-
Lifetime components in ns. Must be scalar or one
|
1108
|
+
Lifetime components in ns. Must be scalar or one-dimensional.
|
1109
|
+
harmonic : int, sequence of int, or 'all', default: 1
|
1110
|
+
Harmonics included in `real` and `imag`.
|
1111
|
+
If an integer, the harmonics at which `real` and `imag` were acquired
|
1112
|
+
or calculated.
|
1113
|
+
If a sequence, the harmonics included in the first axis of `real` and
|
1114
|
+
`imag`.
|
1115
|
+
If `'all'`, the first axis of `real` and `imag` contains lower
|
1116
|
+
harmonics.
|
1117
|
+
The default is the first harmonic (fundamental frequency).
|
1118
|
+
skip_axis : int or sequence of int, optional
|
1119
|
+
Axes in `reference_mean` to exclude from reference center calculation.
|
1120
|
+
By default, all axes except harmonics are included.
|
999
1121
|
fraction : array_like, optional
|
1000
1122
|
Fractional intensities or pre-exponential amplitudes of the lifetime
|
1001
1123
|
components. Fractions are normalized to sum to 1.
|
@@ -1007,17 +1129,18 @@ def phasor_calibrate(
|
|
1007
1129
|
Product of `frequency` and `lifetime` units' prefix factors.
|
1008
1130
|
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1009
1131
|
Use 1.0 for Hz and s.
|
1010
|
-
reverse : bool, optional
|
1011
|
-
Reverse calibration.
|
1012
1132
|
method : str, optional
|
1013
|
-
Method used for calculating center of
|
1014
|
-
`reference_imag`:
|
1133
|
+
Method used for calculating center of reference phasor coordinates:
|
1015
1134
|
|
1016
|
-
- ``'mean'``: Arithmetic mean
|
1017
|
-
- ``'median'``: Spatial median
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1135
|
+
- ``'mean'``: Arithmetic mean.
|
1136
|
+
- ``'median'``: Spatial median.
|
1137
|
+
|
1138
|
+
nan_safe : bool, optional
|
1139
|
+
Ensure `method` is applied to same elements of reference arrays.
|
1140
|
+
By default, distribute NaNs among reference arrays before applying
|
1141
|
+
`method`.
|
1142
|
+
reverse : bool, optional
|
1143
|
+
Reverse calibration.
|
1021
1144
|
|
1022
1145
|
Returns
|
1023
1146
|
-------
|
@@ -1031,6 +1154,7 @@ def phasor_calibrate(
|
|
1031
1154
|
ValueError
|
1032
1155
|
The array shapes of `real` and `imag`, or `reference_real` and
|
1033
1156
|
`reference_imag` do not match.
|
1157
|
+
Number of harmonics does not match the first axis of `real` and `imag`.
|
1034
1158
|
|
1035
1159
|
See Also
|
1036
1160
|
--------
|
@@ -1050,11 +1174,13 @@ def phasor_calibrate(
|
|
1050
1174
|
imag,
|
1051
1175
|
*polar_from_reference_phasor(
|
1052
1176
|
*phasor_center(
|
1177
|
+
reference_mean,
|
1053
1178
|
reference_real,
|
1054
1179
|
reference_imag,
|
1055
1180
|
skip_axis,
|
1056
1181
|
method,
|
1057
|
-
|
1182
|
+
nan_safe,
|
1183
|
+
)[1:],
|
1058
1184
|
*phasor_from_lifetime(
|
1059
1185
|
frequency,
|
1060
1186
|
lifetime,
|
@@ -1081,6 +1207,7 @@ def phasor_calibrate(
|
|
1081
1207
|
>>> phasor_calibrate(
|
1082
1208
|
... [0.1, 0.2, 0.3],
|
1083
1209
|
... [0.4, 0.5, 0.6],
|
1210
|
+
... [1.0, 1.0, 1.0],
|
1084
1211
|
... [0.2, 0.3, 0.4],
|
1085
1212
|
... [0.5, 0.6, 0.7],
|
1086
1213
|
... frequency=80,
|
@@ -1093,6 +1220,7 @@ def phasor_calibrate(
|
|
1093
1220
|
>>> phasor_calibrate(
|
1094
1221
|
... [0.0658, 0.132, 0.198],
|
1095
1222
|
... [0.2657, 0.332, 0.399],
|
1223
|
+
... [1.0, 1.0, 1.0],
|
1096
1224
|
... [0.2, 0.3, 0.4],
|
1097
1225
|
... [0.5, 0.6, 0.7],
|
1098
1226
|
... frequency=80,
|
@@ -1102,20 +1230,52 @@ def phasor_calibrate(
|
|
1102
1230
|
(array([0.1, 0.2, 0.3]), array([0.4, 0.5, 0.6]))
|
1103
1231
|
|
1104
1232
|
"""
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
if
|
1233
|
+
real = numpy.asarray(real)
|
1234
|
+
imag = numpy.asarray(imag)
|
1235
|
+
reference_mean = numpy.asarray(reference_mean)
|
1236
|
+
reference_real = numpy.asarray(reference_real)
|
1237
|
+
reference_imag = numpy.asarray(reference_imag)
|
1238
|
+
|
1239
|
+
if real.shape != imag.shape:
|
1240
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
1241
|
+
if reference_real.shape != reference_imag.shape:
|
1242
|
+
raise ValueError(f'{reference_real.shape=} != {reference_imag.shape=}')
|
1243
|
+
|
1244
|
+
has_harmonic_axis = reference_mean.ndim + 1 == reference_real.ndim
|
1245
|
+
harmonic, _ = parse_harmonic(
|
1246
|
+
harmonic, reference_real.shape[0] if has_harmonic_axis else None
|
1247
|
+
)
|
1248
|
+
|
1249
|
+
if has_harmonic_axis:
|
1250
|
+
if real.ndim == 0:
|
1251
|
+
raise ValueError(f'{real.shape=} != {len(harmonic)=}')
|
1252
|
+
if real.shape[0] != len(harmonic):
|
1253
|
+
raise ValueError(f'{real.shape[0]=} != {len(harmonic)=}')
|
1254
|
+
if reference_real.shape[0] != len(harmonic):
|
1255
|
+
raise ValueError(f'{reference_real.shape[0]=} != {len(harmonic)=}')
|
1256
|
+
if reference_mean.shape != reference_real.shape[1:]:
|
1257
|
+
raise ValueError(
|
1258
|
+
f'{reference_mean.shape=} != {reference_real.shape[1:]=}'
|
1259
|
+
)
|
1260
|
+
elif reference_mean.shape != reference_real.shape:
|
1261
|
+
raise ValueError(f'{reference_mean.shape=} != {reference_real.shape=}')
|
1262
|
+
elif len(harmonic) > 1:
|
1112
1263
|
raise ValueError(
|
1113
|
-
f'
|
1114
|
-
f'!= reference_imag.shape{ref_im.shape}'
|
1264
|
+
f'{reference_mean.shape=} does not have harmonic axis'
|
1115
1265
|
)
|
1116
|
-
|
1117
|
-
|
1266
|
+
|
1267
|
+
frequency = numpy.asarray(frequency)
|
1268
|
+
frequency = frequency * harmonic
|
1269
|
+
|
1270
|
+
_, measured_re, measured_im = phasor_center(
|
1271
|
+
reference_mean,
|
1272
|
+
reference_real,
|
1273
|
+
reference_imag,
|
1274
|
+
skip_axis=skip_axis,
|
1275
|
+
method=method,
|
1276
|
+
nan_safe=nan_safe,
|
1118
1277
|
)
|
1278
|
+
|
1119
1279
|
known_re, known_im = phasor_from_lifetime(
|
1120
1280
|
frequency,
|
1121
1281
|
lifetime,
|
@@ -1123,27 +1283,26 @@ def phasor_calibrate(
|
|
1123
1283
|
preexponential=preexponential,
|
1124
1284
|
unit_conversion=unit_conversion,
|
1125
1285
|
)
|
1286
|
+
|
1126
1287
|
phi_zero, mod_zero = polar_from_reference_phasor(
|
1127
1288
|
measured_re, measured_im, known_re, known_im
|
1128
1289
|
)
|
1290
|
+
|
1129
1291
|
if numpy.ndim(phi_zero) > 0:
|
1130
1292
|
if reverse:
|
1131
1293
|
numpy.negative(phi_zero, out=phi_zero)
|
1132
1294
|
numpy.reciprocal(mod_zero, out=mod_zero)
|
1133
|
-
_, axis = _parse_skip_axis(
|
1295
|
+
_, axis = _parse_skip_axis(
|
1296
|
+
skip_axis, real.ndim - int(has_harmonic_axis), has_harmonic_axis
|
1297
|
+
)
|
1134
1298
|
if axis is not None:
|
1135
|
-
phi_zero = numpy.expand_dims(
|
1136
|
-
|
1137
|
-
axis=axis,
|
1138
|
-
)
|
1139
|
-
mod_zero = numpy.expand_dims(
|
1140
|
-
mod_zero,
|
1141
|
-
axis=axis,
|
1142
|
-
)
|
1299
|
+
phi_zero = numpy.expand_dims(phi_zero, axis=axis)
|
1300
|
+
mod_zero = numpy.expand_dims(mod_zero, axis=axis)
|
1143
1301
|
elif reverse:
|
1144
1302
|
phi_zero = -phi_zero
|
1145
1303
|
mod_zero = 1.0 / mod_zero
|
1146
|
-
|
1304
|
+
|
1305
|
+
return phasor_transform(real, imag, phi_zero, mod_zero)
|
1147
1306
|
|
1148
1307
|
|
1149
1308
|
def phasor_transform(
|
@@ -2643,39 +2802,59 @@ def phasor_to_principal_plane(
|
|
2643
2802
|
)
|
2644
2803
|
|
2645
2804
|
|
2646
|
-
def
|
2805
|
+
def phasor_filter_median(
|
2806
|
+
mean: ArrayLike,
|
2647
2807
|
real: ArrayLike,
|
2648
2808
|
imag: ArrayLike,
|
2649
2809
|
/,
|
2650
2810
|
*,
|
2651
|
-
method: Literal['median'] = 'median',
|
2652
2811
|
repeat: int = 1,
|
2812
|
+
size: int = 3,
|
2813
|
+
skip_axis: int | Sequence[int] | None = None,
|
2814
|
+
use_scipy: bool = False,
|
2815
|
+
num_threads: int | None = None,
|
2653
2816
|
**kwargs: Any,
|
2654
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2655
|
-
"""Return filtered phasor coordinates.
|
2817
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2818
|
+
"""Return median-filtered phasor coordinates.
|
2656
2819
|
|
2657
|
-
By default, a median filter
|
2658
|
-
components of phasor coordinates once with a kernel size of 3
|
2659
|
-
multiplied by the number of dimensions of the input arrays.
|
2820
|
+
By default, apply a NaN-aware median filter independently to the real
|
2821
|
+
and imaginary components of phasor coordinates once with a kernel size of 3
|
2822
|
+
multiplied by the number of dimensions of the input arrays. Return the
|
2823
|
+
intensity unchanged.
|
2660
2824
|
|
2661
2825
|
Parameters
|
2662
2826
|
----------
|
2827
|
+
mean : array_like
|
2828
|
+
Intensity of phasor coordinates.
|
2663
2829
|
real : array_like
|
2664
2830
|
Real component of phasor coordinates to be filtered.
|
2665
2831
|
imag : array_like
|
2666
2832
|
Imaginary component of phasor coordinates to be filtered.
|
2667
|
-
method : str, optional
|
2668
|
-
Method used for filtering:
|
2669
|
-
|
2670
|
-
- ``'median'``: Spatial median of phasor coordinates.
|
2671
|
-
|
2672
2833
|
repeat : int, optional
|
2673
|
-
Number of times to apply filter. The default is 1.
|
2834
|
+
Number of times to apply median filter. The default is 1.
|
2835
|
+
size : int, optional
|
2836
|
+
Size of median filter kernel. The default is 3.
|
2837
|
+
skip_axis : int or sequence of int, optional
|
2838
|
+
Axes in `mean` to exclude from filter.
|
2839
|
+
By default, all axes except harmonics are included.
|
2840
|
+
use_scipy : bool, optional
|
2841
|
+
Use :py:func:`scipy.ndimage.median_filter`.
|
2842
|
+
This function has undefined behavior if the input arrays contain
|
2843
|
+
`NaN` values but is faster when filtering more than 2 dimensions.
|
2844
|
+
See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
|
2845
|
+
num_threads : int, optional
|
2846
|
+
Number of OpenMP threads to use for parallelization.
|
2847
|
+
Applies to filtering in two dimensions when not using scipy.
|
2848
|
+
By default, multi-threading is disabled.
|
2849
|
+
If zero, up to half of logical CPUs are used.
|
2850
|
+
OpenMP may not be available on all platforms.
|
2674
2851
|
**kwargs
|
2675
2852
|
Optional arguments passed to :py:func:`scipy.ndimage.median_filter`.
|
2676
2853
|
|
2677
2854
|
Returns
|
2678
2855
|
-------
|
2856
|
+
mean : ndarray
|
2857
|
+
Unchanged intensity of phasor coordinates.
|
2679
2858
|
real : ndarray
|
2680
2859
|
Filtered real component of phasor coordinates.
|
2681
2860
|
imag : ndarray
|
@@ -2684,53 +2863,135 @@ def phasor_filter(
|
|
2684
2863
|
Raises
|
2685
2864
|
------
|
2686
2865
|
ValueError
|
2687
|
-
If
|
2688
|
-
|
2689
|
-
|
2690
|
-
|
2691
|
-
Notes
|
2692
|
-
-----
|
2693
|
-
For now, only the median filter method is implemented.
|
2694
|
-
Additional filtering methods may be added in the future.
|
2695
|
-
|
2696
|
-
The implementation of the median filter method is based on
|
2697
|
-
:py:func:`scipy.ndimage.median_filter`,
|
2698
|
-
which has undefined behavior if the input arrays contain `NaN` values.
|
2699
|
-
See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
|
2866
|
+
If `repeat` is less than 0.
|
2867
|
+
If `size` is less than 1.
|
2868
|
+
The array shapes of `mean`, `real`, and `imag` do not match.
|
2700
2869
|
|
2701
2870
|
Examples
|
2702
2871
|
--------
|
2703
2872
|
Apply three times a median filter with a kernel size of three:
|
2704
2873
|
|
2705
|
-
>>>
|
2706
|
-
... [[
|
2707
|
-
... [[
|
2874
|
+
>>> mean, real, imag = phasor_filter_median(
|
2875
|
+
... [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
|
2876
|
+
... [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5], [0.2, 0.2, 0.2]],
|
2877
|
+
... [[0.3, 0.3, 0.3], [0.6, math.nan, 0.6], [0.4, 0.4, 0.4]],
|
2708
2878
|
... size=3,
|
2709
2879
|
... repeat=3,
|
2710
2880
|
... )
|
2711
|
-
|
2712
|
-
|
2713
|
-
|
2714
|
-
|
2715
|
-
|
2716
|
-
|
2881
|
+
>>> mean
|
2882
|
+
array([[1, 2, 3],
|
2883
|
+
[4, 5, 6],
|
2884
|
+
[7, 8, 9]])
|
2885
|
+
>>> real
|
2886
|
+
array([[0, 0, 0],
|
2887
|
+
[0.2, 0.2, 0.2],
|
2888
|
+
[0.2, 0.2, 0.2]])
|
2889
|
+
>>> imag
|
2890
|
+
array([[0.3, 0.3, 0.3],
|
2891
|
+
[0.4, nan, 0.4],
|
2892
|
+
[0.4, 0.4, 0.4]])
|
2717
2893
|
|
2718
2894
|
"""
|
2719
|
-
|
2720
|
-
|
2721
|
-
|
2722
|
-
|
2723
|
-
|
2724
|
-
|
2725
|
-
|
2726
|
-
imag = numpy.asarray(imag)
|
2895
|
+
if repeat < 0:
|
2896
|
+
raise ValueError(f'{repeat=} < 0')
|
2897
|
+
if size < 1:
|
2898
|
+
raise ValueError(f'{size=} < 1')
|
2899
|
+
if size == 1:
|
2900
|
+
# no need to filter
|
2901
|
+
repeat = 0
|
2727
2902
|
|
2903
|
+
mean = numpy.asarray(mean)
|
2904
|
+
if use_scipy or repeat == 0: # or using nD numpy filter
|
2905
|
+
real = numpy.asarray(real)
|
2906
|
+
elif isinstance(real, numpy.ndarray) and real.dtype == numpy.float32:
|
2907
|
+
real = real.copy()
|
2908
|
+
else:
|
2909
|
+
real = numpy.array(real, numpy.float64, copy=True)
|
2910
|
+
if use_scipy or repeat == 0: # or using nD numpy filter
|
2911
|
+
imag = numpy.asarray(imag)
|
2912
|
+
elif isinstance(imag, numpy.ndarray) and imag.dtype == numpy.float32:
|
2913
|
+
imag = imag.copy()
|
2914
|
+
else:
|
2915
|
+
imag = numpy.array(imag, numpy.float64, copy=True)
|
2916
|
+
|
2917
|
+
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
2918
|
+
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
2728
2919
|
if real.shape != imag.shape:
|
2729
2920
|
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
2730
|
-
if repeat < 1:
|
2731
|
-
raise ValueError(f'{repeat=} < 1')
|
2732
2921
|
|
2733
|
-
|
2922
|
+
prepend_axis = mean.ndim + 1 == real.ndim
|
2923
|
+
_, axes = _parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
|
2924
|
+
|
2925
|
+
# in case mean is also filtered
|
2926
|
+
# if prepend_axis:
|
2927
|
+
# mean = numpy.expand_dims(mean, axis=0)
|
2928
|
+
# ...
|
2929
|
+
# if prepend_axis:
|
2930
|
+
# mean = numpy.asarray(mean[0])
|
2931
|
+
|
2932
|
+
if repeat == 0:
|
2933
|
+
# no need to call filter
|
2934
|
+
return mean, real, imag
|
2935
|
+
|
2936
|
+
if use_scipy:
|
2937
|
+
# use scipy NaN-unaware fallback
|
2938
|
+
from scipy.ndimage import median_filter
|
2939
|
+
|
2940
|
+
kwargs.pop('axes', None)
|
2941
|
+
|
2942
|
+
for _ in range(repeat):
|
2943
|
+
real = median_filter(real, size=size, axes=axes, **kwargs)
|
2944
|
+
imag = median_filter(imag, size=size, axes=axes, **kwargs)
|
2945
|
+
|
2946
|
+
return mean, numpy.asarray(real), numpy.asarray(imag)
|
2947
|
+
|
2948
|
+
if len(axes) != 2:
|
2949
|
+
# n-dimensional median filter using numpy
|
2950
|
+
from numpy.lib.stride_tricks import sliding_window_view
|
2951
|
+
|
2952
|
+
kernel_shape = tuple(
|
2953
|
+
size if i in axes else 1 for i in range(real.ndim)
|
2954
|
+
)
|
2955
|
+
pad_width = [
|
2956
|
+
(s // 2, s // 2) if s > 1 else (0, 0) for s in kernel_shape
|
2957
|
+
]
|
2958
|
+
axis = tuple(range(-real.ndim, 0))
|
2959
|
+
|
2960
|
+
nan_mask = numpy.isnan(real)
|
2961
|
+
for _ in range(repeat):
|
2962
|
+
real = numpy.pad(real, pad_width, mode='edge')
|
2963
|
+
real = sliding_window_view(real, kernel_shape)
|
2964
|
+
real = numpy.nanmedian(real, axis=axis)
|
2965
|
+
real = numpy.where(nan_mask, numpy.nan, real)
|
2966
|
+
|
2967
|
+
nan_mask = numpy.isnan(imag)
|
2968
|
+
for _ in range(repeat):
|
2969
|
+
imag = numpy.pad(imag, pad_width, mode='edge')
|
2970
|
+
imag = sliding_window_view(imag, kernel_shape)
|
2971
|
+
imag = numpy.nanmedian(imag, axis=axis)
|
2972
|
+
imag = numpy.where(nan_mask, numpy.nan, imag)
|
2973
|
+
|
2974
|
+
return mean, real, imag
|
2975
|
+
|
2976
|
+
# 2-dimensional median filter using optimized Cython implementation
|
2977
|
+
num_threads = number_threads(num_threads)
|
2978
|
+
|
2979
|
+
buffer = numpy.empty(
|
2980
|
+
tuple(real.shape[axis] for axis in axes), dtype=real.dtype
|
2981
|
+
)
|
2982
|
+
|
2983
|
+
for index in numpy.ndindex(
|
2984
|
+
*[real.shape[ax] for ax in range(real.ndim) if ax not in axes]
|
2985
|
+
):
|
2986
|
+
index_list: list[int | slice] = list(index)
|
2987
|
+
for ax in axes:
|
2988
|
+
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
2989
|
+
full_index = tuple(index_list)
|
2990
|
+
|
2991
|
+
_median_filter_2d(real[full_index], buffer, size, repeat, num_threads)
|
2992
|
+
_median_filter_2d(imag[full_index], buffer, size, repeat, num_threads)
|
2993
|
+
|
2994
|
+
return mean, real, imag
|
2734
2995
|
|
2735
2996
|
|
2736
2997
|
def phasor_threshold(
|
@@ -2750,6 +3011,7 @@ def phasor_threshold(
|
|
2750
3011
|
modulation_min: ArrayLike | None = None,
|
2751
3012
|
modulation_max: ArrayLike | None = None,
|
2752
3013
|
open_interval: bool = False,
|
3014
|
+
detect_harmonics: bool = True,
|
2753
3015
|
**kwargs: Any,
|
2754
3016
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2755
3017
|
"""Return phasor coordinates with values out of interval replaced by NaN.
|
@@ -2759,11 +3021,13 @@ def phasor_threshold(
|
|
2759
3021
|
Phasor coordinates smaller than minimum thresholds or larger than maximum
|
2760
3022
|
thresholds are replaced NaN.
|
2761
3023
|
No threshold is applied by default.
|
3024
|
+
NaNs in `mean` or any `real` and `imag` harmonic are propagated to
|
3025
|
+
`mean` and all harmonics in `real` and `imag`.
|
2762
3026
|
|
2763
3027
|
Parameters
|
2764
3028
|
----------
|
2765
3029
|
mean : array_like
|
2766
|
-
|
3030
|
+
Intensity of phasor coordinates.
|
2767
3031
|
real : array_like
|
2768
3032
|
Real component of phasor coordinates.
|
2769
3033
|
imag : array_like
|
@@ -2791,8 +3055,12 @@ def phasor_threshold(
|
|
2791
3055
|
open_interval : bool, optional
|
2792
3056
|
If true, the interval is open, and the threshold values are
|
2793
3057
|
not included in the interval.
|
2794
|
-
If
|
2795
|
-
included in the interval.
|
3058
|
+
If false (default), the interval is closed, and the threshold values
|
3059
|
+
are included in the interval.
|
3060
|
+
detect_harmonics : bool, optional
|
3061
|
+
By default, detect presence of multiple harmonics from array shapes.
|
3062
|
+
If false, no harmonics are assumed to be present, and the function
|
3063
|
+
behaves like a numpy universal function.
|
2796
3064
|
**kwargs
|
2797
3065
|
Optional `arguments passed to numpy universal functions
|
2798
3066
|
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
@@ -2800,7 +3068,7 @@ def phasor_threshold(
|
|
2800
3068
|
Returns
|
2801
3069
|
-------
|
2802
3070
|
mean : ndarray
|
2803
|
-
Thresholded
|
3071
|
+
Thresholded intensity of phasor coordinates.
|
2804
3072
|
real : ndarray
|
2805
3073
|
Thresholded real component of phasor coordinates.
|
2806
3074
|
imag : ndarray
|
@@ -2875,96 +3143,144 @@ def phasor_threshold(
|
|
2875
3143
|
else:
|
2876
3144
|
threshold_mean_only = False
|
2877
3145
|
|
2878
|
-
if
|
2879
|
-
|
2880
|
-
|
3146
|
+
if detect_harmonics:
|
3147
|
+
mean = numpy.asarray(mean)
|
3148
|
+
real = numpy.asarray(real)
|
3149
|
+
imag = numpy.asarray(imag)
|
3150
|
+
|
3151
|
+
shape = numpy.broadcast_shapes(mean.shape, real.shape, imag.shape)
|
3152
|
+
ndim = len(shape)
|
3153
|
+
|
3154
|
+
has_harmonic_axis = (
|
3155
|
+
# detect multi-harmonic in axis 0
|
3156
|
+
mean.ndim + 1 == ndim
|
3157
|
+
and real.shape == shape
|
3158
|
+
and imag.shape == shape
|
3159
|
+
and mean.shape == shape[-mean.ndim if mean.ndim else 1 :]
|
2881
3160
|
)
|
3161
|
+
else:
|
3162
|
+
has_harmonic_axis = False
|
2882
3163
|
|
2883
|
-
if threshold_mean_only:
|
3164
|
+
if threshold_mean_only is None:
|
3165
|
+
mean, real, imag = _phasor_threshold_nan(mean, real, imag, **kwargs)
|
3166
|
+
|
3167
|
+
elif threshold_mean_only:
|
2884
3168
|
mean_func = (
|
2885
3169
|
_phasor_threshold_mean_open
|
2886
3170
|
if open_interval
|
2887
3171
|
else _phasor_threshold_mean_closed
|
2888
3172
|
)
|
2889
|
-
|
3173
|
+
mean, real, imag = mean_func(
|
2890
3174
|
mean, real, imag, mean_min, mean_max, **kwargs
|
2891
3175
|
)
|
2892
3176
|
|
2893
|
-
|
2894
|
-
|
2895
|
-
|
2896
|
-
|
2897
|
-
|
2898
|
-
|
2899
|
-
imag
|
2900
|
-
|
2901
|
-
|
2902
|
-
|
2903
|
-
|
2904
|
-
|
2905
|
-
|
2906
|
-
|
2907
|
-
|
2908
|
-
|
2909
|
-
|
2910
|
-
|
2911
|
-
|
3177
|
+
else:
|
3178
|
+
func = (
|
3179
|
+
_phasor_threshold_open
|
3180
|
+
if open_interval
|
3181
|
+
else _phasor_threshold_closed
|
3182
|
+
)
|
3183
|
+
mean, real, imag = func(
|
3184
|
+
mean,
|
3185
|
+
real,
|
3186
|
+
imag,
|
3187
|
+
mean_min,
|
3188
|
+
mean_max,
|
3189
|
+
real_min,
|
3190
|
+
real_max,
|
3191
|
+
imag_min,
|
3192
|
+
imag_max,
|
3193
|
+
phase_min,
|
3194
|
+
phase_max,
|
3195
|
+
modulation_min,
|
3196
|
+
modulation_max,
|
3197
|
+
**kwargs,
|
3198
|
+
)
|
3199
|
+
|
3200
|
+
mean = numpy.asarray(mean)
|
3201
|
+
real = numpy.asarray(real)
|
3202
|
+
imag = numpy.asarray(imag)
|
3203
|
+
if has_harmonic_axis and mean.ndim > 0:
|
3204
|
+
# propagate NaN to all dimensions
|
3205
|
+
mean = numpy.mean(mean, axis=0, keepdims=True)
|
3206
|
+
mask = numpy.where(numpy.isnan(mean), numpy.nan, 1.0)
|
3207
|
+
numpy.multiply(real, mask, out=real)
|
3208
|
+
numpy.multiply(imag, mask, out=imag)
|
3209
|
+
# remove harmonic dimension created by broadcasting
|
3210
|
+
mean = numpy.asarray(numpy.asarray(mean)[0])
|
3211
|
+
|
3212
|
+
return mean, real, imag
|
2912
3213
|
|
2913
3214
|
|
2914
3215
|
def phasor_center(
|
3216
|
+
mean: ArrayLike,
|
2915
3217
|
real: ArrayLike,
|
2916
3218
|
imag: ArrayLike,
|
2917
3219
|
/,
|
2918
3220
|
*,
|
2919
3221
|
skip_axis: int | Sequence[int] | None = None,
|
2920
3222
|
method: Literal['mean', 'median'] = 'mean',
|
3223
|
+
nan_safe: bool = True,
|
2921
3224
|
**kwargs: Any,
|
2922
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
3225
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2923
3226
|
"""Return center of phasor coordinates.
|
2924
3227
|
|
2925
3228
|
Parameters
|
2926
3229
|
----------
|
3230
|
+
mean : array_like
|
3231
|
+
Intensity of phasor coordinates.
|
2927
3232
|
real : array_like
|
2928
3233
|
Real component of phasor coordinates.
|
2929
3234
|
imag : array_like
|
2930
3235
|
Imaginary component of phasor coordinates.
|
2931
3236
|
skip_axis : int or sequence of int, optional
|
2932
|
-
Axes to
|
2933
|
-
axes are
|
3237
|
+
Axes in `mean` to excluded from center calculation.
|
3238
|
+
By default, all axes except harmonics are included.
|
2934
3239
|
method : str, optional
|
2935
3240
|
Method used for center calculation:
|
2936
3241
|
|
2937
3242
|
- ``'mean'``: Arithmetic mean of phasor coordinates.
|
2938
3243
|
- ``'median'``: Spatial median of phasor coordinates.
|
2939
3244
|
|
3245
|
+
nan_safe : bool, optional
|
3246
|
+
Ensure `method` is applied to same elements of input arrays.
|
3247
|
+
By default, distribute NaNs among input arrays before applying
|
3248
|
+
`method`. May be disabled if phasor coordinates were filtered by
|
3249
|
+
:py:func:`phasor_threshold`.
|
2940
3250
|
**kwargs
|
2941
3251
|
Optional arguments passed to :py:func:`numpy.nanmean` or
|
2942
3252
|
:py:func:`numpy.nanmedian`.
|
2943
3253
|
|
2944
3254
|
Returns
|
2945
3255
|
-------
|
3256
|
+
mean_center : ndarray
|
3257
|
+
Intensity center coordinates.
|
2946
3258
|
real_center : ndarray
|
2947
|
-
Real center coordinates
|
3259
|
+
Real center coordinates.
|
2948
3260
|
imag_center : ndarray
|
2949
|
-
Imaginary center coordinates
|
3261
|
+
Imaginary center coordinates.
|
2950
3262
|
|
2951
3263
|
Raises
|
2952
3264
|
------
|
2953
3265
|
ValueError
|
2954
3266
|
If the specified method is not supported.
|
2955
|
-
If the shapes of
|
3267
|
+
If the shapes of `mean`, `real`, and `imag` do not match.
|
2956
3268
|
|
2957
3269
|
Examples
|
2958
3270
|
--------
|
2959
|
-
Compute center coordinates with the 'mean' method:
|
3271
|
+
Compute center coordinates with the default 'mean' method:
|
2960
3272
|
|
2961
|
-
>>> phasor_center(
|
2962
|
-
|
3273
|
+
>>> phasor_center(
|
3274
|
+
... [2, 1, 2], [0.1, 0.2, 0.3], [0.4, 0.5, 0.6]
|
3275
|
+
... ) # doctest: +NUMBER
|
3276
|
+
(1.67, 0.2, 0.5)
|
2963
3277
|
|
2964
3278
|
Compute center coordinates with the 'median' method:
|
2965
3279
|
|
2966
|
-
>>> phasor_center(
|
2967
|
-
|
3280
|
+
>>> phasor_center(
|
3281
|
+
... [1, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], method='median'
|
3282
|
+
... )
|
3283
|
+
(2.0, 0.2, 0.5)
|
2968
3284
|
|
2969
3285
|
"""
|
2970
3286
|
methods = {
|
@@ -2973,124 +3289,70 @@ def phasor_center(
|
|
2973
3289
|
}
|
2974
3290
|
if method not in methods:
|
2975
3291
|
raise ValueError(
|
2976
|
-
f
|
3292
|
+
f'Method not supported, supported methods are: '
|
2977
3293
|
f"{', '.join(methods)}"
|
2978
3294
|
)
|
3295
|
+
|
3296
|
+
mean = numpy.asarray(mean)
|
2979
3297
|
real = numpy.asarray(real)
|
2980
3298
|
imag = numpy.asarray(imag)
|
2981
3299
|
if real.shape != imag.shape:
|
2982
3300
|
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
3301
|
+
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
3302
|
+
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
2983
3303
|
|
2984
|
-
|
3304
|
+
prepend_axis = mean.ndim + 1 == real.ndim
|
3305
|
+
_, axis = _parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
|
3306
|
+
if prepend_axis:
|
3307
|
+
mean = numpy.expand_dims(mean, axis=0)
|
2985
3308
|
|
2986
|
-
|
3309
|
+
if nan_safe:
|
3310
|
+
mean, real, imag = phasor_threshold(mean, real, imag)
|
2987
3311
|
|
3312
|
+
mean, real, imag = methods[method](mean, real, imag, axis=axis, **kwargs)
|
2988
3313
|
|
2989
|
-
|
2990
|
-
|
2991
|
-
|
2992
|
-
"""Return the mean center of phasor coordinates.
|
3314
|
+
if prepend_axis:
|
3315
|
+
mean = numpy.asarray(mean[0])
|
3316
|
+
return mean, real, imag
|
2993
3317
|
|
2994
|
-
Parameters
|
2995
|
-
----------
|
2996
|
-
real : ndarray
|
2997
|
-
Real components of phasor coordinates.
|
2998
|
-
imag : ndarray
|
2999
|
-
Imaginary components of phasor coordinates.
|
3000
|
-
**kwargs
|
3001
|
-
Optional arguments passed to :py:func:`numpy.nanmean`.
|
3002
3318
|
|
3003
|
-
|
3004
|
-
|
3005
|
-
|
3006
|
-
|
3007
|
-
|
3008
|
-
|
3009
|
-
|
3010
|
-
|
3011
|
-
|
3012
|
-
|
3013
|
-
(
|
3014
|
-
|
3015
|
-
|
3016
|
-
|
3319
|
+
def _mean(
|
3320
|
+
mean: NDArray[Any],
|
3321
|
+
real: NDArray[Any],
|
3322
|
+
imag: NDArray[Any],
|
3323
|
+
/,
|
3324
|
+
**kwargs: Any,
|
3325
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
3326
|
+
"""Return mean center of phasor coordinates."""
|
3327
|
+
real = numpy.nanmean(real * mean, **kwargs)
|
3328
|
+
imag = numpy.nanmean(imag * mean, **kwargs)
|
3329
|
+
mean = numpy.nanmean(mean, **kwargs)
|
3330
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
3331
|
+
real /= mean
|
3332
|
+
imag /= mean
|
3333
|
+
return mean, real, imag
|
3017
3334
|
|
3018
3335
|
|
3019
3336
|
def _median(
|
3020
|
-
|
3021
|
-
|
3022
|
-
|
3023
|
-
|
3024
|
-
Parameters
|
3025
|
-
----------
|
3026
|
-
real : ndarray
|
3027
|
-
Real components of the phasor coordinates.
|
3028
|
-
imag : ndarray
|
3029
|
-
Imaginary components of the phasor coordinates.
|
3030
|
-
**kwargs
|
3031
|
-
Optional arguments passed to :py:func:`numpy.nanmedian`.
|
3032
|
-
|
3033
|
-
Returns
|
3034
|
-
-------
|
3035
|
-
real_center : ndarray
|
3036
|
-
Spatial median center for real coordinates.
|
3037
|
-
imag_center : ndarray
|
3038
|
-
Spatial median center for imaginary coordinates.
|
3039
|
-
|
3040
|
-
Examples
|
3041
|
-
--------
|
3042
|
-
>>> _median([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])
|
3043
|
-
(2.0, 5.0)
|
3044
|
-
|
3045
|
-
"""
|
3046
|
-
return numpy.nanmedian(real, **kwargs), numpy.nanmedian(imag, **kwargs)
|
3047
|
-
|
3048
|
-
|
3049
|
-
def _median_filter(
|
3050
|
-
real: ArrayLike,
|
3051
|
-
imag: ArrayLike,
|
3052
|
-
repeat: int = 1,
|
3053
|
-
size: int | tuple[int] | None = 3,
|
3337
|
+
mean: NDArray[Any],
|
3338
|
+
real: NDArray[Any],
|
3339
|
+
imag: NDArray[Any],
|
3340
|
+
/,
|
3054
3341
|
**kwargs: Any,
|
3055
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
3056
|
-
"""Return
|
3057
|
-
|
3058
|
-
|
3059
|
-
|
3060
|
-
|
3061
|
-
|
3062
|
-
real : ndarray
|
3063
|
-
Real components of the phasor coordinates.
|
3064
|
-
imag : ndarray
|
3065
|
-
Imaginary components of the phasor coordinates.
|
3066
|
-
repeat : int, optional
|
3067
|
-
Number of times to apply filter. The default is 1.
|
3068
|
-
size : int or tuple of int, optional
|
3069
|
-
The size of the median filter kernel. Default is 3.
|
3070
|
-
**kwargs
|
3071
|
-
Optional arguments passed to :py:func:`scipy.ndimage.median_filter`.
|
3072
|
-
|
3073
|
-
Returns
|
3074
|
-
-------
|
3075
|
-
real : ndarray
|
3076
|
-
Filtered real component of phasor coordinates.
|
3077
|
-
imag : ndarray
|
3078
|
-
Filtered imaginary component of phasor coordinates.
|
3079
|
-
|
3080
|
-
"""
|
3081
|
-
from scipy.ndimage import median_filter
|
3082
|
-
|
3083
|
-
for _ in range(repeat):
|
3084
|
-
real = median_filter(real, size=size, **kwargs)
|
3085
|
-
imag = median_filter(imag, size=size, **kwargs)
|
3086
|
-
|
3087
|
-
return numpy.asarray(real), numpy.asarray(imag)
|
3342
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
3343
|
+
"""Return spatial median center of phasor coordinates."""
|
3344
|
+
return (
|
3345
|
+
numpy.nanmedian(mean, **kwargs),
|
3346
|
+
numpy.nanmedian(real, **kwargs),
|
3347
|
+
numpy.nanmedian(imag, **kwargs),
|
3348
|
+
)
|
3088
3349
|
|
3089
3350
|
|
3090
3351
|
def _parse_skip_axis(
|
3091
3352
|
skip_axis: int | Sequence[int] | None,
|
3092
3353
|
/,
|
3093
3354
|
ndim: int,
|
3355
|
+
prepend_axis: bool = False,
|
3094
3356
|
) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
3095
3357
|
"""Return axes to skip and not to skip.
|
3096
3358
|
|
@@ -3099,10 +3361,12 @@ def _parse_skip_axis(
|
|
3099
3361
|
|
3100
3362
|
Parameters
|
3101
3363
|
----------
|
3102
|
-
skip_axis :
|
3364
|
+
skip_axis : int or sequence of int, optional
|
3103
3365
|
Axes to skip. If None, no axes are skipped.
|
3104
3366
|
ndim : int
|
3105
3367
|
Dimensionality of array in which to skip axes.
|
3368
|
+
prepend_axis : bool, optional
|
3369
|
+
Prepend one dimension and include in `skip_axis`.
|
3106
3370
|
|
3107
3371
|
Returns
|
3108
3372
|
-------
|
@@ -3121,15 +3385,23 @@ def _parse_skip_axis(
|
|
3121
3385
|
>>> _parse_skip_axis((1, -2), 5)
|
3122
3386
|
((1, 3), (0, 2, 4))
|
3123
3387
|
|
3388
|
+
>>> _parse_skip_axis((1, -2), 5, True)
|
3389
|
+
((0, 2, 4), (1, 3, 5))
|
3390
|
+
|
3124
3391
|
"""
|
3125
3392
|
if ndim < 0:
|
3126
3393
|
raise ValueError(f'invalid {ndim=}')
|
3127
3394
|
if skip_axis is None:
|
3395
|
+
if prepend_axis:
|
3396
|
+
return (0,), tuple(range(1, ndim + 1))
|
3128
3397
|
return (), tuple(range(ndim))
|
3129
3398
|
if not isinstance(skip_axis, Sequence):
|
3130
3399
|
skip_axis = (skip_axis,)
|
3131
3400
|
if any(i >= ndim or i < -ndim for i in skip_axis):
|
3132
|
-
raise IndexError(f
|
3133
|
-
skip_axis =
|
3401
|
+
raise IndexError(f'skip_axis={skip_axis} out of range for {ndim=}')
|
3402
|
+
skip_axis = sorted(int(i % ndim) for i in skip_axis)
|
3403
|
+
if prepend_axis:
|
3404
|
+
skip_axis = [0] + [i + 1 for i in skip_axis]
|
3405
|
+
ndim += 1
|
3134
3406
|
other_axis = tuple(i for i in range(ndim) if i not in skip_axis)
|
3135
|
-
return skip_axis, other_axis
|
3407
|
+
return tuple(skip_axis), other_axis
|