phasorpy 0.7__cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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/__init__.py +9 -0
- phasorpy/__main__.py +7 -0
- phasorpy/_phasorpy.cpython-311-aarch64-linux-gnu.so +0 -0
- phasorpy/_phasorpy.pyx +2688 -0
- phasorpy/_typing.py +77 -0
- phasorpy/_utils.py +786 -0
- phasorpy/cli.py +160 -0
- phasorpy/cluster.py +200 -0
- phasorpy/color.py +589 -0
- phasorpy/component.py +707 -0
- phasorpy/conftest.py +38 -0
- phasorpy/cursor.py +500 -0
- phasorpy/datasets.py +722 -0
- phasorpy/experimental.py +310 -0
- phasorpy/io/__init__.py +138 -0
- phasorpy/io/_flimlabs.py +360 -0
- phasorpy/io/_leica.py +331 -0
- phasorpy/io/_ometiff.py +444 -0
- phasorpy/io/_other.py +890 -0
- phasorpy/io/_simfcs.py +652 -0
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +2018 -0
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +723 -0
- phasorpy/plot/_lifetime_plots.py +563 -0
- phasorpy/plot/_phasorplot.py +1507 -0
- phasorpy/plot/_phasorplot_fret.py +561 -0
- phasorpy/py.typed +0 -0
- phasorpy/utils.py +172 -0
- phasorpy-0.7.dist-info/METADATA +74 -0
- phasorpy-0.7.dist-info/RECORD +36 -0
- phasorpy-0.7.dist-info/WHEEL +7 -0
- phasorpy-0.7.dist-info/entry_points.txt +2 -0
- phasorpy-0.7.dist-info/licenses/LICENSE.txt +21 -0
- phasorpy-0.7.dist-info/top_level.txt +1 -0
- phasorpy.libs/libgomp-947d5fa1.so.1.0.0 +0 -0
phasorpy/_utils.py
ADDED
@@ -0,0 +1,786 @@
|
|
1
|
+
"""Private auxiliary and convenience functions."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
__all__ = [
|
6
|
+
'chunk_iter',
|
7
|
+
'dilate_coordinates',
|
8
|
+
'init_module',
|
9
|
+
'kwargs_notnone',
|
10
|
+
'parse_harmonic',
|
11
|
+
'parse_kwargs',
|
12
|
+
'parse_signal_axis',
|
13
|
+
'parse_skip_axis',
|
14
|
+
'phasor_from_polar_scalar',
|
15
|
+
'phasor_to_polar_scalar',
|
16
|
+
'scale_matrix',
|
17
|
+
'sort_coordinates',
|
18
|
+
'squeeze_dims',
|
19
|
+
'update_kwargs',
|
20
|
+
'xarray_metadata',
|
21
|
+
]
|
22
|
+
|
23
|
+
import math
|
24
|
+
import numbers
|
25
|
+
import os
|
26
|
+
import sys
|
27
|
+
from collections.abc import Sequence
|
28
|
+
from typing import TYPE_CHECKING
|
29
|
+
|
30
|
+
if TYPE_CHECKING:
|
31
|
+
from ._typing import (
|
32
|
+
Any,
|
33
|
+
ArrayLike,
|
34
|
+
Literal,
|
35
|
+
NDArray,
|
36
|
+
Iterator,
|
37
|
+
Container,
|
38
|
+
PathLike,
|
39
|
+
)
|
40
|
+
|
41
|
+
import numpy
|
42
|
+
|
43
|
+
|
44
|
+
def parse_kwargs(
|
45
|
+
kwargs: dict[str, Any],
|
46
|
+
/,
|
47
|
+
*keys: str,
|
48
|
+
_del: bool = True,
|
49
|
+
**keyvalues: Any,
|
50
|
+
) -> dict[str, Any]:
|
51
|
+
"""Return dict with keys from keys|keyvals and values from kwargs|keyvals.
|
52
|
+
|
53
|
+
If `_del` is true (default), existing keys are deleted from `kwargs`.
|
54
|
+
|
55
|
+
Parameters
|
56
|
+
----------
|
57
|
+
kwargs : dict
|
58
|
+
Source dictionary to extract keys from.
|
59
|
+
*keys : str
|
60
|
+
Keys to extract from kwargs if present.
|
61
|
+
_del : bool, default: True
|
62
|
+
If True, remove extracted keys from kwargs.
|
63
|
+
**keyvalues : Any
|
64
|
+
Key-value pairs. If key exists in kwargs, use kwargs value,
|
65
|
+
otherwise use provided default value.
|
66
|
+
|
67
|
+
Returns
|
68
|
+
-------
|
69
|
+
dict
|
70
|
+
Dictionary containing extracted keys and values.
|
71
|
+
|
72
|
+
>>> kwargs = {'one': 1, 'two': 2, 'four': 4}
|
73
|
+
>>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
|
74
|
+
>>> kwargs == {'one': 1}
|
75
|
+
True
|
76
|
+
>>> kwargs2 == {'two': 2, 'four': 4, 'five': 5}
|
77
|
+
True
|
78
|
+
|
79
|
+
"""
|
80
|
+
result = {}
|
81
|
+
for key in keys:
|
82
|
+
if key in kwargs:
|
83
|
+
result[key] = kwargs[key]
|
84
|
+
if _del:
|
85
|
+
del kwargs[key]
|
86
|
+
for key, value in keyvalues.items():
|
87
|
+
if key in kwargs:
|
88
|
+
result[key] = kwargs[key]
|
89
|
+
if _del:
|
90
|
+
del kwargs[key]
|
91
|
+
else:
|
92
|
+
result[key] = value
|
93
|
+
return result
|
94
|
+
|
95
|
+
|
96
|
+
def update_kwargs(kwargs: dict[str, Any], /, **keyvalues: Any) -> None:
|
97
|
+
"""Update dict with keys and values if keys do not already exist.
|
98
|
+
|
99
|
+
>>> kwargs = {'one': 1}
|
100
|
+
>>> update_kwargs(kwargs, one=None, two=2)
|
101
|
+
>>> kwargs == {'one': 1, 'two': 2}
|
102
|
+
True
|
103
|
+
|
104
|
+
"""
|
105
|
+
for key, value in keyvalues.items():
|
106
|
+
if key not in kwargs:
|
107
|
+
kwargs[key] = value
|
108
|
+
|
109
|
+
|
110
|
+
def kwargs_notnone(**kwargs: Any) -> dict[str, Any]:
|
111
|
+
"""Return dict of kwargs which values are not None.
|
112
|
+
|
113
|
+
>>> kwargs_notnone(one=1, none=None)
|
114
|
+
{'one': 1}
|
115
|
+
|
116
|
+
"""
|
117
|
+
return dict(item for item in kwargs.items() if item[1] is not None)
|
118
|
+
|
119
|
+
|
120
|
+
def scale_matrix(factor: float, origin: Sequence[float]) -> NDArray[Any]:
|
121
|
+
"""Return matrix to scale homogeneous coordinates by factor around origin.
|
122
|
+
|
123
|
+
Parameters
|
124
|
+
----------
|
125
|
+
factor : float
|
126
|
+
Scale factor.
|
127
|
+
origin : (float, float)
|
128
|
+
Coordinates of point around which to scale.
|
129
|
+
|
130
|
+
Returns
|
131
|
+
-------
|
132
|
+
matrix : ndarray
|
133
|
+
A 3x3 homogeneous transformation matrix.
|
134
|
+
|
135
|
+
Examples
|
136
|
+
--------
|
137
|
+
>>> scale_matrix(1.1, [0.0, 0.5])
|
138
|
+
array([[1.1, 0, -0],
|
139
|
+
[0, 1.1, -0.05],
|
140
|
+
[0, 0, 1]])
|
141
|
+
|
142
|
+
"""
|
143
|
+
mat = numpy.diag((factor, factor, 1.0))
|
144
|
+
mat[:2, 2] = origin[:2]
|
145
|
+
mat[:2, 2] *= 1.0 - factor
|
146
|
+
return mat
|
147
|
+
|
148
|
+
|
149
|
+
def sort_coordinates(
|
150
|
+
real: ArrayLike,
|
151
|
+
imag: ArrayLike,
|
152
|
+
/,
|
153
|
+
origin: ArrayLike | None = None,
|
154
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
155
|
+
"""Return cartesian coordinates sorted counterclockwise around origin.
|
156
|
+
|
157
|
+
Parameters
|
158
|
+
----------
|
159
|
+
real, imag : array_like
|
160
|
+
Coordinates to be sorted.
|
161
|
+
origin : array_like, optional
|
162
|
+
Coordinates around which to sort by angle.
|
163
|
+
By default, sort around the mean of `real` and `imag`.
|
164
|
+
|
165
|
+
Returns
|
166
|
+
-------
|
167
|
+
real : ndarray
|
168
|
+
Sorted real coordinates.
|
169
|
+
imag : ndarray
|
170
|
+
Sorted imaginary coordinates.
|
171
|
+
indices : ndarray
|
172
|
+
Indices used to reorder coordinates.
|
173
|
+
Use ``indices.argsort()`` to get original order.
|
174
|
+
|
175
|
+
Examples
|
176
|
+
--------
|
177
|
+
>>> sort_coordinates([0, 1, 2, 3], [0, 1, -1, 0])
|
178
|
+
(array([2, 3, 1, 0]), array([-1, 0, 1, 0]), array([2, 3, 1, 0]...))
|
179
|
+
|
180
|
+
"""
|
181
|
+
x, y = numpy.atleast_1d(real, imag)
|
182
|
+
if x.ndim != 1 or x.shape != y.shape:
|
183
|
+
raise ValueError(f'invalid {x.shape=} or {y.shape=}')
|
184
|
+
if x.size < 3:
|
185
|
+
return x, y, numpy.arange(x.size)
|
186
|
+
if origin is None:
|
187
|
+
ox, oy = x.mean(), y.mean()
|
188
|
+
else:
|
189
|
+
origin = numpy.asarray(origin, dtype=numpy.float64)
|
190
|
+
ox = origin[0]
|
191
|
+
oy = origin[1]
|
192
|
+
indices = numpy.argsort(numpy.arctan2(y - oy, x - ox))
|
193
|
+
return x[indices], y[indices], indices
|
194
|
+
|
195
|
+
|
196
|
+
def dilate_coordinates(
|
197
|
+
real: ArrayLike,
|
198
|
+
imag: ArrayLike,
|
199
|
+
offset: float,
|
200
|
+
/,
|
201
|
+
) -> tuple[NDArray[Any], NDArray[Any]]:
|
202
|
+
"""Return dilated coordinates.
|
203
|
+
|
204
|
+
Parameters
|
205
|
+
----------
|
206
|
+
real, imag : array_like
|
207
|
+
Coordinates of convex hull, sorted by angle.
|
208
|
+
offset : float
|
209
|
+
Amount by which to dilate coordinates.
|
210
|
+
|
211
|
+
Returns
|
212
|
+
-------
|
213
|
+
real : ndarray
|
214
|
+
Dilated real coordinates.
|
215
|
+
imag : ndarray
|
216
|
+
Dilated imaginary coordinates.
|
217
|
+
|
218
|
+
Examples
|
219
|
+
--------
|
220
|
+
>>> dilate_coordinates([2, 3, 1, 0], [-1, 0, 1, 0], 0.05)
|
221
|
+
(array([2.022, 3.05, 0.9776, -0.05]), array([-1.045, 0, 1.045, 0]))
|
222
|
+
|
223
|
+
"""
|
224
|
+
x = numpy.asanyarray(real, dtype=numpy.float64)
|
225
|
+
y = numpy.asanyarray(imag, dtype=numpy.float64)
|
226
|
+
if x.ndim != 1 or x.shape != y.shape or x.size < 1:
|
227
|
+
raise ValueError(f'invalid {x.shape=} or {y.shape=}')
|
228
|
+
if x.size > 1:
|
229
|
+
dx = numpy.diff(numpy.diff(x, prepend=x[-1], append=x[0]))
|
230
|
+
dy = numpy.diff(numpy.diff(y, prepend=y[-1], append=y[0]))
|
231
|
+
else:
|
232
|
+
# TODO: this assumes coordinate on universal semicircle
|
233
|
+
dx = numpy.diff(x, append=0.5)
|
234
|
+
dy = numpy.diff(y, append=0.0)
|
235
|
+
s = numpy.hypot(dx, dy)
|
236
|
+
dx /= s
|
237
|
+
dx *= -offset
|
238
|
+
dx += x
|
239
|
+
dy /= s
|
240
|
+
dy *= -offset
|
241
|
+
dy += y
|
242
|
+
return dx, dy
|
243
|
+
|
244
|
+
|
245
|
+
def phasor_to_polar_scalar(
|
246
|
+
real: float,
|
247
|
+
imag: float,
|
248
|
+
/,
|
249
|
+
*,
|
250
|
+
degree: bool = False,
|
251
|
+
percent: bool = False,
|
252
|
+
) -> tuple[float, float]:
|
253
|
+
"""Return polar from scalar phasor coordinates.
|
254
|
+
|
255
|
+
Parameters
|
256
|
+
----------
|
257
|
+
real : float
|
258
|
+
Real component of phasor coordinate.
|
259
|
+
imag : float
|
260
|
+
Imaginary component of phasor coordinate.
|
261
|
+
degree : bool, optional
|
262
|
+
If true, return phase in degrees instead of radians.
|
263
|
+
percent : bool, optional
|
264
|
+
If true, return modulation as percentage instead of fraction.
|
265
|
+
|
266
|
+
Returns
|
267
|
+
-------
|
268
|
+
phase : float
|
269
|
+
Phase angle in radians (or degrees if degree=True).
|
270
|
+
modulation : float
|
271
|
+
Modulation depth as fraction (or percentage if percent=True).
|
272
|
+
|
273
|
+
Examples
|
274
|
+
--------
|
275
|
+
>>> phasor_to_polar_scalar(0.0, 1.0, degree=True, percent=True)
|
276
|
+
(90.0, 100.0)
|
277
|
+
|
278
|
+
"""
|
279
|
+
phi = math.atan2(imag, real)
|
280
|
+
mod = math.hypot(imag, real)
|
281
|
+
if degree:
|
282
|
+
phi = math.degrees(phi)
|
283
|
+
if percent:
|
284
|
+
mod *= 100.0
|
285
|
+
return phi, mod
|
286
|
+
|
287
|
+
|
288
|
+
def phasor_from_polar_scalar(
|
289
|
+
phase: float,
|
290
|
+
modulation: float,
|
291
|
+
/,
|
292
|
+
*,
|
293
|
+
degree: bool = False,
|
294
|
+
percent: bool = False,
|
295
|
+
) -> tuple[float, float]:
|
296
|
+
"""Return phasor from scalar polar coordinates.
|
297
|
+
|
298
|
+
Parameters
|
299
|
+
----------
|
300
|
+
phase : float
|
301
|
+
Phase angle in radians (or degrees if degree=True).
|
302
|
+
modulation : float
|
303
|
+
Modulation depth as fraction (or percentage if percent=True).
|
304
|
+
degree : bool, optional
|
305
|
+
If true, phase is in degrees instead of radians.
|
306
|
+
percent : bool, optional
|
307
|
+
If true, modulation is as percentage instead of fraction.
|
308
|
+
|
309
|
+
Returns
|
310
|
+
-------
|
311
|
+
real : float
|
312
|
+
Real component of phasor coordinate.
|
313
|
+
imag : float
|
314
|
+
Imaginary component of phasor coordinate.
|
315
|
+
|
316
|
+
Examples
|
317
|
+
--------
|
318
|
+
>>> phasor_from_polar_scalar(0.0, 100.0, degree=True, percent=True)
|
319
|
+
(1.0, 0.0)
|
320
|
+
|
321
|
+
"""
|
322
|
+
if degree:
|
323
|
+
phase = math.radians(phase)
|
324
|
+
if percent:
|
325
|
+
modulation /= 100.0
|
326
|
+
real = modulation * math.cos(phase)
|
327
|
+
imag = modulation * math.sin(phase)
|
328
|
+
return real, imag
|
329
|
+
|
330
|
+
|
331
|
+
def parse_signal_axis(
|
332
|
+
signal: ArrayLike,
|
333
|
+
/,
|
334
|
+
axis: int | str | None = None,
|
335
|
+
) -> tuple[int, str]:
|
336
|
+
"""Return axis over which phasor coordinates are computed.
|
337
|
+
|
338
|
+
The axis parameter is not validated against the signal shape.
|
339
|
+
|
340
|
+
Parameters
|
341
|
+
----------
|
342
|
+
signal : array_like
|
343
|
+
Signal array.
|
344
|
+
Axis names are used if it has a `dims` attribute.
|
345
|
+
axis : int, str, or None, default: None
|
346
|
+
Axis over which to compute phasor coordinates.
|
347
|
+
If None, automatically selects 'H' or 'C' axis if available,
|
348
|
+
otherwise uses the last axis (-1).
|
349
|
+
If int, specifies axis index.
|
350
|
+
If str, specifies axis name (requires `signal.dims`).
|
351
|
+
|
352
|
+
Returns
|
353
|
+
-------
|
354
|
+
axis : int
|
355
|
+
Index of axis over which phasor coordinates are computed.
|
356
|
+
axis_label : str
|
357
|
+
Label of axis from `signal.dims` if available, empty string otherwise.
|
358
|
+
|
359
|
+
Raises
|
360
|
+
------
|
361
|
+
ValueError
|
362
|
+
If axis string is not found in signal.dims.
|
363
|
+
If axis string is provided but signal has no dims attribute.
|
364
|
+
|
365
|
+
Examples
|
366
|
+
--------
|
367
|
+
>>> parse_signal_axis([])
|
368
|
+
(-1, '')
|
369
|
+
>>> parse_signal_axis([], 1)
|
370
|
+
(1, '')
|
371
|
+
>>> class DataArray:
|
372
|
+
... dims = ('C', 'H', 'Y', 'X')
|
373
|
+
...
|
374
|
+
>>> parse_signal_axis(DataArray())
|
375
|
+
(1, 'H')
|
376
|
+
>>> parse_signal_axis(DataArray(), 'C')
|
377
|
+
(0, 'C')
|
378
|
+
>>> parse_signal_axis(DataArray(), 1)
|
379
|
+
(1, 'H')
|
380
|
+
|
381
|
+
"""
|
382
|
+
if hasattr(signal, 'dims'):
|
383
|
+
assert isinstance(signal.dims, tuple)
|
384
|
+
if axis is None:
|
385
|
+
for ax in 'HC':
|
386
|
+
if ax in signal.dims:
|
387
|
+
return signal.dims.index(ax), ax
|
388
|
+
return -1, signal.dims[-1]
|
389
|
+
if isinstance(axis, int):
|
390
|
+
return axis, signal.dims[axis]
|
391
|
+
if axis in signal.dims:
|
392
|
+
return signal.dims.index(axis), axis
|
393
|
+
raise ValueError(f'{axis=} not found in {signal.dims}')
|
394
|
+
if axis is None:
|
395
|
+
return -1, ''
|
396
|
+
if isinstance(axis, int):
|
397
|
+
return axis, ''
|
398
|
+
raise ValueError(f'{axis=} not valid for {type(signal)=}')
|
399
|
+
|
400
|
+
|
401
|
+
def parse_skip_axis(
|
402
|
+
skip_axis: int | Sequence[int] | None,
|
403
|
+
/,
|
404
|
+
ndim: int,
|
405
|
+
prepend_axis: bool = False,
|
406
|
+
) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
407
|
+
"""Return axes to skip and not to skip.
|
408
|
+
|
409
|
+
This helper function is used to validate and parse `skip_axis`
|
410
|
+
parameters.
|
411
|
+
|
412
|
+
Parameters
|
413
|
+
----------
|
414
|
+
skip_axis : int or sequence of int, optional
|
415
|
+
Axes to skip. If None, no axes are skipped.
|
416
|
+
ndim : int
|
417
|
+
Dimensionality of array in which to skip axes.
|
418
|
+
prepend_axis : bool, optional
|
419
|
+
Prepend one dimension and include in `skip_axis`.
|
420
|
+
|
421
|
+
Returns
|
422
|
+
-------
|
423
|
+
skip_axis : tuple of int
|
424
|
+
Ordered, positive values of `skip_axis`.
|
425
|
+
other_axis : tuple of int
|
426
|
+
Axes indices not included in `skip_axis`.
|
427
|
+
|
428
|
+
Raises
|
429
|
+
------
|
430
|
+
ValueError
|
431
|
+
If ndim is negative.
|
432
|
+
IndexError
|
433
|
+
If any `skip_axis` value is out of bounds of `ndim`.
|
434
|
+
|
435
|
+
Examples
|
436
|
+
--------
|
437
|
+
>>> parse_skip_axis([1, -2], 5)
|
438
|
+
((1, 3), (0, 2, 4))
|
439
|
+
|
440
|
+
>>> parse_skip_axis([1, -2], 5, True)
|
441
|
+
((0, 2, 4), (1, 3, 5))
|
442
|
+
|
443
|
+
"""
|
444
|
+
if ndim < 0:
|
445
|
+
raise ValueError(f'invalid {ndim=}')
|
446
|
+
if skip_axis is None:
|
447
|
+
if prepend_axis:
|
448
|
+
return (0,), tuple(range(1, ndim + 1))
|
449
|
+
return (), tuple(range(ndim))
|
450
|
+
if not isinstance(skip_axis, Sequence):
|
451
|
+
skip_axis = (skip_axis,)
|
452
|
+
if any(i >= ndim or i < -ndim for i in skip_axis):
|
453
|
+
raise IndexError(f'skip_axis={skip_axis} out of range for {ndim=}')
|
454
|
+
skip_axis = sorted(int(i % ndim) for i in skip_axis)
|
455
|
+
if prepend_axis:
|
456
|
+
skip_axis = [0] + [i + 1 for i in skip_axis]
|
457
|
+
ndim += 1
|
458
|
+
other_axis = tuple(i for i in range(ndim) if i not in skip_axis)
|
459
|
+
return tuple(skip_axis), other_axis
|
460
|
+
|
461
|
+
|
462
|
+
def parse_harmonic(
|
463
|
+
harmonic: int | Sequence[int] | Literal['all'] | str | None,
|
464
|
+
harmonic_max: int | None = None,
|
465
|
+
/,
|
466
|
+
) -> tuple[list[int], bool]:
|
467
|
+
"""Return parsed harmonic parameter.
|
468
|
+
|
469
|
+
This function performs common, but not necessarily all, verifications
|
470
|
+
of user-provided `harmonic` parameter.
|
471
|
+
|
472
|
+
Parameters
|
473
|
+
----------
|
474
|
+
harmonic : int, sequence of int, 'all', or None
|
475
|
+
Harmonic parameter to parse.
|
476
|
+
harmonic_max : int, optional
|
477
|
+
Maximum value allowed in `harmonic`. Must be one or greater.
|
478
|
+
To verify against known number of signal samples,
|
479
|
+
pass ``samples // 2``.
|
480
|
+
If `harmonic='all'`, a range of harmonics from one to `harmonic_max`
|
481
|
+
(included) is returned.
|
482
|
+
|
483
|
+
Returns
|
484
|
+
-------
|
485
|
+
harmonic : list of int
|
486
|
+
Parsed list of harmonics.
|
487
|
+
has_harmonic_axis : bool
|
488
|
+
False if `harmonic` input parameter is a scalar integer.
|
489
|
+
|
490
|
+
Raises
|
491
|
+
------
|
492
|
+
IndexError
|
493
|
+
Any element is out of range `[1, harmonic_max]`.
|
494
|
+
ValueError
|
495
|
+
Elements are not unique.
|
496
|
+
Harmonic is empty.
|
497
|
+
String input is not 'all'.
|
498
|
+
`harmonic_max` is smaller than 1.
|
499
|
+
TypeError
|
500
|
+
Any element is not an integer.
|
501
|
+
`harmonic` is `'all'` and `harmonic_max` is None.
|
502
|
+
|
503
|
+
"""
|
504
|
+
if harmonic_max is not None and harmonic_max < 1:
|
505
|
+
raise ValueError(f'{harmonic_max=} < 1')
|
506
|
+
|
507
|
+
if harmonic is None:
|
508
|
+
return [1], False
|
509
|
+
|
510
|
+
if isinstance(harmonic, (int, numbers.Integral)):
|
511
|
+
if harmonic < 1 or (
|
512
|
+
harmonic_max is not None and harmonic > harmonic_max
|
513
|
+
):
|
514
|
+
raise IndexError(f'{harmonic=} out of range [1, {harmonic_max}]')
|
515
|
+
return [int(harmonic)], False
|
516
|
+
|
517
|
+
if isinstance(harmonic, str):
|
518
|
+
if harmonic == 'all':
|
519
|
+
if harmonic_max is None:
|
520
|
+
raise TypeError(
|
521
|
+
f'maximum harmonic must be specified for {harmonic=!r}'
|
522
|
+
)
|
523
|
+
return list(range(1, harmonic_max + 1)), True
|
524
|
+
raise ValueError(f'{harmonic=!r} is not a valid harmonic')
|
525
|
+
|
526
|
+
h = numpy.atleast_1d(harmonic)
|
527
|
+
if h.size == 0:
|
528
|
+
raise ValueError(f'{harmonic=} is empty')
|
529
|
+
if h.dtype.kind not in 'iu' or h.ndim != 1:
|
530
|
+
raise TypeError(f'{harmonic=} element not an integer')
|
531
|
+
if numpy.any(h < 1):
|
532
|
+
raise IndexError(f'{harmonic=} element < 1')
|
533
|
+
if harmonic_max is not None and numpy.any(h > harmonic_max):
|
534
|
+
raise IndexError(f'{harmonic=} element > {harmonic_max}]')
|
535
|
+
if numpy.unique(h).size != h.size:
|
536
|
+
raise ValueError(f'{harmonic=} elements must be unique')
|
537
|
+
return [int(i) for i in harmonic], True
|
538
|
+
|
539
|
+
|
540
|
+
def chunk_iter(
|
541
|
+
shape: tuple[int, ...],
|
542
|
+
chunk_shape: tuple[int, ...],
|
543
|
+
/,
|
544
|
+
dims: Sequence[str] | None = None,
|
545
|
+
*,
|
546
|
+
pattern: str | None = None,
|
547
|
+
squeeze: bool = False,
|
548
|
+
use_index: bool = False,
|
549
|
+
) -> Iterator[tuple[tuple[int | slice, ...], str, bool]]:
|
550
|
+
"""Yield indices and labels of chunks from ndarray's shape.
|
551
|
+
|
552
|
+
Parameters
|
553
|
+
----------
|
554
|
+
shape : tuple of int
|
555
|
+
Shape of C-order ndarray to chunk.
|
556
|
+
chunk_shape : tuple of int
|
557
|
+
Shape of chunks in the most significant dimensions.
|
558
|
+
dims : sequence of str, optional
|
559
|
+
Labels for each axis in shape if `pattern` is None.
|
560
|
+
pattern : str, optional
|
561
|
+
String to format chunk indices.
|
562
|
+
If None, use ``_[{dims[index]}{chunk_index[index]}]`` for each axis.
|
563
|
+
squeeze : bool, optional
|
564
|
+
If true, do not include length-1 chunked dimensions in label
|
565
|
+
unless dimensions are part of `chunk_shape`.
|
566
|
+
Applies only if `pattern` is None.
|
567
|
+
use_index : bool, optional
|
568
|
+
If true, use indices of chunks in `shape` instead of chunk indices to
|
569
|
+
format pattern.
|
570
|
+
|
571
|
+
Yields
|
572
|
+
------
|
573
|
+
index : tuple of int or slice
|
574
|
+
Indices of chunk in ndarray.
|
575
|
+
label : str
|
576
|
+
Pattern formatted with chunk indices.
|
577
|
+
cropped : bool
|
578
|
+
True if chunk exceeds any border of ndarray.
|
579
|
+
Indexing ndarray with `index` will yield a slice smaller than
|
580
|
+
`chunk_shape`.
|
581
|
+
|
582
|
+
Examples
|
583
|
+
--------
|
584
|
+
|
585
|
+
>>> list(chunk_iter((2, 2), (2,), pattern='Y{}'))
|
586
|
+
[((0, slice(0, 2, 1)), 'Y0', False), ((1, slice(0, 2, 1)), 'Y1', False)]
|
587
|
+
|
588
|
+
Chunk a four-dimensional image stack into 2x2 sized image tiles:
|
589
|
+
|
590
|
+
>>> stack = numpy.zeros((2, 3, 4, 5))
|
591
|
+
>>> for index, label, cropped in chunk_iter(stack.shape, (2, 2)):
|
592
|
+
... chunk = stack[index]
|
593
|
+
...
|
594
|
+
|
595
|
+
"""
|
596
|
+
ndim = len(shape)
|
597
|
+
|
598
|
+
sep = '_'
|
599
|
+
if dims is None:
|
600
|
+
dims = sep * ndim
|
601
|
+
sep = ''
|
602
|
+
elif ndim != len(dims):
|
603
|
+
raise ValueError(f'{len(shape)=} != {len(dims)=}')
|
604
|
+
|
605
|
+
if pattern is not None:
|
606
|
+
try:
|
607
|
+
pattern.format(*shape)
|
608
|
+
except Exception as exc:
|
609
|
+
raise ValueError('pattern cannot be formatted') from exc
|
610
|
+
|
611
|
+
# number of high dimensions not included in chaunk_shape
|
612
|
+
hdim = ndim - len(chunk_shape)
|
613
|
+
if hdim < 0:
|
614
|
+
raise ValueError(f'{len(shape)=} < {len(chunk_shape)=}')
|
615
|
+
if hdim > 0:
|
616
|
+
# prepend length-1 dimensions
|
617
|
+
chunk_shape = ((1,) * hdim) + chunk_shape
|
618
|
+
|
619
|
+
chunked_shape = []
|
620
|
+
pattern_list = []
|
621
|
+
for i, (size, chunk_size, ax) in enumerate(zip(shape, chunk_shape, dims)):
|
622
|
+
if size <= 0:
|
623
|
+
raise ValueError('shape must contain positive sizes')
|
624
|
+
if chunk_size <= 0:
|
625
|
+
raise ValueError('chunk_shape must contain positive sizes')
|
626
|
+
div, mod = divmod(size, chunk_size)
|
627
|
+
chunked_shape.append(div + 1 if mod else div)
|
628
|
+
|
629
|
+
if not squeeze or chunked_shape[-1] > 1:
|
630
|
+
if use_index:
|
631
|
+
digits = int(math.log10(size)) + 1
|
632
|
+
else:
|
633
|
+
digits = int(math.log10(chunked_shape[-1])) + 1
|
634
|
+
pattern_list.append(f'{sep}{ax}{{{i}:0{digits}d}}')
|
635
|
+
|
636
|
+
if pattern is None:
|
637
|
+
pattern = ''.join(pattern_list)
|
638
|
+
|
639
|
+
chunk_index: tuple[int, ...]
|
640
|
+
for chunk_index in numpy.ndindex(tuple(chunked_shape)):
|
641
|
+
index: tuple[int | slice, ...] = tuple(
|
642
|
+
(
|
643
|
+
chunk_index[i]
|
644
|
+
if i < hdim
|
645
|
+
else slice(
|
646
|
+
chunk_index[i] * chunk_shape[i],
|
647
|
+
(chunk_index[i] + 1) * chunk_shape[i],
|
648
|
+
1,
|
649
|
+
)
|
650
|
+
)
|
651
|
+
for i in range(ndim)
|
652
|
+
)
|
653
|
+
if use_index:
|
654
|
+
format_index = tuple(
|
655
|
+
chunk_index[i] * chunk_shape[i] for i in range(ndim)
|
656
|
+
)
|
657
|
+
else:
|
658
|
+
format_index = chunk_index
|
659
|
+
yield (
|
660
|
+
index,
|
661
|
+
pattern.format(*format_index),
|
662
|
+
any(
|
663
|
+
(chunk_index[i] + 1) * chunk_shape[i] > shape[i]
|
664
|
+
for i in range(ndim)
|
665
|
+
),
|
666
|
+
)
|
667
|
+
|
668
|
+
|
669
|
+
def init_module(globs: dict[str, Any], /) -> None:
|
670
|
+
"""Add names in module to ``__all__`` and set ``__module__`` attributes.
|
671
|
+
|
672
|
+
Parameters
|
673
|
+
----------
|
674
|
+
globs : dict
|
675
|
+
Module namespace to modify.
|
676
|
+
|
677
|
+
Examples
|
678
|
+
--------
|
679
|
+
>>> init_module(globals())
|
680
|
+
|
681
|
+
"""
|
682
|
+
names = globs['__all__']
|
683
|
+
module_name = globs['__name__']
|
684
|
+
module = sys.modules[module_name]
|
685
|
+
for name in dir(module):
|
686
|
+
if name.startswith('_') or name in {
|
687
|
+
'annotations',
|
688
|
+
'init_module',
|
689
|
+
'utils', # TODO: where does this come from?
|
690
|
+
}:
|
691
|
+
continue
|
692
|
+
names.append(name)
|
693
|
+
obj = getattr(module, name)
|
694
|
+
if hasattr(obj, '__module__'):
|
695
|
+
obj.__module__ = module_name
|
696
|
+
globs['__all__'] = sorted(set(names))
|
697
|
+
|
698
|
+
|
699
|
+
def xarray_metadata(
|
700
|
+
dims: Sequence[str] | None,
|
701
|
+
shape: tuple[int, ...],
|
702
|
+
/,
|
703
|
+
name: str | PathLike[Any] | None = None,
|
704
|
+
attrs: dict[str, Any] | None = None,
|
705
|
+
**coords: Any,
|
706
|
+
) -> dict[str, Any]:
|
707
|
+
"""Return xarray-style dims, coords, and attrs in a dict.
|
708
|
+
|
709
|
+
>>> xarray_metadata('SYX', (3, 2, 1), S=['0', '1', '2'])
|
710
|
+
{'dims': ('S', 'Y', 'X'), 'coords': {'S': ['0', '1', '2']}, 'attrs': {}}
|
711
|
+
|
712
|
+
"""
|
713
|
+
assert dims is not None
|
714
|
+
dims = tuple(dims)
|
715
|
+
if len(dims) != len(shape):
|
716
|
+
raise ValueError(
|
717
|
+
f'dims do not match shape {len(dims)} != {len(shape)}'
|
718
|
+
)
|
719
|
+
coords = {dim: coords[dim] for dim in dims if dim in coords}
|
720
|
+
if attrs is None:
|
721
|
+
attrs = {}
|
722
|
+
metadata = {'dims': dims, 'coords': coords, 'attrs': attrs}
|
723
|
+
if name:
|
724
|
+
metadata['name'] = os.path.basename(name)
|
725
|
+
return metadata
|
726
|
+
|
727
|
+
|
728
|
+
def squeeze_dims(
|
729
|
+
shape: Sequence[int],
|
730
|
+
dims: Sequence[str],
|
731
|
+
/,
|
732
|
+
skip: Container[str] = 'XY',
|
733
|
+
) -> tuple[tuple[int, ...], tuple[str, ...], tuple[bool, ...]]:
|
734
|
+
"""Return shape and axes with length-1 dimensions removed.
|
735
|
+
|
736
|
+
Remove unused dimensions unless their axes are listed in the `skip`
|
737
|
+
parameter.
|
738
|
+
|
739
|
+
Adapted from the tifffile library.
|
740
|
+
|
741
|
+
Parameters
|
742
|
+
----------
|
743
|
+
shape : tuple of ints
|
744
|
+
Sequence of dimension sizes.
|
745
|
+
dims : sequence of str
|
746
|
+
Character codes for dimensions in `shape`.
|
747
|
+
skip : container of str, optional
|
748
|
+
Character codes for dimensions whose length-1 dimensions are
|
749
|
+
not removed. The default is 'XY'.
|
750
|
+
|
751
|
+
Returns
|
752
|
+
-------
|
753
|
+
shape : tuple of ints
|
754
|
+
Sequence of dimension sizes with length-1 dimensions removed.
|
755
|
+
dims : tuple of str
|
756
|
+
Character codes for dimensions in output `shape`.
|
757
|
+
squeezed : str
|
758
|
+
Dimensions were kept (True) or removed (False).
|
759
|
+
|
760
|
+
Examples
|
761
|
+
--------
|
762
|
+
>>> squeeze_dims((5, 1, 2, 1, 1), 'TZYXC')
|
763
|
+
((5, 2, 1), ('T', 'Y', 'X'), (True, False, True, True, False))
|
764
|
+
>>> squeeze_dims((1,), ('Q',))
|
765
|
+
((1,), ('Q',), (True,))
|
766
|
+
|
767
|
+
"""
|
768
|
+
if len(shape) != len(dims):
|
769
|
+
raise ValueError(f'{len(shape)=} != {len(dims)=}')
|
770
|
+
if not dims:
|
771
|
+
return tuple(shape), tuple(dims), ()
|
772
|
+
squeezed: list[bool] = []
|
773
|
+
shape_squeezed: list[int] = []
|
774
|
+
dims_squeezed: list[str] = []
|
775
|
+
for size, ax in zip(shape, dims):
|
776
|
+
if size > 1 or ax in skip:
|
777
|
+
squeezed.append(True)
|
778
|
+
shape_squeezed.append(size)
|
779
|
+
dims_squeezed.append(ax)
|
780
|
+
else:
|
781
|
+
squeezed.append(False)
|
782
|
+
if len(shape_squeezed) == 0:
|
783
|
+
squeezed[-1] = True
|
784
|
+
shape_squeezed.append(shape[-1])
|
785
|
+
dims_squeezed.append(dims[-1])
|
786
|
+
return tuple(shape_squeezed), tuple(dims_squeezed), tuple(squeezed)
|