phasorpy 0.6__cp312-cp312-win_amd64.whl → 0.8__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 CHANGED
@@ -5,5 +5,5 @@ from __future__ import annotations
5
5
  __all__ = ['__version__']
6
6
 
7
7
 
8
- __version__ = '0.6'
8
+ __version__ = '0.8'
9
9
  """PhasorPy version string."""
Binary file
phasorpy/_phasorpy.pyx CHANGED
@@ -6,7 +6,13 @@
6
6
  # cython: nonecheck = False
7
7
  # cython: freethreading_compatible = True
8
8
 
9
- """Cython implementation of low-level functions for the PhasorPy library."""
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 = phasor_from_lifetime(donor_lifetime, omega)
497
+ real, imag = phasor_from_single_lifetime(donor_lifetime, omega)
492
498
 
493
499
  # phasor of quenched donor
494
- quenched_real, quenched_imag = phasor_from_lifetime(
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 = phasor_from_lifetime(donor_lifetime, omega)
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 = phasor_from_lifetime(
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 = phasor_from_lifetime(
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 (float_t, float_t) phasor_from_lifetime(
650
- float_t lifetime,
651
- float_t omega,
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 <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
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, (0.0, 0.5))
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: tuple[float, float] | None = None,
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 : (float, float)
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, imag : ndarray
150
- Coordinates sorted by angle.
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 < 4:
190
+ if x.size < 3:
164
191
  return x, y, numpy.arange(x.size)
165
192
  if origin is None:
166
- origin = x.mean(), y.mean()
167
- indices = numpy.argsort(numpy.arctan2(y - origin[1], x - origin[0]))
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, imag : ndarray
189
- Coordinates dilated by offset.
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
- >>> phasor_to_polar_scalar(1.0, 0.0, degree=True, percent=True)
229
- (0.0, 100.0)
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
- Image stack.
277
- axis : int or str, optional
278
- Axis over which phasor coordinates are computed.
279
- By default, the 'H' or 'C' axes if `signal` contains such
280
- dimension names, else the last axis (-1).
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
- Axis over which phasor coordinates are computed.
361
+ Index of axis over which phasor coordinates are computed.
286
362
  axis_label : str
287
- Axis label from `signal.dims` if any.
363
+ Label of axis from `signal.dims` if available, empty string otherwise.
288
364
 
289
365
  Raises
290
366
  ------
291
367
  ValueError
292
- Axis not found in signal.dims or invalid for signal type.
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((1, -2), 5)
443
+ >>> parse_skip_axis([1, -2], 5)
365
444
  ((1, 3), (0, 2, 4))
366
445
 
367
- >>> parse_skip_axis((1, -2), 5, True)
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 `hamonic`. Must be one or greater.
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__`` and set ``__module__`` attributes.
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
- obj = getattr(module, name)
621
- if hasattr(obj, '__module__'):
622
- obj.__module__ = module_name
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