phasorpy 0.5__cp312-cp312-win_amd64.whl → 0.6__cp312-cp312-win_amd64.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.cp312-win_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +185 -2
- phasorpy/_utils.py +121 -9
- phasorpy/cli.py +56 -3
- phasorpy/cluster.py +42 -6
- phasorpy/components.py +226 -55
- phasorpy/experimental.py +312 -0
- phasorpy/io/__init__.py +137 -0
- phasorpy/io/_flimlabs.py +350 -0
- phasorpy/io/_leica.py +329 -0
- phasorpy/io/_ometiff.py +445 -0
- phasorpy/io/_other.py +782 -0
- phasorpy/io/_simfcs.py +627 -0
- phasorpy/phasor.py +307 -1
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +717 -0
- phasorpy/plot/_lifetime_plots.py +553 -0
- phasorpy/plot/_phasorplot.py +1119 -0
- phasorpy/plot/_phasorplot_fret.py +559 -0
- phasorpy/utils.py +84 -296
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/METADATA +2 -2
- phasorpy-0.6.dist-info/RECORD +34 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/WHEEL +1 -1
- phasorpy/_io.py +0 -2655
- 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.6.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/top_level.txt +0 -0
phasorpy/__init__.py
CHANGED
@@ -2,9 +2,8 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
__all__ = ['__version__'
|
5
|
+
__all__ = ['__version__']
|
6
6
|
|
7
|
-
from .version import __version__, versions
|
8
7
|
|
9
|
-
__version__ =
|
8
|
+
__version__ = '0.6'
|
10
9
|
"""PhasorPy version string."""
|
Binary file
|
phasorpy/_phasorpy.pyx
CHANGED
@@ -50,6 +50,12 @@ ctypedef fused uint_t:
|
|
50
50
|
uint32_t
|
51
51
|
uint64_t
|
52
52
|
|
53
|
+
ctypedef fused int_t:
|
54
|
+
int8_t
|
55
|
+
int16_t
|
56
|
+
int32_t
|
57
|
+
int64_t
|
58
|
+
|
53
59
|
ctypedef fused signal_t:
|
54
60
|
uint8_t
|
55
61
|
uint16_t
|
@@ -767,6 +773,33 @@ cdef (float_t, float_t) _phasor_from_apparent_lifetime(
|
|
767
773
|
return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
|
768
774
|
|
769
775
|
|
776
|
+
@cython.ufunc
|
777
|
+
cdef float_t _phasor_to_normal_lifetime(
|
778
|
+
float_t real,
|
779
|
+
float_t imag,
|
780
|
+
float_t omega,
|
781
|
+
) noexcept nogil:
|
782
|
+
"""Return normal lifetimes from phasor coordinates."""
|
783
|
+
cdef:
|
784
|
+
double taunorm = INFINITY
|
785
|
+
double t
|
786
|
+
|
787
|
+
if isnan(real) or isnan(imag):
|
788
|
+
return <float_t> NAN
|
789
|
+
|
790
|
+
omega *= omega
|
791
|
+
if omega > 0.0:
|
792
|
+
t = 0.5 * (1.0 + cos(atan2(imag, real - 0.5)))
|
793
|
+
if t <= 0.0:
|
794
|
+
taunorm = INFINITY
|
795
|
+
elif t > 1.0:
|
796
|
+
taunorm = NAN
|
797
|
+
else:
|
798
|
+
taunorm = sqrt((1.0 - t) / (omega * t))
|
799
|
+
|
800
|
+
return <float_t> taunorm
|
801
|
+
|
802
|
+
|
770
803
|
@cython.ufunc
|
771
804
|
cdef (float_t, float_t) _phasor_from_single_lifetime(
|
772
805
|
float_t lifetime,
|
@@ -1204,6 +1237,44 @@ cdef unsigned char _is_inside_stadium(
|
|
1204
1237
|
_is_near_segment = _is_inside_stadium
|
1205
1238
|
|
1206
1239
|
|
1240
|
+
@cython.ufunc
|
1241
|
+
cdef unsigned char _is_inside_semicircle(
|
1242
|
+
float_t x, # point
|
1243
|
+
float_t y,
|
1244
|
+
float_t r, # distance
|
1245
|
+
) noexcept nogil:
|
1246
|
+
"""Return whether point is inside universal semicircle."""
|
1247
|
+
if r < 0.0 or isnan(x) or isnan(y):
|
1248
|
+
return False
|
1249
|
+
if y < -r:
|
1250
|
+
return False
|
1251
|
+
if y <= 0.0:
|
1252
|
+
if x >= 0.0 and x <= 1.0:
|
1253
|
+
return True
|
1254
|
+
# near endpoints?
|
1255
|
+
if x > 0.5:
|
1256
|
+
x -= <float_t> 1.0
|
1257
|
+
return x * x + y * y <= r * r
|
1258
|
+
return hypot(x - 0.5, y) <= r + 0.5
|
1259
|
+
|
1260
|
+
|
1261
|
+
@cython.ufunc
|
1262
|
+
cdef unsigned char _is_near_semicircle(
|
1263
|
+
float_t x, # point
|
1264
|
+
float_t y,
|
1265
|
+
float_t r, # distance
|
1266
|
+
) noexcept nogil:
|
1267
|
+
"""Return whether point is near universal semicircle."""
|
1268
|
+
if r < 0.0 or isnan(x) or isnan(y):
|
1269
|
+
return False
|
1270
|
+
if y < 0.0:
|
1271
|
+
# near endpoints?
|
1272
|
+
if x > 0.5:
|
1273
|
+
x -= <float_t> 1.0
|
1274
|
+
return x * x + y * y <= r * r
|
1275
|
+
return fabs(hypot(x - 0.5, y) - 0.5) <= r
|
1276
|
+
|
1277
|
+
|
1207
1278
|
@cython.ufunc
|
1208
1279
|
cdef unsigned char _is_near_line(
|
1209
1280
|
float_t x, # point
|
@@ -1470,6 +1541,22 @@ cdef float_t _distance_from_line(
|
|
1470
1541
|
return <float_t> hypot(x, y)
|
1471
1542
|
|
1472
1543
|
|
1544
|
+
@cython.ufunc
|
1545
|
+
cdef float_t _distance_from_semicircle(
|
1546
|
+
float_t x, # point
|
1547
|
+
float_t y,
|
1548
|
+
) noexcept nogil:
|
1549
|
+
"""Return distance from universal semicircle."""
|
1550
|
+
if isnan(x) or isnan(y):
|
1551
|
+
return NAN
|
1552
|
+
if y < 0.0:
|
1553
|
+
# distance to endpoints
|
1554
|
+
if x > 0.5:
|
1555
|
+
x -= <float_t> 1.0
|
1556
|
+
return <float_t> hypot(x, y)
|
1557
|
+
return <float_t> fabs(hypot(x - 0.5, y) - 0.5)
|
1558
|
+
|
1559
|
+
|
1473
1560
|
@cython.ufunc
|
1474
1561
|
cdef (float_t, float_t, float_t) _segment_direction_and_length(
|
1475
1562
|
float_t x0, # segment start
|
@@ -1495,7 +1582,7 @@ cdef (float_t, float_t, float_t) _segment_direction_and_length(
|
|
1495
1582
|
|
1496
1583
|
|
1497
1584
|
@cython.ufunc
|
1498
|
-
cdef (float_t, float_t, float_t, float_t)
|
1585
|
+
cdef (float_t, float_t, float_t, float_t) _intersect_circle_circle(
|
1499
1586
|
float_t x0, # circle 0
|
1500
1587
|
float_t y0,
|
1501
1588
|
float_t r0,
|
@@ -1541,7 +1628,7 @@ cdef (float_t, float_t, float_t, float_t) _intersection_circle_circle(
|
|
1541
1628
|
|
1542
1629
|
|
1543
1630
|
@cython.ufunc
|
1544
|
-
cdef (float_t, float_t, float_t, float_t)
|
1631
|
+
cdef (float_t, float_t, float_t, float_t) _intersect_circle_line(
|
1545
1632
|
float_t x, # circle
|
1546
1633
|
float_t y,
|
1547
1634
|
float_t r,
|
@@ -1583,10 +1670,106 @@ cdef (float_t, float_t, float_t, float_t) _intersection_circle_line(
|
|
1583
1670
|
)
|
1584
1671
|
|
1585
1672
|
|
1673
|
+
@cython.ufunc
|
1674
|
+
cdef (float_t, float_t, float_t, float_t) _intersect_semicircle_line(
|
1675
|
+
float_t x0, # line start
|
1676
|
+
float_t y0,
|
1677
|
+
float_t x1, # line end
|
1678
|
+
float_t y1,
|
1679
|
+
) noexcept nogil:
|
1680
|
+
"""Return coordinates of intersections of line and universal semicircle."""
|
1681
|
+
cdef:
|
1682
|
+
double dx, dy, dr, dd, rdd
|
1683
|
+
|
1684
|
+
if isnan(x0) or isnan(x1) or isnan(y0) or isnan(y1):
|
1685
|
+
return NAN, NAN, NAN, NAN
|
1686
|
+
|
1687
|
+
dx = x1 - x0
|
1688
|
+
dy = y1 - y0
|
1689
|
+
dr = dx * dx + dy * dy
|
1690
|
+
dd = (x0 - 0.5) * y1 - (x1 - 0.5) * y0
|
1691
|
+
rdd = 0.25 * dr - dd * dd # discriminant
|
1692
|
+
if rdd < 0.0 or dr <= 0.0:
|
1693
|
+
# no intersection
|
1694
|
+
return NAN, NAN, NAN, NAN
|
1695
|
+
rdd = sqrt(rdd)
|
1696
|
+
x0 = <float_t> ((dd * dy - copysign(1.0, dy) * dx * rdd) / dr + 0.5)
|
1697
|
+
y0 = <float_t> ((-dd * dx - fabs(dy) * rdd) / dr)
|
1698
|
+
x1 = <float_t> ((dd * dy + copysign(1.0, dy) * dx * rdd) / dr + 0.5)
|
1699
|
+
y1 = <float_t> ((-dd * dx + fabs(dy) * rdd) / dr)
|
1700
|
+
if y0 < 0.0:
|
1701
|
+
x0 = NAN
|
1702
|
+
y0 = NAN
|
1703
|
+
if y1 < 0.0:
|
1704
|
+
x1 = NAN
|
1705
|
+
y1 = NAN
|
1706
|
+
return x0, y0, x1, y1
|
1707
|
+
|
1708
|
+
|
1709
|
+
def _nearest_neighbor_2d(
|
1710
|
+
int_t[::1] indices,
|
1711
|
+
const float_t[::1] x0,
|
1712
|
+
const float_t[::1] y0,
|
1713
|
+
const float_t[::1] x1,
|
1714
|
+
const float_t[::1] y1,
|
1715
|
+
const float_t distance_max,
|
1716
|
+
const int num_threads
|
1717
|
+
):
|
1718
|
+
"""Find nearest neighbors in 2D.
|
1719
|
+
|
1720
|
+
For each point in the first set of arrays (x0, y0) find the nearest point
|
1721
|
+
in the second set of arrays (x1, y1) and store the index of the nearest
|
1722
|
+
point in the second array in the indices array.
|
1723
|
+
If any coordinates are NaN, or the distance to the nearest point
|
1724
|
+
is larger than distance_max, the index is set to -1.
|
1725
|
+
|
1726
|
+
"""
|
1727
|
+
cdef:
|
1728
|
+
ssize_t i, j, index
|
1729
|
+
float_t x, y, dmin
|
1730
|
+
float_t distance_max_squared = distance_max * distance_max
|
1731
|
+
|
1732
|
+
if (
|
1733
|
+
indices.shape[0] != x0.shape[0]
|
1734
|
+
or x0.shape[0] != y0.shape[0]
|
1735
|
+
or x1.shape[0] != y1.shape[0]
|
1736
|
+
):
|
1737
|
+
raise ValueError('input array size mismatch')
|
1738
|
+
|
1739
|
+
with nogil, parallel(num_threads=num_threads):
|
1740
|
+
for i in prange(x0.shape[0]):
|
1741
|
+
x = x0[i]
|
1742
|
+
y = y0[i]
|
1743
|
+
if isnan(x) or isnan(y):
|
1744
|
+
indices[i] = -1
|
1745
|
+
continue
|
1746
|
+
index = -1
|
1747
|
+
dmin = INFINITY
|
1748
|
+
for j in range(x1.shape[0]):
|
1749
|
+
x = x0[i] - x1[j]
|
1750
|
+
y = y0[i] - y1[j]
|
1751
|
+
x = x * x + y * y
|
1752
|
+
if x < dmin:
|
1753
|
+
dmin = x
|
1754
|
+
index = j
|
1755
|
+
indices[i] = -1 if dmin > distance_max_squared else <int_t> index
|
1756
|
+
|
1757
|
+
|
1586
1758
|
###############################################################################
|
1587
1759
|
# Blend ufuncs
|
1588
1760
|
|
1589
1761
|
|
1762
|
+
@cython.ufunc
|
1763
|
+
cdef float_t _blend_and(
|
1764
|
+
float_t a, # base layer
|
1765
|
+
float_t b, # blend layer
|
1766
|
+
) noexcept nogil:
|
1767
|
+
"""Return blended layers using `and` mode."""
|
1768
|
+
if isnan(a):
|
1769
|
+
return NAN
|
1770
|
+
return b
|
1771
|
+
|
1772
|
+
|
1590
1773
|
@cython.ufunc
|
1591
1774
|
cdef float_t _blend_normal(
|
1592
1775
|
float_t a, # base layer
|
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
|
|
@@ -581,8 +593,8 @@ def chunk_iter(
|
|
581
593
|
)
|
582
594
|
|
583
595
|
|
584
|
-
def
|
585
|
-
"""
|
596
|
+
def init_module(globs: dict[str, Any], /) -> None:
|
597
|
+
"""Add names in module to ``__all__`` and set ``__module__`` attributes.
|
586
598
|
|
587
599
|
Parameters
|
588
600
|
----------
|
@@ -591,11 +603,111 @@ def set_module(globs: dict[str, Any], /) -> None:
|
|
591
603
|
|
592
604
|
Examples
|
593
605
|
--------
|
594
|
-
>>>
|
606
|
+
>>> init_module(globals())
|
595
607
|
|
596
608
|
"""
|
597
|
-
|
598
|
-
|
599
|
-
|
609
|
+
names = globs['__all__']
|
610
|
+
module_name = globs['__name__']
|
611
|
+
module = sys.modules[module_name]
|
612
|
+
for name in dir(module):
|
613
|
+
if name.startswith('_') or name in {
|
614
|
+
'annotations',
|
615
|
+
'init_module',
|
616
|
+
'utils', # TODO: where does this come from?
|
617
|
+
}:
|
618
|
+
continue
|
619
|
+
names.append(name)
|
620
|
+
obj = getattr(module, name)
|
600
621
|
if hasattr(obj, '__module__'):
|
601
|
-
obj.__module__ =
|
622
|
+
obj.__module__ = module_name
|
623
|
+
globs['__all__'] = sorted(set(names))
|
624
|
+
|
625
|
+
|
626
|
+
def xarray_metadata(
|
627
|
+
dims: Sequence[str] | None,
|
628
|
+
shape: tuple[int, ...],
|
629
|
+
/,
|
630
|
+
name: str | PathLike[Any] | None = None,
|
631
|
+
attrs: dict[str, Any] | None = None,
|
632
|
+
**coords: Any,
|
633
|
+
) -> dict[str, Any]:
|
634
|
+
"""Return xarray-style dims, coords, and attrs in a dict.
|
635
|
+
|
636
|
+
>>> xarray_metadata('SYX', (3, 2, 1), S=['0', '1', '2'])
|
637
|
+
{'dims': ('S', 'Y', 'X'), 'coords': {'S': ['0', '1', '2']}, 'attrs': {}}
|
638
|
+
|
639
|
+
"""
|
640
|
+
assert dims is not None
|
641
|
+
dims = tuple(dims)
|
642
|
+
if len(dims) != len(shape):
|
643
|
+
raise ValueError(
|
644
|
+
f'dims do not match shape {len(dims)} != {len(shape)}'
|
645
|
+
)
|
646
|
+
coords = {dim: coords[dim] for dim in dims if dim in coords}
|
647
|
+
if attrs is None:
|
648
|
+
attrs = {}
|
649
|
+
metadata = {'dims': dims, 'coords': coords, 'attrs': attrs}
|
650
|
+
if name:
|
651
|
+
metadata['name'] = os.path.basename(name)
|
652
|
+
return metadata
|
653
|
+
|
654
|
+
|
655
|
+
def squeeze_dims(
|
656
|
+
shape: Sequence[int],
|
657
|
+
dims: Sequence[str],
|
658
|
+
/,
|
659
|
+
skip: Container[str] = 'XY',
|
660
|
+
) -> tuple[tuple[int, ...], tuple[str, ...], tuple[bool, ...]]:
|
661
|
+
"""Return shape and axes with length-1 dimensions removed.
|
662
|
+
|
663
|
+
Remove unused dimensions unless their axes are listed in the `skip`
|
664
|
+
parameter.
|
665
|
+
|
666
|
+
Adapted from the tifffile library.
|
667
|
+
|
668
|
+
Parameters
|
669
|
+
----------
|
670
|
+
shape : tuple of ints
|
671
|
+
Sequence of dimension sizes.
|
672
|
+
dims : sequence of str
|
673
|
+
Character codes for dimensions in `shape`.
|
674
|
+
skip : container of str, optional
|
675
|
+
Character codes for dimensions whose length-1 dimensions are
|
676
|
+
not removed. The default is 'XY'.
|
677
|
+
|
678
|
+
Returns
|
679
|
+
-------
|
680
|
+
shape : tuple of ints
|
681
|
+
Sequence of dimension sizes with length-1 dimensions removed.
|
682
|
+
dims : tuple of str
|
683
|
+
Character codes for dimensions in output `shape`.
|
684
|
+
squeezed : str
|
685
|
+
Dimensions were kept (True) or removed (False).
|
686
|
+
|
687
|
+
Examples
|
688
|
+
--------
|
689
|
+
>>> squeeze_dims((5, 1, 2, 1, 1), 'TZYXC')
|
690
|
+
((5, 2, 1), ('T', 'Y', 'X'), (True, False, True, True, False))
|
691
|
+
>>> squeeze_dims((1,), ('Q',))
|
692
|
+
((1,), ('Q',), (True,))
|
693
|
+
|
694
|
+
"""
|
695
|
+
if len(shape) != len(dims):
|
696
|
+
raise ValueError(f'{len(shape)=} != {len(dims)=}')
|
697
|
+
if not dims:
|
698
|
+
return tuple(shape), tuple(dims), ()
|
699
|
+
squeezed: list[bool] = []
|
700
|
+
shape_squeezed: list[int] = []
|
701
|
+
dims_squeezed: list[str] = []
|
702
|
+
for size, ax in zip(shape, dims):
|
703
|
+
if size > 1 or ax in skip:
|
704
|
+
squeezed.append(True)
|
705
|
+
shape_squeezed.append(size)
|
706
|
+
dims_squeezed.append(ax)
|
707
|
+
else:
|
708
|
+
squeezed.append(False)
|
709
|
+
if len(shape_squeezed) == 0:
|
710
|
+
squeezed[-1] = True
|
711
|
+
shape_squeezed.append(shape[-1])
|
712
|
+
dims_squeezed.append(dims[-1])
|
713
|
+
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,57 @@ def fret(hide: bool) -> None:
|
|
83
85
|
plot.show()
|
84
86
|
|
85
87
|
|
88
|
+
@main.command(help='Start interactive lifetime plots.')
|
89
|
+
@click.option(
|
90
|
+
'-f',
|
91
|
+
'--frequency',
|
92
|
+
type=float,
|
93
|
+
required=False,
|
94
|
+
help='Laser/modulation frequency in MHz.',
|
95
|
+
)
|
96
|
+
@click.option(
|
97
|
+
'-l',
|
98
|
+
'--lifetime',
|
99
|
+
default=(4.0, 1.0),
|
100
|
+
type=float,
|
101
|
+
multiple=True,
|
102
|
+
required=False,
|
103
|
+
help='Lifetime in ns.',
|
104
|
+
)
|
105
|
+
@click.option(
|
106
|
+
'-a',
|
107
|
+
'--fraction',
|
108
|
+
type=float,
|
109
|
+
multiple=True,
|
110
|
+
required=False,
|
111
|
+
help='Fractional intensity of lifetime.',
|
112
|
+
)
|
113
|
+
@click.option(
|
114
|
+
'--hide',
|
115
|
+
default=False,
|
116
|
+
is_flag=True,
|
117
|
+
type=click.BOOL,
|
118
|
+
help='Do not show interactive plot.',
|
119
|
+
)
|
120
|
+
def lifetime(
|
121
|
+
frequency: float | None,
|
122
|
+
lifetime: tuple[float, ...],
|
123
|
+
fraction: tuple[float, ...],
|
124
|
+
hide: bool,
|
125
|
+
) -> None:
|
126
|
+
"""Lifetime command group."""
|
127
|
+
from .plot import LifetimePlots
|
128
|
+
|
129
|
+
plot = LifetimePlots(
|
130
|
+
frequency,
|
131
|
+
lifetime,
|
132
|
+
fraction if len(fraction) > 0 else None,
|
133
|
+
interactive=True,
|
134
|
+
)
|
135
|
+
if not hide:
|
136
|
+
plot.show()
|
137
|
+
|
138
|
+
|
86
139
|
if __name__ == '__main__':
|
87
140
|
import sys
|
88
141
|
|
phasorpy/cluster.py
CHANGED
@@ -16,7 +16,7 @@ __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
|
|
@@ -31,6 +31,7 @@ def phasor_cluster_gmm(
|
|
31
31
|
*,
|
32
32
|
sigma: float = 2.0,
|
33
33
|
clusters: int = 1,
|
34
|
+
sort: Literal['polar', 'phasor', 'area'] | None = None,
|
34
35
|
**kwargs: Any,
|
35
36
|
) -> tuple[
|
36
37
|
tuple[float, ...],
|
@@ -58,6 +59,13 @@ def phasor_cluster_gmm(
|
|
58
59
|
clusters : int, optional
|
59
60
|
Number of Gaussian distributions to fit to phasor coordinates.
|
60
61
|
Defaults to 1.
|
62
|
+
sort: {'polar', 'phasor', 'area'}, optional
|
63
|
+
Sorting method for output clusters. Defaults to 'polar'.
|
64
|
+
|
65
|
+
- 'polar': Sort by polar coordinates (phase, then modulation).
|
66
|
+
- 'phasor': Sort by phasor coordinates (real, then imaginary).
|
67
|
+
- 'area': Sort by inverse area of ellipse (-major * minor).
|
68
|
+
|
61
69
|
**kwargs
|
62
70
|
Additional keyword arguments passed to
|
63
71
|
:py:class:`sklearn.mixture.GaussianMixture`.
|
@@ -161,10 +169,38 @@ def phasor_cluster_gmm(
|
|
161
169
|
radius_major.append(sigma * math.sqrt(2 * eigenvalues[0]))
|
162
170
|
radius_minor.append(sigma * math.sqrt(2 * eigenvalues[1]))
|
163
171
|
|
172
|
+
if clusters == 1:
|
173
|
+
argsort = [0]
|
174
|
+
else:
|
175
|
+
if sort is None or sort == 'polar':
|
176
|
+
|
177
|
+
def sort_key(i: int) -> Any:
|
178
|
+
return (
|
179
|
+
math.atan2(center_imag[i], center_real[i]),
|
180
|
+
math.hypot(center_real[i], center_imag[i]),
|
181
|
+
)
|
182
|
+
|
183
|
+
elif sort == 'phasor':
|
184
|
+
|
185
|
+
def sort_key(i: int) -> Any:
|
186
|
+
return center_imag[i], center_real[i]
|
187
|
+
|
188
|
+
elif sort == 'area':
|
189
|
+
|
190
|
+
def sort_key(i: int) -> Any:
|
191
|
+
return -radius_major[i] * radius_minor[i]
|
192
|
+
|
193
|
+
else:
|
194
|
+
raise ValueError(
|
195
|
+
f"invalid {sort=!r} != 'phasor', 'polar', or 'area'"
|
196
|
+
)
|
197
|
+
|
198
|
+
argsort = sorted(range(len(center_real)), key=sort_key)
|
199
|
+
|
164
200
|
return (
|
165
|
-
tuple(center_real),
|
166
|
-
tuple(center_imag),
|
167
|
-
tuple(radius_major),
|
168
|
-
tuple(radius_minor),
|
169
|
-
tuple(angle),
|
201
|
+
tuple(center_real[i] for i in argsort),
|
202
|
+
tuple(center_imag[i] for i in argsort),
|
203
|
+
tuple(radius_major[i] for i in argsort),
|
204
|
+
tuple(radius_minor[i] for i in argsort),
|
205
|
+
tuple(angle[i] for i in argsort),
|
170
206
|
)
|