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/component.py
ADDED
@@ -0,0 +1,707 @@
|
|
1
|
+
"""Component analysis of phasor coordinates.
|
2
|
+
|
3
|
+
The ``phasorpy.component`` module provides functions to:
|
4
|
+
|
5
|
+
- calculate fractions of two known components by projecting onto the
|
6
|
+
line between the components (:py:func:`phasor_component_fraction`)
|
7
|
+
|
8
|
+
- calculate phasor coordinates of second component if only one is
|
9
|
+
known (not implemented)
|
10
|
+
|
11
|
+
- calculate fractions of multiple known components by using higher
|
12
|
+
harmonic information (:py:func:`phasor_component_fit`)
|
13
|
+
|
14
|
+
- calculate fractions of two or three known components by resolving
|
15
|
+
graphically with histogram (:py:func:`phasor_component_graphical`)
|
16
|
+
|
17
|
+
- calculate mean value coordinates of phasor coordinates with respect
|
18
|
+
to three or more components (:py:func:`phasor_component_mvc`)
|
19
|
+
|
20
|
+
- blindly resolve fractions of multiple components by using harmonic
|
21
|
+
information (:py:func:`phasor_component_blind`, not implemented)
|
22
|
+
|
23
|
+
- calculate phasor coordinates from fractional intensities of
|
24
|
+
components (:py:func:`phasor_from_component`)
|
25
|
+
|
26
|
+
"""
|
27
|
+
|
28
|
+
from __future__ import annotations
|
29
|
+
|
30
|
+
__all__ = [
|
31
|
+
# phasor_component_blind,
|
32
|
+
'phasor_component_fit',
|
33
|
+
'phasor_component_fraction',
|
34
|
+
'phasor_component_graphical',
|
35
|
+
'phasor_component_mvc',
|
36
|
+
'phasor_from_component',
|
37
|
+
]
|
38
|
+
|
39
|
+
import numbers
|
40
|
+
from typing import TYPE_CHECKING
|
41
|
+
|
42
|
+
if TYPE_CHECKING:
|
43
|
+
from ._typing import Any, ArrayLike, DTypeLike, NDArray
|
44
|
+
|
45
|
+
import numpy
|
46
|
+
|
47
|
+
from ._phasorpy import (
|
48
|
+
_blend_and,
|
49
|
+
_fraction_on_segment,
|
50
|
+
_is_inside_circle,
|
51
|
+
_is_inside_stadium,
|
52
|
+
_mean_value_coordinates,
|
53
|
+
_segment_direction_and_length,
|
54
|
+
)
|
55
|
+
from ._utils import sort_coordinates
|
56
|
+
from .phasor import phasor_threshold
|
57
|
+
from .utils import number_threads
|
58
|
+
|
59
|
+
|
60
|
+
def phasor_from_component(
|
61
|
+
component_real: ArrayLike,
|
62
|
+
component_imag: ArrayLike,
|
63
|
+
fraction: ArrayLike,
|
64
|
+
/,
|
65
|
+
axis: int = 0,
|
66
|
+
dtype: DTypeLike | None = None,
|
67
|
+
) -> tuple[NDArray[Any], NDArray[Any]]:
|
68
|
+
"""Return phasor coordinates from fractional intensities of components.
|
69
|
+
|
70
|
+
Return the dot products of the fractional intensities of components
|
71
|
+
with the real and imaginary phasor coordinates of the components.
|
72
|
+
|
73
|
+
Multi-dimensional component arrays are currently not supported.
|
74
|
+
|
75
|
+
Parameters
|
76
|
+
----------
|
77
|
+
component_real : array_like, shape (n,)
|
78
|
+
Real coordinates of components.
|
79
|
+
At least two components are required.
|
80
|
+
component_imag : array_like, shape (n,)
|
81
|
+
Imaginary coordinates of components.
|
82
|
+
fraction : array_like
|
83
|
+
Fractional intensities of components.
|
84
|
+
Fractions are normalized to sum to one along `axis`.
|
85
|
+
axis : int, optional, default: 0
|
86
|
+
Axis of components in `fraction`.
|
87
|
+
dtype : dtype_like, optional
|
88
|
+
Floating point data type used for calculation and output values.
|
89
|
+
Either `float32` or `float64`. The default is `float64`.
|
90
|
+
|
91
|
+
Returns
|
92
|
+
-------
|
93
|
+
real : ndarray
|
94
|
+
Real component of phasor coordinates.
|
95
|
+
imag : ndarray
|
96
|
+
Imaginary component of phasor coordinates.
|
97
|
+
|
98
|
+
Examples
|
99
|
+
--------
|
100
|
+
Calculate phasor coordinates from two components and their fractional
|
101
|
+
intensities:
|
102
|
+
|
103
|
+
>>> phasor_from_component(
|
104
|
+
... [0.6, 0.4], [0.3, 0.2], [[1.0, 0.2, 0.9], [0.0, 0.8, 0.1]]
|
105
|
+
... )
|
106
|
+
(array([0.6, 0.44, 0.58]), array([0.3, 0.22, 0.29]))
|
107
|
+
|
108
|
+
"""
|
109
|
+
dtype = numpy.dtype(dtype)
|
110
|
+
if dtype.char not in {'f', 'd'}:
|
111
|
+
raise ValueError(f'{dtype=} is not a floating point type')
|
112
|
+
|
113
|
+
fraction = numpy.array(fraction, dtype=dtype, copy=True)
|
114
|
+
if fraction.ndim < 1:
|
115
|
+
raise ValueError(f'{fraction.ndim=} < 1')
|
116
|
+
if fraction.shape[axis] < 2:
|
117
|
+
raise ValueError(f'{fraction.shape[axis]=} < 2')
|
118
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
119
|
+
fraction /= fraction.sum(axis=axis, keepdims=True)
|
120
|
+
|
121
|
+
component_real = numpy.asarray(component_real, dtype=dtype)
|
122
|
+
component_imag = numpy.asarray(component_imag, dtype=dtype)
|
123
|
+
if component_real.shape != component_imag.shape:
|
124
|
+
raise ValueError(f'{component_real.shape=} != {component_imag.shape=}')
|
125
|
+
if component_real.ndim != 1:
|
126
|
+
raise ValueError(f'{component_real.ndim=} != 1')
|
127
|
+
if component_real.size != fraction.shape[axis]:
|
128
|
+
raise ValueError(f'{component_real.size=} != {fraction.shape[axis]=}')
|
129
|
+
|
130
|
+
fraction = numpy.moveaxis(fraction, axis, -1)
|
131
|
+
real = numpy.dot(fraction, component_real)
|
132
|
+
imag = numpy.dot(fraction, component_imag)
|
133
|
+
return real, imag
|
134
|
+
|
135
|
+
|
136
|
+
def phasor_component_fraction(
|
137
|
+
real: ArrayLike,
|
138
|
+
imag: ArrayLike,
|
139
|
+
component_real: ArrayLike,
|
140
|
+
component_imag: ArrayLike,
|
141
|
+
/,
|
142
|
+
) -> NDArray[Any]:
|
143
|
+
"""Return fraction of first of two components from phasor coordinates.
|
144
|
+
|
145
|
+
Return the relative distance (normalized by the distance between the two
|
146
|
+
components) to the second component for each phasor coordinate projected
|
147
|
+
onto the line between two components.
|
148
|
+
|
149
|
+
Parameters
|
150
|
+
----------
|
151
|
+
real : array_like
|
152
|
+
Real component of phasor coordinates.
|
153
|
+
imag : array_like
|
154
|
+
Imaginary component of phasor coordinates.
|
155
|
+
component_real : array_like, shape (2,)
|
156
|
+
Real coordinates of first and second components.
|
157
|
+
component_imag : array_like, shape (2,)
|
158
|
+
Imaginary coordinates of first and second components.
|
159
|
+
|
160
|
+
Returns
|
161
|
+
-------
|
162
|
+
fraction : ndarray
|
163
|
+
Fractions of first component.
|
164
|
+
|
165
|
+
Raises
|
166
|
+
------
|
167
|
+
ValueError
|
168
|
+
If the real or imaginary coordinates of the known components are
|
169
|
+
not of size 2.
|
170
|
+
|
171
|
+
See Also
|
172
|
+
--------
|
173
|
+
:ref:`sphx_glr_tutorials_api_phasorpy_component.py`
|
174
|
+
|
175
|
+
Notes
|
176
|
+
-----
|
177
|
+
The fraction of the second component is ``1.0 - fraction``.
|
178
|
+
|
179
|
+
For now, calculation of fraction of components from different
|
180
|
+
channels or frequencies is not supported. Only one pair of components can
|
181
|
+
be analyzed and will be broadcast to all channels/frequencies.
|
182
|
+
|
183
|
+
Examples
|
184
|
+
--------
|
185
|
+
>>> phasor_component_fraction(
|
186
|
+
... [0.6, 0.5, 0.4], [0.4, 0.3, 0.2], [0.2, 0.9], [0.4, 0.3]
|
187
|
+
... )
|
188
|
+
array([0.44, 0.56, 0.68])
|
189
|
+
|
190
|
+
"""
|
191
|
+
component_real = numpy.asarray(component_real)
|
192
|
+
component_imag = numpy.asarray(component_imag)
|
193
|
+
if component_real.shape != (2,):
|
194
|
+
raise ValueError(f'{component_real.shape=} != (2,)')
|
195
|
+
if component_imag.shape != (2,):
|
196
|
+
raise ValueError(f'{component_imag.shape=} != (2,)')
|
197
|
+
if (
|
198
|
+
component_real[0] == component_real[1]
|
199
|
+
and component_imag[0] == component_imag[1]
|
200
|
+
):
|
201
|
+
raise ValueError('components must have different coordinates')
|
202
|
+
|
203
|
+
return _fraction_on_segment( # type: ignore[no-any-return]
|
204
|
+
real,
|
205
|
+
imag,
|
206
|
+
component_real[0],
|
207
|
+
component_imag[0],
|
208
|
+
component_real[1],
|
209
|
+
component_imag[1],
|
210
|
+
)
|
211
|
+
|
212
|
+
|
213
|
+
def phasor_component_graphical(
|
214
|
+
real: ArrayLike,
|
215
|
+
imag: ArrayLike,
|
216
|
+
component_real: ArrayLike,
|
217
|
+
component_imag: ArrayLike,
|
218
|
+
/,
|
219
|
+
*,
|
220
|
+
radius: float = 0.05,
|
221
|
+
fractions: ArrayLike | None = None,
|
222
|
+
) -> NDArray[Any]:
|
223
|
+
r"""Return fractions of two or three components from phasor coordinates.
|
224
|
+
|
225
|
+
The graphical method is based on moving circular cursors along the line
|
226
|
+
between pairs of components and quantifying the phasors for each
|
227
|
+
fraction.
|
228
|
+
|
229
|
+
Parameters
|
230
|
+
----------
|
231
|
+
real : array_like
|
232
|
+
Real component of phasor coordinates.
|
233
|
+
imag : array_like
|
234
|
+
Imaginary component of phasor coordinates.
|
235
|
+
component_real : array_like, shape (2,) or (3,)
|
236
|
+
Real coordinates for two or three components.
|
237
|
+
component_imag : array_like, shape (2,) or (3,)
|
238
|
+
Imaginary coordinates for two or three components.
|
239
|
+
radius : float, optional, default: 0.05
|
240
|
+
Radius of cursor.
|
241
|
+
fractions : array_like or int, optional
|
242
|
+
Number of equidistant fractions, or 1D array of fraction values.
|
243
|
+
Fraction values must be in range [0.0, 1.0].
|
244
|
+
If an integer, ``numpy.linspace(0.0, 1.0, fractions)`` fraction values
|
245
|
+
are used.
|
246
|
+
If None (default), the number of fractions is determined from the
|
247
|
+
longest distance between any pair of components and the radius of
|
248
|
+
the cursor (see Notes below).
|
249
|
+
|
250
|
+
Returns
|
251
|
+
-------
|
252
|
+
counts : ndarray
|
253
|
+
Counts along each line segment connecting components.
|
254
|
+
Ordered 0-1 (2 components) or 0-1, 0-2, 1-2 (3 components).
|
255
|
+
Shaped `(number fractions,)` (2 components) or
|
256
|
+
`(3, number fractions)` (3 components).
|
257
|
+
|
258
|
+
Raises
|
259
|
+
------
|
260
|
+
ValueError
|
261
|
+
The array shapes of `real` and `imag`, or `component_real` and
|
262
|
+
`component_imag` do not match.
|
263
|
+
The number of components is not 2 or 3.
|
264
|
+
Fraction values are out of range [0.0, 1.0].
|
265
|
+
|
266
|
+
See Also
|
267
|
+
--------
|
268
|
+
:ref:`sphx_glr_tutorials_api_phasorpy_component.py`
|
269
|
+
|
270
|
+
Notes
|
271
|
+
-----
|
272
|
+
For now, calculation of fraction of components from different
|
273
|
+
channels or frequencies is not supported. Only one set of components can
|
274
|
+
be analyzed and will be broadcast to all channels/frequencies.
|
275
|
+
|
276
|
+
The graphical method was first introduced in [1]_.
|
277
|
+
|
278
|
+
If no `fractions` are provided, the number of fractions (:math:`N`) used
|
279
|
+
is determined from the longest distance between any pair of components
|
280
|
+
(:math:`D`) and the radius of the cursor (:math:`R`):
|
281
|
+
|
282
|
+
.. math::
|
283
|
+
|
284
|
+
N = \frac{2 \cdot D}{R} + 1
|
285
|
+
|
286
|
+
The fractions can be retrieved by:
|
287
|
+
|
288
|
+
.. code-block:: python
|
289
|
+
|
290
|
+
fractions = numpy.linspace(0.0, 1.0, len(counts[0]))
|
291
|
+
|
292
|
+
References
|
293
|
+
----------
|
294
|
+
.. [1] Ranjit S, Datta R, Dvornikov A, and Gratton E.
|
295
|
+
`Multicomponent analysis of phasor plot in a single pixel to
|
296
|
+
calculate changes of metabolic trajectory in biological systems
|
297
|
+
<https://doi.org/10.1021/acs.jpca.9b07880>`_.
|
298
|
+
*J Phys Chem A*, 123(45): 9865-9873 (2019)
|
299
|
+
|
300
|
+
Examples
|
301
|
+
--------
|
302
|
+
Count the number of phasors between two components:
|
303
|
+
|
304
|
+
>>> phasor_component_graphical(
|
305
|
+
... [0.6, 0.3], [0.35, 0.38], [0.2, 0.9], [0.4, 0.3], fractions=6
|
306
|
+
... )
|
307
|
+
array([0, 0, 1, 0, 1, 0], dtype=uint8)
|
308
|
+
|
309
|
+
Count the number of phasors between the combinations of three components:
|
310
|
+
|
311
|
+
>>> phasor_component_graphical(
|
312
|
+
... [0.4, 0.5],
|
313
|
+
... [0.2, 0.3],
|
314
|
+
... [0.0, 0.2, 0.9],
|
315
|
+
... [0.0, 0.4, 0.3],
|
316
|
+
... fractions=6,
|
317
|
+
... )
|
318
|
+
array([[0, 1, 1, 1, 1, 0],
|
319
|
+
[0, 1, 0, 0, 0, 0],
|
320
|
+
[0, 1, 2, 0, 0, 0]], dtype=uint8)
|
321
|
+
|
322
|
+
"""
|
323
|
+
real = numpy.asarray(real)
|
324
|
+
imag = numpy.asarray(imag)
|
325
|
+
component_real = numpy.asarray(component_real)
|
326
|
+
component_imag = numpy.asarray(component_imag)
|
327
|
+
if (
|
328
|
+
real.shape != imag.shape
|
329
|
+
or component_real.shape != component_imag.shape
|
330
|
+
):
|
331
|
+
raise ValueError('input array shapes must match')
|
332
|
+
if component_real.ndim != 1:
|
333
|
+
raise ValueError(
|
334
|
+
'component arrays are not one-dimensional: '
|
335
|
+
f'{component_real.ndim} dimensions found'
|
336
|
+
)
|
337
|
+
num_components = len(component_real)
|
338
|
+
if num_components not in {2, 3}:
|
339
|
+
raise ValueError('number of components must be 2 or 3')
|
340
|
+
|
341
|
+
if fractions is None:
|
342
|
+
longest_distance = 0
|
343
|
+
for i in range(num_components):
|
344
|
+
a_real = component_real[i]
|
345
|
+
a_imag = component_imag[i]
|
346
|
+
for j in range(i + 1, num_components):
|
347
|
+
b_real = component_real[j]
|
348
|
+
b_imag = component_imag[j]
|
349
|
+
_, _, length = _segment_direction_and_length(
|
350
|
+
a_real, a_imag, b_real, b_imag
|
351
|
+
)
|
352
|
+
longest_distance = max(longest_distance, length)
|
353
|
+
fractions = numpy.linspace(
|
354
|
+
0.0, 1.0, int(round(longest_distance / (radius / 2) + 1))
|
355
|
+
)
|
356
|
+
elif isinstance(fractions, (int, numbers.Integral)):
|
357
|
+
fractions = numpy.linspace(0.0, 1.0, fractions)
|
358
|
+
else:
|
359
|
+
fractions = numpy.asarray(fractions)
|
360
|
+
if fractions.ndim != 1:
|
361
|
+
raise ValueError('fractions is not a one-dimensional array')
|
362
|
+
|
363
|
+
dtype = numpy.min_scalar_type(real.size)
|
364
|
+
counts = numpy.empty(
|
365
|
+
(1 if num_components == 2 else 3, fractions.size), dtype
|
366
|
+
)
|
367
|
+
|
368
|
+
c = 0
|
369
|
+
for i in range(num_components):
|
370
|
+
a_real = component_real[i]
|
371
|
+
a_imag = component_imag[i]
|
372
|
+
for j in range(i + 1, num_components):
|
373
|
+
b_real = component_real[j]
|
374
|
+
b_imag = component_imag[j]
|
375
|
+
ab_real = a_real - b_real
|
376
|
+
ab_imag = a_imag - b_imag
|
377
|
+
|
378
|
+
for k, f in enumerate(fractions):
|
379
|
+
if f < 0.0 or f > 1.0:
|
380
|
+
raise ValueError(f'fraction {f} out of bounds [0.0, 1.0]')
|
381
|
+
if num_components == 2:
|
382
|
+
mask = _is_inside_circle(
|
383
|
+
real,
|
384
|
+
imag,
|
385
|
+
b_real + f * ab_real, # cursor_real
|
386
|
+
b_imag + f * ab_imag, # cursor_imag
|
387
|
+
radius,
|
388
|
+
)
|
389
|
+
else:
|
390
|
+
# num_components == 3
|
391
|
+
mask = _is_inside_stadium(
|
392
|
+
real,
|
393
|
+
imag,
|
394
|
+
b_real + f * ab_real, # cursor_real
|
395
|
+
b_imag + f * ab_imag, # cursor_imag
|
396
|
+
component_real[3 - i - j], # c_real
|
397
|
+
component_imag[3 - i - j], # c_imag
|
398
|
+
radius,
|
399
|
+
)
|
400
|
+
counts[c, k] = numpy.sum(mask, dtype=dtype)
|
401
|
+
c += 1
|
402
|
+
|
403
|
+
return counts[0] if num_components == 2 else counts
|
404
|
+
|
405
|
+
|
406
|
+
def phasor_component_fit(
|
407
|
+
mean: ArrayLike,
|
408
|
+
real: ArrayLike,
|
409
|
+
imag: ArrayLike,
|
410
|
+
component_real: ArrayLike,
|
411
|
+
component_imag: ArrayLike,
|
412
|
+
/,
|
413
|
+
**kwargs: Any,
|
414
|
+
) -> NDArray[Any]:
|
415
|
+
"""Return fractions of multiple components from phasor coordinates.
|
416
|
+
|
417
|
+
Component fractions are obtained from the least-squares solution of a
|
418
|
+
linear matrix equation that relates phasor coordinates from one or
|
419
|
+
multiple harmonics to component fractions according to [2]_.
|
420
|
+
|
421
|
+
Up to ``2 * number harmonics + 1`` components can be fit to multi-harmonic
|
422
|
+
phasor coordinates, that is up to three components for single harmonic
|
423
|
+
phasor coordinates.
|
424
|
+
|
425
|
+
Parameters
|
426
|
+
----------
|
427
|
+
mean : array_like
|
428
|
+
Intensity of phasor coordinates.
|
429
|
+
real : array_like
|
430
|
+
Real component of phasor coordinates.
|
431
|
+
Harmonics, if any, must be in the first dimension.
|
432
|
+
imag : array_like
|
433
|
+
Imaginary component of phasor coordinates.
|
434
|
+
Harmonics, if any, must be in the first dimension.
|
435
|
+
component_real : array_like
|
436
|
+
Real coordinates of components.
|
437
|
+
Must be one or two-dimensional with harmonics in the first dimension.
|
438
|
+
component_imag : array_like
|
439
|
+
Imaginary coordinates of components.
|
440
|
+
Must be one or two-dimensional with harmonics in the first dimension.
|
441
|
+
**kwargs : optional
|
442
|
+
Additional arguments passed to :py:func:`scipy.linalg.lstsq()`.
|
443
|
+
|
444
|
+
Returns
|
445
|
+
-------
|
446
|
+
fractions : ndarray
|
447
|
+
Component fractions.
|
448
|
+
Fractions may not exactly add up to 1.0.
|
449
|
+
|
450
|
+
Raises
|
451
|
+
------
|
452
|
+
ValueError
|
453
|
+
The array shapes of `real` and `imag` do not match.
|
454
|
+
The array shapes of `component_real` and `component_imag` do not match.
|
455
|
+
The number of harmonics in the components does not
|
456
|
+
match the ones in the phasor coordinates.
|
457
|
+
The system is underdetermined; the component matrix having more
|
458
|
+
columns than rows.
|
459
|
+
|
460
|
+
See Also
|
461
|
+
--------
|
462
|
+
:ref:`sphx_glr_tutorials_api_phasorpy_component.py`
|
463
|
+
:ref:`sphx_glr_tutorials_applications_phasorpy_component_fit.py`
|
464
|
+
|
465
|
+
Notes
|
466
|
+
-----
|
467
|
+
For now, calculation of fractions of components from different channels
|
468
|
+
or frequencies is not supported. Only one set of components can be
|
469
|
+
analyzed and is broadcast to all channels/frequencies.
|
470
|
+
|
471
|
+
The method builds a linear matrix equation,
|
472
|
+
:math:`A\\mathbf{x} = \\mathbf{b}`, where :math:`A` consists of the
|
473
|
+
phasor coordinates of individual components, :math:`\\mathbf{x}` are
|
474
|
+
the unknown fractions, and :math:`\\mathbf{b}` represents the measured
|
475
|
+
phasor coordinates in the mixture. The least-squares solution of this
|
476
|
+
linear matrix equation yields the fractions.
|
477
|
+
|
478
|
+
References
|
479
|
+
----------
|
480
|
+
.. [2] Vallmitjana A, Lepanto P, Irigoin F, and Malacrida L.
|
481
|
+
`Phasor-based multi-harmonic unmixing for in-vivo hyperspectral
|
482
|
+
imaging <https://doi.org/10.1088/2050-6120/ac9ae9>`_.
|
483
|
+
*Methods Appl Fluoresc*, 11(1): 014001 (2022)
|
484
|
+
|
485
|
+
Examples
|
486
|
+
--------
|
487
|
+
>>> phasor_component_fit(
|
488
|
+
... [1, 1, 1], [0.6, 0.5, 0.4], [0.4, 0.3, 0.2], [0.2, 0.9], [0.4, 0.3]
|
489
|
+
... )
|
490
|
+
array([[0.4644, 0.5356, 0.6068],
|
491
|
+
[0.5559, 0.4441, 0.3322]])
|
492
|
+
|
493
|
+
"""
|
494
|
+
from scipy.linalg import lstsq
|
495
|
+
|
496
|
+
mean = numpy.atleast_1d(mean)
|
497
|
+
real = numpy.atleast_1d(real)
|
498
|
+
imag = numpy.atleast_1d(imag)
|
499
|
+
component_real = numpy.atleast_1d(component_real)
|
500
|
+
component_imag = numpy.atleast_1d(component_imag)
|
501
|
+
|
502
|
+
if real.shape != imag.shape:
|
503
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
504
|
+
if mean.shape != real.shape[-mean.ndim :]:
|
505
|
+
raise ValueError(f'{mean.shape=} does not match {real.shape=}')
|
506
|
+
|
507
|
+
if component_real.shape != component_imag.shape:
|
508
|
+
raise ValueError(f'{component_real.shape=} != {component_imag.shape=}')
|
509
|
+
if numpy.isnan(component_real).any() or numpy.isnan(component_imag).any():
|
510
|
+
raise ValueError(
|
511
|
+
'component phasor coordinates must not contain NaN values'
|
512
|
+
)
|
513
|
+
if numpy.isinf(component_real).any() or numpy.isinf(component_imag).any():
|
514
|
+
raise ValueError(
|
515
|
+
'component phasor coordinates must not contain infinite values'
|
516
|
+
)
|
517
|
+
|
518
|
+
if component_real.ndim == 1:
|
519
|
+
component_real = component_real.reshape(1, -1)
|
520
|
+
component_imag = component_imag.reshape(1, -1)
|
521
|
+
elif component_real.ndim > 2:
|
522
|
+
raise ValueError(f'{component_real.ndim=} > 2')
|
523
|
+
|
524
|
+
num_harmonics, num_components = component_real.shape
|
525
|
+
|
526
|
+
# create component matrix for least squares solving:
|
527
|
+
# [real coordinates of components (for each harmonic)] +
|
528
|
+
# [imaginary coordinates of components (for each harmonic)] +
|
529
|
+
# [ones for intensity constraint]
|
530
|
+
component_matrix = numpy.ones((2 * num_harmonics + 1, num_components))
|
531
|
+
component_matrix[:num_harmonics] = component_real
|
532
|
+
component_matrix[num_harmonics : 2 * num_harmonics] = component_imag
|
533
|
+
|
534
|
+
if component_matrix.shape[0] < component_matrix.shape[1]:
|
535
|
+
raise ValueError(
|
536
|
+
'the system is undetermined '
|
537
|
+
f'({num_components=} > {num_harmonics * 2 + 1=})'
|
538
|
+
)
|
539
|
+
|
540
|
+
has_harmonic_axis = mean.ndim + 1 == real.ndim
|
541
|
+
if not has_harmonic_axis:
|
542
|
+
real = numpy.expand_dims(real, axis=0)
|
543
|
+
imag = numpy.expand_dims(imag, axis=0)
|
544
|
+
elif real.shape[0] != num_harmonics:
|
545
|
+
raise ValueError(f'{real.shape[0]=} != {component_real.shape[0]=}')
|
546
|
+
|
547
|
+
# TODO: replace Inf with NaN values?
|
548
|
+
mean, real, imag = phasor_threshold(mean, real, imag)
|
549
|
+
|
550
|
+
# replace NaN values with 0.0 for least squares solving
|
551
|
+
real = numpy.nan_to_num(real, nan=0.0, copy=False)
|
552
|
+
imag = numpy.nan_to_num(imag, nan=0.0, copy=False)
|
553
|
+
|
554
|
+
# create coordinates matrix for least squares solving:
|
555
|
+
# [real coordinates (for each harmonic)] +
|
556
|
+
# [imaginary coordinates (for each harmonic)] +
|
557
|
+
# [ones for intensity constraint]
|
558
|
+
coords = numpy.ones(
|
559
|
+
(2 * num_harmonics + 1,) + real.shape[1:] # type: ignore[union-attr]
|
560
|
+
)
|
561
|
+
coords[:num_harmonics] = real
|
562
|
+
coords[num_harmonics : 2 * num_harmonics] = imag
|
563
|
+
|
564
|
+
fractions = lstsq(
|
565
|
+
component_matrix, coords.reshape(coords.shape[0], -1), **kwargs
|
566
|
+
)[0]
|
567
|
+
|
568
|
+
# reshape to match input dimensions
|
569
|
+
fractions = fractions.reshape((num_components,) + coords.shape[1:])
|
570
|
+
|
571
|
+
# TODO: normalize fractions to sum up to 1.0?
|
572
|
+
# fractions /= numpy.sum(fractions, axis=0, keepdims=True)
|
573
|
+
|
574
|
+
# restore NaN values in fractions from mean
|
575
|
+
_blend_and(mean, fractions, out=fractions)
|
576
|
+
|
577
|
+
return numpy.asarray(fractions)
|
578
|
+
|
579
|
+
|
580
|
+
def phasor_component_mvc(
|
581
|
+
real: ArrayLike,
|
582
|
+
imag: ArrayLike,
|
583
|
+
component_real: ArrayLike,
|
584
|
+
component_imag: ArrayLike,
|
585
|
+
/,
|
586
|
+
*,
|
587
|
+
dtype: DTypeLike = None,
|
588
|
+
num_threads: int | None = None,
|
589
|
+
) -> NDArray[Any]:
|
590
|
+
"""Return mean value coordinates of phasor coordinates from components.
|
591
|
+
|
592
|
+
The mean value coordinates of phasor coordinates with respect to three or
|
593
|
+
more components spanning an arbitrary simple polygon are computed using
|
594
|
+
the stable method described in [3]_.
|
595
|
+
For three components, mean value coordinates are equivalent to
|
596
|
+
barycentric coordinates.
|
597
|
+
|
598
|
+
Parameters
|
599
|
+
----------
|
600
|
+
real : array_like
|
601
|
+
Real component of phasor coordinates.
|
602
|
+
imag : array_like
|
603
|
+
Imaginary component of phasor coordinates.
|
604
|
+
component_real : array_like
|
605
|
+
Real coordinates of at least three components.
|
606
|
+
component_imag : array_like
|
607
|
+
Imaginary coordinates of at least three components.
|
608
|
+
dtype : dtype_like, optional
|
609
|
+
Floating point data type used for calculation and output values.
|
610
|
+
Either `float32` or `float64`. The default is `float64`.
|
611
|
+
num_threads : int, optional
|
612
|
+
Number of OpenMP threads to use for parallelization.
|
613
|
+
By default, multi-threading is disabled.
|
614
|
+
If zero, up to half of logical CPUs are used.
|
615
|
+
OpenMP may not be available on all platforms.
|
616
|
+
|
617
|
+
Returns
|
618
|
+
-------
|
619
|
+
fractions : ndarray
|
620
|
+
Mean value coordinates for each phasor coordinate.
|
621
|
+
|
622
|
+
Raises
|
623
|
+
------
|
624
|
+
ValueError
|
625
|
+
The array shapes of `real` and `imag` do not match.
|
626
|
+
The array shapes of `component_real` and `component_imag` do not match.
|
627
|
+
|
628
|
+
Notes
|
629
|
+
-----
|
630
|
+
Calculation of mean value coordinates for different channels,
|
631
|
+
frequencies, or harmonics is not supported. Only one set of components
|
632
|
+
can be analyzed and is broadcast to all channels/frequencies/harmonics.
|
633
|
+
|
634
|
+
For three components, this function returns the same result as
|
635
|
+
:py:func:`phasor_component_fit`. For more than three components,
|
636
|
+
the system is underdetermined and the mean value coordinates represent
|
637
|
+
one of multiple solutions. However, the special properties of the mean
|
638
|
+
value coordinates make them particularly useful for interpolating and
|
639
|
+
visualizing multi-component data.
|
640
|
+
|
641
|
+
References
|
642
|
+
----------
|
643
|
+
.. [3] Fuda C and Hormann K.
|
644
|
+
`A new stable method to compute mean value coordinates
|
645
|
+
<https://doi.org/10.1016/j.cagd.2024.102310>`_.
|
646
|
+
*Computer Aided Geometric Design*, 111: 102310 (2024)
|
647
|
+
|
648
|
+
Examples
|
649
|
+
--------
|
650
|
+
Calculate the barycentric coordinates of a phasor coordinate
|
651
|
+
in a triangle defined by three components:
|
652
|
+
|
653
|
+
>>> phasor_component_mvc(0.6, 0.3, [0.0, 1.0, 0.0], [1.0, 0.0, 0.0])
|
654
|
+
array([0.3, 0.6, 0.1])
|
655
|
+
|
656
|
+
The barycentric coordinates of phasor coordinates outside the polygon
|
657
|
+
defined by the components may be outside the range [0.0, 1.0]:
|
658
|
+
|
659
|
+
>>> phasor_component_mvc(0.6, 0.6, [0.0, 1.0, 0.0], [1.0, 0.0, 0.0])
|
660
|
+
array([0.6, 0.6, -0.2])
|
661
|
+
|
662
|
+
"""
|
663
|
+
num_threads = number_threads(num_threads)
|
664
|
+
|
665
|
+
dtype = numpy.dtype(dtype)
|
666
|
+
if dtype.char not in {'f', 'd'}:
|
667
|
+
raise ValueError(f'{dtype=} is not a floating point type')
|
668
|
+
|
669
|
+
real = numpy.ascontiguousarray(real, dtype=dtype)
|
670
|
+
imag = numpy.ascontiguousarray(imag, dtype=dtype)
|
671
|
+
component_real = numpy.ascontiguousarray(component_real, dtype=dtype)
|
672
|
+
component_imag = numpy.ascontiguousarray(component_imag, dtype=dtype)
|
673
|
+
|
674
|
+
if real.shape != imag.shape:
|
675
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
676
|
+
if component_real.shape != component_imag.shape:
|
677
|
+
raise ValueError(f'{component_real.shape=} != {component_imag.shape=}')
|
678
|
+
if component_real.ndim != 1 or component_real.size < 3:
|
679
|
+
raise ValueError('number of components must be three or more')
|
680
|
+
if numpy.isnan(component_real).any() or numpy.isnan(component_imag).any():
|
681
|
+
raise ValueError('component coordinates must not contain NaN values')
|
682
|
+
if numpy.isinf(component_real).any() or numpy.isinf(component_imag).any():
|
683
|
+
raise ValueError(
|
684
|
+
'component coordinates must not contain infinite values'
|
685
|
+
)
|
686
|
+
|
687
|
+
# TODO:: sorting not strictly required for three components?
|
688
|
+
component_real, component_imag, indices = sort_coordinates(
|
689
|
+
component_real, component_imag
|
690
|
+
)
|
691
|
+
|
692
|
+
shape = real.shape
|
693
|
+
real = real.reshape(-1)
|
694
|
+
imag = imag.reshape(-1)
|
695
|
+
fraction = numpy.zeros((component_real.size, real.size), dtype=dtype)
|
696
|
+
|
697
|
+
_mean_value_coordinates(
|
698
|
+
fraction,
|
699
|
+
indices,
|
700
|
+
real,
|
701
|
+
imag,
|
702
|
+
component_real,
|
703
|
+
component_imag,
|
704
|
+
num_threads,
|
705
|
+
)
|
706
|
+
|
707
|
+
return numpy.asarray(fraction.reshape((-1, *shape)).squeeze())
|