phasorpy 0.6__cp312-cp312-win_arm64.whl → 0.8__cp312-cp312-win_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/cli.py CHANGED
@@ -86,6 +86,13 @@ def fret(hide: bool) -> None:
86
86
 
87
87
 
88
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
+ )
89
96
  @click.option(
90
97
  '-f',
91
98
  '--frequency',
@@ -96,7 +103,7 @@ def fret(hide: bool) -> None:
96
103
  @click.option(
97
104
  '-l',
98
105
  '--lifetime',
99
- default=(4.0, 1.0),
106
+ # default=(4.0, 1.0),
100
107
  type=float,
101
108
  multiple=True,
102
109
  required=False,
@@ -118,14 +125,25 @@ def fret(hide: bool) -> None:
118
125
  help='Do not show interactive plot.',
119
126
  )
120
127
  def lifetime(
128
+ number_lifetimes: int,
121
129
  frequency: float | None,
122
130
  lifetime: tuple[float, ...],
123
131
  fraction: tuple[float, ...],
124
132
  hide: bool,
125
133
  ) -> None:
126
134
  """Lifetime command group."""
135
+ from .lifetime import phasor_semicircle, phasor_to_normal_lifetime
127
136
  from .plot import LifetimePlots
128
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
+
129
147
  plot = LifetimePlots(
130
148
  frequency,
131
149
  lifetime,
phasorpy/cluster.py CHANGED
@@ -1,8 +1,8 @@
1
1
  """Cluster phasor coordinates.
2
2
 
3
- The `phasorpy.cluster` module provides functions to:
3
+ The ``phasorpy.cluster`` module provides functions to:
4
4
 
5
- - fit elliptic clusters to phasor coordinates using
5
+ - fit elliptic clusters to phasor coordinates using a
6
6
  Gaussian Mixture Model (GMM):
7
7
 
8
8
  - :py:func:`phasor_cluster_gmm`
@@ -21,7 +21,6 @@ if TYPE_CHECKING:
21
21
  import math
22
22
 
23
23
  import numpy
24
- from sklearn.mixture import GaussianMixture
25
24
 
26
25
 
27
26
  def phasor_cluster_gmm(
@@ -52,14 +51,14 @@ def phasor_cluster_gmm(
52
51
  Real component of phasor coordinates.
53
52
  imag : array_like
54
53
  Imaginary component of phasor coordinates.
55
- sigma: float, default = 2.0
54
+ sigma : float, optional
56
55
  Scaling factor for radii of major and minor axes.
57
- Defaults to 2, which corresponds to the scaling of eigenvalues for a
58
- 95% confidence ellipse.
56
+ Defaults to 2.0, which corresponds to the scaling of eigenvalues for
57
+ a 95% confidence ellipse.
59
58
  clusters : int, optional
60
59
  Number of Gaussian distributions to fit to phasor coordinates.
61
60
  Defaults to 1.
62
- sort: {'polar', 'phasor', 'area'}, optional
61
+ sort : {'polar', 'phasor', 'area'}, optional
63
62
  Sorting method for output clusters. Defaults to 'polar'.
64
63
 
65
64
  - 'polar': Sort by polar coordinates (phase, then modulation).
@@ -67,7 +66,7 @@ def phasor_cluster_gmm(
67
66
  - 'area': Sort by inverse area of ellipse (-major * minor).
68
67
 
69
68
  **kwargs
70
- Additional keyword arguments passed to
69
+ Optional arguments passed to
71
70
  :py:class:`sklearn.mixture.GaussianMixture`.
72
71
 
73
72
  Common options include:
@@ -89,19 +88,12 @@ def phasor_cluster_gmm(
89
88
  angle : tuple of float
90
89
  Rotation angles of major axes in radians, within range [0, pi].
91
90
 
92
- Raises
93
- ------
94
- ValueError
95
- If the array shapes of `real` and `imag` do not match.
96
- If `clusters` is not a positive integer.
97
-
98
-
99
91
  References
100
92
  ----------
101
93
  .. [1] Vallmitjana A, Torrado B, and Gratton E.
102
94
  `Phasor-based image segmentation: machine learning clustering techniques
103
95
  <https://doi.org/10.1364/BOE.422766>`_.
104
- *Biomed Opt Express*, 12(6): 3410-3422 (2021).
96
+ *Biomed Opt Express*, 12(6): 3410-3422 (2021)
105
97
 
106
98
  Examples
107
99
  --------
@@ -119,11 +111,13 @@ def phasor_cluster_gmm(
119
111
  >>> center_real, center_imag, radius_major, radius_minor, angle = (
120
112
  ... phasor_cluster_gmm(real, imag, clusters=2)
121
113
  ... )
122
- >>> centers_real # doctest: +SKIP
114
+ >>> center_real # doctest: +SKIP
123
115
  (0.2, 0.4)
124
116
 
125
117
  """
126
- coords = numpy.stack((real, imag), axis=-1).reshape(-1, 2)
118
+ from sklearn.mixture import GaussianMixture
119
+
120
+ coords = numpy.stack([real, imag], axis=-1).reshape(-1, 2)
127
121
 
128
122
  valid_data = ~numpy.isnan(coords).any(axis=1)
129
123
  coords = coords[valid_data]
phasorpy/color.py CHANGED
@@ -1,4 +1,4 @@
1
- """Color palettes and manipulation."""
1
+ """Color palettes and color manipulation utilities."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -19,20 +19,22 @@ def wavelength2rgb(
19
19
  ) -> tuple[float, float, float] | NDArray[Any]:
20
20
  """Return approximate sRGB color components of visible wavelength(s).
21
21
 
22
- Wavelengths are clipped to range [360, 750] nm, rounded, and used to
22
+ Wavelengths are clipped to the range [360, 750] nm, rounded, and used to
23
23
  index the :py:attr:`SRGB_SPECTRUM` palette.
24
24
 
25
25
  Parameters
26
26
  ----------
27
27
  wavelength : array_like
28
28
  Scalar or array of wavelengths in nm.
29
- dtype : data-type, optional
29
+ dtype : dtype_like, optional
30
30
  Data-type of return value. The default is ``float32``.
31
31
 
32
32
  Returns
33
33
  -------
34
- ndarray or tuple
35
- Approximate sRGB color components of visible wavelength.
34
+ ndarray or tuple of float
35
+ Approximate sRGB color components of visible wavelength(s).
36
+ If input is scalar, return tuple of three floats.
37
+ If input is array, return ndarray with shape (..., 3).
36
38
  Floating-point values are in range [0.0, 1.0].
37
39
  Integer values are scaled to the dtype's maximum value.
38
40
 
@@ -74,7 +76,7 @@ def float2int(
74
76
  ----------
75
77
  rgb : array_like
76
78
  Scalar or array of normalized floating-point color components.
77
- dtype : data-type, optional
79
+ dtype : dtype_like, optional
78
80
  Data type of return value. The default is ``uint8``.
79
81
 
80
82
  Returns
@@ -169,7 +171,7 @@ CATEGORICAL: NDArray[numpy.float32] = numpy.array([
169
171
  ], dtype=numpy.float32)
170
172
  """Categorical sRGB color palette inspired by C. Glasbey.
171
173
 
172
- Contains 64 maximally distinct colours for visualization.
174
+ Contains 64 maximally distinct colors for visualization.
173
175
 
174
176
  Generated using the `glasbey <https://glasbey.readthedocs.io>`_ package::
175
177
 
@@ -575,6 +577,8 @@ SRGB_SPECTRUM: NDArray[numpy.float32] = numpy.array([
575
577
  ], dtype=numpy.float32)
576
578
  """sRGB color components for wavelengths of visible light (360-750 nm).
577
579
 
580
+ Array of shape (391, 3) containing normalized sRGB color components
581
+ for wavelengths from 360 to 750 nm in 1 nm increments.
578
582
  Based on the CIE 1931 2° Standard Observer.
579
583
 
580
584
  Generated using the `colour <https://colour.readthedocs.io>`_ package::
@@ -1,6 +1,6 @@
1
- """Component analysis of phasor coordinates.
1
+ """Analyze components in phasor coordinates.
2
2
 
3
- The ``phasorpy.components`` module provides functions to:
3
+ The ``phasorpy.component`` module provides functions to:
4
4
 
5
5
  - calculate fractions of two known components by projecting onto the
6
6
  line between the components (:py:func:`phasor_component_fraction`)
@@ -14,9 +14,15 @@ The ``phasorpy.components`` module provides functions to:
14
14
  - calculate fractions of two or three known components by resolving
15
15
  graphically with histogram (:py:func:`phasor_component_graphical`)
16
16
 
17
+ - calculate mean value coordinates of phasor coordinates with respect
18
+ to three or more components (:py:func:`phasor_component_mvc`)
19
+
17
20
  - blindly resolve fractions of multiple components by using harmonic
18
21
  information (:py:func:`phasor_component_blind`, not implemented)
19
22
 
23
+ - calculate phasor coordinates from fractional intensities of
24
+ components (:py:func:`phasor_from_component`)
25
+
20
26
  """
21
27
 
22
28
  from __future__ import annotations
@@ -26,13 +32,15 @@ __all__ = [
26
32
  'phasor_component_fit',
27
33
  'phasor_component_fraction',
28
34
  'phasor_component_graphical',
35
+ 'phasor_component_mvc',
36
+ 'phasor_from_component',
29
37
  ]
30
38
 
31
39
  import numbers
32
40
  from typing import TYPE_CHECKING
33
41
 
34
42
  if TYPE_CHECKING:
35
- from ._typing import Any, ArrayLike, NDArray
43
+ from ._typing import Any, ArrayLike, DTypeLike, NDArray
36
44
 
37
45
  import numpy
38
46
 
@@ -41,9 +49,92 @@ from ._phasorpy import (
41
49
  _fraction_on_segment,
42
50
  _is_inside_circle,
43
51
  _is_inside_stadium,
52
+ _mean_value_coordinates,
44
53
  _segment_direction_and_length,
45
54
  )
46
- from .phasor import phasor_threshold
55
+ from ._utils import sort_coordinates
56
+ from .filter 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
+ See Also
99
+ --------
100
+ phasorpy.phasor.phasor_combine
101
+
102
+ Examples
103
+ --------
104
+ Calculate phasor coordinates from two components and their fractional
105
+ intensities:
106
+
107
+ >>> phasor_from_component(
108
+ ... [0.6, 0.4], [0.3, 0.2], [[1.0, 0.2, 0.9], [0.0, 0.8, 0.1]]
109
+ ... )
110
+ (array([0.6, 0.44, 0.58]), array([0.3, 0.22, 0.29]))
111
+
112
+ """
113
+ dtype = numpy.dtype(dtype)
114
+ if dtype.char not in {'f', 'd'}:
115
+ raise ValueError(f'{dtype=} is not a floating point type')
116
+
117
+ fraction = numpy.asarray(fraction, dtype=dtype, copy=True)
118
+ if fraction.ndim < 1:
119
+ raise ValueError(f'{fraction.ndim=} < 1')
120
+ if fraction.shape[axis] < 2:
121
+ raise ValueError(f'{fraction.shape[axis]=} < 2')
122
+ with numpy.errstate(divide='ignore', invalid='ignore'):
123
+ fraction /= fraction.sum(axis=axis, keepdims=True)
124
+
125
+ component_real = numpy.asarray(component_real, dtype=dtype)
126
+ component_imag = numpy.asarray(component_imag, dtype=dtype)
127
+ if component_real.shape != component_imag.shape:
128
+ raise ValueError(f'{component_real.shape=} != {component_imag.shape=}')
129
+ if component_real.ndim != 1:
130
+ raise ValueError(f'{component_real.ndim=} != 1')
131
+ if component_real.size != fraction.shape[axis]:
132
+ raise ValueError(f'{component_real.size=} != {fraction.shape[axis]=}')
133
+
134
+ fraction = numpy.moveaxis(fraction, axis, -1)
135
+ real = numpy.dot(fraction, component_real)
136
+ imag = numpy.dot(fraction, component_imag)
137
+ return real, imag
47
138
 
48
139
 
49
140
  def phasor_component_fraction(
@@ -83,7 +174,7 @@ def phasor_component_fraction(
83
174
 
84
175
  See Also
85
176
  --------
86
- :ref:`sphx_glr_tutorials_api_phasorpy_components.py`
177
+ :ref:`sphx_glr_tutorials_api_phasorpy_component.py`
87
178
 
88
179
  Notes
89
180
  -----
@@ -97,7 +188,7 @@ def phasor_component_fraction(
97
188
  --------
98
189
  >>> phasor_component_fraction(
99
190
  ... [0.6, 0.5, 0.4], [0.4, 0.3, 0.2], [0.2, 0.9], [0.4, 0.3]
100
- ... ) # doctest: +NUMBER
191
+ ... )
101
192
  array([0.44, 0.56, 0.68])
102
193
 
103
194
  """
@@ -132,7 +223,7 @@ def phasor_component_graphical(
132
223
  *,
133
224
  radius: float = 0.05,
134
225
  fractions: ArrayLike | None = None,
135
- ) -> tuple[NDArray[Any], ...]:
226
+ ) -> NDArray[Any]:
136
227
  r"""Return fractions of two or three components from phasor coordinates.
137
228
 
138
229
  The graphical method is based on moving circular cursors along the line
@@ -162,9 +253,11 @@ def phasor_component_graphical(
162
253
 
163
254
  Returns
164
255
  -------
165
- counts : tuple of ndarray
256
+ counts : ndarray
166
257
  Counts along each line segment connecting components.
167
258
  Ordered 0-1 (2 components) or 0-1, 0-2, 1-2 (3 components).
259
+ Shaped `(number fractions,)` (2 components) or
260
+ `(3, number fractions)` (3 components).
168
261
 
169
262
  Raises
170
263
  ------
@@ -176,7 +269,7 @@ def phasor_component_graphical(
176
269
 
177
270
  See Also
178
271
  --------
179
- :ref:`sphx_glr_tutorials_api_phasorpy_components.py`
272
+ :ref:`sphx_glr_tutorials_api_phasorpy_component.py`
180
273
 
181
274
  Notes
182
275
  -----
@@ -202,7 +295,6 @@ def phasor_component_graphical(
202
295
 
203
296
  References
204
297
  ----------
205
-
206
298
  .. [1] Ranjit S, Datta R, Dvornikov A, and Gratton E.
207
299
  `Multicomponent analysis of phasor plot in a single pixel to
208
300
  calculate changes of metabolic trajectory in biological systems
@@ -215,8 +307,8 @@ def phasor_component_graphical(
215
307
 
216
308
  >>> phasor_component_graphical(
217
309
  ... [0.6, 0.3], [0.35, 0.38], [0.2, 0.9], [0.4, 0.3], fractions=6
218
- ... ) # doctest: +NUMBER
219
- (array([0, 0, 1, 0, 1, 0], dtype=uint64),)
310
+ ... )
311
+ array([0, 0, 1, 0, 1, 0], dtype=uint8)
220
312
 
221
313
  Count the number of phasors between the combinations of three components:
222
314
 
@@ -226,10 +318,10 @@ def phasor_component_graphical(
226
318
  ... [0.0, 0.2, 0.9],
227
319
  ... [0.0, 0.4, 0.3],
228
320
  ... fractions=6,
229
- ... ) # doctest: +NUMBER +NORMALIZE_WHITESPACE
230
- (array([0, 1, 1, 1, 1, 0], dtype=uint64),
231
- array([0, 1, 0, 0, 0, 0], dtype=uint64),
232
- array([0, 1, 2, 0, 0, 0], dtype=uint64))
321
+ ... )
322
+ array([[0, 1, 1, 1, 1, 0],
323
+ [0, 1, 0, 0, 0, 0],
324
+ [0, 1, 2, 0, 0, 0]], dtype=uint8)
233
325
 
234
326
  """
235
327
  real = numpy.asarray(real)
@@ -272,7 +364,12 @@ def phasor_component_graphical(
272
364
  if fractions.ndim != 1:
273
365
  raise ValueError('fractions is not a one-dimensional array')
274
366
 
275
- counts = []
367
+ dtype = numpy.min_scalar_type(real.size)
368
+ counts = numpy.empty(
369
+ (1 if num_components == 2 else 3, fractions.size), dtype=dtype
370
+ )
371
+
372
+ c = 0
276
373
  for i in range(num_components):
277
374
  a_real = component_real[i]
278
375
  a_imag = component_imag[i]
@@ -282,8 +379,7 @@ def phasor_component_graphical(
282
379
  ab_real = a_real - b_real
283
380
  ab_imag = a_imag - b_imag
284
381
 
285
- component_counts = []
286
- for f in fractions:
382
+ for k, f in enumerate(fractions):
287
383
  if f < 0.0 or f > 1.0:
288
384
  raise ValueError(f'fraction {f} out of bounds [0.0, 1.0]')
289
385
  if num_components == 2:
@@ -305,12 +401,10 @@ def phasor_component_graphical(
305
401
  component_imag[3 - i - j], # c_imag
306
402
  radius,
307
403
  )
308
- fraction_counts = numpy.sum(mask, dtype=numpy.uint64)
309
- component_counts.append(fraction_counts)
404
+ counts[c, k] = numpy.sum(mask, dtype=dtype)
405
+ c += 1
310
406
 
311
- counts.append(numpy.asarray(component_counts))
312
-
313
- return tuple(counts)
407
+ return counts[0] if num_components == 2 else counts
314
408
 
315
409
 
316
410
  def phasor_component_fit(
@@ -321,7 +415,7 @@ def phasor_component_fit(
321
415
  component_imag: ArrayLike,
322
416
  /,
323
417
  **kwargs: Any,
324
- ) -> tuple[NDArray[Any], ...]:
418
+ ) -> NDArray[Any]:
325
419
  """Return fractions of multiple components from phasor coordinates.
326
420
 
327
421
  Component fractions are obtained from the least-squares solution of a
@@ -348,13 +442,13 @@ def phasor_component_fit(
348
442
  component_imag : array_like
349
443
  Imaginary coordinates of components.
350
444
  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()`.
445
+ **kwargs
446
+ Optional arguments passed to :py:func:`scipy.linalg.lstsq`.
353
447
 
354
448
  Returns
355
449
  -------
356
- fractions : tuple of ndarray
357
- Component fractions, one array per component.
450
+ fractions : ndarray
451
+ Component fractions.
358
452
  Fractions may not exactly add up to 1.0.
359
453
 
360
454
  Raises
@@ -369,7 +463,7 @@ def phasor_component_fit(
369
463
 
370
464
  See Also
371
465
  --------
372
- :ref:`sphx_glr_tutorials_api_phasorpy_components.py`
466
+ :ref:`sphx_glr_tutorials_api_phasorpy_component.py`
373
467
  :ref:`sphx_glr_tutorials_applications_phasorpy_component_fit.py`
374
468
 
375
469
  Notes
@@ -392,12 +486,13 @@ def phasor_component_fit(
392
486
  imaging <https://doi.org/10.1088/2050-6120/ac9ae9>`_.
393
487
  *Methods Appl Fluoresc*, 11(1): 014001 (2022)
394
488
 
395
- Example
396
- -------
489
+ Examples
490
+ --------
397
491
  >>> phasor_component_fit(
398
492
  ... [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]))
493
+ ... )
494
+ array([[0.4644, 0.5356, 0.6068],
495
+ [0.5559, 0.4441, 0.3322]])
401
496
 
402
497
  """
403
498
  from scipy.linalg import lstsq
@@ -464,7 +559,9 @@ def phasor_component_fit(
464
559
  # [real coordinates (for each harmonic)] +
465
560
  # [imaginary coordinates (for each harmonic)] +
466
561
  # [ones for intensity constraint]
467
- coords = numpy.ones((2 * num_harmonics + 1,) + real.shape[1:])
562
+ coords = numpy.ones(
563
+ (2 * num_harmonics + 1,) + real.shape[1:] # type: ignore[union-attr]
564
+ )
468
565
  coords[:num_harmonics] = real
469
566
  coords[num_harmonics : 2 * num_harmonics] = imag
470
567
 
@@ -481,4 +578,134 @@ def phasor_component_fit(
481
578
  # restore NaN values in fractions from mean
482
579
  _blend_and(mean, fractions, out=fractions)
483
580
 
484
- return tuple(fractions)
581
+ return numpy.asarray(fractions)
582
+
583
+
584
+ def phasor_component_mvc(
585
+ real: ArrayLike,
586
+ imag: ArrayLike,
587
+ component_real: ArrayLike,
588
+ component_imag: ArrayLike,
589
+ /,
590
+ *,
591
+ dtype: DTypeLike = None,
592
+ num_threads: int | None = None,
593
+ ) -> NDArray[Any]:
594
+ """Return mean value coordinates of phasor coordinates from components.
595
+
596
+ The mean value coordinates of phasor coordinates with respect to three or
597
+ more components spanning an arbitrary simple polygon are computed using
598
+ the stable method described in [3]_.
599
+ For three components, mean value coordinates are equivalent to
600
+ barycentric coordinates.
601
+
602
+ Parameters
603
+ ----------
604
+ real : array_like
605
+ Real component of phasor coordinates.
606
+ imag : array_like
607
+ Imaginary component of phasor coordinates.
608
+ component_real : array_like
609
+ Real coordinates of at least three components.
610
+ component_imag : array_like
611
+ Imaginary coordinates of at least three components.
612
+ dtype : dtype_like, optional
613
+ Floating point data type used for calculation and output values.
614
+ Either `float32` or `float64`. The default is `float64`.
615
+ num_threads : int, optional
616
+ Number of OpenMP threads to use for parallelization.
617
+ By default, multi-threading is disabled.
618
+ If zero, up to half of logical CPUs are used.
619
+ OpenMP may not be available on all platforms.
620
+
621
+ Returns
622
+ -------
623
+ fractions : ndarray
624
+ Mean value coordinates for each phasor coordinate.
625
+
626
+ Raises
627
+ ------
628
+ ValueError
629
+ The array shapes of `real` and `imag` do not match.
630
+ The array shapes of `component_real` and `component_imag` do not match.
631
+
632
+ Notes
633
+ -----
634
+ Calculation of mean value coordinates for different channels,
635
+ frequencies, or harmonics is not supported. Only one set of components
636
+ can be analyzed and is broadcast to all channels/frequencies/harmonics.
637
+
638
+ For three components, this function returns the same result as
639
+ :py:func:`phasor_component_fit`. For more than three components,
640
+ the system is underdetermined and the mean value coordinates represent
641
+ one of multiple solutions. However, the special properties of the mean
642
+ value coordinates make them particularly useful for interpolating and
643
+ visualizing multi-component data.
644
+
645
+ References
646
+ ----------
647
+ .. [3] Fuda C and Hormann K.
648
+ `A new stable method to compute mean value coordinates
649
+ <https://doi.org/10.1016/j.cagd.2024.102310>`_.
650
+ *Computer Aided Geometric Design*, 111: 102310 (2024)
651
+
652
+ Examples
653
+ --------
654
+ Calculate the barycentric coordinates of a phasor coordinate
655
+ in a triangle defined by three components:
656
+
657
+ >>> phasor_component_mvc(0.6, 0.3, [0.0, 1.0, 0.0], [1.0, 0.0, 0.0])
658
+ array([0.3, 0.6, 0.1])
659
+
660
+ The barycentric coordinates of phasor coordinates outside the polygon
661
+ defined by the components may be outside the range [0.0, 1.0]:
662
+
663
+ >>> phasor_component_mvc(0.6, 0.6, [0.0, 1.0, 0.0], [1.0, 0.0, 0.0])
664
+ array([0.6, 0.6, -0.2])
665
+
666
+ """
667
+ num_threads = number_threads(num_threads)
668
+
669
+ dtype = numpy.dtype(dtype)
670
+ if dtype.char not in {'f', 'd'}:
671
+ raise ValueError(f'{dtype=} is not a floating point type')
672
+
673
+ real = numpy.ascontiguousarray(real, dtype=dtype)
674
+ imag = numpy.ascontiguousarray(imag, dtype=dtype)
675
+ component_real = numpy.ascontiguousarray(component_real, dtype=dtype)
676
+ component_imag = numpy.ascontiguousarray(component_imag, dtype=dtype)
677
+
678
+ if real.shape != imag.shape:
679
+ raise ValueError(f'{real.shape=} != {imag.shape=}')
680
+ if component_real.shape != component_imag.shape:
681
+ raise ValueError(f'{component_real.shape=} != {component_imag.shape=}')
682
+ if component_real.ndim != 1 or component_real.size < 3:
683
+ raise ValueError('number of components must be three or more')
684
+ if numpy.isnan(component_real).any() or numpy.isnan(component_imag).any():
685
+ raise ValueError('component coordinates must not contain NaN values')
686
+ if numpy.isinf(component_real).any() or numpy.isinf(component_imag).any():
687
+ raise ValueError(
688
+ 'component coordinates must not contain infinite values'
689
+ )
690
+
691
+ # TODO:: sorting not strictly required for three components?
692
+ component_real, component_imag, indices = sort_coordinates(
693
+ component_real, component_imag
694
+ )
695
+
696
+ shape = real.shape
697
+ real = real.reshape(-1)
698
+ imag = imag.reshape(-1)
699
+ fraction = numpy.zeros((component_real.size, real.size), dtype=dtype)
700
+
701
+ _mean_value_coordinates(
702
+ fraction,
703
+ indices,
704
+ real,
705
+ imag,
706
+ component_real,
707
+ component_imag,
708
+ num_threads,
709
+ )
710
+
711
+ return numpy.asarray(fraction.reshape((-1, *shape)).squeeze())