phasorpy 0.6__cp312-cp312-macosx_11_0_arm64.whl → 0.7__cp312-cp312-macosx_11_0_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 +1 -1
- phasorpy/_phasorpy.cpython-312-darwin.so +0 -0
- phasorpy/_phasorpy.pyx +281 -9
- phasorpy/_utils.py +101 -28
- phasorpy/cli.py +19 -1
- phasorpy/cluster.py +10 -16
- phasorpy/color.py +11 -7
- phasorpy/{components.py → component.py} +255 -32
- phasorpy/{cursors.py → cursor.py} +31 -33
- phasorpy/datasets.py +117 -7
- phasorpy/experimental.py +8 -10
- phasorpy/io/__init__.py +1 -0
- phasorpy/io/_flimlabs.py +20 -10
- phasorpy/io/_leica.py +3 -1
- phasorpy/io/_ometiff.py +2 -3
- phasorpy/io/_other.py +115 -7
- phasorpy/io/_simfcs.py +41 -16
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +71 -1947
- phasorpy/plot/_functions.py +8 -2
- phasorpy/plot/_lifetime_plots.py +33 -23
- phasorpy/plot/_phasorplot.py +547 -159
- phasorpy/plot/_phasorplot_fret.py +11 -9
- phasorpy/utils.py +21 -10
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/METADATA +2 -2
- phasorpy-0.7.dist-info/RECORD +35 -0
- phasorpy-0.6.dist-info/RECORD +0 -34
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/WHEEL +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.7.dist-info}/top_level.txt +0 -0
phasorpy/__init__.py
CHANGED
Binary file
|
phasorpy/_phasorpy.pyx
CHANGED
@@ -488,10 +488,10 @@ cdef (double, double) _phasor_from_fret_donor(
|
|
488
488
|
return 1.0, 0.0
|
489
489
|
|
490
490
|
# phasor of pure donor at frequency
|
491
|
-
real, imag =
|
491
|
+
real, imag = phasor_from_single_lifetime(donor_lifetime, omega)
|
492
492
|
|
493
493
|
# phasor of quenched donor
|
494
|
-
quenched_real, quenched_imag =
|
494
|
+
quenched_real, quenched_imag = phasor_from_single_lifetime(
|
495
495
|
donor_lifetime * (1.0 - fret_efficiency), omega
|
496
496
|
)
|
497
497
|
|
@@ -555,14 +555,14 @@ cdef (double, double) _phasor_from_fret_acceptor(
|
|
555
555
|
acceptor_background = 0.0
|
556
556
|
|
557
557
|
# phasor of pure donor at frequency
|
558
|
-
donor_real, donor_imag =
|
558
|
+
donor_real, donor_imag = phasor_from_single_lifetime(donor_lifetime, omega)
|
559
559
|
|
560
560
|
if fret_efficiency == 0.0:
|
561
561
|
quenched_real = donor_real
|
562
562
|
quenched_imag = donor_imag
|
563
563
|
else:
|
564
564
|
# phasor of quenched donor
|
565
|
-
quenched_real, quenched_imag =
|
565
|
+
quenched_real, quenched_imag = phasor_from_single_lifetime(
|
566
566
|
donor_lifetime * (1.0 - fret_efficiency), omega
|
567
567
|
)
|
568
568
|
|
@@ -580,7 +580,7 @@ cdef (double, double) _phasor_from_fret_acceptor(
|
|
580
580
|
)
|
581
581
|
|
582
582
|
# phasor of acceptor at frequency
|
583
|
-
acceptor_real, acceptor_imag =
|
583
|
+
acceptor_real, acceptor_imag = phasor_from_single_lifetime(
|
584
584
|
acceptor_lifetime, omega
|
585
585
|
)
|
586
586
|
|
@@ -646,9 +646,9 @@ cdef inline (double, double) linear_combination(
|
|
646
646
|
)
|
647
647
|
|
648
648
|
|
649
|
-
cdef inline (
|
650
|
-
|
651
|
-
|
649
|
+
cdef inline (double, double) phasor_from_single_lifetime(
|
650
|
+
const double lifetime,
|
651
|
+
const double omega,
|
652
652
|
) noexcept nogil:
|
653
653
|
"""Return phasor coordinates from single lifetime component."""
|
654
654
|
cdef:
|
@@ -656,7 +656,7 @@ cdef inline (float_t, float_t) phasor_from_lifetime(
|
|
656
656
|
double mod = 1.0 / sqrt(1.0 + t * t)
|
657
657
|
double phi = atan(t)
|
658
658
|
|
659
|
-
return
|
659
|
+
return mod * cos(phi), mod * sin(phi)
|
660
660
|
|
661
661
|
|
662
662
|
###############################################################################
|
@@ -1706,6 +1706,164 @@ cdef (float_t, float_t, float_t, float_t) _intersect_semicircle_line(
|
|
1706
1706
|
return x0, y0, x1, y1
|
1707
1707
|
|
1708
1708
|
|
1709
|
+
###############################################################################
|
1710
|
+
# Search functions
|
1711
|
+
|
1712
|
+
|
1713
|
+
def _lifetime_search_2(
|
1714
|
+
float_t[:, ::] lifetime, # (num_components, pixels)
|
1715
|
+
float_t[:, ::] fraction, # (num_components, pixels)
|
1716
|
+
const float_t[:, ::] real, # (num_components, pixels)
|
1717
|
+
const float_t[:, ::] imag, # (num_components, pixels)
|
1718
|
+
const double[::] candidate, # real coordinates to scan
|
1719
|
+
const double omega_sqr,
|
1720
|
+
const int num_threads
|
1721
|
+
):
|
1722
|
+
"""Find two lifetime components and fractions in harmonic coordinates.
|
1723
|
+
|
1724
|
+
https://doi.org/10.1021/acs.jpcb.0c06946
|
1725
|
+
|
1726
|
+
"""
|
1727
|
+
cdef:
|
1728
|
+
ssize_t i, u
|
1729
|
+
double re1, im1, re2, im2
|
1730
|
+
double g0, g1, g0h1, s0h1, g1h1, s1h1, g0h2, s0h2, g1h2, s1h2
|
1731
|
+
double x, y, dx, dy, dr, dd, rdd
|
1732
|
+
double dmin, d, f, t
|
1733
|
+
|
1734
|
+
if lifetime.shape[0] != 2 or lifetime.shape[1] != real.shape[1]:
|
1735
|
+
raise ValueError('lifetime shape invalid')
|
1736
|
+
if fraction.shape[0] != 2 or fraction.shape[1] != real.shape[1]:
|
1737
|
+
raise ValueError('fraction shape invalid')
|
1738
|
+
if real.shape[0] != imag.shape[0] != 2:
|
1739
|
+
raise ValueError('phasor harmonics invalid')
|
1740
|
+
if real.shape[1] != imag.shape[1]:
|
1741
|
+
raise ValueError('phasor size invalid')
|
1742
|
+
if candidate.shape[0] < 1:
|
1743
|
+
raise ValueError('candidate size < 1')
|
1744
|
+
|
1745
|
+
with nogil, parallel(num_threads=num_threads):
|
1746
|
+
|
1747
|
+
for u in prange(real.shape[1]):
|
1748
|
+
# loop over phasor coordinates
|
1749
|
+
re1 = real[0, u]
|
1750
|
+
re2 = real[1, u]
|
1751
|
+
im1 = imag[0, u]
|
1752
|
+
im2 = imag[1, u]
|
1753
|
+
|
1754
|
+
if (
|
1755
|
+
isnan(re1)
|
1756
|
+
or isnan(im1)
|
1757
|
+
or isnan(re2)
|
1758
|
+
or isnan(im2)
|
1759
|
+
# outside semicircle?
|
1760
|
+
or re1 < 0.0
|
1761
|
+
or re2 < 0.0
|
1762
|
+
or re1 > 1.0
|
1763
|
+
or re2 > 1.0
|
1764
|
+
or im1 < 0.0
|
1765
|
+
or im2 < 0.0
|
1766
|
+
or im1 * im1 > re1 - re1 * re1 + 1e-9
|
1767
|
+
or im2 * im2 > re2 - re2 * re2 + 1e-9
|
1768
|
+
):
|
1769
|
+
lifetime[0, u] = NAN
|
1770
|
+
lifetime[1, u] = NAN
|
1771
|
+
fraction[0, u] = NAN
|
1772
|
+
fraction[1, u] = NAN
|
1773
|
+
continue
|
1774
|
+
|
1775
|
+
dmin = INFINITY
|
1776
|
+
g0 = NAN
|
1777
|
+
g1 = NAN
|
1778
|
+
f = NAN
|
1779
|
+
|
1780
|
+
for i in range(candidate.shape[0]):
|
1781
|
+
# scan first component
|
1782
|
+
g0h1 = candidate[i]
|
1783
|
+
s0h1 = sqrt(g0h1 - g0h1 * g0h1)
|
1784
|
+
|
1785
|
+
# second component is intersection of semicircle with line
|
1786
|
+
# between first component and phasor coordinate
|
1787
|
+
dx = re1 - g0h1
|
1788
|
+
dy = im1 - s0h1
|
1789
|
+
dr = dx * dx + dy * dy
|
1790
|
+
dd = (g0h1 - 0.5) * im1 - (re1 - 0.5) * s0h1
|
1791
|
+
rdd = 0.25 * dr - dd * dd # discriminant
|
1792
|
+
if rdd < 0.0 or dr <= 0.0:
|
1793
|
+
# no intersection
|
1794
|
+
g0 = g0h1
|
1795
|
+
g1 = g0h1 # NAN?
|
1796
|
+
f = 1.0
|
1797
|
+
break
|
1798
|
+
rdd = sqrt(rdd)
|
1799
|
+
g0h1 = (dd * dy - copysign(1.0, dy) * dx * rdd) / dr + 0.5
|
1800
|
+
s0h1 = (-dd * dx - fabs(dy) * rdd) / dr
|
1801
|
+
g1h1 = (dd * dy + copysign(1.0, dy) * dx * rdd) / dr + 0.5
|
1802
|
+
s1h1 = (-dd * dx + fabs(dy) * rdd) / dr
|
1803
|
+
|
1804
|
+
# this check is numerically unstable if candidate=1.0
|
1805
|
+
if s0h1 < 0.0 or s1h1 < 0.0:
|
1806
|
+
# no other intersection with semicircle
|
1807
|
+
continue
|
1808
|
+
|
1809
|
+
if g0h1 < g1h1:
|
1810
|
+
t = g0h1
|
1811
|
+
g0h1 = g1h1
|
1812
|
+
g1h1 = t
|
1813
|
+
t = s0h1
|
1814
|
+
s0h1 = s1h1
|
1815
|
+
s1h1 = t
|
1816
|
+
|
1817
|
+
# second harmonic component coordinates on semicircle
|
1818
|
+
g0h2 = g0h1 / (4.0 - 3.0 * g0h1)
|
1819
|
+
s0h2 = sqrt(g0h2 - g0h2 * g0h2)
|
1820
|
+
g1h2 = g1h1 / (4.0 - 3.0 * g1h1)
|
1821
|
+
s1h2 = sqrt(g1h2 - g1h2 * g1h2)
|
1822
|
+
|
1823
|
+
# distance of phasor coordinates to line between
|
1824
|
+
# components at second harmonic
|
1825
|
+
# normalize line coordinates
|
1826
|
+
dx = g1h2 - g0h2
|
1827
|
+
dy = s1h2 - s0h2
|
1828
|
+
x = re2 - g0h2
|
1829
|
+
y = im2 - s0h2
|
1830
|
+
# square of line length
|
1831
|
+
t = dx * dx + dy * dy
|
1832
|
+
if t == 0.0:
|
1833
|
+
continue
|
1834
|
+
# projection of point on line using dot product
|
1835
|
+
t = (x * dx + y * dy) / t
|
1836
|
+
# square of distance of point to line
|
1837
|
+
dx = x - t * dx
|
1838
|
+
dy = y - t * dy
|
1839
|
+
d = dx * dx + dy * dy
|
1840
|
+
|
1841
|
+
if d < dmin:
|
1842
|
+
dmin = d
|
1843
|
+
g0 = g0h1
|
1844
|
+
g1 = g1h1
|
1845
|
+
f = t
|
1846
|
+
|
1847
|
+
lifetime[0, u] = <float_t> phasor_to_single_lifetime(g0, omega_sqr)
|
1848
|
+
lifetime[1, u] = <float_t> phasor_to_single_lifetime(g1, omega_sqr)
|
1849
|
+
fraction[0, u] = <float_t> (1.0 - f)
|
1850
|
+
fraction[1, u] = <float_t> f
|
1851
|
+
|
1852
|
+
|
1853
|
+
cdef inline double phasor_to_single_lifetime(
|
1854
|
+
const double real,
|
1855
|
+
const double omega_sqr,
|
1856
|
+
) noexcept nogil:
|
1857
|
+
"""Return single exponential lifetime from real coordinate."""
|
1858
|
+
cdef:
|
1859
|
+
double t
|
1860
|
+
|
1861
|
+
if isnan(real) or real < 0.0 or real > 1.0:
|
1862
|
+
return NAN
|
1863
|
+
t = real * omega_sqr
|
1864
|
+
return sqrt((1.0 - real) / t) if t > 0.0 else INFINITY
|
1865
|
+
|
1866
|
+
|
1709
1867
|
def _nearest_neighbor_2d(
|
1710
1868
|
int_t[::1] indices,
|
1711
1869
|
const float_t[::1] x0,
|
@@ -1755,6 +1913,120 @@ def _nearest_neighbor_2d(
|
|
1755
1913
|
indices[i] = -1 if dmin > distance_max_squared else <int_t> index
|
1756
1914
|
|
1757
1915
|
|
1916
|
+
###############################################################################
|
1917
|
+
# Interpolation functions
|
1918
|
+
|
1919
|
+
|
1920
|
+
def _mean_value_coordinates(
|
1921
|
+
float_t[:, ::1] fraction, # vertices, points
|
1922
|
+
const ssize_t[::1] order,
|
1923
|
+
const float_t[::1] px, # points
|
1924
|
+
const float_t[::1] py,
|
1925
|
+
const float_t[::1] vx, # polygon vertices
|
1926
|
+
const float_t[::1] vy,
|
1927
|
+
const int num_threads
|
1928
|
+
):
|
1929
|
+
"""Calculate mean value coordinates of points in polygon.
|
1930
|
+
|
1931
|
+
https://doi.org/10.1016/j.cagd.2024.102310
|
1932
|
+
|
1933
|
+
"""
|
1934
|
+
cdef:
|
1935
|
+
ssize_t i, j, k, p, nv
|
1936
|
+
double x, y, alpha, weight, weight_sum
|
1937
|
+
double* weights = NULL
|
1938
|
+
double* sigma = NULL
|
1939
|
+
double* length = NULL
|
1940
|
+
|
1941
|
+
if px.shape[0] != py.shape[0]:
|
1942
|
+
raise ValueError('px and py shape mismatch')
|
1943
|
+
if vx.shape[0] != vy.shape[0]:
|
1944
|
+
raise ValueError('vx and vy shape mismatch')
|
1945
|
+
if fraction.shape[0] != vx.shape[0] or fraction.shape[1] != px.shape[0]:
|
1946
|
+
raise ValueError('fraction, vx or px shape mismatch')
|
1947
|
+
if fraction.shape[0] != order.shape[0]:
|
1948
|
+
raise ValueError('fraction and order shape mismatch')
|
1949
|
+
if fraction.shape[0] < 3:
|
1950
|
+
raise ValueError('not a polygon')
|
1951
|
+
|
1952
|
+
nv = fraction.shape[0]
|
1953
|
+
|
1954
|
+
with nogil, parallel(num_threads=num_threads):
|
1955
|
+
weights = <double *> malloc(3 * nv * sizeof(double))
|
1956
|
+
if weights == NULL:
|
1957
|
+
with gil:
|
1958
|
+
raise MemoryError('failed to allocate thread-local buffer')
|
1959
|
+
sigma = &weights[nv]
|
1960
|
+
length = &weights[nv * 2]
|
1961
|
+
|
1962
|
+
for p in prange(px.shape[0]):
|
1963
|
+
x = px[p]
|
1964
|
+
y = py[p]
|
1965
|
+
|
1966
|
+
if isnan(x) or isnan(y):
|
1967
|
+
for i in range(nv):
|
1968
|
+
fraction[i, p] = <float_t> NAN
|
1969
|
+
continue
|
1970
|
+
|
1971
|
+
for i in range(nv):
|
1972
|
+
j = (i + 1) % nv # next vertex, wrapped around
|
1973
|
+
sigma[i] = (
|
1974
|
+
angle(vx[j], vy[j], vx[i], vy[i], x, y) # beta
|
1975
|
+
- angle(vx[i], vy[i], vx[j], vy[j], x, y) # gamma
|
1976
|
+
)
|
1977
|
+
length[i] = hypot(vx[i] - x, vy[i] - y)
|
1978
|
+
|
1979
|
+
weight_sum = 0.0
|
1980
|
+
for i in range(nv):
|
1981
|
+
j = (i + 1) % nv # next vertex, wrapped around
|
1982
|
+
k = (i - 1 + nv) % nv # previous vertex, wrapped around
|
1983
|
+
|
1984
|
+
alpha = angle(vx[k], vy[k], x, y, vx[j], vy[j])
|
1985
|
+
if sign(alpha) != sign(
|
1986
|
+
M_PI * (sign(sigma[k]) + sign(sigma[i]))
|
1987
|
+
- sigma[k] - sigma[i]
|
1988
|
+
):
|
1989
|
+
alpha = -alpha
|
1990
|
+
weight = length[k] * sin(alpha * 0.5)
|
1991
|
+
for j in range(nv):
|
1992
|
+
if j != k and j != i:
|
1993
|
+
weight = weight * length[j] * sin(fabs(sigma[j]) * 0.5)
|
1994
|
+
weight_sum = weight_sum + weight
|
1995
|
+
weights[i] = weight
|
1996
|
+
|
1997
|
+
if fabs(weight_sum) > 1e-12:
|
1998
|
+
for i in range(nv):
|
1999
|
+
fraction[order[i], p] = <float_t> (weights[i] / weight_sum)
|
2000
|
+
else:
|
2001
|
+
for i in range(nv):
|
2002
|
+
fraction[i, p] = <float_t> NAN
|
2003
|
+
|
2004
|
+
free(weights)
|
2005
|
+
|
2006
|
+
|
2007
|
+
cdef inline int sign(const double x) noexcept nogil:
|
2008
|
+
"""Return sign of x."""
|
2009
|
+
return 0 if fabs(x) < 1e-12 else (1 if x > 0.0 else -1)
|
2010
|
+
|
2011
|
+
|
2012
|
+
cdef inline double angle(
|
2013
|
+
const double x0,
|
2014
|
+
const double y0,
|
2015
|
+
const double x1,
|
2016
|
+
const double y1,
|
2017
|
+
const double x2,
|
2018
|
+
const double y2,
|
2019
|
+
) noexcept nogil:
|
2020
|
+
"""Return angle at (x1, y1)."""
|
2021
|
+
cdef:
|
2022
|
+
double ax = x0 - x1
|
2023
|
+
double ay = y0 - y1
|
2024
|
+
double bx = x2 - x1
|
2025
|
+
double by = y2 - y1
|
2026
|
+
|
2027
|
+
return atan2(ax * by - ay * bx, ax * bx + ay * by)
|
2028
|
+
|
2029
|
+
|
1758
2030
|
###############################################################################
|
1759
2031
|
# Blend ufuncs
|
1760
2032
|
|
phasorpy/_utils.py
CHANGED
@@ -52,6 +52,23 @@ def parse_kwargs(
|
|
52
52
|
|
53
53
|
If `_del` is true (default), existing keys are deleted from `kwargs`.
|
54
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
|
+
|
55
72
|
>>> kwargs = {'one': 1, 'two': 2, 'four': 4}
|
56
73
|
>>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
|
57
74
|
>>> kwargs == {'one': 1}
|
@@ -105,19 +122,19 @@ def scale_matrix(factor: float, origin: Sequence[float]) -> NDArray[Any]:
|
|
105
122
|
|
106
123
|
Parameters
|
107
124
|
----------
|
108
|
-
factor: float
|
125
|
+
factor : float
|
109
126
|
Scale factor.
|
110
|
-
origin: (float, float)
|
127
|
+
origin : (float, float)
|
111
128
|
Coordinates of point around which to scale.
|
112
129
|
|
113
130
|
Returns
|
114
131
|
-------
|
115
|
-
matrix: ndarray
|
132
|
+
matrix : ndarray
|
116
133
|
A 3x3 homogeneous transformation matrix.
|
117
134
|
|
118
135
|
Examples
|
119
136
|
--------
|
120
|
-
>>> scale_matrix(1.1,
|
137
|
+
>>> scale_matrix(1.1, [0.0, 0.5])
|
121
138
|
array([[1.1, 0, -0],
|
122
139
|
[0, 1.1, -0.05],
|
123
140
|
[0, 0, 1]])
|
@@ -133,7 +150,7 @@ def sort_coordinates(
|
|
133
150
|
real: ArrayLike,
|
134
151
|
imag: ArrayLike,
|
135
152
|
/,
|
136
|
-
origin:
|
153
|
+
origin: ArrayLike | None = None,
|
137
154
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
138
155
|
"""Return cartesian coordinates sorted counterclockwise around origin.
|
139
156
|
|
@@ -141,15 +158,19 @@ def sort_coordinates(
|
|
141
158
|
----------
|
142
159
|
real, imag : array_like
|
143
160
|
Coordinates to be sorted.
|
144
|
-
origin :
|
161
|
+
origin : array_like, optional
|
145
162
|
Coordinates around which to sort by angle.
|
163
|
+
By default, sort around the mean of `real` and `imag`.
|
146
164
|
|
147
165
|
Returns
|
148
166
|
-------
|
149
|
-
real
|
150
|
-
|
167
|
+
real : ndarray
|
168
|
+
Sorted real coordinates.
|
169
|
+
imag : ndarray
|
170
|
+
Sorted imaginary coordinates.
|
151
171
|
indices : ndarray
|
152
172
|
Indices used to reorder coordinates.
|
173
|
+
Use ``indices.argsort()`` to get original order.
|
153
174
|
|
154
175
|
Examples
|
155
176
|
--------
|
@@ -160,11 +181,15 @@ def sort_coordinates(
|
|
160
181
|
x, y = numpy.atleast_1d(real, imag)
|
161
182
|
if x.ndim != 1 or x.shape != y.shape:
|
162
183
|
raise ValueError(f'invalid {x.shape=} or {y.shape=}')
|
163
|
-
if x.size <
|
184
|
+
if x.size < 3:
|
164
185
|
return x, y, numpy.arange(x.size)
|
165
186
|
if origin is None:
|
166
|
-
|
167
|
-
|
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))
|
168
193
|
return x[indices], y[indices], indices
|
169
194
|
|
170
195
|
|
@@ -185,8 +210,10 @@ def dilate_coordinates(
|
|
185
210
|
|
186
211
|
Returns
|
187
212
|
-------
|
188
|
-
real
|
189
|
-
|
213
|
+
real : ndarray
|
214
|
+
Dilated real coordinates.
|
215
|
+
imag : ndarray
|
216
|
+
Dilated imaginary coordinates.
|
190
217
|
|
191
218
|
Examples
|
192
219
|
--------
|
@@ -225,8 +252,28 @@ def phasor_to_polar_scalar(
|
|
225
252
|
) -> tuple[float, float]:
|
226
253
|
"""Return polar from scalar phasor coordinates.
|
227
254
|
|
228
|
-
|
229
|
-
|
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)
|
230
277
|
|
231
278
|
"""
|
232
279
|
phi = math.atan2(imag, real)
|
@@ -248,6 +295,26 @@ def phasor_from_polar_scalar(
|
|
248
295
|
) -> tuple[float, float]:
|
249
296
|
"""Return phasor from scalar polar coordinates.
|
250
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
|
+
--------
|
251
318
|
>>> phasor_from_polar_scalar(0.0, 100.0, degree=True, percent=True)
|
252
319
|
(1.0, 0.0)
|
253
320
|
|
@@ -273,23 +340,27 @@ def parse_signal_axis(
|
|
273
340
|
Parameters
|
274
341
|
----------
|
275
342
|
signal : array_like
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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`).
|
281
351
|
|
282
352
|
Returns
|
283
353
|
-------
|
284
354
|
axis : int
|
285
|
-
|
355
|
+
Index of axis over which phasor coordinates are computed.
|
286
356
|
axis_label : str
|
287
|
-
|
357
|
+
Label of axis from `signal.dims` if available, empty string otherwise.
|
288
358
|
|
289
359
|
Raises
|
290
360
|
------
|
291
361
|
ValueError
|
292
|
-
|
362
|
+
If axis string is not found in signal.dims.
|
363
|
+
If axis string is provided but signal has no dims attribute.
|
293
364
|
|
294
365
|
Examples
|
295
366
|
--------
|
@@ -356,15 +427,17 @@ def parse_skip_axis(
|
|
356
427
|
|
357
428
|
Raises
|
358
429
|
------
|
430
|
+
ValueError
|
431
|
+
If ndim is negative.
|
359
432
|
IndexError
|
360
433
|
If any `skip_axis` value is out of bounds of `ndim`.
|
361
434
|
|
362
435
|
Examples
|
363
436
|
--------
|
364
|
-
>>> parse_skip_axis(
|
437
|
+
>>> parse_skip_axis([1, -2], 5)
|
365
438
|
((1, 3), (0, 2, 4))
|
366
439
|
|
367
|
-
>>> parse_skip_axis(
|
440
|
+
>>> parse_skip_axis([1, -2], 5, True)
|
368
441
|
((0, 2, 4), (1, 3, 5))
|
369
442
|
|
370
443
|
"""
|
@@ -401,7 +474,7 @@ def parse_harmonic(
|
|
401
474
|
harmonic : int, sequence of int, 'all', or None
|
402
475
|
Harmonic parameter to parse.
|
403
476
|
harmonic_max : int, optional
|
404
|
-
Maximum value allowed in `
|
477
|
+
Maximum value allowed in `harmonic`. Must be one or greater.
|
405
478
|
To verify against known number of signal samples,
|
406
479
|
pass ``samples // 2``.
|
407
480
|
If `harmonic='all'`, a range of harmonics from one to `harmonic_max`
|
@@ -487,11 +560,11 @@ def chunk_iter(
|
|
487
560
|
pattern : str, optional
|
488
561
|
String to format chunk indices.
|
489
562
|
If None, use ``_[{dims[index]}{chunk_index[index]}]`` for each axis.
|
490
|
-
squeeze : bool
|
563
|
+
squeeze : bool, optional
|
491
564
|
If true, do not include length-1 chunked dimensions in label
|
492
565
|
unless dimensions are part of `chunk_shape`.
|
493
566
|
Applies only if `pattern` is None.
|
494
|
-
use_index : bool
|
567
|
+
use_index : bool, optional
|
495
568
|
If true, use indices of chunks in `shape` instead of chunk indices to
|
496
569
|
format pattern.
|
497
570
|
|
phasorpy/cli.py
CHANGED
@@ -86,6 +86,13 @@ def fret(hide: bool) -> None:
|
|
86
86
|
|
87
87
|
|
88
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
|
+
)
|
89
96
|
@click.option(
|
90
97
|
'-f',
|
91
98
|
'--frequency',
|
@@ -96,7 +103,7 @@ def fret(hide: bool) -> None:
|
|
96
103
|
@click.option(
|
97
104
|
'-l',
|
98
105
|
'--lifetime',
|
99
|
-
default=(4.0, 1.0),
|
106
|
+
# default=(4.0, 1.0),
|
100
107
|
type=float,
|
101
108
|
multiple=True,
|
102
109
|
required=False,
|
@@ -118,14 +125,25 @@ def fret(hide: bool) -> None:
|
|
118
125
|
help='Do not show interactive plot.',
|
119
126
|
)
|
120
127
|
def lifetime(
|
128
|
+
number_lifetimes: int,
|
121
129
|
frequency: float | None,
|
122
130
|
lifetime: tuple[float, ...],
|
123
131
|
fraction: tuple[float, ...],
|
124
132
|
hide: bool,
|
125
133
|
) -> None:
|
126
134
|
"""Lifetime command group."""
|
135
|
+
from .lifetime import phasor_semicircle, phasor_to_normal_lifetime
|
127
136
|
from .plot import LifetimePlots
|
128
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
|
+
|
129
147
|
plot = LifetimePlots(
|
130
148
|
frequency,
|
131
149
|
lifetime,
|