phasorpy 0.5__cp311-cp311-win_arm64.whl → 0.7__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/__init__.py +2 -3
- phasorpy/_phasorpy.cp311-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +466 -11
- phasorpy/_utils.py +222 -37
- phasorpy/cli.py +74 -3
- phasorpy/cluster.py +51 -21
- phasorpy/color.py +11 -7
- phasorpy/component.py +707 -0
- phasorpy/{cursors.py → cursor.py} +31 -33
- phasorpy/datasets.py +117 -7
- 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 +184 -1754
- 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/utils.py +89 -290
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/METADATA +3 -3
- phasorpy-0.7.dist-info/RECORD +35 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/WHEEL +1 -1
- phasorpy/_io.py +0 -2655
- phasorpy/components.py +0 -313
- phasorpy/io.py +0 -9
- phasorpy/plot.py +0 -2318
- phasorpy/version.py +0 -80
- phasorpy-0.5.dist-info/RECORD +0 -26
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/top_level.txt +0 -0
phasorpy/_utils.py
CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
5
5
|
__all__ = [
|
6
6
|
'chunk_iter',
|
7
7
|
'dilate_coordinates',
|
8
|
+
'init_module',
|
8
9
|
'kwargs_notnone',
|
9
10
|
'parse_harmonic',
|
10
11
|
'parse_kwargs',
|
@@ -13,18 +14,29 @@ __all__ = [
|
|
13
14
|
'phasor_from_polar_scalar',
|
14
15
|
'phasor_to_polar_scalar',
|
15
16
|
'scale_matrix',
|
16
|
-
'set_module',
|
17
17
|
'sort_coordinates',
|
18
|
+
'squeeze_dims',
|
18
19
|
'update_kwargs',
|
20
|
+
'xarray_metadata',
|
19
21
|
]
|
20
22
|
|
21
23
|
import math
|
22
24
|
import numbers
|
25
|
+
import os
|
26
|
+
import sys
|
23
27
|
from collections.abc import Sequence
|
24
28
|
from typing import TYPE_CHECKING
|
25
29
|
|
26
30
|
if TYPE_CHECKING:
|
27
|
-
from ._typing import
|
31
|
+
from ._typing import (
|
32
|
+
Any,
|
33
|
+
ArrayLike,
|
34
|
+
Literal,
|
35
|
+
NDArray,
|
36
|
+
Iterator,
|
37
|
+
Container,
|
38
|
+
PathLike,
|
39
|
+
)
|
28
40
|
|
29
41
|
import numpy
|
30
42
|
|
@@ -40,6 +52,23 @@ def parse_kwargs(
|
|
40
52
|
|
41
53
|
If `_del` is true (default), existing keys are deleted from `kwargs`.
|
42
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
|
+
|
43
72
|
>>> kwargs = {'one': 1, 'two': 2, 'four': 4}
|
44
73
|
>>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
|
45
74
|
>>> kwargs == {'one': 1}
|
@@ -93,19 +122,19 @@ def scale_matrix(factor: float, origin: Sequence[float]) -> NDArray[Any]:
|
|
93
122
|
|
94
123
|
Parameters
|
95
124
|
----------
|
96
|
-
factor: float
|
125
|
+
factor : float
|
97
126
|
Scale factor.
|
98
|
-
origin: (float, float)
|
127
|
+
origin : (float, float)
|
99
128
|
Coordinates of point around which to scale.
|
100
129
|
|
101
130
|
Returns
|
102
131
|
-------
|
103
|
-
matrix: ndarray
|
132
|
+
matrix : ndarray
|
104
133
|
A 3x3 homogeneous transformation matrix.
|
105
134
|
|
106
135
|
Examples
|
107
136
|
--------
|
108
|
-
>>> scale_matrix(1.1,
|
137
|
+
>>> scale_matrix(1.1, [0.0, 0.5])
|
109
138
|
array([[1.1, 0, -0],
|
110
139
|
[0, 1.1, -0.05],
|
111
140
|
[0, 0, 1]])
|
@@ -121,7 +150,7 @@ def sort_coordinates(
|
|
121
150
|
real: ArrayLike,
|
122
151
|
imag: ArrayLike,
|
123
152
|
/,
|
124
|
-
origin:
|
153
|
+
origin: ArrayLike | None = None,
|
125
154
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
126
155
|
"""Return cartesian coordinates sorted counterclockwise around origin.
|
127
156
|
|
@@ -129,15 +158,19 @@ def sort_coordinates(
|
|
129
158
|
----------
|
130
159
|
real, imag : array_like
|
131
160
|
Coordinates to be sorted.
|
132
|
-
origin :
|
161
|
+
origin : array_like, optional
|
133
162
|
Coordinates around which to sort by angle.
|
163
|
+
By default, sort around the mean of `real` and `imag`.
|
134
164
|
|
135
165
|
Returns
|
136
166
|
-------
|
137
|
-
real
|
138
|
-
|
167
|
+
real : ndarray
|
168
|
+
Sorted real coordinates.
|
169
|
+
imag : ndarray
|
170
|
+
Sorted imaginary coordinates.
|
139
171
|
indices : ndarray
|
140
172
|
Indices used to reorder coordinates.
|
173
|
+
Use ``indices.argsort()`` to get original order.
|
141
174
|
|
142
175
|
Examples
|
143
176
|
--------
|
@@ -148,11 +181,15 @@ def sort_coordinates(
|
|
148
181
|
x, y = numpy.atleast_1d(real, imag)
|
149
182
|
if x.ndim != 1 or x.shape != y.shape:
|
150
183
|
raise ValueError(f'invalid {x.shape=} or {y.shape=}')
|
151
|
-
if x.size <
|
184
|
+
if x.size < 3:
|
152
185
|
return x, y, numpy.arange(x.size)
|
153
186
|
if origin is None:
|
154
|
-
|
155
|
-
|
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))
|
156
193
|
return x[indices], y[indices], indices
|
157
194
|
|
158
195
|
|
@@ -173,8 +210,10 @@ def dilate_coordinates(
|
|
173
210
|
|
174
211
|
Returns
|
175
212
|
-------
|
176
|
-
real
|
177
|
-
|
213
|
+
real : ndarray
|
214
|
+
Dilated real coordinates.
|
215
|
+
imag : ndarray
|
216
|
+
Dilated imaginary coordinates.
|
178
217
|
|
179
218
|
Examples
|
180
219
|
--------
|
@@ -213,8 +252,28 @@ def phasor_to_polar_scalar(
|
|
213
252
|
) -> tuple[float, float]:
|
214
253
|
"""Return polar from scalar phasor coordinates.
|
215
254
|
|
216
|
-
|
217
|
-
|
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)
|
218
277
|
|
219
278
|
"""
|
220
279
|
phi = math.atan2(imag, real)
|
@@ -236,6 +295,26 @@ def phasor_from_polar_scalar(
|
|
236
295
|
) -> tuple[float, float]:
|
237
296
|
"""Return phasor from scalar polar coordinates.
|
238
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
|
+
--------
|
239
318
|
>>> phasor_from_polar_scalar(0.0, 100.0, degree=True, percent=True)
|
240
319
|
(1.0, 0.0)
|
241
320
|
|
@@ -261,23 +340,27 @@ def parse_signal_axis(
|
|
261
340
|
Parameters
|
262
341
|
----------
|
263
342
|
signal : array_like
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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`).
|
269
351
|
|
270
352
|
Returns
|
271
353
|
-------
|
272
354
|
axis : int
|
273
|
-
|
355
|
+
Index of axis over which phasor coordinates are computed.
|
274
356
|
axis_label : str
|
275
|
-
|
357
|
+
Label of axis from `signal.dims` if available, empty string otherwise.
|
276
358
|
|
277
359
|
Raises
|
278
360
|
------
|
279
361
|
ValueError
|
280
|
-
|
362
|
+
If axis string is not found in signal.dims.
|
363
|
+
If axis string is provided but signal has no dims attribute.
|
281
364
|
|
282
365
|
Examples
|
283
366
|
--------
|
@@ -344,15 +427,17 @@ def parse_skip_axis(
|
|
344
427
|
|
345
428
|
Raises
|
346
429
|
------
|
430
|
+
ValueError
|
431
|
+
If ndim is negative.
|
347
432
|
IndexError
|
348
433
|
If any `skip_axis` value is out of bounds of `ndim`.
|
349
434
|
|
350
435
|
Examples
|
351
436
|
--------
|
352
|
-
>>> parse_skip_axis(
|
437
|
+
>>> parse_skip_axis([1, -2], 5)
|
353
438
|
((1, 3), (0, 2, 4))
|
354
439
|
|
355
|
-
>>> parse_skip_axis(
|
440
|
+
>>> parse_skip_axis([1, -2], 5, True)
|
356
441
|
((0, 2, 4), (1, 3, 5))
|
357
442
|
|
358
443
|
"""
|
@@ -389,7 +474,7 @@ def parse_harmonic(
|
|
389
474
|
harmonic : int, sequence of int, 'all', or None
|
390
475
|
Harmonic parameter to parse.
|
391
476
|
harmonic_max : int, optional
|
392
|
-
Maximum value allowed in `
|
477
|
+
Maximum value allowed in `harmonic`. Must be one or greater.
|
393
478
|
To verify against known number of signal samples,
|
394
479
|
pass ``samples // 2``.
|
395
480
|
If `harmonic='all'`, a range of harmonics from one to `harmonic_max`
|
@@ -475,11 +560,11 @@ def chunk_iter(
|
|
475
560
|
pattern : str, optional
|
476
561
|
String to format chunk indices.
|
477
562
|
If None, use ``_[{dims[index]}{chunk_index[index]}]`` for each axis.
|
478
|
-
squeeze : bool
|
563
|
+
squeeze : bool, optional
|
479
564
|
If true, do not include length-1 chunked dimensions in label
|
480
565
|
unless dimensions are part of `chunk_shape`.
|
481
566
|
Applies only if `pattern` is None.
|
482
|
-
use_index : bool
|
567
|
+
use_index : bool, optional
|
483
568
|
If true, use indices of chunks in `shape` instead of chunk indices to
|
484
569
|
format pattern.
|
485
570
|
|
@@ -581,8 +666,8 @@ def chunk_iter(
|
|
581
666
|
)
|
582
667
|
|
583
668
|
|
584
|
-
def
|
585
|
-
"""
|
669
|
+
def init_module(globs: dict[str, Any], /) -> None:
|
670
|
+
"""Add names in module to ``__all__`` and set ``__module__`` attributes.
|
586
671
|
|
587
672
|
Parameters
|
588
673
|
----------
|
@@ -591,11 +676,111 @@ def set_module(globs: dict[str, Any], /) -> None:
|
|
591
676
|
|
592
677
|
Examples
|
593
678
|
--------
|
594
|
-
>>>
|
679
|
+
>>> init_module(globals())
|
595
680
|
|
596
681
|
"""
|
597
|
-
|
598
|
-
|
599
|
-
|
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)
|
600
694
|
if hasattr(obj, '__module__'):
|
601
|
-
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)
|
phasorpy/cli.py
CHANGED
@@ -18,11 +18,11 @@ if TYPE_CHECKING:
|
|
18
18
|
|
19
19
|
import click
|
20
20
|
|
21
|
-
from . import
|
21
|
+
from . import __version__
|
22
22
|
|
23
23
|
|
24
24
|
@click.group(help='PhasorPy package command line interface.')
|
25
|
-
@click.version_option(version=
|
25
|
+
@click.version_option(version=__version__)
|
26
26
|
def main() -> int:
|
27
27
|
"""PhasorPy command line interface."""
|
28
28
|
return 0
|
@@ -38,7 +38,9 @@ def main() -> int:
|
|
38
38
|
)
|
39
39
|
def versions(verbose: bool) -> None:
|
40
40
|
"""Versions command group."""
|
41
|
-
|
41
|
+
from .utils import versions
|
42
|
+
|
43
|
+
click.echo(versions(verbose=verbose))
|
42
44
|
|
43
45
|
|
44
46
|
@main.command(help='Fetch sample files from remote repositories.')
|
@@ -83,6 +85,75 @@ def fret(hide: bool) -> None:
|
|
83
85
|
plot.show()
|
84
86
|
|
85
87
|
|
88
|
+
@main.command(help='Start interactive lifetime plots.')
|
89
|
+
@click.argument(
|
90
|
+
'number_lifetimes',
|
91
|
+
default=2,
|
92
|
+
type=click.IntRange(1, 5),
|
93
|
+
required=False,
|
94
|
+
# help='Number of preconfigured lifetimes.',
|
95
|
+
)
|
96
|
+
@click.option(
|
97
|
+
'-f',
|
98
|
+
'--frequency',
|
99
|
+
type=float,
|
100
|
+
required=False,
|
101
|
+
help='Laser/modulation frequency in MHz.',
|
102
|
+
)
|
103
|
+
@click.option(
|
104
|
+
'-l',
|
105
|
+
'--lifetime',
|
106
|
+
# default=(4.0, 1.0),
|
107
|
+
type=float,
|
108
|
+
multiple=True,
|
109
|
+
required=False,
|
110
|
+
help='Lifetime in ns.',
|
111
|
+
)
|
112
|
+
@click.option(
|
113
|
+
'-a',
|
114
|
+
'--fraction',
|
115
|
+
type=float,
|
116
|
+
multiple=True,
|
117
|
+
required=False,
|
118
|
+
help='Fractional intensity of lifetime.',
|
119
|
+
)
|
120
|
+
@click.option(
|
121
|
+
'--hide',
|
122
|
+
default=False,
|
123
|
+
is_flag=True,
|
124
|
+
type=click.BOOL,
|
125
|
+
help='Do not show interactive plot.',
|
126
|
+
)
|
127
|
+
def lifetime(
|
128
|
+
number_lifetimes: int,
|
129
|
+
frequency: float | None,
|
130
|
+
lifetime: tuple[float, ...],
|
131
|
+
fraction: tuple[float, ...],
|
132
|
+
hide: bool,
|
133
|
+
) -> None:
|
134
|
+
"""Lifetime command group."""
|
135
|
+
from .lifetime import phasor_semicircle, phasor_to_normal_lifetime
|
136
|
+
from .plot import LifetimePlots
|
137
|
+
|
138
|
+
if not lifetime:
|
139
|
+
if number_lifetimes == 2:
|
140
|
+
lifetime = (4.0, 1.0)
|
141
|
+
else:
|
142
|
+
real, imag = phasor_semicircle(number_lifetimes + 2)
|
143
|
+
lifetime = phasor_to_normal_lifetime(
|
144
|
+
real[1:-1], imag[1:-1], frequency if frequency else 80.0
|
145
|
+
) # type: ignore[assignment]
|
146
|
+
|
147
|
+
plot = LifetimePlots(
|
148
|
+
frequency,
|
149
|
+
lifetime,
|
150
|
+
fraction if len(fraction) > 0 else None,
|
151
|
+
interactive=True,
|
152
|
+
)
|
153
|
+
if not hide:
|
154
|
+
plot.show()
|
155
|
+
|
156
|
+
|
86
157
|
if __name__ == '__main__':
|
87
158
|
import sys
|
88
159
|
|
phasorpy/cluster.py
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
The `phasorpy.cluster` module provides functions to:
|
4
4
|
|
5
|
-
- fit elliptic clusters to phasor coordinates using
|
5
|
+
- fit elliptic clusters to phasor coordinates using a
|
6
6
|
Gaussian Mixture Model (GMM):
|
7
7
|
|
8
8
|
- :py:func:`phasor_cluster_gmm`
|
@@ -16,12 +16,11 @@ __all__ = ['phasor_cluster_gmm']
|
|
16
16
|
from typing import TYPE_CHECKING
|
17
17
|
|
18
18
|
if TYPE_CHECKING:
|
19
|
-
from ._typing import Any, ArrayLike
|
19
|
+
from ._typing import Any, ArrayLike, Literal
|
20
20
|
|
21
21
|
import math
|
22
22
|
|
23
23
|
import numpy
|
24
|
-
from sklearn.mixture import GaussianMixture
|
25
24
|
|
26
25
|
|
27
26
|
def phasor_cluster_gmm(
|
@@ -31,6 +30,7 @@ def phasor_cluster_gmm(
|
|
31
30
|
*,
|
32
31
|
sigma: float = 2.0,
|
33
32
|
clusters: int = 1,
|
33
|
+
sort: Literal['polar', 'phasor', 'area'] | None = None,
|
34
34
|
**kwargs: Any,
|
35
35
|
) -> tuple[
|
36
36
|
tuple[float, ...],
|
@@ -51,13 +51,20 @@ def phasor_cluster_gmm(
|
|
51
51
|
Real component of phasor coordinates.
|
52
52
|
imag : array_like
|
53
53
|
Imaginary component of phasor coordinates.
|
54
|
-
sigma: float,
|
54
|
+
sigma : float, optional
|
55
55
|
Scaling factor for radii of major and minor axes.
|
56
|
-
Defaults to 2, which corresponds to the scaling of eigenvalues for
|
57
|
-
95% confidence ellipse.
|
56
|
+
Defaults to 2.0, which corresponds to the scaling of eigenvalues for
|
57
|
+
a 95% confidence ellipse.
|
58
58
|
clusters : int, optional
|
59
59
|
Number of Gaussian distributions to fit to phasor coordinates.
|
60
60
|
Defaults to 1.
|
61
|
+
sort : {'polar', 'phasor', 'area'}, optional
|
62
|
+
Sorting method for output clusters. Defaults to 'polar'.
|
63
|
+
|
64
|
+
- 'polar': Sort by polar coordinates (phase, then modulation).
|
65
|
+
- 'phasor': Sort by phasor coordinates (real, then imaginary).
|
66
|
+
- 'area': Sort by inverse area of ellipse (-major * minor).
|
67
|
+
|
61
68
|
**kwargs
|
62
69
|
Additional keyword arguments passed to
|
63
70
|
:py:class:`sklearn.mixture.GaussianMixture`.
|
@@ -81,19 +88,12 @@ def phasor_cluster_gmm(
|
|
81
88
|
angle : tuple of float
|
82
89
|
Rotation angles of major axes in radians, within range [0, pi].
|
83
90
|
|
84
|
-
Raises
|
85
|
-
------
|
86
|
-
ValueError
|
87
|
-
If the array shapes of `real` and `imag` do not match.
|
88
|
-
If `clusters` is not a positive integer.
|
89
|
-
|
90
|
-
|
91
91
|
References
|
92
92
|
----------
|
93
93
|
.. [1] Vallmitjana A, Torrado B, and Gratton E.
|
94
94
|
`Phasor-based image segmentation: machine learning clustering techniques
|
95
95
|
<https://doi.org/10.1364/BOE.422766>`_.
|
96
|
-
*Biomed Opt Express*, 12(6): 3410-3422 (2021)
|
96
|
+
*Biomed Opt Express*, 12(6): 3410-3422 (2021)
|
97
97
|
|
98
98
|
Examples
|
99
99
|
--------
|
@@ -111,11 +111,13 @@ def phasor_cluster_gmm(
|
|
111
111
|
>>> center_real, center_imag, radius_major, radius_minor, angle = (
|
112
112
|
... phasor_cluster_gmm(real, imag, clusters=2)
|
113
113
|
... )
|
114
|
-
>>>
|
114
|
+
>>> center_real # doctest: +SKIP
|
115
115
|
(0.2, 0.4)
|
116
116
|
|
117
117
|
"""
|
118
|
-
|
118
|
+
from sklearn.mixture import GaussianMixture
|
119
|
+
|
120
|
+
coords = numpy.stack([real, imag], axis=-1).reshape(-1, 2)
|
119
121
|
|
120
122
|
valid_data = ~numpy.isnan(coords).any(axis=1)
|
121
123
|
coords = coords[valid_data]
|
@@ -161,10 +163,38 @@ def phasor_cluster_gmm(
|
|
161
163
|
radius_major.append(sigma * math.sqrt(2 * eigenvalues[0]))
|
162
164
|
radius_minor.append(sigma * math.sqrt(2 * eigenvalues[1]))
|
163
165
|
|
166
|
+
if clusters == 1:
|
167
|
+
argsort = [0]
|
168
|
+
else:
|
169
|
+
if sort is None or sort == 'polar':
|
170
|
+
|
171
|
+
def sort_key(i: int) -> Any:
|
172
|
+
return (
|
173
|
+
math.atan2(center_imag[i], center_real[i]),
|
174
|
+
math.hypot(center_real[i], center_imag[i]),
|
175
|
+
)
|
176
|
+
|
177
|
+
elif sort == 'phasor':
|
178
|
+
|
179
|
+
def sort_key(i: int) -> Any:
|
180
|
+
return center_imag[i], center_real[i]
|
181
|
+
|
182
|
+
elif sort == 'area':
|
183
|
+
|
184
|
+
def sort_key(i: int) -> Any:
|
185
|
+
return -radius_major[i] * radius_minor[i]
|
186
|
+
|
187
|
+
else:
|
188
|
+
raise ValueError(
|
189
|
+
f"invalid {sort=!r} != 'phasor', 'polar', or 'area'"
|
190
|
+
)
|
191
|
+
|
192
|
+
argsort = sorted(range(len(center_real)), key=sort_key)
|
193
|
+
|
164
194
|
return (
|
165
|
-
tuple(center_real),
|
166
|
-
tuple(center_imag),
|
167
|
-
tuple(radius_major),
|
168
|
-
tuple(radius_minor),
|
169
|
-
tuple(angle),
|
195
|
+
tuple(center_real[i] for i in argsort),
|
196
|
+
tuple(center_imag[i] for i in argsort),
|
197
|
+
tuple(radius_major[i] for i in argsort),
|
198
|
+
tuple(radius_minor[i] for i in argsort),
|
199
|
+
tuple(angle[i] for i in argsort),
|
170
200
|
)
|
phasorpy/color.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""Color palettes and manipulation."""
|
1
|
+
"""Color palettes and color manipulation utilities."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
@@ -19,20 +19,22 @@ def wavelength2rgb(
|
|
19
19
|
) -> tuple[float, float, float] | NDArray[Any]:
|
20
20
|
"""Return approximate sRGB color components of visible wavelength(s).
|
21
21
|
|
22
|
-
Wavelengths are clipped to range [360, 750] nm, rounded, and used to
|
22
|
+
Wavelengths are clipped to the range [360, 750] nm, rounded, and used to
|
23
23
|
index the :py:attr:`SRGB_SPECTRUM` palette.
|
24
24
|
|
25
25
|
Parameters
|
26
26
|
----------
|
27
27
|
wavelength : array_like
|
28
28
|
Scalar or array of wavelengths in nm.
|
29
|
-
dtype :
|
29
|
+
dtype : dtype_like, optional
|
30
30
|
Data-type of return value. The default is ``float32``.
|
31
31
|
|
32
32
|
Returns
|
33
33
|
-------
|
34
|
-
ndarray or tuple
|
35
|
-
Approximate sRGB color components of visible wavelength.
|
34
|
+
ndarray or tuple of float
|
35
|
+
Approximate sRGB color components of visible wavelength(s).
|
36
|
+
If input is scalar, return tuple of three floats.
|
37
|
+
If input is array, return ndarray with shape (..., 3).
|
36
38
|
Floating-point values are in range [0.0, 1.0].
|
37
39
|
Integer values are scaled to the dtype's maximum value.
|
38
40
|
|
@@ -74,7 +76,7 @@ def float2int(
|
|
74
76
|
----------
|
75
77
|
rgb : array_like
|
76
78
|
Scalar or array of normalized floating-point color components.
|
77
|
-
dtype :
|
79
|
+
dtype : dtype_like, optional
|
78
80
|
Data type of return value. The default is ``uint8``.
|
79
81
|
|
80
82
|
Returns
|
@@ -169,7 +171,7 @@ CATEGORICAL: NDArray[numpy.float32] = numpy.array([
|
|
169
171
|
], dtype=numpy.float32)
|
170
172
|
"""Categorical sRGB color palette inspired by C. Glasbey.
|
171
173
|
|
172
|
-
Contains 64 maximally distinct
|
174
|
+
Contains 64 maximally distinct colors for visualization.
|
173
175
|
|
174
176
|
Generated using the `glasbey <https://glasbey.readthedocs.io>`_ package::
|
175
177
|
|
@@ -575,6 +577,8 @@ SRGB_SPECTRUM: NDArray[numpy.float32] = numpy.array([
|
|
575
577
|
], dtype=numpy.float32)
|
576
578
|
"""sRGB color components for wavelengths of visible light (360-750 nm).
|
577
579
|
|
580
|
+
Array of shape (391, 3) containing normalized sRGB color components
|
581
|
+
for wavelengths from 360 to 750 nm in 1 nm increments.
|
578
582
|
Based on the CIE 1931 2° Standard Observer.
|
579
583
|
|
580
584
|
Generated using the `colour <https://colour.readthedocs.io>`_ package::
|