Coverage for continuous/multi_signals.py: 65%
211 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Multi-Signals
3=============
5Define multi-continuous signal support for colour science computations.
7This module provides the :class:`colour.continuous.MultiSignals` class for
8representing and operating on multiple continuous signals simultaneously,
9supporting interpolation and extrapolation operations.
11- :class:`colour.continuous.MultiSignals`
12"""
14from __future__ import annotations
16import typing
17from collections.abc import Iterator, KeysView, Mapping, ValuesView
19import numpy as np
21from colour.constants import DTYPE_FLOAT_DEFAULT
22from colour.continuous import AbstractContinuousFunction, Signal
24if typing.TYPE_CHECKING:
25 from colour.hints import (
26 Any,
27 Dict,
28 DTypeFloat,
29 List,
30 Literal,
31 NDArrayFloat,
32 ProtocolExtrapolator,
33 ProtocolInterpolator,
34 Real,
35 Self,
36 Sequence,
37 Type,
38 )
40from colour.hints import ArrayLike, Callable, Sequence, cast
41from colour.utilities import (
42 as_float_array,
43 attest,
44 first_item,
45 int_digest,
46 is_iterable,
47 is_pandas_installed,
48 multiline_repr,
49 optional,
50 required,
51 tsplit,
52 tstack,
53 validate_method,
54)
55from colour.utilities.documentation import is_documentation_building
57if typing.TYPE_CHECKING or is_pandas_installed():
58 from pandas import DataFrame, Series # pragma: no cover
59else: # pragma: no cover
60 from unittest import mock
62 DataFrame = mock.MagicMock()
63 Series = mock.MagicMock()
65__author__ = "Colour Developers"
66__copyright__ = "Copyright 2013 Colour Developers"
67__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
68__maintainer__ = "Colour Developers"
69__email__ = "colour-developers@colour-science.org"
70__status__ = "Production"
72__all__ = [
73 "MultiSignals",
74]
77class MultiSignals(AbstractContinuousFunction):
78 """
79 Define the base class for multi-signals, a container for
80 multiple :class:`colour.continuous.Signal` sub-class instances.
82 .. important::
84 Specific documentation about getting, setting, indexing and slicing
85 the multi-signals values is available in the
86 :ref:`spectral-representation-and-continuous-signal` section.
88 Parameters
89 ----------
90 data
91 Data to be stored in the multi-signals.
92 domain
93 Values to initialise the multiple :class:`colour.continuous.Signal`
94 sub-class instances :attr:`colour.continuous.Signal.domain`
95 attribute with. If both ``data`` and ``domain`` arguments are
96 defined, the latter will be used to initialise the
97 :attr:`colour.continuous.Signal.domain` attribute.
98 labels
99 Names to use for the :class:`colour.continuous.Signal` sub-class
100 instances.
102 Other Parameters
103 ----------------
104 dtype
105 Floating point data type.
106 extrapolator
107 Extrapolator class type to use as extrapolating function for the
108 :class:`colour.continuous.Signal` sub-class instances.
109 extrapolator_kwargs
110 Arguments to use when instantiating the extrapolating function of
111 the :class:`colour.continuous.Signal` sub-class instances.
112 interpolator
113 Interpolator class type to use as interpolating function for the
114 :class:`colour.continuous.Signal` sub-class instances.
115 interpolator_kwargs
116 Arguments to use when instantiating the interpolating function of
117 the :class:`colour.continuous.Signal` sub-class instances.
118 name
119 Multi-signals name.
120 signal_type
121 The :class:`colour.continuous.Signal` sub-class type used for
122 instances.
124 Attributes
125 ----------
126 - :attr:`~colour.continuous.MultiSignals.dtype`
127 - :attr:`~colour.continuous.MultiSignals.domain`
128 - :attr:`~colour.continuous.MultiSignals.range`
129 - :attr:`~colour.continuous.MultiSignals.interpolator`
130 - :attr:`~colour.continuous.MultiSignals.interpolator_kwargs`
131 - :attr:`~colour.continuous.MultiSignals.extrapolator`
132 - :attr:`~colour.continuous.MultiSignals.extrapolator_kwargs`
133 - :attr:`~colour.continuous.MultiSignals.function`
134 - :attr:`~colour.continuous.MultiSignals.signals`
135 - :attr:`~colour.continuous.MultiSignals.labels`
136 - :attr:`~colour.continuous.MultiSignals.signal_type`
138 Methods
139 -------
140 - :meth:`~colour.continuous.MultiSignals.__init__`
141 - :meth:`~colour.continuous.MultiSignals.__str__`
142 - :meth:`~colour.continuous.MultiSignals.__repr__`
143 - :meth:`~colour.continuous.MultiSignals.__hash__`
144 - :meth:`~colour.continuous.MultiSignals.__getitem__`
145 - :meth:`~colour.continuous.MultiSignals.__setitem__`
146 - :meth:`~colour.continuous.MultiSignals.__contains__`
147 - :meth:`~colour.continuous.MultiSignals.__eq__`
148 - :meth:`~colour.continuous.MultiSignals.__ne__`
149 - :meth:`~colour.continuous.MultiSignals.arithmetical_operation`
150 - :meth:`~colour.continuous.MultiSignals.multi_signals_unpack_data`
151 - :meth:`~colour.continuous.MultiSignals.fill_nan`
152 - :meth:`~colour.continuous.MultiSignals.to_dataframe`
154 Examples
155 --------
156 Instantiation with implicit *domain* and a single signal:
158 >>> range_ = np.linspace(10, 100, 10)
159 >>> print(MultiSignals(range_))
160 [[ 0. 10.]
161 [ 1. 20.]
162 [ 2. 30.]
163 [ 3. 40.]
164 [ 4. 50.]
165 [ 5. 60.]
166 [ 6. 70.]
167 [ 7. 80.]
168 [ 8. 90.]
169 [ 9. 100.]]
171 Instantiation with explicit *domain* and a single signal:
173 >>> domain = np.arange(100, 1100, 100)
174 >>> print(MultiSignals(range_, domain))
175 [[ 100. 10.]
176 [ 200. 20.]
177 [ 300. 30.]
178 [ 400. 40.]
179 [ 500. 50.]
180 [ 600. 60.]
181 [ 700. 70.]
182 [ 800. 80.]
183 [ 900. 90.]
184 [ 1000. 100.]]
186 Instantiation with multiple signals:
188 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3)
189 >>> range_ += np.array([0, 10, 20])
190 >>> print(MultiSignals(range_, domain))
191 [[ 100. 10. 20. 30.]
192 [ 200. 20. 30. 40.]
193 [ 300. 30. 40. 50.]
194 [ 400. 40. 50. 60.]
195 [ 500. 50. 60. 70.]
196 [ 600. 60. 70. 80.]
197 [ 700. 70. 80. 90.]
198 [ 800. 80. 90. 100.]
199 [ 900. 90. 100. 110.]
200 [ 1000. 100. 110. 120.]]
202 Instantiation with a *dict*:
204 >>> print(MultiSignals(dict(zip(domain, range_))))
205 [[ 100. 10. 20. 30.]
206 [ 200. 20. 30. 40.]
207 [ 300. 30. 40. 50.]
208 [ 400. 40. 50. 60.]
209 [ 500. 50. 60. 70.]
210 [ 600. 60. 70. 80.]
211 [ 700. 70. 80. 90.]
212 [ 800. 80. 90. 100.]
213 [ 900. 90. 100. 110.]
214 [ 1000. 100. 110. 120.]]
216 Instantiation using a *Signal* sub-class:
218 >>> class NotSignal(Signal):
219 ... pass
221 >>> multi_signals = MultiSignals(range_, domain, signal_type=NotSignal)
222 >>> print(multi_signals)
223 [[ 100. 10. 20. 30.]
224 [ 200. 20. 30. 40.]
225 [ 300. 30. 40. 50.]
226 [ 400. 40. 50. 60.]
227 [ 500. 50. 60. 70.]
228 [ 600. 60. 70. 80.]
229 [ 700. 70. 80. 90.]
230 [ 800. 80. 90. 100.]
231 [ 900. 90. 100. 110.]
232 [ 1000. 100. 110. 120.]]
233 >>> type(multi_signals.signals[0]) # doctest: +SKIP
234 <class 'multi_signals.NotSignal'>
236 Instantiation with a *Pandas* `Series`:
238 >>> if is_pandas_installed():
239 ... from pandas import Series
240 ...
241 ... print(
242 ... MultiSignals( # doctest: +SKIP
243 ... Series(dict(zip(domain, np.linspace(10, 100, 10))))
244 ... )
245 ... )
246 [[ 100. 10.]
247 [ 200. 20.]
248 [ 300. 30.]
249 [ 400. 40.]
250 [ 500. 50.]
251 [ 600. 60.]
252 [ 700. 70.]
253 [ 800. 80.]
254 [ 900. 90.]
255 [ 1000. 100.]]
257 Instantiation with a *Pandas* :class:`pandas.DataFrame`:
259 >>> if is_pandas_installed():
260 ... from pandas import DataFrame
261 ...
262 ... data = dict(zip(["a", "b", "c"], tsplit(range_)))
263 ... print(MultiSignals(DataFrame(data, domain))) # doctest: +SKIP
264 [[ 100. 10. 20. 30.]
265 [ 200. 20. 30. 40.]
266 [ 300. 30. 40. 50.]
267 [ 400. 40. 50. 60.]
268 [ 500. 50. 60. 70.]
269 [ 600. 60. 70. 80.]
270 [ 700. 70. 80. 90.]
271 [ 800. 80. 90. 100.]
272 [ 900. 90. 100. 110.]
273 [ 1000. 100. 110. 120.]]
275 Retrieving domain *y* variable for arbitrary range *x* variable:
277 >>> x = 150
278 >>> range_ = tstack([np.sin(np.linspace(0, 1, 10))] * 3)
279 >>> range_ += np.array([0.0, 0.25, 0.5])
280 >>> MultiSignals(range_, domain)[x] # doctest: +ELLIPSIS
281 array([ 0.0359701..., 0.2845447..., 0.5331193...])
282 >>> x = np.linspace(100, 1000, 3)
283 >>> MultiSignals(range_, domain)[x] # doctest: +ELLIPSIS
284 array([[ 4.4085384...e-20, 2.5000000...e-01, 5.0000000...e-01],
285 [ 4.7669395...e-01, 7.2526859...e-01, 9.7384323...e-01],
286 [ 8.4147098...e-01, 1.0914709...e+00, 1.3414709...e+00]])
288 Using an alternative interpolating function:
290 >>> x = 150
291 >>> from colour.algebra import CubicSplineInterpolator
292 >>> MultiSignals(range_, domain, interpolator=CubicSplineInterpolator)[
293 ... x
294 ... ] # doctest: +ELLIPSIS
295 array([ 0.0555274..., 0.3055274..., 0.5555274...])
296 >>> x = np.linspace(100, 1000, 3)
297 >>> MultiSignals(range_, domain, interpolator=CubicSplineInterpolator)[
298 ... x
299 ... ] # doctest: +ELLIPSIS
300 array([[ 0. ..., 0.25 ..., 0.5 ...],
301 [ 0.4794253..., 0.7294253..., 0.9794253...],
302 [ 0.8414709..., 1.0914709..., 1.3414709...]])
303 """
305 def __init__(
306 self,
307 data: (
308 ArrayLike
309 | DataFrame
310 | dict
311 | Self
312 | Sequence
313 | Series
314 | Signal
315 | ValuesView
316 | None
317 ) = None,
318 domain: ArrayLike | KeysView | None = None,
319 labels: Sequence | None = None,
320 **kwargs: Any,
321 ) -> None:
322 super().__init__(kwargs.get("name"))
324 self._signal_type: Type[Signal] = kwargs.get("signal_type", Signal)
326 self._signals: Dict[str, Signal] = self.multi_signals_unpack_data(
327 data, domain, labels, **kwargs
328 )
330 @property
331 def dtype(self) -> Type[DTypeFloat]:
332 """
333 Getter and setter for the multi-signals dtype.
335 Parameters
336 ----------
337 value
338 Value to set the multi-signals dtype with.
340 Returns
341 -------
342 Type[DTypeFloat]
343 Multi-signals dtype.
344 """
346 return first_item(self._signals.values()).dtype
348 @dtype.setter
349 def dtype(self, value: Type[DTypeFloat]) -> None:
350 """Setter for the **self.dtype** property."""
352 for signal in self._signals.values():
353 signal.dtype = value
355 @property
356 def domain(self) -> NDArrayFloat:
357 """
358 Getter and setter for the multi-signals' independent
359 domain variable :math:`x`.
361 Parameters
362 ----------
363 value
364 Value to set the multi-signals independent domain
365 variable :math:`x` with.
367 Returns
368 -------
369 :class:`numpy.ndarray`
370 Multi-signals independent domain variable
371 :math:`x`.
372 """
374 return first_item(self._signals.values()).domain
376 @domain.setter
377 def domain(self, value: ArrayLike) -> None:
378 """Setter for the **self.domain** property."""
380 for signal in self._signals.values():
381 signal.domain = as_float_array(value, self.dtype)
383 @property
384 def range(self) -> NDArrayFloat:
385 """
386 Getter and setter for the multi-signals' range
387 variable :math:`y`.
389 Parameters
390 ----------
391 value
392 Value to set the multi-signals' range variable
393 :math:`y` with.
395 Returns
396 -------
397 :class:`numpy.ndarray`
398 Multi-signals' range variable :math:`y`.
399 """
401 return tstack([signal.range for signal in self._signals.values()])
403 @range.setter
404 def range(self, value: ArrayLike) -> None:
405 """Setter for the **self.range** property."""
407 value = as_float_array(value)
409 if value.ndim in (0, 1):
410 for signal in self._signals.values():
411 signal.range = value
412 else:
413 attest(
414 value.shape[-1] == len(self._signals),
415 'Corresponding "y" variable columns must have '
416 'same count than underlying "Signal" components!',
417 )
419 for signal, y in zip(self._signals.values(), tsplit(value), strict=True):
420 signal.range = y
422 @property
423 def interpolator(self) -> Type[ProtocolInterpolator]:
424 """
425 Getter and setter for the multi-signals interpolator
426 type.
428 Parameters
429 ----------
430 value
431 Value to set the multi-signals interpolator type
432 with.
434 Returns
435 -------
436 Type[ProtocolInterpolator]
437 Multi-signals interpolator type.
438 """
440 return first_item(self._signals.values()).interpolator
442 @interpolator.setter
443 def interpolator(self, value: Type[ProtocolInterpolator]) -> None:
444 """Setter for the **self.interpolator** property."""
446 if value is not None:
447 for signal in self._signals.values():
448 signal.interpolator = value
450 @property
451 def interpolator_kwargs(self) -> dict:
452 """
453 Getter and setter for the interpolator instantiation time arguments.
455 Parameters
456 ----------
457 value
458 Value to set the multi-signals interpolator
459 instantiation time arguments to.
461 Returns
462 -------
463 :class:`dict`
464 Multi-signals interpolator instantiation time
465 arguments.
466 """
468 return first_item(self._signals.values()).interpolator_kwargs
470 @interpolator_kwargs.setter
471 def interpolator_kwargs(self, value: dict) -> None:
472 """Setter for the **self.interpolator_kwargs** property."""
474 for signal in self._signals.values():
475 signal.interpolator_kwargs = value
477 @property
478 def extrapolator(self) -> Type[ProtocolExtrapolator]:
479 """
480 Getter and setter for the multi-signals extrapolator
481 type.
483 Parameters
484 ----------
485 value
486 Value to set the multi-signals extrapolator type
487 with.
489 Returns
490 -------
491 Type[ProtocolExtrapolator]
492 Multi-signals extrapolator type.
493 """
495 return first_item(self._signals.values()).extrapolator
497 @extrapolator.setter
498 def extrapolator(self, value: Type[ProtocolExtrapolator]) -> None:
499 """Setter for the **self.extrapolator** property."""
501 for signal in self._signals.values():
502 signal.extrapolator = value
504 @property
505 def extrapolator_kwargs(self) -> dict:
506 """
507 Getter and setter for the multi-signals extrapolator
508 instantiation time arguments.
510 Parameters
511 ----------
512 value
513 Value to set the multi-signals extrapolator
514 instantiation time arguments to.
516 Returns
517 -------
518 :class:`dict`
519 Multi-signals extrapolator instantiation time
520 arguments.
521 """
523 return first_item(self._signals.values()).extrapolator_kwargs
525 @extrapolator_kwargs.setter
526 def extrapolator_kwargs(self, value: dict) -> None:
527 """Setter for the **self.extrapolator_kwargs** property."""
529 for signal in self._signals.values():
530 signal.extrapolator_kwargs = value
532 @property
533 def function(self) -> Callable:
534 """
535 Getter for the multi-signals callable.
537 Returns
538 -------
539 Callable
540 Multi-signals callable.
541 """
543 return first_item(self._signals.values()).function
545 @property
546 def signals(self) -> Dict[str, Signal]:
547 """
548 Getter and setter for the dictionary of
549 :class:`colour.continuous.Signal` sub-class instances.
551 Parameters
552 ----------
553 value
554 Dictionary of :class:`colour.continuous.Signal` sub-class
555 instances to set.
557 Returns
558 -------
559 :class:`dict`
560 Dictionary mapping signal names to their corresponding
561 :class:`colour.continuous.Signal` sub-class instances.
562 """
564 return self._signals
566 @signals.setter
567 def signals(
568 self,
569 value: ArrayLike | DataFrame | dict | Self | Series | Signal | None,
570 ) -> None:
571 """Setter for the **self.signals** property."""
573 self._signals = self.multi_signals_unpack_data(
574 value, signal_type=self._signal_type
575 )
577 @property
578 def labels(self) -> List[str]:
579 """
580 Getter and setter for the :class:`colour.continuous.Signal` sub-class
581 instance names.
583 Parameters
584 ----------
585 value
586 Value to set the :class:`colour.continuous.Signal` sub-class
587 instance names.
589 Returns
590 -------
591 :class:`list`
592 :class:`colour.continuous.Signal` sub-class instance names.
593 """
595 return [str(key) for key in self._signals]
597 @labels.setter
598 def labels(self, value: Sequence) -> None:
599 """Setter for the **self.labels** property."""
601 attest(
602 is_iterable(value),
603 f'"labels" property: "{value}" is not an "iterable" like object!',
604 )
606 attest(
607 len(set(value)) == len(value),
608 '"labels" property: values must be unique!',
609 )
611 attest(
612 len(value) == len(self.labels),
613 f'"labels" property: length must be "{len(self._signals)}"!',
614 )
616 self._signals = {
617 str(value[i]): signal for i, signal in enumerate(self._signals.values())
618 }
620 @property
621 def signal_type(self) -> Type[Signal]:
622 """
623 Getter for the type of :class:`colour.continuous.Signal`
624 sub-class instances.
626 Returns
627 -------
628 Type[Signal]
629 Type of :class:`colour.continuous.Signal` sub-class
630 instances used in this multi-signal collection.
631 """
633 return self._signal_type
635 def __str__(self) -> str:
636 """
637 Return a formatted string representation of the multi-signals.
639 Returns
640 -------
641 :class:`str`
642 Formatted string representation.
644 Examples
645 --------
646 >>> domain = np.arange(0, 10, 1)
647 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3)
648 >>> range_ += np.array([0, 10, 20])
649 >>> print(MultiSignals(range_))
650 [[ 0. 10. 20. 30.]
651 [ 1. 20. 30. 40.]
652 [ 2. 30. 40. 50.]
653 [ 3. 40. 50. 60.]
654 [ 4. 50. 60. 70.]
655 [ 5. 60. 70. 80.]
656 [ 6. 70. 80. 90.]
657 [ 7. 80. 90. 100.]
658 [ 8. 90. 100. 110.]
659 [ 9. 100. 110. 120.]]
660 """
662 return str(np.hstack([self.domain[:, None], self.range]))
664 def __repr__(self) -> str:
665 """
666 Return an evaluable string representation of the multi-signals.
668 Returns
669 -------
670 :class:`str`
671 Evaluable string representation.
673 Examples
674 --------
675 >>> domain = np.arange(0, 10, 1)
676 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3)
677 >>> range_ += np.array([0, 10, 20])
678 >>> MultiSignals(range_)
679 MultiSignals([[ 0., 10., 20., 30.],
680 [ 1., 20., 30., 40.],
681 [ 2., 30., 40., 50.],
682 [ 3., 40., 50., 60.],
683 [ 4., 50., 60., 70.],
684 [ 5., 60., 70., 80.],
685 [ 6., 70., 80., 90.],
686 [ 7., 80., 90., 100.],
687 [ 8., 90., 100., 110.],
688 [ 9., 100., 110., 120.]],
689 ['0', '1', '2'],
690 KernelInterpolator,
691 {},
692 Extrapolator,
693 {'method': 'Constant', 'left': nan, 'right': nan})
694 """
696 if is_documentation_building(): # pragma: no cover
697 return f"{self.__class__.__name__}(name='{self.name}', ...)"
699 return multiline_repr(
700 self,
701 [
702 {
703 "formatter": lambda x: repr( # noqa: ARG005
704 np.hstack([self.domain[:, None], self.range])
705 ),
706 },
707 {"name": "labels"},
708 {
709 "name": "interpolator",
710 "formatter": lambda x: ( # noqa: ARG005
711 self.interpolator.__name__
712 ),
713 },
714 {"name": "interpolator_kwargs"},
715 {
716 "name": "extrapolator",
717 "formatter": lambda x: ( # noqa: ARG005
718 self.extrapolator.__name__
719 ),
720 },
721 {"name": "extrapolator_kwargs"},
722 ],
723 )
725 def __hash__(self) -> int:
726 """
727 Compute the hash of the multi-signals.
729 Returns
730 -------
731 :class:`int`
732 Object hash.
733 """
735 return hash(
736 (
737 int_digest(self.domain.tobytes()),
738 *[hash(signal) for signal in self._signals.values()],
739 self.interpolator.__name__,
740 repr(self.interpolator_kwargs),
741 self.extrapolator.__name__,
742 repr(self.extrapolator_kwargs),
743 )
744 )
746 def __getitem__(self, x: ArrayLike | slice) -> NDArrayFloat:
747 """
748 Return the corresponding range variable :math:`y` for the specified
749 independent domain variable :math:`x`.
751 Parameters
752 ----------
753 x
754 Independent domain variable :math:`x`.
756 Returns
757 -------
758 :class:`numpy.ndarray`
759 Variable :math:`y` range value.
761 Examples
762 --------
763 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3)
764 >>> range_ += np.array([0, 10, 20])
765 >>> multi_signals = MultiSignals(range_)
766 >>> print(multi_signals)
767 [[ 0. 10. 20. 30.]
768 [ 1. 20. 30. 40.]
769 [ 2. 30. 40. 50.]
770 [ 3. 40. 50. 60.]
771 [ 4. 50. 60. 70.]
772 [ 5. 60. 70. 80.]
773 [ 6. 70. 80. 90.]
774 [ 7. 80. 90. 100.]
775 [ 8. 90. 100. 110.]
776 [ 9. 100. 110. 120.]]
777 >>> multi_signals[0]
778 array([ 10., 20., 30.])
779 >>> multi_signals[np.array([0, 1, 2])]
780 array([[ 10., 20., 30.],
781 [ 20., 30., 40.],
782 [ 30., 40., 50.]])
783 >>> multi_signals[np.linspace(0, 5, 5)] # doctest: +ELLIPSIS
784 array([[ 10. ..., 20. ..., 30. ...],
785 [ 22.8348902..., 32.8046056..., 42.774321 ...],
786 [ 34.8004492..., 44.7434347..., 54.6864201...],
787 [ 47.5535392..., 57.5232546..., 67.4929700...],
788 [ 60. ..., 70. ..., 80. ...]])
789 >>> multi_signals[0:3]
790 array([[ 10., 20., 30.],
791 [ 20., 30., 40.],
792 [ 30., 40., 50.]])
793 >>> multi_signals[:, 0:2]
794 array([[ 10., 20.],
795 [ 20., 30.],
796 [ 30., 40.],
797 [ 40., 50.],
798 [ 50., 60.],
799 [ 60., 70.],
800 [ 70., 80.],
801 [ 80., 90.],
802 [ 90., 100.],
803 [ 100., 110.]])
804 """
806 x_r, x_c = (x[0], x[1]) if isinstance(x, tuple) else (x, slice(None))
808 values = tstack([signal[x_r] for signal in self._signals.values()])
810 return values[..., x_c] # pyright: ignore
812 def __setitem__(self, x: ArrayLike | slice, y: ArrayLike) -> None:
813 """
814 Set the corresponding range variable :math:`y` for the specified
815 independent domain variable :math:`x`.
817 Parameters
818 ----------
819 x
820 Independent domain variable :math:`x`.
821 y
822 Corresponding range variable :math:`y`.
824 Examples
825 --------
826 >>> domain = np.arange(0, 10, 1)
827 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3)
828 >>> range_ += np.array([0, 10, 20])
829 >>> multi_signals = MultiSignals(range_)
830 >>> print(multi_signals)
831 [[ 0. 10. 20. 30.]
832 [ 1. 20. 30. 40.]
833 [ 2. 30. 40. 50.]
834 [ 3. 40. 50. 60.]
835 [ 4. 50. 60. 70.]
836 [ 5. 60. 70. 80.]
837 [ 6. 70. 80. 90.]
838 [ 7. 80. 90. 100.]
839 [ 8. 90. 100. 110.]
840 [ 9. 100. 110. 120.]]
841 >>> multi_signals[0] = 20
842 >>> multi_signals[0]
843 array([ 20., 20., 20.])
844 >>> multi_signals[np.array([0, 1, 2])] = 30
845 >>> multi_signals[np.array([0, 1, 2])]
846 array([[ 30., 30., 30.],
847 [ 30., 30., 30.],
848 [ 30., 30., 30.]])
849 >>> multi_signals[np.linspace(0, 5, 5)] = 50
850 >>> print(multi_signals)
851 [[ 0. 50. 50. 50. ]
852 [ 1. 30. 30. 30. ]
853 [ 1.25 50. 50. 50. ]
854 [ 2. 30. 30. 30. ]
855 [ 2.5 50. 50. 50. ]
856 [ 3. 40. 50. 60. ]
857 [ 3.75 50. 50. 50. ]
858 [ 4. 50. 60. 70. ]
859 [ 5. 50. 50. 50. ]
860 [ 6. 70. 80. 90. ]
861 [ 7. 80. 90. 100. ]
862 [ 8. 90. 100. 110. ]
863 [ 9. 100. 110. 120. ]]
864 >>> multi_signals[np.array([0, 1, 2])] = np.array([10, 20, 30])
865 >>> print(multi_signals)
866 [[ 0. 10. 20. 30. ]
867 [ 1. 10. 20. 30. ]
868 [ 1.25 50. 50. 50. ]
869 [ 2. 10. 20. 30. ]
870 [ 2.5 50. 50. 50. ]
871 [ 3. 40. 50. 60. ]
872 [ 3.75 50. 50. 50. ]
873 [ 4. 50. 60. 70. ]
874 [ 5. 50. 50. 50. ]
875 [ 6. 70. 80. 90. ]
876 [ 7. 80. 90. 100. ]
877 [ 8. 90. 100. 110. ]
878 [ 9. 100. 110. 120. ]]
879 >>> y = np.reshape(np.arange(1, 10, 1), (3, 3))
880 >>> multi_signals[np.array([0, 1, 2])] = y
881 >>> print(multi_signals)
882 [[ 0. 1. 2. 3. ]
883 [ 1. 4. 5. 6. ]
884 [ 1.25 50. 50. 50. ]
885 [ 2. 7. 8. 9. ]
886 [ 2.5 50. 50. 50. ]
887 [ 3. 40. 50. 60. ]
888 [ 3.75 50. 50. 50. ]
889 [ 4. 50. 60. 70. ]
890 [ 5. 50. 50. 50. ]
891 [ 6. 70. 80. 90. ]
892 [ 7. 80. 90. 100. ]
893 [ 8. 90. 100. 110. ]
894 [ 9. 100. 110. 120. ]]
895 >>> multi_signals[0:3] = 40
896 >>> multi_signals[0:3]
897 array([[ 40., 40., 40.],
898 [ 40., 40., 40.],
899 [ 40., 40., 40.]])
900 >>> multi_signals[:, 0:2] = 50
901 >>> print(multi_signals)
902 [[ 0. 50. 50. 40. ]
903 [ 1. 50. 50. 40. ]
904 [ 1.25 50. 50. 40. ]
905 [ 2. 50. 50. 9. ]
906 [ 2.5 50. 50. 50. ]
907 [ 3. 50. 50. 60. ]
908 [ 3.75 50. 50. 50. ]
909 [ 4. 50. 50. 70. ]
910 [ 5. 50. 50. 50. ]
911 [ 6. 50. 50. 90. ]
912 [ 7. 50. 50. 100. ]
913 [ 8. 50. 50. 110. ]
914 [ 9. 50. 50. 120. ]]
915 """
917 y = as_float_array(y)
919 x_r, x_c = (x[0], x[1]) if isinstance(x, tuple) else (x, slice(None))
921 attest(
922 y.ndim in range(3),
923 'Corresponding "y" variable must be a numeric or a 1-dimensional '
924 "or 2-dimensional array!",
925 )
927 if y.ndim == 0:
928 y = np.tile(y, len(self._signals))
929 elif y.ndim == 1:
930 y = y[None, :]
932 attest(
933 y.shape[-1] == len(self._signals),
934 'Corresponding "y" variable columns must have same count than '
935 'underlying "Signal" components!',
936 )
938 values = list(zip(self._signals.values(), tsplit(y), strict=True))
940 for signal, y in values[x_c]: # pyright: ignore
941 signal[x_r] = y
943 def __contains__(self, x: ArrayLike | slice) -> bool:
944 """
945 Determine whether the multi-signals contains the
946 specified independent domain variable :math:`x`.
948 Parameters
949 ----------
950 x
951 Independent domain variable :math:`x`.
953 Returns
954 -------
955 :class:`bool`
956 Whether :math:`x` domain value is contained.
958 Examples
959 --------
960 >>> range_ = np.linspace(10, 100, 10)
961 >>> multi_signals = MultiSignals(range_)
962 >>> 0 in multi_signals
963 True
964 >>> 0.5 in multi_signals
965 True
966 >>> 1000 in multi_signals
967 False
968 """
970 return x in first_item(self._signals.values())
972 def __eq__(self, other: object) -> bool:
973 """
974 Determine whether the multi-signals equals the specified
975 object.
977 Parameters
978 ----------
979 other
980 Object to determine for equality with the multi-signals.
982 Returns
983 -------
984 :class:`bool`
985 Whether the specified object is equal to the multi-signals.
987 Examples
988 --------
989 >>> range_ = np.linspace(10, 100, 10)
990 >>> multi_signals_1 = MultiSignals(range_)
991 >>> multi_signals_2 = MultiSignals(range_)
992 >>> multi_signals_1 == multi_signals_2
993 True
994 >>> multi_signals_2[0] = 20
995 >>> multi_signals_1 == multi_signals_2
996 False
997 >>> multi_signals_2[0] = 10
998 >>> multi_signals_1 == multi_signals_2
999 True
1000 >>> from colour.algebra import CubicSplineInterpolator
1001 >>> multi_signals_2.interpolator = CubicSplineInterpolator
1002 >>> multi_signals_1 == multi_signals_2
1003 False
1004 """
1006 # NOTE: Comparing "interpolator_kwargs" and "extrapolator_kwargs" using
1007 # their string representation because of presence of NaNs.
1008 if isinstance(other, MultiSignals):
1009 return all(
1010 [
1011 np.array_equal(self.domain, other.domain),
1012 np.array_equal(self.range, other.range),
1013 self.interpolator is other.interpolator,
1014 str(self.interpolator_kwargs) == str(other.interpolator_kwargs),
1015 self.extrapolator is other.extrapolator,
1016 str(self.extrapolator_kwargs) == str(other.extrapolator_kwargs),
1017 self.labels == other.labels,
1018 ]
1019 )
1021 return False
1023 def __ne__(self, other: object) -> bool:
1024 """
1025 Determine whether the multi-signals is not equal to the
1026 specified object.
1028 Parameters
1029 ----------
1030 other
1031 Object to test whether it is not equal to the multi-signals.
1033 Returns
1034 -------
1035 :class:`bool`
1036 Whether the specified object is not equal to the multi-signals.
1038 Examples
1039 --------
1040 >>> range_ = np.linspace(10, 100, 10)
1041 >>> multi_signals_1 = MultiSignals(range_)
1042 >>> multi_signals_2 = MultiSignals(range_)
1043 >>> multi_signals_1 != multi_signals_2
1044 False
1045 >>> multi_signals_2[0] = 20
1046 >>> multi_signals_1 != multi_signals_2
1047 True
1048 >>> multi_signals_2[0] = 10
1049 >>> multi_signals_1 != multi_signals_2
1050 False
1051 >>> from colour.algebra import CubicSplineInterpolator
1052 >>> multi_signals_2.interpolator = CubicSplineInterpolator
1053 >>> multi_signals_1 != multi_signals_2
1054 True
1055 """
1057 return not (self == other)
1059 def arithmetical_operation(
1060 self,
1061 a: ArrayLike | AbstractContinuousFunction,
1062 operation: Literal["+", "-", "*", "/", "**"],
1063 in_place: bool = False,
1064 ) -> MultiSignals:
1065 """
1066 Perform the specified arithmetical operation with operand :math:`a`,
1067 either on a copy or in-place.
1069 Parameters
1070 ----------
1071 a
1072 Operand :math:`a`. Can be a numeric value, array-like object, or
1073 another continuous function instance.
1074 operation
1075 Operation to perform.
1076 in_place
1077 Operation happens in place.
1079 Returns
1080 -------
1081 :class:`colour.continuous.MultiSignals`
1082 Multi-signals.
1084 Examples
1085 --------
1086 Adding a single *numeric* variable:
1088 >>> domain = np.arange(0, 10, 1)
1089 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3)
1090 >>> range_ += np.array([0, 10, 20])
1091 >>> multi_signals_1 = MultiSignals(range_)
1092 >>> print(multi_signals_1)
1093 [[ 0. 10. 20. 30.]
1094 [ 1. 20. 30. 40.]
1095 [ 2. 30. 40. 50.]
1096 [ 3. 40. 50. 60.]
1097 [ 4. 50. 60. 70.]
1098 [ 5. 60. 70. 80.]
1099 [ 6. 70. 80. 90.]
1100 [ 7. 80. 90. 100.]
1101 [ 8. 90. 100. 110.]
1102 [ 9. 100. 110. 120.]]
1103 >>> print(multi_signals_1.arithmetical_operation(10, "+", True))
1104 [[ 0. 20. 30. 40.]
1105 [ 1. 30. 40. 50.]
1106 [ 2. 40. 50. 60.]
1107 [ 3. 50. 60. 70.]
1108 [ 4. 60. 70. 80.]
1109 [ 5. 70. 80. 90.]
1110 [ 6. 80. 90. 100.]
1111 [ 7. 90. 100. 110.]
1112 [ 8. 100. 110. 120.]
1113 [ 9. 110. 120. 130.]]
1115 Adding an `ArrayLike` variable:
1117 >>> a = np.linspace(10, 100, 10)
1118 >>> print(multi_signals_1.arithmetical_operation(a, "+", True))
1119 [[ 0. 30. 40. 50.]
1120 [ 1. 50. 60. 70.]
1121 [ 2. 70. 80. 90.]
1122 [ 3. 90. 100. 110.]
1123 [ 4. 110. 120. 130.]
1124 [ 5. 130. 140. 150.]
1125 [ 6. 150. 160. 170.]
1126 [ 7. 170. 180. 190.]
1127 [ 8. 190. 200. 210.]
1128 [ 9. 210. 220. 230.]]
1130 >>> a = np.array([[10, 20, 30]])
1131 >>> print(multi_signals_1.arithmetical_operation(a, "+", True))
1132 [[ 0. 40. 60. 80.]
1133 [ 1. 60. 80. 100.]
1134 [ 2. 80. 100. 120.]
1135 [ 3. 100. 120. 140.]
1136 [ 4. 120. 140. 160.]
1137 [ 5. 140. 160. 180.]
1138 [ 6. 160. 180. 200.]
1139 [ 7. 180. 200. 220.]
1140 [ 8. 200. 220. 240.]
1141 [ 9. 220. 240. 260.]]
1143 >>> a = np.reshape(np.arange(0, 30, 1), (10, 3))
1144 >>> print(multi_signals_1.arithmetical_operation(a, "+", True))
1145 [[ 0. 40. 61. 82.]
1146 [ 1. 63. 84. 105.]
1147 [ 2. 86. 107. 128.]
1148 [ 3. 109. 130. 151.]
1149 [ 4. 132. 153. 174.]
1150 [ 5. 155. 176. 197.]
1151 [ 6. 178. 199. 220.]
1152 [ 7. 201. 222. 243.]
1153 [ 8. 224. 245. 266.]
1154 [ 9. 247. 268. 289.]]
1156 Adding a :class:`colour.continuous.Signal` sub-class:
1158 >>> multi_signals_2 = MultiSignals(range_)
1159 >>> print(multi_signals_1.arithmetical_operation(multi_signals_2, "+", True))
1160 [[ 0. 50. 81. 112.]
1161 [ 1. 83. 114. 145.]
1162 [ 2. 116. 147. 178.]
1163 [ 3. 149. 180. 211.]
1164 [ 4. 182. 213. 244.]
1165 [ 5. 215. 246. 277.]
1166 [ 6. 248. 279. 310.]
1167 [ 7. 281. 312. 343.]
1168 [ 8. 314. 345. 376.]
1169 [ 9. 347. 378. 409.]]
1170 """
1172 multi_signals = self if in_place else self.copy()
1174 if isinstance(a, MultiSignals):
1175 attest(
1176 len(self.signals) == len(a.signals),
1177 '"MultiSignals" operands must have same count than '
1178 'underlying "Signal" components!',
1179 )
1181 for signal_a, signal_b in zip(
1182 multi_signals.signals.values(), a.signals.values(), strict=True
1183 ):
1184 signal_a.arithmetical_operation(signal_b, operation, True)
1185 else:
1186 a = as_float_array(cast("ArrayLike", a))
1188 attest(
1189 a.ndim in range(3),
1190 'Operand "a" variable must be a numeric or a 1-dimensional or '
1191 "2-dimensional array!",
1192 )
1194 if a.ndim in (0, 1):
1195 for signal in multi_signals.signals.values():
1196 signal.arithmetical_operation(a, operation, True)
1197 else:
1198 attest(
1199 a.shape[-1] == len(multi_signals.signals),
1200 'Operand "a" variable columns must have same count than '
1201 'underlying "Signal" components!',
1202 )
1204 for signal, y in zip(
1205 multi_signals.signals.values(), tsplit(a), strict=True
1206 ):
1207 signal.arithmetical_operation(y, operation, True)
1209 return multi_signals
1211 @staticmethod
1212 def multi_signals_unpack_data(
1213 data: (
1214 ArrayLike
1215 | DataFrame
1216 | dict
1217 | MultiSignals
1218 | Sequence
1219 | Series
1220 | Signal
1221 | ValuesView
1222 | None
1223 ) = None,
1224 domain: ArrayLike | KeysView | None = None,
1225 labels: Sequence | None = None,
1226 dtype: Type[DTypeFloat] | None = None,
1227 signal_type: Type[Signal] = Signal,
1228 **kwargs: Any,
1229 ) -> Dict[str, Signal]:
1230 """
1231 Unpack specified data for multi-signals instantiation.
1233 Parameters
1234 ----------
1235 data
1236 Data to unpack for multi-signals instantiation.
1237 domain
1238 Values to initialise the multiple :class:`colour.continuous.Signal`
1239 sub-class instances :attr:`colour.continuous.Signal.domain`
1240 attribute with. If both ``data`` and ``domain`` arguments are
1241 defined, the latter will be used to initialise the
1242 :attr:`colour.continuous.Signal.domain` property.
1243 labels
1244 Names to use for the :class:`colour.continuous.Signal` sub-class
1245 instances.
1246 dtype
1247 Floating point data type.
1248 signal_type
1249 A :class:`colour.continuous.Signal` sub-class type.
1251 Other Parameters
1252 ----------------
1253 extrapolator
1254 Extrapolator class type to use as extrapolating function for the
1255 :class:`colour.continuous.Signal` sub-class instances.
1256 extrapolator_kwargs
1257 Arguments to use when instantiating the extrapolating function
1258 of the :class:`colour.continuous.Signal` sub-class instances.
1259 interpolator
1260 Interpolator class type to use as interpolating function for the
1261 :class:`colour.continuous.Signal` sub-class instances.
1262 interpolator_kwargs
1263 Arguments to use when instantiating the interpolating function
1264 of the :class:`colour.continuous.Signal` sub-class instances.
1265 name
1266 Multi-signals name.
1268 Returns
1269 -------
1270 :class:`dict`
1271 Mapping of labeled :class:`colour.continuous.Signal` sub-class
1272 instances.
1274 Examples
1275 --------
1276 Unpacking using implicit *domain* and data for a single signal:
1278 >>> range_ = np.linspace(10, 100, 10)
1279 >>> signals = MultiSignals.multi_signals_unpack_data(range_)
1280 >>> list(signals.keys())
1281 ['0']
1282 >>> print(signals["0"])
1283 [[ 0. 10.]
1284 [ 1. 20.]
1285 [ 2. 30.]
1286 [ 3. 40.]
1287 [ 4. 50.]
1288 [ 5. 60.]
1289 [ 6. 70.]
1290 [ 7. 80.]
1291 [ 8. 90.]
1292 [ 9. 100.]]
1294 Unpacking using explicit *domain* and data for a single signal:
1296 >>> domain = np.arange(100, 1100, 100)
1297 >>> signals = MultiSignals.multi_signals_unpack_data(range_, domain)
1298 >>> list(signals.keys())
1299 ['0']
1300 >>> print(signals["0"])
1301 [[ 100. 10.]
1302 [ 200. 20.]
1303 [ 300. 30.]
1304 [ 400. 40.]
1305 [ 500. 50.]
1306 [ 600. 60.]
1307 [ 700. 70.]
1308 [ 800. 80.]
1309 [ 900. 90.]
1310 [ 1000. 100.]]
1312 Unpacking using data for multiple signals:
1314 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3)
1315 >>> range_ += np.array([0, 10, 20])
1316 >>> signals = MultiSignals.multi_signals_unpack_data(range_, domain)
1317 >>> list(signals.keys())
1318 ['0', '1', '2']
1319 >>> print(signals["2"])
1320 [[ 100. 30.]
1321 [ 200. 40.]
1322 [ 300. 50.]
1323 [ 400. 60.]
1324 [ 500. 70.]
1325 [ 600. 80.]
1326 [ 700. 90.]
1327 [ 800. 100.]
1328 [ 900. 110.]
1329 [ 1000. 120.]]
1331 Unpacking using a *dict*:
1333 >>> signals = MultiSignals.multi_signals_unpack_data(dict(zip(domain, range_)))
1334 >>> list(signals.keys())
1335 ['0', '1', '2']
1336 >>> print(signals["2"])
1337 [[ 100. 30.]
1338 [ 200. 40.]
1339 [ 300. 50.]
1340 [ 400. 60.]
1341 [ 500. 70.]
1342 [ 600. 80.]
1343 [ 700. 90.]
1344 [ 800. 100.]
1345 [ 900. 110.]
1346 [ 1000. 120.]]
1348 Unpacking using a sequence of *Signal* instances, note how the keys
1349 are :class:`str` instances because the *Signal* names are used:
1351 >>> signals = MultiSignals.multi_signals_unpack_data(
1352 ... dict(zip(domain, range_))
1353 ... ).values()
1354 >>> signals = MultiSignals.multi_signals_unpack_data(signals)
1355 >>> list(signals.keys())
1356 ['0', '1', '2']
1357 >>> print(signals["2"])
1358 [[ 100. 30.]
1359 [ 200. 40.]
1360 [ 300. 50.]
1361 [ 400. 60.]
1362 [ 500. 70.]
1363 [ 600. 80.]
1364 [ 700. 90.]
1365 [ 800. 100.]
1366 [ 900. 110.]
1367 [ 1000. 120.]]
1369 Unpacking using *MultiSignals.multi_signals_unpack_data* method output:
1371 >>> signals = MultiSignals.multi_signals_unpack_data(dict(zip(domain, range_)))
1372 >>> signals = MultiSignals.multi_signals_unpack_data(signals)
1373 >>> list(signals.keys())
1374 ['0', '1', '2']
1375 >>> print(signals["2"])
1376 [[ 100. 30.]
1377 [ 200. 40.]
1378 [ 300. 50.]
1379 [ 400. 60.]
1380 [ 500. 70.]
1381 [ 600. 80.]
1382 [ 700. 90.]
1383 [ 800. 100.]
1384 [ 900. 110.]
1385 [ 1000. 120.]]
1387 Unpacking using a *Pandas* `Series`:
1389 >>> if is_pandas_installed():
1390 ... from pandas import Series
1391 ...
1392 ... signals = MultiSignals.multi_signals_unpack_data(
1393 ... Series(dict(zip(domain, np.linspace(10, 100, 10))))
1394 ... )
1395 ... print(signals[0]) # doctest: +SKIP
1396 [[ 100. 10.]
1397 [ 200. 20.]
1398 [ 300. 30.]
1399 [ 400. 40.]
1400 [ 500. 50.]
1401 [ 600. 60.]
1402 [ 700. 70.]
1403 [ 800. 80.]
1404 [ 900. 90.]
1405 [ 1000. 100.]]
1407 Unpacking using a *Pandas* :class:`pandas.DataFrame`:
1409 >>> if is_pandas_installed():
1410 ... from pandas import DataFrame
1411 ...
1412 ... data = dict(zip(["a", "b", "c"], tsplit(range_)))
1413 ... signals = MultiSignals.multi_signals_unpack_data(
1414 ... DataFrame(data, domain)
1415 ... )
1416 ... print(signals["c"]) # doctest: +SKIP
1417 [[ 100. 30.]
1418 [ 200. 40.]
1419 [ 300. 50.]
1420 [ 400. 60.]
1421 [ 500. 70.]
1422 [ 600. 80.]
1423 [ 700. 90.]
1424 [ 800. 100.]
1425 [ 900. 110.]
1426 [ 1000. 120.]]
1427 """
1429 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1431 settings = {}
1432 settings.update(kwargs)
1433 settings.update({"dtype": dtype})
1435 # domain_unpacked, range_unpacked, signals = (
1436 # np.array([]), np.array([]), {})
1438 signals = {}
1440 if isinstance(data, Signal):
1441 signals[data.name] = data
1442 elif isinstance(data, MultiSignals):
1443 signals = data.signals
1444 elif issubclass(type(data), Sequence) or isinstance(
1445 data, (tuple, list, np.ndarray, Iterator, ValuesView)
1446 ):
1447 data_sequence = list(cast("Sequence", data))
1449 is_signal = True
1450 for i in data_sequence:
1451 if not isinstance(i, Signal):
1452 is_signal = False
1453 break
1455 if is_signal:
1456 for signal in data_sequence:
1457 signals[signal.name] = signal_type(
1458 signal.range, signal.domain, **settings
1459 )
1460 else:
1461 data_array = tsplit(data_sequence)
1462 attest(
1463 data_array.ndim in (1, 2),
1464 'User "data" must be 1-dimensional or 2-dimensional!',
1465 )
1467 if data_array.ndim == 1:
1468 data_array = data_array[None, :]
1470 for i, range_unpacked in enumerate(data_array):
1471 signals[str(i)] = signal_type(range_unpacked, domain, **settings)
1472 elif issubclass(type(data), Mapping) or isinstance(data, dict):
1473 data_mapping = dict(cast("Mapping", data))
1475 is_signal = all(isinstance(i, Signal) for i in data_mapping.values())
1477 if is_signal:
1478 for label, signal in data_mapping.items():
1479 signals[label] = signal_type(
1480 signal.range, signal.domain, **settings
1481 )
1482 else:
1483 domain_unpacked, range_unpacked = zip(
1484 *sorted(data_mapping.items()), strict=True
1485 )
1486 for i, values_unpacked in enumerate(tsplit(range_unpacked)):
1487 signals[str(i)] = signal_type(
1488 values_unpacked, domain_unpacked, **settings
1489 )
1490 elif is_pandas_installed():
1491 if isinstance(data, Series):
1492 signals["0"] = signal_type(data, **settings)
1493 elif isinstance(data, DataFrame):
1494 domain_unpacked = as_float_array(data.index.values, dtype) # pyright: ignore
1495 signals = {
1496 label: signal_type(
1497 data[label],
1498 domain_unpacked,
1499 **settings,
1500 )
1501 for label in data
1502 }
1504 if domain is not None:
1505 if isinstance(domain, KeysView):
1506 domain = list(domain)
1508 domain_array = as_float_array(domain, dtype)
1510 for signal in signals.values():
1511 attest(
1512 len(domain_array) == len(signal.domain),
1513 'User "domain" length is not compatible with unpacked "signals"!',
1514 )
1516 signal.domain = domain_array
1518 signals = {str(label): signal for label, signal in signals.items()}
1520 if labels is not None:
1521 attest(
1522 len(labels) == len(signals),
1523 'User "labels" length is not compatible with unpacked "signals"!',
1524 )
1526 if len(labels) != len(set(labels)):
1527 labels = [f"{label} - {i}" for i, label in enumerate(labels)]
1529 signals = {
1530 str(labels[i]): signal for i, signal in enumerate(signals.values())
1531 }
1533 for label in signals:
1534 signals[label].name = label
1536 if not signals:
1537 signals = {"Undefined": Signal(name="Undefined")}
1539 return signals
1541 def fill_nan(
1542 self,
1543 method: Literal["Constant", "Interpolation"] | str = "Interpolation",
1544 default: Real = 0,
1545 ) -> MultiSignals:
1546 """
1547 Fill NaNs in independent domain variable :math:`x` and corresponding
1548 range variable :math:`y` using the specified method.
1550 Parameters
1551 ----------
1552 method
1553 *Interpolation* method linearly interpolates through the NaNs,
1554 *Constant* method replaces NaNs with ``default``.
1555 default
1556 Value to use with the *Constant* method.
1558 Returns
1559 -------
1560 :class:`colour.continuous.MultiSignals`
1561 Multi-signals with NaN values filled.
1563 Examples
1564 --------
1565 >>> domain = np.arange(0, 10, 1)
1566 >>> range_ = tstack([np.linspace(10, 100, 10)] * 3)
1567 >>> range_ += np.array([0, 10, 20])
1568 >>> multi_signals = MultiSignals(range_)
1569 >>> multi_signals[3:7] = np.nan
1570 >>> print(multi_signals)
1571 [[ 0. 10. 20. 30.]
1572 [ 1. 20. 30. 40.]
1573 [ 2. 30. 40. 50.]
1574 [ 3. nan nan nan]
1575 [ 4. nan nan nan]
1576 [ 5. nan nan nan]
1577 [ 6. nan nan nan]
1578 [ 7. 80. 90. 100.]
1579 [ 8. 90. 100. 110.]
1580 [ 9. 100. 110. 120.]]
1581 >>> print(multi_signals.fill_nan())
1582 [[ 0. 10. 20. 30.]
1583 [ 1. 20. 30. 40.]
1584 [ 2. 30. 40. 50.]
1585 [ 3. 40. 50. 60.]
1586 [ 4. 50. 60. 70.]
1587 [ 5. 60. 70. 80.]
1588 [ 6. 70. 80. 90.]
1589 [ 7. 80. 90. 100.]
1590 [ 8. 90. 100. 110.]
1591 [ 9. 100. 110. 120.]]
1592 >>> multi_signals[3:7] = np.nan
1593 >>> print(multi_signals.fill_nan(method="Constant"))
1594 [[ 0. 10. 20. 30.]
1595 [ 1. 20. 30. 40.]
1596 [ 2. 30. 40. 50.]
1597 [ 3. 0. 0. 0.]
1598 [ 4. 0. 0. 0.]
1599 [ 5. 0. 0. 0.]
1600 [ 6. 0. 0. 0.]
1601 [ 7. 80. 90. 100.]
1602 [ 8. 90. 100. 110.]
1603 [ 9. 100. 110. 120.]]
1604 """
1606 method = validate_method(method, ("Interpolation", "Constant"))
1608 for signal in self._signals.values():
1609 signal.fill_nan(method, default)
1611 return self
1613 @required("Pandas")
1614 def to_dataframe(self) -> DataFrame:
1615 """
1616 Convert the continuous signal to a *Pandas* :class:`pandas.DataFrame`
1617 class instance.
1619 Returns
1620 -------
1621 :class:`pandas.DataFrame`
1622 Continuous signal as a *Pandas* :class:`pandas.DataFrame` class
1623 instance.
1625 Examples
1626 --------
1627 >>> if is_pandas_installed():
1628 ... domain = np.arange(0, 10, 1)
1629 ... range_ = tstack([np.linspace(10, 100, 10)] * 3)
1630 ... range_ += np.array([0, 10, 20])
1631 ... multi_signals = MultiSignals(range_)
1632 ... print(multi_signals.to_dataframe()) # doctest: +SKIP
1633 0 1 2
1634 0.0 10.0 20.0 30.0
1635 1.0 20.0 30.0 40.0
1636 2.0 30.0 40.0 50.0
1637 3.0 40.0 50.0 60.0
1638 4.0 50.0 60.0 70.0
1639 5.0 60.0 70.0 80.0
1640 6.0 70.0 80.0 90.0
1641 7.0 80.0 90.0 100.0
1642 8.0 90.0 100.0 110.0
1643 9.0 100.0 110.0 120.0
1644 """
1646 return DataFrame(
1647 data=self.range,
1648 index=self.domain,
1649 columns=self.labels, # pyright: ignore
1650 )