phasorpy 0.2__cp310-cp310-win_amd64.whl → 0.3__cp310-cp310-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.cp310-win_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +55 -37
- phasorpy/io.py +149 -7
- phasorpy/phasor.py +464 -368
- phasorpy/plot.py +6 -2
- phasorpy/version.py +1 -1
- {phasorpy-0.2.dist-info → phasorpy-0.3.dist-info}/METADATA +1 -1
- {phasorpy-0.2.dist-info → phasorpy-0.3.dist-info}/RECORD +12 -12
- {phasorpy-0.2.dist-info → phasorpy-0.3.dist-info}/LICENSE.txt +0 -0
- {phasorpy-0.2.dist-info → phasorpy-0.3.dist-info}/WHEEL +0 -0
- {phasorpy-0.2.dist-info → phasorpy-0.3.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.2.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',
|
@@ -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.
|
@@ -201,6 +204,14 @@ def phasor_from_signal(
|
|
201
204
|
dtype : dtype_like, optional
|
202
205
|
Data type of output arrays. Either float32 or float64.
|
203
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.
|
204
215
|
num_threads : int, optional
|
205
216
|
Number of OpenMP threads to use for parallelization when not using FFT.
|
206
217
|
By default, multi-threading is disabled.
|
@@ -231,13 +242,15 @@ def phasor_from_signal(
|
|
231
242
|
See Also
|
232
243
|
--------
|
233
244
|
phasorpy.phasor.phasor_to_signal
|
245
|
+
phasorpy.phasor.phasor_normalize
|
234
246
|
:ref:`sphx_glr_tutorials_benchmarks_phasorpy_phasor_from_signal.py`
|
235
247
|
|
236
248
|
Notes
|
237
249
|
-----
|
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`
|
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:
|
241
254
|
|
242
255
|
.. math::
|
243
256
|
|
@@ -312,7 +325,9 @@ def phasor_from_signal(
|
|
312
325
|
if rfft is None:
|
313
326
|
rfft = numpy.fft.rfft
|
314
327
|
|
315
|
-
fft: NDArray[Any] = rfft(
|
328
|
+
fft: NDArray[Any] = rfft(
|
329
|
+
signal, axis=axis, norm='forward' if normalize else 'backward'
|
330
|
+
)
|
316
331
|
|
317
332
|
mean = fft.take(0, axis=axis).real
|
318
333
|
if not mean.ndim == 0:
|
@@ -332,10 +347,10 @@ def phasor_from_signal(
|
|
332
347
|
real = numpy.moveaxis(real, axis, 0)
|
333
348
|
imag = numpy.moveaxis(imag, axis, 0)
|
334
349
|
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
350
|
+
if normalize:
|
351
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
352
|
+
real /= dc
|
353
|
+
imag /= dc
|
339
354
|
numpy.negative(imag, out=imag)
|
340
355
|
|
341
356
|
if not keepdims and real.ndim == 0:
|
@@ -369,7 +384,7 @@ def phasor_from_signal(
|
|
369
384
|
phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype)
|
370
385
|
signal = signal.reshape((size0, samples, size1))
|
371
386
|
|
372
|
-
_phasor_from_signal(phasor, signal, sincos, num_threads)
|
387
|
+
_phasor_from_signal(phasor, signal, sincos, normalize, num_threads)
|
373
388
|
|
374
389
|
# restore original shape
|
375
390
|
shape = shape0 + shape1
|
@@ -400,7 +415,7 @@ def phasor_to_signal(
|
|
400
415
|
----------
|
401
416
|
mean : array_like
|
402
417
|
Average signal intensity (DC).
|
403
|
-
If not scalar, shape must match the last
|
418
|
+
If not scalar, shape must match the last dimensions of `real`.
|
404
419
|
real : array_like
|
405
420
|
Real component of phasor coordinates.
|
406
421
|
Multiple harmonics, if any, must be in the first axis.
|
@@ -507,7 +522,6 @@ def phasor_to_signal(
|
|
507
522
|
if len(harmonic) != real.shape[0]:
|
508
523
|
raise ValueError(f'{len(harmonic)=} != {real.shape[0]=}')
|
509
524
|
|
510
|
-
# complex multiplication by mean signal
|
511
525
|
real *= mean
|
512
526
|
imag *= mean
|
513
527
|
numpy.negative(imag, out=imag)
|
@@ -952,9 +966,103 @@ def phasor_divide(
|
|
952
966
|
)
|
953
967
|
|
954
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
|
+
|
955
1062
|
def phasor_calibrate(
|
956
1063
|
real: ArrayLike,
|
957
1064
|
imag: ArrayLike,
|
1065
|
+
reference_mean: ArrayLike,
|
958
1066
|
reference_real: ArrayLike,
|
959
1067
|
reference_imag: ArrayLike,
|
960
1068
|
/,
|
@@ -966,11 +1074,11 @@ def phasor_calibrate(
|
|
966
1074
|
fraction: ArrayLike | None = None,
|
967
1075
|
preexponential: bool = False,
|
968
1076
|
unit_conversion: float = 1e-3,
|
969
|
-
reverse: bool = False,
|
970
1077
|
method: Literal['mean', 'median'] = 'mean',
|
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,10 +1090,13 @@ 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.
|
@@ -1005,9 +1116,8 @@ def phasor_calibrate(
|
|
1005
1116
|
harmonics.
|
1006
1117
|
The default is the first harmonic (fundamental frequency).
|
1007
1118
|
skip_axis : int or sequence of int, optional
|
1008
|
-
Axes to
|
1009
|
-
|
1010
|
-
are specified.
|
1119
|
+
Axes in `reference_mean` to exclude from reference center calculation.
|
1120
|
+
By default, all axes except harmonics are included.
|
1011
1121
|
fraction : array_like, optional
|
1012
1122
|
Fractional intensities or pre-exponential amplitudes of the lifetime
|
1013
1123
|
components. Fractions are normalized to sum to 1.
|
@@ -1019,14 +1129,18 @@ def phasor_calibrate(
|
|
1019
1129
|
Product of `frequency` and `lifetime` units' prefix factors.
|
1020
1130
|
The default is 1e-3 for MHz and ns, or Hz and ms.
|
1021
1131
|
Use 1.0 for Hz and s.
|
1022
|
-
reverse : bool, optional
|
1023
|
-
Reverse calibration.
|
1024
1132
|
method : str, optional
|
1025
|
-
Method used for calculating center of
|
1026
|
-
`reference_imag`:
|
1133
|
+
Method used for calculating center of reference phasor coordinates:
|
1027
1134
|
|
1028
|
-
- ``'mean'``: Arithmetic mean
|
1029
|
-
- ``'median'``: Spatial median
|
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.
|
1030
1144
|
|
1031
1145
|
Returns
|
1032
1146
|
-------
|
@@ -1060,11 +1174,13 @@ def phasor_calibrate(
|
|
1060
1174
|
imag,
|
1061
1175
|
*polar_from_reference_phasor(
|
1062
1176
|
*phasor_center(
|
1177
|
+
reference_mean,
|
1063
1178
|
reference_real,
|
1064
1179
|
reference_imag,
|
1065
1180
|
skip_axis,
|
1066
1181
|
method,
|
1067
|
-
|
1182
|
+
nan_safe,
|
1183
|
+
)[1:],
|
1068
1184
|
*phasor_from_lifetime(
|
1069
1185
|
frequency,
|
1070
1186
|
lifetime,
|
@@ -1091,6 +1207,7 @@ def phasor_calibrate(
|
|
1091
1207
|
>>> phasor_calibrate(
|
1092
1208
|
... [0.1, 0.2, 0.3],
|
1093
1209
|
... [0.4, 0.5, 0.6],
|
1210
|
+
... [1.0, 1.0, 1.0],
|
1094
1211
|
... [0.2, 0.3, 0.4],
|
1095
1212
|
... [0.5, 0.6, 0.7],
|
1096
1213
|
... frequency=80,
|
@@ -1103,6 +1220,7 @@ def phasor_calibrate(
|
|
1103
1220
|
>>> phasor_calibrate(
|
1104
1221
|
... [0.0658, 0.132, 0.198],
|
1105
1222
|
... [0.2657, 0.332, 0.399],
|
1223
|
+
... [1.0, 1.0, 1.0],
|
1106
1224
|
... [0.2, 0.3, 0.4],
|
1107
1225
|
... [0.5, 0.6, 0.7],
|
1108
1226
|
... frequency=80,
|
@@ -1112,36 +1230,52 @@ def phasor_calibrate(
|
|
1112
1230
|
(array([0.1, 0.2, 0.3]), array([0.4, 0.5, 0.6]))
|
1113
1231
|
|
1114
1232
|
"""
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
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:
|
1122
1263
|
raise ValueError(
|
1123
|
-
f'
|
1124
|
-
f'!= reference_imag.shape{ref_im.shape}'
|
1264
|
+
f'{reference_mean.shape=} does not have harmonic axis'
|
1125
1265
|
)
|
1126
1266
|
|
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
1267
|
frequency = numpy.asarray(frequency)
|
1135
1268
|
frequency = frequency * harmonic
|
1136
1269
|
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
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,
|
1144
1277
|
)
|
1278
|
+
|
1145
1279
|
known_re, known_im = phasor_from_lifetime(
|
1146
1280
|
frequency,
|
1147
1281
|
lifetime,
|
@@ -1149,26 +1283,26 @@ def phasor_calibrate(
|
|
1149
1283
|
preexponential=preexponential,
|
1150
1284
|
unit_conversion=unit_conversion,
|
1151
1285
|
)
|
1286
|
+
|
1152
1287
|
phi_zero, mod_zero = polar_from_reference_phasor(
|
1153
1288
|
measured_re, measured_im, known_re, known_im
|
1154
1289
|
)
|
1290
|
+
|
1155
1291
|
if numpy.ndim(phi_zero) > 0:
|
1156
1292
|
if reverse:
|
1157
1293
|
numpy.negative(phi_zero, out=phi_zero)
|
1158
1294
|
numpy.reciprocal(mod_zero, out=mod_zero)
|
1295
|
+
_, axis = _parse_skip_axis(
|
1296
|
+
skip_axis, real.ndim - int(has_harmonic_axis), has_harmonic_axis
|
1297
|
+
)
|
1159
1298
|
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
|
-
)
|
1299
|
+
phi_zero = numpy.expand_dims(phi_zero, axis=axis)
|
1300
|
+
mod_zero = numpy.expand_dims(mod_zero, axis=axis)
|
1168
1301
|
elif reverse:
|
1169
1302
|
phi_zero = -phi_zero
|
1170
1303
|
mod_zero = 1.0 / mod_zero
|
1171
|
-
|
1304
|
+
|
1305
|
+
return phasor_transform(real, imag, phi_zero, mod_zero)
|
1172
1306
|
|
1173
1307
|
|
1174
1308
|
def phasor_transform(
|
@@ -2668,46 +2802,49 @@ def phasor_to_principal_plane(
|
|
2668
2802
|
)
|
2669
2803
|
|
2670
2804
|
|
2671
|
-
def
|
2805
|
+
def phasor_filter_median(
|
2806
|
+
mean: ArrayLike,
|
2672
2807
|
real: ArrayLike,
|
2673
2808
|
imag: ArrayLike,
|
2674
2809
|
/,
|
2675
2810
|
*,
|
2676
|
-
method: Literal['median', 'median_scipy'] = 'median',
|
2677
2811
|
repeat: int = 1,
|
2678
2812
|
size: int = 3,
|
2679
2813
|
skip_axis: int | Sequence[int] | None = None,
|
2814
|
+
use_scipy: bool = False,
|
2680
2815
|
num_threads: int | None = None,
|
2681
2816
|
**kwargs: Any,
|
2682
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
2683
|
-
"""Return filtered phasor coordinates.
|
2817
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2818
|
+
"""Return median-filtered phasor coordinates.
|
2684
2819
|
|
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.
|
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.
|
2688
2824
|
|
2689
2825
|
Parameters
|
2690
2826
|
----------
|
2827
|
+
mean : array_like
|
2828
|
+
Intensity of phasor coordinates.
|
2691
2829
|
real : array_like
|
2692
2830
|
Real component of phasor coordinates to be filtered.
|
2693
2831
|
imag : array_like
|
2694
2832
|
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
2833
|
repeat : int, optional
|
2703
|
-
Number of times to apply filter. The default is 1.
|
2834
|
+
Number of times to apply median filter. The default is 1.
|
2704
2835
|
size : int, optional
|
2705
|
-
Size of filter kernel. The default is 3.
|
2836
|
+
Size of median filter kernel. The default is 3.
|
2706
2837
|
skip_axis : int or sequence of int, optional
|
2707
|
-
|
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>`_.
|
2708
2845
|
num_threads : int, optional
|
2709
2846
|
Number of OpenMP threads to use for parallelization.
|
2710
|
-
Applies to filtering in two dimensions
|
2847
|
+
Applies to filtering in two dimensions when not using scipy.
|
2711
2848
|
By default, multi-threading is disabled.
|
2712
2849
|
If zero, up to half of logical CPUs are used.
|
2713
2850
|
OpenMP may not be available on all platforms.
|
@@ -2716,6 +2853,8 @@ def phasor_filter(
|
|
2716
2853
|
|
2717
2854
|
Returns
|
2718
2855
|
-------
|
2856
|
+
mean : ndarray
|
2857
|
+
Unchanged intensity of phasor coordinates.
|
2719
2858
|
real : ndarray
|
2720
2859
|
Filtered real component of phasor coordinates.
|
2721
2860
|
imag : ndarray
|
@@ -2724,78 +2863,136 @@ def phasor_filter(
|
|
2724
2863
|
Raises
|
2725
2864
|
------
|
2726
2865
|
ValueError
|
2727
|
-
If the specified method is not supported.
|
2728
2866
|
If `repeat` is less than 0.
|
2729
2867
|
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.
|
2868
|
+
The array shapes of `mean`, `real`, and `imag` do not match.
|
2747
2869
|
|
2748
2870
|
Examples
|
2749
2871
|
--------
|
2750
2872
|
Apply three times a median filter with a kernel size of three:
|
2751
2873
|
|
2752
|
-
>>>
|
2753
|
-
... [[
|
2754
|
-
... [[
|
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]],
|
2755
2878
|
... size=3,
|
2756
2879
|
... repeat=3,
|
2757
2880
|
... )
|
2758
|
-
|
2759
|
-
|
2760
|
-
|
2761
|
-
|
2762
|
-
|
2763
|
-
|
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]])
|
2764
2893
|
|
2765
2894
|
"""
|
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
2895
|
if repeat < 0:
|
2778
2896
|
raise ValueError(f'{repeat=} < 0')
|
2779
2897
|
if size < 1:
|
2780
2898
|
raise ValueError(f'{size=} < 1')
|
2899
|
+
if size == 1:
|
2900
|
+
# no need to filter
|
2901
|
+
repeat = 0
|
2781
2902
|
|
2782
|
-
|
2783
|
-
|
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)
|
2784
2916
|
|
2917
|
+
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
2918
|
+
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
2785
2919
|
if real.shape != imag.shape:
|
2786
2920
|
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
2787
2921
|
|
2788
|
-
|
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)
|
2789
2973
|
|
2790
|
-
|
2791
|
-
axes = kwargs.pop('axes')
|
2792
|
-
if method == 'median':
|
2793
|
-
kwargs['num_threads'] = num_threads
|
2974
|
+
return mean, real, imag
|
2794
2975
|
|
2795
|
-
|
2796
|
-
|
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
|
2797
2981
|
)
|
2798
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
|
2995
|
+
|
2799
2996
|
|
2800
2997
|
def phasor_threshold(
|
2801
2998
|
mean: ArrayLike,
|
@@ -2814,6 +3011,7 @@ def phasor_threshold(
|
|
2814
3011
|
modulation_min: ArrayLike | None = None,
|
2815
3012
|
modulation_max: ArrayLike | None = None,
|
2816
3013
|
open_interval: bool = False,
|
3014
|
+
detect_harmonics: bool = True,
|
2817
3015
|
**kwargs: Any,
|
2818
3016
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2819
3017
|
"""Return phasor coordinates with values out of interval replaced by NaN.
|
@@ -2823,11 +3021,13 @@ def phasor_threshold(
|
|
2823
3021
|
Phasor coordinates smaller than minimum thresholds or larger than maximum
|
2824
3022
|
thresholds are replaced NaN.
|
2825
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`.
|
2826
3026
|
|
2827
3027
|
Parameters
|
2828
3028
|
----------
|
2829
3029
|
mean : array_like
|
2830
|
-
|
3030
|
+
Intensity of phasor coordinates.
|
2831
3031
|
real : array_like
|
2832
3032
|
Real component of phasor coordinates.
|
2833
3033
|
imag : array_like
|
@@ -2855,8 +3055,12 @@ def phasor_threshold(
|
|
2855
3055
|
open_interval : bool, optional
|
2856
3056
|
If true, the interval is open, and the threshold values are
|
2857
3057
|
not included in the interval.
|
2858
|
-
If
|
2859
|
-
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.
|
2860
3064
|
**kwargs
|
2861
3065
|
Optional `arguments passed to numpy universal functions
|
2862
3066
|
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
@@ -2864,7 +3068,7 @@ def phasor_threshold(
|
|
2864
3068
|
Returns
|
2865
3069
|
-------
|
2866
3070
|
mean : ndarray
|
2867
|
-
Thresholded
|
3071
|
+
Thresholded intensity of phasor coordinates.
|
2868
3072
|
real : ndarray
|
2869
3073
|
Thresholded real component of phasor coordinates.
|
2870
3074
|
imag : ndarray
|
@@ -2939,96 +3143,144 @@ def phasor_threshold(
|
|
2939
3143
|
else:
|
2940
3144
|
threshold_mean_only = False
|
2941
3145
|
|
2942
|
-
if
|
2943
|
-
|
2944
|
-
|
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 :]
|
2945
3160
|
)
|
3161
|
+
else:
|
3162
|
+
has_harmonic_axis = False
|
3163
|
+
|
3164
|
+
if threshold_mean_only is None:
|
3165
|
+
mean, real, imag = _phasor_threshold_nan(mean, real, imag, **kwargs)
|
2946
3166
|
|
2947
|
-
|
3167
|
+
elif threshold_mean_only:
|
2948
3168
|
mean_func = (
|
2949
3169
|
_phasor_threshold_mean_open
|
2950
3170
|
if open_interval
|
2951
3171
|
else _phasor_threshold_mean_closed
|
2952
3172
|
)
|
2953
|
-
|
3173
|
+
mean, real, imag = mean_func(
|
2954
3174
|
mean, real, imag, mean_min, mean_max, **kwargs
|
2955
3175
|
)
|
2956
3176
|
|
2957
|
-
|
2958
|
-
|
2959
|
-
|
2960
|
-
|
2961
|
-
|
2962
|
-
|
2963
|
-
imag
|
2964
|
-
|
2965
|
-
|
2966
|
-
|
2967
|
-
|
2968
|
-
|
2969
|
-
|
2970
|
-
|
2971
|
-
|
2972
|
-
|
2973
|
-
|
2974
|
-
|
2975
|
-
|
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
|
2976
3213
|
|
2977
3214
|
|
2978
3215
|
def phasor_center(
|
3216
|
+
mean: ArrayLike,
|
2979
3217
|
real: ArrayLike,
|
2980
3218
|
imag: ArrayLike,
|
2981
3219
|
/,
|
2982
3220
|
*,
|
2983
3221
|
skip_axis: int | Sequence[int] | None = None,
|
2984
3222
|
method: Literal['mean', 'median'] = 'mean',
|
3223
|
+
nan_safe: bool = True,
|
2985
3224
|
**kwargs: Any,
|
2986
|
-
) -> tuple[NDArray[Any], NDArray[Any]]:
|
3225
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
2987
3226
|
"""Return center of phasor coordinates.
|
2988
3227
|
|
2989
3228
|
Parameters
|
2990
3229
|
----------
|
3230
|
+
mean : array_like
|
3231
|
+
Intensity of phasor coordinates.
|
2991
3232
|
real : array_like
|
2992
3233
|
Real component of phasor coordinates.
|
2993
3234
|
imag : array_like
|
2994
3235
|
Imaginary component of phasor coordinates.
|
2995
3236
|
skip_axis : int or sequence of int, optional
|
2996
|
-
Axes to
|
2997
|
-
axes are
|
3237
|
+
Axes in `mean` to excluded from center calculation.
|
3238
|
+
By default, all axes except harmonics are included.
|
2998
3239
|
method : str, optional
|
2999
3240
|
Method used for center calculation:
|
3000
3241
|
|
3001
3242
|
- ``'mean'``: Arithmetic mean of phasor coordinates.
|
3002
3243
|
- ``'median'``: Spatial median of phasor coordinates.
|
3003
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`.
|
3004
3250
|
**kwargs
|
3005
3251
|
Optional arguments passed to :py:func:`numpy.nanmean` or
|
3006
3252
|
:py:func:`numpy.nanmedian`.
|
3007
3253
|
|
3008
3254
|
Returns
|
3009
3255
|
-------
|
3256
|
+
mean_center : ndarray
|
3257
|
+
Intensity center coordinates.
|
3010
3258
|
real_center : ndarray
|
3011
|
-
Real center coordinates
|
3259
|
+
Real center coordinates.
|
3012
3260
|
imag_center : ndarray
|
3013
|
-
Imaginary center coordinates
|
3261
|
+
Imaginary center coordinates.
|
3014
3262
|
|
3015
3263
|
Raises
|
3016
3264
|
------
|
3017
3265
|
ValueError
|
3018
3266
|
If the specified method is not supported.
|
3019
|
-
If the shapes of
|
3267
|
+
If the shapes of `mean`, `real`, and `imag` do not match.
|
3020
3268
|
|
3021
3269
|
Examples
|
3022
3270
|
--------
|
3023
|
-
Compute center coordinates with the 'mean' method:
|
3271
|
+
Compute center coordinates with the default 'mean' method:
|
3024
3272
|
|
3025
|
-
>>> phasor_center(
|
3026
|
-
|
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)
|
3027
3277
|
|
3028
3278
|
Compute center coordinates with the 'median' method:
|
3029
3279
|
|
3030
|
-
>>> phasor_center(
|
3031
|
-
|
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)
|
3032
3284
|
|
3033
3285
|
"""
|
3034
3286
|
methods = {
|
@@ -3040,233 +3292,67 @@ def phasor_center(
|
|
3040
3292
|
f'Method not supported, supported methods are: '
|
3041
3293
|
f"{', '.join(methods)}"
|
3042
3294
|
)
|
3295
|
+
|
3296
|
+
mean = numpy.asarray(mean)
|
3043
3297
|
real = numpy.asarray(real)
|
3044
3298
|
imag = numpy.asarray(imag)
|
3045
3299
|
if real.shape != imag.shape:
|
3046
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=}')
|
3047
3303
|
|
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.
|
3073
|
-
|
3074
|
-
Examples
|
3075
|
-
--------
|
3076
|
-
>>> _mean([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])
|
3077
|
-
(2.0, 5.0)
|
3078
|
-
|
3079
|
-
"""
|
3080
|
-
return numpy.nanmean(real, **kwargs), numpy.nanmean(imag, **kwargs)
|
3081
|
-
|
3082
|
-
|
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
|
-
|
3088
|
-
Parameters
|
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`.
|
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)
|
3096
3308
|
|
3097
|
-
|
3098
|
-
|
3099
|
-
real_center : ndarray
|
3100
|
-
Spatial median center of real coordinates.
|
3101
|
-
imag_center : ndarray
|
3102
|
-
Spatial median center of imaginary coordinates.
|
3309
|
+
if nan_safe:
|
3310
|
+
mean, real, imag = phasor_threshold(mean, real, imag)
|
3103
3311
|
|
3104
|
-
|
3105
|
-
--------
|
3106
|
-
>>> _median([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])
|
3107
|
-
(2.0, 5.0)
|
3312
|
+
mean, real, imag = methods[method](mean, real, imag, axis=axis, **kwargs)
|
3108
3313
|
|
3109
|
-
|
3110
|
-
|
3314
|
+
if prepend_axis:
|
3315
|
+
mean = numpy.asarray(mean[0])
|
3316
|
+
return mean, real, imag
|
3111
3317
|
|
3112
3318
|
|
3113
|
-
def
|
3319
|
+
def _mean(
|
3320
|
+
mean: NDArray[Any],
|
3114
3321
|
real: NDArray[Any],
|
3115
3322
|
imag: NDArray[Any],
|
3116
|
-
axes: Sequence[int],
|
3117
3323
|
/,
|
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
|
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
|
3215
3334
|
|
3216
3335
|
|
3217
|
-
def
|
3336
|
+
def _median(
|
3337
|
+
mean: NDArray[Any],
|
3218
3338
|
real: NDArray[Any],
|
3219
3339
|
imag: NDArray[Any],
|
3220
|
-
axes: Sequence[int],
|
3221
3340
|
/,
|
3222
|
-
*,
|
3223
|
-
repeat: int = 1,
|
3224
|
-
size: int = 3,
|
3225
3341
|
**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)
|
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
|
+
)
|
3264
3349
|
|
3265
3350
|
|
3266
3351
|
def _parse_skip_axis(
|
3267
3352
|
skip_axis: int | Sequence[int] | None,
|
3268
3353
|
/,
|
3269
3354
|
ndim: int,
|
3355
|
+
prepend_axis: bool = False,
|
3270
3356
|
) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
3271
3357
|
"""Return axes to skip and not to skip.
|
3272
3358
|
|
@@ -3275,10 +3361,12 @@ def _parse_skip_axis(
|
|
3275
3361
|
|
3276
3362
|
Parameters
|
3277
3363
|
----------
|
3278
|
-
skip_axis :
|
3364
|
+
skip_axis : int or sequence of int, optional
|
3279
3365
|
Axes to skip. If None, no axes are skipped.
|
3280
3366
|
ndim : int
|
3281
3367
|
Dimensionality of array in which to skip axes.
|
3368
|
+
prepend_axis : bool, optional
|
3369
|
+
Prepend one dimension and include in `skip_axis`.
|
3282
3370
|
|
3283
3371
|
Returns
|
3284
3372
|
-------
|
@@ -3297,15 +3385,23 @@ def _parse_skip_axis(
|
|
3297
3385
|
>>> _parse_skip_axis((1, -2), 5)
|
3298
3386
|
((1, 3), (0, 2, 4))
|
3299
3387
|
|
3388
|
+
>>> _parse_skip_axis((1, -2), 5, True)
|
3389
|
+
((0, 2, 4), (1, 3, 5))
|
3390
|
+
|
3300
3391
|
"""
|
3301
3392
|
if ndim < 0:
|
3302
3393
|
raise ValueError(f'invalid {ndim=}')
|
3303
3394
|
if skip_axis is None:
|
3395
|
+
if prepend_axis:
|
3396
|
+
return (0,), tuple(range(1, ndim + 1))
|
3304
3397
|
return (), tuple(range(ndim))
|
3305
3398
|
if not isinstance(skip_axis, Sequence):
|
3306
3399
|
skip_axis = (skip_axis,)
|
3307
3400
|
if any(i >= ndim or i < -ndim for i in skip_axis):
|
3308
3401
|
raise IndexError(f'skip_axis={skip_axis} out of range for {ndim=}')
|
3309
|
-
skip_axis =
|
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
|
3310
3406
|
other_axis = tuple(i for i in range(ndim) if i not in skip_axis)
|
3311
|
-
return skip_axis, other_axis
|
3407
|
+
return tuple(skip_axis), other_axis
|