phasorpy 0.1__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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 +10 -0
- phasorpy/__main__.py +7 -0
- phasorpy/_phasorpy.cpython-313-x86_64-linux-gnu.so +0 -0
- phasorpy/_phasorpy.pyx +1811 -0
- phasorpy/_typing.py +77 -0
- phasorpy/_utils.py +441 -0
- phasorpy/cli.py +87 -0
- phasorpy/color.py +581 -0
- phasorpy/components.py +313 -0
- phasorpy/conftest.py +36 -0
- phasorpy/cursors.py +502 -0
- phasorpy/datasets.py +433 -0
- phasorpy/io.py +1671 -0
- phasorpy/phasor.py +3135 -0
- phasorpy/plot.py +2074 -0
- phasorpy/py.typed +0 -0
- phasorpy/utils.py +68 -0
- phasorpy/version.py +71 -0
- phasorpy-0.1.dist-info/LICENSE.txt +21 -0
- phasorpy-0.1.dist-info/METADATA +78 -0
- phasorpy-0.1.dist-info/RECORD +25 -0
- phasorpy-0.1.dist-info/WHEEL +6 -0
- phasorpy-0.1.dist-info/entry_points.txt +2 -0
- phasorpy-0.1.dist-info/top_level.txt +1 -0
- phasorpy.libs/libgomp-a34b3233.so.1.0.0 +0 -0
phasorpy/_typing.py
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
"""Type annotations.
|
2
|
+
|
3
|
+
This module should only be imported when type-checking, for example::
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from ._typing import Any, ArrayLike, PathLike
|
11
|
+
|
12
|
+
"""
|
13
|
+
|
14
|
+
# flake8: noqa: F401
|
15
|
+
# pylint: disable=unused-import
|
16
|
+
# autoflake: skip_file
|
17
|
+
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
'Any',
|
22
|
+
'ArrayLike',
|
23
|
+
'Callable',
|
24
|
+
'Collection',
|
25
|
+
'Container',
|
26
|
+
'DTypeLike',
|
27
|
+
'DataArray',
|
28
|
+
'EllipsisType',
|
29
|
+
'IO',
|
30
|
+
'ItemsView',
|
31
|
+
'Iterable',
|
32
|
+
'Iterator',
|
33
|
+
'KeysView',
|
34
|
+
'Literal',
|
35
|
+
'Mapping',
|
36
|
+
'NDArray',
|
37
|
+
'Optional',
|
38
|
+
'PathLike',
|
39
|
+
'Pooch',
|
40
|
+
'Sequence',
|
41
|
+
'TextIO',
|
42
|
+
'Union',
|
43
|
+
'ValuesView',
|
44
|
+
'cast',
|
45
|
+
'final',
|
46
|
+
'overload',
|
47
|
+
]
|
48
|
+
|
49
|
+
from collections.abc import (
|
50
|
+
Callable,
|
51
|
+
Collection,
|
52
|
+
Container,
|
53
|
+
ItemsView,
|
54
|
+
Iterable,
|
55
|
+
Iterator,
|
56
|
+
KeysView,
|
57
|
+
Mapping,
|
58
|
+
Sequence,
|
59
|
+
ValuesView,
|
60
|
+
)
|
61
|
+
from os import PathLike
|
62
|
+
from types import EllipsisType
|
63
|
+
from typing import (
|
64
|
+
IO,
|
65
|
+
Any,
|
66
|
+
Literal,
|
67
|
+
Optional,
|
68
|
+
TextIO,
|
69
|
+
Union,
|
70
|
+
cast,
|
71
|
+
final,
|
72
|
+
overload,
|
73
|
+
)
|
74
|
+
|
75
|
+
from numpy.typing import ArrayLike, DTypeLike, NDArray
|
76
|
+
from pooch import Pooch
|
77
|
+
from xarray import DataArray
|
phasorpy/_utils.py
ADDED
@@ -0,0 +1,441 @@
|
|
1
|
+
"""Private auxiliary and convenience functions.
|
2
|
+
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
__all__: list[str] = [
|
8
|
+
'chunk_iter',
|
9
|
+
'dilate_coordinates',
|
10
|
+
'kwargs_notnone',
|
11
|
+
'parse_harmonic',
|
12
|
+
'parse_kwargs',
|
13
|
+
'phasor_from_polar_scalar',
|
14
|
+
'phasor_to_polar_scalar',
|
15
|
+
'scale_matrix',
|
16
|
+
'sort_coordinates',
|
17
|
+
'update_kwargs',
|
18
|
+
]
|
19
|
+
|
20
|
+
import math
|
21
|
+
import numbers
|
22
|
+
from typing import TYPE_CHECKING
|
23
|
+
|
24
|
+
if TYPE_CHECKING:
|
25
|
+
from ._typing import Any, Sequence, ArrayLike, Literal, NDArray, Iterator
|
26
|
+
|
27
|
+
import numpy
|
28
|
+
|
29
|
+
|
30
|
+
def parse_kwargs(
|
31
|
+
kwargs: dict[str, Any],
|
32
|
+
/,
|
33
|
+
*keys: str,
|
34
|
+
_del: bool = True,
|
35
|
+
**keyvalues: Any,
|
36
|
+
) -> dict[str, Any]:
|
37
|
+
"""Return dict with keys from keys|keyvals and values from kwargs|keyvals.
|
38
|
+
|
39
|
+
If `_del` is true (default), existing keys are deleted from `kwargs`.
|
40
|
+
|
41
|
+
>>> kwargs = {'one': 1, 'two': 2, 'four': 4}
|
42
|
+
>>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
|
43
|
+
>>> kwargs == {'one': 1}
|
44
|
+
True
|
45
|
+
>>> kwargs2 == {'two': 2, 'four': 4, 'five': 5}
|
46
|
+
True
|
47
|
+
|
48
|
+
"""
|
49
|
+
result = {}
|
50
|
+
for key in keys:
|
51
|
+
if key in kwargs:
|
52
|
+
result[key] = kwargs[key]
|
53
|
+
if _del:
|
54
|
+
del kwargs[key]
|
55
|
+
for key, value in keyvalues.items():
|
56
|
+
if key in kwargs:
|
57
|
+
result[key] = kwargs[key]
|
58
|
+
if _del:
|
59
|
+
del kwargs[key]
|
60
|
+
else:
|
61
|
+
result[key] = value
|
62
|
+
return result
|
63
|
+
|
64
|
+
|
65
|
+
def update_kwargs(kwargs: dict[str, Any], /, **keyvalues: Any) -> None:
|
66
|
+
"""Update dict with keys and values if keys do not already exist.
|
67
|
+
|
68
|
+
>>> kwargs = {'one': 1}
|
69
|
+
>>> update_kwargs(kwargs, one=None, two=2)
|
70
|
+
>>> kwargs == {'one': 1, 'two': 2}
|
71
|
+
True
|
72
|
+
|
73
|
+
"""
|
74
|
+
for key, value in keyvalues.items():
|
75
|
+
if key not in kwargs:
|
76
|
+
kwargs[key] = value
|
77
|
+
|
78
|
+
|
79
|
+
def kwargs_notnone(**kwargs: Any) -> dict[str, Any]:
|
80
|
+
"""Return dict of kwargs which values are not None.
|
81
|
+
|
82
|
+
>>> kwargs_notnone(one=1, none=None)
|
83
|
+
{'one': 1}
|
84
|
+
|
85
|
+
"""
|
86
|
+
return dict(item for item in kwargs.items() if item[1] is not None)
|
87
|
+
|
88
|
+
|
89
|
+
def scale_matrix(factor: float, origin: Sequence[float]) -> NDArray[Any]:
|
90
|
+
"""Return matrix to scale homogeneous coordinates by factor around origin.
|
91
|
+
|
92
|
+
Parameters
|
93
|
+
----------
|
94
|
+
factor: float
|
95
|
+
Scale factor.
|
96
|
+
origin: (float, float)
|
97
|
+
Coordinates of point around which to scale.
|
98
|
+
|
99
|
+
Returns
|
100
|
+
-------
|
101
|
+
matrix: ndarray
|
102
|
+
A 3x3 homogeneous transformation matrix.
|
103
|
+
|
104
|
+
Examples
|
105
|
+
--------
|
106
|
+
>>> scale_matrix(1.1, (0.0, 0.5))
|
107
|
+
array([[1.1, 0, -0],
|
108
|
+
[0, 1.1, -0.05],
|
109
|
+
[0, 0, 1]])
|
110
|
+
|
111
|
+
"""
|
112
|
+
mat = numpy.diag((factor, factor, 1.0))
|
113
|
+
mat[:2, 2] = origin[:2]
|
114
|
+
mat[:2, 2] *= 1.0 - factor
|
115
|
+
return mat
|
116
|
+
|
117
|
+
|
118
|
+
def sort_coordinates(
|
119
|
+
real: ArrayLike,
|
120
|
+
imag: ArrayLike,
|
121
|
+
/,
|
122
|
+
origin: tuple[float, float] | None = None,
|
123
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
124
|
+
"""Return cartesian coordinates sorted counterclockwise around origin.
|
125
|
+
|
126
|
+
Parameters
|
127
|
+
----------
|
128
|
+
real, imag : array_like
|
129
|
+
Coordinates to be sorted.
|
130
|
+
origin : (float, float)
|
131
|
+
Coordinates around which to sort by angle.
|
132
|
+
|
133
|
+
Returns
|
134
|
+
-------
|
135
|
+
real, imag : ndarray
|
136
|
+
Coordinates sorted by angle.
|
137
|
+
indices : ndarray
|
138
|
+
Indices used to reorder coordinates.
|
139
|
+
|
140
|
+
Examples
|
141
|
+
--------
|
142
|
+
>>> sort_coordinates([0, 1, 2, 3], [0, 1, -1, 0])
|
143
|
+
(array([2, 3, 1, 0]), array([-1, 0, 1, 0]), array([2, 3, 1, 0]...))
|
144
|
+
|
145
|
+
"""
|
146
|
+
x, y = numpy.atleast_1d(real, imag)
|
147
|
+
if x.ndim != 1 or x.shape != y.shape:
|
148
|
+
raise ValueError(f'invalid {x.shape=} or {y.shape=}')
|
149
|
+
if x.size < 4:
|
150
|
+
return x, y, numpy.arange(x.size)
|
151
|
+
if origin is None:
|
152
|
+
origin = x.mean(), y.mean()
|
153
|
+
indices = numpy.argsort(numpy.arctan2(y - origin[1], x - origin[0]))
|
154
|
+
return x[indices], y[indices], indices
|
155
|
+
|
156
|
+
|
157
|
+
def dilate_coordinates(
|
158
|
+
real: ArrayLike,
|
159
|
+
imag: ArrayLike,
|
160
|
+
offset: float,
|
161
|
+
/,
|
162
|
+
) -> tuple[NDArray[Any], NDArray[Any]]:
|
163
|
+
"""Return dilated coordinates.
|
164
|
+
|
165
|
+
Parameters
|
166
|
+
----------
|
167
|
+
real, imag : array_like
|
168
|
+
Coordinates of convex hull, sorted by angle.
|
169
|
+
offset : float
|
170
|
+
Amount by which to dilate coordinates.
|
171
|
+
|
172
|
+
Returns
|
173
|
+
-------
|
174
|
+
real, imag : ndarray
|
175
|
+
Coordinates dilated by offset.
|
176
|
+
|
177
|
+
Examples
|
178
|
+
--------
|
179
|
+
>>> dilate_coordinates([2, 3, 1, 0], [-1, 0, 1, 0], 0.05)
|
180
|
+
(array([2.022, 3.05, 0.9776, -0.05]), array([-1.045, 0, 1.045, 0]))
|
181
|
+
|
182
|
+
"""
|
183
|
+
x = numpy.asanyarray(real, dtype=numpy.float64)
|
184
|
+
y = numpy.asanyarray(imag, dtype=numpy.float64)
|
185
|
+
if x.ndim != 1 or x.shape != y.shape or x.size < 1:
|
186
|
+
raise ValueError(f'invalid {x.shape=} or {y.shape=}')
|
187
|
+
if x.size > 1:
|
188
|
+
dx = numpy.diff(numpy.diff(x, prepend=x[-1], append=x[0]))
|
189
|
+
dy = numpy.diff(numpy.diff(y, prepend=y[-1], append=y[0]))
|
190
|
+
else:
|
191
|
+
# TODO: this assumes coordinate on universal semicircle
|
192
|
+
dx = numpy.diff(x, append=0.5)
|
193
|
+
dy = numpy.diff(y, append=0.0)
|
194
|
+
s = numpy.hypot(dx, dy)
|
195
|
+
dx /= s
|
196
|
+
dx *= -offset
|
197
|
+
dx += x
|
198
|
+
dy /= s
|
199
|
+
dy *= -offset
|
200
|
+
dy += y
|
201
|
+
return dx, dy
|
202
|
+
|
203
|
+
|
204
|
+
def phasor_to_polar_scalar(
|
205
|
+
real: float,
|
206
|
+
imag: float,
|
207
|
+
/,
|
208
|
+
*,
|
209
|
+
degree: bool = False,
|
210
|
+
percent: bool = False,
|
211
|
+
) -> tuple[float, float]:
|
212
|
+
"""Return polar from scalar phasor coordinates.
|
213
|
+
|
214
|
+
>>> phasor_to_polar_scalar(1.0, 0.0, degree=True, percent=True)
|
215
|
+
(0.0, 100.0)
|
216
|
+
|
217
|
+
"""
|
218
|
+
phi = math.atan2(imag, real)
|
219
|
+
mod = math.hypot(imag, real)
|
220
|
+
if degree:
|
221
|
+
phi = math.degrees(phi)
|
222
|
+
if percent:
|
223
|
+
mod *= 100.0
|
224
|
+
return phi, mod
|
225
|
+
|
226
|
+
|
227
|
+
def phasor_from_polar_scalar(
|
228
|
+
phase: float,
|
229
|
+
modulation: float,
|
230
|
+
/,
|
231
|
+
*,
|
232
|
+
degree: bool = False,
|
233
|
+
percent: bool = False,
|
234
|
+
) -> tuple[float, float]:
|
235
|
+
"""Return phasor from scalar polar coordinates.
|
236
|
+
|
237
|
+
>>> phasor_from_polar_scalar(0.0, 100.0, degree=True, percent=True)
|
238
|
+
(1.0, 0.0)
|
239
|
+
|
240
|
+
"""
|
241
|
+
if degree:
|
242
|
+
phase = math.radians(phase)
|
243
|
+
if percent:
|
244
|
+
modulation /= 100.0
|
245
|
+
real = modulation * math.cos(phase)
|
246
|
+
imag = modulation * math.sin(phase)
|
247
|
+
return real, imag
|
248
|
+
|
249
|
+
|
250
|
+
def parse_harmonic(
|
251
|
+
harmonic: int | Sequence[int] | Literal['all'] | str | None,
|
252
|
+
samples: int,
|
253
|
+
/,
|
254
|
+
) -> tuple[list[int], bool]:
|
255
|
+
"""Return parsed harmonic parameter.
|
256
|
+
|
257
|
+
Parameters
|
258
|
+
----------
|
259
|
+
harmonic : int, list of int, 'all', or None
|
260
|
+
Harmonic parameter to parse.
|
261
|
+
samples : int
|
262
|
+
Number of samples in signal.
|
263
|
+
Used to verify harmonic values and set maximum harmonic value.
|
264
|
+
|
265
|
+
Returns
|
266
|
+
-------
|
267
|
+
harmonic : list of int
|
268
|
+
Parsed list of harmonics.
|
269
|
+
has_harmonic_axis : bool
|
270
|
+
If true, `harmonic` input parameter is an integer, else a list.
|
271
|
+
|
272
|
+
Raises
|
273
|
+
------
|
274
|
+
IndexError
|
275
|
+
Any element is out of range [1..samples // 2].
|
276
|
+
ValueError
|
277
|
+
Elements are not unique.
|
278
|
+
Harmonic is empty.
|
279
|
+
String input is not 'all'.
|
280
|
+
TypeError
|
281
|
+
Any element is not an integer.
|
282
|
+
|
283
|
+
"""
|
284
|
+
if samples < 3:
|
285
|
+
raise ValueError(f'{samples=} < 3')
|
286
|
+
|
287
|
+
if harmonic is None:
|
288
|
+
return [1], False
|
289
|
+
|
290
|
+
harmonic_max = samples // 2
|
291
|
+
if isinstance(harmonic, (int, numbers.Integral)):
|
292
|
+
if harmonic < 1 or harmonic > harmonic_max:
|
293
|
+
raise IndexError(f'{harmonic=} out of range [1..{harmonic_max}]')
|
294
|
+
return [int(harmonic)], False
|
295
|
+
|
296
|
+
if isinstance(harmonic, str):
|
297
|
+
if harmonic == 'all':
|
298
|
+
return list(range(1, harmonic_max + 1)), True
|
299
|
+
raise ValueError(f'{harmonic=!r} is not a valid harmonic')
|
300
|
+
|
301
|
+
h = numpy.atleast_1d(numpy.asarray(harmonic))
|
302
|
+
if h.size == 0:
|
303
|
+
raise ValueError(f'{harmonic=} is empty')
|
304
|
+
if h.dtype.kind not in 'iu' or h.ndim != 1:
|
305
|
+
raise TypeError(f'{harmonic=} element not an integer')
|
306
|
+
if numpy.any(h < 1) or numpy.any(h > harmonic_max):
|
307
|
+
raise IndexError(
|
308
|
+
f'{harmonic=} element out of range [1..{harmonic_max}]'
|
309
|
+
)
|
310
|
+
if numpy.unique(h).size != h.size:
|
311
|
+
raise ValueError(f'{harmonic=} elements must be unique')
|
312
|
+
return h.tolist(), True
|
313
|
+
|
314
|
+
|
315
|
+
def chunk_iter(
|
316
|
+
shape: tuple[int, ...],
|
317
|
+
chunk_shape: tuple[int, ...],
|
318
|
+
/,
|
319
|
+
axes: str | Sequence[str] | None = None,
|
320
|
+
*,
|
321
|
+
pattern: str | None = None,
|
322
|
+
squeeze: bool = False,
|
323
|
+
use_index: bool = False,
|
324
|
+
) -> Iterator[tuple[tuple[int | slice, ...], str, bool]]:
|
325
|
+
"""Yield indices and labels of chunks from ndarray's shape.
|
326
|
+
|
327
|
+
Parameters
|
328
|
+
----------
|
329
|
+
shape : tuple of int
|
330
|
+
Shape of C-order ndarray to chunk.
|
331
|
+
chunk_shape : tuple of int
|
332
|
+
Shape of chunks in the most significant dimensions.
|
333
|
+
axes : str or sequence of str, optional
|
334
|
+
Labels for each axis in shape if `pattern` is None.
|
335
|
+
pattern : str, optional
|
336
|
+
String to format chunk indices.
|
337
|
+
If None, use ``_[{axes[index]}{chunk_index[index]}]`` for each axis.
|
338
|
+
squeeze : bool
|
339
|
+
If true, do not include length-1 chunked dimensions in label
|
340
|
+
unless dimensions are part of `chunk_shape`.
|
341
|
+
Applies only if `pattern` is None.
|
342
|
+
use_index : bool
|
343
|
+
If true, use indices of chunks in `shape` instead of chunk indices to
|
344
|
+
format pattern.
|
345
|
+
|
346
|
+
Yields
|
347
|
+
------
|
348
|
+
index : tuple of int or slice
|
349
|
+
Indices of chunk in ndarray.
|
350
|
+
label : str
|
351
|
+
Pattern formatted with chunk indices.
|
352
|
+
cropped : bool
|
353
|
+
True if chunk exceeds any border of ndarray.
|
354
|
+
Indexing ndarray with `index` will yield a slice smaller than
|
355
|
+
`chunk_shape`.
|
356
|
+
|
357
|
+
Examples
|
358
|
+
--------
|
359
|
+
|
360
|
+
>>> list(chunk_iter((2, 2), (2,), pattern='Y{}'))
|
361
|
+
[((0, slice(0, 2, 1)), 'Y0', False), ((1, slice(0, 2, 1)), 'Y1', False)]
|
362
|
+
|
363
|
+
Chunk a four-dimensional image stack into 2x2 sized image tiles:
|
364
|
+
|
365
|
+
>>> stack = numpy.zeros((2, 3, 4, 5))
|
366
|
+
>>> for index, label, cropped in chunk_iter(stack.shape, (2, 2)):
|
367
|
+
... chunk = stack[index]
|
368
|
+
...
|
369
|
+
|
370
|
+
"""
|
371
|
+
ndim = len(shape)
|
372
|
+
|
373
|
+
sep = '_'
|
374
|
+
if axes is None:
|
375
|
+
axes = sep * ndim
|
376
|
+
sep = ''
|
377
|
+
elif ndim != len(axes):
|
378
|
+
raise ValueError(f'{len(shape)=} != {len(axes)=}')
|
379
|
+
|
380
|
+
if pattern is not None:
|
381
|
+
try:
|
382
|
+
pattern.format(*shape)
|
383
|
+
except Exception as exc:
|
384
|
+
raise ValueError('pattern cannot be formatted') from exc
|
385
|
+
|
386
|
+
# number of high dimensions not included in chaunk_shape
|
387
|
+
hdim = ndim - len(chunk_shape)
|
388
|
+
if hdim < 0:
|
389
|
+
raise ValueError(f'{len(shape)=} < {len(chunk_shape)=}')
|
390
|
+
if hdim > 0:
|
391
|
+
# prepend length-1 dimensions
|
392
|
+
chunk_shape = ((1,) * hdim) + chunk_shape
|
393
|
+
|
394
|
+
chunked_shape = []
|
395
|
+
pattern_list = []
|
396
|
+
for i, (size, chunk_size, ax) in enumerate(zip(shape, chunk_shape, axes)):
|
397
|
+
if size <= 0:
|
398
|
+
raise ValueError('shape must contain positive sizes')
|
399
|
+
if chunk_size <= 0:
|
400
|
+
raise ValueError('chunk_shape must contain positive sizes')
|
401
|
+
div, mod = divmod(size, chunk_size)
|
402
|
+
chunked_shape.append(div + 1 if mod else div)
|
403
|
+
|
404
|
+
if not squeeze or chunked_shape[-1] > 1:
|
405
|
+
if use_index:
|
406
|
+
digits = int(math.log10(size)) + 1
|
407
|
+
else:
|
408
|
+
digits = int(math.log10(chunked_shape[-1])) + 1
|
409
|
+
pattern_list.append(f'{sep}{ax}{{{i}:0{digits}d}}')
|
410
|
+
|
411
|
+
if pattern is None:
|
412
|
+
pattern = ''.join(pattern_list)
|
413
|
+
|
414
|
+
chunk_index: tuple[int, ...]
|
415
|
+
for chunk_index in numpy.ndindex(tuple(chunked_shape)):
|
416
|
+
index: tuple[int | slice, ...] = tuple(
|
417
|
+
(
|
418
|
+
chunk_index[i]
|
419
|
+
if i < hdim
|
420
|
+
else slice(
|
421
|
+
chunk_index[i] * chunk_shape[i],
|
422
|
+
(chunk_index[i] + 1) * chunk_shape[i],
|
423
|
+
1,
|
424
|
+
)
|
425
|
+
)
|
426
|
+
for i in range(ndim)
|
427
|
+
)
|
428
|
+
if use_index:
|
429
|
+
format_index = tuple(
|
430
|
+
chunk_index[i] * chunk_shape[i] for i in range(ndim)
|
431
|
+
)
|
432
|
+
else:
|
433
|
+
format_index = chunk_index
|
434
|
+
yield (
|
435
|
+
index,
|
436
|
+
pattern.format(*format_index),
|
437
|
+
any(
|
438
|
+
(chunk_index[i] + 1) * chunk_shape[i] > shape[i]
|
439
|
+
for i in range(ndim)
|
440
|
+
),
|
441
|
+
)
|
phasorpy/cli.py
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
"""PhasorPy package command line interface.
|
2
|
+
|
3
|
+
Invoke the command line application with::
|
4
|
+
|
5
|
+
$ python -m phasorpy --help
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from __future__ import annotations
|
10
|
+
|
11
|
+
import os
|
12
|
+
from typing import TYPE_CHECKING
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from ._typing import Iterable
|
16
|
+
|
17
|
+
import click
|
18
|
+
|
19
|
+
from . import version
|
20
|
+
|
21
|
+
|
22
|
+
@click.group(help='PhasorPy package command line interface.')
|
23
|
+
@click.version_option(version=version.__version__)
|
24
|
+
def main() -> int:
|
25
|
+
"""PhasorPy command line interface."""
|
26
|
+
return 0
|
27
|
+
|
28
|
+
|
29
|
+
@main.command(help='Show runtime versions.')
|
30
|
+
@click.option(
|
31
|
+
'--verbose',
|
32
|
+
default=False,
|
33
|
+
is_flag=True,
|
34
|
+
type=click.BOOL,
|
35
|
+
help='Show module paths.',
|
36
|
+
)
|
37
|
+
def versions(verbose: bool) -> None:
|
38
|
+
"""Versions command group."""
|
39
|
+
click.echo(version.versions(verbose=verbose))
|
40
|
+
|
41
|
+
|
42
|
+
@main.command(help='Fetch sample files from remote repositories.')
|
43
|
+
@click.argument('files', nargs=-1)
|
44
|
+
@click.option(
|
45
|
+
'--hideprogress',
|
46
|
+
default=False,
|
47
|
+
is_flag=True,
|
48
|
+
type=click.BOOL,
|
49
|
+
help='Hide progressbar.',
|
50
|
+
)
|
51
|
+
def fetch(files: Iterable[str], hideprogress: bool) -> None:
|
52
|
+
"""Fetch command group."""
|
53
|
+
from . import datasets
|
54
|
+
|
55
|
+
files = datasets.fetch(
|
56
|
+
*files, return_scalar=False, progressbar=not hideprogress
|
57
|
+
)
|
58
|
+
click.echo(f'Cached at {os.path.commonpath(files)}')
|
59
|
+
|
60
|
+
|
61
|
+
@main.command(help='Start interactive FRET phasor plot.')
|
62
|
+
@click.option(
|
63
|
+
'--hide',
|
64
|
+
default=False,
|
65
|
+
is_flag=True,
|
66
|
+
type=click.BOOL,
|
67
|
+
help='Do not show interactive plot.',
|
68
|
+
)
|
69
|
+
def fret(hide: bool) -> None:
|
70
|
+
"""FRET command group."""
|
71
|
+
from .plot import PhasorPlotFret
|
72
|
+
|
73
|
+
plot = PhasorPlotFret(
|
74
|
+
frequency=60.0,
|
75
|
+
donor_lifetime=4.2,
|
76
|
+
acceptor_lifetime=3.0,
|
77
|
+
fret_efficiency=0.5,
|
78
|
+
interactive=True,
|
79
|
+
)
|
80
|
+
if not hide:
|
81
|
+
plot.show()
|
82
|
+
|
83
|
+
|
84
|
+
if __name__ == '__main__':
|
85
|
+
import sys
|
86
|
+
|
87
|
+
sys.exit(main()) # pylint: disable=no-value-for-parameter
|