phasorpy 0.7__cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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 +9 -0
- phasorpy/__main__.py +7 -0
- phasorpy/_phasorpy.cpython-312-aarch64-linux-gnu.so +0 -0
- phasorpy/_phasorpy.pyx +2688 -0
- phasorpy/_typing.py +77 -0
- phasorpy/_utils.py +786 -0
- phasorpy/cli.py +160 -0
- phasorpy/cluster.py +200 -0
- phasorpy/color.py +589 -0
- phasorpy/component.py +707 -0
- phasorpy/conftest.py +38 -0
- phasorpy/cursor.py +500 -0
- phasorpy/datasets.py +722 -0
- phasorpy/experimental.py +310 -0
- phasorpy/io/__init__.py +138 -0
- phasorpy/io/_flimlabs.py +360 -0
- phasorpy/io/_leica.py +331 -0
- phasorpy/io/_ometiff.py +444 -0
- phasorpy/io/_other.py +890 -0
- phasorpy/io/_simfcs.py +652 -0
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +2018 -0
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +723 -0
- phasorpy/plot/_lifetime_plots.py +563 -0
- phasorpy/plot/_phasorplot.py +1507 -0
- phasorpy/plot/_phasorplot_fret.py +561 -0
- phasorpy/py.typed +0 -0
- phasorpy/utils.py +172 -0
- phasorpy-0.7.dist-info/METADATA +74 -0
- phasorpy-0.7.dist-info/RECORD +36 -0
- phasorpy-0.7.dist-info/WHEEL +7 -0
- phasorpy-0.7.dist-info/entry_points.txt +2 -0
- phasorpy-0.7.dist-info/licenses/LICENSE.txt +21 -0
- phasorpy-0.7.dist-info/top_level.txt +1 -0
- phasorpy.libs/libgomp-947d5fa1.so.1.0.0 +0 -0
phasorpy/cli.py
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
"""PhasorPy package command line interface.
|
2
|
+
|
3
|
+
Invoke the command line application with::
|
4
|
+
|
5
|
+
$ python -m phasorpy --help
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from __future__ import annotations
|
10
|
+
|
11
|
+
__all__: list[str] = []
|
12
|
+
|
13
|
+
import os
|
14
|
+
from typing import TYPE_CHECKING
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from ._typing import Iterable
|
18
|
+
|
19
|
+
import click
|
20
|
+
|
21
|
+
from . import __version__
|
22
|
+
|
23
|
+
|
24
|
+
@click.group(help='PhasorPy package command line interface.')
|
25
|
+
@click.version_option(version=__version__)
|
26
|
+
def main() -> int:
|
27
|
+
"""PhasorPy command line interface."""
|
28
|
+
return 0
|
29
|
+
|
30
|
+
|
31
|
+
@main.command(help='Show runtime versions.')
|
32
|
+
@click.option(
|
33
|
+
'--verbose',
|
34
|
+
default=False,
|
35
|
+
is_flag=True,
|
36
|
+
type=click.BOOL,
|
37
|
+
help='Show module paths.',
|
38
|
+
)
|
39
|
+
def versions(verbose: bool) -> None:
|
40
|
+
"""Versions command group."""
|
41
|
+
from .utils import versions
|
42
|
+
|
43
|
+
click.echo(versions(verbose=verbose))
|
44
|
+
|
45
|
+
|
46
|
+
@main.command(help='Fetch sample files from remote repositories.')
|
47
|
+
@click.argument('files', nargs=-1)
|
48
|
+
@click.option(
|
49
|
+
'--hideprogress',
|
50
|
+
default=False,
|
51
|
+
is_flag=True,
|
52
|
+
type=click.BOOL,
|
53
|
+
help='Hide progressbar.',
|
54
|
+
)
|
55
|
+
def fetch(files: Iterable[str], hideprogress: bool) -> None:
|
56
|
+
"""Fetch command group."""
|
57
|
+
from . import datasets
|
58
|
+
|
59
|
+
files = datasets.fetch(
|
60
|
+
*files, return_scalar=False, progressbar=not hideprogress
|
61
|
+
)
|
62
|
+
click.echo(f'Cached at {os.path.commonpath(files)}')
|
63
|
+
|
64
|
+
|
65
|
+
@main.command(help='Start interactive FRET phasor plot.')
|
66
|
+
@click.option(
|
67
|
+
'--hide',
|
68
|
+
default=False,
|
69
|
+
is_flag=True,
|
70
|
+
type=click.BOOL,
|
71
|
+
help='Do not show interactive plot.',
|
72
|
+
)
|
73
|
+
def fret(hide: bool) -> None:
|
74
|
+
"""FRET command group."""
|
75
|
+
from .plot import PhasorPlotFret
|
76
|
+
|
77
|
+
plot = PhasorPlotFret(
|
78
|
+
frequency=60.0,
|
79
|
+
donor_lifetime=4.2,
|
80
|
+
acceptor_lifetime=3.0,
|
81
|
+
fret_efficiency=0.5,
|
82
|
+
interactive=True,
|
83
|
+
)
|
84
|
+
if not hide:
|
85
|
+
plot.show()
|
86
|
+
|
87
|
+
|
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
|
+
)
|
96
|
+
@click.option(
|
97
|
+
'-f',
|
98
|
+
'--frequency',
|
99
|
+
type=float,
|
100
|
+
required=False,
|
101
|
+
help='Laser/modulation frequency in MHz.',
|
102
|
+
)
|
103
|
+
@click.option(
|
104
|
+
'-l',
|
105
|
+
'--lifetime',
|
106
|
+
# default=(4.0, 1.0),
|
107
|
+
type=float,
|
108
|
+
multiple=True,
|
109
|
+
required=False,
|
110
|
+
help='Lifetime in ns.',
|
111
|
+
)
|
112
|
+
@click.option(
|
113
|
+
'-a',
|
114
|
+
'--fraction',
|
115
|
+
type=float,
|
116
|
+
multiple=True,
|
117
|
+
required=False,
|
118
|
+
help='Fractional intensity of lifetime.',
|
119
|
+
)
|
120
|
+
@click.option(
|
121
|
+
'--hide',
|
122
|
+
default=False,
|
123
|
+
is_flag=True,
|
124
|
+
type=click.BOOL,
|
125
|
+
help='Do not show interactive plot.',
|
126
|
+
)
|
127
|
+
def lifetime(
|
128
|
+
number_lifetimes: int,
|
129
|
+
frequency: float | None,
|
130
|
+
lifetime: tuple[float, ...],
|
131
|
+
fraction: tuple[float, ...],
|
132
|
+
hide: bool,
|
133
|
+
) -> None:
|
134
|
+
"""Lifetime command group."""
|
135
|
+
from .lifetime import phasor_semicircle, phasor_to_normal_lifetime
|
136
|
+
from .plot import LifetimePlots
|
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
|
+
|
147
|
+
plot = LifetimePlots(
|
148
|
+
frequency,
|
149
|
+
lifetime,
|
150
|
+
fraction if len(fraction) > 0 else None,
|
151
|
+
interactive=True,
|
152
|
+
)
|
153
|
+
if not hide:
|
154
|
+
plot.show()
|
155
|
+
|
156
|
+
|
157
|
+
if __name__ == '__main__':
|
158
|
+
import sys
|
159
|
+
|
160
|
+
sys.exit(main()) # pylint: disable=no-value-for-parameter
|
phasorpy/cluster.py
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
"""Cluster phasor coordinates.
|
2
|
+
|
3
|
+
The `phasorpy.cluster` module provides functions to:
|
4
|
+
|
5
|
+
- fit elliptic clusters to phasor coordinates using a
|
6
|
+
Gaussian Mixture Model (GMM):
|
7
|
+
|
8
|
+
- :py:func:`phasor_cluster_gmm`
|
9
|
+
|
10
|
+
"""
|
11
|
+
|
12
|
+
from __future__ import annotations
|
13
|
+
|
14
|
+
__all__ = ['phasor_cluster_gmm']
|
15
|
+
|
16
|
+
from typing import TYPE_CHECKING
|
17
|
+
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from ._typing import Any, ArrayLike, Literal
|
20
|
+
|
21
|
+
import math
|
22
|
+
|
23
|
+
import numpy
|
24
|
+
|
25
|
+
|
26
|
+
def phasor_cluster_gmm(
|
27
|
+
real: ArrayLike,
|
28
|
+
imag: ArrayLike,
|
29
|
+
/,
|
30
|
+
*,
|
31
|
+
sigma: float = 2.0,
|
32
|
+
clusters: int = 1,
|
33
|
+
sort: Literal['polar', 'phasor', 'area'] | None = None,
|
34
|
+
**kwargs: Any,
|
35
|
+
) -> tuple[
|
36
|
+
tuple[float, ...],
|
37
|
+
tuple[float, ...],
|
38
|
+
tuple[float, ...],
|
39
|
+
tuple[float, ...],
|
40
|
+
tuple[float, ...],
|
41
|
+
]:
|
42
|
+
"""Return elliptic clusters in phasor coordinates using GMM.
|
43
|
+
|
44
|
+
Fit a Gaussian Mixture Model (GMM) to the provided phasor coordinates and
|
45
|
+
extract the parameters of ellipses that represent each cluster according
|
46
|
+
to [1]_.
|
47
|
+
|
48
|
+
Parameters
|
49
|
+
----------
|
50
|
+
real : array_like
|
51
|
+
Real component of phasor coordinates.
|
52
|
+
imag : array_like
|
53
|
+
Imaginary component of phasor coordinates.
|
54
|
+
sigma : float, optional
|
55
|
+
Scaling factor for radii of major and minor axes.
|
56
|
+
Defaults to 2.0, which corresponds to the scaling of eigenvalues for
|
57
|
+
a 95% confidence ellipse.
|
58
|
+
clusters : int, optional
|
59
|
+
Number of Gaussian distributions to fit to phasor coordinates.
|
60
|
+
Defaults to 1.
|
61
|
+
sort : {'polar', 'phasor', 'area'}, optional
|
62
|
+
Sorting method for output clusters. Defaults to 'polar'.
|
63
|
+
|
64
|
+
- 'polar': Sort by polar coordinates (phase, then modulation).
|
65
|
+
- 'phasor': Sort by phasor coordinates (real, then imaginary).
|
66
|
+
- 'area': Sort by inverse area of ellipse (-major * minor).
|
67
|
+
|
68
|
+
**kwargs
|
69
|
+
Additional keyword arguments passed to
|
70
|
+
:py:class:`sklearn.mixture.GaussianMixture`.
|
71
|
+
|
72
|
+
Common options include:
|
73
|
+
|
74
|
+
- covariance_type : {'full', 'tied', 'diag', 'spherical'}
|
75
|
+
- max_iter : int, maximum number of EM iterations
|
76
|
+
- random_state : int, for reproducible results
|
77
|
+
|
78
|
+
Returns
|
79
|
+
-------
|
80
|
+
center_real : tuple of float
|
81
|
+
Real component of ellipse centers.
|
82
|
+
center_imag : tuple of float
|
83
|
+
Imaginary component of ellipse centers.
|
84
|
+
radius_major : tuple of float
|
85
|
+
Major radii of ellipses.
|
86
|
+
radius_minor : tuple of float
|
87
|
+
Minor radii of ellipses.
|
88
|
+
angle : tuple of float
|
89
|
+
Rotation angles of major axes in radians, within range [0, pi].
|
90
|
+
|
91
|
+
References
|
92
|
+
----------
|
93
|
+
.. [1] Vallmitjana A, Torrado B, and Gratton E.
|
94
|
+
`Phasor-based image segmentation: machine learning clustering techniques
|
95
|
+
<https://doi.org/10.1364/BOE.422766>`_.
|
96
|
+
*Biomed Opt Express*, 12(6): 3410-3422 (2021)
|
97
|
+
|
98
|
+
Examples
|
99
|
+
--------
|
100
|
+
Recover the clusters from a synthetic distribution of phasor coordinates
|
101
|
+
with two clusters:
|
102
|
+
|
103
|
+
>>> real1, imag1 = numpy.random.multivariate_normal(
|
104
|
+
... [0.2, 0.3], [[3e-3, 1e-3], [1e-3, 2e-3]], 100
|
105
|
+
... ).T
|
106
|
+
>>> real2, imag2 = numpy.random.multivariate_normal(
|
107
|
+
... [0.4, 0.5], [[2e-3, -1e-3], [-1e-3, 3e-3]], 100
|
108
|
+
... ).T
|
109
|
+
>>> real = numpy.concatenate([real1, real2])
|
110
|
+
>>> imag = numpy.concatenate([imag1, imag2])
|
111
|
+
>>> center_real, center_imag, radius_major, radius_minor, angle = (
|
112
|
+
... phasor_cluster_gmm(real, imag, clusters=2)
|
113
|
+
... )
|
114
|
+
>>> center_real # doctest: +SKIP
|
115
|
+
(0.2, 0.4)
|
116
|
+
|
117
|
+
"""
|
118
|
+
from sklearn.mixture import GaussianMixture
|
119
|
+
|
120
|
+
coords = numpy.stack([real, imag], axis=-1).reshape(-1, 2)
|
121
|
+
|
122
|
+
valid_data = ~numpy.isnan(coords).any(axis=1)
|
123
|
+
coords = coords[valid_data]
|
124
|
+
|
125
|
+
kwargs.pop('n_components', None)
|
126
|
+
|
127
|
+
gmm = GaussianMixture(n_components=clusters, **kwargs)
|
128
|
+
gmm.fit(coords)
|
129
|
+
|
130
|
+
center_real = []
|
131
|
+
center_imag = []
|
132
|
+
radius_major = []
|
133
|
+
radius_minor = []
|
134
|
+
angle = []
|
135
|
+
|
136
|
+
for i in range(clusters):
|
137
|
+
center_real.append(float(gmm.means_[i, 0]))
|
138
|
+
center_imag.append(float(gmm.means_[i, 1]))
|
139
|
+
|
140
|
+
if gmm.covariance_type == 'full':
|
141
|
+
cov = gmm.covariances_[i]
|
142
|
+
elif gmm.covariance_type == 'tied':
|
143
|
+
cov = gmm.covariances_
|
144
|
+
elif gmm.covariance_type == 'diag':
|
145
|
+
cov = numpy.diag(gmm.covariances_[i])
|
146
|
+
else: # 'spherical'
|
147
|
+
cov = numpy.eye(2) * gmm.covariances_[i]
|
148
|
+
|
149
|
+
eigenvalues, eigenvectors = numpy.linalg.eigh(cov[:2, :2])
|
150
|
+
|
151
|
+
idx = eigenvalues.argsort()[::-1]
|
152
|
+
eigenvalues = eigenvalues[idx]
|
153
|
+
eigenvectors = eigenvectors[:, idx]
|
154
|
+
|
155
|
+
major_vector = eigenvectors[:, 0]
|
156
|
+
current_angle = math.atan2(major_vector[1], major_vector[0])
|
157
|
+
|
158
|
+
if current_angle < 0:
|
159
|
+
current_angle += math.pi
|
160
|
+
|
161
|
+
angle.append(float(current_angle))
|
162
|
+
|
163
|
+
radius_major.append(sigma * math.sqrt(2 * eigenvalues[0]))
|
164
|
+
radius_minor.append(sigma * math.sqrt(2 * eigenvalues[1]))
|
165
|
+
|
166
|
+
if clusters == 1:
|
167
|
+
argsort = [0]
|
168
|
+
else:
|
169
|
+
if sort is None or sort == 'polar':
|
170
|
+
|
171
|
+
def sort_key(i: int) -> Any:
|
172
|
+
return (
|
173
|
+
math.atan2(center_imag[i], center_real[i]),
|
174
|
+
math.hypot(center_real[i], center_imag[i]),
|
175
|
+
)
|
176
|
+
|
177
|
+
elif sort == 'phasor':
|
178
|
+
|
179
|
+
def sort_key(i: int) -> Any:
|
180
|
+
return center_imag[i], center_real[i]
|
181
|
+
|
182
|
+
elif sort == 'area':
|
183
|
+
|
184
|
+
def sort_key(i: int) -> Any:
|
185
|
+
return -radius_major[i] * radius_minor[i]
|
186
|
+
|
187
|
+
else:
|
188
|
+
raise ValueError(
|
189
|
+
f"invalid {sort=!r} != 'phasor', 'polar', or 'area'"
|
190
|
+
)
|
191
|
+
|
192
|
+
argsort = sorted(range(len(center_real)), key=sort_key)
|
193
|
+
|
194
|
+
return (
|
195
|
+
tuple(center_real[i] for i in argsort),
|
196
|
+
tuple(center_imag[i] for i in argsort),
|
197
|
+
tuple(radius_major[i] for i in argsort),
|
198
|
+
tuple(radius_minor[i] for i in argsort),
|
199
|
+
tuple(angle[i] for i in argsort),
|
200
|
+
)
|