phasorpy 0.5__cp313-cp313-macosx_11_0_arm64.whl → 0.7__cp313-cp313-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 CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __all__ = ['__version__', 'versions']
5
+ __all__ = ['__version__']
6
6
 
7
- from .version import __version__, versions
8
7
 
9
- __version__ = __version__ # pylint: disable=self-assigning-variable
8
+ __version__ = '0.7'
10
9
  """PhasorPy version string."""
Binary file
phasorpy/_phasorpy.pyx CHANGED
@@ -50,6 +50,12 @@ ctypedef fused uint_t:
50
50
  uint32_t
51
51
  uint64_t
52
52
 
53
+ ctypedef fused int_t:
54
+ int8_t
55
+ int16_t
56
+ int32_t
57
+ int64_t
58
+
53
59
  ctypedef fused signal_t:
54
60
  uint8_t
55
61
  uint16_t
@@ -482,10 +488,10 @@ cdef (double, double) _phasor_from_fret_donor(
482
488
  return 1.0, 0.0
483
489
 
484
490
  # phasor of pure donor at frequency
485
- real, imag = phasor_from_lifetime(donor_lifetime, omega)
491
+ real, imag = phasor_from_single_lifetime(donor_lifetime, omega)
486
492
 
487
493
  # phasor of quenched donor
488
- quenched_real, quenched_imag = phasor_from_lifetime(
494
+ quenched_real, quenched_imag = phasor_from_single_lifetime(
489
495
  donor_lifetime * (1.0 - fret_efficiency), omega
490
496
  )
491
497
 
@@ -549,14 +555,14 @@ cdef (double, double) _phasor_from_fret_acceptor(
549
555
  acceptor_background = 0.0
550
556
 
551
557
  # phasor of pure donor at frequency
552
- donor_real, donor_imag = phasor_from_lifetime(donor_lifetime, omega)
558
+ donor_real, donor_imag = phasor_from_single_lifetime(donor_lifetime, omega)
553
559
 
554
560
  if fret_efficiency == 0.0:
555
561
  quenched_real = donor_real
556
562
  quenched_imag = donor_imag
557
563
  else:
558
564
  # phasor of quenched donor
559
- quenched_real, quenched_imag = phasor_from_lifetime(
565
+ quenched_real, quenched_imag = phasor_from_single_lifetime(
560
566
  donor_lifetime * (1.0 - fret_efficiency), omega
561
567
  )
562
568
 
@@ -574,7 +580,7 @@ cdef (double, double) _phasor_from_fret_acceptor(
574
580
  )
575
581
 
576
582
  # phasor of acceptor at frequency
577
- acceptor_real, acceptor_imag = phasor_from_lifetime(
583
+ acceptor_real, acceptor_imag = phasor_from_single_lifetime(
578
584
  acceptor_lifetime, omega
579
585
  )
580
586
 
@@ -640,9 +646,9 @@ cdef inline (double, double) linear_combination(
640
646
  )
641
647
 
642
648
 
643
- cdef inline (float_t, float_t) phasor_from_lifetime(
644
- float_t lifetime,
645
- float_t omega,
649
+ cdef inline (double, double) phasor_from_single_lifetime(
650
+ const double lifetime,
651
+ const double omega,
646
652
  ) noexcept nogil:
647
653
  """Return phasor coordinates from single lifetime component."""
648
654
  cdef:
@@ -650,7 +656,7 @@ cdef inline (float_t, float_t) phasor_from_lifetime(
650
656
  double mod = 1.0 / sqrt(1.0 + t * t)
651
657
  double phi = atan(t)
652
658
 
653
- return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
659
+ return mod * cos(phi), mod * sin(phi)
654
660
 
655
661
 
656
662
  ###############################################################################
@@ -767,6 +773,33 @@ cdef (float_t, float_t) _phasor_from_apparent_lifetime(
767
773
  return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
768
774
 
769
775
 
776
+ @cython.ufunc
777
+ cdef float_t _phasor_to_normal_lifetime(
778
+ float_t real,
779
+ float_t imag,
780
+ float_t omega,
781
+ ) noexcept nogil:
782
+ """Return normal lifetimes from phasor coordinates."""
783
+ cdef:
784
+ double taunorm = INFINITY
785
+ double t
786
+
787
+ if isnan(real) or isnan(imag):
788
+ return <float_t> NAN
789
+
790
+ omega *= omega
791
+ if omega > 0.0:
792
+ t = 0.5 * (1.0 + cos(atan2(imag, real - 0.5)))
793
+ if t <= 0.0:
794
+ taunorm = INFINITY
795
+ elif t > 1.0:
796
+ taunorm = NAN
797
+ else:
798
+ taunorm = sqrt((1.0 - t) / (omega * t))
799
+
800
+ return <float_t> taunorm
801
+
802
+
770
803
  @cython.ufunc
771
804
  cdef (float_t, float_t) _phasor_from_single_lifetime(
772
805
  float_t lifetime,
@@ -1204,6 +1237,44 @@ cdef unsigned char _is_inside_stadium(
1204
1237
  _is_near_segment = _is_inside_stadium
1205
1238
 
1206
1239
 
1240
+ @cython.ufunc
1241
+ cdef unsigned char _is_inside_semicircle(
1242
+ float_t x, # point
1243
+ float_t y,
1244
+ float_t r, # distance
1245
+ ) noexcept nogil:
1246
+ """Return whether point is inside universal semicircle."""
1247
+ if r < 0.0 or isnan(x) or isnan(y):
1248
+ return False
1249
+ if y < -r:
1250
+ return False
1251
+ if y <= 0.0:
1252
+ if x >= 0.0 and x <= 1.0:
1253
+ return True
1254
+ # near endpoints?
1255
+ if x > 0.5:
1256
+ x -= <float_t> 1.0
1257
+ return x * x + y * y <= r * r
1258
+ return hypot(x - 0.5, y) <= r + 0.5
1259
+
1260
+
1261
+ @cython.ufunc
1262
+ cdef unsigned char _is_near_semicircle(
1263
+ float_t x, # point
1264
+ float_t y,
1265
+ float_t r, # distance
1266
+ ) noexcept nogil:
1267
+ """Return whether point is near universal semicircle."""
1268
+ if r < 0.0 or isnan(x) or isnan(y):
1269
+ return False
1270
+ if y < 0.0:
1271
+ # near endpoints?
1272
+ if x > 0.5:
1273
+ x -= <float_t> 1.0
1274
+ return x * x + y * y <= r * r
1275
+ return fabs(hypot(x - 0.5, y) - 0.5) <= r
1276
+
1277
+
1207
1278
  @cython.ufunc
1208
1279
  cdef unsigned char _is_near_line(
1209
1280
  float_t x, # point
@@ -1470,6 +1541,22 @@ cdef float_t _distance_from_line(
1470
1541
  return <float_t> hypot(x, y)
1471
1542
 
1472
1543
 
1544
+ @cython.ufunc
1545
+ cdef float_t _distance_from_semicircle(
1546
+ float_t x, # point
1547
+ float_t y,
1548
+ ) noexcept nogil:
1549
+ """Return distance from universal semicircle."""
1550
+ if isnan(x) or isnan(y):
1551
+ return NAN
1552
+ if y < 0.0:
1553
+ # distance to endpoints
1554
+ if x > 0.5:
1555
+ x -= <float_t> 1.0
1556
+ return <float_t> hypot(x, y)
1557
+ return <float_t> fabs(hypot(x - 0.5, y) - 0.5)
1558
+
1559
+
1473
1560
  @cython.ufunc
1474
1561
  cdef (float_t, float_t, float_t) _segment_direction_and_length(
1475
1562
  float_t x0, # segment start
@@ -1495,7 +1582,7 @@ cdef (float_t, float_t, float_t) _segment_direction_and_length(
1495
1582
 
1496
1583
 
1497
1584
  @cython.ufunc
1498
- cdef (float_t, float_t, float_t, float_t) _intersection_circle_circle(
1585
+ cdef (float_t, float_t, float_t, float_t) _intersect_circle_circle(
1499
1586
  float_t x0, # circle 0
1500
1587
  float_t y0,
1501
1588
  float_t r0,
@@ -1541,7 +1628,7 @@ cdef (float_t, float_t, float_t, float_t) _intersection_circle_circle(
1541
1628
 
1542
1629
 
1543
1630
  @cython.ufunc
1544
- cdef (float_t, float_t, float_t, float_t) _intersection_circle_line(
1631
+ cdef (float_t, float_t, float_t, float_t) _intersect_circle_line(
1545
1632
  float_t x, # circle
1546
1633
  float_t y,
1547
1634
  float_t r,
@@ -1583,10 +1670,378 @@ cdef (float_t, float_t, float_t, float_t) _intersection_circle_line(
1583
1670
  )
1584
1671
 
1585
1672
 
1673
+ @cython.ufunc
1674
+ cdef (float_t, float_t, float_t, float_t) _intersect_semicircle_line(
1675
+ float_t x0, # line start
1676
+ float_t y0,
1677
+ float_t x1, # line end
1678
+ float_t y1,
1679
+ ) noexcept nogil:
1680
+ """Return coordinates of intersections of line and universal semicircle."""
1681
+ cdef:
1682
+ double dx, dy, dr, dd, rdd
1683
+
1684
+ if isnan(x0) or isnan(x1) or isnan(y0) or isnan(y1):
1685
+ return NAN, NAN, NAN, NAN
1686
+
1687
+ dx = x1 - x0
1688
+ dy = y1 - y0
1689
+ dr = dx * dx + dy * dy
1690
+ dd = (x0 - 0.5) * y1 - (x1 - 0.5) * y0
1691
+ rdd = 0.25 * dr - dd * dd # discriminant
1692
+ if rdd < 0.0 or dr <= 0.0:
1693
+ # no intersection
1694
+ return NAN, NAN, NAN, NAN
1695
+ rdd = sqrt(rdd)
1696
+ x0 = <float_t> ((dd * dy - copysign(1.0, dy) * dx * rdd) / dr + 0.5)
1697
+ y0 = <float_t> ((-dd * dx - fabs(dy) * rdd) / dr)
1698
+ x1 = <float_t> ((dd * dy + copysign(1.0, dy) * dx * rdd) / dr + 0.5)
1699
+ y1 = <float_t> ((-dd * dx + fabs(dy) * rdd) / dr)
1700
+ if y0 < 0.0:
1701
+ x0 = NAN
1702
+ y0 = NAN
1703
+ if y1 < 0.0:
1704
+ x1 = NAN
1705
+ y1 = NAN
1706
+ return x0, y0, x1, y1
1707
+
1708
+
1709
+ ###############################################################################
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
+
1867
+ def _nearest_neighbor_2d(
1868
+ int_t[::1] indices,
1869
+ const float_t[::1] x0,
1870
+ const float_t[::1] y0,
1871
+ const float_t[::1] x1,
1872
+ const float_t[::1] y1,
1873
+ const float_t distance_max,
1874
+ const int num_threads
1875
+ ):
1876
+ """Find nearest neighbors in 2D.
1877
+
1878
+ For each point in the first set of arrays (x0, y0) find the nearest point
1879
+ in the second set of arrays (x1, y1) and store the index of the nearest
1880
+ point in the second array in the indices array.
1881
+ If any coordinates are NaN, or the distance to the nearest point
1882
+ is larger than distance_max, the index is set to -1.
1883
+
1884
+ """
1885
+ cdef:
1886
+ ssize_t i, j, index
1887
+ float_t x, y, dmin
1888
+ float_t distance_max_squared = distance_max * distance_max
1889
+
1890
+ if (
1891
+ indices.shape[0] != x0.shape[0]
1892
+ or x0.shape[0] != y0.shape[0]
1893
+ or x1.shape[0] != y1.shape[0]
1894
+ ):
1895
+ raise ValueError('input array size mismatch')
1896
+
1897
+ with nogil, parallel(num_threads=num_threads):
1898
+ for i in prange(x0.shape[0]):
1899
+ x = x0[i]
1900
+ y = y0[i]
1901
+ if isnan(x) or isnan(y):
1902
+ indices[i] = -1
1903
+ continue
1904
+ index = -1
1905
+ dmin = INFINITY
1906
+ for j in range(x1.shape[0]):
1907
+ x = x0[i] - x1[j]
1908
+ y = y0[i] - y1[j]
1909
+ x = x * x + y * y
1910
+ if x < dmin:
1911
+ dmin = x
1912
+ index = j
1913
+ indices[i] = -1 if dmin > distance_max_squared else <int_t> index
1914
+
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
+
1586
2030
  ###############################################################################
1587
2031
  # Blend ufuncs
1588
2032
 
1589
2033
 
2034
+ @cython.ufunc
2035
+ cdef float_t _blend_and(
2036
+ float_t a, # base layer
2037
+ float_t b, # blend layer
2038
+ ) noexcept nogil:
2039
+ """Return blended layers using `and` mode."""
2040
+ if isnan(a):
2041
+ return NAN
2042
+ return b
2043
+
2044
+
1590
2045
  @cython.ufunc
1591
2046
  cdef float_t _blend_normal(
1592
2047
  float_t a, # base layer