phasorpy 0.3__cp311-cp311-win_arm64.whl → 0.5__cp311-cp311-win_arm64.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/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 = -1,
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
- The default is the last axis (-1).
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, real = numpy.atleast_1d(mean, real)
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.0 to :math:`\pi`. The default is the 8th sample.
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 .. 2 pi]')
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 semicircle phasor coordinates.
755
+ Real component of phasor coordinates on universal semicircle.
749
756
  imag : ndarray
750
- Imaginary component of semicircle phasor coordinates.
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 of `real` and `imag`.
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, reference_real.shape[0] if has_harmonic_axis else None
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(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)=}')
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
- donor_freting: ArrayLike = 1.0,
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..1].
2451
- donor_freting : array_like, optional, default 1
2452
- Fraction of donors participating in FRET. Range [0..1].
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
- ... donor_freting=0.9,
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
- donor_freting,
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
- donor_freting: ArrayLike = 1.0,
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..1].
2555
- donor_freting : array_like, optional, default 1
2556
- Fraction of donors participating in FRET. Range [0..1].
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
- ... donor_freting=0.9,
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
- donor_freting,
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, & Terenzi C.
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 = _parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
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 = _parse_skip_axis(skip_axis, mean.ndim, prepend_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