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/__init__.py +2 -3
- phasorpy/_phasorpy.cp312-win_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +185 -2
- phasorpy/_utils.py +121 -9
- phasorpy/cli.py +56 -3
- phasorpy/cluster.py +42 -6
- phasorpy/components.py +226 -55
- phasorpy/experimental.py +312 -0
- phasorpy/io/__init__.py +137 -0
- phasorpy/io/_flimlabs.py +350 -0
- phasorpy/io/_leica.py +329 -0
- phasorpy/io/_ometiff.py +445 -0
- phasorpy/io/_other.py +782 -0
- phasorpy/io/_simfcs.py +627 -0
- phasorpy/phasor.py +307 -1
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +717 -0
- phasorpy/plot/_lifetime_plots.py +553 -0
- phasorpy/plot/_phasorplot.py +1119 -0
- phasorpy/plot/_phasorplot_fret.py +559 -0
- phasorpy/utils.py +84 -296
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/METADATA +2 -2
- phasorpy-0.6.dist-info/RECORD +34 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/WHEEL +1 -1
- phasorpy/_io.py +0 -2655
- phasorpy/io.py +0 -9
- phasorpy/plot.py +0 -2318
- phasorpy/version.py +0 -80
- phasorpy-0.5.dist-info/RECORD +0 -26
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/top_level.txt +0 -0
phasorpy/components.py
CHANGED
@@ -3,31 +3,29 @@
|
|
3
3
|
The ``phasorpy.components`` module provides functions to:
|
4
4
|
|
5
5
|
- calculate fractions of two known components by projecting onto the
|
6
|
-
line between the components:
|
7
|
-
|
8
|
-
- :py:func:`two_fractions_from_phasor`
|
6
|
+
line between the components (:py:func:`phasor_component_fraction`)
|
9
7
|
|
10
8
|
- calculate phasor coordinates of second component if only one is
|
11
9
|
known (not implemented)
|
12
10
|
|
13
|
-
- calculate fractions of
|
14
|
-
harmonic information (
|
11
|
+
- calculate fractions of multiple known components by using higher
|
12
|
+
harmonic information (:py:func:`phasor_component_fit`)
|
15
13
|
|
16
14
|
- calculate fractions of two or three known components by resolving
|
17
|
-
graphically with histogram:
|
18
|
-
|
19
|
-
- :py:func:`graphical_component_analysis`
|
15
|
+
graphically with histogram (:py:func:`phasor_component_graphical`)
|
20
16
|
|
21
|
-
- blindly resolve fractions of
|
22
|
-
information (not implemented)
|
17
|
+
- blindly resolve fractions of multiple components by using harmonic
|
18
|
+
information (:py:func:`phasor_component_blind`, not implemented)
|
23
19
|
|
24
20
|
"""
|
25
21
|
|
26
22
|
from __future__ import annotations
|
27
23
|
|
28
24
|
__all__ = [
|
29
|
-
|
30
|
-
'
|
25
|
+
# phasor_component_blind,
|
26
|
+
'phasor_component_fit',
|
27
|
+
'phasor_component_fraction',
|
28
|
+
'phasor_component_graphical',
|
31
29
|
]
|
32
30
|
|
33
31
|
import numbers
|
@@ -39,18 +37,20 @@ if TYPE_CHECKING:
|
|
39
37
|
import numpy
|
40
38
|
|
41
39
|
from ._phasorpy import (
|
40
|
+
_blend_and,
|
42
41
|
_fraction_on_segment,
|
43
42
|
_is_inside_circle,
|
44
43
|
_is_inside_stadium,
|
45
44
|
_segment_direction_and_length,
|
46
45
|
)
|
46
|
+
from .phasor import phasor_threshold
|
47
47
|
|
48
48
|
|
49
|
-
def
|
49
|
+
def phasor_component_fraction(
|
50
50
|
real: ArrayLike,
|
51
51
|
imag: ArrayLike,
|
52
|
-
|
53
|
-
|
52
|
+
component_real: ArrayLike,
|
53
|
+
component_imag: ArrayLike,
|
54
54
|
/,
|
55
55
|
) -> NDArray[Any]:
|
56
56
|
"""Return fraction of first of two components from phasor coordinates.
|
@@ -65,9 +65,9 @@ def two_fractions_from_phasor(
|
|
65
65
|
Real component of phasor coordinates.
|
66
66
|
imag : array_like
|
67
67
|
Imaginary component of phasor coordinates.
|
68
|
-
|
68
|
+
component_real : array_like, shape (2,)
|
69
69
|
Real coordinates of first and second components.
|
70
|
-
|
70
|
+
component_imag : array_like, shape (2,)
|
71
71
|
Imaginary coordinates of first and second components.
|
72
72
|
|
73
73
|
Returns
|
@@ -95,39 +95,39 @@ def two_fractions_from_phasor(
|
|
95
95
|
|
96
96
|
Examples
|
97
97
|
--------
|
98
|
-
>>>
|
98
|
+
>>> phasor_component_fraction(
|
99
99
|
... [0.6, 0.5, 0.4], [0.4, 0.3, 0.2], [0.2, 0.9], [0.4, 0.3]
|
100
100
|
... ) # doctest: +NUMBER
|
101
101
|
array([0.44, 0.56, 0.68])
|
102
102
|
|
103
103
|
"""
|
104
|
-
|
105
|
-
|
106
|
-
if
|
107
|
-
raise ValueError(f'{
|
108
|
-
if
|
109
|
-
raise ValueError(f'{
|
104
|
+
component_real = numpy.asarray(component_real)
|
105
|
+
component_imag = numpy.asarray(component_imag)
|
106
|
+
if component_real.shape != (2,):
|
107
|
+
raise ValueError(f'{component_real.shape=} != (2,)')
|
108
|
+
if component_imag.shape != (2,):
|
109
|
+
raise ValueError(f'{component_imag.shape=} != (2,)')
|
110
110
|
if (
|
111
|
-
|
112
|
-
and
|
111
|
+
component_real[0] == component_real[1]
|
112
|
+
and component_imag[0] == component_imag[1]
|
113
113
|
):
|
114
114
|
raise ValueError('components must have different coordinates')
|
115
115
|
|
116
116
|
return _fraction_on_segment( # type: ignore[no-any-return]
|
117
117
|
real,
|
118
118
|
imag,
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
119
|
+
component_real[0],
|
120
|
+
component_imag[0],
|
121
|
+
component_real[1],
|
122
|
+
component_imag[1],
|
123
123
|
)
|
124
124
|
|
125
125
|
|
126
|
-
def
|
126
|
+
def phasor_component_graphical(
|
127
127
|
real: ArrayLike,
|
128
128
|
imag: ArrayLike,
|
129
|
-
|
130
|
-
|
129
|
+
component_real: ArrayLike,
|
130
|
+
component_imag: ArrayLike,
|
131
131
|
/,
|
132
132
|
*,
|
133
133
|
radius: float = 0.05,
|
@@ -145,9 +145,9 @@ def graphical_component_analysis(
|
|
145
145
|
Real component of phasor coordinates.
|
146
146
|
imag : array_like
|
147
147
|
Imaginary component of phasor coordinates.
|
148
|
-
|
148
|
+
component_real : array_like, shape (2,) or (3,)
|
149
149
|
Real coordinates for two or three components.
|
150
|
-
|
150
|
+
component_imag : array_like, shape (2,) or (3,)
|
151
151
|
Imaginary coordinates for two or three components.
|
152
152
|
radius : float, optional, default: 0.05
|
153
153
|
Radius of cursor.
|
@@ -169,8 +169,8 @@ def graphical_component_analysis(
|
|
169
169
|
Raises
|
170
170
|
------
|
171
171
|
ValueError
|
172
|
-
The array shapes of `real` and `imag`, or `
|
173
|
-
`
|
172
|
+
The array shapes of `real` and `imag`, or `component_real` and
|
173
|
+
`component_imag` do not match.
|
174
174
|
The number of components is not 2 or 3.
|
175
175
|
Fraction values are out of range [0.0, 1.0].
|
176
176
|
|
@@ -213,14 +213,14 @@ def graphical_component_analysis(
|
|
213
213
|
--------
|
214
214
|
Count the number of phasors between two components:
|
215
215
|
|
216
|
-
>>>
|
216
|
+
>>> phasor_component_graphical(
|
217
217
|
... [0.6, 0.3], [0.35, 0.38], [0.2, 0.9], [0.4, 0.3], fractions=6
|
218
218
|
... ) # doctest: +NUMBER
|
219
219
|
(array([0, 0, 1, 0, 1, 0], dtype=uint64),)
|
220
220
|
|
221
221
|
Count the number of phasors between the combinations of three components:
|
222
222
|
|
223
|
-
>>>
|
223
|
+
>>> phasor_component_graphical(
|
224
224
|
... [0.4, 0.5],
|
225
225
|
... [0.2, 0.3],
|
226
226
|
... [0.0, 0.2, 0.9],
|
@@ -234,30 +234,30 @@ def graphical_component_analysis(
|
|
234
234
|
"""
|
235
235
|
real = numpy.asarray(real)
|
236
236
|
imag = numpy.asarray(imag)
|
237
|
-
|
238
|
-
|
237
|
+
component_real = numpy.asarray(component_real)
|
238
|
+
component_imag = numpy.asarray(component_imag)
|
239
239
|
if (
|
240
240
|
real.shape != imag.shape
|
241
|
-
or
|
241
|
+
or component_real.shape != component_imag.shape
|
242
242
|
):
|
243
243
|
raise ValueError('input array shapes must match')
|
244
|
-
if
|
244
|
+
if component_real.ndim != 1:
|
245
245
|
raise ValueError(
|
246
246
|
'component arrays are not one-dimensional: '
|
247
|
-
f'{
|
247
|
+
f'{component_real.ndim} dimensions found'
|
248
248
|
)
|
249
|
-
num_components = len(
|
249
|
+
num_components = len(component_real)
|
250
250
|
if num_components not in {2, 3}:
|
251
251
|
raise ValueError('number of components must be 2 or 3')
|
252
252
|
|
253
253
|
if fractions is None:
|
254
254
|
longest_distance = 0
|
255
255
|
for i in range(num_components):
|
256
|
-
a_real =
|
257
|
-
a_imag =
|
256
|
+
a_real = component_real[i]
|
257
|
+
a_imag = component_imag[i]
|
258
258
|
for j in range(i + 1, num_components):
|
259
|
-
b_real =
|
260
|
-
b_imag =
|
259
|
+
b_real = component_real[j]
|
260
|
+
b_imag = component_imag[j]
|
261
261
|
_, _, length = _segment_direction_and_length(
|
262
262
|
a_real, a_imag, b_real, b_imag
|
263
263
|
)
|
@@ -274,11 +274,11 @@ def graphical_component_analysis(
|
|
274
274
|
|
275
275
|
counts = []
|
276
276
|
for i in range(num_components):
|
277
|
-
a_real =
|
278
|
-
a_imag =
|
277
|
+
a_real = component_real[i]
|
278
|
+
a_imag = component_imag[i]
|
279
279
|
for j in range(i + 1, num_components):
|
280
|
-
b_real =
|
281
|
-
b_imag =
|
280
|
+
b_real = component_real[j]
|
281
|
+
b_imag = component_imag[j]
|
282
282
|
ab_real = a_real - b_real
|
283
283
|
ab_imag = a_imag - b_imag
|
284
284
|
|
@@ -301,8 +301,8 @@ def graphical_component_analysis(
|
|
301
301
|
imag,
|
302
302
|
b_real + f * ab_real, # cursor_real
|
303
303
|
b_imag + f * ab_imag, # cursor_imag
|
304
|
-
|
305
|
-
|
304
|
+
component_real[3 - i - j], # c_real
|
305
|
+
component_imag[3 - i - j], # c_imag
|
306
306
|
radius,
|
307
307
|
)
|
308
308
|
fraction_counts = numpy.sum(mask, dtype=numpy.uint64)
|
@@ -311,3 +311,174 @@ def graphical_component_analysis(
|
|
311
311
|
counts.append(numpy.asarray(component_counts))
|
312
312
|
|
313
313
|
return tuple(counts)
|
314
|
+
|
315
|
+
|
316
|
+
def phasor_component_fit(
|
317
|
+
mean: ArrayLike,
|
318
|
+
real: ArrayLike,
|
319
|
+
imag: ArrayLike,
|
320
|
+
component_real: ArrayLike,
|
321
|
+
component_imag: ArrayLike,
|
322
|
+
/,
|
323
|
+
**kwargs: Any,
|
324
|
+
) -> tuple[NDArray[Any], ...]:
|
325
|
+
"""Return fractions of multiple components from phasor coordinates.
|
326
|
+
|
327
|
+
Component fractions are obtained from the least-squares solution of a
|
328
|
+
linear matrix equation that relates phasor coordinates from one or
|
329
|
+
multiple harmonics to component fractions according to [2]_.
|
330
|
+
|
331
|
+
Up to ``2 * number harmonics + 1`` components can be fit to multi-harmonic
|
332
|
+
phasor coordinates, that is up to three components for single harmonic
|
333
|
+
phasor coordinates.
|
334
|
+
|
335
|
+
Parameters
|
336
|
+
----------
|
337
|
+
mean : array_like
|
338
|
+
Intensity of phasor coordinates.
|
339
|
+
real : array_like
|
340
|
+
Real component of phasor coordinates.
|
341
|
+
Harmonics, if any, must be in the first dimension.
|
342
|
+
imag : array_like
|
343
|
+
Imaginary component of phasor coordinates.
|
344
|
+
Harmonics, if any, must be in the first dimension.
|
345
|
+
component_real : array_like
|
346
|
+
Real coordinates of components.
|
347
|
+
Must be one or two-dimensional with harmonics in the first dimension.
|
348
|
+
component_imag : array_like
|
349
|
+
Imaginary coordinates of components.
|
350
|
+
Must be one or two-dimensional with harmonics in the first dimension.
|
351
|
+
**kwargs : optional
|
352
|
+
Additional arguments passed to :py:func:`scipy.linalg.lstsq()`.
|
353
|
+
|
354
|
+
Returns
|
355
|
+
-------
|
356
|
+
fractions : tuple of ndarray
|
357
|
+
Component fractions, one array per component.
|
358
|
+
Fractions may not exactly add up to 1.0.
|
359
|
+
|
360
|
+
Raises
|
361
|
+
------
|
362
|
+
ValueError
|
363
|
+
The array shapes of `real` and `imag` do not match.
|
364
|
+
The array shapes of `component_real` and `component_imag` do not match.
|
365
|
+
The number of harmonics in the components does not
|
366
|
+
match the ones in the phasor coordinates.
|
367
|
+
The system is underdetermined; the component matrix having more
|
368
|
+
columns than rows.
|
369
|
+
|
370
|
+
See Also
|
371
|
+
--------
|
372
|
+
:ref:`sphx_glr_tutorials_api_phasorpy_components.py`
|
373
|
+
:ref:`sphx_glr_tutorials_applications_phasorpy_component_fit.py`
|
374
|
+
|
375
|
+
Notes
|
376
|
+
-----
|
377
|
+
For now, calculation of fractions of components from different channels
|
378
|
+
or frequencies is not supported. Only one set of components can be
|
379
|
+
analyzed and is broadcast to all channels/frequencies.
|
380
|
+
|
381
|
+
The method builds a linear matrix equation,
|
382
|
+
:math:`A\\mathbf{x} = \\mathbf{b}`, where :math:`A` consists of the
|
383
|
+
phasor coordinates of individual components, :math:`\\mathbf{x}` are
|
384
|
+
the unknown fractions, and :math:`\\mathbf{b}` represents the measured
|
385
|
+
phasor coordinates in the mixture. The least-squares solution of this
|
386
|
+
linear matrix equation yields the fractions.
|
387
|
+
|
388
|
+
References
|
389
|
+
----------
|
390
|
+
.. [2] Vallmitjana A, Lepanto P, Irigoin F, and Malacrida L.
|
391
|
+
`Phasor-based multi-harmonic unmixing for in-vivo hyperspectral
|
392
|
+
imaging <https://doi.org/10.1088/2050-6120/ac9ae9>`_.
|
393
|
+
*Methods Appl Fluoresc*, 11(1): 014001 (2022)
|
394
|
+
|
395
|
+
Example
|
396
|
+
-------
|
397
|
+
>>> phasor_component_fit(
|
398
|
+
... [1, 1, 1], [0.6, 0.5, 0.4], [0.4, 0.3, 0.2], [0.2, 0.9], [0.4, 0.3]
|
399
|
+
... ) # doctest: +NUMBER
|
400
|
+
(array([0.4644, 0.5356, 0.6068]), array([0.5559, 0.4441, 0.3322]))
|
401
|
+
|
402
|
+
"""
|
403
|
+
from scipy.linalg import lstsq
|
404
|
+
|
405
|
+
mean = numpy.atleast_1d(mean)
|
406
|
+
real = numpy.atleast_1d(real)
|
407
|
+
imag = numpy.atleast_1d(imag)
|
408
|
+
component_real = numpy.atleast_1d(component_real)
|
409
|
+
component_imag = numpy.atleast_1d(component_imag)
|
410
|
+
|
411
|
+
if real.shape != imag.shape:
|
412
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
413
|
+
if mean.shape != real.shape[-mean.ndim :]:
|
414
|
+
raise ValueError(f'{mean.shape=} does not match {real.shape=}')
|
415
|
+
|
416
|
+
if component_real.shape != component_imag.shape:
|
417
|
+
raise ValueError(f'{component_real.shape=} != {component_imag.shape=}')
|
418
|
+
if numpy.isnan(component_real).any() or numpy.isnan(component_imag).any():
|
419
|
+
raise ValueError(
|
420
|
+
'component phasor coordinates must not contain NaN values'
|
421
|
+
)
|
422
|
+
if numpy.isinf(component_real).any() or numpy.isinf(component_imag).any():
|
423
|
+
raise ValueError(
|
424
|
+
'component phasor coordinates must not contain infinite values'
|
425
|
+
)
|
426
|
+
|
427
|
+
if component_real.ndim == 1:
|
428
|
+
component_real = component_real.reshape(1, -1)
|
429
|
+
component_imag = component_imag.reshape(1, -1)
|
430
|
+
elif component_real.ndim > 2:
|
431
|
+
raise ValueError(f'{component_real.ndim=} > 2')
|
432
|
+
|
433
|
+
num_harmonics, num_components = component_real.shape
|
434
|
+
|
435
|
+
# create component matrix for least squares solving:
|
436
|
+
# [real coordinates of components (for each harmonic)] +
|
437
|
+
# [imaginary coordinates of components (for each harmonic)] +
|
438
|
+
# [ones for intensity constraint]
|
439
|
+
component_matrix = numpy.ones((2 * num_harmonics + 1, num_components))
|
440
|
+
component_matrix[:num_harmonics] = component_real
|
441
|
+
component_matrix[num_harmonics : 2 * num_harmonics] = component_imag
|
442
|
+
|
443
|
+
if component_matrix.shape[0] < component_matrix.shape[1]:
|
444
|
+
raise ValueError(
|
445
|
+
'the system is undetermined '
|
446
|
+
f'({num_components=} > {num_harmonics * 2 + 1=})'
|
447
|
+
)
|
448
|
+
|
449
|
+
has_harmonic_axis = mean.ndim + 1 == real.ndim
|
450
|
+
if not has_harmonic_axis:
|
451
|
+
real = numpy.expand_dims(real, axis=0)
|
452
|
+
imag = numpy.expand_dims(imag, axis=0)
|
453
|
+
elif real.shape[0] != num_harmonics:
|
454
|
+
raise ValueError(f'{real.shape[0]=} != {component_real.shape[0]=}')
|
455
|
+
|
456
|
+
# TODO: replace Inf with NaN values?
|
457
|
+
mean, real, imag = phasor_threshold(mean, real, imag)
|
458
|
+
|
459
|
+
# replace NaN values with 0.0 for least squares solving
|
460
|
+
real = numpy.nan_to_num(real, nan=0.0, copy=False)
|
461
|
+
imag = numpy.nan_to_num(imag, nan=0.0, copy=False)
|
462
|
+
|
463
|
+
# create coordinates matrix for least squares solving:
|
464
|
+
# [real coordinates (for each harmonic)] +
|
465
|
+
# [imaginary coordinates (for each harmonic)] +
|
466
|
+
# [ones for intensity constraint]
|
467
|
+
coords = numpy.ones((2 * num_harmonics + 1,) + real.shape[1:])
|
468
|
+
coords[:num_harmonics] = real
|
469
|
+
coords[num_harmonics : 2 * num_harmonics] = imag
|
470
|
+
|
471
|
+
fractions = lstsq(
|
472
|
+
component_matrix, coords.reshape(coords.shape[0], -1), **kwargs
|
473
|
+
)[0]
|
474
|
+
|
475
|
+
# reshape to match input dimensions
|
476
|
+
fractions = fractions.reshape((num_components,) + coords.shape[1:])
|
477
|
+
|
478
|
+
# TODO: normalize fractions to sum up to 1.0?
|
479
|
+
# fractions /= numpy.sum(fractions, axis=0, keepdims=True)
|
480
|
+
|
481
|
+
# restore NaN values in fractions from mean
|
482
|
+
_blend_and(mean, fractions, out=fractions)
|
483
|
+
|
484
|
+
return tuple(fractions)
|