phasorpy 0.2__cp313-cp313-win_amd64.whl → 0.4__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/_io.py +2431 -0
- phasorpy/_phasorpy.cp313-win_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +124 -37
- phasorpy/_utils.py +77 -12
- phasorpy/color.py +1 -2
- phasorpy/datasets.py +81 -0
- phasorpy/io.py +4 -1668
- phasorpy/phasor.py +480 -372
- phasorpy/plot.py +31 -11
- phasorpy/version.py +2 -1
- {phasorpy-0.2.dist-info → phasorpy-0.4.dist-info}/LICENSE.txt +1 -1
- {phasorpy-0.2.dist-info → phasorpy-0.4.dist-info}/METADATA +4 -3
- phasorpy-0.4.dist-info/RECORD +25 -0
- {phasorpy-0.2.dist-info → phasorpy-0.4.dist-info}/WHEEL +1 -1
- phasorpy-0.2.dist-info/RECORD +0 -24
- {phasorpy-0.2.dist-info → phasorpy-0.4.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.2.dist-info → phasorpy-0.4.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',
|
@@ -147,7 +149,7 @@ from ._phasorpy import (
|
|
147
149
|
_polar_from_single_lifetime,
|
148
150
|
_polar_to_apparent_lifetime,
|
149
151
|
)
|
150
|
-
from ._utils import parse_harmonic
|
152
|
+
from ._utils import parse_harmonic, parse_signal_axis
|
151
153
|
from .utils import number_threads
|
152
154
|
|
153
155
|
|
@@ -155,12 +157,13 @@ def phasor_from_signal(
|
|
155
157
|
signal: ArrayLike,
|
156
158
|
/,
|
157
159
|
*,
|
158
|
-
axis: int =
|
160
|
+
axis: int | str | None = None,
|
159
161
|
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
160
162
|
sample_phase: ArrayLike | None = None,
|
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.
|
@@ -171,9 +174,10 @@ def phasor_from_signal(
|
|
171
174
|
Frequency-domain, time-domain, or hyperspectral data.
|
172
175
|
A minimum of three samples are required along `axis`.
|
173
176
|
The samples must be uniformly spaced.
|
174
|
-
axis : int, optional
|
177
|
+
axis : int or str, optional
|
175
178
|
Axis over which to compute phasor coordinates.
|
176
|
-
|
179
|
+
By default, the 'H' or 'C' axes if signal contains such dimension
|
180
|
+
names, else the last axis (-1).
|
177
181
|
harmonic : int, sequence of int, or 'all', optional
|
178
182
|
Harmonics to return.
|
179
183
|
If `'all'`, return all harmonics for `signal` samples along `axis`.
|
@@ -201,6 +205,14 @@ def phasor_from_signal(
|
|
201
205
|
dtype : dtype_like, optional
|
202
206
|
Data type of output arrays. Either float32 or float64.
|
203
207
|
The default is float64 unless the `signal` is float32.
|
208
|
+
normalize : bool, optional
|
209
|
+
Return normalized phasor coordinates.
|
210
|
+
If true (default), return average of `signal` along `axis` and
|
211
|
+
Fourier coefficients divided by sum of `signal` along `axis`.
|
212
|
+
Else, return sum of `signal` along `axis` and unscaled Fourier
|
213
|
+
coefficients.
|
214
|
+
Un-normalized phasor coordinates cannot be used with most of PhasorPy's
|
215
|
+
functions but may be required for intermediate processing.
|
204
216
|
num_threads : int, optional
|
205
217
|
Number of OpenMP threads to use for parallelization when not using FFT.
|
206
218
|
By default, multi-threading is disabled.
|
@@ -231,13 +243,15 @@ def phasor_from_signal(
|
|
231
243
|
See Also
|
232
244
|
--------
|
233
245
|
phasorpy.phasor.phasor_to_signal
|
246
|
+
phasorpy.phasor.phasor_normalize
|
234
247
|
:ref:`sphx_glr_tutorials_benchmarks_phasorpy_phasor_from_signal.py`
|
235
248
|
|
236
249
|
Notes
|
237
250
|
-----
|
238
|
-
The phasor coordinates `real` (:math:`G`), `imag` (:math:`S`),
|
239
|
-
`mean` (:math:`F_{DC}`) are calculated from
|
240
|
-
signal :math:`F` af `harmonic` :math:`h`
|
251
|
+
The normalized phasor coordinates `real` (:math:`G`), `imag` (:math:`S`),
|
252
|
+
and average intensity `mean` (:math:`F_{DC}`) are calculated from
|
253
|
+
:math:`K\ge3` samples of the signal :math:`F` af `harmonic` :math:`h`
|
254
|
+
according to:
|
241
255
|
|
242
256
|
.. math::
|
243
257
|
|
@@ -274,6 +288,9 @@ def phasor_from_signal(
|
|
274
288
|
"""
|
275
289
|
# TODO: C-order not required by rfft?
|
276
290
|
# TODO: preserve array subtypes?
|
291
|
+
|
292
|
+
axis, _ = parse_signal_axis(signal, axis)
|
293
|
+
|
277
294
|
signal = numpy.asarray(signal, order='C')
|
278
295
|
if signal.dtype.kind not in 'uif':
|
279
296
|
raise TypeError(f'signal must be real valued, not {signal.dtype=}')
|
@@ -312,7 +329,9 @@ def phasor_from_signal(
|
|
312
329
|
if rfft is None:
|
313
330
|
rfft = numpy.fft.rfft
|
314
331
|
|
315
|
-
fft: NDArray[Any] = rfft(
|
332
|
+
fft: NDArray[Any] = rfft(
|
333
|
+
signal, axis=axis, norm='forward' if normalize else 'backward'
|
334
|
+
)
|
316
335
|
|
317
336
|
mean = fft.take(0, axis=axis).real
|
318
337
|
if not mean.ndim == 0:
|
@@ -332,10 +351,10 @@ def phasor_from_signal(
|
|
332
351
|
real = numpy.moveaxis(real, axis, 0)
|
333
352
|
imag = numpy.moveaxis(imag, axis, 0)
|
334
353
|
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
354
|
+
if normalize:
|
355
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
356
|
+
real /= dc
|
357
|
+
imag /= dc
|
339
358
|
numpy.negative(imag, out=imag)
|
340
359
|
|
341
360
|
if not keepdims and real.ndim == 0:
|
@@ -369,7 +388,7 @@ def phasor_from_signal(
|
|
369
388
|
phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype)
|
370
389
|
signal = signal.reshape((size0, samples, size1))
|
371
390
|
|
372
|
-
_phasor_from_signal(phasor, signal, sincos, num_threads)
|
391
|
+
_phasor_from_signal(phasor, signal, sincos, normalize, num_threads)
|
373
392
|
|
374
393
|
# restore original shape
|
375
394
|
shape = shape0 + shape1
|
@@ -400,7 +419,7 @@ def phasor_to_signal(
|
|
400
419
|
----------
|
401
420
|
mean : array_like
|
402
421
|
Average signal intensity (DC).
|
403
|
-
If not scalar, shape must match the last
|
422
|
+
If not scalar, shape must match the last dimensions of `real`.
|
404
423
|
real : array_like
|
405
424
|
Real component of phasor coordinates.
|
406
425
|
Multiple harmonics, if any, must be in the first axis.
|
@@ -507,7 +526,6 @@ def phasor_to_signal(
|
|
507
526
|
if len(harmonic) != real.shape[0]:
|
508
527
|
raise ValueError(f'{len(harmonic)=} != {real.shape[0]=}')
|
509
528
|
|
510
|
-
# complex multiplication by mean signal
|
511
529
|
real *= mean
|
512
530
|
imag *= mean
|
513
531
|
numpy.negative(imag, out=imag)
|
@@ -952,9 +970,103 @@ def phasor_divide(
|
|
952
970
|
)
|
953
971
|
|
954
972
|
|
973
|
+
def phasor_normalize(
|
974
|
+
mean_unnormalized: ArrayLike,
|
975
|
+
real_unnormalized: ArrayLike,
|
976
|
+
imag_unnormalized: ArrayLike,
|
977
|
+
/,
|
978
|
+
samples: int = 1,
|
979
|
+
dtype: DTypeLike = None,
|
980
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
981
|
+
r"""Return normalized phasor coordinates.
|
982
|
+
|
983
|
+
Use to normalize the phasor coordinates returned by
|
984
|
+
``phasor_from_signal(..., normalize=False)``.
|
985
|
+
|
986
|
+
Parameters
|
987
|
+
----------
|
988
|
+
mean_unnormalized : array_like
|
989
|
+
Unnormalized intensity of phasor coordinates.
|
990
|
+
real_unnormalized : array_like
|
991
|
+
Unnormalized real component of phasor coordinates.
|
992
|
+
imag_unnormalized : array_like
|
993
|
+
Unnormalized imaginary component of phasor coordinates.
|
994
|
+
samples : int, default: 1
|
995
|
+
Number of signal samples over which `mean` was integrated.
|
996
|
+
dtype : dtype_like, optional
|
997
|
+
Data type of output arrays. Either float32 or float64.
|
998
|
+
The default is float64 unless the `real` is float32.
|
999
|
+
|
1000
|
+
Returns
|
1001
|
+
-------
|
1002
|
+
mean : ndarray
|
1003
|
+
Normalized intensity.
|
1004
|
+
real : ndarray
|
1005
|
+
Normalized real component.
|
1006
|
+
imag : ndarray
|
1007
|
+
Normalized imaginary component.
|
1008
|
+
|
1009
|
+
Notes
|
1010
|
+
-----
|
1011
|
+
The average intensity `mean` (:math:`F_{DC}`) and normalized phasor
|
1012
|
+
coordinates `real` (:math:`G`) and `imag` (:math:`S`) are calculated from
|
1013
|
+
the signal `intensity` (:math:`F`), the number of `samples` (:math:`K`),
|
1014
|
+
`real_unnormalized` (:math:`G'`), and `imag_unnormalized` (:math:`S'`)
|
1015
|
+
according to:
|
1016
|
+
|
1017
|
+
.. math::
|
1018
|
+
|
1019
|
+
F_{DC} &= F / K
|
1020
|
+
|
1021
|
+
G &= G' / F
|
1022
|
+
|
1023
|
+
S &= S' / F
|
1024
|
+
|
1025
|
+
If :math:`F = 0`, the normalized phasor coordinates (:math:`G`)
|
1026
|
+
and (:math:`S`) are undefined (:math:`NaN` or :math:`\infty`).
|
1027
|
+
|
1028
|
+
Examples
|
1029
|
+
--------
|
1030
|
+
Normalize phasor coordinates with intensity integrated over 10 samples:
|
1031
|
+
|
1032
|
+
>>> phasor_normalize([0.0, 0.1], [0.0, 0.3], [0.4, 0.5], samples=10)
|
1033
|
+
(array([0, 0.01]), array([nan, 3]), array([inf, 5]))
|
1034
|
+
|
1035
|
+
Normalize multi-harmonic phasor coordinates:
|
1036
|
+
|
1037
|
+
>>> phasor_normalize(0.1, [0.0, 0.3], [0.4, 0.5], samples=10)
|
1038
|
+
(array(0.01), array([0, 3]), array([4, 5]))
|
1039
|
+
|
1040
|
+
"""
|
1041
|
+
if samples < 1:
|
1042
|
+
raise ValueError(f'{samples=} < 1')
|
1043
|
+
|
1044
|
+
if (
|
1045
|
+
dtype is None
|
1046
|
+
and isinstance(real_unnormalized, numpy.ndarray)
|
1047
|
+
and real_unnormalized.dtype == numpy.float32
|
1048
|
+
):
|
1049
|
+
real = real_unnormalized.copy()
|
1050
|
+
else:
|
1051
|
+
real = numpy.array(real_unnormalized, dtype, copy=True)
|
1052
|
+
imag = numpy.array(imag_unnormalized, real.dtype, copy=True)
|
1053
|
+
mean = numpy.array(
|
1054
|
+
mean_unnormalized, real.dtype, copy=None if samples == 1 else True
|
1055
|
+
)
|
1056
|
+
|
1057
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
1058
|
+
numpy.divide(real, mean, out=real)
|
1059
|
+
numpy.divide(imag, mean, out=imag)
|
1060
|
+
if samples > 1:
|
1061
|
+
numpy.divide(mean, samples, out=mean)
|
1062
|
+
|
1063
|
+
return mean, real, imag
|
1064
|
+
|
1065
|
+
|
955
1066
|
def phasor_calibrate(
|
956
1067
|
real: ArrayLike,
|
957
1068
|
imag: ArrayLike,
|
1069
|
+
reference_mean: ArrayLike,
|
958
1070
|
reference_real: ArrayLike,
|
959
1071
|
reference_imag: ArrayLike,
|
960
1072
|
/,
|
@@ -966,11 +1078,11 @@ def phasor_calibrate(
|
|
966
1078
|
fraction: ArrayLike | None = None,
|
967
1079
|
preexponential: bool = False,
|
968
1080
|
unit_conversion: float = 1e-3,
|
969
|
-
reverse: bool = False,
|
970
1081
|
method: Literal['mean', 'median'] = 'mean',
|
1082
|
+
nan_safe: bool = True,
|
1083
|
+
reverse: bool = False,
|
971
1084
|
) -> tuple[NDArray[Any], NDArray[Any]]:
|
972
|
-
"""
|
973
|
-
Return calibrated/referenced phasor coordinates.
|
1085
|
+
"""Return calibrated/referenced phasor coordinates.
|
974
1086
|
|
975
1087
|
Calibration of phasor coordinates from time-resolved measurements is
|
976
1088
|
necessary to account for the instrument response function (IRF) and delays
|
@@ -982,10 +1094,13 @@ def phasor_calibrate(
|
|
982
1094
|
Real component of phasor coordinates to be calibrated.
|
983
1095
|
imag : array_like
|
984
1096
|
Imaginary component of phasor coordinates to be calibrated.
|
1097
|
+
reference_mean : array_like or None
|
1098
|
+
Intensity of phasor coordinates from reference of known lifetime.
|
1099
|
+
Used to re-normalize averaged phasor coordinates.
|
985
1100
|
reference_real : array_like
|
986
1101
|
Real component of phasor coordinates from reference of known lifetime.
|
987
1102
|
Must be measured with the same instrument setting as the phasor
|
988
|
-
coordinates to be calibrated.
|
1103
|
+
coordinates to be calibrated. Dimensions must be the same as `real`.
|
989
1104
|
reference_imag : array_like
|
990
1105
|
Imaginary component of phasor coordinates from reference of known
|
991
1106
|
lifetime.
|
@@ -1005,9 +1120,8 @@ def phasor_calibrate(
|
|
1005
1120
|
harmonics.
|
1006
1121
|
The default is the first harmonic (fundamental frequency).
|
1007
1122
|
skip_axis : int or sequence of int, optional
|
1008
|
-
Axes to
|
1009
|
-
|
1010
|
-
are specified.
|
1123
|
+
Axes in `reference_mean` to exclude from reference center calculation.
|
1124
|
+
By default, all axes except harmonics are included.
|
1011
1125
|
fraction : array_like, optional
|
1012
1126
|
Fractional intensities or pre-exponential amplitudes of the lifetime
|
1013
1127
|
components. Fractions are normalized to sum to 1.
|
@@ -1019,14 +1133,18 @@ def phasor_calibrate(
|
|
1019
1133
|
Product of `frequency` and `lifetime` units' prefix factors.
|
1020
1134
|
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1021
1135
|
Use 1.0 for Hz and s.
|
1022
|
-
reverse : bool, optional
|
1023
|
-
Reverse calibration.
|
1024
1136
|
method : str, optional
|
1025
|
-
Method used for calculating center of
|
1026
|
-
`reference_imag`:
|
1137
|
+
Method used for calculating center of reference phasor coordinates:
|
1027
1138
|
|
1028
|
-
- ``'mean'``: Arithmetic mean
|
1029
|
-
- ``'median'``: Spatial median
|
1139
|
+
- ``'mean'``: Arithmetic mean.
|
1140
|
+
- ``'median'``: Spatial median.
|
1141
|
+
|
1142
|
+
nan_safe : bool, optional
|
1143
|
+
Ensure `method` is applied to same elements of reference arrays.
|
1144
|
+
By default, distribute NaNs among reference arrays before applying
|
1145
|
+
`method`.
|
1146
|
+
reverse : bool, optional
|
1147
|
+
Reverse calibration.
|
1030
1148
|
|
1031
1149
|
Returns
|
1032
1150
|
-------
|
@@ -1060,11 +1178,13 @@ def phasor_calibrate(
|
|
1060
1178
|
imag,
|
1061
1179
|
*polar_from_reference_phasor(
|
1062
1180
|
*phasor_center(
|
1181
|
+
reference_mean,
|
1063
1182
|
reference_real,
|
1064
1183
|
reference_imag,
|
1065
1184
|
skip_axis,
|
1066
1185
|
method,
|
1067
|
-
|
1186
|
+
nan_safe,
|
1187
|
+
)[1:],
|
1068
1188
|
*phasor_from_lifetime(
|
1069
1189
|
frequency,
|
1070
1190
|
lifetime,
|
@@ -1091,6 +1211,7 @@ def phasor_calibrate(
|
|
1091
1211
|
>>> phasor_calibrate(
|
1092
1212
|
... [0.1, 0.2, 0.3],
|
1093
1213
|
... [0.4, 0.5, 0.6],
|
1214
|
+
... [1.0, 1.0, 1.0],
|
1094
1215
|
... [0.2, 0.3, 0.4],
|
1095
1216
|
... [0.5, 0.6, 0.7],
|
1096
1217
|
... frequency=80,
|
@@ -1103,6 +1224,7 @@ def phasor_calibrate(
|
|
1103
1224
|
>>> phasor_calibrate(
|
1104
1225
|
... [0.0658, 0.132, 0.198],
|
1105
1226
|
... [0.2657, 0.332, 0.399],
|
1227
|
+
... [1.0, 1.0, 1.0],
|
1106
1228
|
... [0.2, 0.3, 0.4],
|
1107
1229
|
... [0.5, 0.6, 0.7],
|
1108
1230
|
... frequency=80,
|
@@ -1112,36 +1234,59 @@ def phasor_calibrate(
|
|
1112
1234
|
(array([0.1, 0.2, 0.3]), array([0.4, 0.5, 0.6]))
|
1113
1235
|
|
1114
1236
|
"""
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
if
|
1237
|
+
real = numpy.asarray(real)
|
1238
|
+
imag = numpy.asarray(imag)
|
1239
|
+
reference_mean = numpy.asarray(reference_mean)
|
1240
|
+
reference_real = numpy.asarray(reference_real)
|
1241
|
+
reference_imag = numpy.asarray(reference_imag)
|
1242
|
+
|
1243
|
+
if real.shape != imag.shape:
|
1244
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
1245
|
+
if reference_real.shape != reference_imag.shape:
|
1246
|
+
raise ValueError(f'{reference_real.shape=} != {reference_imag.shape=}')
|
1247
|
+
|
1248
|
+
has_harmonic_axis = reference_mean.ndim + 1 == reference_real.ndim
|
1249
|
+
harmonic, _ = parse_harmonic(
|
1250
|
+
harmonic,
|
1251
|
+
(
|
1252
|
+
reference_real.shape[0]
|
1253
|
+
if has_harmonic_axis
|
1254
|
+
and isinstance(harmonic, str)
|
1255
|
+
and harmonic == 'all'
|
1256
|
+
else None
|
1257
|
+
),
|
1258
|
+
)
|
1259
|
+
|
1260
|
+
if has_harmonic_axis:
|
1261
|
+
if real.ndim == 0:
|
1262
|
+
raise ValueError(f'{real.shape=} != {len(harmonic)=}')
|
1263
|
+
if real.shape[0] != len(harmonic):
|
1264
|
+
raise ValueError(f'{real.shape[0]=} != {len(harmonic)=}')
|
1265
|
+
if reference_real.shape[0] != len(harmonic):
|
1266
|
+
raise ValueError(f'{reference_real.shape[0]=} != {len(harmonic)=}')
|
1267
|
+
if reference_mean.shape != reference_real.shape[1:]:
|
1268
|
+
raise ValueError(
|
1269
|
+
f'{reference_mean.shape=} != {reference_real.shape[1:]=}'
|
1270
|
+
)
|
1271
|
+
elif reference_mean.shape != reference_real.shape:
|
1272
|
+
raise ValueError(f'{reference_mean.shape=} != {reference_real.shape=}')
|
1273
|
+
elif len(harmonic) > 1:
|
1122
1274
|
raise ValueError(
|
1123
|
-
f'
|
1124
|
-
f'!= reference_imag.shape{ref_im.shape}'
|
1275
|
+
f'{reference_mean.shape=} does not have harmonic axis'
|
1125
1276
|
)
|
1126
1277
|
|
1127
|
-
if harmonic == 'all' and re.ndim > 0:
|
1128
|
-
harmonic, has_harmonic_axis = parse_harmonic(harmonic, re.shape[0])
|
1129
|
-
else:
|
1130
|
-
harmonic, has_harmonic_axis = parse_harmonic(harmonic)
|
1131
|
-
if has_harmonic_axis and len(harmonic) != re.shape[0]:
|
1132
|
-
raise ValueError(f'{len(harmonic)=} != real.shape[0]={re.shape[0]}')
|
1133
|
-
|
1134
1278
|
frequency = numpy.asarray(frequency)
|
1135
1279
|
frequency = frequency * harmonic
|
1136
1280
|
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1281
|
+
_, measured_re, measured_im = phasor_center(
|
1282
|
+
reference_mean,
|
1283
|
+
reference_real,
|
1284
|
+
reference_imag,
|
1285
|
+
skip_axis=skip_axis,
|
1286
|
+
method=method,
|
1287
|
+
nan_safe=nan_safe,
|
1144
1288
|
)
|
1289
|
+
|
1145
1290
|
known_re, known_im = phasor_from_lifetime(
|
1146
1291
|
frequency,
|
1147
1292
|
lifetime,
|
@@ -1149,26 +1294,26 @@ def phasor_calibrate(
|
|
1149
1294
|
preexponential=preexponential,
|
1150
1295
|
unit_conversion=unit_conversion,
|
1151
1296
|
)
|
1297
|
+
|
1152
1298
|
phi_zero, mod_zero = polar_from_reference_phasor(
|
1153
1299
|
measured_re, measured_im, known_re, known_im
|
1154
1300
|
)
|
1301
|
+
|
1155
1302
|
if numpy.ndim(phi_zero) > 0:
|
1156
1303
|
if reverse:
|
1157
1304
|
numpy.negative(phi_zero, out=phi_zero)
|
1158
1305
|
numpy.reciprocal(mod_zero, out=mod_zero)
|
1306
|
+
_, axis = _parse_skip_axis(
|
1307
|
+
skip_axis, real.ndim - int(has_harmonic_axis), has_harmonic_axis
|
1308
|
+
)
|
1159
1309
|
if axis is not None:
|
1160
|
-
phi_zero = numpy.expand_dims(
|
1161
|
-
|
1162
|
-
axis=axis,
|
1163
|
-
)
|
1164
|
-
mod_zero = numpy.expand_dims(
|
1165
|
-
mod_zero,
|
1166
|
-
axis=axis,
|
1167
|
-
)
|
1310
|
+
phi_zero = numpy.expand_dims(phi_zero, axis=axis)
|
1311
|
+
mod_zero = numpy.expand_dims(mod_zero, axis=axis)
|
1168
1312
|
elif reverse:
|
1169
1313
|
phi_zero = -phi_zero
|
1170
1314
|
mod_zero = 1.0 / mod_zero
|
1171
|
-
|
1315
|
+
|
1316
|
+
return phasor_transform(real, imag, phi_zero, mod_zero)
|
1172
1317
|
|
1173
1318
|
|
1174
1319
|
def phasor_transform(
|
@@ -1832,6 +1977,7 @@ def lifetime_fraction_from_amplitude(
|
|
1832
1977
|
array([0.8, 0.2])
|
1833
1978
|
|
1834
1979
|
"""
|
1980
|
+
t: NDArray[numpy.float64]
|
1835
1981
|
t = numpy.multiply(amplitude, lifetime, dtype=numpy.float64)
|
1836
1982
|
t /= numpy.sum(t, axis=axis, keepdims=True)
|
1837
1983
|
return t
|
@@ -2668,46 +2814,49 @@ def phasor_to_principal_plane(
|
|
2668
2814
|
)
|
2669
2815
|
|
2670
2816
|
|
2671
|
-
def
|
2817
|
+
def phasor_filter_median(
|
2818
|
+
mean: ArrayLike,
|
2672
2819
|
real: ArrayLike,
|
2673
2820
|
imag: ArrayLike,
|
2674
2821
|
/,
|
2675
2822
|
*,
|
2676
|
-
method: Literal['median', 'median_scipy'] = 'median',
|
2677
2823
|
repeat: int = 1,
|
2678
2824
|
size: int = 3,
|
2679
2825
|
skip_axis: int | Sequence[int] | None = None,
|
2826
|
+
use_scipy: bool = False,
|
2680
2827
|
num_threads: int | None = None,
|
2681
2828
|
**kwargs: Any,
|
2682
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2683
|
-
"""Return filtered phasor coordinates.
|
2829
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2830
|
+
"""Return median-filtered phasor coordinates.
|
2684
2831
|
|
2685
|
-
By default, a median filter
|
2686
|
-
imaginary components of phasor coordinates once with a kernel size of 3
|
2687
|
-
multiplied by the number of dimensions of the input arrays.
|
2832
|
+
By default, apply a NaN-aware median filter independently to the real
|
2833
|
+
and imaginary components of phasor coordinates once with a kernel size of 3
|
2834
|
+
multiplied by the number of dimensions of the input arrays. Return the
|
2835
|
+
intensity unchanged.
|
2688
2836
|
|
2689
2837
|
Parameters
|
2690
2838
|
----------
|
2839
|
+
mean : array_like
|
2840
|
+
Intensity of phasor coordinates.
|
2691
2841
|
real : array_like
|
2692
2842
|
Real component of phasor coordinates to be filtered.
|
2693
2843
|
imag : array_like
|
2694
2844
|
Imaginary component of phasor coordinates to be filtered.
|
2695
|
-
method : str, optional
|
2696
|
-
Method used for filtering:
|
2697
|
-
|
2698
|
-
- ``'median'``: Spatial median of phasor coordinates.
|
2699
|
-
- ``'median_scipy'``: Spatial median of phasor coordinates
|
2700
|
-
based on :py:func:`scipy.ndimage.median_filter`.
|
2701
|
-
|
2702
2845
|
repeat : int, optional
|
2703
|
-
Number of times to apply filter. The default is 1.
|
2846
|
+
Number of times to apply median filter. The default is 1.
|
2704
2847
|
size : int, optional
|
2705
|
-
Size of filter kernel. The default is 3.
|
2848
|
+
Size of median filter kernel. The default is 3.
|
2706
2849
|
skip_axis : int or sequence of int, optional
|
2707
|
-
|
2850
|
+
Axes in `mean` to exclude from filter.
|
2851
|
+
By default, all axes except harmonics are included.
|
2852
|
+
use_scipy : bool, optional
|
2853
|
+
Use :py:func:`scipy.ndimage.median_filter`.
|
2854
|
+
This function has undefined behavior if the input arrays contain
|
2855
|
+
`NaN` values but is faster when filtering more than 2 dimensions.
|
2856
|
+
See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
|
2708
2857
|
num_threads : int, optional
|
2709
2858
|
Number of OpenMP threads to use for parallelization.
|
2710
|
-
Applies to filtering in two dimensions
|
2859
|
+
Applies to filtering in two dimensions when not using scipy.
|
2711
2860
|
By default, multi-threading is disabled.
|
2712
2861
|
If zero, up to half of logical CPUs are used.
|
2713
2862
|
OpenMP may not be available on all platforms.
|
@@ -2716,6 +2865,8 @@ def phasor_filter(
|
|
2716
2865
|
|
2717
2866
|
Returns
|
2718
2867
|
-------
|
2868
|
+
mean : ndarray
|
2869
|
+
Unchanged intensity of phasor coordinates.
|
2719
2870
|
real : ndarray
|
2720
2871
|
Filtered real component of phasor coordinates.
|
2721
2872
|
imag : ndarray
|
@@ -2724,78 +2875,136 @@ def phasor_filter(
|
|
2724
2875
|
Raises
|
2725
2876
|
------
|
2726
2877
|
ValueError
|
2727
|
-
If the specified method is not supported.
|
2728
2878
|
If `repeat` is less than 0.
|
2729
2879
|
If `size` is less than 1.
|
2730
|
-
The array shapes of `real
|
2731
|
-
|
2732
|
-
Notes
|
2733
|
-
-----
|
2734
|
-
Additional filtering methods may be added in the future.
|
2735
|
-
|
2736
|
-
The `median` method ignores `NaN` values. If the kernel contains an even
|
2737
|
-
number of elements, the median is the average of the two middle elements.
|
2738
|
-
|
2739
|
-
The implementation of the `median_scipy` method is based on
|
2740
|
-
:py:func:`scipy.ndimage.median_filter`,
|
2741
|
-
which has undefined behavior if the input arrays contain `NaN` values.
|
2742
|
-
See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
|
2743
|
-
|
2744
|
-
When filtering in more than two dimensions, the `median` method is
|
2745
|
-
slower than the `median_scipy` method. When filtering in two
|
2746
|
-
dimensions, both methods have similar performance.
|
2880
|
+
The array shapes of `mean`, `real`, and `imag` do not match.
|
2747
2881
|
|
2748
2882
|
Examples
|
2749
2883
|
--------
|
2750
2884
|
Apply three times a median filter with a kernel size of three:
|
2751
2885
|
|
2752
|
-
>>>
|
2753
|
-
... [[
|
2754
|
-
... [[
|
2886
|
+
>>> mean, real, imag = phasor_filter_median(
|
2887
|
+
... [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
|
2888
|
+
... [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5], [0.2, 0.2, 0.2]],
|
2889
|
+
... [[0.3, 0.3, 0.3], [0.6, math.nan, 0.6], [0.4, 0.4, 0.4]],
|
2755
2890
|
... size=3,
|
2756
2891
|
... repeat=3,
|
2757
2892
|
... )
|
2758
|
-
|
2759
|
-
|
2760
|
-
|
2761
|
-
|
2762
|
-
|
2763
|
-
|
2893
|
+
>>> mean
|
2894
|
+
array([[1, 2, 3],
|
2895
|
+
[4, 5, 6],
|
2896
|
+
[7, 8, 9]])
|
2897
|
+
>>> real
|
2898
|
+
array([[0, 0, 0],
|
2899
|
+
[0.2, 0.2, 0.2],
|
2900
|
+
[0.2, 0.2, 0.2]])
|
2901
|
+
>>> imag
|
2902
|
+
array([[0.3, 0.3, 0.3],
|
2903
|
+
[0.4, nan, 0.4],
|
2904
|
+
[0.4, 0.4, 0.4]])
|
2764
2905
|
|
2765
2906
|
"""
|
2766
|
-
methods: dict[str, Callable[..., Any]] = {
|
2767
|
-
'median': _median_filter,
|
2768
|
-
'median_scipy': _median_filter_scipy,
|
2769
|
-
}
|
2770
|
-
if method not in methods:
|
2771
|
-
raise ValueError(
|
2772
|
-
f'Method not supported, supported methods are: '
|
2773
|
-
f"{', '.join(methods)}"
|
2774
|
-
)
|
2775
|
-
if repeat == 0 or size == 1:
|
2776
|
-
return numpy.asarray(real), numpy.asarray(imag)
|
2777
2907
|
if repeat < 0:
|
2778
2908
|
raise ValueError(f'{repeat=} < 0')
|
2779
2909
|
if size < 1:
|
2780
2910
|
raise ValueError(f'{size=} < 1')
|
2911
|
+
if size == 1:
|
2912
|
+
# no need to filter
|
2913
|
+
repeat = 0
|
2781
2914
|
|
2782
|
-
|
2783
|
-
|
2915
|
+
mean = numpy.asarray(mean)
|
2916
|
+
if use_scipy or repeat == 0: # or using nD numpy filter
|
2917
|
+
real = numpy.asarray(real)
|
2918
|
+
elif isinstance(real, numpy.ndarray) and real.dtype == numpy.float32:
|
2919
|
+
real = real.copy()
|
2920
|
+
else:
|
2921
|
+
real = numpy.array(real, numpy.float64, copy=True)
|
2922
|
+
if use_scipy or repeat == 0: # or using nD numpy filter
|
2923
|
+
imag = numpy.asarray(imag)
|
2924
|
+
elif isinstance(imag, numpy.ndarray) and imag.dtype == numpy.float32:
|
2925
|
+
imag = imag.copy()
|
2926
|
+
else:
|
2927
|
+
imag = numpy.array(imag, numpy.float64, copy=True)
|
2784
2928
|
|
2929
|
+
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
2930
|
+
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
2785
2931
|
if real.shape != imag.shape:
|
2786
2932
|
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
2787
2933
|
|
2788
|
-
|
2934
|
+
prepend_axis = mean.ndim + 1 == real.ndim
|
2935
|
+
_, axes = _parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
|
2936
|
+
|
2937
|
+
# in case mean is also filtered
|
2938
|
+
# if prepend_axis:
|
2939
|
+
# mean = numpy.expand_dims(mean, axis=0)
|
2940
|
+
# ...
|
2941
|
+
# if prepend_axis:
|
2942
|
+
# mean = numpy.asarray(mean[0])
|
2943
|
+
|
2944
|
+
if repeat == 0:
|
2945
|
+
# no need to call filter
|
2946
|
+
return mean, real, imag
|
2947
|
+
|
2948
|
+
if use_scipy:
|
2949
|
+
# use scipy NaN-unaware fallback
|
2950
|
+
from scipy.ndimage import median_filter
|
2789
2951
|
|
2790
|
-
|
2791
|
-
axes = kwargs.pop('axes')
|
2792
|
-
if method == 'median':
|
2793
|
-
kwargs['num_threads'] = num_threads
|
2952
|
+
kwargs.pop('axes', None)
|
2794
2953
|
|
2795
|
-
|
2796
|
-
|
2954
|
+
for _ in range(repeat):
|
2955
|
+
real = median_filter(real, size=size, axes=axes, **kwargs)
|
2956
|
+
imag = median_filter(imag, size=size, axes=axes, **kwargs)
|
2957
|
+
|
2958
|
+
return mean, numpy.asarray(real), numpy.asarray(imag)
|
2959
|
+
|
2960
|
+
if len(axes) != 2:
|
2961
|
+
# n-dimensional median filter using numpy
|
2962
|
+
from numpy.lib.stride_tricks import sliding_window_view
|
2963
|
+
|
2964
|
+
kernel_shape = tuple(
|
2965
|
+
size if i in axes else 1 for i in range(real.ndim)
|
2966
|
+
)
|
2967
|
+
pad_width = [
|
2968
|
+
(s // 2, s // 2) if s > 1 else (0, 0) for s in kernel_shape
|
2969
|
+
]
|
2970
|
+
axis = tuple(range(-real.ndim, 0))
|
2971
|
+
|
2972
|
+
nan_mask = numpy.isnan(real)
|
2973
|
+
for _ in range(repeat):
|
2974
|
+
real = numpy.pad(real, pad_width, mode='edge')
|
2975
|
+
real = sliding_window_view(real, kernel_shape)
|
2976
|
+
real = numpy.nanmedian(real, axis=axis)
|
2977
|
+
real = numpy.where(nan_mask, numpy.nan, real)
|
2978
|
+
|
2979
|
+
nan_mask = numpy.isnan(imag)
|
2980
|
+
for _ in range(repeat):
|
2981
|
+
imag = numpy.pad(imag, pad_width, mode='edge')
|
2982
|
+
imag = sliding_window_view(imag, kernel_shape)
|
2983
|
+
imag = numpy.nanmedian(imag, axis=axis)
|
2984
|
+
imag = numpy.where(nan_mask, numpy.nan, imag)
|
2985
|
+
|
2986
|
+
return mean, real, imag
|
2987
|
+
|
2988
|
+
# 2-dimensional median filter using optimized Cython implementation
|
2989
|
+
num_threads = number_threads(num_threads)
|
2990
|
+
|
2991
|
+
buffer = numpy.empty(
|
2992
|
+
tuple(real.shape[axis] for axis in axes), dtype=real.dtype
|
2797
2993
|
)
|
2798
2994
|
|
2995
|
+
for index in numpy.ndindex(
|
2996
|
+
*[real.shape[ax] for ax in range(real.ndim) if ax not in axes]
|
2997
|
+
):
|
2998
|
+
index_list: list[int | slice] = list(index)
|
2999
|
+
for ax in axes:
|
3000
|
+
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
3001
|
+
full_index = tuple(index_list)
|
3002
|
+
|
3003
|
+
_median_filter_2d(real[full_index], buffer, size, repeat, num_threads)
|
3004
|
+
_median_filter_2d(imag[full_index], buffer, size, repeat, num_threads)
|
3005
|
+
|
3006
|
+
return mean, real, imag
|
3007
|
+
|
2799
3008
|
|
2800
3009
|
def phasor_threshold(
|
2801
3010
|
mean: ArrayLike,
|
@@ -2814,6 +3023,7 @@ def phasor_threshold(
|
|
2814
3023
|
modulation_min: ArrayLike | None = None,
|
2815
3024
|
modulation_max: ArrayLike | None = None,
|
2816
3025
|
open_interval: bool = False,
|
3026
|
+
detect_harmonics: bool = True,
|
2817
3027
|
**kwargs: Any,
|
2818
3028
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2819
3029
|
"""Return phasor coordinates with values out of interval replaced by NaN.
|
@@ -2823,11 +3033,13 @@ def phasor_threshold(
|
|
2823
3033
|
Phasor coordinates smaller than minimum thresholds or larger than maximum
|
2824
3034
|
thresholds are replaced NaN.
|
2825
3035
|
No threshold is applied by default.
|
3036
|
+
NaNs in `mean` or any `real` and `imag` harmonic are propagated to
|
3037
|
+
`mean` and all harmonics in `real` and `imag`.
|
2826
3038
|
|
2827
3039
|
Parameters
|
2828
3040
|
----------
|
2829
3041
|
mean : array_like
|
2830
|
-
|
3042
|
+
Intensity of phasor coordinates.
|
2831
3043
|
real : array_like
|
2832
3044
|
Real component of phasor coordinates.
|
2833
3045
|
imag : array_like
|
@@ -2855,8 +3067,12 @@ def phasor_threshold(
|
|
2855
3067
|
open_interval : bool, optional
|
2856
3068
|
If true, the interval is open, and the threshold values are
|
2857
3069
|
not included in the interval.
|
2858
|
-
If
|
2859
|
-
included in the interval.
|
3070
|
+
If false (default), the interval is closed, and the threshold values
|
3071
|
+
are included in the interval.
|
3072
|
+
detect_harmonics : bool, optional
|
3073
|
+
By default, detect presence of multiple harmonics from array shapes.
|
3074
|
+
If false, no harmonics are assumed to be present, and the function
|
3075
|
+
behaves like a numpy universal function.
|
2860
3076
|
**kwargs
|
2861
3077
|
Optional `arguments passed to numpy universal functions
|
2862
3078
|
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
@@ -2864,7 +3080,7 @@ def phasor_threshold(
|
|
2864
3080
|
Returns
|
2865
3081
|
-------
|
2866
3082
|
mean : ndarray
|
2867
|
-
Thresholded
|
3083
|
+
Thresholded intensity of phasor coordinates.
|
2868
3084
|
real : ndarray
|
2869
3085
|
Thresholded real component of phasor coordinates.
|
2870
3086
|
imag : ndarray
|
@@ -2939,96 +3155,144 @@ def phasor_threshold(
|
|
2939
3155
|
else:
|
2940
3156
|
threshold_mean_only = False
|
2941
3157
|
|
2942
|
-
if
|
2943
|
-
|
2944
|
-
|
3158
|
+
if detect_harmonics:
|
3159
|
+
mean = numpy.asarray(mean)
|
3160
|
+
real = numpy.asarray(real)
|
3161
|
+
imag = numpy.asarray(imag)
|
3162
|
+
|
3163
|
+
shape = numpy.broadcast_shapes(mean.shape, real.shape, imag.shape)
|
3164
|
+
ndim = len(shape)
|
3165
|
+
|
3166
|
+
has_harmonic_axis = (
|
3167
|
+
# detect multi-harmonic in axis 0
|
3168
|
+
mean.ndim + 1 == ndim
|
3169
|
+
and real.shape == shape
|
3170
|
+
and imag.shape == shape
|
3171
|
+
and mean.shape == shape[-mean.ndim if mean.ndim else 1 :]
|
2945
3172
|
)
|
3173
|
+
else:
|
3174
|
+
has_harmonic_axis = False
|
2946
3175
|
|
2947
|
-
if threshold_mean_only:
|
3176
|
+
if threshold_mean_only is None:
|
3177
|
+
mean, real, imag = _phasor_threshold_nan(mean, real, imag, **kwargs)
|
3178
|
+
|
3179
|
+
elif threshold_mean_only:
|
2948
3180
|
mean_func = (
|
2949
3181
|
_phasor_threshold_mean_open
|
2950
3182
|
if open_interval
|
2951
3183
|
else _phasor_threshold_mean_closed
|
2952
3184
|
)
|
2953
|
-
|
3185
|
+
mean, real, imag = mean_func(
|
2954
3186
|
mean, real, imag, mean_min, mean_max, **kwargs
|
2955
3187
|
)
|
2956
3188
|
|
2957
|
-
|
2958
|
-
|
2959
|
-
|
2960
|
-
|
2961
|
-
|
2962
|
-
|
2963
|
-
imag
|
2964
|
-
|
2965
|
-
|
2966
|
-
|
2967
|
-
|
2968
|
-
|
2969
|
-
|
2970
|
-
|
2971
|
-
|
2972
|
-
|
2973
|
-
|
2974
|
-
|
2975
|
-
|
3189
|
+
else:
|
3190
|
+
func = (
|
3191
|
+
_phasor_threshold_open
|
3192
|
+
if open_interval
|
3193
|
+
else _phasor_threshold_closed
|
3194
|
+
)
|
3195
|
+
mean, real, imag = func(
|
3196
|
+
mean,
|
3197
|
+
real,
|
3198
|
+
imag,
|
3199
|
+
mean_min,
|
3200
|
+
mean_max,
|
3201
|
+
real_min,
|
3202
|
+
real_max,
|
3203
|
+
imag_min,
|
3204
|
+
imag_max,
|
3205
|
+
phase_min,
|
3206
|
+
phase_max,
|
3207
|
+
modulation_min,
|
3208
|
+
modulation_max,
|
3209
|
+
**kwargs,
|
3210
|
+
)
|
3211
|
+
|
3212
|
+
mean = numpy.asarray(mean)
|
3213
|
+
real = numpy.asarray(real)
|
3214
|
+
imag = numpy.asarray(imag)
|
3215
|
+
if has_harmonic_axis and mean.ndim > 0:
|
3216
|
+
# propagate NaN to all dimensions
|
3217
|
+
mean = numpy.mean(mean, axis=0, keepdims=True)
|
3218
|
+
mask = numpy.where(numpy.isnan(mean), numpy.nan, 1.0)
|
3219
|
+
numpy.multiply(real, mask, out=real)
|
3220
|
+
numpy.multiply(imag, mask, out=imag)
|
3221
|
+
# remove harmonic dimension created by broadcasting
|
3222
|
+
mean = numpy.asarray(numpy.asarray(mean)[0])
|
3223
|
+
|
3224
|
+
return mean, real, imag
|
2976
3225
|
|
2977
3226
|
|
2978
3227
|
def phasor_center(
|
3228
|
+
mean: ArrayLike,
|
2979
3229
|
real: ArrayLike,
|
2980
3230
|
imag: ArrayLike,
|
2981
3231
|
/,
|
2982
3232
|
*,
|
2983
3233
|
skip_axis: int | Sequence[int] | None = None,
|
2984
3234
|
method: Literal['mean', 'median'] = 'mean',
|
3235
|
+
nan_safe: bool = True,
|
2985
3236
|
**kwargs: Any,
|
2986
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
3237
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2987
3238
|
"""Return center of phasor coordinates.
|
2988
3239
|
|
2989
3240
|
Parameters
|
2990
3241
|
----------
|
3242
|
+
mean : array_like
|
3243
|
+
Intensity of phasor coordinates.
|
2991
3244
|
real : array_like
|
2992
3245
|
Real component of phasor coordinates.
|
2993
3246
|
imag : array_like
|
2994
3247
|
Imaginary component of phasor coordinates.
|
2995
3248
|
skip_axis : int or sequence of int, optional
|
2996
|
-
Axes to
|
2997
|
-
axes are
|
3249
|
+
Axes in `mean` to excluded from center calculation.
|
3250
|
+
By default, all axes except harmonics are included.
|
2998
3251
|
method : str, optional
|
2999
3252
|
Method used for center calculation:
|
3000
3253
|
|
3001
3254
|
- ``'mean'``: Arithmetic mean of phasor coordinates.
|
3002
3255
|
- ``'median'``: Spatial median of phasor coordinates.
|
3003
3256
|
|
3257
|
+
nan_safe : bool, optional
|
3258
|
+
Ensure `method` is applied to same elements of input arrays.
|
3259
|
+
By default, distribute NaNs among input arrays before applying
|
3260
|
+
`method`. May be disabled if phasor coordinates were filtered by
|
3261
|
+
:py:func:`phasor_threshold`.
|
3004
3262
|
**kwargs
|
3005
3263
|
Optional arguments passed to :py:func:`numpy.nanmean` or
|
3006
3264
|
:py:func:`numpy.nanmedian`.
|
3007
3265
|
|
3008
3266
|
Returns
|
3009
3267
|
-------
|
3268
|
+
mean_center : ndarray
|
3269
|
+
Intensity center coordinates.
|
3010
3270
|
real_center : ndarray
|
3011
|
-
Real center coordinates
|
3271
|
+
Real center coordinates.
|
3012
3272
|
imag_center : ndarray
|
3013
|
-
Imaginary center coordinates
|
3273
|
+
Imaginary center coordinates.
|
3014
3274
|
|
3015
3275
|
Raises
|
3016
3276
|
------
|
3017
3277
|
ValueError
|
3018
3278
|
If the specified method is not supported.
|
3019
|
-
If the shapes of
|
3279
|
+
If the shapes of `mean`, `real`, and `imag` do not match.
|
3020
3280
|
|
3021
3281
|
Examples
|
3022
3282
|
--------
|
3023
|
-
Compute center coordinates with the 'mean' method:
|
3283
|
+
Compute center coordinates with the default 'mean' method:
|
3024
3284
|
|
3025
|
-
>>> phasor_center(
|
3026
|
-
|
3285
|
+
>>> phasor_center(
|
3286
|
+
... [2, 1, 2], [0.1, 0.2, 0.3], [0.4, 0.5, 0.6]
|
3287
|
+
... ) # doctest: +NUMBER
|
3288
|
+
(1.67, 0.2, 0.5)
|
3027
3289
|
|
3028
3290
|
Compute center coordinates with the 'median' method:
|
3029
3291
|
|
3030
|
-
>>> phasor_center(
|
3031
|
-
|
3292
|
+
>>> phasor_center(
|
3293
|
+
... [1, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], method='median'
|
3294
|
+
... )
|
3295
|
+
(2.0, 0.2, 0.5)
|
3032
3296
|
|
3033
3297
|
"""
|
3034
3298
|
methods = {
|
@@ -3040,233 +3304,67 @@ def phasor_center(
|
|
3040
3304
|
f'Method not supported, supported methods are: '
|
3041
3305
|
f"{', '.join(methods)}"
|
3042
3306
|
)
|
3307
|
+
|
3308
|
+
mean = numpy.asarray(mean)
|
3043
3309
|
real = numpy.asarray(real)
|
3044
3310
|
imag = numpy.asarray(imag)
|
3045
3311
|
if real.shape != imag.shape:
|
3046
3312
|
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
3313
|
+
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
3314
|
+
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
3047
3315
|
|
3048
|
-
|
3049
|
-
|
3050
|
-
|
3051
|
-
|
3052
|
-
|
3053
|
-
def _mean(
|
3054
|
-
real: NDArray[Any], imag: NDArray[Any], /, **kwargs: Any
|
3055
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
3056
|
-
"""Return the mean center of phasor coordinates.
|
3057
|
-
|
3058
|
-
Parameters
|
3059
|
-
----------
|
3060
|
-
real : ndarray
|
3061
|
-
Real components of phasor coordinates.
|
3062
|
-
imag : ndarray
|
3063
|
-
Imaginary components of phasor coordinates.
|
3064
|
-
**kwargs
|
3065
|
-
Optional arguments passed to :py:func:`numpy.nanmean`.
|
3066
|
-
|
3067
|
-
Returns
|
3068
|
-
-------
|
3069
|
-
real_center : ndarray
|
3070
|
-
Mean real center coordinates.
|
3071
|
-
imag_center : ndarray
|
3072
|
-
Mean imaginary center coordinates.
|
3316
|
+
prepend_axis = mean.ndim + 1 == real.ndim
|
3317
|
+
_, axis = _parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
|
3318
|
+
if prepend_axis:
|
3319
|
+
mean = numpy.expand_dims(mean, axis=0)
|
3073
3320
|
|
3074
|
-
|
3075
|
-
|
3076
|
-
>>> _mean([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])
|
3077
|
-
(2.0, 5.0)
|
3321
|
+
if nan_safe:
|
3322
|
+
mean, real, imag = phasor_threshold(mean, real, imag)
|
3078
3323
|
|
3079
|
-
|
3080
|
-
return numpy.nanmean(real, **kwargs), numpy.nanmean(imag, **kwargs)
|
3324
|
+
mean, real, imag = methods[method](mean, real, imag, axis=axis, **kwargs)
|
3081
3325
|
|
3326
|
+
if prepend_axis:
|
3327
|
+
mean = numpy.asarray(mean[0])
|
3328
|
+
return mean, real, imag
|
3082
3329
|
|
3083
|
-
def _median(
|
3084
|
-
real: NDArray[Any], imag: NDArray[Any], /, **kwargs: Any
|
3085
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
3086
|
-
"""Return the spatial median center of phasor coordinates.
|
3087
3330
|
|
3088
|
-
|
3089
|
-
|
3090
|
-
real : ndarray
|
3091
|
-
Real components of phasor coordinates.
|
3092
|
-
imag : ndarray
|
3093
|
-
Imaginary components of phasor coordinates.
|
3094
|
-
**kwargs
|
3095
|
-
Optional arguments passed to :py:func:`numpy.nanmedian`.
|
3096
|
-
|
3097
|
-
Returns
|
3098
|
-
-------
|
3099
|
-
real_center : ndarray
|
3100
|
-
Spatial median center of real coordinates.
|
3101
|
-
imag_center : ndarray
|
3102
|
-
Spatial median center of imaginary coordinates.
|
3103
|
-
|
3104
|
-
Examples
|
3105
|
-
--------
|
3106
|
-
>>> _median([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])
|
3107
|
-
(2.0, 5.0)
|
3108
|
-
|
3109
|
-
"""
|
3110
|
-
return numpy.nanmedian(real, **kwargs), numpy.nanmedian(imag, **kwargs)
|
3111
|
-
|
3112
|
-
|
3113
|
-
def _median_filter(
|
3331
|
+
def _mean(
|
3332
|
+
mean: NDArray[Any],
|
3114
3333
|
real: NDArray[Any],
|
3115
3334
|
imag: NDArray[Any],
|
3116
|
-
axes: Sequence[int],
|
3117
3335
|
/,
|
3118
|
-
|
3119
|
-
|
3120
|
-
|
3121
|
-
|
3122
|
-
|
3123
|
-
|
3124
|
-
|
3125
|
-
|
3126
|
-
|
3127
|
-
real
|
3128
|
-
Real components of phasor coordinates.
|
3129
|
-
imag : ndarray
|
3130
|
-
Imaginary components of phasor coordinates.
|
3131
|
-
axes : sequence of int
|
3132
|
-
Axes along which to apply median filter.
|
3133
|
-
repeat : int, optional
|
3134
|
-
Number of times to apply filter. The default is 1.
|
3135
|
-
size : int, optional
|
3136
|
-
Size of median filter kernel. The default is 3.
|
3137
|
-
num_threads : int, optional
|
3138
|
-
Number of OpenMP threads to use for parallelization.
|
3139
|
-
By default, multi-threading is disabled.
|
3140
|
-
If zero, up to half of logical CPUs are used.
|
3141
|
-
OpenMP may not be available on all platforms.
|
3142
|
-
|
3143
|
-
Returns
|
3144
|
-
-------
|
3145
|
-
real : ndarray
|
3146
|
-
Median-filtered real component of phasor coordinates.
|
3147
|
-
imag : ndarray
|
3148
|
-
Median-filtered imaginary component of phasor coordinates.
|
3149
|
-
|
3150
|
-
"""
|
3151
|
-
real = numpy.asarray(real)
|
3152
|
-
if real.dtype == numpy.float32:
|
3153
|
-
real = real.copy()
|
3154
|
-
else:
|
3155
|
-
real = real.astype(float)
|
3156
|
-
|
3157
|
-
imag = numpy.asarray(imag)
|
3158
|
-
if imag.dtype == numpy.float32:
|
3159
|
-
imag = imag.copy()
|
3160
|
-
else:
|
3161
|
-
imag = imag.astype(float)
|
3162
|
-
|
3163
|
-
if len(axes) != 2:
|
3164
|
-
# n-dimensional median filter using numpy
|
3165
|
-
from numpy.lib.stride_tricks import sliding_window_view
|
3166
|
-
|
3167
|
-
kernel_shape = tuple(
|
3168
|
-
size if i in axes else 1 for i in range(real.ndim)
|
3169
|
-
)
|
3170
|
-
pad_width = [
|
3171
|
-
(s // 2, s // 2) if s > 1 else (0, 0) for s in kernel_shape
|
3172
|
-
]
|
3173
|
-
axis = tuple(range(-real.ndim, 0))
|
3174
|
-
|
3175
|
-
nan_mask = numpy.isnan(real)
|
3176
|
-
for _ in range(repeat):
|
3177
|
-
real = numpy.pad(real, pad_width, mode='edge')
|
3178
|
-
real = sliding_window_view(real, kernel_shape)
|
3179
|
-
real = numpy.nanmedian(real, axis=axis)
|
3180
|
-
real = numpy.where(nan_mask, numpy.nan, real)
|
3181
|
-
|
3182
|
-
nan_mask = numpy.isnan(imag)
|
3183
|
-
for _ in range(repeat):
|
3184
|
-
imag = numpy.pad(imag, pad_width, mode='edge')
|
3185
|
-
imag = sliding_window_view(imag, kernel_shape)
|
3186
|
-
imag = numpy.nanmedian(imag, axis=axis)
|
3187
|
-
imag = numpy.where(nan_mask, numpy.nan, imag)
|
3188
|
-
|
3189
|
-
return real, imag
|
3190
|
-
|
3191
|
-
# 2-dimensional median filter using optimized Cython implementation
|
3192
|
-
num_threads = number_threads(num_threads)
|
3193
|
-
|
3194
|
-
filtered_slice = numpy.empty(
|
3195
|
-
tuple(real.shape[axis] for axis in axes), dtype=real.dtype
|
3196
|
-
)
|
3197
|
-
|
3198
|
-
for index in numpy.ndindex(
|
3199
|
-
*[real.shape[ax] for ax in range(real.ndim) if ax not in axes]
|
3200
|
-
):
|
3201
|
-
index_list: list[int | slice] = list(index)
|
3202
|
-
for ax in axes:
|
3203
|
-
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
3204
|
-
full_index = tuple(index_list)
|
3205
|
-
|
3206
|
-
_median_filter_2d(
|
3207
|
-
real[full_index], filtered_slice, size, repeat, num_threads
|
3208
|
-
)
|
3209
|
-
|
3210
|
-
_median_filter_2d(
|
3211
|
-
imag[full_index], filtered_slice, size, repeat, num_threads
|
3212
|
-
)
|
3213
|
-
|
3214
|
-
return real, imag
|
3336
|
+
**kwargs: Any,
|
3337
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
3338
|
+
"""Return mean center of phasor coordinates."""
|
3339
|
+
real = numpy.nanmean(real * mean, **kwargs)
|
3340
|
+
imag = numpy.nanmean(imag * mean, **kwargs)
|
3341
|
+
mean = numpy.nanmean(mean, **kwargs)
|
3342
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
3343
|
+
real /= mean
|
3344
|
+
imag /= mean
|
3345
|
+
return mean, real, imag
|
3215
3346
|
|
3216
3347
|
|
3217
|
-
def
|
3348
|
+
def _median(
|
3349
|
+
mean: NDArray[Any],
|
3218
3350
|
real: NDArray[Any],
|
3219
3351
|
imag: NDArray[Any],
|
3220
|
-
axes: Sequence[int],
|
3221
3352
|
/,
|
3222
|
-
*,
|
3223
|
-
repeat: int = 1,
|
3224
|
-
size: int = 3,
|
3225
3353
|
**kwargs: Any,
|
3226
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
3227
|
-
"""Return median
|
3228
|
-
|
3229
|
-
|
3230
|
-
|
3231
|
-
|
3232
|
-
|
3233
|
-
real : ndarray
|
3234
|
-
Real components of phasor coordinates.
|
3235
|
-
imag : ndarray
|
3236
|
-
Imaginary components of phasor coordinates.
|
3237
|
-
axes : sequence of int
|
3238
|
-
Axes along which to apply median filter.
|
3239
|
-
repeat : int, optional
|
3240
|
-
Number of times to apply filter. The default is 1.
|
3241
|
-
size : int, optional
|
3242
|
-
Size of median filter kernel. The default is 3.
|
3243
|
-
**kwargs
|
3244
|
-
Optional arguments passed to :py:func:`scipy.ndimage.median_filter`.
|
3245
|
-
|
3246
|
-
Returns
|
3247
|
-
-------
|
3248
|
-
real : ndarray
|
3249
|
-
Median-filtered real component of phasor coordinates.
|
3250
|
-
imag : ndarray
|
3251
|
-
Median-filtered imaginary component of phasor coordinates.
|
3252
|
-
|
3253
|
-
"""
|
3254
|
-
from scipy.ndimage import median_filter
|
3255
|
-
|
3256
|
-
real = numpy.asarray(real)
|
3257
|
-
imag = numpy.asarray(imag)
|
3258
|
-
|
3259
|
-
for _ in range(repeat):
|
3260
|
-
real = median_filter(real, size=size, axes=axes, **kwargs)
|
3261
|
-
imag = median_filter(imag, size=size, axes=axes, **kwargs)
|
3262
|
-
|
3263
|
-
return numpy.asarray(real), numpy.asarray(imag)
|
3354
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
3355
|
+
"""Return spatial median center of phasor coordinates."""
|
3356
|
+
return (
|
3357
|
+
numpy.nanmedian(mean, **kwargs),
|
3358
|
+
numpy.nanmedian(real, **kwargs),
|
3359
|
+
numpy.nanmedian(imag, **kwargs),
|
3360
|
+
)
|
3264
3361
|
|
3265
3362
|
|
3266
3363
|
def _parse_skip_axis(
|
3267
3364
|
skip_axis: int | Sequence[int] | None,
|
3268
3365
|
/,
|
3269
3366
|
ndim: int,
|
3367
|
+
prepend_axis: bool = False,
|
3270
3368
|
) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
3271
3369
|
"""Return axes to skip and not to skip.
|
3272
3370
|
|
@@ -3275,10 +3373,12 @@ def _parse_skip_axis(
|
|
3275
3373
|
|
3276
3374
|
Parameters
|
3277
3375
|
----------
|
3278
|
-
skip_axis :
|
3376
|
+
skip_axis : int or sequence of int, optional
|
3279
3377
|
Axes to skip. If None, no axes are skipped.
|
3280
3378
|
ndim : int
|
3281
3379
|
Dimensionality of array in which to skip axes.
|
3380
|
+
prepend_axis : bool, optional
|
3381
|
+
Prepend one dimension and include in `skip_axis`.
|
3282
3382
|
|
3283
3383
|
Returns
|
3284
3384
|
-------
|
@@ -3297,15 +3397,23 @@ def _parse_skip_axis(
|
|
3297
3397
|
>>> _parse_skip_axis((1, -2), 5)
|
3298
3398
|
((1, 3), (0, 2, 4))
|
3299
3399
|
|
3400
|
+
>>> _parse_skip_axis((1, -2), 5, True)
|
3401
|
+
((0, 2, 4), (1, 3, 5))
|
3402
|
+
|
3300
3403
|
"""
|
3301
3404
|
if ndim < 0:
|
3302
3405
|
raise ValueError(f'invalid {ndim=}')
|
3303
3406
|
if skip_axis is None:
|
3407
|
+
if prepend_axis:
|
3408
|
+
return (0,), tuple(range(1, ndim + 1))
|
3304
3409
|
return (), tuple(range(ndim))
|
3305
3410
|
if not isinstance(skip_axis, Sequence):
|
3306
3411
|
skip_axis = (skip_axis,)
|
3307
3412
|
if any(i >= ndim or i < -ndim for i in skip_axis):
|
3308
3413
|
raise IndexError(f'skip_axis={skip_axis} out of range for {ndim=}')
|
3309
|
-
skip_axis =
|
3414
|
+
skip_axis = sorted(int(i % ndim) for i in skip_axis)
|
3415
|
+
if prepend_axis:
|
3416
|
+
skip_axis = [0] + [i + 1 for i in skip_axis]
|
3417
|
+
ndim += 1
|
3310
3418
|
other_axis = tuple(i for i in range(ndim) if i not in skip_axis)
|
3311
|
-
return skip_axis, other_axis
|
3419
|
+
return tuple(skip_axis), other_axis
|