phasorpy 0.6__cp313-cp313-win_amd64.whl → 0.8__cp313-cp313-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 +1 -1
- phasorpy/_phasorpy.cp313-win_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +320 -10
- phasorpy/_utils.py +114 -33
- phasorpy/cli.py +19 -1
- phasorpy/cluster.py +12 -18
- phasorpy/color.py +11 -7
- phasorpy/{components.py → component.py} +263 -36
- phasorpy/{cursors.py → cursor.py} +31 -33
- phasorpy/datasets.py +118 -8
- phasorpy/experimental.py +4 -168
- phasorpy/filter.py +966 -0
- phasorpy/io/__init__.py +3 -1
- phasorpy/io/_flimlabs.py +26 -16
- phasorpy/io/_leica.py +38 -34
- phasorpy/io/_ometiff.py +10 -9
- phasorpy/io/_other.py +116 -8
- phasorpy/io/_simfcs.py +52 -24
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +106 -2502
- phasorpy/plot/_functions.py +13 -7
- phasorpy/plot/_lifetime_plots.py +34 -24
- phasorpy/plot/_phasorplot.py +561 -176
- phasorpy/plot/_phasorplot_fret.py +12 -10
- phasorpy/utils.py +22 -10
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/METADATA +8 -7
- phasorpy-0.8.dist-info/RECORD +36 -0
- phasorpy-0.6.dist-info/RECORD +0 -34
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/WHEEL +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.6.dist-info → phasorpy-0.8.dist-info}/top_level.txt +0 -0
phasorpy/__init__.py
CHANGED
Binary file
|
phasorpy/_phasorpy.pyx
CHANGED
@@ -6,7 +6,13 @@
|
|
6
6
|
# cython: nonecheck = False
|
7
7
|
# cython: freethreading_compatible = True
|
8
8
|
|
9
|
-
"""
|
9
|
+
"""Private functions implemented in Cython for performance.
|
10
|
+
|
11
|
+
.. note::
|
12
|
+
This module and its functions are not part of the public interface.
|
13
|
+
They are intended to facilitate the development of the PhasorPy library.
|
14
|
+
|
15
|
+
"""
|
10
16
|
|
11
17
|
cimport cython
|
12
18
|
|
@@ -488,10 +494,10 @@ cdef (double, double) _phasor_from_fret_donor(
|
|
488
494
|
return 1.0, 0.0
|
489
495
|
|
490
496
|
# phasor of pure donor at frequency
|
491
|
-
real, imag =
|
497
|
+
real, imag = phasor_from_single_lifetime(donor_lifetime, omega)
|
492
498
|
|
493
499
|
# phasor of quenched donor
|
494
|
-
quenched_real, quenched_imag =
|
500
|
+
quenched_real, quenched_imag = phasor_from_single_lifetime(
|
495
501
|
donor_lifetime * (1.0 - fret_efficiency), omega
|
496
502
|
)
|
497
503
|
|
@@ -555,14 +561,14 @@ cdef (double, double) _phasor_from_fret_acceptor(
|
|
555
561
|
acceptor_background = 0.0
|
556
562
|
|
557
563
|
# phasor of pure donor at frequency
|
558
|
-
donor_real, donor_imag =
|
564
|
+
donor_real, donor_imag = phasor_from_single_lifetime(donor_lifetime, omega)
|
559
565
|
|
560
566
|
if fret_efficiency == 0.0:
|
561
567
|
quenched_real = donor_real
|
562
568
|
quenched_imag = donor_imag
|
563
569
|
else:
|
564
570
|
# phasor of quenched donor
|
565
|
-
quenched_real, quenched_imag =
|
571
|
+
quenched_real, quenched_imag = phasor_from_single_lifetime(
|
566
572
|
donor_lifetime * (1.0 - fret_efficiency), omega
|
567
573
|
)
|
568
574
|
|
@@ -580,7 +586,7 @@ cdef (double, double) _phasor_from_fret_acceptor(
|
|
580
586
|
)
|
581
587
|
|
582
588
|
# phasor of acceptor at frequency
|
583
|
-
acceptor_real, acceptor_imag =
|
589
|
+
acceptor_real, acceptor_imag = phasor_from_single_lifetime(
|
584
590
|
acceptor_lifetime, omega
|
585
591
|
)
|
586
592
|
|
@@ -646,9 +652,9 @@ cdef inline (double, double) linear_combination(
|
|
646
652
|
)
|
647
653
|
|
648
654
|
|
649
|
-
cdef inline (
|
650
|
-
|
651
|
-
|
655
|
+
cdef inline (double, double) phasor_from_single_lifetime(
|
656
|
+
const double lifetime,
|
657
|
+
const double omega,
|
652
658
|
) noexcept nogil:
|
653
659
|
"""Return phasor coordinates from single lifetime component."""
|
654
660
|
cdef:
|
@@ -656,7 +662,7 @@ cdef inline (float_t, float_t) phasor_from_lifetime(
|
|
656
662
|
double mod = 1.0 / sqrt(1.0 + t * t)
|
657
663
|
double phi = atan(t)
|
658
664
|
|
659
|
-
return
|
665
|
+
return mod * cos(phi), mod * sin(phi)
|
660
666
|
|
661
667
|
|
662
668
|
###############################################################################
|
@@ -1005,6 +1011,38 @@ cdef (float_t, float_t) _phasor_divide(
|
|
1005
1011
|
)
|
1006
1012
|
|
1007
1013
|
|
1014
|
+
@cython.ufunc
|
1015
|
+
cdef (float_t, float_t, float_t) _phasor_combine(
|
1016
|
+
float_t int0,
|
1017
|
+
float_t real0,
|
1018
|
+
float_t imag0,
|
1019
|
+
float_t int1,
|
1020
|
+
float_t real1,
|
1021
|
+
float_t imag1,
|
1022
|
+
float_t fraction0,
|
1023
|
+
float_t fraction1,
|
1024
|
+
) noexcept nogil:
|
1025
|
+
"""Return linear combination of two phasor coordinates."""
|
1026
|
+
cdef:
|
1027
|
+
float_t intensity
|
1028
|
+
|
1029
|
+
fraction1 += fraction0
|
1030
|
+
if fraction1 == 0.0:
|
1031
|
+
return <float_t> 0.0, <float_t> NAN, <float_t> NAN
|
1032
|
+
fraction0 /= fraction1
|
1033
|
+
|
1034
|
+
int0 *= fraction0
|
1035
|
+
int1 *= <float_t> 1.0 - fraction0
|
1036
|
+
intensity = int0 + int1
|
1037
|
+
|
1038
|
+
if intensity == 0.0:
|
1039
|
+
return <float_t> 0.0, <float_t> NAN, <float_t> NAN
|
1040
|
+
|
1041
|
+
int0 /= intensity
|
1042
|
+
int1 /= intensity
|
1043
|
+
return intensity, int0 * real0 + int1 * real1, int0 * imag0 + int1 * imag1
|
1044
|
+
|
1045
|
+
|
1008
1046
|
###############################################################################
|
1009
1047
|
# Geometry ufuncs
|
1010
1048
|
|
@@ -1706,6 +1744,164 @@ cdef (float_t, float_t, float_t, float_t) _intersect_semicircle_line(
|
|
1706
1744
|
return x0, y0, x1, y1
|
1707
1745
|
|
1708
1746
|
|
1747
|
+
###############################################################################
|
1748
|
+
# Search functions
|
1749
|
+
|
1750
|
+
|
1751
|
+
def _lifetime_search_2(
|
1752
|
+
float_t[:, ::] lifetime, # (num_components, pixels)
|
1753
|
+
float_t[:, ::] fraction, # (num_components, pixels)
|
1754
|
+
const float_t[:, ::] real, # (num_components, pixels)
|
1755
|
+
const float_t[:, ::] imag, # (num_components, pixels)
|
1756
|
+
const double[::] candidate, # real coordinates to scan
|
1757
|
+
const double omega_sqr,
|
1758
|
+
const int num_threads
|
1759
|
+
):
|
1760
|
+
"""Find two lifetime components and fractions in harmonic coordinates.
|
1761
|
+
|
1762
|
+
https://doi.org/10.1021/acs.jpcb.0c06946
|
1763
|
+
|
1764
|
+
"""
|
1765
|
+
cdef:
|
1766
|
+
ssize_t i, u
|
1767
|
+
double re1, im1, re2, im2
|
1768
|
+
double g0, g1, g0h1, s0h1, g1h1, s1h1, g0h2, s0h2, g1h2, s1h2
|
1769
|
+
double x, y, dx, dy, dr, dd, rdd
|
1770
|
+
double dmin, d, f, t
|
1771
|
+
|
1772
|
+
if lifetime.shape[0] != 2 or lifetime.shape[1] != real.shape[1]:
|
1773
|
+
raise ValueError('lifetime shape invalid')
|
1774
|
+
if fraction.shape[0] != 2 or fraction.shape[1] != real.shape[1]:
|
1775
|
+
raise ValueError('fraction shape invalid')
|
1776
|
+
if real.shape[0] != imag.shape[0] != 2:
|
1777
|
+
raise ValueError('phasor harmonics invalid')
|
1778
|
+
if real.shape[1] != imag.shape[1]:
|
1779
|
+
raise ValueError('phasor size invalid')
|
1780
|
+
if candidate.shape[0] < 1:
|
1781
|
+
raise ValueError('candidate size < 1')
|
1782
|
+
|
1783
|
+
with nogil, parallel(num_threads=num_threads):
|
1784
|
+
|
1785
|
+
for u in prange(real.shape[1]):
|
1786
|
+
# loop over phasor coordinates
|
1787
|
+
re1 = real[0, u]
|
1788
|
+
re2 = real[1, u]
|
1789
|
+
im1 = imag[0, u]
|
1790
|
+
im2 = imag[1, u]
|
1791
|
+
|
1792
|
+
if (
|
1793
|
+
isnan(re1)
|
1794
|
+
or isnan(im1)
|
1795
|
+
or isnan(re2)
|
1796
|
+
or isnan(im2)
|
1797
|
+
# outside semicircle?
|
1798
|
+
or re1 < 0.0
|
1799
|
+
or re2 < 0.0
|
1800
|
+
or re1 > 1.0
|
1801
|
+
or re2 > 1.0
|
1802
|
+
or im1 < 0.0
|
1803
|
+
or im2 < 0.0
|
1804
|
+
or im1 * im1 > re1 - re1 * re1 + 1e-9
|
1805
|
+
or im2 * im2 > re2 - re2 * re2 + 1e-9
|
1806
|
+
):
|
1807
|
+
lifetime[0, u] = NAN
|
1808
|
+
lifetime[1, u] = NAN
|
1809
|
+
fraction[0, u] = NAN
|
1810
|
+
fraction[1, u] = NAN
|
1811
|
+
continue
|
1812
|
+
|
1813
|
+
dmin = INFINITY
|
1814
|
+
g0 = NAN
|
1815
|
+
g1 = NAN
|
1816
|
+
f = NAN
|
1817
|
+
|
1818
|
+
for i in range(candidate.shape[0]):
|
1819
|
+
# scan first component
|
1820
|
+
g0h1 = candidate[i]
|
1821
|
+
s0h1 = sqrt(g0h1 - g0h1 * g0h1)
|
1822
|
+
|
1823
|
+
# second component is intersection of semicircle with line
|
1824
|
+
# between first component and phasor coordinate
|
1825
|
+
dx = re1 - g0h1
|
1826
|
+
dy = im1 - s0h1
|
1827
|
+
dr = dx * dx + dy * dy
|
1828
|
+
dd = (g0h1 - 0.5) * im1 - (re1 - 0.5) * s0h1
|
1829
|
+
rdd = 0.25 * dr - dd * dd # discriminant
|
1830
|
+
if rdd < 0.0 or dr <= 0.0:
|
1831
|
+
# no intersection
|
1832
|
+
g0 = g0h1
|
1833
|
+
g1 = g0h1 # NAN?
|
1834
|
+
f = 1.0
|
1835
|
+
break
|
1836
|
+
rdd = sqrt(rdd)
|
1837
|
+
g0h1 = (dd * dy - copysign(1.0, dy) * dx * rdd) / dr + 0.5
|
1838
|
+
s0h1 = (-dd * dx - fabs(dy) * rdd) / dr
|
1839
|
+
g1h1 = (dd * dy + copysign(1.0, dy) * dx * rdd) / dr + 0.5
|
1840
|
+
s1h1 = (-dd * dx + fabs(dy) * rdd) / dr
|
1841
|
+
|
1842
|
+
# this check is numerically unstable if candidate=1.0
|
1843
|
+
if s0h1 < 0.0 or s1h1 < 0.0:
|
1844
|
+
# no other intersection with semicircle
|
1845
|
+
continue
|
1846
|
+
|
1847
|
+
if g0h1 < g1h1:
|
1848
|
+
t = g0h1
|
1849
|
+
g0h1 = g1h1
|
1850
|
+
g1h1 = t
|
1851
|
+
t = s0h1
|
1852
|
+
s0h1 = s1h1
|
1853
|
+
s1h1 = t
|
1854
|
+
|
1855
|
+
# second harmonic component coordinates on semicircle
|
1856
|
+
g0h2 = g0h1 / (4.0 - 3.0 * g0h1)
|
1857
|
+
s0h2 = sqrt(g0h2 - g0h2 * g0h2)
|
1858
|
+
g1h2 = g1h1 / (4.0 - 3.0 * g1h1)
|
1859
|
+
s1h2 = sqrt(g1h2 - g1h2 * g1h2)
|
1860
|
+
|
1861
|
+
# distance of phasor coordinates to line between
|
1862
|
+
# components at second harmonic
|
1863
|
+
# normalize line coordinates
|
1864
|
+
dx = g1h2 - g0h2
|
1865
|
+
dy = s1h2 - s0h2
|
1866
|
+
x = re2 - g0h2
|
1867
|
+
y = im2 - s0h2
|
1868
|
+
# square of line length
|
1869
|
+
t = dx * dx + dy * dy
|
1870
|
+
if t == 0.0:
|
1871
|
+
continue
|
1872
|
+
# projection of point on line using dot product
|
1873
|
+
t = (x * dx + y * dy) / t
|
1874
|
+
# square of distance of point to line
|
1875
|
+
dx = x - t * dx
|
1876
|
+
dy = y - t * dy
|
1877
|
+
d = dx * dx + dy * dy
|
1878
|
+
|
1879
|
+
if d < dmin:
|
1880
|
+
dmin = d
|
1881
|
+
g0 = g0h1
|
1882
|
+
g1 = g1h1
|
1883
|
+
f = t
|
1884
|
+
|
1885
|
+
lifetime[0, u] = <float_t> phasor_to_single_lifetime(g0, omega_sqr)
|
1886
|
+
lifetime[1, u] = <float_t> phasor_to_single_lifetime(g1, omega_sqr)
|
1887
|
+
fraction[0, u] = <float_t> (1.0 - f)
|
1888
|
+
fraction[1, u] = <float_t> f
|
1889
|
+
|
1890
|
+
|
1891
|
+
cdef inline double phasor_to_single_lifetime(
|
1892
|
+
const double real,
|
1893
|
+
const double omega_sqr,
|
1894
|
+
) noexcept nogil:
|
1895
|
+
"""Return single exponential lifetime from real coordinate."""
|
1896
|
+
cdef:
|
1897
|
+
double t
|
1898
|
+
|
1899
|
+
if isnan(real) or real < 0.0 or real > 1.0:
|
1900
|
+
return NAN
|
1901
|
+
t = real * omega_sqr
|
1902
|
+
return sqrt((1.0 - real) / t) if t > 0.0 else INFINITY
|
1903
|
+
|
1904
|
+
|
1709
1905
|
def _nearest_neighbor_2d(
|
1710
1906
|
int_t[::1] indices,
|
1711
1907
|
const float_t[::1] x0,
|
@@ -1755,6 +1951,120 @@ def _nearest_neighbor_2d(
|
|
1755
1951
|
indices[i] = -1 if dmin > distance_max_squared else <int_t> index
|
1756
1952
|
|
1757
1953
|
|
1954
|
+
###############################################################################
|
1955
|
+
# Interpolation functions
|
1956
|
+
|
1957
|
+
|
1958
|
+
def _mean_value_coordinates(
|
1959
|
+
float_t[:, ::1] fraction, # vertices, points
|
1960
|
+
const ssize_t[::1] order,
|
1961
|
+
const float_t[::1] px, # points
|
1962
|
+
const float_t[::1] py,
|
1963
|
+
const float_t[::1] vx, # polygon vertices
|
1964
|
+
const float_t[::1] vy,
|
1965
|
+
const int num_threads
|
1966
|
+
):
|
1967
|
+
"""Calculate mean value coordinates of points in polygon.
|
1968
|
+
|
1969
|
+
https://doi.org/10.1016/j.cagd.2024.102310
|
1970
|
+
|
1971
|
+
"""
|
1972
|
+
cdef:
|
1973
|
+
ssize_t i, j, k, p, nv
|
1974
|
+
double x, y, alpha, weight, weight_sum
|
1975
|
+
double* weights = NULL
|
1976
|
+
double* sigma = NULL
|
1977
|
+
double* length = NULL
|
1978
|
+
|
1979
|
+
if px.shape[0] != py.shape[0]:
|
1980
|
+
raise ValueError('px and py shape mismatch')
|
1981
|
+
if vx.shape[0] != vy.shape[0]:
|
1982
|
+
raise ValueError('vx and vy shape mismatch')
|
1983
|
+
if fraction.shape[0] != vx.shape[0] or fraction.shape[1] != px.shape[0]:
|
1984
|
+
raise ValueError('fraction, vx or px shape mismatch')
|
1985
|
+
if fraction.shape[0] != order.shape[0]:
|
1986
|
+
raise ValueError('fraction and order shape mismatch')
|
1987
|
+
if fraction.shape[0] < 3:
|
1988
|
+
raise ValueError('not a polygon')
|
1989
|
+
|
1990
|
+
nv = fraction.shape[0]
|
1991
|
+
|
1992
|
+
with nogil, parallel(num_threads=num_threads):
|
1993
|
+
weights = <double *> malloc(3 * nv * sizeof(double))
|
1994
|
+
if weights == NULL:
|
1995
|
+
with gil:
|
1996
|
+
raise MemoryError('failed to allocate thread-local buffer')
|
1997
|
+
sigma = &weights[nv]
|
1998
|
+
length = &weights[nv * 2]
|
1999
|
+
|
2000
|
+
for p in prange(px.shape[0]):
|
2001
|
+
x = px[p]
|
2002
|
+
y = py[p]
|
2003
|
+
|
2004
|
+
if isnan(x) or isnan(y):
|
2005
|
+
for i in range(nv):
|
2006
|
+
fraction[i, p] = <float_t> NAN
|
2007
|
+
continue
|
2008
|
+
|
2009
|
+
for i in range(nv):
|
2010
|
+
j = (i + 1) % nv # next vertex, wrapped around
|
2011
|
+
sigma[i] = (
|
2012
|
+
angle(vx[j], vy[j], vx[i], vy[i], x, y) # beta
|
2013
|
+
- angle(vx[i], vy[i], vx[j], vy[j], x, y) # gamma
|
2014
|
+
)
|
2015
|
+
length[i] = hypot(vx[i] - x, vy[i] - y)
|
2016
|
+
|
2017
|
+
weight_sum = 0.0
|
2018
|
+
for i in range(nv):
|
2019
|
+
j = (i + 1) % nv # next vertex, wrapped around
|
2020
|
+
k = (i - 1 + nv) % nv # previous vertex, wrapped around
|
2021
|
+
|
2022
|
+
alpha = angle(vx[k], vy[k], x, y, vx[j], vy[j])
|
2023
|
+
if sign(alpha) != sign(
|
2024
|
+
M_PI * (sign(sigma[k]) + sign(sigma[i]))
|
2025
|
+
- sigma[k] - sigma[i]
|
2026
|
+
):
|
2027
|
+
alpha = -alpha
|
2028
|
+
weight = length[k] * sin(alpha * 0.5)
|
2029
|
+
for j in range(nv):
|
2030
|
+
if j != k and j != i:
|
2031
|
+
weight = weight * length[j] * sin(fabs(sigma[j]) * 0.5)
|
2032
|
+
weight_sum = weight_sum + weight
|
2033
|
+
weights[i] = weight
|
2034
|
+
|
2035
|
+
if fabs(weight_sum) > 1e-12:
|
2036
|
+
for i in range(nv):
|
2037
|
+
fraction[order[i], p] = <float_t> (weights[i] / weight_sum)
|
2038
|
+
else:
|
2039
|
+
for i in range(nv):
|
2040
|
+
fraction[i, p] = <float_t> NAN
|
2041
|
+
|
2042
|
+
free(weights)
|
2043
|
+
|
2044
|
+
|
2045
|
+
cdef inline int sign(const double x) noexcept nogil:
|
2046
|
+
"""Return sign of x."""
|
2047
|
+
return 0 if fabs(x) < 1e-12 else (1 if x > 0.0 else -1)
|
2048
|
+
|
2049
|
+
|
2050
|
+
cdef inline double angle(
|
2051
|
+
const double x0,
|
2052
|
+
const double y0,
|
2053
|
+
const double x1,
|
2054
|
+
const double y1,
|
2055
|
+
const double x2,
|
2056
|
+
const double y2,
|
2057
|
+
) noexcept nogil:
|
2058
|
+
"""Return angle at (x1, y1)."""
|
2059
|
+
cdef:
|
2060
|
+
double ax = x0 - x1
|
2061
|
+
double ay = y0 - y1
|
2062
|
+
double bx = x2 - x1
|
2063
|
+
double by = y2 - y1
|
2064
|
+
|
2065
|
+
return atan2(ax * by - ay * bx, ax * bx + ay * by)
|
2066
|
+
|
2067
|
+
|
1758
2068
|
###############################################################################
|
1759
2069
|
# Blend ufuncs
|
1760
2070
|
|
phasorpy/_utils.py
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
"""Private auxiliary and convenience functions.
|
1
|
+
"""Private auxiliary and convenience functions.
|
2
|
+
|
3
|
+
.. note::
|
4
|
+
This module and its functions are not part of the public interface.
|
5
|
+
They are intended to facilitate the development of the PhasorPy library.
|
6
|
+
|
7
|
+
"""
|
2
8
|
|
3
9
|
from __future__ import annotations
|
4
10
|
|
@@ -52,6 +58,23 @@ def parse_kwargs(
|
|
52
58
|
|
53
59
|
If `_del` is true (default), existing keys are deleted from `kwargs`.
|
54
60
|
|
61
|
+
Parameters
|
62
|
+
----------
|
63
|
+
kwargs : dict
|
64
|
+
Source dictionary to extract keys from.
|
65
|
+
*keys : str
|
66
|
+
Keys to extract from kwargs if present.
|
67
|
+
_del : bool, default: True
|
68
|
+
If True, remove extracted keys from kwargs.
|
69
|
+
**keyvalues : Any
|
70
|
+
Key-value pairs. If key exists in kwargs, use kwargs value,
|
71
|
+
otherwise use provided default value.
|
72
|
+
|
73
|
+
Returns
|
74
|
+
-------
|
75
|
+
dict
|
76
|
+
Dictionary containing extracted keys and values.
|
77
|
+
|
55
78
|
>>> kwargs = {'one': 1, 'two': 2, 'four': 4}
|
56
79
|
>>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
|
57
80
|
>>> kwargs == {'one': 1}
|
@@ -105,19 +128,19 @@ def scale_matrix(factor: float, origin: Sequence[float]) -> NDArray[Any]:
|
|
105
128
|
|
106
129
|
Parameters
|
107
130
|
----------
|
108
|
-
factor: float
|
131
|
+
factor : float
|
109
132
|
Scale factor.
|
110
|
-
origin: (float, float)
|
133
|
+
origin : (float, float)
|
111
134
|
Coordinates of point around which to scale.
|
112
135
|
|
113
136
|
Returns
|
114
137
|
-------
|
115
|
-
matrix: ndarray
|
138
|
+
matrix : ndarray
|
116
139
|
A 3x3 homogeneous transformation matrix.
|
117
140
|
|
118
141
|
Examples
|
119
142
|
--------
|
120
|
-
>>> scale_matrix(1.1,
|
143
|
+
>>> scale_matrix(1.1, [0.0, 0.5])
|
121
144
|
array([[1.1, 0, -0],
|
122
145
|
[0, 1.1, -0.05],
|
123
146
|
[0, 0, 1]])
|
@@ -133,7 +156,7 @@ def sort_coordinates(
|
|
133
156
|
real: ArrayLike,
|
134
157
|
imag: ArrayLike,
|
135
158
|
/,
|
136
|
-
origin:
|
159
|
+
origin: ArrayLike | None = None,
|
137
160
|
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
138
161
|
"""Return cartesian coordinates sorted counterclockwise around origin.
|
139
162
|
|
@@ -141,15 +164,19 @@ def sort_coordinates(
|
|
141
164
|
----------
|
142
165
|
real, imag : array_like
|
143
166
|
Coordinates to be sorted.
|
144
|
-
origin :
|
167
|
+
origin : array_like, optional
|
145
168
|
Coordinates around which to sort by angle.
|
169
|
+
By default, sort around the mean of `real` and `imag`.
|
146
170
|
|
147
171
|
Returns
|
148
172
|
-------
|
149
|
-
real
|
150
|
-
|
173
|
+
real : ndarray
|
174
|
+
Sorted real coordinates.
|
175
|
+
imag : ndarray
|
176
|
+
Sorted imaginary coordinates.
|
151
177
|
indices : ndarray
|
152
178
|
Indices used to reorder coordinates.
|
179
|
+
Use ``indices.argsort()`` to get original order.
|
153
180
|
|
154
181
|
Examples
|
155
182
|
--------
|
@@ -160,11 +187,15 @@ def sort_coordinates(
|
|
160
187
|
x, y = numpy.atleast_1d(real, imag)
|
161
188
|
if x.ndim != 1 or x.shape != y.shape:
|
162
189
|
raise ValueError(f'invalid {x.shape=} or {y.shape=}')
|
163
|
-
if x.size <
|
190
|
+
if x.size < 3:
|
164
191
|
return x, y, numpy.arange(x.size)
|
165
192
|
if origin is None:
|
166
|
-
|
167
|
-
|
193
|
+
ox, oy = x.mean(), y.mean()
|
194
|
+
else:
|
195
|
+
origin = numpy.asarray(origin, dtype=numpy.float64)
|
196
|
+
ox = origin[0]
|
197
|
+
oy = origin[1]
|
198
|
+
indices = numpy.argsort(numpy.arctan2(y - oy, x - ox))
|
168
199
|
return x[indices], y[indices], indices
|
169
200
|
|
170
201
|
|
@@ -185,8 +216,10 @@ def dilate_coordinates(
|
|
185
216
|
|
186
217
|
Returns
|
187
218
|
-------
|
188
|
-
real
|
189
|
-
|
219
|
+
real : ndarray
|
220
|
+
Dilated real coordinates.
|
221
|
+
imag : ndarray
|
222
|
+
Dilated imaginary coordinates.
|
190
223
|
|
191
224
|
Examples
|
192
225
|
--------
|
@@ -225,8 +258,28 @@ def phasor_to_polar_scalar(
|
|
225
258
|
) -> tuple[float, float]:
|
226
259
|
"""Return polar from scalar phasor coordinates.
|
227
260
|
|
228
|
-
|
229
|
-
|
261
|
+
Parameters
|
262
|
+
----------
|
263
|
+
real : float
|
264
|
+
Real component of phasor coordinate.
|
265
|
+
imag : float
|
266
|
+
Imaginary component of phasor coordinate.
|
267
|
+
degree : bool, optional
|
268
|
+
If true, return phase in degrees instead of radians.
|
269
|
+
percent : bool, optional
|
270
|
+
If true, return modulation as percentage instead of fraction.
|
271
|
+
|
272
|
+
Returns
|
273
|
+
-------
|
274
|
+
phase : float
|
275
|
+
Phase angle in radians (or degrees if degree=True).
|
276
|
+
modulation : float
|
277
|
+
Modulation depth as fraction (or percentage if percent=True).
|
278
|
+
|
279
|
+
Examples
|
280
|
+
--------
|
281
|
+
>>> phasor_to_polar_scalar(0.0, 1.0, degree=True, percent=True)
|
282
|
+
(90.0, 100.0)
|
230
283
|
|
231
284
|
"""
|
232
285
|
phi = math.atan2(imag, real)
|
@@ -248,6 +301,26 @@ def phasor_from_polar_scalar(
|
|
248
301
|
) -> tuple[float, float]:
|
249
302
|
"""Return phasor from scalar polar coordinates.
|
250
303
|
|
304
|
+
Parameters
|
305
|
+
----------
|
306
|
+
phase : float
|
307
|
+
Phase angle in radians (or degrees if degree=True).
|
308
|
+
modulation : float
|
309
|
+
Modulation depth as fraction (or percentage if percent=True).
|
310
|
+
degree : bool, optional
|
311
|
+
If true, phase is in degrees instead of radians.
|
312
|
+
percent : bool, optional
|
313
|
+
If true, modulation is as percentage instead of fraction.
|
314
|
+
|
315
|
+
Returns
|
316
|
+
-------
|
317
|
+
real : float
|
318
|
+
Real component of phasor coordinate.
|
319
|
+
imag : float
|
320
|
+
Imaginary component of phasor coordinate.
|
321
|
+
|
322
|
+
Examples
|
323
|
+
--------
|
251
324
|
>>> phasor_from_polar_scalar(0.0, 100.0, degree=True, percent=True)
|
252
325
|
(1.0, 0.0)
|
253
326
|
|
@@ -273,23 +346,27 @@ def parse_signal_axis(
|
|
273
346
|
Parameters
|
274
347
|
----------
|
275
348
|
signal : array_like
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
349
|
+
Signal array.
|
350
|
+
Axis names are used if it has a `dims` attribute.
|
351
|
+
axis : int, str, or None, default: None
|
352
|
+
Axis over which to compute phasor coordinates.
|
353
|
+
If None, automatically selects 'H' or 'C' axis if available,
|
354
|
+
otherwise uses the last axis (-1).
|
355
|
+
If int, specifies axis index.
|
356
|
+
If str, specifies axis name (requires `signal.dims`).
|
281
357
|
|
282
358
|
Returns
|
283
359
|
-------
|
284
360
|
axis : int
|
285
|
-
|
361
|
+
Index of axis over which phasor coordinates are computed.
|
286
362
|
axis_label : str
|
287
|
-
|
363
|
+
Label of axis from `signal.dims` if available, empty string otherwise.
|
288
364
|
|
289
365
|
Raises
|
290
366
|
------
|
291
367
|
ValueError
|
292
|
-
|
368
|
+
If axis string is not found in signal.dims.
|
369
|
+
If axis string is provided but signal has no dims attribute.
|
293
370
|
|
294
371
|
Examples
|
295
372
|
--------
|
@@ -356,15 +433,17 @@ def parse_skip_axis(
|
|
356
433
|
|
357
434
|
Raises
|
358
435
|
------
|
436
|
+
ValueError
|
437
|
+
If ndim is negative.
|
359
438
|
IndexError
|
360
439
|
If any `skip_axis` value is out of bounds of `ndim`.
|
361
440
|
|
362
441
|
Examples
|
363
442
|
--------
|
364
|
-
>>> parse_skip_axis(
|
443
|
+
>>> parse_skip_axis([1, -2], 5)
|
365
444
|
((1, 3), (0, 2, 4))
|
366
445
|
|
367
|
-
>>> parse_skip_axis(
|
446
|
+
>>> parse_skip_axis([1, -2], 5, True)
|
368
447
|
((0, 2, 4), (1, 3, 5))
|
369
448
|
|
370
449
|
"""
|
@@ -401,7 +480,7 @@ def parse_harmonic(
|
|
401
480
|
harmonic : int, sequence of int, 'all', or None
|
402
481
|
Harmonic parameter to parse.
|
403
482
|
harmonic_max : int, optional
|
404
|
-
Maximum value allowed in `
|
483
|
+
Maximum value allowed in `harmonic`. Must be one or greater.
|
405
484
|
To verify against known number of signal samples,
|
406
485
|
pass ``samples // 2``.
|
407
486
|
If `harmonic='all'`, a range of harmonics from one to `harmonic_max`
|
@@ -487,11 +566,11 @@ def chunk_iter(
|
|
487
566
|
pattern : str, optional
|
488
567
|
String to format chunk indices.
|
489
568
|
If None, use ``_[{dims[index]}{chunk_index[index]}]`` for each axis.
|
490
|
-
squeeze : bool
|
569
|
+
squeeze : bool, optional
|
491
570
|
If true, do not include length-1 chunked dimensions in label
|
492
571
|
unless dimensions are part of `chunk_shape`.
|
493
572
|
Applies only if `pattern` is None.
|
494
|
-
use_index : bool
|
573
|
+
use_index : bool, optional
|
495
574
|
If true, use indices of chunks in `shape` instead of chunk indices to
|
496
575
|
format pattern.
|
497
576
|
|
@@ -594,7 +673,7 @@ def chunk_iter(
|
|
594
673
|
|
595
674
|
|
596
675
|
def init_module(globs: dict[str, Any], /) -> None:
|
597
|
-
"""Add names in module to ``__all__``
|
676
|
+
"""Add names in module to ``__all__`` attribute.
|
598
677
|
|
599
678
|
Parameters
|
600
679
|
----------
|
@@ -617,9 +696,11 @@ def init_module(globs: dict[str, Any], /) -> None:
|
|
617
696
|
}:
|
618
697
|
continue
|
619
698
|
names.append(name)
|
620
|
-
|
621
|
-
|
622
|
-
|
699
|
+
# do not change __module__ attributes because that may interfere
|
700
|
+
# with introspection and pickling
|
701
|
+
# obj = getattr(module, name)
|
702
|
+
# if hasattr(obj, '__module__'):
|
703
|
+
# obj.__module__ = module_name
|
623
704
|
globs['__all__'] = sorted(set(names))
|
624
705
|
|
625
706
|
|