phasorpy 0.5__cp312-cp312-win_amd64.whl → 0.6__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/phasor.py CHANGED
@@ -17,6 +17,7 @@ The ``phasorpy.phasor`` module provides functions to:
17
17
  - :py:func:`phasor_from_lifetime`
18
18
  - :py:func:`phasor_from_apparent_lifetime`
19
19
  - :py:func:`phasor_to_apparent_lifetime`
20
+ - :py:func:`phasor_to_normal_lifetime`
20
21
 
21
22
  - convert to and from polar coordinates (phase and modulation):
22
23
 
@@ -69,6 +70,10 @@ The ``phasorpy.phasor`` module provides functions to:
69
70
  - :py:func:`phasor_filter_pawflim`
70
71
  - :py:func:`phasor_threshold`
71
72
 
73
+ - find nearest neighbor phasor coordinates from another set of phasor coordinates:
74
+
75
+ - :py:func:`phasor_nearest_neighbor`
76
+
72
77
  """
73
78
 
74
79
  from __future__ import annotations
@@ -92,11 +97,14 @@ __all__ = [
92
97
  'phasor_from_polar',
93
98
  'phasor_from_signal',
94
99
  'phasor_multiply',
100
+ 'phasor_nearest_neighbor',
95
101
  'phasor_normalize',
96
102
  'phasor_semicircle',
103
+ 'phasor_semicircle_intersect',
97
104
  'phasor_threshold',
98
105
  'phasor_to_apparent_lifetime',
99
106
  'phasor_to_complex',
107
+ 'phasor_to_normal_lifetime',
100
108
  'phasor_to_polar',
101
109
  'phasor_to_principal_plane',
102
110
  'phasor_to_signal',
@@ -125,7 +133,9 @@ import numpy
125
133
 
126
134
  from ._phasorpy import (
127
135
  _gaussian_signal,
136
+ _intersect_semicircle_line,
128
137
  _median_filter_2d,
138
+ _nearest_neighbor_2d,
129
139
  _phasor_at_harmonic,
130
140
  _phasor_divide,
131
141
  _phasor_from_apparent_lifetime,
@@ -142,6 +152,7 @@ from ._phasorpy import (
142
152
  _phasor_threshold_nan,
143
153
  _phasor_threshold_open,
144
154
  _phasor_to_apparent_lifetime,
155
+ _phasor_to_normal_lifetime,
145
156
  _phasor_to_polar,
146
157
  _phasor_transform,
147
158
  _phasor_transform_const,
@@ -674,7 +685,7 @@ def lifetime_to_signal(
674
685
  mean = 1.0
675
686
  mean = numpy.asarray(mean)
676
687
  mean -= background
677
- if numpy.any(mean <= 0.0):
688
+ if numpy.any(mean < 0.0):
678
689
  raise ValueError('mean - background must not be less than zero')
679
690
 
680
691
  scale = samples / (2.0 * math.pi)
@@ -794,6 +805,65 @@ def phasor_semicircle(
794
805
  return real, imag
795
806
 
796
807
 
808
+ def phasor_semicircle_intersect(
809
+ real0: ArrayLike,
810
+ imag0: ArrayLike,
811
+ real1: ArrayLike,
812
+ imag1: ArrayLike,
813
+ /,
814
+ **kwargs: Any,
815
+ ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], NDArray[Any]]:
816
+ """Return intersection of line through phasors with universal semicircle.
817
+
818
+ Return the phasor coordinates of two intersections of the universal
819
+ semicircle with the line between two phasor coordinates.
820
+ Return NaN if the line does not intersect the semicircle.
821
+
822
+ Parameters
823
+ ----------
824
+ real0 : array_like
825
+ Real component of first set of phasor coordinates.
826
+ imag0 : array_like
827
+ Imaginary component of first set of phasor coordinates.
828
+ real1 : array_like
829
+ Real component of second set of phasor coordinates.
830
+ imag1 : array_like
831
+ Imaginary component of second set of phasor coordinates.
832
+ **kwargs
833
+ Optional `arguments passed to numpy universal functions
834
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
835
+
836
+ Returns
837
+ -------
838
+ real0 : ndarray
839
+ Real component of first intersect of phasors with semicircle.
840
+ imag0 : ndarray
841
+ Imaginary component of first intersect of phasors with semicircle.
842
+ real1 : ndarray
843
+ Real component of second intersect of phasors with semicircle.
844
+ imag1 : ndarray
845
+ Imaginary component of second intersect of phasors with semicircle.
846
+
847
+ Examples
848
+ --------
849
+ Calculate two intersects of a line through two phasor coordinates
850
+ with the universal semicircle:
851
+
852
+ >>> phasor_semicircle_intersect(0.2, 0.25, 0.6, 0.25) # doctest: +NUMBER
853
+ (0.066, 0.25, 0.933, 0.25)
854
+
855
+ The line between two phasor coordinates may not intersect the semicircle
856
+ at two points:
857
+
858
+ >>> phasor_semicircle_intersect(0.2, 0.0, 0.6, 0.25) # doctest: +NUMBER
859
+ (nan, nan, 0.817, 0.386)
860
+
861
+ """
862
+ return _intersect_semicircle_line( # type: ignore[no-any-return]
863
+ real0, imag0, real1, imag1, **kwargs
864
+ )
865
+
866
+
797
867
  def phasor_to_complex(
798
868
  real: ArrayLike,
799
869
  imag: ArrayLike,
@@ -1577,6 +1647,7 @@ def phasor_to_polar(
1577
1647
  See Also
1578
1648
  --------
1579
1649
  phasorpy.phasor.phasor_from_polar
1650
+ :ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
1580
1651
 
1581
1652
  Examples
1582
1653
  --------
@@ -1684,6 +1755,7 @@ def phasor_to_apparent_lifetime(
1684
1755
  See Also
1685
1756
  --------
1686
1757
  phasorpy.phasor.phasor_from_apparent_lifetime
1758
+ :ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
1687
1759
 
1688
1760
  Notes
1689
1761
  -----
@@ -1819,6 +1891,86 @@ def phasor_from_apparent_lifetime(
1819
1891
  )
1820
1892
 
1821
1893
 
1894
+ def phasor_to_normal_lifetime(
1895
+ real: ArrayLike,
1896
+ imag: ArrayLike,
1897
+ /,
1898
+ frequency: ArrayLike,
1899
+ *,
1900
+ unit_conversion: float = 1e-3,
1901
+ **kwargs: Any,
1902
+ ) -> NDArray[Any]:
1903
+ r"""Return normal lifetimes from phasor coordinates.
1904
+
1905
+ The normal lifetime of phasor coordinates represents the single lifetime
1906
+ equivalent corresponding to the perpendicular projection of the coordinates
1907
+ onto the universal semicircle, as defined in [3]_.
1908
+
1909
+ Parameters
1910
+ ----------
1911
+ real : array_like
1912
+ Real component of phasor coordinates.
1913
+ imag : array_like
1914
+ Imaginary component of phasor coordinates.
1915
+ frequency : array_like
1916
+ Laser pulse or modulation frequency in MHz.
1917
+ unit_conversion : float, optional
1918
+ Product of `frequency` and returned `lifetime` units' prefix factors.
1919
+ The default is 1e-3 for MHz and ns, or Hz and ms.
1920
+ Use 1.0 for Hz and s.
1921
+ **kwargs
1922
+ Optional `arguments passed to numpy universal functions
1923
+ <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
1924
+
1925
+ Returns
1926
+ -------
1927
+ normal_lifetime : ndarray
1928
+ Normal lifetime from of phasor coordinates.
1929
+
1930
+ See Also
1931
+ --------
1932
+ :ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
1933
+
1934
+ Notes
1935
+ -----
1936
+ The phasor coordinates `real` (:math:`G`) and `imag` (:math:`S`)
1937
+ are converted to normal lifetimes `normal_lifetime` (:math:`\tau_{N}`)
1938
+ at frequency :math:`f` according to:
1939
+
1940
+ .. math::
1941
+
1942
+ \omega &= 2 \pi f
1943
+
1944
+ G_{N} &= 0.5 \cdot (1 + \cos{\arctan{\frac{S}{G - 0.5}}})
1945
+
1946
+ \tau_{N} &= \sqrt{\frac{1 - G_{N}}{\omega^{2} \cdot G_{N}}}
1947
+
1948
+ References
1949
+ ----------
1950
+
1951
+ .. [3] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
1952
+ uncertainty to enable lower photon count in FLIM experiments
1953
+ <https://doi.org/10.1088/2050-6120/aa72ab>`_.
1954
+ *Methods Appl Fluoresc*, 5(2): 024016 (2017)
1955
+
1956
+ Examples
1957
+ --------
1958
+ The normal lifetimes of phasor coordinates with a real component of 0.5
1959
+ are independent of the imaginary component:
1960
+
1961
+ >>> phasor_to_normal_lifetime(
1962
+ ... 0.5, [0.5, 0.45], frequency=80
1963
+ ... ) # doctest: +NUMBER
1964
+ array([1.989, 1.989])
1965
+
1966
+ """
1967
+ omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
1968
+ omega *= math.pi * 2.0 * unit_conversion
1969
+ return _phasor_to_normal_lifetime( # type: ignore[no-any-return]
1970
+ real, imag, omega, **kwargs
1971
+ )
1972
+
1973
+
1822
1974
  def lifetime_to_frequency(
1823
1975
  lifetime: ArrayLike,
1824
1976
  *,
@@ -2138,6 +2290,11 @@ def phasor_from_lifetime(
2138
2290
  ValueError
2139
2291
  Input arrays exceed their allowed dimensionality or do not match.
2140
2292
 
2293
+ See Also
2294
+ --------
2295
+ :ref:`sphx_glr_tutorials_api_phasorpy_phasor_from_lifetime.py`
2296
+ :ref:`sphx_glr_tutorials_phasorpy_lifetime_geometry.py`
2297
+
2141
2298
  Notes
2142
2299
  -----
2143
2300
  The phasor coordinates :math:`G` (`real`) and :math:`S` (`imag`) for
@@ -2517,6 +2674,7 @@ def phasor_from_fret_donor(
2517
2674
  --------
2518
2675
  phasorpy.phasor.phasor_from_fret_acceptor
2519
2676
  :ref:`sphx_glr_tutorials_api_phasorpy_fret.py`
2677
+ :ref:`sphx_glr_tutorials_applications_phasorpy_fret_efficiency.py`
2520
2678
 
2521
2679
  Examples
2522
2680
  --------
@@ -3452,6 +3610,154 @@ def phasor_threshold(
3452
3610
  return mean, real, imag
3453
3611
 
3454
3612
 
3613
+ def phasor_nearest_neighbor(
3614
+ real: ArrayLike,
3615
+ imag: ArrayLike,
3616
+ neighbor_real: ArrayLike,
3617
+ neighbor_imag: ArrayLike,
3618
+ /,
3619
+ *,
3620
+ values: ArrayLike | None = None,
3621
+ dtype: DTypeLike | None = None,
3622
+ distance_max: float | None = None,
3623
+ num_threads: int | None = None,
3624
+ ) -> NDArray[Any]:
3625
+ """Return indices or values of nearest neighbor from other coordinates.
3626
+
3627
+ For each phasor coordinate, find the nearest neighbor in another set of
3628
+ phasor coordinates and return its flat index. If more than one neighbor
3629
+ has the same distance, return the smallest index.
3630
+
3631
+ For phasor coordinates that are NaN, or have a distance to the nearest
3632
+ neighbor that is larger than `distance_max`, return an index of -1.
3633
+
3634
+ If `values` are provided, return the values corresponding to the nearest
3635
+ neighbor coordinates instead of indices. Return NaN values for indices
3636
+ that are -1.
3637
+
3638
+ This function does not support multi-harmonic, multi-channel, or
3639
+ multi-frequency phasor coordinates.
3640
+
3641
+ Parameters
3642
+ ----------
3643
+ real : array_like
3644
+ Real component of phasor coordinates.
3645
+ imag : array_like
3646
+ Imaginary component of phasor coordinates.
3647
+ neighbor_real : array_like
3648
+ Real component of neighbor phasor coordinates.
3649
+ neighbor_imag : array_like
3650
+ Imaginary component of neighbor phasor coordinates.
3651
+ values : array_like, optional
3652
+ Array of values corresponding to neighbor coordinates.
3653
+ If provided, return the values corresponding to the nearest
3654
+ neighbor coordinates.
3655
+ distance_max : float, optional
3656
+ Maximum Euclidean distance to consider a neighbor valid.
3657
+ By default, all neighbors are considered.
3658
+ dtype : dtype_like, optional
3659
+ Floating point data type used for calculation and output values.
3660
+ Either `float32` or `float64`. The default is `float64`.
3661
+ num_threads : int, optional
3662
+ Number of OpenMP threads to use for parallelization.
3663
+ By default, multi-threading is disabled.
3664
+ If zero, up to half of logical CPUs are used.
3665
+ OpenMP may not be available on all platforms.
3666
+
3667
+ Returns
3668
+ -------
3669
+ nearest : ndarray
3670
+ Flat indices (or the corresponding values if provided) of the nearest
3671
+ neighbor coordinates.
3672
+
3673
+ Raises
3674
+ ------
3675
+ ValueError
3676
+ If the shapes of `real`, and `imag` do not match.
3677
+ If the shapes of `neighbor_real` and `neighbor_imag` do not match.
3678
+ If the shapes of `values` and `neighbor_real` do not match.
3679
+ If `distance_max` is less than or equal to zero.
3680
+
3681
+ See Also
3682
+ --------
3683
+ :ref:`sphx_glr_tutorials_applications_phasorpy_fret_efficiency.py`
3684
+
3685
+ Notes
3686
+ -----
3687
+ This function uses linear search, which is inefficient for large
3688
+ number of coordinates or neighbors.
3689
+ ``scipy.spatial.KDTree.query()`` would be more efficient in those cases.
3690
+ However, KDTree is known to return non-deterministic results in case of
3691
+ multiple neighbors with the same distance.
3692
+
3693
+ Examples
3694
+ --------
3695
+ >>> phasor_nearest_neighbor(
3696
+ ... [0.1, 0.5, numpy.nan],
3697
+ ... [0.1, 0.5, numpy.nan],
3698
+ ... [0, 0.4],
3699
+ ... [0, 0.4],
3700
+ ... values=[10, 20],
3701
+ ... )
3702
+ array([10, 20, nan])
3703
+
3704
+ """
3705
+ dtype = numpy.dtype(dtype)
3706
+ if dtype.char not in {'f', 'd'}:
3707
+ raise ValueError(f'{dtype=} is not a floating point type')
3708
+
3709
+ real = numpy.ascontiguousarray(real, dtype=dtype)
3710
+ imag = numpy.ascontiguousarray(imag, dtype=dtype)
3711
+ neighbor_real = numpy.ascontiguousarray(neighbor_real, dtype=dtype)
3712
+ neighbor_imag = numpy.ascontiguousarray(neighbor_imag, dtype=dtype)
3713
+
3714
+ if real.shape != imag.shape:
3715
+ raise ValueError(f'{real.shape=} != {imag.shape=}')
3716
+ if neighbor_real.shape != neighbor_imag.shape:
3717
+ raise ValueError(f'{neighbor_real.shape=} != {neighbor_imag.shape=}')
3718
+
3719
+ shape = real.shape
3720
+ real = real.ravel()
3721
+ imag = imag.ravel()
3722
+ neighbor_real = neighbor_real.ravel()
3723
+ neighbor_imag = neighbor_imag.ravel()
3724
+
3725
+ indices = numpy.empty(
3726
+ real.shape, numpy.min_scalar_type(-neighbor_real.size)
3727
+ )
3728
+
3729
+ if distance_max is None:
3730
+ distance_max = numpy.inf
3731
+ else:
3732
+ distance_max = float(distance_max)
3733
+ if distance_max <= 0:
3734
+ raise ValueError(f'{distance_max=} <= 0')
3735
+
3736
+ num_threads = number_threads(num_threads)
3737
+
3738
+ _nearest_neighbor_2d(
3739
+ indices,
3740
+ real,
3741
+ imag,
3742
+ neighbor_real,
3743
+ neighbor_imag,
3744
+ distance_max,
3745
+ num_threads,
3746
+ )
3747
+
3748
+ if values is None:
3749
+ return numpy.asarray(indices.reshape(shape))
3750
+
3751
+ values = numpy.ascontiguousarray(values, dtype=dtype).ravel()
3752
+ if values.shape != neighbor_real.shape:
3753
+ raise ValueError(f'{values.shape=} != {neighbor_real.shape=}')
3754
+
3755
+ nearest_values = values[indices]
3756
+ nearest_values[indices == -1] = numpy.nan
3757
+
3758
+ return numpy.asarray(nearest_values.reshape(shape))
3759
+
3760
+
3455
3761
  def phasor_center(
3456
3762
  mean: ArrayLike,
3457
3763
  real: ArrayLike,
@@ -0,0 +1,27 @@
1
+ """Plot phasor coordinates and related data.
2
+
3
+ The ``phasorpy.plot`` module provides functions and classes to visualize
4
+ phasor coordinates and related data using the
5
+ `matplotlib <https://matplotlib.org/>`_ library.
6
+
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __all__: list[str] = []
12
+
13
+ from .._utils import init_module
14
+ from ._functions import *
15
+ from ._lifetime_plots import *
16
+ from ._phasorplot import *
17
+ from ._phasorplot_fret import *
18
+
19
+ # The `init_module()` function dynamically populates the `__all__` list with
20
+ # all public symbols imported from submodules or defined in this module.
21
+ # Any name not starting with an underscore will be automatically exported
22
+ # when using "from phasorpy.plot import *"
23
+
24
+ init_module(globals())
25
+ del init_module
26
+
27
+ # flake8: noqa: F401, F403