Coverage for colorimetry/spectrum.py: 66%
312 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"""
2Spectrum
3========
5Define classes and objects for handling spectral data computations.
7- :class:`colour.SPECTRAL_SHAPE_DEFAULT`
8- :class:`colour.SpectralShape`
9- :class:`colour.SpectralDistribution`
10- :class:`colour.MultiSpectralDistributions`
11- :func:`colour.colorimetry.sds_and_msds_to_sds`
12- :func:`colour.colorimetry.sds_and_msds_to_msds`
13- :func:`colour.colorimetry.reshape_sd`
14- :func:`colour.colorimetry.reshape_msds`
16References
17----------
18- :cite:`CIETC1-382005e` : CIE TC 1-38. (2005). 9. INTERPOLATION. In CIE
19 167:2005 Recommended Practice for Tabulating Spectral Data for Use in
20 Colour Computations (pp. 14-19). ISBN:978-3-901906-41-1
21- :cite:`CIETC1-382005g` : CIE TC 1-38. (2005). EXTRAPOLATION. In CIE
22 167:2005 Recommended Practice for Tabulating Spectral Data for Use in
23 Colour Computations (pp. 19-20). ISBN:978-3-901906-41-1
24- :cite:`CIETC1-482004l` : CIE TC 1-48. (2004). Extrapolation. In CIE
25 015:2004 Colorimetry, 3rd Edition (p. 24). ISBN:978-3-901906-33-6
26"""
28from __future__ import annotations
30import typing
31from collections.abc import KeysView, Mapping, ValuesView
33import numpy as np
35from colour.algebra import (
36 CubicSplineInterpolator,
37 Extrapolator,
38 SpragueInterpolator,
39 sdiv,
40 sdiv_mode,
41)
42from colour.constants import DTYPE_FLOAT_DEFAULT
43from colour.continuous import MultiSignals, Signal
45if typing.TYPE_CHECKING:
46 from colour.hints import (
47 ArrayLike,
48 DTypeFloat,
49 Generator,
50 List,
51 Literal,
52 NDArrayFloat,
53 ProtocolExtrapolator,
54 ProtocolInterpolator,
55 Real,
56 Self,
57 Sequence,
58 Type,
59 TypeVar,
60 )
62from colour.hints import Any, TypeVar, cast
63from colour.utilities import (
64 CACHE_REGISTRY,
65 as_float_array,
66 as_int,
67 attest,
68 filter_kwargs,
69 first_item,
70 interval,
71 is_caching_enabled,
72 is_iterable,
73 is_numeric,
74 is_pandas_installed,
75 is_uniform,
76 optional,
77 runtime_warning,
78 tstack,
79 validate_method,
80)
82if typing.TYPE_CHECKING or is_pandas_installed():
83 from pandas import DataFrame, Series # pragma: no cover
84else: # pragma: no cover
85 from unittest import mock
87 DataFrame = mock.MagicMock()
88 Series = mock.MagicMock()
90__author__ = "Colour Developers"
91__copyright__ = "Copyright 2013 Colour Developers"
92__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
93__maintainer__ = "Colour Developers"
94__email__ = "colour-developers@colour-science.org"
95__status__ = "Production"
97__all__ = [
98 "SpectralShape",
99 "SPECTRAL_SHAPE_DEFAULT",
100 "SpectralDistribution",
101 "MultiSpectralDistributions",
102 "reshape_sd",
103 "reshape_msds",
104 "sds_and_msds_to_sds",
105 "sds_and_msds_to_msds",
106]
108_CACHE_SHAPE_RANGE: dict = CACHE_REGISTRY.register_cache(
109 f"{__name__}._CACHE_SHAPE_RANGE"
110)
113class SpectralShape:
114 """
115 Define the base object for spectral distribution shape.
117 The :class:`colour.SpectralShape` class represents the shape of spectral
118 data by defining its wavelength range and sampling interval. It provides
119 a structured way to handle spectral data boundaries and generate
120 wavelength arrays for spectral computations.
122 Parameters
123 ----------
124 start
125 Wavelength :math:`\\lambda_{i}` range start in nm.
126 end
127 Wavelength :math:`\\lambda_{i}` range end in nm.
128 interval
129 Wavelength :math:`\\lambda_{i}` range interval.
131 Attributes
132 ----------
133 - :attr:`~colour.SpectralShape.start`
134 - :attr:`~colour.SpectralShape.end`
135 - :attr:`~colour.SpectralShape.interval`
136 - :attr:`~colour.SpectralShape.boundaries`
137 - :attr:`~colour.SpectralShape.wavelengths`
139 Methods
140 -------
141 - :meth:`~colour.SpectralShape.__init__`
142 - :meth:`~colour.SpectralShape.__str__`
143 - :meth:`~colour.SpectralShape.__repr__`
144 - :meth:`~colour.SpectralShape.__hash__`
145 - :meth:`~colour.SpectralShape.__iter__`
146 - :meth:`~colour.SpectralShape.__contains__`
147 - :meth:`~colour.SpectralShape.__len__`
148 - :meth:`~colour.SpectralShape.__eq__`
149 - :meth:`~colour.SpectralShape.__ne__`
150 - :meth:`~colour.SpectralShape.range`
152 Examples
153 --------
154 >>> SpectralShape(360, 830, 1)
155 SpectralShape(360, 830, 1)
156 """
158 def __init__(self, start: Real, end: Real, interval: Real) -> None:
159 self._start: Real = 0
160 self._end: Real = np.inf
161 self._interval: Real = 1
162 self.start = start
163 self.end = end
164 self.interval = interval
166 @property
167 def start(self) -> Real:
168 """
169 Getter and setter for the spectral shape start.
171 Parameters
172 ----------
173 value
174 Value to set the spectral shape start wavelength with.
176 Returns
177 -------
178 Real
179 Start wavelength of the spectral shape in nanometres.
180 """
182 return self._start
184 @start.setter
185 def start(self, value: Real) -> None:
186 """Setter for the **self.start** property."""
188 attest(
189 is_numeric(value),
190 f'"start" property: "{value}" is not a "number"!',
191 )
193 attest(
194 bool(value < self._end),
195 f'"start" attribute value must be strictly less than "{self._end}"!',
196 )
198 self._start = value
200 @property
201 def end(self) -> Real:
202 """
203 Getter and setter for the spectral shape end.
205 Parameters
206 ----------
207 value
208 Value to set the spectral shape end wavelength with.
210 Returns
211 -------
212 Real
213 End wavelength of the spectral shape in nanometres.
214 .
215 """
217 return self._end
219 @end.setter
220 def end(self, value: Real) -> None:
221 """Setter for the **self.end** property."""
223 attest(
224 is_numeric(value),
225 f'"end" property: "{value}" is not a "number"!',
226 )
228 attest(
229 bool(value > self._start),
230 f'"end" attribute value must be strictly greater than "{self._start}"!',
231 )
233 self._end = value
235 @property
236 def interval(self) -> Real:
237 """
238 Getter and setter for the spectral shape interval.
240 The interval defines the wavelength spacing between consecutive
241 samples in the spectral distribution.
243 Parameters
244 ----------
245 value
246 Value to set the spectral shape interval with.
248 Returns
249 -------
250 Real
251 Spectral shape interval.
252 """
254 return self._interval
256 @interval.setter
257 def interval(self, value: Real) -> None:
258 """Setter for the **self.interval** property."""
260 attest(
261 is_numeric(value),
262 f'"interval" property: "{value}" is not a "number"!',
263 )
265 self._interval = value
267 @property
268 def boundaries(self) -> tuple:
269 """
270 Getter and setter for the boundaries of the spectral shape.
272 The boundaries define the start and end points of the spectral
273 range as a tuple of two values.
275 Parameters
276 ----------
277 value
278 Value to set the spectral shape boundaries with.
280 Returns
281 -------
282 :class:`tuple`
283 Spectral shape boundaries.
284 """
286 return self._start, self._end
288 @boundaries.setter
289 def boundaries(self, value: ArrayLike) -> None:
290 """Setter for the **self.boundaries** property."""
292 value = np.asarray(value)
294 attest(
295 value.size == 2,
296 f'"boundaries" property: "{value}" must have exactly two elements!',
297 )
299 self.start, self.end = value
301 @property
302 def wavelengths(self) -> NDArrayFloat:
303 """
304 Getter for the spectral shape wavelengths.
306 Returns
307 -------
308 :class:`numpy.ndarray`
309 Spectral shape wavelengths.
310 """
312 return self.range()
314 def __str__(self) -> str:
315 """
316 Return a formatted string representation of the spectral shape.
318 Returns
319 -------
320 :class:`str`
321 Formatted string representation.
322 """
324 return f"({self._start}, {self._end}, {self._interval})"
326 def __repr__(self) -> str:
327 """
328 Return an evaluable string representation of the spectral shape.
330 Returns
331 -------
332 :class:`str`
333 Evaluable string representation.
334 """
336 return f"SpectralShape({self._start}, {self._end}, {self._interval})"
338 def __hash__(self) -> int:
339 """
340 Return the hash value of the spectral shape.
342 The hash is computed based on the spectral shape's start wavelength,
343 end wavelength, and wavelength interval.
345 Returns
346 -------
347 :class:`int`
348 Hash value of the spectral shape.
349 """
351 return hash((self.start, self.end, self.interval))
353 def __iter__(self) -> Generator:
354 """
355 Generate wavelengths for the spectral shape range.
357 Yields
358 ------
359 Generator
360 Wavelength values from start to end at the specified interval.
362 Examples
363 --------
364 >>> shape = SpectralShape(0, 10, 1)
365 >>> for wavelength in shape:
366 ... print(wavelength)
367 0.0
368 1.0
369 2.0
370 3.0
371 4.0
372 5.0
373 6.0
374 7.0
375 8.0
376 9.0
377 10.0
378 """
380 yield from self.wavelengths
382 def __contains__(self, wavelength: ArrayLike) -> bool:
383 """
384 Determine if the spectral shape contains the specified wavelength
385 :math:`\\lambda`.
387 Parameters
388 ----------
389 wavelength
390 Wavelength :math:`\\lambda` to check for containment.
392 Returns
393 -------
394 :class:`bool`
395 Whether the wavelength :math:`\\lambda` is contained within the
396 spectral shape.
398 Examples
399 --------
400 >>> 0.5 in SpectralShape(0, 10, 0.1)
401 True
402 >>> 0.6 in SpectralShape(0, 10, 0.1)
403 True
404 >>> 0.51 in SpectralShape(0, 10, 0.1)
405 False
406 >>> np.array([0.5, 0.6]) in SpectralShape(0, 10, 0.1)
407 True
408 >>> np.array([0.51, 0.6]) in SpectralShape(0, 10, 0.1)
409 False
410 """
412 decimals = np.finfo(cast("Any", DTYPE_FLOAT_DEFAULT)).precision
414 return bool(
415 np.all(
416 np.isin(
417 np.around(
418 wavelength, # pyright: ignore
419 decimals,
420 ),
421 np.around(
422 self.wavelengths,
423 decimals,
424 ),
425 )
426 )
427 )
429 def __len__(self) -> int:
430 """
431 Return the spectral shape wavelength :math:`\\lambda_n` count.
433 Returns
434 -------
435 :class:`int`
436 Spectral shape wavelength :math:`\\lambda_n` count.
438 Examples
439 --------
440 >>> len(SpectralShape(0, 10, 0.1))
441 101
442 """
444 return len(self.wavelengths)
446 def __eq__(self, other: object) -> bool:
447 """
448 Determine whether the spectral shape is equal to the specified other
449 object.
451 Parameters
452 ----------
453 other
454 Object to determine whether it is equal to the spectral shape.
456 Returns
457 -------
458 :class:`bool`
459 Whether the specified object is equal to the spectral shape.
461 Examples
462 --------
463 >>> SpectralShape(0, 10, 0.1) == SpectralShape(0, 10, 0.1)
464 True
465 >>> SpectralShape(0, 10, 0.1) == SpectralShape(0, 10, 1)
466 False
467 """
469 if isinstance(other, SpectralShape):
470 return np.array_equal(self.wavelengths, other.wavelengths)
472 return False
474 def __ne__(self, other: object) -> bool:
475 """
476 Determine whether the spectral shape is not equal to the specified
477 other object.
479 Parameters
480 ----------
481 other
482 Object to determine whether it is not equal to the spectral
483 shape.
485 Returns
486 -------
487 :class:`bool`
488 Whether the specified object is not equal to the spectral
489 shape.
491 Examples
492 --------
493 >>> SpectralShape(0, 10, 0.1) != SpectralShape(0, 10, 0.1)
494 False
495 >>> SpectralShape(0, 10, 0.1) != SpectralShape(0, 10, 1)
496 True
497 """
499 return not (self == other)
501 def range(self, dtype: Type[DTypeFloat] | None = None) -> NDArrayFloat:
502 """
503 Return an iterable range for the spectral shape.
505 Parameters
506 ----------
507 dtype
508 Data type used to generate the range.
510 Returns
511 -------
512 :class:`numpy.ndarray`
513 Iterable range for the spectral distribution shape.
515 Examples
516 --------
517 >>> SpectralShape(0, 10, 0.1).wavelengths
518 array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8,
519 0.9, 1. , 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7,
520 1.8, 1.9, 2. , 2.1, 2.2, 2.3, 2.4, 2.5, 2.6,
521 2.7, 2.8, 2.9, 3. , 3.1, 3.2, 3.3, 3.4, 3.5,
522 3.6, 3.7, 3.8, 3.9, 4. , 4.1, 4.2, 4.3, 4.4,
523 4.5, 4.6, 4.7, 4.8, 4.9, 5. , 5.1, 5.2, 5.3,
524 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 6. , 6.1, 6.2,
525 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 7. , 7.1,
526 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8, 7.9, 8. ,
527 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9,
528 9. , 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8,
529 9.9, 10. ])
530 """
532 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
534 hash_key = hash((self, dtype))
536 if is_caching_enabled() and hash_key in _CACHE_SHAPE_RANGE:
537 return _CACHE_SHAPE_RANGE[hash_key].copy()
539 start, end, interval = (
540 dtype(self._start),
541 dtype(self._end),
542 dtype(self._interval),
543 )
545 samples = as_int(round((interval + end - start) / interval))
546 range_, interval_effective = np.linspace(
547 start, end, samples, retstep=True, dtype=dtype
548 )
550 _CACHE_SHAPE_RANGE[hash_key] = range_
552 if interval_effective != self._interval:
553 self._interval = cast("float", interval_effective)
554 runtime_warning(
555 f'"{(start, end, interval)}" shape could not be honoured, '
556 f'using "{self}"!'
557 )
559 return range_
562SPECTRAL_SHAPE_DEFAULT: SpectralShape = SpectralShape(360, 780, 1)
563"""Default spectral shape according to *ASTM E308-15* practise shape."""
566class SpectralDistribution(Signal):
567 """
568 Define the spectral distribution: the base object for spectral
569 computations.
571 Initialise spectral distribution according to *CIE 15:2004* recommendation:
572 use the method developed by *Sprague (1880)* for interpolating functions
573 having uniformly spaced independent variables and the *Cubic Spline* method
574 for non-uniformly spaced independent variables. Perform extrapolation
575 according to *CIE 167:2005* recommendation.
577 .. important::
579 Specific documentation about getting, setting, indexing and slicing
580 the spectral power distribution values is available in the
581 :ref:`spectral-representation-and-continuous-signal` section.
583 Parameters
584 ----------
585 data
586 Data to be stored in the spectral distribution.
587 domain
588 Values to initialise the
589 :attr:`colour.SpectralDistribution.wavelength` property with.
590 If both ``data`` and ``domain`` arguments are defined, the latter
591 will be used to initialise the
592 :attr:`colour.SpectralDistribution.wavelength` property.
594 Other Parameters
595 ----------------
596 extrapolator
597 Extrapolator class type to use as extrapolating function.
598 extrapolator_kwargs
599 Arguments to use when instantiating the extrapolating function.
600 interpolator
601 Interpolator class type to use as interpolating function.
602 interpolator_kwargs
603 Arguments to use when instantiating the interpolating function.
604 name
605 Spectral distribution name.
606 display_name
607 Spectral distribution name for figures, default to
608 :attr:`colour.SpectralDistribution.name` property value.
610 Warnings
611 --------
612 The *Cubic Spline* method might produce unexpected results with
613 exceptionally noisy or non-uniformly spaced data.
615 Attributes
616 ----------
617 - :attr:`~colour.SpectralDistribution.display_name`
618 - :attr:`~colour.SpectralDistribution.wavelengths`
619 - :attr:`~colour.SpectralDistribution.values`
620 - :attr:`~colour.SpectralDistribution.shape`
622 Methods
623 -------
624 - :meth:`~colour.SpectralDistribution.__init__`
625 - :meth:`~colour.SpectralDistribution.interpolate`
626 - :meth:`~colour.SpectralDistribution.extrapolate`
627 - :meth:`~colour.SpectralDistribution.align`
628 - :meth:`~colour.SpectralDistribution.trim`
629 - :meth:`~colour.SpectralDistribution.normalise`
631 References
632 ----------
633 :cite:`CIETC1-382005e`, :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l`
635 Examples
636 --------
637 Instantiating a spectral distribution with a uniformly spaced independent
638 variable:
640 >>> from colour.utilities import numpy_print_options
641 >>> data = {
642 ... 500: 0.0651,
643 ... 520: 0.0705,
644 ... 540: 0.0772,
645 ... 560: 0.0870,
646 ... 580: 0.1128,
647 ... 600: 0.1360,
648 ... }
649 >>> with numpy_print_options(suppress=True):
650 ... SpectralDistribution(data) # doctest: +ELLIPSIS
651 SpectralDistribution([[ 500. , 0.0651],
652 [ 520. , 0.0705],
653 [ 540. , 0.0772],
654 [ 560. , 0.087 ],
655 [ 580. , 0.1128],
656 [ 600. , 0.136 ]],
657 SpragueInterpolator,
658 {},
659 Extrapolator,
660 {'method': 'Constant', 'left': None, 'right': None})
662 Instantiating a spectral distribution with a non-uniformly spaced
663 independent variable:
665 >>> data[510] = 0.31416
666 >>> with numpy_print_options(suppress=True):
667 ... SpectralDistribution(data) # doctest: +ELLIPSIS
668 SpectralDistribution([[ 500. , 0.0651 ],
669 [ 510. , 0.31416],
670 [ 520. , 0.0705 ],
671 [ 540. , 0.0772 ],
672 [ 560. , 0.087 ],
673 [ 580. , 0.1128 ],
674 [ 600. , 0.136 ]],
675 CubicSplineInterpolator,
676 {},
677 Extrapolator,
678 {'method': 'Constant', 'left': None, 'right': None})
680 Instantiation with a *Pandas* :class:`pandas.Series`:
682 >>> from colour.utilities import is_pandas_installed
683 >>> if is_pandas_installed():
684 ... from pandas import Series
685 ...
686 ... print(SpectralDistribution(Series(data))) # doctest: +SKIP
687 [[ 5.0000000...e+02 6.5100000...e-02]
688 [ 5.2000000...e+02 7.0500000...e-02]
689 [ 5.4000000...e+02 7.7200000...e-02]
690 [ 5.6000000...e+02 8.7000000...e-02]
691 [ 5.8000000...e+02 1.1280000...e-01]
692 [ 6.0000000...e+02 1.3600000...e-01]
693 [ 5.1000000...e+02 3.1416000...e-01]]
694 """
696 def __init__(
697 self,
698 data: ArrayLike | dict | Series | Signal | ValuesView | None = None,
699 domain: ArrayLike | SpectralShape | KeysView | None = None,
700 **kwargs: Any,
701 ) -> None:
702 domain = domain.wavelengths if isinstance(domain, SpectralShape) else domain
704 domain_unpacked, range_unpacked = self.signal_unpack_data(data, domain)
706 # Initialising with *CIE 15:2004* and *CIE 167:2005* recommendations
707 # defaults.
708 kwargs["interpolator"] = kwargs.get(
709 "interpolator",
710 (
711 SpragueInterpolator
712 if domain_unpacked.size != 0 and is_uniform(domain_unpacked)
713 else CubicSplineInterpolator
714 ),
715 )
716 kwargs["interpolator_kwargs"] = kwargs.get("interpolator_kwargs", {})
718 kwargs["extrapolator"] = kwargs.get("extrapolator", Extrapolator)
719 kwargs["extrapolator_kwargs"] = kwargs.get(
720 "extrapolator_kwargs",
721 {"method": "Constant", "left": None, "right": None},
722 )
723 super().__init__(range_unpacked, domain_unpacked, **kwargs)
725 self._display_name: str = self.name
726 self.display_name = kwargs.get("display_name", self._display_name)
728 self._shape: SpectralShape | None = None
730 self.register_callback("_domain", "on_domain_changed", self._on_domain_changed)
732 @staticmethod
733 def _on_domain_changed(
734 sd: SpectralDistribution, name: str, value: NDArrayFloat
735 ) -> NDArrayFloat:
736 """
737 Invalidate the cached spectral shape when the spectral
738 distribution domain is modified.
740 This callback ensures that the internal *_shape* attribute is reset
741 to *None* whenever the domain values change, maintaining consistency
742 between the domain and its derived shape representation.
744 Parameters
745 ----------
746 sd
747 Spectral distribution instance whose domain has changed.
748 name
749 Name of the modified attribute (expected to be "_domain").
750 value
751 New domain values that triggered the callback.
753 Returns
754 -------
755 :class:`numpy.ndarray`
756 The specified domain values, unchanged.
757 """
759 if name == "_domain":
760 sd._shape = None
762 return value
764 @property
765 def display_name(self) -> str:
766 """
767 Getter and setter for the spectral distribution's display name.
769 The display name provides a human-readable identifier for the
770 spectral distribution, used for visualization and reporting purposes.
772 Parameters
773 ----------
774 value
775 Value to set the spectral distribution's display name
776 with.
778 Returns
779 -------
780 :class:`str`
781 Spectral distribution's display name.
782 """
784 return self._display_name
786 @display_name.setter
787 def display_name(self, value: str) -> None:
788 """Setter for the **self.display_name** property."""
790 attest(
791 isinstance(value, str),
792 f'"display_name" property: "{value}" type is not "str"!',
793 )
795 self._display_name = value
797 @property
798 def wavelengths(self) -> NDArrayFloat:
799 """
800 Getter and setter for the spectral distribution wavelengths
801 :math:`\\lambda_n`.
803 Parameters
804 ----------
805 value
806 Value to set the spectral distribution wavelengths
807 :math:`\\lambda_n` with.
809 Returns
810 -------
811 :class:`numpy.ndarray`
812 Spectral distribution wavelengths :math:`\\lambda_n`.
813 """
815 return self.domain
817 @wavelengths.setter
818 def wavelengths(self, value: ArrayLike) -> None:
819 """Setter for the **self.wavelengths** property."""
821 self.domain = as_float_array(value, self.dtype)
823 @property
824 def values(self) -> NDArrayFloat:
825 """
826 Getter and setter for the spectral distribution values.
828 Parameters
829 ----------
830 value
831 Value to set the spectral distribution wavelengths values with.
833 Returns
834 -------
835 :class:`numpy.ndarray`
836 Spectral distribution values.
837 """
839 return self.range
841 @values.setter
842 def values(self, value: ArrayLike) -> None:
843 """Setter for the **self.values** property."""
845 self.range = as_float_array(value, self.dtype)
847 @property
848 def shape(self) -> SpectralShape:
849 """
850 Getter property for the spectral distribution shape.
852 Returns
853 -------
854 :class:`colour.SpectralShape`
855 Spectral distribution shape.
857 Notes
858 -----
859 - A spectral distribution with a non-uniformly spaced independent
860 variable have multiple intervals, in that case
861 :attr:`colour.SpectralDistribution.shape` property returns
862 the *minimum* interval size.
864 Examples
865 --------
866 Shape of a spectral distribution with a uniformly spaced independent
867 variable:
869 >>> data = {
870 ... 500: 0.0651,
871 ... 520: 0.0705,
872 ... 540: 0.0772,
873 ... 560: 0.0870,
874 ... 580: 0.1128,
875 ... 600: 0.1360,
876 ... }
877 >>> SpectralDistribution(data).shape
878 SpectralShape(500.0, 600.0, 20.0)
880 Shape of a spectral distribution with a non-uniformly spaced
881 independent variable:
883 >>> data[510] = 0.31416
884 >>> SpectralDistribution(data).shape
885 SpectralShape(500.0, 600.0, 10.0)
886 """
888 if self._shape is None:
889 wavelengths = self.wavelengths
890 wavelengths_interval = interval(wavelengths)
891 if wavelengths_interval.size != 1:
892 runtime_warning(
893 f'"{self.name}" spectral distribution is not uniform, '
894 "using minimum interval!"
895 )
897 self._shape = SpectralShape(
898 wavelengths[0], wavelengths[-1], min(wavelengths_interval)
899 )
901 return self._shape
903 def interpolate(
904 self,
905 shape: SpectralShape,
906 interpolator: Type[ProtocolInterpolator] | None = None,
907 interpolator_kwargs: dict | None = None,
908 ) -> Self:
909 """
910 Interpolate the spectral distribution in-place according to
911 *CIE 167:2005* recommendation (if the interpolator has not been
912 changed at instantiation time) or specified interpolation arguments.
914 The logic for choosing the interpolator class when ``interpolator`` is
915 not specified is as follows:
917 .. code-block:: python
919 if self.interpolator not in (
920 SpragueInterpolator,
921 CubicSplineInterpolator,
922 ):
923 interpolator = self.interpolator
924 elif self.is_uniform():
925 interpolator = SpragueInterpolator
926 else:
927 interpolator = CubicSplineInterpolator
929 The logic for choosing the interpolator keyword arguments when
930 ``interpolator_kwargs`` is not specified is as follows:
932 .. code-block:: python
934 if self.interpolator not in (
935 SpragueInterpolator,
936 CubicSplineInterpolator,
937 ):
938 interpolator_kwargs = self.interpolator_kwargs
939 else:
940 interpolator_kwargs = {}
942 Parameters
943 ----------
944 shape
945 Spectral shape used for interpolation.
946 interpolator
947 Interpolator class type to use as interpolating function.
948 interpolator_kwargs
949 Arguments to use when instantiating the interpolating function.
951 Returns
952 -------
953 :class:`colour.SpectralDistribution`
954 Interpolated spectral distribution.
956 Notes
957 -----
958 - Interpolation will be performed over boundaries range, if it is
959 required to extend the range of the spectral distribution use the
960 :meth:`colour.SpectralDistribution.extrapolate` or
961 :meth:`colour.SpectralDistribution.align` methods.
963 Warnings
964 --------
965 - *Cubic Spline* interpolator requires at least 3 wavelengths
966 :math:`\\lambda_n` for interpolation.
967 - *Sprague (1880)* interpolator requires at least 6 wavelengths
968 :math:`\\lambda_n` for interpolation.
970 References
971 ----------
972 :cite:`CIETC1-382005e`
974 Examples
975 --------
976 Spectral distribution with a uniformly spaced independent variable
977 uses *Sprague (1880)* interpolation:
979 >>> from colour.utilities import numpy_print_options
980 >>> data = {
981 ... 500: 0.0651,
982 ... 520: 0.0705,
983 ... 540: 0.0772,
984 ... 560: 0.0870,
985 ... 580: 0.1128,
986 ... 600: 0.1360,
987 ... }
988 >>> sd = SpectralDistribution(data)
989 >>> with numpy_print_options(suppress=True):
990 ... print(sd.interpolate(SpectralShape(500, 600, 1)))
991 ... # doctest: +ELLIPSIS
992 [[ 500. 0.0651 ...]
993 [ 501. 0.0653522...]
994 [ 502. 0.0656105...]
995 [ 503. 0.0658715...]
996 [ 504. 0.0661328...]
997 [ 505. 0.0663929...]
998 [ 506. 0.0666509...]
999 [ 507. 0.0669069...]
1000 [ 508. 0.0671613...]
1001 [ 509. 0.0674150...]
1002 [ 510. 0.0676692...]
1003 [ 511. 0.0679253...]
1004 [ 512. 0.0681848...]
1005 [ 513. 0.0684491...]
1006 [ 514. 0.0687197...]
1007 [ 515. 0.0689975...]
1008 [ 516. 0.0692832...]
1009 [ 517. 0.0695771...]
1010 [ 518. 0.0698787...]
1011 [ 519. 0.0701870...]
1012 [ 520. 0.0705 ...]
1013 [ 521. 0.0708155...]
1014 [ 522. 0.0711336...]
1015 [ 523. 0.0714547...]
1016 [ 524. 0.0717789...]
1017 [ 525. 0.0721063...]
1018 [ 526. 0.0724367...]
1019 [ 527. 0.0727698...]
1020 [ 528. 0.0731051...]
1021 [ 529. 0.0734423...]
1022 [ 530. 0.0737808...]
1023 [ 531. 0.0741203...]
1024 [ 532. 0.0744603...]
1025 [ 533. 0.0748006...]
1026 [ 534. 0.0751409...]
1027 [ 535. 0.0754813...]
1028 [ 536. 0.0758220...]
1029 [ 537. 0.0761633...]
1030 [ 538. 0.0765060...]
1031 [ 539. 0.0768511...]
1032 [ 540. 0.0772 ...]
1033 [ 541. 0.0775527...]
1034 [ 542. 0.0779042...]
1035 [ 543. 0.0782507...]
1036 [ 544. 0.0785908...]
1037 [ 545. 0.0789255...]
1038 [ 546. 0.0792576...]
1039 [ 547. 0.0795917...]
1040 [ 548. 0.0799334...]
1041 [ 549. 0.0802895...]
1042 [ 550. 0.0806671...]
1043 [ 551. 0.0810740...]
1044 [ 552. 0.0815176...]
1045 [ 553. 0.0820049...]
1046 [ 554. 0.0825423...]
1047 [ 555. 0.0831351...]
1048 [ 556. 0.0837873...]
1049 [ 557. 0.0845010...]
1050 [ 558. 0.0852763...]
1051 [ 559. 0.0861110...]
1052 [ 560. 0.087 ...]
1053 [ 561. 0.0879383...]
1054 [ 562. 0.0889300...]
1055 [ 563. 0.0899793...]
1056 [ 564. 0.0910876...]
1057 [ 565. 0.0922541...]
1058 [ 566. 0.0934760...]
1059 [ 567. 0.0947487...]
1060 [ 568. 0.0960663...]
1061 [ 569. 0.0974220...]
1062 [ 570. 0.0988081...]
1063 [ 571. 0.1002166...]
1064 [ 572. 0.1016394...]
1065 [ 573. 0.1030687...]
1066 [ 574. 0.1044972...]
1067 [ 575. 0.1059186...]
1068 [ 576. 0.1073277...]
1069 [ 577. 0.1087210...]
1070 [ 578. 0.1100968...]
1071 [ 579. 0.1114554...]
1072 [ 580. 0.1128 ...]
1073 [ 581. 0.1141333...]
1074 [ 582. 0.1154495...]
1075 [ 583. 0.1167424...]
1076 [ 584. 0.1180082...]
1077 [ 585. 0.1192452...]
1078 [ 586. 0.1204536...]
1079 [ 587. 0.1216348...]
1080 [ 588. 0.1227915...]
1081 [ 589. 0.1239274...]
1082 [ 590. 0.1250465...]
1083 [ 591. 0.1261531...]
1084 [ 592. 0.1272517...]
1085 [ 593. 0.1283460...]
1086 [ 594. 0.1294393...]
1087 [ 595. 0.1305340...]
1088 [ 596. 0.1316310...]
1089 [ 597. 0.1327297...]
1090 [ 598. 0.1338277...]
1091 [ 599. 0.1349201...]
1092 [ 600. 0.136 ...]]
1094 Spectral distribution with a non-uniformly spaced independent
1095 variable uses *Cubic Spline* interpolation:
1097 >>> sd = SpectralDistribution(data)
1098 >>> sd[510] = np.pi / 10
1099 >>> with numpy_print_options(suppress=True):
1100 ... print(sd.interpolate(SpectralShape(500, 600, 1)))
1101 ... # doctest: +ELLIPSIS
1102 [[ 500. 0.0651 ...]
1103 [ 501. 0.1365202...]
1104 [ 502. 0.1953263...]
1105 [ 503. 0.2423724...]
1106 [ 504. 0.2785126...]
1107 [ 505. 0.3046010...]
1108 [ 506. 0.3214916...]
1109 [ 507. 0.3300387...]
1110 [ 508. 0.3310962...]
1111 [ 509. 0.3255184...]
1112 [ 510. 0.3141592...]
1113 [ 511. 0.2978729...]
1114 [ 512. 0.2775135...]
1115 [ 513. 0.2539351...]
1116 [ 514. 0.2279918...]
1117 [ 515. 0.2005378...]
1118 [ 516. 0.1724271...]
1119 [ 517. 0.1445139...]
1120 [ 518. 0.1176522...]
1121 [ 519. 0.0926962...]
1122 [ 520. 0.0705 ...]
1123 [ 521. 0.0517370...]
1124 [ 522. 0.0363589...]
1125 [ 523. 0.0241365...]
1126 [ 524. 0.0148407...]
1127 [ 525. 0.0082424...]
1128 [ 526. 0.0041126...]
1129 [ 527. 0.0022222...]
1130 [ 528. 0.0023421...]
1131 [ 529. 0.0042433...]
1132 [ 530. 0.0076966...]
1133 [ 531. 0.0124729...]
1134 [ 532. 0.0183432...]
1135 [ 533. 0.0250785...]
1136 [ 534. 0.0324496...]
1137 [ 535. 0.0402274...]
1138 [ 536. 0.0481829...]
1139 [ 537. 0.0560870...]
1140 [ 538. 0.0637106...]
1141 [ 539. 0.0708246...]
1142 [ 540. 0.0772 ...]
1143 [ 541. 0.0826564...]
1144 [ 542. 0.0872086...]
1145 [ 543. 0.0909203...]
1146 [ 544. 0.0938549...]
1147 [ 545. 0.0960760...]
1148 [ 546. 0.0976472...]
1149 [ 547. 0.0986321...]
1150 [ 548. 0.0990942...]
1151 [ 549. 0.0990971...]
1152 [ 550. 0.0987043...]
1153 [ 551. 0.0979794...]
1154 [ 552. 0.0969861...]
1155 [ 553. 0.0957877...]
1156 [ 554. 0.0944480...]
1157 [ 555. 0.0930304...]
1158 [ 556. 0.0915986...]
1159 [ 557. 0.0902161...]
1160 [ 558. 0.0889464...]
1161 [ 559. 0.0878532...]
1162 [ 560. 0.087 ...]
1163 [ 561. 0.0864371...]
1164 [ 562. 0.0861623...]
1165 [ 563. 0.0861600...]
1166 [ 564. 0.0864148...]
1167 [ 565. 0.0869112...]
1168 [ 566. 0.0876336...]
1169 [ 567. 0.0885665...]
1170 [ 568. 0.0896945...]
1171 [ 569. 0.0910020...]
1172 [ 570. 0.0924735...]
1173 [ 571. 0.0940936...]
1174 [ 572. 0.0958467...]
1175 [ 573. 0.0977173...]
1176 [ 574. 0.0996899...]
1177 [ 575. 0.1017491...]
1178 [ 576. 0.1038792...]
1179 [ 577. 0.1060649...]
1180 [ 578. 0.1082906...]
1181 [ 579. 0.1105408...]
1182 [ 580. 0.1128 ...]
1183 [ 581. 0.1150526...]
1184 [ 582. 0.1172833...]
1185 [ 583. 0.1194765...]
1186 [ 584. 0.1216167...]
1187 [ 585. 0.1236884...]
1188 [ 586. 0.1256760...]
1189 [ 587. 0.1275641...]
1190 [ 588. 0.1293373...]
1191 [ 589. 0.1309798...]
1192 [ 590. 0.1324764...]
1193 [ 591. 0.1338114...]
1194 [ 592. 0.1349694...]
1195 [ 593. 0.1359349...]
1196 [ 594. 0.1366923...]
1197 [ 595. 0.1372262...]
1198 [ 596. 0.1375211...]
1199 [ 597. 0.1375614...]
1200 [ 598. 0.1373316...]
1201 [ 599. 0.1368163...]
1202 [ 600. 0.136 ...]]
1203 """
1205 shape_start, shape_end, shape_interval = as_float_array(
1206 [
1207 self.shape.start,
1208 self.shape.end,
1209 self.shape.interval,
1210 ]
1211 )
1213 shape = SpectralShape(
1214 *[
1215 x[0] if x[0] is not None else x[1]
1216 for x in zip(
1217 (shape.start, shape.end, shape.interval),
1218 (shape_start, shape_end, shape_interval),
1219 strict=True,
1220 )
1221 ]
1222 )
1224 shape.start = max([shape.start, shape_start])
1225 shape.end = min([shape.end, shape_end])
1227 if interpolator is None:
1228 if self.interpolator not in (
1229 SpragueInterpolator,
1230 CubicSplineInterpolator,
1231 ):
1232 interpolator = self.interpolator
1233 elif self.is_uniform():
1234 interpolator = SpragueInterpolator
1235 else:
1236 interpolator = CubicSplineInterpolator
1238 if interpolator_kwargs is None:
1239 if self.interpolator not in (
1240 SpragueInterpolator,
1241 CubicSplineInterpolator,
1242 ):
1243 interpolator_kwargs = self.interpolator_kwargs
1244 else:
1245 interpolator_kwargs = {}
1247 self_interpolator, self.interpolator = self.interpolator, interpolator
1248 self_interpolator_kwargs, self.interpolator_kwargs = (
1249 self.interpolator_kwargs,
1250 interpolator_kwargs,
1251 )
1253 values = self[shape.wavelengths]
1255 self.domain = shape.wavelengths
1256 self.values = values
1258 self.interpolator = self_interpolator
1259 self.interpolator_kwargs = self_interpolator_kwargs
1261 return self
1263 def extrapolate(
1264 self,
1265 shape: SpectralShape,
1266 extrapolator: Type[ProtocolExtrapolator] | None = None,
1267 extrapolator_kwargs: dict | None = None,
1268 ) -> Self:
1269 """
1270 Extrapolate the spectral distribution in-place according to
1271 *CIE 15:2004* and *CIE 167:2005* recommendations or specified extrapolation
1272 arguments.
1274 Parameters
1275 ----------
1276 shape
1277 Spectral shape used for extrapolation.
1278 extrapolator
1279 Extrapolator class type to use as extrapolating function.
1280 extrapolator_kwargs
1281 Arguments to use when instantiating the extrapolating function.
1283 Returns
1284 -------
1285 :class:`colour.SpectralDistribution`
1286 Extrapolated spectral distribution.
1288 References
1289 ----------
1290 :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l`
1292 Examples
1293 --------
1294 >>> from colour.utilities import numpy_print_options
1295 >>> data = {
1296 ... 500: 0.0651,
1297 ... 520: 0.0705,
1298 ... 540: 0.0772,
1299 ... 560: 0.0870,
1300 ... 580: 0.1128,
1301 ... 600: 0.1360,
1302 ... }
1303 >>> sd = SpectralDistribution(data)
1304 >>> sd.extrapolate(SpectralShape(400, 700, 20)).shape
1305 SpectralShape(400.0, 700.0, 20.0)
1306 >>> with numpy_print_options(suppress=True):
1307 ... print(sd)
1308 [[ 400. 0.0651]
1309 [ 420. 0.0651]
1310 [ 440. 0.0651]
1311 [ 460. 0.0651]
1312 [ 480. 0.0651]
1313 [ 500. 0.0651]
1314 [ 520. 0.0705]
1315 [ 540. 0.0772]
1316 [ 560. 0.087 ]
1317 [ 580. 0.1128]
1318 [ 600. 0.136 ]
1319 [ 620. 0.136 ]
1320 [ 640. 0.136 ]
1321 [ 660. 0.136 ]
1322 [ 680. 0.136 ]
1323 [ 700. 0.136 ]]
1324 """
1326 shape_start, shape_end, shape_interval = as_float_array(
1327 [
1328 self.shape.start,
1329 self.shape.end,
1330 self.shape.interval,
1331 ]
1332 )
1334 wavelengths = np.hstack(
1335 [
1336 np.arange(shape.start, shape_start, shape_interval),
1337 np.arange(shape_end, shape.end, shape_interval) + shape_interval,
1338 ]
1339 )
1341 extrapolator = optional(extrapolator, Extrapolator)
1342 extrapolator_kwargs = optional(
1343 extrapolator_kwargs,
1344 {"method": "Constant", "left": None, "right": None},
1345 )
1347 self_extrapolator = self.extrapolator
1348 self_extrapolator_kwargs = self.extrapolator_kwargs
1350 self.extrapolator = extrapolator
1351 self.extrapolator_kwargs = extrapolator_kwargs
1353 # The following self-assignment is written as intended and triggers the
1354 # extrapolation.
1355 self[wavelengths] = self[wavelengths]
1357 self.extrapolator = self_extrapolator
1358 self.extrapolator_kwargs = self_extrapolator_kwargs
1360 return self
1362 def align(
1363 self,
1364 shape: SpectralShape,
1365 interpolator: Type[ProtocolInterpolator] | None = None,
1366 interpolator_kwargs: dict | None = None,
1367 extrapolator: Type[ProtocolExtrapolator] | None = None,
1368 extrapolator_kwargs: dict | None = None,
1369 ) -> Self:
1370 """
1371 Align the spectral distribution in-place to the specified spectral
1372 shape: Interpolate first then extrapolate to fit the specified range.
1374 Interpolation is performed according to *CIE 167:2005*
1375 recommendation (if the interpolator has not been changed at
1376 instantiation time) or specified interpolation arguments.
1378 The logic for choosing the interpolator class when ``interpolator``
1379 is not specified is as follows:
1381 .. code-block:: python
1383 if self.interpolator not in (
1384 SpragueInterpolator,
1385 CubicSplineInterpolator,
1386 ):
1387 interpolator = self.interpolator
1388 elif self.is_uniform():
1389 interpolator = SpragueInterpolator
1390 else:
1391 interpolator = CubicSplineInterpolator
1393 The logic for choosing the interpolator keyword arguments when
1394 ``interpolator_kwargs`` is not specified is as follows:
1396 .. code-block:: python
1398 if self.interpolator not in (
1399 SpragueInterpolator,
1400 CubicSplineInterpolator,
1401 ):
1402 interpolator_kwargs = self.interpolator_kwargs
1403 else:
1404 interpolator_kwargs = {}
1406 Parameters
1407 ----------
1408 shape
1409 Spectral shape used for alignment.
1410 interpolator
1411 Interpolator class type to use as interpolating function.
1412 interpolator_kwargs
1413 Arguments to use when instantiating the interpolating function.
1414 extrapolator
1415 Extrapolator class type to use as extrapolating function.
1416 extrapolator_kwargs
1417 Arguments to use when instantiating the extrapolating function.
1419 Returns
1420 -------
1421 :class:`colour.SpectralDistribution`
1422 Aligned spectral distribution.
1424 Examples
1425 --------
1426 >>> from colour.utilities import numpy_print_options
1427 >>> data = {
1428 ... 500: 0.0651,
1429 ... 520: 0.0705,
1430 ... 540: 0.0772,
1431 ... 560: 0.0870,
1432 ... 580: 0.1128,
1433 ... 600: 0.1360,
1434 ... }
1435 >>> sd = SpectralDistribution(data)
1436 >>> with numpy_print_options(suppress=True):
1437 ... print(sd.align(SpectralShape(505, 565, 1)))
1438 ... # doctest: +ELLIPSIS
1439 [[ 505. 0.0663929...]
1440 [ 506. 0.0666509...]
1441 [ 507. 0.0669069...]
1442 [ 508. 0.0671613...]
1443 [ 509. 0.0674150...]
1444 [ 510. 0.0676692...]
1445 [ 511. 0.0679253...]
1446 [ 512. 0.0681848...]
1447 [ 513. 0.0684491...]
1448 [ 514. 0.0687197...]
1449 [ 515. 0.0689975...]
1450 [ 516. 0.0692832...]
1451 [ 517. 0.0695771...]
1452 [ 518. 0.0698787...]
1453 [ 519. 0.0701870...]
1454 [ 520. 0.0705 ...]
1455 [ 521. 0.0708155...]
1456 [ 522. 0.0711336...]
1457 [ 523. 0.0714547...]
1458 [ 524. 0.0717789...]
1459 [ 525. 0.0721063...]
1460 [ 526. 0.0724367...]
1461 [ 527. 0.0727698...]
1462 [ 528. 0.0731051...]
1463 [ 529. 0.0734423...]
1464 [ 530. 0.0737808...]
1465 [ 531. 0.0741203...]
1466 [ 532. 0.0744603...]
1467 [ 533. 0.0748006...]
1468 [ 534. 0.0751409...]
1469 [ 535. 0.0754813...]
1470 [ 536. 0.0758220...]
1471 [ 537. 0.0761633...]
1472 [ 538. 0.0765060...]
1473 [ 539. 0.0768511...]
1474 [ 540. 0.0772 ...]
1475 [ 541. 0.0775527...]
1476 [ 542. 0.0779042...]
1477 [ 543. 0.0782507...]
1478 [ 544. 0.0785908...]
1479 [ 545. 0.0789255...]
1480 [ 546. 0.0792576...]
1481 [ 547. 0.0795917...]
1482 [ 548. 0.0799334...]
1483 [ 549. 0.0802895...]
1484 [ 550. 0.0806671...]
1485 [ 551. 0.0810740...]
1486 [ 552. 0.0815176...]
1487 [ 553. 0.0820049...]
1488 [ 554. 0.0825423...]
1489 [ 555. 0.0831351...]
1490 [ 556. 0.0837873...]
1491 [ 557. 0.0845010...]
1492 [ 558. 0.0852763...]
1493 [ 559. 0.0861110...]
1494 [ 560. 0.087 ...]
1495 [ 561. 0.0879383...]
1496 [ 562. 0.0889300...]
1497 [ 563. 0.0899793...]
1498 [ 564. 0.0910876...]
1499 [ 565. 0.0922541...]]
1500 """
1502 self.interpolate(shape, interpolator, interpolator_kwargs)
1503 self.extrapolate(shape, extrapolator, extrapolator_kwargs)
1505 return self
1507 def trim(self, shape: SpectralShape) -> Self:
1508 """
1509 Trim the spectral distribution wavelengths to the specified spectral shape.
1511 Parameters
1512 ----------
1513 shape
1514 Spectral shape used for trimming.
1516 Returns
1517 -------
1518 :class:`colour.SpectralDistribution`
1519 Trimmed spectral distribution.
1521 Examples
1522 --------
1523 >>> from colour.utilities import numpy_print_options
1524 >>> data = {
1525 ... 500: 0.0651,
1526 ... 520: 0.0705,
1527 ... 540: 0.0772,
1528 ... 560: 0.0870,
1529 ... 580: 0.1128,
1530 ... 600: 0.1360,
1531 ... }
1532 >>> sd = SpectralDistribution(data)
1533 >>> sd = sd.interpolate(SpectralShape(500, 600, 1))
1534 >>> with numpy_print_options(suppress=True):
1535 ... print(sd.trim(SpectralShape(520, 580, 5)))
1536 ... # doctest: +ELLIPSIS
1537 [[ 520. 0.0705 ...]
1538 [ 521. 0.0708155...]
1539 [ 522. 0.0711336...]
1540 [ 523. 0.0714547...]
1541 [ 524. 0.0717789...]
1542 [ 525. 0.0721063...]
1543 [ 526. 0.0724367...]
1544 [ 527. 0.0727698...]
1545 [ 528. 0.0731051...]
1546 [ 529. 0.0734423...]
1547 [ 530. 0.0737808...]
1548 [ 531. 0.0741203...]
1549 [ 532. 0.0744603...]
1550 [ 533. 0.0748006...]
1551 [ 534. 0.0751409...]
1552 [ 535. 0.0754813...]
1553 [ 536. 0.0758220...]
1554 [ 537. 0.0761633...]
1555 [ 538. 0.0765060...]
1556 [ 539. 0.0768511...]
1557 [ 540. 0.0772 ...]
1558 [ 541. 0.0775527...]
1559 [ 542. 0.0779042...]
1560 [ 543. 0.0782507...]
1561 [ 544. 0.0785908...]
1562 [ 545. 0.0789255...]
1563 [ 546. 0.0792576...]
1564 [ 547. 0.0795917...]
1565 [ 548. 0.0799334...]
1566 [ 549. 0.0802895...]
1567 [ 550. 0.0806671...]
1568 [ 551. 0.0810740...]
1569 [ 552. 0.0815176...]
1570 [ 553. 0.0820049...]
1571 [ 554. 0.0825423...]
1572 [ 555. 0.0831351...]
1573 [ 556. 0.0837873...]
1574 [ 557. 0.0845010...]
1575 [ 558. 0.0852763...]
1576 [ 559. 0.0861110...]
1577 [ 560. 0.087 ...]
1578 [ 561. 0.0879383...]
1579 [ 562. 0.0889300...]
1580 [ 563. 0.0899793...]
1581 [ 564. 0.0910876...]
1582 [ 565. 0.0922541...]
1583 [ 566. 0.0934760...]
1584 [ 567. 0.0947487...]
1585 [ 568. 0.0960663...]
1586 [ 569. 0.0974220...]
1587 [ 570. 0.0988081...]
1588 [ 571. 0.1002166...]
1589 [ 572. 0.1016394...]
1590 [ 573. 0.1030687...]
1591 [ 574. 0.1044972...]
1592 [ 575. 0.1059186...]
1593 [ 576. 0.1073277...]
1594 [ 577. 0.1087210...]
1595 [ 578. 0.1100968...]
1596 [ 579. 0.1114554...]
1597 [ 580. 0.1128 ...]]
1598 """
1600 start = max([shape.start, self.shape.start])
1601 end = min([shape.end, self.shape.end])
1603 indexes = np.where(np.logical_and(self.domain >= start, self.domain <= end))
1605 wavelengths = self.wavelengths[indexes]
1606 values = self.values[indexes]
1608 self.wavelengths = wavelengths
1609 self.values = values
1611 if self.shape.boundaries != shape.boundaries:
1612 runtime_warning(
1613 f'"{shape}" shape could not be honoured, using "{self.shape}"!'
1614 )
1616 return self
1618 def normalise(self, factor: Real = 1) -> Self:
1619 """
1620 Normalise the spectral distribution with the specified normalization
1621 factor.
1623 Parameters
1624 ----------
1625 factor
1626 Normalisation factor.
1628 Returns
1629 -------
1630 :class:`colour.SpectralDistribution`
1631 Normalised spectral distribution.
1633 Examples
1634 --------
1635 >>> from colour.utilities import numpy_print_options
1636 >>> data = {
1637 ... 500: 0.0651,
1638 ... 520: 0.0705,
1639 ... 540: 0.0772,
1640 ... 560: 0.0870,
1641 ... 580: 0.1128,
1642 ... 600: 0.1360,
1643 ... }
1644 >>> sd = SpectralDistribution(data)
1645 >>> with numpy_print_options(suppress=True):
1646 ... print(sd.normalise()) # doctest: +ELLIPSIS
1647 [[ 500. 0.4786764...]
1648 [ 520. 0.5183823...]
1649 [ 540. 0.5676470...]
1650 [ 560. 0.6397058...]
1651 [ 580. 0.8294117...]
1652 [ 600. 1. ...]]
1653 """
1655 with sdiv_mode():
1656 self *= sdiv(1, max(self.values)) * factor
1658 return self
1661class MultiSpectralDistributions(MultiSignals):
1662 """
1663 Define multi-spectral distributions: the base object for multi-spectral
1664 computations. Model colour matching functions, display primaries, camera
1665 sensitivities, and related spectral data sets.
1667 Initialise multi-spectral distributions according to *CIE 15:2004*
1668 recommendation: use the method developed by *Sprague (1880)* for
1669 interpolating functions having uniformly spaced independent variables
1670 and the *Cubic Spline* method for non-uniformly spaced independent
1671 variables. Perform extrapolation according to *CIE 167:2005*
1672 recommendation.
1674 .. important::
1676 Specific documentation about getting, setting, indexing and slicing
1677 the multi-spectral power distributions values is available in the
1678 :ref:`spectral-representation-and-continuous-signal` section.
1680 Parameters
1681 ----------
1682 data
1683 Data to be stored in the multi-spectral distributions.
1684 domain
1685 Values to initialise the multiple :class:`colour.SpectralDistribution`
1686 class instances :attr:`colour.continuous.Signal.wavelengths` attribute
1687 with. If both ``data`` and ``domain`` arguments are defined, the
1688 latter will be used to initialise the
1689 :attr:`colour.continuous.Signal.wavelengths` property.
1690 labels
1691 Names to use for the :class:`colour.SpectralDistribution` class
1692 instances.
1694 Other Parameters
1695 ----------------
1696 extrapolator
1697 Extrapolator class type to use as extrapolating function for the
1698 :class:`colour.SpectralDistribution` class instances.
1699 extrapolator_kwargs
1700 Arguments to use when instantiating the extrapolating function of the
1701 :class:`colour.SpectralDistribution` class instances.
1702 interpolator
1703 Interpolator class type to use as interpolating function for the
1704 :class:`colour.SpectralDistribution` class instances.
1705 interpolator_kwargs
1706 Arguments to use when instantiating the interpolating function of the
1707 :class:`colour.SpectralDistribution` class instances.
1708 name
1709 Multi-spectral distributions name.
1710 display_labels
1711 Multi-spectral distributions labels for figures, default to
1712 :attr:`colour.MultiSpectralDistributions.labels` property value.
1714 Warnings
1715 --------
1716 The *Cubic Spline* method might produce unexpected results with
1717 exceptionally noisy or non-uniformly spaced data.
1719 Attributes
1720 ----------
1721 - :attr:`~colour.MultiSpectralDistributions.display_name`
1722 - :attr:`~colour.MultiSpectralDistributions.display_labels`
1723 - :attr:`~colour.MultiSpectralDistributions.wavelengths`
1724 - :attr:`~colour.MultiSpectralDistributions.values`
1725 - :attr:`~colour.MultiSpectralDistributions.shape`
1727 Methods
1728 -------
1729 - :meth:`~colour.MultiSpectralDistributions.__init__`
1730 - :meth:`~colour.MultiSpectralDistributions.interpolate`
1731 - :meth:`~colour.MultiSpectralDistributions.extrapolate`
1732 - :meth:`~colour.MultiSpectralDistributions.align`
1733 - :meth:`~colour.MultiSpectralDistributions.trim`
1734 - :meth:`~colour.MultiSpectralDistributions.normalise`
1735 - :meth:`~colour.MultiSpectralDistributions.to_sds`
1737 References
1738 ----------
1739 :cite:`CIETC1-382005e`, :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l`
1741 Examples
1742 --------
1743 Instantiating the multi-spectral distributions with a uniformly spaced
1744 independent variable:
1746 >>> from colour.utilities import numpy_print_options
1747 >>> data = {
1748 ... 500: (0.004900, 0.323000, 0.272000),
1749 ... 510: (0.009300, 0.503000, 0.158200),
1750 ... 520: (0.063270, 0.710000, 0.078250),
1751 ... 530: (0.165500, 0.862000, 0.042160),
1752 ... 540: (0.290400, 0.954000, 0.020300),
1753 ... 550: (0.433450, 0.994950, 0.008750),
1754 ... 560: (0.594500, 0.995000, 0.003900),
1755 ... }
1756 >>> labels = ("x_bar", "y_bar", "z_bar")
1757 >>> with numpy_print_options(suppress=True):
1758 ... MultiSpectralDistributions(data, labels=labels)
1759 ... # doctest: +ELLIPSIS
1760 ...
1761 MultiSpectral...([[ 500. , 0.0049 , 0.323 , 0.272 ],
1762 ... [ 510. , 0.0093 , 0.503 , 0.1582 ],
1763 ... [ 520. , 0.06327, 0.71 , 0.07825],
1764 ... [ 530. , 0.1655 , 0.862 , 0.04216],
1765 ... [ 540. , 0.2904 , 0.954 , 0.0203 ],
1766 ... [ 550. , 0.43345, 0.99495, 0.00875],
1767 ... [ 560. , 0.5945 , 0.995 , 0.0039 ]],
1768 ... [...'x_bar', ...'y_bar', ...'z_bar'],
1769 ... SpragueInterpolator,
1770 ... {},
1771 ... Extrapolator,
1772 ... {'method': 'Constant', 'left': None, 'right': None})
1774 Instantiating a spectral distribution with a non-uniformly spaced
1775 independent variable:
1777 >>> data[511] = (0.00314, 0.31416, 0.03142)
1778 >>> with numpy_print_options(suppress=True):
1779 ... MultiSpectralDistributions(data, labels=labels)
1780 ... # doctest: +ELLIPSIS
1781 ...
1782 MultiSpectral...([[ 500. , 0.0049 , 0.323 , 0.272 ],
1783 ... [ 510. , 0.0093 , 0.503 , 0.1582 ],
1784 ... [ 511. , 0.00314, 0.31416, 0.03142],
1785 ... [ 520. , 0.06327, 0.71 , 0.07825],
1786 ... [ 530. , 0.1655 , 0.862 , 0.04216],
1787 ... [ 540. , 0.2904 , 0.954 , 0.0203 ],
1788 ... [ 550. , 0.43345, 0.99495, 0.00875],
1789 ... [ 560. , 0.5945 , 0.995 , 0.0039 ]],
1790 ... [...'x_bar', ...'y_bar', ...'z_bar'],
1791 ... CubicSplineInterpolator,
1792 ... {},
1793 ... Extrapolator,
1794 ... {'method': 'Constant', 'left': None, 'right': None})
1796 Instantiation with a *Pandas* `DataFrame`:
1798 >>> from colour.utilities import is_pandas_installed
1799 >>> if is_pandas_installed():
1800 ... from pandas import DataFrame
1801 ...
1802 ... x_bar = [data[key][0] for key in sorted(data.keys())]
1803 ... y_bar = [data[key][1] for key in sorted(data.keys())]
1804 ... z_bar = [data[key][2] for key in sorted(data.keys())]
1805 ... print(
1806 ... MultiSignals( # doctest: +SKIP
1807 ... DataFrame(
1808 ... dict(zip(labels, [x_bar, y_bar, z_bar])), data.keys()
1809 ... )
1810 ... )
1811 ... )
1812 ...
1813 [[ 5.0000000...e+02 4.9000000...e-03 3.2300000...e-01 \
18142.7200000...e-01]
1815 [ 5.1000000...e+02 9.3000000...e-03 5.0300000...e-01 \
18161.5820000...e-01]
1817 [ 5.2000000...e+02 3.1400000...e-03 3.1416000...e-01 \
18183.1420000...e-02]
1819 [ 5.3000000...e+02 6.3270000...e-02 7.1000000...e-01 \
18207.8250000...e-02]
1821 [ 5.4000000...e+02 1.6550000...e-01 8.6200000...e-01 \
18224.2160000...e-02]
1823 [ 5.5000000...e+02 2.9040000...e-01 9.5400000...e-01 \
18242.0300000...e-02]
1825 [ 5.6000000...e+02 4.3345000...e-01 9.9495000...e-01 \
18268.7500000...e-03]
1827 [ 5.1100000...e+02 5.9450000...e-01 9.9500000...e-01 \
18283.9000000...e-03]]
1829 """
1831 def __init__(
1832 self,
1833 data: (
1834 ArrayLike
1835 | DataFrame
1836 | dict
1837 | MultiSignals
1838 | Sequence
1839 | Series
1840 | Signal
1841 | SpectralDistribution
1842 | ValuesView
1843 | None
1844 ) = None,
1845 domain: ArrayLike | SpectralShape | KeysView | None = None,
1846 labels: Sequence | None = None,
1847 **kwargs: Any,
1848 ) -> None:
1849 domain = domain.wavelengths if isinstance(domain, SpectralShape) else domain
1850 signals = self.multi_signals_unpack_data(data, domain, labels)
1852 domain = signals[next(iter(signals.keys()))].domain if signals else None
1853 uniform = is_uniform(domain) if domain is not None and len(domain) > 0 else True
1855 # Initialising with *CIE 15:2004* and *CIE 167:2005* recommendations
1856 # defaults.
1857 kwargs["interpolator"] = kwargs.get(
1858 "interpolator",
1859 SpragueInterpolator if uniform else CubicSplineInterpolator,
1860 )
1861 kwargs["interpolator_kwargs"] = kwargs.get("interpolator_kwargs", {})
1863 kwargs["extrapolator"] = kwargs.get("extrapolator", Extrapolator)
1864 kwargs["extrapolator_kwargs"] = kwargs.get(
1865 "extrapolator_kwargs",
1866 {"method": "Constant", "left": None, "right": None},
1867 )
1869 super().__init__(signals, domain, signal_type=SpectralDistribution, **kwargs)
1871 self._display_name: str = self.name
1872 self.display_name = kwargs.get("display_name", self._display_name)
1873 self._display_labels: list = list(self.signals.keys())
1874 self.display_labels = kwargs.get("display_labels", self._display_labels)
1876 @property
1877 def display_name(self) -> str:
1878 """
1879 Getter and setter for the multi-spectral distributions' display name.
1881 The display name provides a human-readable identifier for the
1882 multi-spectral distribution collection, used for visualization
1883 and reporting purposes.
1885 Parameters
1886 ----------
1887 value
1888 Value to set the multi-spectral distributions' display name
1889 with.
1891 Returns
1892 -------
1893 :class:`str`
1894 Multi-spectral distributions' display name.
1895 """
1897 return self._display_name
1899 @display_name.setter
1900 def display_name(self, value: str) -> None:
1901 """Setter for the **self.display_name** property."""
1903 attest(
1904 isinstance(value, str),
1905 f'"display_name" property: "{value}" type is not "str"!',
1906 )
1908 self._display_name = value
1910 @property
1911 def display_labels(self) -> List[str]:
1912 """
1913 Getter and setter for the display labels of the multi-spectral
1914 distributions.
1916 The display labels provide human-readable identifiers for each spectral
1917 distribution in the multi-spectral collection, facilitating data
1918 visualization and interpretation.
1920 Parameters
1921 ----------
1922 value
1923 Value to set the multi-spectral distributions display labels with.
1925 Returns
1926 -------
1927 :class:`list`
1928 Multi-spectral distributions display labels.
1929 """
1931 return self._display_labels
1933 @display_labels.setter
1934 def display_labels(self, value: Sequence) -> None:
1935 """Setter for the **self.display_labels** property."""
1937 attest(
1938 is_iterable(value),
1939 f'"display_labels" property: "{value}" is not an "iterable" like object!',
1940 )
1942 attest(
1943 len(set(value)) == len(value),
1944 '"display_labels" property: values must be unique!',
1945 )
1947 attest(
1948 len(value) == len(self.labels),
1949 f'"display_labels" property: length must be "{len(self.labels)}"!',
1950 )
1952 self._display_labels = [str(label) for label in value]
1953 for i, signal in enumerate(self.signals.values()):
1954 cast("SpectralDistribution", signal).display_name = self._display_labels[i]
1956 @property
1957 def wavelengths(self) -> NDArrayFloat:
1958 """
1959 Getter and setter for the multi-spectral distributions
1960 wavelengths :math:`\\lambda_n`.
1962 Parameters
1963 ----------
1964 value
1965 Value to set the multi-spectral distributions wavelengths
1966 :math:`\\lambda_n` with.
1968 Returns
1969 -------
1970 :class:`numpy.ndarray`
1971 Multi-spectral distributions wavelengths :math:`\\lambda_n`.
1972 """
1974 return self.domain
1976 @wavelengths.setter
1977 def wavelengths(self, value: ArrayLike) -> None:
1978 """Setter for the **self.wavelengths** property."""
1980 self.domain = as_float_array(value, self.dtype)
1982 @property
1983 def values(self) -> NDArrayFloat:
1984 """
1985 Getter and setter for the multi-spectral distributions values.
1987 Parameters
1988 ----------
1989 value
1990 Value to set the multi-spectral distributions wavelengths values
1991 with.
1993 Returns
1994 -------
1995 :class:`numpy.ndarray`
1996 Multi-spectral distributions values.
1997 """
1999 return self.range
2001 @values.setter
2002 def values(self, value: ArrayLike) -> None:
2003 """Setter for the **self.values** property."""
2005 self.range = as_float_array(value, self.dtype)
2007 @property
2008 def shape(self) -> SpectralShape:
2009 """
2010 Getter property for the multi-spectral distributions shape.
2012 Returns
2013 -------
2014 :class:`colour.SpectralShape`
2015 Multi-spectral distributions shape.
2017 Notes
2018 -----
2019 - Multi-spectral distributions with a non-uniformly spaced
2020 independent variable have multiple intervals, in that case
2021 :attr:`colour.MultiSpectralDistributions.shape` property returns
2022 the *minimum* interval size.
2024 Examples
2025 --------
2026 Shape of the multi-spectral distributions with a uniformly spaced
2027 independent variable:
2029 >>> from colour.utilities import numpy_print_options
2030 >>> data = {
2031 ... 500: (0.004900, 0.323000, 0.272000),
2032 ... 510: (0.009300, 0.503000, 0.158200),
2033 ... 520: (0.063270, 0.710000, 0.078250),
2034 ... 530: (0.165500, 0.862000, 0.042160),
2035 ... 540: (0.290400, 0.954000, 0.020300),
2036 ... 550: (0.433450, 0.994950, 0.008750),
2037 ... 560: (0.594500, 0.995000, 0.003900),
2038 ... }
2039 >>> MultiSpectralDistributions(data).shape
2040 SpectralShape(500.0, 560.0, 10.0)
2042 Shape of the multi-spectral distributions with a non-uniformly spaced
2043 independent variable:
2045 >>> data[511] = (0.00314, 0.31416, 0.03142)
2046 >>> MultiSpectralDistributions(data).shape
2047 SpectralShape(500.0, 560.0, 1.0)
2048 """
2050 return first_item(self._signals.values()).shape
2052 def interpolate(
2053 self,
2054 shape: SpectralShape,
2055 interpolator: Type[ProtocolInterpolator] | None = None,
2056 interpolator_kwargs: dict | None = None,
2057 ) -> Self:
2058 """
2059 Interpolate the multi-spectral distributions in-place according to
2060 *CIE 167:2005* recommendation (if the interpolator has not been changed
2061 at instantiation time) or specified interpolation arguments.
2063 The logic for choosing the interpolator class when ``interpolator`` is
2064 not specified is as follows:
2066 .. code-block:: python
2068 if self.interpolator not in (
2069 SpragueInterpolator,
2070 CubicSplineInterpolator,
2071 ):
2072 interpolator = self.interpolator
2073 elif self.is_uniform():
2074 interpolator = SpragueInterpolator
2075 else:
2076 interpolator = CubicSplineInterpolator
2078 The logic for choosing the interpolator keyword arguments when
2079 ``interpolator_kwargs`` is not specified is as follows:
2081 .. code-block:: python
2083 if self.interpolator not in (
2084 SpragueInterpolator,
2085 CubicSplineInterpolator,
2086 ):
2087 interpolator_kwargs = self.interpolator_kwargs
2088 else:
2089 interpolator_kwargs = {}
2091 Parameters
2092 ----------
2093 shape
2094 Spectral shape used for interpolation.
2095 interpolator
2096 Interpolator class type to use as interpolating function.
2097 interpolator_kwargs
2098 Arguments to use when instantiating the interpolating function.
2100 Returns
2101 -------
2102 :class:`colour.MultiSpectralDistributions`
2103 Interpolated multi-spectral distributions.
2105 Notes
2106 -----
2107 - See :meth:`colour.SpectralDistribution.interpolate` method notes
2108 section.
2110 Warnings
2111 --------
2112 See :meth:`colour.SpectralDistribution.interpolate` method warning
2113 section.
2115 References
2116 ----------
2117 :cite:`CIETC1-382005e`
2119 Examples
2120 --------
2121 Multi-spectral distributions with a uniformly spaced independent
2122 variable uses *Sprague (1880)* interpolation:
2124 >>> from colour.utilities import numpy_print_options
2125 >>> data = {
2126 ... 500: (0.004900, 0.323000, 0.272000),
2127 ... 510: (0.009300, 0.503000, 0.158200),
2128 ... 520: (0.063270, 0.710000, 0.078250),
2129 ... 530: (0.165500, 0.862000, 0.042160),
2130 ... 540: (0.290400, 0.954000, 0.020300),
2131 ... 550: (0.433450, 0.994950, 0.008750),
2132 ... 560: (0.594500, 0.995000, 0.003900),
2133 ... }
2134 >>> msds = MultiSpectralDistributions(data)
2135 >>> with numpy_print_options(suppress=True):
2136 ... print(msds.interpolate(SpectralShape(500, 560, 1)))
2137 ... # doctest: +ELLIPSIS
2138 [[ 500. 0.0049 ... 0.323 ... 0.272 ...]
2139 [ 501. 0.0043252... 0.3400642... 0.2599848...]
2140 [ 502. 0.0037950... 0.3572165... 0.2479849...]
2141 [ 503. 0.0033761... 0.3744030... 0.2360688...]
2142 [ 504. 0.0031397... 0.3916650... 0.2242878...]
2143 [ 505. 0.0031582... 0.4091067... 0.2126801...]
2144 [ 506. 0.0035019... 0.4268629... 0.2012748...]
2145 [ 507. 0.0042365... 0.4450668... 0.1900968...]
2146 [ 508. 0.0054192... 0.4638181... 0.1791709...]
2147 [ 509. 0.0070965... 0.4831505... 0.1685260...]
2148 [ 510. 0.0093 ... 0.503 ... 0.1582 ...]
2149 [ 511. 0.0120562... 0.5232543... 0.1482365...]
2150 [ 512. 0.0154137... 0.5439717... 0.1386625...]
2151 [ 513. 0.0193991... 0.565139 ... 0.1294993...]
2152 [ 514. 0.0240112... 0.5866255... 0.1207676...]
2153 [ 515. 0.0292289... 0.6082226... 0.1124864...]
2154 [ 516. 0.0350192... 0.6296821... 0.1046717...]
2155 [ 517. 0.0413448... 0.6507558... 0.0973361...]
2156 [ 518. 0.0481727... 0.6712346... 0.0904871...]
2157 [ 519. 0.0554816... 0.6909873... 0.0841267...]
2158 [ 520. 0.06327 ... 0.71 ... 0.07825 ...]
2159 [ 521. 0.0715642... 0.7283456... 0.0728614...]
2160 [ 522. 0.0803970... 0.7459679... 0.0680051...]
2161 [ 523. 0.0897629... 0.7628184... 0.0636823...]
2162 [ 524. 0.0996227... 0.7789004... 0.0598449...]
2163 [ 525. 0.1099142... 0.7942533... 0.0564111...]
2164 [ 526. 0.1205637... 0.8089368... 0.0532822...]
2165 [ 527. 0.1314973... 0.8230153... 0.0503588...]
2166 [ 528. 0.1426523... 0.8365417... 0.0475571...]
2167 [ 529. 0.1539887... 0.8495422... 0.0448253...]
2168 [ 530. 0.1655 ... 0.862 ... 0.04216 ...]
2169 [ 531. 0.1772055... 0.8738585... 0.0395936...]
2170 [ 532. 0.1890877... 0.8850940... 0.0371046...]
2171 [ 533. 0.2011304... 0.8957073... 0.0346733...]
2172 [ 534. 0.2133310... 0.9057092... 0.0323006...]
2173 [ 535. 0.2256968... 0.9151181... 0.0300011...]
2174 [ 536. 0.2382403... 0.9239560... 0.0277974...]
2175 [ 537. 0.2509754... 0.9322459... 0.0257131...]
2176 [ 538. 0.2639130... 0.9400080... 0.0237668...]
2177 [ 539. 0.2770569... 0.9472574... 0.0219659...]
2178 [ 540. 0.2904 ... 0.954 ... 0.0203 ...]
2179 [ 541. 0.3039194... 0.9602409... 0.0187414...]
2180 [ 542. 0.3175893... 0.9660106... 0.0172748...]
2181 [ 543. 0.3314022... 0.9713260... 0.0158947...]
2182 [ 544. 0.3453666... 0.9761850... 0.0146001...]
2183 [ 545. 0.3595019... 0.9805731... 0.0133933...]
2184 [ 546. 0.3738324... 0.9844703... 0.0122777...]
2185 [ 547. 0.3883818... 0.9878583... 0.0112562...]
2186 [ 548. 0.4031674... 0.9907270... 0.0103302...]
2187 [ 549. 0.4181943... 0.9930817... 0.0094972...]
2188 [ 550. 0.43345 ... 0.99495 ... 0.00875 ...]
2189 [ 551. 0.4489082... 0.9963738... 0.0080748...]
2190 [ 552. 0.4645599... 0.9973682... 0.0074580...]
2191 [ 553. 0.4803950... 0.9979568... 0.0068902...]
2192 [ 554. 0.4963962... 0.9981802... 0.0063660...]
2193 [ 555. 0.5125410... 0.9980910... 0.0058818...]
2194 [ 556. 0.5288034... 0.9977488... 0.0054349...]
2195 [ 557. 0.5451560... 0.9972150... 0.0050216...]
2196 [ 558. 0.5615719... 0.9965479... 0.0046357...]
2197 [ 559. 0.5780267... 0.9957974... 0.0042671...]
2198 [ 560. 0.5945 ... 0.995 ... 0.0039 ...]]
2200 Multi-spectral distributions with a non-uniformly spaced independent
2201 variable uses *Cubic Spline* interpolation:
2203 >>> data[511] = (0.00314, 0.31416, 0.03142)
2204 >>> msds = MultiSpectralDistributions(data)
2205 >>> with numpy_print_options(suppress=True):
2206 ... print(msds.interpolate(SpectralShape(500, 560, 1)))
2207 ... # doctest: +ELLIPSIS
2208 [[ 500. 0.0049 ... 0.323 ... 0.272 ...]
2209 [ 501. 0.0300110... 0.9455153... 0.5985102...]
2210 [ 502. 0.0462136... 1.3563103... 0.8066498...]
2211 [ 503. 0.0547925... 1.5844039... 0.9126502...]
2212 [ 504. 0.0570325... 1.6588148... 0.9327429...]
2213 [ 505. 0.0542183... 1.6085619... 0.8831594...]
2214 [ 506. 0.0476346... 1.4626640... 0.7801312...]
2215 [ 507. 0.0385662... 1.2501401... 0.6398896...]
2216 [ 508. 0.0282978... 1.0000089... 0.4786663...]
2217 [ 509. 0.0181142... 0.7412892... 0.3126925...]
2218 [ 510. 0.0093 ... 0.503 ... 0.1582 ...]
2219 [ 511. 0.00314 ... 0.31416 ... 0.03142 ...]
2220 [ 512. 0.0006228... 0.1970419... -0.0551709...]
2221 [ 513. 0.0015528... 0.1469341... -0.1041165...]
2222 [ 514. 0.0054381... 0.1523785... -0.1217152...]
2223 [ 515. 0.0117869... 0.2019173... -0.1142659...]
2224 [ 516. 0.0201073... 0.2840925... -0.0880670...]
2225 [ 517. 0.0299077... 0.3874463... -0.0494174...]
2226 [ 518. 0.0406961... 0.5005208... -0.0046156...]
2227 [ 519. 0.0519808... 0.6118579... 0.0400397...]
2228 [ 520. 0.06327 ... 0.71 ... 0.07825 ...]
2229 [ 521. 0.0741690... 0.7859059... 0.1050384...]
2230 [ 522. 0.0846726... 0.8402033... 0.1207164...]
2231 [ 523. 0.0948728... 0.8759363... 0.1269173...]
2232 [ 524. 0.1048614... 0.8961496... 0.1252743...]
2233 [ 525. 0.1147305... 0.9038874... 0.1174207...]
2234 [ 526. 0.1245719... 0.9021942... 0.1049899...]
2235 [ 527. 0.1344776... 0.8941145... 0.0896151...]
2236 [ 528. 0.1445395... 0.8826926... 0.0729296...]
2237 [ 529. 0.1548497... 0.8709729... 0.0565668...]
2238 [ 530. 0.1655 ... 0.862 ... 0.04216 ...]
2239 [ 531. 0.1765618... 0.858179 ... 0.0309976...]
2240 [ 532. 0.1880244... 0.8593588... 0.0229897...]
2241 [ 533. 0.1998566... 0.8647493... 0.0177013...]
2242 [ 534. 0.2120269... 0.8735601... 0.0146975...]
2243 [ 535. 0.2245042... 0.8850011... 0.0135435...]
2244 [ 536. 0.2372572... 0.8982820... 0.0138044...]
2245 [ 537. 0.2502546... 0.9126126... 0.0150454...]
2246 [ 538. 0.2634650... 0.9272026... 0.0168315...]
2247 [ 539. 0.2768572... 0.9412618... 0.0187280...]
2248 [ 540. 0.2904 ... 0.954 ... 0.0203 ...]
2249 [ 541. 0.3040682... 0.9647869... 0.0211987...]
2250 [ 542. 0.3178617... 0.9736329... 0.0214207...]
2251 [ 543. 0.3317865... 0.9807080... 0.0210486...]
2252 [ 544. 0.3458489... 0.9861825... 0.0201650...]
2253 [ 545. 0.3600548... 0.9902267... 0.0188525...]
2254 [ 546. 0.3744103... 0.9930107... 0.0171939...]
2255 [ 547. 0.3889215... 0.9947048... 0.0152716...]
2256 [ 548. 0.4035944... 0.9954792... 0.0131685...]
2257 [ 549. 0.4184352... 0.9955042... 0.0109670...]
2258 [ 550. 0.43345 ... 0.99495 ... 0.00875 ...]
2259 [ 551. 0.4486447... 0.9939867... 0.0065999...]
2260 [ 552. 0.4640255... 0.9927847... 0.0045994...]
2261 [ 553. 0.4795984... 0.9915141... 0.0028313...]
2262 [ 554. 0.4953696... 0.9903452... 0.0013781...]
2263 [ 555. 0.5113451... 0.9894483... 0.0003224...]
2264 [ 556. 0.5275310... 0.9889934... -0.0002530...]
2265 [ 557. 0.5439334... 0.9891509... -0.0002656...]
2266 [ 558. 0.5605583... 0.9900910... 0.0003672...]
2267 [ 559. 0.5774118... 0.9919840... 0.0017282...]
2268 [ 560. 0.5945 ... 0.995 ... 0.0039 ...]]
2269 """
2271 for signal in self.signals.values():
2272 cast("SpectralDistribution", signal).interpolate(
2273 shape, interpolator, interpolator_kwargs
2274 )
2276 return self
2278 def extrapolate(
2279 self,
2280 shape: SpectralShape,
2281 extrapolator: Type[ProtocolExtrapolator] | None = None,
2282 extrapolator_kwargs: dict | None = None,
2283 ) -> Self:
2284 """
2285 Extrapolate the multi-spectral distributions in-place according to
2286 *CIE 15:2004* and *CIE 167:2005* recommendations or specified extrapolation
2287 arguments.
2289 Parameters
2290 ----------
2291 shape
2292 Spectral shape used for extrapolation.
2293 extrapolator
2294 Extrapolator class type to use as extrapolating function.
2295 extrapolator_kwargs
2296 Arguments to use when instantiating the extrapolating function.
2298 Returns
2299 -------
2300 :class:`colour.MultiSpectralDistributions`
2301 Extrapolated multi-spectral distributions.
2303 References
2304 ----------
2305 :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l`
2307 Examples
2308 --------
2309 >>> from colour.utilities import numpy_print_options
2310 >>> data = {
2311 ... 500: (0.004900, 0.323000, 0.272000),
2312 ... 510: (0.009300, 0.503000, 0.158200),
2313 ... 520: (0.063270, 0.710000, 0.078250),
2314 ... 530: (0.165500, 0.862000, 0.042160),
2315 ... 540: (0.290400, 0.954000, 0.020300),
2316 ... 550: (0.433450, 0.994950, 0.008750),
2317 ... 560: (0.594500, 0.995000, 0.003900),
2318 ... }
2319 >>> msds = MultiSpectralDistributions(data)
2320 >>> msds.extrapolate(SpectralShape(400, 700, 10)).shape
2321 SpectralShape(400.0, 700.0, 10.0)
2322 >>> with numpy_print_options(suppress=True):
2323 ... print(msds)
2324 [[ 400. 0.0049 0.323 0.272 ]
2325 [ 410. 0.0049 0.323 0.272 ]
2326 [ 420. 0.0049 0.323 0.272 ]
2327 [ 430. 0.0049 0.323 0.272 ]
2328 [ 440. 0.0049 0.323 0.272 ]
2329 [ 450. 0.0049 0.323 0.272 ]
2330 [ 460. 0.0049 0.323 0.272 ]
2331 [ 470. 0.0049 0.323 0.272 ]
2332 [ 480. 0.0049 0.323 0.272 ]
2333 [ 490. 0.0049 0.323 0.272 ]
2334 [ 500. 0.0049 0.323 0.272 ]
2335 [ 510. 0.0093 0.503 0.1582 ]
2336 [ 520. 0.06327 0.71 0.07825]
2337 [ 530. 0.1655 0.862 0.04216]
2338 [ 540. 0.2904 0.954 0.0203 ]
2339 [ 550. 0.43345 0.99495 0.00875]
2340 [ 560. 0.5945 0.995 0.0039 ]
2341 [ 570. 0.5945 0.995 0.0039 ]
2342 [ 580. 0.5945 0.995 0.0039 ]
2343 [ 590. 0.5945 0.995 0.0039 ]
2344 [ 600. 0.5945 0.995 0.0039 ]
2345 [ 610. 0.5945 0.995 0.0039 ]
2346 [ 620. 0.5945 0.995 0.0039 ]
2347 [ 630. 0.5945 0.995 0.0039 ]
2348 [ 640. 0.5945 0.995 0.0039 ]
2349 [ 650. 0.5945 0.995 0.0039 ]
2350 [ 660. 0.5945 0.995 0.0039 ]
2351 [ 670. 0.5945 0.995 0.0039 ]
2352 [ 680. 0.5945 0.995 0.0039 ]
2353 [ 690. 0.5945 0.995 0.0039 ]
2354 [ 700. 0.5945 0.995 0.0039 ]]
2355 """
2357 for signal in self.signals.values():
2358 cast("SpectralDistribution", signal).extrapolate(
2359 shape, extrapolator, extrapolator_kwargs
2360 )
2362 return self
2364 def align(
2365 self,
2366 shape: SpectralShape,
2367 interpolator: Type[ProtocolInterpolator] | None = None,
2368 interpolator_kwargs: dict | None = None,
2369 extrapolator: Type[ProtocolExtrapolator] | None = None,
2370 extrapolator_kwargs: dict | None = None,
2371 ) -> Self:
2372 """
2373 Align the multi-spectral distributions in-place to the specified spectral
2374 shape: Interpolates first then extrapolates to fit the specified range.
2376 Interpolation is performed according to *CIE 167:2005* recommendation
2377 (if the interpolator has not been changed at instantiation time) or
2378 specified interpolation arguments.
2380 The logic for choosing the interpolator class when ``interpolator`` is
2381 not specified is as follows:
2383 .. code-block:: python
2385 if self.interpolator not in (
2386 SpragueInterpolator,
2387 CubicSplineInterpolator,
2388 ):
2389 interpolator = self.interpolator
2390 elif self.is_uniform():
2391 interpolator = SpragueInterpolator
2392 else:
2393 interpolator = CubicSplineInterpolator
2395 The logic for choosing the interpolator keyword arguments when
2396 ``interpolator_kwargs`` is not specified is as follows:
2398 .. code-block:: python
2400 if self.interpolator not in (
2401 SpragueInterpolator,
2402 CubicSplineInterpolator,
2403 ):
2404 interpolator_kwargs = self.interpolator_kwargs
2405 else:
2406 interpolator_kwargs = {}
2408 Parameters
2409 ----------
2410 shape
2411 Spectral shape used for alignment.
2412 interpolator
2413 Interpolator class type to use as interpolating function.
2414 interpolator_kwargs
2415 Arguments to use when instantiating the interpolating function.
2416 extrapolator
2417 Extrapolator class type to use as extrapolating function.
2418 extrapolator_kwargs
2419 Arguments to use when instantiating the extrapolating function.
2421 Returns
2422 -------
2423 :class:`colour.MultiSpectralDistributions`
2424 Aligned multi-spectral distributions.
2426 Examples
2427 --------
2428 >>> from colour.utilities import numpy_print_options
2429 >>> data = {
2430 ... 500: (0.004900, 0.323000, 0.272000),
2431 ... 510: (0.009300, 0.503000, 0.158200),
2432 ... 520: (0.063270, 0.710000, 0.078250),
2433 ... 530: (0.165500, 0.862000, 0.042160),
2434 ... 540: (0.290400, 0.954000, 0.020300),
2435 ... 550: (0.433450, 0.994950, 0.008750),
2436 ... 560: (0.594500, 0.995000, 0.003900),
2437 ... }
2438 >>> msds = MultiSpectralDistributions(data)
2439 >>> with numpy_print_options(suppress=True):
2440 ... print(msds.align(SpectralShape(505, 565, 1)))
2441 ... # doctest: +ELLIPSIS
2442 [[ 505. 0.0031582... 0.4091067... 0.2126801...]
2443 [ 506. 0.0035019... 0.4268629... 0.2012748...]
2444 [ 507. 0.0042365... 0.4450668... 0.1900968...]
2445 [ 508. 0.0054192... 0.4638181... 0.1791709...]
2446 [ 509. 0.0070965... 0.4831505... 0.1685260...]
2447 [ 510. 0.0093 ... 0.503 ... 0.1582 ...]
2448 [ 511. 0.0120562... 0.5232543... 0.1482365...]
2449 [ 512. 0.0154137... 0.5439717... 0.1386625...]
2450 [ 513. 0.0193991... 0.565139 ... 0.1294993...]
2451 [ 514. 0.0240112... 0.5866255... 0.1207676...]
2452 [ 515. 0.0292289... 0.6082226... 0.1124864...]
2453 [ 516. 0.0350192... 0.6296821... 0.1046717...]
2454 [ 517. 0.0413448... 0.6507558... 0.0973361...]
2455 [ 518. 0.0481727... 0.6712346... 0.0904871...]
2456 [ 519. 0.0554816... 0.6909873... 0.0841267...]
2457 [ 520. 0.06327 ... 0.71 ... 0.07825 ...]
2458 [ 521. 0.0715642... 0.7283456... 0.0728614...]
2459 [ 522. 0.0803970... 0.7459679... 0.0680051...]
2460 [ 523. 0.0897629... 0.7628184... 0.0636823...]
2461 [ 524. 0.0996227... 0.7789004... 0.0598449...]
2462 [ 525. 0.1099142... 0.7942533... 0.0564111...]
2463 [ 526. 0.1205637... 0.8089368... 0.0532822...]
2464 [ 527. 0.1314973... 0.8230153... 0.0503588...]
2465 [ 528. 0.1426523... 0.8365417... 0.0475571...]
2466 [ 529. 0.1539887... 0.8495422... 0.0448253...]
2467 [ 530. 0.1655 ... 0.862 ... 0.04216 ...]
2468 [ 531. 0.1772055... 0.8738585... 0.0395936...]
2469 [ 532. 0.1890877... 0.8850940... 0.0371046...]
2470 [ 533. 0.2011304... 0.8957073... 0.0346733...]
2471 [ 534. 0.2133310... 0.9057092... 0.0323006...]
2472 [ 535. 0.2256968... 0.9151181... 0.0300011...]
2473 [ 536. 0.2382403... 0.9239560... 0.0277974...]
2474 [ 537. 0.2509754... 0.9322459... 0.0257131...]
2475 [ 538. 0.2639130... 0.9400080... 0.0237668...]
2476 [ 539. 0.2770569... 0.9472574... 0.0219659...]
2477 [ 540. 0.2904 ... 0.954 ... 0.0203 ...]
2478 [ 541. 0.3039194... 0.9602409... 0.0187414...]
2479 [ 542. 0.3175893... 0.9660106... 0.0172748...]
2480 [ 543. 0.3314022... 0.9713260... 0.0158947...]
2481 [ 544. 0.3453666... 0.9761850... 0.0146001...]
2482 [ 545. 0.3595019... 0.9805731... 0.0133933...]
2483 [ 546. 0.3738324... 0.9844703... 0.0122777...]
2484 [ 547. 0.3883818... 0.9878583... 0.0112562...]
2485 [ 548. 0.4031674... 0.9907270... 0.0103302...]
2486 [ 549. 0.4181943... 0.9930817... 0.0094972...]
2487 [ 550. 0.43345 ... 0.99495 ... 0.00875 ...]
2488 [ 551. 0.4489082... 0.9963738... 0.0080748...]
2489 [ 552. 0.4645599... 0.9973682... 0.0074580...]
2490 [ 553. 0.4803950... 0.9979568... 0.0068902...]
2491 [ 554. 0.4963962... 0.9981802... 0.0063660...]
2492 [ 555. 0.5125410... 0.9980910... 0.0058818...]
2493 [ 556. 0.5288034... 0.9977488... 0.0054349...]
2494 [ 557. 0.5451560... 0.9972150... 0.0050216...]
2495 [ 558. 0.5615719... 0.9965479... 0.0046357...]
2496 [ 559. 0.5780267... 0.9957974... 0.0042671...]
2497 [ 560. 0.5945 ... 0.995 ... 0.0039 ...]
2498 [ 561. 0.5945 ... 0.995 ... 0.0039 ...]
2499 [ 562. 0.5945 ... 0.995 ... 0.0039 ...]
2500 [ 563. 0.5945 ... 0.995 ... 0.0039 ...]
2501 [ 564. 0.5945 ... 0.995 ... 0.0039 ...]
2502 [ 565. 0.5945 ... 0.995 ... 0.0039 ...]]
2503 """
2505 for signal in self.signals.values():
2506 cast("SpectralDistribution", signal).align(
2507 shape,
2508 interpolator,
2509 interpolator_kwargs,
2510 extrapolator,
2511 extrapolator_kwargs,
2512 )
2514 return self
2516 def trim(self, shape: SpectralShape) -> Self:
2517 """
2518 Trim the multi-spectral distributions wavelengths to the specified shape.
2520 Parameters
2521 ----------
2522 shape
2523 Spectral shape used for trimming.
2525 Returns
2526 -------
2527 :class:`colour.MultiSpectralDistributions`
2528 Trimmed multi-spectral distributions.
2530 Examples
2531 --------
2532 >>> from colour.utilities import numpy_print_options
2533 >>> data = {
2534 ... 500: (0.004900, 0.323000, 0.272000),
2535 ... 510: (0.009300, 0.503000, 0.158200),
2536 ... 520: (0.063270, 0.710000, 0.078250),
2537 ... 530: (0.165500, 0.862000, 0.042160),
2538 ... 540: (0.290400, 0.954000, 0.020300),
2539 ... 550: (0.433450, 0.994950, 0.008750),
2540 ... 560: (0.594500, 0.995000, 0.003900),
2541 ... }
2542 >>> msds = MultiSpectralDistributions(data)
2543 >>> msds = msds.interpolate(SpectralShape(500, 560, 1))
2544 >>> with numpy_print_options(suppress=True):
2545 ... print(msds.trim(SpectralShape(520, 580, 5)))
2546 ... # doctest: +ELLIPSIS
2547 [[ 520. 0.06327 ... 0.71 ... 0.07825 ...]
2548 [ 521. 0.0715642... 0.7283456... 0.0728614...]
2549 [ 522. 0.0803970... 0.7459679... 0.0680051...]
2550 [ 523. 0.0897629... 0.7628184... 0.0636823...]
2551 [ 524. 0.0996227... 0.7789004... 0.0598449...]
2552 [ 525. 0.1099142... 0.7942533... 0.0564111...]
2553 [ 526. 0.1205637... 0.8089368... 0.0532822...]
2554 [ 527. 0.1314973... 0.8230153... 0.0503588...]
2555 [ 528. 0.1426523... 0.8365417... 0.0475571...]
2556 [ 529. 0.1539887... 0.8495422... 0.0448253...]
2557 [ 530. 0.1655 ... 0.862 ... 0.04216 ...]
2558 [ 531. 0.1772055... 0.8738585... 0.0395936...]
2559 [ 532. 0.1890877... 0.8850940... 0.0371046...]
2560 [ 533. 0.2011304... 0.8957073... 0.0346733...]
2561 [ 534. 0.2133310... 0.9057092... 0.0323006...]
2562 [ 535. 0.2256968... 0.9151181... 0.0300011...]
2563 [ 536. 0.2382403... 0.9239560... 0.0277974...]
2564 [ 537. 0.2509754... 0.9322459... 0.0257131...]
2565 [ 538. 0.2639130... 0.9400080... 0.0237668...]
2566 [ 539. 0.2770569... 0.9472574... 0.0219659...]
2567 [ 540. 0.2904 ... 0.954 ... 0.0203 ...]
2568 [ 541. 0.3039194... 0.9602409... 0.0187414...]
2569 [ 542. 0.3175893... 0.9660106... 0.0172748...]
2570 [ 543. 0.3314022... 0.9713260... 0.0158947...]
2571 [ 544. 0.3453666... 0.9761850... 0.0146001...]
2572 [ 545. 0.3595019... 0.9805731... 0.0133933...]
2573 [ 546. 0.3738324... 0.9844703... 0.0122777...]
2574 [ 547. 0.3883818... 0.9878583... 0.0112562...]
2575 [ 548. 0.4031674... 0.9907270... 0.0103302...]
2576 [ 549. 0.4181943... 0.9930817... 0.0094972...]
2577 [ 550. 0.43345 ... 0.99495 ... 0.00875 ...]
2578 [ 551. 0.4489082... 0.9963738... 0.0080748...]
2579 [ 552. 0.4645599... 0.9973682... 0.0074580...]
2580 [ 553. 0.4803950... 0.9979568... 0.0068902...]
2581 [ 554. 0.4963962... 0.9981802... 0.0063660...]
2582 [ 555. 0.5125410... 0.9980910... 0.0058818...]
2583 [ 556. 0.5288034... 0.9977488... 0.0054349...]
2584 [ 557. 0.5451560... 0.9972150... 0.0050216...]
2585 [ 558. 0.5615719... 0.9965479... 0.0046357...]
2586 [ 559. 0.5780267... 0.9957974... 0.0042671...]
2587 [ 560. 0.5945 ... 0.995 ... 0.0039 ...]]
2588 """
2590 for signal in self.signals.values():
2591 cast("SpectralDistribution", signal).trim(shape)
2593 return self
2595 def normalise(self, factor: Real = 1) -> Self:
2596 """
2597 Normalise the multi-spectral distributions with the specified normalization
2598 factor.
2600 Parameters
2601 ----------
2602 factor
2603 Normalization factor.
2605 Returns
2606 -------
2607 :class:`colour.MultiSpectralDistributions`
2608 Normalised multi- spectral distribution.
2610 Notes
2611 -----
2612 - The implementation uses the maximum value for each
2613 :class:`colour.SpectralDistribution` class instances.
2615 Examples
2616 --------
2617 >>> from colour.utilities import numpy_print_options
2618 >>> data = {
2619 ... 500: (0.004900, 0.323000, 0.272000),
2620 ... 510: (0.009300, 0.503000, 0.158200),
2621 ... 520: (0.063270, 0.710000, 0.078250),
2622 ... 530: (0.165500, 0.862000, 0.042160),
2623 ... 540: (0.290400, 0.954000, 0.020300),
2624 ... 550: (0.433450, 0.994950, 0.008750),
2625 ... 560: (0.594500, 0.995000, 0.003900),
2626 ... }
2627 >>> msds = MultiSpectralDistributions(data)
2628 >>> with numpy_print_options(suppress=True):
2629 ... print(msds.normalise()) # doctest: +ELLIPSIS
2630 [[ 500. 0.0082422... 0.3246231... 1. ...]
2631 [ 510. 0.0156434... 0.5055276... 0.5816176...]
2632 [ 520. 0.1064255... 0.7135678... 0.2876838...]
2633 [ 530. 0.2783852... 0.8663316... 0.155 ...]
2634 [ 540. 0.4884777... 0.9587939... 0.0746323...]
2635 [ 550. 0.7291000... 0.9999497... 0.0321691...]
2636 [ 560. 1. ... 1. ... 0.0143382...]]
2637 """
2639 for signal in self.signals.values():
2640 cast("SpectralDistribution", signal).normalise(factor)
2642 return self
2644 def to_sds(self) -> List[SpectralDistribution]:
2645 """
2646 Convert the multi-spectral distributions to a list of spectral
2647 distributions.
2649 Returns
2650 -------
2651 :class:`list`
2652 List of spectral distributions.
2654 Examples
2655 --------
2656 >>> from colour.utilities import numpy_print_options
2657 >>> data = {
2658 ... 500: (0.004900, 0.323000, 0.272000),
2659 ... 510: (0.009300, 0.503000, 0.158200),
2660 ... 520: (0.063270, 0.710000, 0.078250),
2661 ... 530: (0.165500, 0.862000, 0.042160),
2662 ... 540: (0.290400, 0.954000, 0.020300),
2663 ... 550: (0.433450, 0.994950, 0.008750),
2664 ... 560: (0.594500, 0.995000, 0.003900),
2665 ... }
2666 >>> msds = MultiSpectralDistributions(data)
2667 >>> with numpy_print_options(suppress=True):
2668 ... for sd in msds.to_sds():
2669 ... print(sd) # doctest: +ELLIPSIS
2670 [[ 500. 0.0049 ...]
2671 [ 510. 0.0093 ...]
2672 [ 520. 0.06327...]
2673 [ 530. 0.1655 ...]
2674 [ 540. 0.2904 ...]
2675 [ 550. 0.43345...]
2676 [ 560. 0.5945 ...]]
2677 [[ 500. 0.323 ...]
2678 [ 510. 0.503 ...]
2679 [ 520. 0.71 ...]
2680 [ 530. 0.862 ...]
2681 [ 540. 0.954 ...]
2682 [ 550. 0.99495...]
2683 [ 560. 0.995 ...]]
2684 [[ 500. 0.272 ...]
2685 [ 510. 0.1582 ...]
2686 [ 520. 0.07825...]
2687 [ 530. 0.04216...]
2688 [ 540. 0.0203 ...]
2689 [ 550. 0.00875...]
2690 [ 560. 0.0039 ...]]
2691 """
2693 return [
2694 cast("SpectralDistribution", signal.copy())
2695 for signal in self.signals.values()
2696 ]
2699_CACHE_RESHAPED_SDS_AND_MSDS: dict = CACHE_REGISTRY.register_cache(
2700 f"{__name__}._CACHE_RESHAPED_SDS_AND_MSDS"
2701)
2703TypeSpectralDistribution = TypeVar(
2704 "TypeSpectralDistribution", bound="SpectralDistribution"
2705)
2708def reshape_sd(
2709 sd: TypeSpectralDistribution,
2710 shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT,
2711 method: (Literal["Align", "Extrapolate", "Interpolate", "Trim"] | str) = "Align",
2712 copy: bool = True,
2713 **kwargs: Any,
2714) -> TypeSpectralDistribution:
2715 """
2716 Reshape the specified spectral distribution to match the specified spectral
2717 shape.
2719 The reshaped object is cached, thus another call to the definition with
2720 the same arguments will yield the cached object immediately.
2722 Parameters
2723 ----------
2724 sd
2725 Spectral distribution to reshape.
2726 shape
2727 Target spectral shape for reshaping the spectral distribution.
2728 method
2729 Method to use for reshaping.
2730 copy
2731 Whether to return a copy of the cached spectral distribution.
2732 Default is *True*.
2734 Other Parameters
2735 ----------------
2736 kwargs
2737 {:meth:`colour.SpectralDistribution.align`,
2738 :meth:`colour.SpectralDistribution.extrapolate`,
2739 :meth:`colour.SpectralDistribution.interpolate`,
2740 :meth:`colour.SpectralDistribution.trim`},
2741 See the documentation of the previously listed methods.
2743 Returns
2744 -------
2745 :class:`colour.SpectralDistribution`
2747 Warnings
2748 --------
2749 Contrary to *Numpy*, reshaping a spectral distribution alters its data!
2750 """
2752 method = validate_method(
2753 method, valid_methods=("Align", "Extrapolate", "Interpolate", "Trim")
2754 )
2756 # Handling dict-like keyword arguments.
2757 kwargs_items = list(kwargs.items())
2758 for i, (keyword, value) in enumerate(kwargs_items):
2759 if isinstance(value, Mapping):
2760 kwargs_items[i] = (keyword, tuple(value.items()))
2762 hash_key = hash((sd, shape, method, tuple(kwargs_items)))
2764 if is_caching_enabled() and hash_key in _CACHE_RESHAPED_SDS_AND_MSDS:
2765 reshaped_sd = _CACHE_RESHAPED_SDS_AND_MSDS[hash_key]
2767 return reshaped_sd.copy() if copy else reshaped_sd
2769 function = getattr(sd, method)
2771 reshaped_sd = getattr(sd.copy(), method)(shape, **filter_kwargs(function, **kwargs))
2773 _CACHE_RESHAPED_SDS_AND_MSDS[hash_key] = reshaped_sd
2775 return reshaped_sd
2778TypeMultiSpectralDistributions = TypeVar(
2779 "TypeMultiSpectralDistributions", bound="MultiSpectralDistributions"
2780)
2783def reshape_msds(
2784 msds: TypeMultiSpectralDistributions,
2785 shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT,
2786 method: (Literal["Align", "Extrapolate", "Interpolate", "Trim"] | str) = "Align",
2787 copy: bool = True,
2788 **kwargs: Any,
2789) -> TypeMultiSpectralDistributions:
2790 """
2791 Reshape the specified multi-spectral distributions to match the specified
2792 spectral shape.
2794 The reshaped object is cached, thus another call to the definition with
2795 the same arguments will yield the cached object immediately.
2797 Parameters
2798 ----------
2799 msds
2800 Multi-spectral distributions to reshape.
2801 shape
2802 Target spectral shape for reshaping the multi-spectral distributions.
2803 method
2804 Method to use for reshaping.
2805 copy
2806 Whether to return a copy of the cached multi-spectral distributions.
2807 Default is *True*.
2809 Other Parameters
2810 ----------------
2811 kwargs
2812 {:meth:`colour.MultiSpectralDistributions.align`,
2813 :meth:`colour.MultiSpectralDistributions.extrapolate`,
2814 :meth:`colour.MultiSpectralDistributions.interpolate`,
2815 :meth:`colour.MultiSpectralDistributions.trim`},
2816 See the documentation of the previously listed methods.
2818 Returns
2819 -------
2820 :class:`colour.MultiSpectralDistributions`
2822 Warnings
2823 --------
2824 Contrary to *Numpy*, reshaping multi-spectral distributions alters their
2825 data!
2826 """
2828 return reshape_sd(msds, shape, method, copy, **kwargs) # pyright: ignore
2831def sds_and_msds_to_sds(
2832 sds: (
2833 Sequence[SpectralDistribution | MultiSpectralDistributions]
2834 | SpectralDistribution
2835 | MultiSpectralDistributions
2836 | ValuesView
2837 ),
2838) -> List[SpectralDistribution]:
2839 """
2840 Convert specified spectral and multi-spectral distributions to a list of
2841 spectral distributions.
2843 Parameters
2844 ----------
2845 sds
2846 Spectral and multi-spectral distributions to convert to a list of
2847 spectral distributions. Each multi-spectral distribution is expanded
2848 into its constituent spectral distributions.
2850 Returns
2851 -------
2852 :class:`list`
2853 List of spectral distributions where multi-spectral distributions
2854 have been expanded into individual spectral distributions.
2856 Examples
2857 --------
2858 >>> data = {
2859 ... 500: 0.0651,
2860 ... 520: 0.0705,
2861 ... 540: 0.0772,
2862 ... 560: 0.0870,
2863 ... 580: 0.1128,
2864 ... 600: 0.1360,
2865 ... }
2866 >>> sd_1 = SpectralDistribution(data)
2867 >>> sd_2 = SpectralDistribution(data)
2868 >>> data = {
2869 ... 500: (0.004900, 0.323000, 0.272000),
2870 ... 510: (0.009300, 0.503000, 0.158200),
2871 ... 520: (0.063270, 0.710000, 0.078250),
2872 ... 530: (0.165500, 0.862000, 0.042160),
2873 ... 540: (0.290400, 0.954000, 0.020300),
2874 ... 550: (0.433450, 0.994950, 0.008750),
2875 ... 560: (0.594500, 0.995000, 0.003900),
2876 ... }
2877 >>> multi_sds_1 = MultiSpectralDistributions(data)
2878 >>> multi_sds_2 = MultiSpectralDistributions(data)
2879 >>> len(sds_and_msds_to_sds([sd_1, sd_2, multi_sds_1, multi_sds_2]))
2880 8
2881 """
2883 if isinstance(sds, SpectralDistribution):
2884 return sds_and_msds_to_sds([sds])
2886 if isinstance(sds, MultiSpectralDistributions):
2887 sds_converted = sds.to_sds()
2888 else:
2889 sds_converted = []
2891 for sd in sds:
2892 sds_converted += (
2893 sd.to_sds() if isinstance(sd, MultiSpectralDistributions) else [sd]
2894 )
2896 return sds_converted
2899def sds_and_msds_to_msds(
2900 sds: (
2901 Sequence[SpectralDistribution | MultiSpectralDistributions]
2902 | SpectralDistribution
2903 | MultiSpectralDistributions
2904 | ValuesView
2905 ),
2906) -> MultiSpectralDistributions:
2907 """
2908 Convert specified spectral and multi-spectral distributions to
2909 multi-spectral distributions.
2911 The spectral and multi-spectral distributions will be aligned to the
2912 intersection of their spectral shapes.
2914 Parameters
2915 ----------
2916 sds
2917 Spectral and multi-spectral distributions to convert to
2918 multi-spectral distributions.
2920 Returns
2921 -------
2922 :class:`colour.MultiSpectralDistributions`
2923 Multi-spectral distributions.
2925 Examples
2926 --------
2927 >>> data = {
2928 ... 500: 0.0651,
2929 ... 520: 0.0705,
2930 ... 540: 0.0772,
2931 ... 560: 0.0870,
2932 ... 580: 0.1128,
2933 ... 600: 0.1360,
2934 ... }
2935 >>> sd_1 = SpectralDistribution(data)
2936 >>> sd_2 = SpectralDistribution(data)
2937 >>> data = {
2938 ... 500: (0.004900, 0.323000, 0.272000),
2939 ... 510: (0.009300, 0.503000, 0.158200),
2940 ... 520: (0.063270, 0.710000, 0.078250),
2941 ... 530: (0.165500, 0.862000, 0.042160),
2942 ... 540: (0.290400, 0.954000, 0.020300),
2943 ... 550: (0.433450, 0.994950, 0.008750),
2944 ... 560: (0.594500, 0.995000, 0.003900),
2945 ... }
2946 >>> multi_sds_1 = MultiSpectralDistributions(data)
2947 >>> multi_sds_2 = MultiSpectralDistributions(data)
2948 >>> from colour.utilities import numpy_print_options
2949 >>> with numpy_print_options(suppress=True, linewidth=160):
2950 ... sds_and_msds_to_msds( # doctest: +SKIP
2951 ... [sd_1, sd_2, multi_sds_1, multi_sds_2]
2952 ... )
2953 ...
2954 MultiSpectralDistributions([[ 500. , 0.0651 ...,\
29550.0651 ..., 0.0049 ..., 0.323 ..., 0.272 ...,\
29560.0049 ..., 0.323 ..., 0.272 ...],
2957 [ 510. , 0.0676692...,\
29580.0676692..., 0.0093 ..., 0.503 ..., 0.1582 ...,\
29590.0093 ..., 0.503 ..., 0.1582 ...],
2960 [ 520. , 0.0705 ...,\
29610.0705 ..., 0.06327 ..., 0.71 ..., 0.07825 ...,\
29620.06327 ..., 0.71 ..., 0.07825 ...],
2963 [ 530. , 0.0737808...,\
29640.0737808..., 0.1655 ..., 0.862 ..., 0.04216 ...,\
29650.1655 ..., 0.862 ..., 0.04216 ...],
2966 [ 540. , 0.0772 ...,\
29670.0772 ..., 0.2904 ..., 0.954 ..., 0.0203 ...,\
29680.2904 ..., 0.954 ..., 0.0203 ...],
2969 [ 550. , 0.0806671...,\
29700.0806671..., 0.43345 ..., 0.99495 ..., 0.00875 ...,\
29710.43345 ..., 0.99495 ..., 0.00875 ...],
2972 [ 560. , 0.087 ...,\
29730.087 ..., 0.5945 ..., 0.995 ..., 0.0039 ...,\
29740.5945 ..., 0.995 ..., 0.0039 ...]],
2975 labels=['SpectralDistribution (...)', \
2976'SpectralDistribution (...)', '0 - SpectralDistribution (...)', \
2977'1 - SpectralDistribution (...)', '2 - SpectralDistribution (...)', \
2978'0 - SpectralDistribution (...)', '1 - SpectralDistribution (...)', \
2979'2 - SpectralDistribution (...)'],
2980 interpolator=SpragueInterpolator,
2981 interpolator_kwargs={},
2982 extrapolator=Extrapolator,
2983 extrapolator_kwargs={...})
2984 """
2986 if isinstance(sds, SpectralDistribution):
2987 return sds_and_msds_to_msds([sds])
2989 if isinstance(sds, MultiSpectralDistributions):
2990 msds_converted = sds
2991 else:
2992 sds_converted = sds_and_msds_to_sds(sds)
2994 shapes = tuple({sd.shape for sd in sds_converted})
2995 shape = SpectralShape(
2996 max(shape.start for shape in shapes),
2997 min(shape.end for shape in shapes),
2998 min(shape.interval for shape in shapes),
2999 )
3001 values = []
3002 labels = []
3003 display_labels = []
3004 for sd in sds_converted:
3005 if sd.shape != shape:
3006 sd = sd.copy().align(shape) # noqa: PLW2901
3008 values.append(sd.values)
3009 labels.append(sd.name if sd.name not in labels else f"{sd.name} ({id(sd)})")
3010 display_labels.append(
3011 sd.display_name
3012 if sd.display_name not in display_labels
3013 else f"{sd.display_name} ({id(sd)})"
3014 )
3016 msds_converted = MultiSpectralDistributions(
3017 tstack(values),
3018 shape.wavelengths,
3019 labels,
3020 display_labels=display_labels,
3021 )
3023 return msds_converted