phasorpy 0.3__cp312-cp312-win_amd64.whl → 0.5__cp312-cp312-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phasorpy/_io.py +2655 -0
- phasorpy/_phasorpy.cp312-win_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +119 -47
- phasorpy/_utils.py +165 -18
- phasorpy/cli.py +2 -0
- phasorpy/cluster.py +170 -0
- phasorpy/color.py +17 -13
- phasorpy/components.py +18 -18
- phasorpy/conftest.py +2 -0
- phasorpy/cursors.py +9 -9
- phasorpy/datasets.py +169 -10
- phasorpy/io.py +7 -1809
- phasorpy/phasor.py +281 -100
- phasorpy/plot.py +276 -36
- phasorpy/utils.py +12 -7
- phasorpy/version.py +14 -5
- {phasorpy-0.3.dist-info → phasorpy-0.5.dist-info}/METADATA +11 -15
- phasorpy-0.5.dist-info/RECORD +26 -0
- {phasorpy-0.3.dist-info → phasorpy-0.5.dist-info}/WHEEL +1 -1
- {phasorpy-0.3.dist-info → phasorpy-0.5.dist-info/licenses}/LICENSE.txt +1 -1
- phasorpy-0.3.dist-info/RECORD +0 -24
- {phasorpy-0.3.dist-info → phasorpy-0.5.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.3.dist-info → phasorpy-0.5.dist-info}/top_level.txt +0 -0
phasorpy/phasor.py
CHANGED
@@ -59,13 +59,14 @@ The ``phasorpy.phasor`` module provides functions to:
|
|
59
59
|
- :py:func:`lifetime_fraction_from_amplitude`
|
60
60
|
- :py:func:`lifetime_fraction_to_amplitude`
|
61
61
|
|
62
|
-
- calculate phasor coordinates on semicircle at other harmonics:
|
62
|
+
- calculate phasor coordinates on universal semicircle at other harmonics:
|
63
63
|
|
64
64
|
- :py:func:`phasor_at_harmonic`
|
65
65
|
|
66
66
|
- filter phasor coordinates:
|
67
67
|
|
68
68
|
- :py:func:`phasor_filter_median`
|
69
|
+
- :py:func:`phasor_filter_pawflim`
|
69
70
|
- :py:func:`phasor_threshold`
|
70
71
|
|
71
72
|
"""
|
@@ -83,6 +84,7 @@ __all__ = [
|
|
83
84
|
'phasor_center',
|
84
85
|
'phasor_divide',
|
85
86
|
'phasor_filter_median',
|
87
|
+
'phasor_filter_pawflim',
|
86
88
|
'phasor_from_apparent_lifetime',
|
87
89
|
'phasor_from_fret_acceptor',
|
88
90
|
'phasor_from_fret_donor',
|
@@ -149,7 +151,7 @@ from ._phasorpy import (
|
|
149
151
|
_polar_from_single_lifetime,
|
150
152
|
_polar_to_apparent_lifetime,
|
151
153
|
)
|
152
|
-
from ._utils import parse_harmonic
|
154
|
+
from ._utils import parse_harmonic, parse_signal_axis, parse_skip_axis
|
153
155
|
from .utils import number_threads
|
154
156
|
|
155
157
|
|
@@ -157,7 +159,7 @@ def phasor_from_signal(
|
|
157
159
|
signal: ArrayLike,
|
158
160
|
/,
|
159
161
|
*,
|
160
|
-
axis: int =
|
162
|
+
axis: int | str | None = None,
|
161
163
|
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
162
164
|
sample_phase: ArrayLike | None = None,
|
163
165
|
use_fft: bool | None = None,
|
@@ -174,9 +176,10 @@ def phasor_from_signal(
|
|
174
176
|
Frequency-domain, time-domain, or hyperspectral data.
|
175
177
|
A minimum of three samples are required along `axis`.
|
176
178
|
The samples must be uniformly spaced.
|
177
|
-
axis : int, optional
|
179
|
+
axis : int or str, optional
|
178
180
|
Axis over which to compute phasor coordinates.
|
179
|
-
|
181
|
+
By default, the 'H' or 'C' axes if signal contains such dimension
|
182
|
+
names, else the last axis (-1).
|
180
183
|
harmonic : int, sequence of int, or 'all', optional
|
181
184
|
Harmonics to return.
|
182
185
|
If `'all'`, return all harmonics for `signal` samples along `axis`.
|
@@ -287,6 +290,9 @@ def phasor_from_signal(
|
|
287
290
|
"""
|
288
291
|
# TODO: C-order not required by rfft?
|
289
292
|
# TODO: preserve array subtypes?
|
293
|
+
|
294
|
+
axis, _ = parse_signal_axis(signal, axis)
|
295
|
+
|
290
296
|
signal = numpy.asarray(signal, order='C')
|
291
297
|
if signal.dtype.kind not in 'uif':
|
292
298
|
raise TypeError(f'signal must be real valued, not {signal.dtype=}')
|
@@ -498,7 +504,8 @@ def phasor_to_signal(
|
|
498
504
|
else:
|
499
505
|
keepdims = mean.ndim > 0 or real.ndim > 0
|
500
506
|
|
501
|
-
mean
|
507
|
+
mean = numpy.asarray(numpy.atleast_1d(mean))
|
508
|
+
real = numpy.asarray(numpy.atleast_1d(real))
|
502
509
|
|
503
510
|
if real.dtype.kind != 'f' or imag.dtype.kind != 'f':
|
504
511
|
raise ValueError(f'{real.dtype=} or {imag.dtype=} not floating point')
|
@@ -591,7 +598,7 @@ def lifetime_to_signal(
|
|
591
598
|
or `1` to synthesize a homodyne signal.
|
592
599
|
zero_phase : float, optional
|
593
600
|
Position of instrument response function in radians.
|
594
|
-
Must be in range 0
|
601
|
+
Must be in range [0, pi]. The default is the 8th sample.
|
595
602
|
zero_stdev : float, optional
|
596
603
|
Standard deviation of instrument response function in radians.
|
597
604
|
Must be at least 1.5 samples and no more than one tenth of samples
|
@@ -679,7 +686,7 @@ def lifetime_to_signal(
|
|
679
686
|
stdev = zero_stdev * scale # in sample units
|
680
687
|
|
681
688
|
if zero_phase < 0 or zero_phase > 2.0 * math.pi:
|
682
|
-
raise ValueError(f'{zero_phase=} out of range [0
|
689
|
+
raise ValueError(f'{zero_phase=} out of range [0, 2 pi]')
|
683
690
|
if stdev < 1.5:
|
684
691
|
raise ValueError(
|
685
692
|
f'{zero_stdev=} < {1.5 / scale} cannot be sampled sufficiently'
|
@@ -745,9 +752,9 @@ def phasor_semicircle(
|
|
745
752
|
Returns
|
746
753
|
-------
|
747
754
|
real : ndarray
|
748
|
-
Real component of
|
755
|
+
Real component of phasor coordinates on universal semicircle.
|
749
756
|
imag : ndarray
|
750
|
-
Imaginary component of
|
757
|
+
Imaginary component of phasor coordinates on universal semicircle.
|
751
758
|
|
752
759
|
Raises
|
753
760
|
------
|
@@ -1046,9 +1053,7 @@ def phasor_normalize(
|
|
1046
1053
|
else:
|
1047
1054
|
real = numpy.array(real_unnormalized, dtype, copy=True)
|
1048
1055
|
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
|
-
)
|
1056
|
+
mean = numpy.array(mean_unnormalized, real.dtype, copy=True)
|
1052
1057
|
|
1053
1058
|
with numpy.errstate(divide='ignore', invalid='ignore'):
|
1054
1059
|
numpy.divide(real, mean, out=real)
|
@@ -1154,7 +1159,8 @@ def phasor_calibrate(
|
|
1154
1159
|
ValueError
|
1155
1160
|
The array shapes of `real` and `imag`, or `reference_real` and
|
1156
1161
|
`reference_imag` do not match.
|
1157
|
-
Number of harmonics does not match the first axis
|
1162
|
+
Number of harmonics or frequencies does not match the first axis
|
1163
|
+
of `real` and `imag`.
|
1158
1164
|
|
1159
1165
|
See Also
|
1160
1166
|
--------
|
@@ -1243,16 +1249,34 @@ def phasor_calibrate(
|
|
1243
1249
|
|
1244
1250
|
has_harmonic_axis = reference_mean.ndim + 1 == reference_real.ndim
|
1245
1251
|
harmonic, _ = parse_harmonic(
|
1246
|
-
harmonic,
|
1252
|
+
harmonic,
|
1253
|
+
(
|
1254
|
+
reference_real.shape[0]
|
1255
|
+
if has_harmonic_axis
|
1256
|
+
and isinstance(harmonic, str)
|
1257
|
+
and harmonic == 'all'
|
1258
|
+
else None
|
1259
|
+
),
|
1247
1260
|
)
|
1248
1261
|
|
1262
|
+
frequency = numpy.asarray(frequency)
|
1263
|
+
frequency = frequency * harmonic
|
1264
|
+
|
1249
1265
|
if has_harmonic_axis:
|
1250
1266
|
if real.ndim == 0:
|
1251
|
-
raise ValueError(
|
1252
|
-
|
1253
|
-
|
1254
|
-
if
|
1255
|
-
raise ValueError(
|
1267
|
+
raise ValueError(
|
1268
|
+
f'{real.shape=} != {len(frequency)} frequencies or harmonics'
|
1269
|
+
)
|
1270
|
+
if real.shape[0] != len(frequency):
|
1271
|
+
raise ValueError(
|
1272
|
+
f'{real.shape[0]=} != {len(frequency)} '
|
1273
|
+
'frequencies or harmonics'
|
1274
|
+
)
|
1275
|
+
if reference_real.shape[0] != len(frequency):
|
1276
|
+
raise ValueError(
|
1277
|
+
f'{reference_real.shape[0]=} != {len(frequency)} '
|
1278
|
+
'frequencies or harmonics'
|
1279
|
+
)
|
1256
1280
|
if reference_mean.shape != reference_real.shape[1:]:
|
1257
1281
|
raise ValueError(
|
1258
1282
|
f'{reference_mean.shape=} != {reference_real.shape[1:]=}'
|
@@ -1264,9 +1288,6 @@ def phasor_calibrate(
|
|
1264
1288
|
f'{reference_mean.shape=} does not have harmonic axis'
|
1265
1289
|
)
|
1266
1290
|
|
1267
|
-
frequency = numpy.asarray(frequency)
|
1268
|
-
frequency = frequency * harmonic
|
1269
|
-
|
1270
1291
|
_, measured_re, measured_im = phasor_center(
|
1271
1292
|
reference_mean,
|
1272
1293
|
reference_real,
|
@@ -1284,6 +1305,24 @@ def phasor_calibrate(
|
|
1284
1305
|
unit_conversion=unit_conversion,
|
1285
1306
|
)
|
1286
1307
|
|
1308
|
+
skip_axis, axis = parse_skip_axis(
|
1309
|
+
skip_axis, real.ndim - int(has_harmonic_axis), has_harmonic_axis
|
1310
|
+
)
|
1311
|
+
|
1312
|
+
if has_harmonic_axis and any(skip_axis):
|
1313
|
+
known_re = numpy.expand_dims(
|
1314
|
+
known_re, tuple(range(1, measured_re.ndim))
|
1315
|
+
)
|
1316
|
+
known_re = numpy.broadcast_to(
|
1317
|
+
known_re, (len(frequency), *measured_re.shape[1:])
|
1318
|
+
)
|
1319
|
+
known_im = numpy.expand_dims(
|
1320
|
+
known_im, tuple(range(1, measured_im.ndim))
|
1321
|
+
)
|
1322
|
+
known_im = numpy.broadcast_to(
|
1323
|
+
known_im, (len(frequency), *measured_im.shape[1:])
|
1324
|
+
)
|
1325
|
+
|
1287
1326
|
phi_zero, mod_zero = polar_from_reference_phasor(
|
1288
1327
|
measured_re, measured_im, known_re, known_im
|
1289
1328
|
)
|
@@ -1292,9 +1331,6 @@ def phasor_calibrate(
|
|
1292
1331
|
if reverse:
|
1293
1332
|
numpy.negative(phi_zero, out=phi_zero)
|
1294
1333
|
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
|
-
)
|
1298
1334
|
if axis is not None:
|
1299
1335
|
phi_zero = numpy.expand_dims(phi_zero, axis=axis)
|
1300
1336
|
mod_zero = numpy.expand_dims(mod_zero, axis=axis)
|
@@ -1966,6 +2002,7 @@ def lifetime_fraction_from_amplitude(
|
|
1966
2002
|
array([0.8, 0.2])
|
1967
2003
|
|
1968
2004
|
"""
|
2005
|
+
t: NDArray[numpy.float64]
|
1969
2006
|
t = numpy.multiply(amplitude, lifetime, dtype=numpy.float64)
|
1970
2007
|
t /= numpy.sum(t, axis=axis, keepdims=True)
|
1971
2008
|
return t
|
@@ -2421,7 +2458,7 @@ def phasor_from_fret_donor(
|
|
2421
2458
|
donor_lifetime: ArrayLike,
|
2422
2459
|
*,
|
2423
2460
|
fret_efficiency: ArrayLike = 0.0,
|
2424
|
-
|
2461
|
+
donor_fretting: ArrayLike = 1.0,
|
2425
2462
|
donor_background: ArrayLike = 0.0,
|
2426
2463
|
background_real: ArrayLike = 0.0,
|
2427
2464
|
background_imag: ArrayLike = 0.0,
|
@@ -2447,9 +2484,9 @@ def phasor_from_fret_donor(
|
|
2447
2484
|
donor_lifetime : array_like
|
2448
2485
|
Lifetime of donor without FRET in ns.
|
2449
2486
|
fret_efficiency : array_like, optional, default 0
|
2450
|
-
FRET efficiency in range [0
|
2451
|
-
|
2452
|
-
Fraction of donors participating in FRET. Range [0
|
2487
|
+
FRET efficiency in range [0, 1].
|
2488
|
+
donor_fretting : array_like, optional, default 1
|
2489
|
+
Fraction of donors participating in FRET. Range [0, 1].
|
2453
2490
|
donor_background : array_like, optional, default 0
|
2454
2491
|
Weight of background fluorescence in donor channel
|
2455
2492
|
relative to fluorescence of donor without FRET.
|
@@ -2490,7 +2527,7 @@ def phasor_from_fret_donor(
|
|
2490
2527
|
... frequency=80,
|
2491
2528
|
... donor_lifetime=4.2,
|
2492
2529
|
... fret_efficiency=[0.0, 0.3, 1.0],
|
2493
|
-
...
|
2530
|
+
... donor_fretting=0.9,
|
2494
2531
|
... donor_background=0.1,
|
2495
2532
|
... background_real=0.11,
|
2496
2533
|
... background_imag=0.12,
|
@@ -2504,7 +2541,7 @@ def phasor_from_fret_donor(
|
|
2504
2541
|
omega,
|
2505
2542
|
donor_lifetime,
|
2506
2543
|
fret_efficiency,
|
2507
|
-
|
2544
|
+
donor_fretting,
|
2508
2545
|
donor_background,
|
2509
2546
|
background_real,
|
2510
2547
|
background_imag,
|
@@ -2518,7 +2555,7 @@ def phasor_from_fret_acceptor(
|
|
2518
2555
|
acceptor_lifetime: ArrayLike,
|
2519
2556
|
*,
|
2520
2557
|
fret_efficiency: ArrayLike = 0.0,
|
2521
|
-
|
2558
|
+
donor_fretting: ArrayLike = 1.0,
|
2522
2559
|
donor_bleedthrough: ArrayLike = 0.0,
|
2523
2560
|
acceptor_bleedthrough: ArrayLike = 0.0,
|
2524
2561
|
acceptor_background: ArrayLike = 0.0,
|
@@ -2551,9 +2588,9 @@ def phasor_from_fret_acceptor(
|
|
2551
2588
|
acceptor_lifetime : array_like
|
2552
2589
|
Lifetime of acceptor in ns.
|
2553
2590
|
fret_efficiency : array_like, optional, default 0
|
2554
|
-
FRET efficiency in range [0
|
2555
|
-
|
2556
|
-
Fraction of donors participating in FRET. Range [0
|
2591
|
+
FRET efficiency in range [0, 1].
|
2592
|
+
donor_fretting : array_like, optional, default 1
|
2593
|
+
Fraction of donors participating in FRET. Range [0, 1].
|
2557
2594
|
donor_bleedthrough : array_like, optional, default 0
|
2558
2595
|
Weight of donor fluorescence in acceptor channel
|
2559
2596
|
relative to fluorescence of fully sensitized acceptor.
|
@@ -2606,7 +2643,7 @@ def phasor_from_fret_acceptor(
|
|
2606
2643
|
... donor_lifetime=4.2,
|
2607
2644
|
... acceptor_lifetime=3.0,
|
2608
2645
|
... fret_efficiency=[0.0, 0.3, 1.0],
|
2609
|
-
...
|
2646
|
+
... donor_fretting=0.9,
|
2610
2647
|
... donor_bleedthrough=0.1,
|
2611
2648
|
... acceptor_bleedthrough=0.1,
|
2612
2649
|
... acceptor_background=0.1,
|
@@ -2623,7 +2660,7 @@ def phasor_from_fret_acceptor(
|
|
2623
2660
|
donor_lifetime,
|
2624
2661
|
acceptor_lifetime,
|
2625
2662
|
fret_efficiency,
|
2626
|
-
|
2663
|
+
donor_fretting,
|
2627
2664
|
donor_bleedthrough,
|
2628
2665
|
acceptor_bleedthrough,
|
2629
2666
|
acceptor_background,
|
@@ -2708,7 +2745,7 @@ def phasor_to_principal_plane(
|
|
2708
2745
|
References
|
2709
2746
|
----------
|
2710
2747
|
|
2711
|
-
.. [1] Franssen WMJ, Vergeldt FJ, Bader AN, van Amerongen H,
|
2748
|
+
.. [1] Franssen WMJ, Vergeldt FJ, Bader AN, van Amerongen H, and Terenzi C.
|
2712
2749
|
`Full-harmonics phasor analysis: unravelling multiexponential trends
|
2713
2750
|
in magnetic resonance imaging data
|
2714
2751
|
<https://doi.org/10.1021/acs.jpclett.0c02319>`_.
|
@@ -2920,7 +2957,7 @@ def phasor_filter_median(
|
|
2920
2957
|
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
2921
2958
|
|
2922
2959
|
prepend_axis = mean.ndim + 1 == real.ndim
|
2923
|
-
_, axes =
|
2960
|
+
_, axes = parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
|
2924
2961
|
|
2925
2962
|
# in case mean is also filtered
|
2926
2963
|
# if prepend_axis:
|
@@ -2994,6 +3031,209 @@ def phasor_filter_median(
|
|
2994
3031
|
return mean, real, imag
|
2995
3032
|
|
2996
3033
|
|
3034
|
+
def phasor_filter_pawflim(
|
3035
|
+
mean: ArrayLike,
|
3036
|
+
real: ArrayLike,
|
3037
|
+
imag: ArrayLike,
|
3038
|
+
/,
|
3039
|
+
*,
|
3040
|
+
sigma: float = 2.0,
|
3041
|
+
levels: int = 1,
|
3042
|
+
harmonic: Sequence[int] | None = None,
|
3043
|
+
skip_axis: int | Sequence[int] | None = None,
|
3044
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
3045
|
+
"""Return pawFLIM wavelet-filtered phasor coordinates.
|
3046
|
+
|
3047
|
+
This function must only be used with calibrated, unprocessed phasor
|
3048
|
+
coordinates obtained from FLIM data. The coordinates must not be filtered,
|
3049
|
+
thresholded, or otherwise pre-processed.
|
3050
|
+
|
3051
|
+
The pawFLIM wavelet filter is described in [2]_.
|
3052
|
+
|
3053
|
+
Parameters
|
3054
|
+
----------
|
3055
|
+
mean : array_like
|
3056
|
+
Intensity of phasor coordinates.
|
3057
|
+
real : array_like
|
3058
|
+
Real component of phasor coordinates to be filtered.
|
3059
|
+
Must have at least two harmonics in the first axis.
|
3060
|
+
imag : array_like
|
3061
|
+
Imaginary component of phasor coordinates to be filtered.
|
3062
|
+
Must have at least two harmonics in the first axis.
|
3063
|
+
sigma : float, optional
|
3064
|
+
Significance level to test difference between two phasors.
|
3065
|
+
Given in terms of the equivalent 1D standard deviations.
|
3066
|
+
sigma=2 corresponds to ~95% (or 5%) significance.
|
3067
|
+
levels : int, optional
|
3068
|
+
Number of levels for wavelet decomposition.
|
3069
|
+
Controls the maximum averaging area, which has a length of
|
3070
|
+
:math:`2^level`.
|
3071
|
+
harmonic : sequence of int or None, optional
|
3072
|
+
Harmonics included in first axis of `real` and `imag`.
|
3073
|
+
If None (default), the first axis of `real` and `imag` contains lower
|
3074
|
+
harmonics starting at and increasing by one.
|
3075
|
+
All harmonics must have a corresponding half or double harmonic.
|
3076
|
+
skip_axis : int or sequence of int, optional
|
3077
|
+
Axes in `mean` to exclude from filter.
|
3078
|
+
By default, all axes except harmonics are included.
|
3079
|
+
|
3080
|
+
Returns
|
3081
|
+
-------
|
3082
|
+
mean : ndarray
|
3083
|
+
Unchanged intensity of phasor coordinates.
|
3084
|
+
real : ndarray
|
3085
|
+
Filtered real component of phasor coordinates.
|
3086
|
+
imag : ndarray
|
3087
|
+
Filtered imaginary component of phasor coordinates.
|
3088
|
+
|
3089
|
+
Raises
|
3090
|
+
------
|
3091
|
+
ValueError
|
3092
|
+
If `level` is less than 0.
|
3093
|
+
The array shapes of `mean`, `real`, and `imag` do not match.
|
3094
|
+
If `real` and `imag` have no harmonic axis.
|
3095
|
+
Number of harmonics in `harmonic` is less than 2 or does not match
|
3096
|
+
the first axis of `real` and `imag`.
|
3097
|
+
Not all harmonics in `harmonic` have a corresponding half
|
3098
|
+
or double harmonic.
|
3099
|
+
|
3100
|
+
References
|
3101
|
+
----------
|
3102
|
+
|
3103
|
+
.. [2] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
|
3104
|
+
uncertainty to enable lower photon count in FLIM experiments
|
3105
|
+
<https://doi.org/10.1088/2050-6120/aa72ab>`_.
|
3106
|
+
*Methods Appl Fluoresc*, 5(2): 024016 (2017)
|
3107
|
+
|
3108
|
+
Examples
|
3109
|
+
--------
|
3110
|
+
Apply a pawFLIM wavelet filter with four significance levels (sigma)
|
3111
|
+
and three decomposition levels:
|
3112
|
+
|
3113
|
+
>>> mean, real, imag = phasor_filter_pawflim(
|
3114
|
+
... [[1, 1], [1, 1]],
|
3115
|
+
... [[[0.5, 0.8], [0.5, 0.8]], [[0.2, 0.4], [0.2, 0.4]]],
|
3116
|
+
... [[[0.5, 0.4], [0.5, 0.4]], [[0.4, 0.5], [0.4, 0.5]]],
|
3117
|
+
... sigma=4,
|
3118
|
+
... levels=3,
|
3119
|
+
... harmonic=[1, 2],
|
3120
|
+
... )
|
3121
|
+
>>> mean
|
3122
|
+
array([[1, 1],
|
3123
|
+
[1, 1]])
|
3124
|
+
>>> real
|
3125
|
+
array([[[0.65, 0.65],
|
3126
|
+
[0.65, 0.65]],
|
3127
|
+
[[0.3, 0.3],
|
3128
|
+
[0.3, 0.3]]])
|
3129
|
+
>>> imag
|
3130
|
+
array([[[0.45, 0.45],
|
3131
|
+
[0.45, 0.45]],
|
3132
|
+
[[0.45, 0.45],
|
3133
|
+
[0.45, 0.45]]])
|
3134
|
+
|
3135
|
+
"""
|
3136
|
+
from pawflim import pawflim # type: ignore[import-untyped]
|
3137
|
+
|
3138
|
+
mean = numpy.asarray(mean)
|
3139
|
+
real = numpy.asarray(real)
|
3140
|
+
imag = numpy.asarray(imag)
|
3141
|
+
|
3142
|
+
if levels < 0:
|
3143
|
+
raise ValueError(f'{levels=} < 0')
|
3144
|
+
if levels == 0:
|
3145
|
+
return mean, real, imag
|
3146
|
+
|
3147
|
+
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
3148
|
+
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
3149
|
+
if real.shape != imag.shape:
|
3150
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
3151
|
+
|
3152
|
+
has_harmonic_axis = mean.ndim + 1 == real.ndim
|
3153
|
+
if not has_harmonic_axis:
|
3154
|
+
raise ValueError('no harmonic axis')
|
3155
|
+
if harmonic is None:
|
3156
|
+
harmonics, _ = parse_harmonic('all', real.shape[0])
|
3157
|
+
else:
|
3158
|
+
harmonics, _ = parse_harmonic(harmonic, None)
|
3159
|
+
if len(harmonics) < 2:
|
3160
|
+
raise ValueError(
|
3161
|
+
'at least two harmonics required, ' f'got {len(harmonics)}'
|
3162
|
+
)
|
3163
|
+
if len(harmonics) != real.shape[0]:
|
3164
|
+
raise ValueError(
|
3165
|
+
'number of harmonics does not match first axis of real and imag'
|
3166
|
+
)
|
3167
|
+
|
3168
|
+
mean = numpy.asarray(numpy.nan_to_num(mean), dtype=float)
|
3169
|
+
real = numpy.asarray(numpy.nan_to_num(real * mean), dtype=float)
|
3170
|
+
imag = numpy.asarray(numpy.nan_to_num(imag * mean), dtype=float)
|
3171
|
+
|
3172
|
+
mean_expanded = numpy.broadcast_to(mean, real.shape).copy()
|
3173
|
+
original_mean_expanded = mean_expanded.copy()
|
3174
|
+
real_filtered = real.copy()
|
3175
|
+
imag_filtered = imag.copy()
|
3176
|
+
|
3177
|
+
_, axes = parse_skip_axis(skip_axis, mean.ndim, True)
|
3178
|
+
|
3179
|
+
for index in numpy.ndindex(
|
3180
|
+
*(
|
3181
|
+
real.shape[ax]
|
3182
|
+
for ax in range(real.ndim)
|
3183
|
+
if ax not in axes and ax != 0
|
3184
|
+
)
|
3185
|
+
):
|
3186
|
+
index_list: list[int | slice] = list(index)
|
3187
|
+
for ax in axes:
|
3188
|
+
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
3189
|
+
full_index = tuple(index_list)
|
3190
|
+
|
3191
|
+
processed_harmonics = set()
|
3192
|
+
|
3193
|
+
for h in harmonics:
|
3194
|
+
if h in processed_harmonics and (
|
3195
|
+
h * 4 in harmonics or h * 2 not in harmonics
|
3196
|
+
):
|
3197
|
+
continue
|
3198
|
+
if h * 2 not in harmonics:
|
3199
|
+
raise ValueError(
|
3200
|
+
f'harmonic {h} does not have a corresponding half '
|
3201
|
+
f'or double harmonic in {harmonics}'
|
3202
|
+
)
|
3203
|
+
n = harmonics.index(h)
|
3204
|
+
n2 = harmonics.index(h * 2)
|
3205
|
+
|
3206
|
+
complex_phasor = numpy.empty(
|
3207
|
+
(3, *original_mean_expanded[n][full_index].shape),
|
3208
|
+
dtype=complex,
|
3209
|
+
)
|
3210
|
+
complex_phasor[0] = original_mean_expanded[n][full_index]
|
3211
|
+
complex_phasor[1] = real[n][full_index] + 1j * imag[n][full_index]
|
3212
|
+
complex_phasor[2] = (
|
3213
|
+
real[n2][full_index] + 1j * imag[n2][full_index]
|
3214
|
+
)
|
3215
|
+
|
3216
|
+
complex_phasor = pawflim(
|
3217
|
+
complex_phasor, n_sigmas=sigma, levels=levels
|
3218
|
+
)
|
3219
|
+
|
3220
|
+
for i, idx in enumerate([n, n2]):
|
3221
|
+
if harmonics[idx] in processed_harmonics:
|
3222
|
+
continue
|
3223
|
+
mean_expanded[idx][full_index] = complex_phasor[0].real
|
3224
|
+
real_filtered[idx][full_index] = complex_phasor[i + 1].real
|
3225
|
+
imag_filtered[idx][full_index] = complex_phasor[i + 1].imag
|
3226
|
+
|
3227
|
+
processed_harmonics.add(h)
|
3228
|
+
processed_harmonics.add(h * 2)
|
3229
|
+
|
3230
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
3231
|
+
real = numpy.asarray(numpy.divide(real_filtered, mean_expanded))
|
3232
|
+
imag = numpy.asarray(numpy.divide(imag_filtered, mean_expanded))
|
3233
|
+
|
3234
|
+
return mean, real, imag
|
3235
|
+
|
3236
|
+
|
2997
3237
|
def phasor_threshold(
|
2998
3238
|
mean: ArrayLike,
|
2999
3239
|
real: ArrayLike,
|
@@ -3302,7 +3542,7 @@ def phasor_center(
|
|
3302
3542
|
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
3303
3543
|
|
3304
3544
|
prepend_axis = mean.ndim + 1 == real.ndim
|
3305
|
-
_, axis =
|
3545
|
+
_, axis = parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
|
3306
3546
|
if prepend_axis:
|
3307
3547
|
mean = numpy.expand_dims(mean, axis=0)
|
3308
3548
|
|
@@ -3346,62 +3586,3 @@ def _median(
|
|
3346
3586
|
numpy.nanmedian(real, **kwargs),
|
3347
3587
|
numpy.nanmedian(imag, **kwargs),
|
3348
3588
|
)
|
3349
|
-
|
3350
|
-
|
3351
|
-
def _parse_skip_axis(
|
3352
|
-
skip_axis: int | Sequence[int] | None,
|
3353
|
-
/,
|
3354
|
-
ndim: int,
|
3355
|
-
prepend_axis: bool = False,
|
3356
|
-
) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
3357
|
-
"""Return axes to skip and not to skip.
|
3358
|
-
|
3359
|
-
This helper function is used to validate and parse `skip_axis`
|
3360
|
-
parameters.
|
3361
|
-
|
3362
|
-
Parameters
|
3363
|
-
----------
|
3364
|
-
skip_axis : int or sequence of int, optional
|
3365
|
-
Axes to skip. If None, no axes are skipped.
|
3366
|
-
ndim : int
|
3367
|
-
Dimensionality of array in which to skip axes.
|
3368
|
-
prepend_axis : bool, optional
|
3369
|
-
Prepend one dimension and include in `skip_axis`.
|
3370
|
-
|
3371
|
-
Returns
|
3372
|
-
-------
|
3373
|
-
skip_axis
|
3374
|
-
Ordered, positive values of `skip_axis`.
|
3375
|
-
other_axis
|
3376
|
-
Axes indices not included in `skip_axis`.
|
3377
|
-
|
3378
|
-
Raises
|
3379
|
-
------
|
3380
|
-
IndexError
|
3381
|
-
If any `skip_axis` value is out of bounds of `ndim`.
|
3382
|
-
|
3383
|
-
Examples
|
3384
|
-
--------
|
3385
|
-
>>> _parse_skip_axis((1, -2), 5)
|
3386
|
-
((1, 3), (0, 2, 4))
|
3387
|
-
|
3388
|
-
>>> _parse_skip_axis((1, -2), 5, True)
|
3389
|
-
((0, 2, 4), (1, 3, 5))
|
3390
|
-
|
3391
|
-
"""
|
3392
|
-
if ndim < 0:
|
3393
|
-
raise ValueError(f'invalid {ndim=}')
|
3394
|
-
if skip_axis is None:
|
3395
|
-
if prepend_axis:
|
3396
|
-
return (0,), tuple(range(1, ndim + 1))
|
3397
|
-
return (), tuple(range(ndim))
|
3398
|
-
if not isinstance(skip_axis, Sequence):
|
3399
|
-
skip_axis = (skip_axis,)
|
3400
|
-
if any(i >= ndim or i < -ndim for i in skip_axis):
|
3401
|
-
raise IndexError(f'skip_axis={skip_axis} out of range for {ndim=}')
|
3402
|
-
skip_axis = sorted(int(i % ndim) for i in skip_axis)
|
3403
|
-
if prepend_axis:
|
3404
|
-
skip_axis = [0] + [i + 1 for i in skip_axis]
|
3405
|
-
ndim += 1
|
3406
|
-
other_axis = tuple(i for i in range(ndim) if i not in skip_axis)
|
3407
|
-
return tuple(skip_axis), other_axis
|