phasorpy 0.7__cp313-cp313-win_arm64.whl → 0.8__cp313-cp313-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
@@ -22,17 +22,15 @@ The ``phasorpy.phasor`` module provides functions to:
22
22
  - :py:func:`phasor_divide`
23
23
  - :py:func:`phasor_normalize`
24
24
 
25
+ - linearly combine two phasor coordinates:
26
+
27
+ - :py:func:`phasor_combine`
28
+
25
29
  - reduce dimensionality of arrays of phasor coordinates:
26
30
 
27
31
  - :py:func:`phasor_center`
28
32
  - :py:func:`phasor_to_principal_plane`
29
33
 
30
- - filter phasor coordinates:
31
-
32
- - :py:func:`phasor_filter_median`
33
- - :py:func:`phasor_filter_pawflim`
34
- - :py:func:`phasor_threshold`
35
-
36
34
  - find nearest neighbor phasor coordinates from other phasor coordinates:
37
35
 
38
36
  - :py:func:`phasor_nearest_neighbor`
@@ -44,14 +42,12 @@ from __future__ import annotations
44
42
  __all__ = [
45
43
  'phasor_center',
46
44
  'phasor_divide',
47
- 'phasor_filter_median',
48
- 'phasor_filter_pawflim',
49
45
  'phasor_from_polar',
50
46
  'phasor_from_signal',
47
+ 'phasor_combine',
51
48
  'phasor_multiply',
52
49
  'phasor_nearest_neighbor',
53
50
  'phasor_normalize',
54
- 'phasor_threshold',
55
51
  'phasor_to_complex',
56
52
  'phasor_to_polar',
57
53
  'phasor_to_principal_plane',
@@ -76,22 +72,18 @@ if TYPE_CHECKING:
76
72
  import numpy
77
73
 
78
74
  from ._phasorpy import (
79
- _median_filter_2d,
80
75
  _nearest_neighbor_2d,
76
+ _phasor_combine,
81
77
  _phasor_divide,
82
78
  _phasor_from_polar,
83
79
  _phasor_from_signal,
84
80
  _phasor_multiply,
85
- _phasor_threshold_closed,
86
- _phasor_threshold_mean_closed,
87
- _phasor_threshold_mean_open,
88
- _phasor_threshold_nan,
89
- _phasor_threshold_open,
90
81
  _phasor_to_polar,
91
82
  _phasor_transform,
92
83
  _phasor_transform_const,
93
84
  )
94
85
  from ._utils import parse_harmonic, parse_signal_axis, parse_skip_axis
86
+ from .filter import phasor_threshold
95
87
  from .utils import number_threads
96
88
 
97
89
 
@@ -142,7 +134,8 @@ def phasor_from_signal(
142
134
  calculated, or `rfft` is specified.
143
135
  rfft : callable, optional
144
136
  Drop-in replacement function for ``numpy.fft.rfft``.
145
- For example, ``scipy.fft.rfft`` or ``mkl_fft._numpy_fft.rfft``.
137
+ For example, ``scipy.fft.rfft`` or
138
+ ``mkl_fft.interfaces.numpy_fft.rfft``.
146
139
  Used to calculate the real forward FFT.
147
140
  dtype : dtype_like, optional
148
141
  Data type of output arrays. Either float32 or float64.
@@ -254,8 +247,8 @@ def phasor_from_signal(
254
247
  raise ValueError('sample_phase cannot be used with FFT')
255
248
  if num_harmonics > 1 or harmonic[0] != 1:
256
249
  raise ValueError('sample_phase cannot be used with harmonic != 1')
257
- sample_phase = numpy.atleast_1d(
258
- numpy.asarray(sample_phase, dtype=numpy.float64)
250
+ sample_phase = numpy.array(
251
+ sample_phase, dtype=numpy.float64, ndmin=1, order='C', copy=None
259
252
  )
260
253
  if sample_phase.ndim != 1 or sample_phase.size != samples:
261
254
  raise ValueError(f'{sample_phase.shape=} != ({samples},)')
@@ -277,10 +270,10 @@ def phasor_from_signal(
277
270
 
278
271
  mean = fft.take(0, axis=axis).real
279
272
  if not mean.ndim == 0:
280
- mean = numpy.ascontiguousarray(mean, dtype)
273
+ mean = numpy.ascontiguousarray(mean, dtype=dtype)
281
274
  fft = fft.take(harmonic, axis=axis)
282
- real = numpy.ascontiguousarray(fft.real, dtype)
283
- imag = numpy.ascontiguousarray(fft.imag, dtype)
275
+ real = numpy.ascontiguousarray(fft.real, dtype=dtype)
276
+ imag = numpy.ascontiguousarray(fft.imag, dtype=dtype)
284
277
  del fft
285
278
 
286
279
  if not keepdims and real.shape[axis] == 1:
@@ -327,7 +320,7 @@ def phasor_from_signal(
327
320
  shape1 = signal.shape[axis + 1 :]
328
321
  size0 = math.prod(shape0)
329
322
  size1 = math.prod(shape1)
330
- phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype)
323
+ phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype=dtype)
331
324
  signal = signal.reshape((size0, samples, size1))
332
325
 
333
326
  _phasor_from_signal(phasor, signal, sincos, normalize, num_threads)
@@ -386,7 +379,8 @@ def phasor_to_signal(
386
379
  The default is the last axis (-1).
387
380
  irfft : callable, optional
388
381
  Drop-in replacement function for ``numpy.fft.irfft``.
389
- For example, ``scipy.fft.irfft`` or ``mkl_fft._numpy_fft.irfft``.
382
+ For example, ``scipy.fft.irfft`` or
383
+ ``mkl_fft.interfaces.numpy_fft.irfft``.
390
384
  Used to calculate the real inverse FFT.
391
385
 
392
386
  Returns
@@ -444,8 +438,9 @@ def phasor_to_signal(
444
438
  else:
445
439
  keepdims = mean.ndim > 0 or real.ndim > 0
446
440
 
447
- mean = numpy.asarray(numpy.atleast_1d(mean))
448
- real = numpy.asarray(numpy.atleast_1d(real))
441
+ # mean, real = numpy.atleast_1d(mean, real) not working with Mypy
442
+ mean = numpy.array(mean, ndmin=1, copy=False)
443
+ real = numpy.array(real, ndmin=1, copy=False)
449
444
 
450
445
  if real.dtype.kind != 'f' or imag.dtype.kind != 'f':
451
446
  raise ValueError(f'{real.dtype=} or {imag.dtype=} not floating point')
@@ -537,7 +532,7 @@ def phasor_to_complex(
537
532
  if dtype.kind != 'c':
538
533
  raise ValueError(f'{dtype=} not a complex type')
539
534
 
540
- c = numpy.empty(numpy.broadcast(real, imag).shape, dtype)
535
+ c = numpy.empty(numpy.broadcast(real, imag).shape, dtype=dtype)
541
536
  c.real = real
542
537
  c.imag = imag
543
538
  return c
@@ -749,9 +744,9 @@ def phasor_normalize(
749
744
  ):
750
745
  real = real_unnormalized.copy()
751
746
  else:
752
- real = numpy.array(real_unnormalized, dtype, copy=True)
753
- imag = numpy.array(imag_unnormalized, real.dtype, copy=True)
754
- mean = numpy.array(mean_unnormalized, real.dtype, copy=True)
747
+ real = numpy.asarray(real_unnormalized, dtype=dtype, copy=True)
748
+ imag = numpy.asarray(imag_unnormalized, dtype=real.dtype, copy=True)
749
+ mean = numpy.asarray(mean_unnormalized, dtype=real.dtype, copy=True)
755
750
 
756
751
  with numpy.errstate(divide='ignore', invalid='ignore'):
757
752
  numpy.divide(real, mean, out=real)
@@ -762,6 +757,103 @@ def phasor_normalize(
762
757
  return mean, real, imag
763
758
 
764
759
 
760
+ def phasor_combine(
761
+ int0: ArrayLike,
762
+ real0: ArrayLike,
763
+ imag0: ArrayLike,
764
+ int1: ArrayLike,
765
+ real1: ArrayLike,
766
+ imag1: ArrayLike,
767
+ fraction0: ArrayLike,
768
+ fraction1: ArrayLike | None = None,
769
+ **kwargs: Any,
770
+ ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
771
+ r"""Return linear combination of two phasor coordinates.
772
+
773
+ Combine two sets of phasor coordinates using intensity-weighted mixing.
774
+ This simulates the phasor coordinates that would result from a mixture
775
+ of two components with known individual phasor coordinates.
776
+
777
+ Parameters
778
+ ----------
779
+ int0 : array_like
780
+ Intensity of first phasor coordinates.
781
+ real0 : array_like
782
+ Real component of first phasor coordinates.
783
+ imag0 : array_like
784
+ Imaginary component of first phasor coordinates.
785
+ int1 : array_like
786
+ Intensity of second phasor coordinates.
787
+ real1 : array_like
788
+ Real component of second phasor coordinates.
789
+ imag1 : array_like
790
+ Imaginary component of second phasor coordinates.
791
+ fraction0 : array_like
792
+ Fraction of first phasor coordinates.
793
+ fraction1 : array_like, optional
794
+ Fraction of second phasor coordinates.
795
+ The default is `1 - fraction0`.
796
+
797
+ **kwargs
798
+ Optional `arguments passed to numpy universal functions
799
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
800
+
801
+ Returns
802
+ -------
803
+ intensity : ndarray
804
+ Intensity of the linearly combined phasor coordinates.
805
+ real : ndarray
806
+ Real component of the linearly combined phasor coordinates.
807
+ imag : ndarray
808
+ Imaginary component of the linearly combined phasor coordinates.
809
+
810
+ Notes
811
+ -----
812
+ The phasor coordinates (:math:`I`, :math:`G`, :math:`S`) of the linear
813
+ combination of two phasor coordinates (:math:`I_{0}`, :math:`G_{0}`,
814
+ :math:`S_{0}`, and :math:`I_{1}`, :math:`G_{1}`, :math:`S_{1}`)
815
+ with fractions :math:`f_{0}` and :math:`f_{1}` of the first and second
816
+ coordinates are:
817
+
818
+ .. math::
819
+
820
+ f'_{0} &= f_{0} / (f_{0} + f_{1})
821
+
822
+ f'_{1} &= 1 - f'_{0}
823
+
824
+ I &= I_{0} \cdot f'_{0} + I_{1} \cdot f'_{1}
825
+
826
+ G &= (G_{0} \cdot I_{0} \cdot f'_{0}
827
+ + G_{1} \cdot I_{1} \cdot f'_{1}) / I
828
+
829
+ S &= (S_{0} \cdot I_{0} \cdot f'_{0}
830
+ + S_{1} \cdot I_{1} \cdot f'_{1}) / I
831
+
832
+ If the intensity :math:`I` is zero, the linear combined phasor coordinates
833
+ are undefined (NaN).
834
+
835
+ See Also
836
+ --------
837
+ phasorpy.component.phasor_from_component
838
+
839
+ Examples
840
+ --------
841
+ Calculate the linear combination of two phasor coordinates with equal
842
+ intensities at three fractional intensities:
843
+
844
+ >>> phasor_combine(1.0, 0.6, 0.3, 1.0, 0.4, 0.2, [1.0, 0.2, 0.9])
845
+ (array([1, 1, 1]), array([0.6, 0.44, 0.58]), array([0.3, 0.22, 0.29]))
846
+
847
+ """
848
+ if fraction1 is None:
849
+ fraction1 = numpy.asarray(fraction0, copy=True)
850
+ numpy.subtract(1.0, fraction0, out=fraction1)
851
+
852
+ return _phasor_combine( # type: ignore[no-any-return]
853
+ int0, real0, imag0, int1, real1, imag1, fraction0, fraction1, **kwargs
854
+ )
855
+
856
+
765
857
  def phasor_transform(
766
858
  real: ArrayLike,
767
859
  imag: ArrayLike,
@@ -1122,618 +1214,6 @@ def phasor_to_principal_plane(
1122
1214
  )
1123
1215
 
1124
1216
 
1125
- def phasor_filter_median(
1126
- mean: ArrayLike,
1127
- real: ArrayLike,
1128
- imag: ArrayLike,
1129
- /,
1130
- *,
1131
- repeat: int = 1,
1132
- size: int = 3,
1133
- skip_axis: int | Sequence[int] | None = None,
1134
- use_scipy: bool = False,
1135
- num_threads: int | None = None,
1136
- **kwargs: Any,
1137
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
1138
- """Return median-filtered phasor coordinates.
1139
-
1140
- By default, apply a NaN-aware median filter independently to the real
1141
- and imaginary components of phasor coordinates once with a kernel size of 3
1142
- multiplied by the number of dimensions of the input arrays. Return the
1143
- intensity unchanged.
1144
-
1145
- Parameters
1146
- ----------
1147
- mean : array_like
1148
- Intensity of phasor coordinates.
1149
- real : array_like
1150
- Real component of phasor coordinates to be filtered.
1151
- imag : array_like
1152
- Imaginary component of phasor coordinates to be filtered.
1153
- repeat : int, optional
1154
- Number of times to apply median filter. The default is 1.
1155
- size : int, optional
1156
- Size of median filter kernel. The default is 3.
1157
- skip_axis : int or sequence of int, optional
1158
- Axes in `mean` to exclude from filter.
1159
- By default, all axes except harmonics are included.
1160
- use_scipy : bool, optional
1161
- Use :py:func:`scipy.ndimage.median_filter`.
1162
- This function has undefined behavior if the input arrays contain
1163
- NaN values but is faster when filtering more than 2 dimensions.
1164
- See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
1165
- num_threads : int, optional
1166
- Number of OpenMP threads to use for parallelization.
1167
- Applies to filtering in two dimensions when not using scipy.
1168
- By default, multi-threading is disabled.
1169
- If zero, up to half of logical CPUs are used.
1170
- OpenMP may not be available on all platforms.
1171
- **kwargs
1172
- Optional arguments passed to :py:func:`scipy.ndimage.median_filter`.
1173
-
1174
- Returns
1175
- -------
1176
- mean : ndarray
1177
- Unchanged intensity of phasor coordinates.
1178
- real : ndarray
1179
- Filtered real component of phasor coordinates.
1180
- imag : ndarray
1181
- Filtered imaginary component of phasor coordinates.
1182
-
1183
- Raises
1184
- ------
1185
- ValueError
1186
- If `repeat` is less than 0.
1187
- If `size` is less than 1.
1188
- The array shapes of `mean`, `real`, and `imag` do not match.
1189
-
1190
- Examples
1191
- --------
1192
- Apply three times a median filter with a kernel size of three:
1193
-
1194
- >>> mean, real, imag = phasor_filter_median(
1195
- ... [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
1196
- ... [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5], [0.2, 0.2, 0.2]],
1197
- ... [[0.3, 0.3, 0.3], [0.6, math.nan, 0.6], [0.4, 0.4, 0.4]],
1198
- ... size=3,
1199
- ... repeat=3,
1200
- ... )
1201
- >>> mean
1202
- array([[1, 2, 3],
1203
- [4, 5, 6],
1204
- [7, 8, 9]])
1205
- >>> real
1206
- array([[0, 0, 0],
1207
- [0.2, 0.2, 0.2],
1208
- [0.2, 0.2, 0.2]])
1209
- >>> imag
1210
- array([[0.3, 0.3, 0.3],
1211
- [0.4, nan, 0.4],
1212
- [0.4, 0.4, 0.4]])
1213
-
1214
- """
1215
- if repeat < 0:
1216
- raise ValueError(f'{repeat=} < 0')
1217
- if size < 1:
1218
- raise ValueError(f'{size=} < 1')
1219
- if size == 1:
1220
- # no need to filter
1221
- repeat = 0
1222
-
1223
- mean = numpy.asarray(mean)
1224
- if use_scipy or repeat == 0: # or using nD numpy filter
1225
- real = numpy.asarray(real)
1226
- elif isinstance(real, numpy.ndarray) and real.dtype == numpy.float32:
1227
- real = real.copy()
1228
- else:
1229
- real = numpy.array(real, numpy.float64, copy=True)
1230
- if use_scipy or repeat == 0: # or using nD numpy filter
1231
- imag = numpy.asarray(imag)
1232
- elif isinstance(imag, numpy.ndarray) and imag.dtype == numpy.float32:
1233
- imag = imag.copy()
1234
- else:
1235
- imag = numpy.array(imag, numpy.float64, copy=True)
1236
-
1237
- if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
1238
- raise ValueError(f'{mean.shape=} != {real.shape=}')
1239
- if real.shape != imag.shape:
1240
- raise ValueError(f'{real.shape=} != {imag.shape=}')
1241
-
1242
- prepend_axis = mean.ndim + 1 == real.ndim
1243
- _, axes = parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
1244
-
1245
- # in case mean is also filtered
1246
- # if prepend_axis:
1247
- # mean = numpy.expand_dims(mean, axis=0)
1248
- # ...
1249
- # if prepend_axis:
1250
- # mean = numpy.asarray(mean[0])
1251
-
1252
- if repeat == 0:
1253
- # no need to call filter
1254
- return mean, real, imag
1255
-
1256
- if use_scipy:
1257
- # use scipy NaN-unaware fallback
1258
- from scipy.ndimage import median_filter
1259
-
1260
- kwargs.pop('axes', None)
1261
-
1262
- for _ in range(repeat):
1263
- real = median_filter(real, size=size, axes=axes, **kwargs)
1264
- imag = median_filter(imag, size=size, axes=axes, **kwargs)
1265
-
1266
- return mean, numpy.asarray(real), numpy.asarray(imag)
1267
-
1268
- if len(axes) != 2:
1269
- # n-dimensional median filter using numpy
1270
- from numpy.lib.stride_tricks import sliding_window_view
1271
-
1272
- kernel_shape = tuple(
1273
- size if i in axes else 1 for i in range(real.ndim)
1274
- )
1275
- pad_width = [
1276
- (s // 2, s // 2) if s > 1 else (0, 0) for s in kernel_shape
1277
- ]
1278
- axis = tuple(range(-real.ndim, 0))
1279
-
1280
- nan_mask = numpy.isnan(real)
1281
- for _ in range(repeat):
1282
- real = numpy.pad(real, pad_width, mode='edge')
1283
- real = sliding_window_view(real, kernel_shape)
1284
- real = numpy.nanmedian(real, axis=axis)
1285
- real = numpy.where(nan_mask, numpy.nan, real)
1286
-
1287
- nan_mask = numpy.isnan(imag)
1288
- for _ in range(repeat):
1289
- imag = numpy.pad(imag, pad_width, mode='edge')
1290
- imag = sliding_window_view(imag, kernel_shape)
1291
- imag = numpy.nanmedian(imag, axis=axis)
1292
- imag = numpy.where(nan_mask, numpy.nan, imag)
1293
-
1294
- return mean, real, imag
1295
-
1296
- # 2-dimensional median filter using optimized Cython implementation
1297
- num_threads = number_threads(num_threads)
1298
-
1299
- buffer = numpy.empty(
1300
- tuple(real.shape[axis] for axis in axes), dtype=real.dtype
1301
- )
1302
-
1303
- for index in numpy.ndindex(
1304
- *[real.shape[ax] for ax in range(real.ndim) if ax not in axes]
1305
- ):
1306
- index_list: list[int | slice] = list(index)
1307
- for ax in axes:
1308
- index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
1309
- full_index = tuple(index_list)
1310
-
1311
- _median_filter_2d(real[full_index], buffer, size, repeat, num_threads)
1312
- _median_filter_2d(imag[full_index], buffer, size, repeat, num_threads)
1313
-
1314
- return mean, real, imag
1315
-
1316
-
1317
- def phasor_filter_pawflim(
1318
- mean: ArrayLike,
1319
- real: ArrayLike,
1320
- imag: ArrayLike,
1321
- /,
1322
- *,
1323
- sigma: float = 2.0,
1324
- levels: int = 1,
1325
- harmonic: Sequence[int] | None = None,
1326
- skip_axis: int | Sequence[int] | None = None,
1327
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
1328
- """Return pawFLIM wavelet-filtered phasor coordinates.
1329
-
1330
- This function must only be used with calibrated, unprocessed phasor
1331
- coordinates obtained from FLIM data. The coordinates must not be filtered,
1332
- thresholded, or otherwise pre-processed.
1333
-
1334
- The pawFLIM wavelet filter is described in [2]_.
1335
-
1336
- Parameters
1337
- ----------
1338
- mean : array_like
1339
- Intensity of phasor coordinates.
1340
- real : array_like
1341
- Real component of phasor coordinates to be filtered.
1342
- Must have at least two harmonics in the first axis.
1343
- imag : array_like
1344
- Imaginary component of phasor coordinates to be filtered.
1345
- Must have at least two harmonics in the first axis.
1346
- sigma : float, optional
1347
- Significance level to test difference between two phasors.
1348
- Given in terms of the equivalent 1D standard deviations.
1349
- sigma=2 corresponds to ~95% (or 5%) significance.
1350
- levels : int, optional
1351
- Number of levels for wavelet decomposition.
1352
- Controls the maximum averaging area, which has a length of
1353
- :math:`2^level`.
1354
- harmonic : sequence of int or None, optional
1355
- Harmonics included in first axis of `real` and `imag`.
1356
- If None (default), the first axis of `real` and `imag` contains lower
1357
- harmonics starting at and increasing by one.
1358
- All harmonics must have a corresponding half or double harmonic.
1359
- skip_axis : int or sequence of int, optional
1360
- Axes in `mean` to exclude from filter.
1361
- By default, all axes except harmonics are included.
1362
-
1363
- Returns
1364
- -------
1365
- mean : ndarray
1366
- Unchanged intensity of phasor coordinates.
1367
- real : ndarray
1368
- Filtered real component of phasor coordinates.
1369
- imag : ndarray
1370
- Filtered imaginary component of phasor coordinates.
1371
-
1372
- Raises
1373
- ------
1374
- ValueError
1375
- If `level` is less than 0.
1376
- The array shapes of `mean`, `real`, and `imag` do not match.
1377
- If `real` and `imag` have no harmonic axis.
1378
- Number of harmonics in `harmonic` is less than 2 or does not match
1379
- the first axis of `real` and `imag`.
1380
- Not all harmonics in `harmonic` have a corresponding half
1381
- or double harmonic.
1382
-
1383
- References
1384
- ----------
1385
- .. [2] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
1386
- uncertainty to enable lower photon count in FLIM experiments
1387
- <https://doi.org/10.1088/2050-6120/aa72ab>`_.
1388
- *Methods Appl Fluoresc*, 5(2): 024016 (2017)
1389
-
1390
- Examples
1391
- --------
1392
- Apply a pawFLIM wavelet filter with four significance levels (sigma)
1393
- and three decomposition levels:
1394
-
1395
- >>> mean, real, imag = phasor_filter_pawflim(
1396
- ... [[1, 1], [1, 1]],
1397
- ... [[[0.5, 0.8], [0.5, 0.8]], [[0.2, 0.4], [0.2, 0.4]]],
1398
- ... [[[0.5, 0.4], [0.5, 0.4]], [[0.4, 0.5], [0.4, 0.5]]],
1399
- ... sigma=4,
1400
- ... levels=3,
1401
- ... harmonic=[1, 2],
1402
- ... )
1403
- >>> mean
1404
- array([[1, 1],
1405
- [1, 1]])
1406
- >>> real
1407
- array([[[0.65, 0.65],
1408
- [0.65, 0.65]],
1409
- [[0.3, 0.3],
1410
- [0.3, 0.3]]])
1411
- >>> imag
1412
- array([[[0.45, 0.45],
1413
- [0.45, 0.45]],
1414
- [[0.45, 0.45],
1415
- [0.45, 0.45]]])
1416
-
1417
- """
1418
- from pawflim import pawflim # type: ignore[import-untyped]
1419
-
1420
- mean = numpy.asarray(mean)
1421
- real = numpy.asarray(real)
1422
- imag = numpy.asarray(imag)
1423
-
1424
- if levels < 0:
1425
- raise ValueError(f'{levels=} < 0')
1426
- if levels == 0:
1427
- return mean, real, imag
1428
-
1429
- if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
1430
- raise ValueError(f'{mean.shape=} != {real.shape=}')
1431
- if real.shape != imag.shape:
1432
- raise ValueError(f'{real.shape=} != {imag.shape=}')
1433
-
1434
- has_harmonic_axis = mean.ndim + 1 == real.ndim
1435
- if not has_harmonic_axis:
1436
- raise ValueError('no harmonic axis')
1437
- if harmonic is None:
1438
- harmonics, _ = parse_harmonic('all', real.shape[0])
1439
- else:
1440
- harmonics, _ = parse_harmonic(harmonic, None)
1441
- if len(harmonics) < 2:
1442
- raise ValueError(
1443
- 'at least two harmonics required, ' f'got {len(harmonics)}'
1444
- )
1445
- if len(harmonics) != real.shape[0]:
1446
- raise ValueError(
1447
- 'number of harmonics does not match first axis of real and imag'
1448
- )
1449
-
1450
- mean = numpy.asarray(numpy.nan_to_num(mean), dtype=float)
1451
- real = numpy.asarray(numpy.nan_to_num(real * mean), dtype=float)
1452
- imag = numpy.asarray(numpy.nan_to_num(imag * mean), dtype=float)
1453
-
1454
- mean_expanded = numpy.broadcast_to(mean, real.shape).copy()
1455
- original_mean_expanded = mean_expanded.copy()
1456
- real_filtered = real.copy()
1457
- imag_filtered = imag.copy()
1458
-
1459
- _, axes = parse_skip_axis(skip_axis, mean.ndim, True)
1460
-
1461
- for index in numpy.ndindex(
1462
- *(
1463
- real.shape[ax]
1464
- for ax in range(real.ndim)
1465
- if ax not in axes and ax != 0
1466
- )
1467
- ):
1468
- index_list: list[int | slice] = list(index)
1469
- for ax in axes:
1470
- index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
1471
- full_index = tuple(index_list)
1472
-
1473
- processed_harmonics = set()
1474
-
1475
- for h in harmonics:
1476
- if h in processed_harmonics and (
1477
- h * 4 in harmonics or h * 2 not in harmonics
1478
- ):
1479
- continue
1480
- if h * 2 not in harmonics:
1481
- raise ValueError(
1482
- f'harmonic {h} does not have a corresponding half '
1483
- f'or double harmonic in {harmonics}'
1484
- )
1485
- n = harmonics.index(h)
1486
- n2 = harmonics.index(h * 2)
1487
-
1488
- complex_phasor = numpy.empty(
1489
- (3, *original_mean_expanded[n][full_index].shape),
1490
- dtype=complex,
1491
- )
1492
- complex_phasor[0] = original_mean_expanded[n][full_index]
1493
- complex_phasor[1] = real[n][full_index] + 1j * imag[n][full_index]
1494
- complex_phasor[2] = (
1495
- real[n2][full_index] + 1j * imag[n2][full_index]
1496
- )
1497
-
1498
- complex_phasor = pawflim(
1499
- complex_phasor, n_sigmas=sigma, levels=levels
1500
- )
1501
-
1502
- for i, idx in enumerate([n, n2]):
1503
- if harmonics[idx] in processed_harmonics:
1504
- continue
1505
- mean_expanded[idx][full_index] = complex_phasor[0].real
1506
- real_filtered[idx][full_index] = complex_phasor[i + 1].real
1507
- imag_filtered[idx][full_index] = complex_phasor[i + 1].imag
1508
-
1509
- processed_harmonics.add(h)
1510
- processed_harmonics.add(h * 2)
1511
-
1512
- with numpy.errstate(divide='ignore', invalid='ignore'):
1513
- real = numpy.asarray(numpy.divide(real_filtered, mean_expanded))
1514
- imag = numpy.asarray(numpy.divide(imag_filtered, mean_expanded))
1515
-
1516
- return mean, real, imag
1517
-
1518
-
1519
- def phasor_threshold(
1520
- mean: ArrayLike,
1521
- real: ArrayLike,
1522
- imag: ArrayLike,
1523
- /,
1524
- mean_min: ArrayLike | None = None,
1525
- mean_max: ArrayLike | None = None,
1526
- *,
1527
- real_min: ArrayLike | None = None,
1528
- real_max: ArrayLike | None = None,
1529
- imag_min: ArrayLike | None = None,
1530
- imag_max: ArrayLike | None = None,
1531
- phase_min: ArrayLike | None = None,
1532
- phase_max: ArrayLike | None = None,
1533
- modulation_min: ArrayLike | None = None,
1534
- modulation_max: ArrayLike | None = None,
1535
- open_interval: bool = False,
1536
- detect_harmonics: bool = True,
1537
- **kwargs: Any,
1538
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
1539
- """Return phasor coordinates with values outside interval replaced by NaN.
1540
-
1541
- Interval thresholds can be set for mean intensity, real and imaginary
1542
- coordinates, and phase and modulation.
1543
- Phasor coordinates smaller than minimum thresholds or larger than maximum
1544
- thresholds are replaced with NaN.
1545
- No threshold is applied by default.
1546
- NaNs in `mean` or any `real` and `imag` harmonic are propagated to
1547
- `mean` and all harmonics in `real` and `imag`.
1548
-
1549
- Parameters
1550
- ----------
1551
- mean : array_like
1552
- Intensity of phasor coordinates.
1553
- real : array_like
1554
- Real component of phasor coordinates.
1555
- imag : array_like
1556
- Imaginary component of phasor coordinates.
1557
- mean_min : array_like, optional
1558
- Lower threshold for mean intensity.
1559
- mean_max : array_like, optional
1560
- Upper threshold for mean intensity.
1561
- real_min : array_like, optional
1562
- Lower threshold for real coordinates.
1563
- real_max : array_like, optional
1564
- Upper threshold for real coordinates.
1565
- imag_min : array_like, optional
1566
- Lower threshold for imaginary coordinates.
1567
- imag_max : array_like, optional
1568
- Upper threshold for imaginary coordinates.
1569
- phase_min : array_like, optional
1570
- Lower threshold for phase angle.
1571
- phase_max : array_like, optional
1572
- Upper threshold for phase angle.
1573
- modulation_min : array_like, optional
1574
- Lower threshold for modulation.
1575
- modulation_max : array_like, optional
1576
- Upper threshold for modulation.
1577
- open_interval : bool, optional
1578
- If true, the interval is open, and the threshold values are
1579
- not included in the interval.
1580
- If false (default), the interval is closed, and the threshold values
1581
- are included in the interval.
1582
- detect_harmonics : bool, optional
1583
- By default, detect presence of multiple harmonics from array shapes.
1584
- If false, no harmonics are assumed to be present, and the function
1585
- behaves like a numpy universal function.
1586
- **kwargs
1587
- Optional `arguments passed to numpy universal functions
1588
- <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1589
-
1590
- Returns
1591
- -------
1592
- mean : ndarray
1593
- Thresholded intensity of phasor coordinates.
1594
- real : ndarray
1595
- Thresholded real component of phasor coordinates.
1596
- imag : ndarray
1597
- Thresholded imaginary component of phasor coordinates.
1598
-
1599
- Examples
1600
- --------
1601
- Set phasor coordinates to NaN if mean intensity is smaller than 1.1:
1602
-
1603
- >>> phasor_threshold([1, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], 1.1)
1604
- (array([nan, 2, 3]), array([nan, 0.2, 0.3]), array([nan, 0.5, 0.6]))
1605
-
1606
- Set phasor coordinates to NaN if real component is smaller than 0.15 or
1607
- larger than 0.25:
1608
-
1609
- >>> phasor_threshold(
1610
- ... [1.0, 2.0, 3.0],
1611
- ... [0.1, 0.2, 0.3],
1612
- ... [0.4, 0.5, 0.6],
1613
- ... real_min=0.15,
1614
- ... real_max=0.25,
1615
- ... )
1616
- (array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
1617
-
1618
- Apply NaNs to other input arrays:
1619
-
1620
- >>> phasor_threshold(
1621
- ... [numpy.nan, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, numpy.nan]
1622
- ... )
1623
- (array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
1624
-
1625
- """
1626
- threshold_mean_only = None
1627
- if mean_min is None:
1628
- mean_min = numpy.nan
1629
- else:
1630
- threshold_mean_only = True
1631
- if mean_max is None:
1632
- mean_max = numpy.nan
1633
- else:
1634
- threshold_mean_only = True
1635
- if real_min is None:
1636
- real_min = numpy.nan
1637
- else:
1638
- threshold_mean_only = False
1639
- if real_max is None:
1640
- real_max = numpy.nan
1641
- else:
1642
- threshold_mean_only = False
1643
- if imag_min is None:
1644
- imag_min = numpy.nan
1645
- else:
1646
- threshold_mean_only = False
1647
- if imag_max is None:
1648
- imag_max = numpy.nan
1649
- else:
1650
- threshold_mean_only = False
1651
- if phase_min is None:
1652
- phase_min = numpy.nan
1653
- else:
1654
- threshold_mean_only = False
1655
- if phase_max is None:
1656
- phase_max = numpy.nan
1657
- else:
1658
- threshold_mean_only = False
1659
- if modulation_min is None:
1660
- modulation_min = numpy.nan
1661
- else:
1662
- threshold_mean_only = False
1663
- if modulation_max is None:
1664
- modulation_max = numpy.nan
1665
- else:
1666
- threshold_mean_only = False
1667
-
1668
- if detect_harmonics:
1669
- mean = numpy.asarray(mean)
1670
- real = numpy.asarray(real)
1671
- imag = numpy.asarray(imag)
1672
-
1673
- shape = numpy.broadcast_shapes(mean.shape, real.shape, imag.shape)
1674
- ndim = len(shape)
1675
-
1676
- has_harmonic_axis = (
1677
- # detect multi-harmonic in axis 0
1678
- mean.ndim + 1 == ndim
1679
- and real.shape == shape
1680
- and imag.shape == shape
1681
- and mean.shape == shape[-mean.ndim if mean.ndim else 1 :]
1682
- )
1683
- else:
1684
- has_harmonic_axis = False
1685
-
1686
- if threshold_mean_only is None:
1687
- mean, real, imag = _phasor_threshold_nan(mean, real, imag, **kwargs)
1688
-
1689
- elif threshold_mean_only:
1690
- mean_func = (
1691
- _phasor_threshold_mean_open
1692
- if open_interval
1693
- else _phasor_threshold_mean_closed
1694
- )
1695
- mean, real, imag = mean_func(
1696
- mean, real, imag, mean_min, mean_max, **kwargs
1697
- )
1698
-
1699
- else:
1700
- func = (
1701
- _phasor_threshold_open
1702
- if open_interval
1703
- else _phasor_threshold_closed
1704
- )
1705
- mean, real, imag = func(
1706
- mean,
1707
- real,
1708
- imag,
1709
- mean_min,
1710
- mean_max,
1711
- real_min,
1712
- real_max,
1713
- imag_min,
1714
- imag_max,
1715
- phase_min,
1716
- phase_max,
1717
- modulation_min,
1718
- modulation_max,
1719
- **kwargs,
1720
- )
1721
-
1722
- mean = numpy.asarray(mean)
1723
- real = numpy.asarray(real)
1724
- imag = numpy.asarray(imag)
1725
- if has_harmonic_axis and mean.ndim > 0:
1726
- # propagate NaN to all dimensions
1727
- mean = numpy.mean(mean, axis=0, keepdims=True)
1728
- mask = numpy.where(numpy.isnan(mean), numpy.nan, 1.0)
1729
- numpy.multiply(real, mask, out=real)
1730
- numpy.multiply(imag, mask, out=imag)
1731
- # remove harmonic dimension created by broadcasting
1732
- mean = numpy.asarray(numpy.asarray(mean)[0])
1733
-
1734
- return mean, real, imag
1735
-
1736
-
1737
1217
  def phasor_nearest_neighbor(
1738
1218
  real: ArrayLike,
1739
1219
  imag: ArrayLike,
@@ -1847,7 +1327,7 @@ def phasor_nearest_neighbor(
1847
1327
  neighbor_imag = neighbor_imag.ravel()
1848
1328
 
1849
1329
  indices = numpy.empty(
1850
- real.shape, numpy.min_scalar_type(-neighbor_real.size)
1330
+ real.shape, dtype=numpy.min_scalar_type(-neighbor_real.size)
1851
1331
  )
1852
1332
 
1853
1333
  if distance_max is None: