phasorpy 0.4__cp313-cp313-win_arm64.whl → 0.5__cp313-cp313-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phasorpy/_io.py +382 -158
- phasorpy/_phasorpy.cp313-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +54 -51
- phasorpy/_utils.py +89 -7
- phasorpy/cli.py +2 -0
- phasorpy/cluster.py +170 -0
- phasorpy/color.py +16 -11
- phasorpy/components.py +18 -18
- phasorpy/conftest.py +2 -0
- phasorpy/cursors.py +9 -9
- phasorpy/datasets.py +129 -51
- phasorpy/io.py +4 -0
- phasorpy/phasor.py +265 -96
- phasorpy/plot.py +251 -27
- phasorpy/utils.py +12 -7
- phasorpy/version.py +13 -5
- {phasorpy-0.4.dist-info → phasorpy-0.5.dist-info}/METADATA +10 -15
- phasorpy-0.5.dist-info/RECORD +26 -0
- {phasorpy-0.4.dist-info → phasorpy-0.5.dist-info}/WHEEL +1 -1
- phasorpy-0.4.dist-info/RECORD +0 -25
- {phasorpy-0.4.dist-info → phasorpy-0.5.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.4.dist-info → phasorpy-0.5.dist-info/licenses}/LICENSE.txt +0 -0
- {phasorpy-0.4.dist-info → phasorpy-0.5.dist-info}/top_level.txt +0 -0
Binary file
|
phasorpy/_phasorpy.pyx
CHANGED
@@ -4,15 +4,10 @@
|
|
4
4
|
# cython: wraparound = False
|
5
5
|
# cython: cdivision = True
|
6
6
|
# cython: nonecheck = False
|
7
|
+
# cython: freethreading_compatible = True
|
7
8
|
|
8
9
|
"""Cython implementation of low-level functions for the PhasorPy library."""
|
9
10
|
|
10
|
-
# TODO: replace short with unsigned char when Cython supports it
|
11
|
-
# https://github.com/cython/cython/pull/6196#issuecomment-2209509572
|
12
|
-
|
13
|
-
# TODO: use fused return types for functions returning more than two items
|
14
|
-
# https://github.com/cython/cython/issues/6328
|
15
|
-
|
16
11
|
cimport cython
|
17
12
|
|
18
13
|
from cython.parallel import parallel, prange
|
@@ -451,7 +446,7 @@ cdef (double, double) _phasor_from_fret_donor(
|
|
451
446
|
double omega,
|
452
447
|
double donor_lifetime,
|
453
448
|
double fret_efficiency,
|
454
|
-
double
|
449
|
+
double donor_fretting,
|
455
450
|
double donor_background,
|
456
451
|
double background_real,
|
457
452
|
double background_imag,
|
@@ -471,16 +466,16 @@ cdef (double, double) _phasor_from_fret_donor(
|
|
471
466
|
elif fret_efficiency > 1.0:
|
472
467
|
fret_efficiency = 1.0
|
473
468
|
|
474
|
-
if
|
475
|
-
|
476
|
-
elif
|
477
|
-
|
469
|
+
if donor_fretting < 0.0:
|
470
|
+
donor_fretting = 0.0
|
471
|
+
elif donor_fretting > 1.0:
|
472
|
+
donor_fretting = 1.0
|
478
473
|
|
479
474
|
if donor_background < 0.0:
|
480
475
|
donor_background = 0.0
|
481
476
|
|
482
|
-
f_pure = 1.0 -
|
483
|
-
f_quenched = (1.0 - fret_efficiency) *
|
477
|
+
f_pure = 1.0 - donor_fretting
|
478
|
+
f_quenched = (1.0 - fret_efficiency) * donor_fretting
|
484
479
|
sum = f_pure + f_quenched + donor_background
|
485
480
|
if sum < 1e-9:
|
486
481
|
# no signal in donor channel
|
@@ -516,7 +511,7 @@ cdef (double, double) _phasor_from_fret_acceptor(
|
|
516
511
|
double donor_lifetime,
|
517
512
|
double acceptor_lifetime,
|
518
513
|
double fret_efficiency,
|
519
|
-
double
|
514
|
+
double donor_fretting,
|
520
515
|
double donor_bleedthrough,
|
521
516
|
double acceptor_bleedthrough,
|
522
517
|
double acceptor_background,
|
@@ -541,10 +536,10 @@ cdef (double, double) _phasor_from_fret_acceptor(
|
|
541
536
|
elif fret_efficiency > 1.0:
|
542
537
|
fret_efficiency = 1.0
|
543
538
|
|
544
|
-
if
|
545
|
-
|
546
|
-
elif
|
547
|
-
|
539
|
+
if donor_fretting < 0.0:
|
540
|
+
donor_fretting = 0.0
|
541
|
+
elif donor_fretting > 1.0:
|
542
|
+
donor_fretting = 1.0
|
548
543
|
|
549
544
|
if donor_bleedthrough < 0.0:
|
550
545
|
donor_bleedthrough = 0.0
|
@@ -575,7 +570,7 @@ cdef (double, double) _phasor_from_fret_acceptor(
|
|
575
570
|
quenched_imag,
|
576
571
|
1.0,
|
577
572
|
1.0 - fret_efficiency,
|
578
|
-
1.0 -
|
573
|
+
1.0 - donor_fretting
|
579
574
|
)
|
580
575
|
|
581
576
|
# phasor of acceptor at frequency
|
@@ -597,8 +592,8 @@ cdef (double, double) _phasor_from_fret_acceptor(
|
|
597
592
|
sensitized_imag = mod * sin(phi)
|
598
593
|
|
599
594
|
# weighted average
|
600
|
-
f_donor = donor_bleedthrough * (1.0 -
|
601
|
-
f_acceptor =
|
595
|
+
f_donor = donor_bleedthrough * (1.0 - donor_fretting * fret_efficiency)
|
596
|
+
f_acceptor = donor_fretting * fret_efficiency
|
602
597
|
sum = f_donor + f_acceptor + acceptor_bleedthrough + acceptor_background
|
603
598
|
if sum < 1e-9:
|
604
599
|
# no signal in acceptor channel
|
@@ -919,7 +914,7 @@ cdef (float_t, float_t) _phasor_at_harmonic(
|
|
919
914
|
int harmonic,
|
920
915
|
int other_harmonic,
|
921
916
|
) noexcept nogil:
|
922
|
-
"""Return phasor coordinates on semicircle at other harmonic."""
|
917
|
+
"""Return phasor coordinates on universal semicircle at other harmonic."""
|
923
918
|
if isnan(real):
|
924
919
|
return <float_t> NAN, <float_t> NAN
|
925
920
|
|
@@ -982,7 +977,7 @@ cdef (float_t, float_t) _phasor_divide(
|
|
982
977
|
|
983
978
|
|
984
979
|
@cython.ufunc
|
985
|
-
cdef
|
980
|
+
cdef unsigned char _is_inside_range(
|
986
981
|
float_t x, # point
|
987
982
|
float_t y,
|
988
983
|
float_t xmin, # x range
|
@@ -1002,7 +997,7 @@ cdef short _is_inside_range(
|
|
1002
997
|
|
1003
998
|
|
1004
999
|
@cython.ufunc
|
1005
|
-
cdef
|
1000
|
+
cdef unsigned char _is_inside_rectangle(
|
1006
1001
|
float_t x, # point
|
1007
1002
|
float_t y,
|
1008
1003
|
float_t x0, # segment start
|
@@ -1044,7 +1039,7 @@ cdef short _is_inside_rectangle(
|
|
1044
1039
|
|
1045
1040
|
|
1046
1041
|
@cython.ufunc
|
1047
|
-
cdef
|
1042
|
+
cdef unsigned char _is_inside_polar_rectangle(
|
1048
1043
|
float_t x, # point
|
1049
1044
|
float_t y,
|
1050
1045
|
float_t angle_min, # phase, -pi to pi
|
@@ -1054,7 +1049,7 @@ cdef short _is_inside_polar_rectangle(
|
|
1054
1049
|
) noexcept nogil:
|
1055
1050
|
"""Return whether point is inside polar rectangle.
|
1056
1051
|
|
1057
|
-
Angles should be in range -pi
|
1052
|
+
Angles should be in range [-pi, pi], else performance is degraded.
|
1058
1053
|
|
1059
1054
|
"""
|
1060
1055
|
cdef:
|
@@ -1083,7 +1078,7 @@ cdef short _is_inside_polar_rectangle(
|
|
1083
1078
|
|
1084
1079
|
|
1085
1080
|
@cython.ufunc
|
1086
|
-
cdef
|
1081
|
+
cdef unsigned char _is_inside_circle(
|
1087
1082
|
float_t x, # point
|
1088
1083
|
float_t y,
|
1089
1084
|
float_t x0, # circle center
|
@@ -1100,7 +1095,7 @@ cdef short _is_inside_circle(
|
|
1100
1095
|
|
1101
1096
|
|
1102
1097
|
@cython.ufunc
|
1103
|
-
cdef
|
1098
|
+
cdef unsigned char _is_inside_ellipse(
|
1104
1099
|
float_t x, # point
|
1105
1100
|
float_t y,
|
1106
1101
|
float_t x0, # ellipse center
|
@@ -1135,7 +1130,7 @@ cdef short _is_inside_ellipse(
|
|
1135
1130
|
|
1136
1131
|
|
1137
1132
|
@cython.ufunc
|
1138
|
-
cdef
|
1133
|
+
cdef unsigned char _is_inside_ellipse_(
|
1139
1134
|
float_t x, # point
|
1140
1135
|
float_t y,
|
1141
1136
|
float_t x0, # ellipse center
|
@@ -1164,7 +1159,7 @@ cdef short _is_inside_ellipse_(
|
|
1164
1159
|
|
1165
1160
|
|
1166
1161
|
@cython.ufunc
|
1167
|
-
cdef
|
1162
|
+
cdef unsigned char _is_inside_stadium(
|
1168
1163
|
float_t x, # point
|
1169
1164
|
float_t y,
|
1170
1165
|
float_t x0, # line start
|
@@ -1210,7 +1205,7 @@ _is_near_segment = _is_inside_stadium
|
|
1210
1205
|
|
1211
1206
|
|
1212
1207
|
@cython.ufunc
|
1213
|
-
cdef
|
1208
|
+
cdef unsigned char _is_near_line(
|
1214
1209
|
float_t x, # point
|
1215
1210
|
float_t y,
|
1216
1211
|
float_t x0, # line start
|
@@ -1476,7 +1471,7 @@ cdef float_t _distance_from_line(
|
|
1476
1471
|
|
1477
1472
|
|
1478
1473
|
@cython.ufunc
|
1479
|
-
cdef (
|
1474
|
+
cdef (float_t, float_t, float_t) _segment_direction_and_length(
|
1480
1475
|
float_t x0, # segment start
|
1481
1476
|
float_t y0,
|
1482
1477
|
float_t x1, # segment end
|
@@ -1500,7 +1495,7 @@ cdef (double, double, double) _segment_direction_and_length(
|
|
1500
1495
|
|
1501
1496
|
|
1502
1497
|
@cython.ufunc
|
1503
|
-
cdef (
|
1498
|
+
cdef (float_t, float_t, float_t, float_t) _intersection_circle_circle(
|
1504
1499
|
float_t x0, # circle 0
|
1505
1500
|
float_t y0,
|
1506
1501
|
float_t r0,
|
@@ -1538,15 +1533,15 @@ cdef (double, double, double, double) _intersection_circle_circle(
|
|
1538
1533
|
hd = sqrt(dd) / dr
|
1539
1534
|
ld = ll / dr
|
1540
1535
|
return (
|
1541
|
-
ld * dx + hd * dy + x0,
|
1542
|
-
ld * dy - hd * dx + y0,
|
1543
|
-
ld * dx - hd * dy + x0,
|
1544
|
-
ld * dy + hd * dx + y0,
|
1536
|
+
<float_t> (ld * dx + hd * dy + x0),
|
1537
|
+
<float_t> (ld * dy - hd * dx + y0),
|
1538
|
+
<float_t> (ld * dx - hd * dy + x0),
|
1539
|
+
<float_t> (ld * dy + hd * dx + y0),
|
1545
1540
|
)
|
1546
1541
|
|
1547
1542
|
|
1548
1543
|
@cython.ufunc
|
1549
|
-
cdef (
|
1544
|
+
cdef (float_t, float_t, float_t, float_t) _intersection_circle_line(
|
1550
1545
|
float_t x, # circle
|
1551
1546
|
float_t y,
|
1552
1547
|
float_t r,
|
@@ -1581,10 +1576,10 @@ cdef (double, double, double, double) _intersection_circle_line(
|
|
1581
1576
|
return NAN, NAN, NAN, NAN
|
1582
1577
|
rdd = sqrt(rdd)
|
1583
1578
|
return (
|
1584
|
-
x + (dd * dy + copysign(1.0, dy) * dx * rdd) / dr,
|
1585
|
-
y + (-dd * dx + fabs(dy) * rdd) / dr,
|
1586
|
-
x + (dd * dy - copysign(1.0, dy) * dx * rdd) / dr,
|
1587
|
-
y + (-dd * dx - fabs(dy) * rdd) / dr,
|
1579
|
+
x + <float_t> ((dd * dy + copysign(1.0, dy) * dx * rdd) / dr),
|
1580
|
+
y + <float_t> ((-dd * dx + fabs(dy) * rdd) / dr),
|
1581
|
+
x + <float_t> ((dd * dy - copysign(1.0, dy) * dx * rdd) / dr),
|
1582
|
+
y + <float_t> ((-dd * dx - fabs(dy) * rdd) / dr),
|
1588
1583
|
)
|
1589
1584
|
|
1590
1585
|
|
@@ -1665,7 +1660,7 @@ cdef float_t _blend_lighten(
|
|
1665
1660
|
|
1666
1661
|
|
1667
1662
|
@cython.ufunc
|
1668
|
-
cdef (
|
1663
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_open(
|
1669
1664
|
float_t mean,
|
1670
1665
|
float_t real,
|
1671
1666
|
float_t imag,
|
@@ -1727,7 +1722,7 @@ cdef (double, double, double) _phasor_threshold_open(
|
|
1727
1722
|
|
1728
1723
|
|
1729
1724
|
@cython.ufunc
|
1730
|
-
cdef (
|
1725
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_closed(
|
1731
1726
|
float_t mean,
|
1732
1727
|
float_t real,
|
1733
1728
|
float_t imag,
|
@@ -1789,7 +1784,7 @@ cdef (double, double, double) _phasor_threshold_closed(
|
|
1789
1784
|
|
1790
1785
|
|
1791
1786
|
@cython.ufunc
|
1792
|
-
cdef (
|
1787
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_mean_open(
|
1793
1788
|
float_t mean,
|
1794
1789
|
float_t real,
|
1795
1790
|
float_t imag,
|
@@ -1809,7 +1804,7 @@ cdef (double, double, double) _phasor_threshold_mean_open(
|
|
1809
1804
|
|
1810
1805
|
|
1811
1806
|
@cython.ufunc
|
1812
|
-
cdef (
|
1807
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_mean_closed(
|
1813
1808
|
float_t mean,
|
1814
1809
|
float_t real,
|
1815
1810
|
float_t imag,
|
@@ -1829,7 +1824,7 @@ cdef (double, double, double) _phasor_threshold_mean_closed(
|
|
1829
1824
|
|
1830
1825
|
|
1831
1826
|
@cython.ufunc
|
1832
|
-
cdef (
|
1827
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_nan(
|
1833
1828
|
float_t mean,
|
1834
1829
|
float_t real,
|
1835
1830
|
float_t imag,
|
@@ -2171,6 +2166,7 @@ def _median_filter_2d(
|
|
2171
2166
|
# Decoder functions
|
2172
2167
|
|
2173
2168
|
|
2169
|
+
@cython.boundscheck(True)
|
2174
2170
|
def _flimlabs_signal(
|
2175
2171
|
uint_t[:, :, ::] signal, # channel, pixel, bin
|
2176
2172
|
list data, # list[list[list[[int, int]]]]
|
@@ -2178,6 +2174,7 @@ def _flimlabs_signal(
|
|
2178
2174
|
):
|
2179
2175
|
"""Return TCSPC histogram image from FLIM LABS JSON intensity data."""
|
2180
2176
|
cdef:
|
2177
|
+
uint_t[::] signal_
|
2181
2178
|
list channels, pixels
|
2182
2179
|
ssize_t c, i, h, count
|
2183
2180
|
|
@@ -2186,18 +2183,21 @@ def _flimlabs_signal(
|
|
2186
2183
|
for channels in data:
|
2187
2184
|
i = 0
|
2188
2185
|
for pixels in channels:
|
2186
|
+
signal_ = signal[c, i]
|
2189
2187
|
for h, count in pixels:
|
2190
|
-
|
2188
|
+
signal_[h] = <uint_t> count
|
2191
2189
|
i += 1
|
2192
2190
|
c += 1
|
2193
2191
|
else:
|
2194
2192
|
i = 0
|
2195
2193
|
for pixels in data[channel]:
|
2194
|
+
signal_ = signal[0, i]
|
2196
2195
|
for h, count in pixels:
|
2197
|
-
|
2196
|
+
signal_[h] = <uint_t> count
|
2198
2197
|
i += 1
|
2199
2198
|
|
2200
2199
|
|
2200
|
+
@cython.boundscheck(True)
|
2201
2201
|
def _flimlabs_mean(
|
2202
2202
|
float_t[:, ::] mean, # channel, pixel
|
2203
2203
|
list data, # list[list[list[[int, int]]]]
|
@@ -2205,6 +2205,7 @@ def _flimlabs_mean(
|
|
2205
2205
|
):
|
2206
2206
|
"""Return mean intensity image from FLIM LABS JSON intensity data."""
|
2207
2207
|
cdef:
|
2208
|
+
float_t[::] mean_
|
2208
2209
|
list channels, pixels
|
2209
2210
|
ssize_t c, i, h, count
|
2210
2211
|
double sum
|
@@ -2212,19 +2213,21 @@ def _flimlabs_mean(
|
|
2212
2213
|
if channel < 0:
|
2213
2214
|
c = 0
|
2214
2215
|
for channels in data:
|
2216
|
+
mean_ = mean[c]
|
2215
2217
|
i = 0
|
2216
2218
|
for pixels in channels:
|
2217
2219
|
sum = 0.0
|
2218
2220
|
for h, count in pixels:
|
2219
2221
|
sum += <double> count
|
2220
|
-
|
2222
|
+
mean_[i] = <float_t> (sum / 256.0)
|
2221
2223
|
i += 1
|
2222
2224
|
c += 1
|
2223
2225
|
else:
|
2224
2226
|
i = 0
|
2227
|
+
mean_ = mean[0]
|
2225
2228
|
for pixels in data[channel]:
|
2226
2229
|
sum = 0.0
|
2227
2230
|
for h, count in pixels:
|
2228
2231
|
sum += <double> count
|
2229
|
-
|
2232
|
+
mean_[i] = <float_t> (sum / 256.0)
|
2230
2233
|
i += 1
|
phasorpy/_utils.py
CHANGED
@@ -2,26 +2,29 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
__all__
|
5
|
+
__all__ = [
|
6
6
|
'chunk_iter',
|
7
7
|
'dilate_coordinates',
|
8
8
|
'kwargs_notnone',
|
9
9
|
'parse_harmonic',
|
10
10
|
'parse_kwargs',
|
11
11
|
'parse_signal_axis',
|
12
|
+
'parse_skip_axis',
|
12
13
|
'phasor_from_polar_scalar',
|
13
14
|
'phasor_to_polar_scalar',
|
14
15
|
'scale_matrix',
|
16
|
+
'set_module',
|
15
17
|
'sort_coordinates',
|
16
18
|
'update_kwargs',
|
17
19
|
]
|
18
20
|
|
19
21
|
import math
|
20
22
|
import numbers
|
23
|
+
from collections.abc import Sequence
|
21
24
|
from typing import TYPE_CHECKING
|
22
25
|
|
23
26
|
if TYPE_CHECKING:
|
24
|
-
from ._typing import Any,
|
27
|
+
from ._typing import Any, ArrayLike, Literal, NDArray, Iterator
|
25
28
|
|
26
29
|
import numpy
|
27
30
|
|
@@ -268,7 +271,7 @@ def parse_signal_axis(
|
|
268
271
|
-------
|
269
272
|
axis : int
|
270
273
|
Axis over which phasor coordinates are computed.
|
271
|
-
axis_label: str
|
274
|
+
axis_label : str
|
272
275
|
Axis label from `signal.dims` if any.
|
273
276
|
|
274
277
|
Raises
|
@@ -312,6 +315,65 @@ def parse_signal_axis(
|
|
312
315
|
raise ValueError(f'{axis=} not valid for {type(signal)=}')
|
313
316
|
|
314
317
|
|
318
|
+
def parse_skip_axis(
|
319
|
+
skip_axis: int | Sequence[int] | None,
|
320
|
+
/,
|
321
|
+
ndim: int,
|
322
|
+
prepend_axis: bool = False,
|
323
|
+
) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
324
|
+
"""Return axes to skip and not to skip.
|
325
|
+
|
326
|
+
This helper function is used to validate and parse `skip_axis`
|
327
|
+
parameters.
|
328
|
+
|
329
|
+
Parameters
|
330
|
+
----------
|
331
|
+
skip_axis : int or sequence of int, optional
|
332
|
+
Axes to skip. If None, no axes are skipped.
|
333
|
+
ndim : int
|
334
|
+
Dimensionality of array in which to skip axes.
|
335
|
+
prepend_axis : bool, optional
|
336
|
+
Prepend one dimension and include in `skip_axis`.
|
337
|
+
|
338
|
+
Returns
|
339
|
+
-------
|
340
|
+
skip_axis : tuple of int
|
341
|
+
Ordered, positive values of `skip_axis`.
|
342
|
+
other_axis : tuple of int
|
343
|
+
Axes indices not included in `skip_axis`.
|
344
|
+
|
345
|
+
Raises
|
346
|
+
------
|
347
|
+
IndexError
|
348
|
+
If any `skip_axis` value is out of bounds of `ndim`.
|
349
|
+
|
350
|
+
Examples
|
351
|
+
--------
|
352
|
+
>>> parse_skip_axis((1, -2), 5)
|
353
|
+
((1, 3), (0, 2, 4))
|
354
|
+
|
355
|
+
>>> parse_skip_axis((1, -2), 5, True)
|
356
|
+
((0, 2, 4), (1, 3, 5))
|
357
|
+
|
358
|
+
"""
|
359
|
+
if ndim < 0:
|
360
|
+
raise ValueError(f'invalid {ndim=}')
|
361
|
+
if skip_axis is None:
|
362
|
+
if prepend_axis:
|
363
|
+
return (0,), tuple(range(1, ndim + 1))
|
364
|
+
return (), tuple(range(ndim))
|
365
|
+
if not isinstance(skip_axis, Sequence):
|
366
|
+
skip_axis = (skip_axis,)
|
367
|
+
if any(i >= ndim or i < -ndim for i in skip_axis):
|
368
|
+
raise IndexError(f'skip_axis={skip_axis} out of range for {ndim=}')
|
369
|
+
skip_axis = sorted(int(i % ndim) for i in skip_axis)
|
370
|
+
if prepend_axis:
|
371
|
+
skip_axis = [0] + [i + 1 for i in skip_axis]
|
372
|
+
ndim += 1
|
373
|
+
other_axis = tuple(i for i in range(ndim) if i not in skip_axis)
|
374
|
+
return tuple(skip_axis), other_axis
|
375
|
+
|
376
|
+
|
315
377
|
def parse_harmonic(
|
316
378
|
harmonic: int | Sequence[int] | Literal['all'] | str | None,
|
317
379
|
harmonic_max: int | None = None,
|
@@ -343,7 +405,7 @@ def parse_harmonic(
|
|
343
405
|
Raises
|
344
406
|
------
|
345
407
|
IndexError
|
346
|
-
Any element is out of range `[1
|
408
|
+
Any element is out of range `[1, harmonic_max]`.
|
347
409
|
ValueError
|
348
410
|
Elements are not unique.
|
349
411
|
Harmonic is empty.
|
@@ -364,7 +426,7 @@ def parse_harmonic(
|
|
364
426
|
if harmonic < 1 or (
|
365
427
|
harmonic_max is not None and harmonic > harmonic_max
|
366
428
|
):
|
367
|
-
raise IndexError(f'{harmonic=} out of range [1
|
429
|
+
raise IndexError(f'{harmonic=} out of range [1, {harmonic_max}]')
|
368
430
|
return [int(harmonic)], False
|
369
431
|
|
370
432
|
if isinstance(harmonic, str):
|
@@ -376,7 +438,7 @@ def parse_harmonic(
|
|
376
438
|
return list(range(1, harmonic_max + 1)), True
|
377
439
|
raise ValueError(f'{harmonic=!r} is not a valid harmonic')
|
378
440
|
|
379
|
-
h = numpy.atleast_1d(
|
441
|
+
h = numpy.atleast_1d(harmonic)
|
380
442
|
if h.size == 0:
|
381
443
|
raise ValueError(f'{harmonic=} is empty')
|
382
444
|
if h.dtype.kind not in 'iu' or h.ndim != 1:
|
@@ -387,7 +449,7 @@ def parse_harmonic(
|
|
387
449
|
raise IndexError(f'{harmonic=} element > {harmonic_max}]')
|
388
450
|
if numpy.unique(h).size != h.size:
|
389
451
|
raise ValueError(f'{harmonic=} elements must be unique')
|
390
|
-
return
|
452
|
+
return [int(i) for i in harmonic], True
|
391
453
|
|
392
454
|
|
393
455
|
def chunk_iter(
|
@@ -517,3 +579,23 @@ def chunk_iter(
|
|
517
579
|
for i in range(ndim)
|
518
580
|
),
|
519
581
|
)
|
582
|
+
|
583
|
+
|
584
|
+
def set_module(globs: dict[str, Any], /) -> None:
|
585
|
+
"""Set ``__module__`` attribute for objects in ``__all__``.
|
586
|
+
|
587
|
+
Parameters
|
588
|
+
----------
|
589
|
+
globs : dict
|
590
|
+
Module namespace to modify.
|
591
|
+
|
592
|
+
Examples
|
593
|
+
--------
|
594
|
+
>>> set_module(globals())
|
595
|
+
|
596
|
+
"""
|
597
|
+
name = globs['__name__']
|
598
|
+
for item in globs['__all__']:
|
599
|
+
obj = globs[item]
|
600
|
+
if hasattr(obj, '__module__'):
|
601
|
+
obj.__module__ = name
|
phasorpy/cli.py
CHANGED
phasorpy/cluster.py
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
"""Cluster phasor coordinates.
|
2
|
+
|
3
|
+
The `phasorpy.cluster` module provides functions to:
|
4
|
+
|
5
|
+
- fit elliptic clusters to phasor coordinates using
|
6
|
+
Gaussian Mixture Model (GMM):
|
7
|
+
|
8
|
+
- :py:func:`phasor_cluster_gmm`
|
9
|
+
|
10
|
+
"""
|
11
|
+
|
12
|
+
from __future__ import annotations
|
13
|
+
|
14
|
+
__all__ = ['phasor_cluster_gmm']
|
15
|
+
|
16
|
+
from typing import TYPE_CHECKING
|
17
|
+
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from ._typing import Any, ArrayLike
|
20
|
+
|
21
|
+
import math
|
22
|
+
|
23
|
+
import numpy
|
24
|
+
from sklearn.mixture import GaussianMixture
|
25
|
+
|
26
|
+
|
27
|
+
def phasor_cluster_gmm(
|
28
|
+
real: ArrayLike,
|
29
|
+
imag: ArrayLike,
|
30
|
+
/,
|
31
|
+
*,
|
32
|
+
sigma: float = 2.0,
|
33
|
+
clusters: int = 1,
|
34
|
+
**kwargs: Any,
|
35
|
+
) -> tuple[
|
36
|
+
tuple[float, ...],
|
37
|
+
tuple[float, ...],
|
38
|
+
tuple[float, ...],
|
39
|
+
tuple[float, ...],
|
40
|
+
tuple[float, ...],
|
41
|
+
]:
|
42
|
+
"""Return elliptic clusters in phasor coordinates using GMM.
|
43
|
+
|
44
|
+
Fit a Gaussian Mixture Model (GMM) to the provided phasor coordinates and
|
45
|
+
extract the parameters of ellipses that represent each cluster according
|
46
|
+
to [1]_.
|
47
|
+
|
48
|
+
Parameters
|
49
|
+
----------
|
50
|
+
real : array_like
|
51
|
+
Real component of phasor coordinates.
|
52
|
+
imag : array_like
|
53
|
+
Imaginary component of phasor coordinates.
|
54
|
+
sigma: float, default = 2.0
|
55
|
+
Scaling factor for radii of major and minor axes.
|
56
|
+
Defaults to 2, which corresponds to the scaling of eigenvalues for a
|
57
|
+
95% confidence ellipse.
|
58
|
+
clusters : int, optional
|
59
|
+
Number of Gaussian distributions to fit to phasor coordinates.
|
60
|
+
Defaults to 1.
|
61
|
+
**kwargs
|
62
|
+
Additional keyword arguments passed to
|
63
|
+
:py:class:`sklearn.mixture.GaussianMixture`.
|
64
|
+
|
65
|
+
Common options include:
|
66
|
+
|
67
|
+
- covariance_type : {'full', 'tied', 'diag', 'spherical'}
|
68
|
+
- max_iter : int, maximum number of EM iterations
|
69
|
+
- random_state : int, for reproducible results
|
70
|
+
|
71
|
+
Returns
|
72
|
+
-------
|
73
|
+
center_real : tuple of float
|
74
|
+
Real component of ellipse centers.
|
75
|
+
center_imag : tuple of float
|
76
|
+
Imaginary component of ellipse centers.
|
77
|
+
radius_major : tuple of float
|
78
|
+
Major radii of ellipses.
|
79
|
+
radius_minor : tuple of float
|
80
|
+
Minor radii of ellipses.
|
81
|
+
angle : tuple of float
|
82
|
+
Rotation angles of major axes in radians, within range [0, pi].
|
83
|
+
|
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
|
+
References
|
92
|
+
----------
|
93
|
+
.. [1] Vallmitjana A, Torrado B, and Gratton E.
|
94
|
+
`Phasor-based image segmentation: machine learning clustering techniques
|
95
|
+
<https://doi.org/10.1364/BOE.422766>`_.
|
96
|
+
*Biomed Opt Express*, 12(6): 3410-3422 (2021).
|
97
|
+
|
98
|
+
Examples
|
99
|
+
--------
|
100
|
+
Recover the clusters from a synthetic distribution of phasor coordinates
|
101
|
+
with two clusters:
|
102
|
+
|
103
|
+
>>> real1, imag1 = numpy.random.multivariate_normal(
|
104
|
+
... [0.2, 0.3], [[3e-3, 1e-3], [1e-3, 2e-3]], 100
|
105
|
+
... ).T
|
106
|
+
>>> real2, imag2 = numpy.random.multivariate_normal(
|
107
|
+
... [0.4, 0.5], [[2e-3, -1e-3], [-1e-3, 3e-3]], 100
|
108
|
+
... ).T
|
109
|
+
>>> real = numpy.concatenate([real1, real2])
|
110
|
+
>>> imag = numpy.concatenate([imag1, imag2])
|
111
|
+
>>> center_real, center_imag, radius_major, radius_minor, angle = (
|
112
|
+
... phasor_cluster_gmm(real, imag, clusters=2)
|
113
|
+
... )
|
114
|
+
>>> centers_real # doctest: +SKIP
|
115
|
+
(0.2, 0.4)
|
116
|
+
|
117
|
+
"""
|
118
|
+
coords = numpy.stack((real, imag), axis=-1).reshape(-1, 2)
|
119
|
+
|
120
|
+
valid_data = ~numpy.isnan(coords).any(axis=1)
|
121
|
+
coords = coords[valid_data]
|
122
|
+
|
123
|
+
kwargs.pop('n_components', None)
|
124
|
+
|
125
|
+
gmm = GaussianMixture(n_components=clusters, **kwargs)
|
126
|
+
gmm.fit(coords)
|
127
|
+
|
128
|
+
center_real = []
|
129
|
+
center_imag = []
|
130
|
+
radius_major = []
|
131
|
+
radius_minor = []
|
132
|
+
angle = []
|
133
|
+
|
134
|
+
for i in range(clusters):
|
135
|
+
center_real.append(float(gmm.means_[i, 0]))
|
136
|
+
center_imag.append(float(gmm.means_[i, 1]))
|
137
|
+
|
138
|
+
if gmm.covariance_type == 'full':
|
139
|
+
cov = gmm.covariances_[i]
|
140
|
+
elif gmm.covariance_type == 'tied':
|
141
|
+
cov = gmm.covariances_
|
142
|
+
elif gmm.covariance_type == 'diag':
|
143
|
+
cov = numpy.diag(gmm.covariances_[i])
|
144
|
+
else: # 'spherical'
|
145
|
+
cov = numpy.eye(2) * gmm.covariances_[i]
|
146
|
+
|
147
|
+
eigenvalues, eigenvectors = numpy.linalg.eigh(cov[:2, :2])
|
148
|
+
|
149
|
+
idx = eigenvalues.argsort()[::-1]
|
150
|
+
eigenvalues = eigenvalues[idx]
|
151
|
+
eigenvectors = eigenvectors[:, idx]
|
152
|
+
|
153
|
+
major_vector = eigenvectors[:, 0]
|
154
|
+
current_angle = math.atan2(major_vector[1], major_vector[0])
|
155
|
+
|
156
|
+
if current_angle < 0:
|
157
|
+
current_angle += math.pi
|
158
|
+
|
159
|
+
angle.append(float(current_angle))
|
160
|
+
|
161
|
+
radius_major.append(sigma * math.sqrt(2 * eigenvalues[0]))
|
162
|
+
radius_minor.append(sigma * math.sqrt(2 * eigenvalues[1]))
|
163
|
+
|
164
|
+
return (
|
165
|
+
tuple(center_real),
|
166
|
+
tuple(center_imag),
|
167
|
+
tuple(radius_major),
|
168
|
+
tuple(radius_minor),
|
169
|
+
tuple(angle),
|
170
|
+
)
|