Source code for colour.colorimetry.spectrum

"""
Spectrum
========

Define classes and objects for handling spectral data computations.

-   :class:`colour.SPECTRAL_SHAPE_DEFAULT`
-   :class:`colour.SpectralShape`
-   :class:`colour.SpectralDistribution`
-   :class:`colour.MultiSpectralDistributions`
-   :func:`colour.colorimetry.sds_and_msds_to_sds`
-   :func:`colour.colorimetry.sds_and_msds_to_msds`
-   :func:`colour.colorimetry.reshape_sd`
-   :func:`colour.colorimetry.reshape_msds`

References
----------
-   :cite:`CIETC1-382005e` : CIE TC 1-38. (2005). 9. INTERPOLATION. In CIE
    167:2005 Recommended Practice for Tabulating Spectral Data for Use in
    Colour Computations (pp. 14-19). ISBN:978-3-901906-41-1
-   :cite:`CIETC1-382005g` : CIE TC 1-38. (2005). EXTRAPOLATION. In CIE
    167:2005 Recommended Practice for Tabulating Spectral Data for Use in
    Colour Computations (pp. 19-20). ISBN:978-3-901906-41-1
-   :cite:`CIETC1-482004l` : CIE TC 1-48. (2004). Extrapolation. In CIE
    015:2004 Colorimetry, 3rd Edition (p. 24). ISBN:978-3-901906-33-6
"""

from __future__ import annotations

import typing
from collections.abc import KeysView, Mapping, ValuesView

import numpy as np

from colour.algebra import (
    CubicSplineInterpolator,
    Extrapolator,
    SpragueInterpolator,
    sdiv,
    sdiv_mode,
)
from colour.constants import DTYPE_FLOAT_DEFAULT
from colour.continuous import MultiSignals, Signal

if typing.TYPE_CHECKING:
    from colour.hints import (
        ArrayLike,
        DTypeFloat,
        Generator,
        List,
        Literal,
        NDArrayFloat,
        ProtocolExtrapolator,
        ProtocolInterpolator,
        Real,
        Self,
        Sequence,
        Type,
        TypeVar,
    )

from colour.hints import Any, TypeVar, cast
from colour.utilities import (
    CACHE_REGISTRY,
    as_float_array,
    as_int,
    attest,
    filter_kwargs,
    first_item,
    interval,
    is_caching_enabled,
    is_iterable,
    is_numeric,
    is_pandas_installed,
    is_uniform,
    optional,
    runtime_warning,
    tstack,
    validate_method,
)

if typing.TYPE_CHECKING or is_pandas_installed():
    from pandas import DataFrame, Series  # pragma: no cover
else:  # pragma: no cover
    from unittest import mock

    DataFrame = mock.MagicMock()
    Series = mock.MagicMock()

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "SpectralShape",
    "SPECTRAL_SHAPE_DEFAULT",
    "SpectralDistribution",
    "MultiSpectralDistributions",
    "reshape_sd",
    "reshape_msds",
    "sds_and_msds_to_sds",
    "sds_and_msds_to_msds",
]

_CACHE_SHAPE_RANGE: dict = CACHE_REGISTRY.register_cache(
    f"{__name__}._CACHE_SHAPE_RANGE"
)


[docs] class SpectralShape: """ Define the base object for spectral distribution shape. The :class:`colour.SpectralShape` class represents the shape of spectral data by defining its wavelength range and sampling interval. It provides a structured way to handle spectral data boundaries and generate wavelength arrays for spectral computations. Parameters ---------- start Wavelength :math:`\\lambda_{i}` range start in nm. end Wavelength :math:`\\lambda_{i}` range end in nm. interval Wavelength :math:`\\lambda_{i}` range interval. Attributes ---------- - :attr:`~colour.SpectralShape.start` - :attr:`~colour.SpectralShape.end` - :attr:`~colour.SpectralShape.interval` - :attr:`~colour.SpectralShape.boundaries` - :attr:`~colour.SpectralShape.wavelengths` Methods ------- - :meth:`~colour.SpectralShape.__init__` - :meth:`~colour.SpectralShape.__str__` - :meth:`~colour.SpectralShape.__repr__` - :meth:`~colour.SpectralShape.__hash__` - :meth:`~colour.SpectralShape.__iter__` - :meth:`~colour.SpectralShape.__contains__` - :meth:`~colour.SpectralShape.__len__` - :meth:`~colour.SpectralShape.__eq__` - :meth:`~colour.SpectralShape.__ne__` - :meth:`~colour.SpectralShape.range` Examples -------- >>> SpectralShape(360, 830, 1) SpectralShape(360, 830, 1) """
[docs] def __init__(self, start: Real, end: Real, interval: Real) -> None: self._start: Real = 0 self._end: Real = np.inf self._interval: Real = 1 self.start = start self.end = end self.interval = interval
@property def start(self) -> Real: """ Getter and setter for the spectral shape start. Parameters ---------- value Value to set the spectral shape start wavelength with. Returns ------- Real Start wavelength of the spectral shape in nanometres. """ return self._start @start.setter def start(self, value: Real) -> None: """Setter for the **self.start** property.""" attest( is_numeric(value), f'"start" property: "{value}" is not a "number"!', ) attest( bool(value < self._end), f'"start" attribute value must be strictly less than "{self._end}"!', ) self._start = value @property def end(self) -> Real: """ Getter and setter for the spectral shape end. Parameters ---------- value Value to set the spectral shape end wavelength with. Returns ------- Real End wavelength of the spectral shape in nanometres. . """ return self._end @end.setter def end(self, value: Real) -> None: """Setter for the **self.end** property.""" attest( is_numeric(value), f'"end" property: "{value}" is not a "number"!', ) attest( bool(value > self._start), f'"end" attribute value must be strictly greater than "{self._start}"!', ) self._end = value @property def interval(self) -> Real: """ Getter and setter for the spectral shape interval. The interval defines the wavelength spacing between consecutive samples in the spectral distribution. Parameters ---------- value Value to set the spectral shape interval with. Returns ------- Real Spectral shape interval. """ return self._interval @interval.setter def interval(self, value: Real) -> None: """Setter for the **self.interval** property.""" attest( is_numeric(value), f'"interval" property: "{value}" is not a "number"!', ) self._interval = value @property def boundaries(self) -> tuple: """ Getter and setter for the boundaries of the spectral shape. The boundaries define the start and end points of the spectral range as a tuple of two values. Parameters ---------- value Value to set the spectral shape boundaries with. Returns ------- :class:`tuple` Spectral shape boundaries. """ return self._start, self._end @boundaries.setter def boundaries(self, value: ArrayLike) -> None: """Setter for the **self.boundaries** property.""" value = np.asarray(value) attest( value.size == 2, f'"boundaries" property: "{value}" must have exactly two elements!', ) self.start, self.end = value @property def wavelengths(self) -> NDArrayFloat: """ Getter for the spectral shape wavelengths. Returns ------- :class:`numpy.ndarray` Spectral shape wavelengths. """ return self.range()
[docs] def __str__(self) -> str: """ Return a formatted string representation of the spectral shape. Returns ------- :class:`str` Formatted string representation. """ return f"({self._start}, {self._end}, {self._interval})"
[docs] def __repr__(self) -> str: """ Return an evaluable string representation of the spectral shape. Returns ------- :class:`str` Evaluable string representation. """ return f"SpectralShape({self._start}, {self._end}, {self._interval})"
[docs] def __hash__(self) -> int: """ Return the hash value of the spectral shape. The hash is computed based on the spectral shape's start wavelength, end wavelength, and wavelength interval. Returns ------- :class:`int` Hash value of the spectral shape. """ return hash((self.start, self.end, self.interval))
[docs] def __iter__(self) -> Generator: """ Generate wavelengths for the spectral shape range. Yields ------ Generator Wavelength values from start to end at the specified interval. Examples -------- >>> shape = SpectralShape(0, 10, 1) >>> for wavelength in shape: ... print(wavelength) 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 """ yield from self.wavelengths
[docs] def __contains__(self, wavelength: ArrayLike) -> bool: """ Determine if the spectral shape contains the specified wavelength :math:`\\lambda`. Parameters ---------- wavelength Wavelength :math:`\\lambda` to check for containment. Returns ------- :class:`bool` Whether the wavelength :math:`\\lambda` is contained within the spectral shape. Examples -------- >>> 0.5 in SpectralShape(0, 10, 0.1) True >>> 0.6 in SpectralShape(0, 10, 0.1) True >>> 0.51 in SpectralShape(0, 10, 0.1) False >>> np.array([0.5, 0.6]) in SpectralShape(0, 10, 0.1) True >>> np.array([0.51, 0.6]) in SpectralShape(0, 10, 0.1) False """ decimals = np.finfo(cast("Any", DTYPE_FLOAT_DEFAULT)).precision return bool( np.all( np.isin( np.around( wavelength, # pyright: ignore decimals, ), np.around( self.wavelengths, decimals, ), ) ) )
[docs] def __len__(self) -> int: """ Return the spectral shape wavelength :math:`\\lambda_n` count. Returns ------- :class:`int` Spectral shape wavelength :math:`\\lambda_n` count. Examples -------- >>> len(SpectralShape(0, 10, 0.1)) 101 """ return len(self.wavelengths)
[docs] def __eq__(self, other: object) -> bool: """ Determine whether the spectral shape is equal to the specified other object. Parameters ---------- other Object to determine whether it is equal to the spectral shape. Returns ------- :class:`bool` Whether the specified object is equal to the spectral shape. Examples -------- >>> SpectralShape(0, 10, 0.1) == SpectralShape(0, 10, 0.1) True >>> SpectralShape(0, 10, 0.1) == SpectralShape(0, 10, 1) False """ if isinstance(other, SpectralShape): return np.array_equal(self.wavelengths, other.wavelengths) return False
[docs] def __ne__(self, other: object) -> bool: """ Determine whether the spectral shape is not equal to the specified other object. Parameters ---------- other Object to determine whether it is not equal to the spectral shape. Returns ------- :class:`bool` Whether the specified object is not equal to the spectral shape. Examples -------- >>> SpectralShape(0, 10, 0.1) != SpectralShape(0, 10, 0.1) False >>> SpectralShape(0, 10, 0.1) != SpectralShape(0, 10, 1) True """ return not (self == other)
[docs] def range(self, dtype: Type[DTypeFloat] | None = None) -> NDArrayFloat: """ Return an iterable range for the spectral shape. Parameters ---------- dtype Data type used to generate the range. Returns ------- :class:`numpy.ndarray` Iterable range for the spectral distribution shape. Examples -------- >>> SpectralShape(0, 10, 0.1).wavelengths array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. , 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2. , 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3. , 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 4. , 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 5. , 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 6. , 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 7. , 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8, 7.9, 8. , 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 9. , 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9, 10. ]) """ dtype = optional(dtype, DTYPE_FLOAT_DEFAULT) hash_key = hash((self, dtype)) if is_caching_enabled() and hash_key in _CACHE_SHAPE_RANGE: return _CACHE_SHAPE_RANGE[hash_key].copy() start, end, interval = ( dtype(self._start), dtype(self._end), dtype(self._interval), ) samples = as_int(round((interval + end - start) / interval)) range_, interval_effective = np.linspace( start, end, samples, retstep=True, dtype=dtype ) _CACHE_SHAPE_RANGE[hash_key] = range_ if interval_effective != self._interval: self._interval = cast("float", interval_effective) runtime_warning( f'"{(start, end, interval)}" shape could not be honoured, ' f'using "{self}"!' ) return range_
SPECTRAL_SHAPE_DEFAULT: SpectralShape = SpectralShape(360, 780, 1) """Default spectral shape according to *ASTM E308-15* practise shape."""
[docs] class SpectralDistribution(Signal): """ Define the spectral distribution: the base object for spectral computations. Initialise spectral distribution according to *CIE 15:2004* recommendation: use the method developed by *Sprague (1880)* for interpolating functions having uniformly spaced independent variables and the *Cubic Spline* method for non-uniformly spaced independent variables. Perform extrapolation according to *CIE 167:2005* recommendation. .. important:: Specific documentation about getting, setting, indexing and slicing the spectral power distribution values is available in the :ref:`spectral-representation-and-continuous-signal` section. Parameters ---------- data Data to be stored in the spectral distribution. domain Values to initialise the :attr:`colour.SpectralDistribution.wavelength` property with. If both ``data`` and ``domain`` arguments are defined, the latter will be used to initialise the :attr:`colour.SpectralDistribution.wavelength` property. Other Parameters ---------------- extrapolator Extrapolator class type to use as extrapolating function. extrapolator_kwargs Arguments to use when instantiating the extrapolating function. interpolator Interpolator class type to use as interpolating function. interpolator_kwargs Arguments to use when instantiating the interpolating function. name Spectral distribution name. display_name Spectral distribution name for figures, default to :attr:`colour.SpectralDistribution.name` property value. Warnings -------- The *Cubic Spline* method might produce unexpected results with exceptionally noisy or non-uniformly spaced data. Attributes ---------- - :attr:`~colour.SpectralDistribution.display_name` - :attr:`~colour.SpectralDistribution.wavelengths` - :attr:`~colour.SpectralDistribution.values` - :attr:`~colour.SpectralDistribution.shape` Methods ------- - :meth:`~colour.SpectralDistribution.__init__` - :meth:`~colour.SpectralDistribution.interpolate` - :meth:`~colour.SpectralDistribution.extrapolate` - :meth:`~colour.SpectralDistribution.align` - :meth:`~colour.SpectralDistribution.trim` - :meth:`~colour.SpectralDistribution.normalise` References ---------- :cite:`CIETC1-382005e`, :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l` Examples -------- Instantiating a spectral distribution with a uniformly spaced independent variable: >>> from colour.utilities import numpy_print_options >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... } >>> with numpy_print_options(suppress=True): ... SpectralDistribution(data) # doctest: +ELLIPSIS SpectralDistribution([[ 500. , 0.0651], [ 520. , 0.0705], [ 540. , 0.0772], [ 560. , 0.087 ], [ 580. , 0.1128], [ 600. , 0.136 ]], SpragueInterpolator, {}, Extrapolator, {'method': 'Constant', 'left': None, 'right': None}) Instantiating a spectral distribution with a non-uniformly spaced independent variable: >>> data[510] = 0.31416 >>> with numpy_print_options(suppress=True): ... SpectralDistribution(data) # doctest: +ELLIPSIS SpectralDistribution([[ 500. , 0.0651 ], [ 510. , 0.31416], [ 520. , 0.0705 ], [ 540. , 0.0772 ], [ 560. , 0.087 ], [ 580. , 0.1128 ], [ 600. , 0.136 ]], CubicSplineInterpolator, {}, Extrapolator, {'method': 'Constant', 'left': None, 'right': None}) Instantiation with a *Pandas* :class:`pandas.Series`: >>> from colour.utilities import is_pandas_installed >>> if is_pandas_installed(): ... from pandas import Series ... ... print(SpectralDistribution(Series(data))) # doctest: +SKIP [[ 5.0000000...e+02 6.5100000...e-02] [ 5.2000000...e+02 7.0500000...e-02] [ 5.4000000...e+02 7.7200000...e-02] [ 5.6000000...e+02 8.7000000...e-02] [ 5.8000000...e+02 1.1280000...e-01] [ 6.0000000...e+02 1.3600000...e-01] [ 5.1000000...e+02 3.1416000...e-01]] """
[docs] def __init__( self, data: ArrayLike | dict | Series | Signal | ValuesView | None = None, domain: ArrayLike | SpectralShape | KeysView | None = None, **kwargs: Any, ) -> None: domain = domain.wavelengths if isinstance(domain, SpectralShape) else domain domain_unpacked, range_unpacked = self.signal_unpack_data(data, domain) # Initialising with *CIE 15:2004* and *CIE 167:2005* recommendations # defaults. kwargs["interpolator"] = kwargs.get( "interpolator", ( SpragueInterpolator if domain_unpacked.size != 0 and is_uniform(domain_unpacked) else CubicSplineInterpolator ), ) kwargs["interpolator_kwargs"] = kwargs.get("interpolator_kwargs", {}) kwargs["extrapolator"] = kwargs.get("extrapolator", Extrapolator) kwargs["extrapolator_kwargs"] = kwargs.get( "extrapolator_kwargs", {"method": "Constant", "left": None, "right": None}, ) super().__init__(range_unpacked, domain_unpacked, **kwargs) self._display_name: str = self.name self.display_name = kwargs.get("display_name", self._display_name) self._shape: SpectralShape | None = None self.register_callback("_domain", "on_domain_changed", self._on_domain_changed)
@staticmethod def _on_domain_changed( sd: SpectralDistribution, name: str, value: NDArrayFloat ) -> NDArrayFloat: """ Invalidate the cached spectral shape when the spectral distribution domain is modified. This callback ensures that the internal *_shape* attribute is reset to *None* whenever the domain values change, maintaining consistency between the domain and its derived shape representation. Parameters ---------- sd Spectral distribution instance whose domain has changed. name Name of the modified attribute (expected to be "_domain"). value New domain values that triggered the callback. Returns ------- :class:`numpy.ndarray` The specified domain values, unchanged. """ if name == "_domain": sd._shape = None return value @property def display_name(self) -> str: """ Getter and setter for the spectral distribution's display name. The display name provides a human-readable identifier for the spectral distribution, used for visualization and reporting purposes. Parameters ---------- value Value to set the spectral distribution's display name with. Returns ------- :class:`str` Spectral distribution's display name. """ return self._display_name @display_name.setter def display_name(self, value: str) -> None: """Setter for the **self.display_name** property.""" attest( isinstance(value, str), f'"display_name" property: "{value}" type is not "str"!', ) self._display_name = value @property def wavelengths(self) -> NDArrayFloat: """ Getter and setter for the spectral distribution wavelengths :math:`\\lambda_n`. Parameters ---------- value Value to set the spectral distribution wavelengths :math:`\\lambda_n` with. Returns ------- :class:`numpy.ndarray` Spectral distribution wavelengths :math:`\\lambda_n`. """ return self.domain @wavelengths.setter def wavelengths(self, value: ArrayLike) -> None: """Setter for the **self.wavelengths** property.""" self.domain = as_float_array(value, self.dtype) @property def values(self) -> NDArrayFloat: """ Getter and setter for the spectral distribution values. Parameters ---------- value Value to set the spectral distribution wavelengths values with. Returns ------- :class:`numpy.ndarray` Spectral distribution values. """ return self.range @values.setter def values(self, value: ArrayLike) -> None: """Setter for the **self.values** property.""" self.range = as_float_array(value, self.dtype) @property def shape(self) -> SpectralShape: """ Getter property for the spectral distribution shape. Returns ------- :class:`colour.SpectralShape` Spectral distribution shape. Notes ----- - A spectral distribution with a non-uniformly spaced independent variable have multiple intervals, in that case :attr:`colour.SpectralDistribution.shape` property returns the *minimum* interval size. Examples -------- Shape of a spectral distribution with a uniformly spaced independent variable: >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... } >>> SpectralDistribution(data).shape SpectralShape(500.0, 600.0, 20.0) Shape of a spectral distribution with a non-uniformly spaced independent variable: >>> data[510] = 0.31416 >>> SpectralDistribution(data).shape SpectralShape(500.0, 600.0, 10.0) """ if self._shape is None: wavelengths = self.wavelengths wavelengths_interval = interval(wavelengths) if wavelengths_interval.size != 1: runtime_warning( f'"{self.name}" spectral distribution is not uniform, ' "using minimum interval!" ) self._shape = SpectralShape( wavelengths[0], wavelengths[-1], min(wavelengths_interval) ) return self._shape
[docs] def interpolate( self, shape: SpectralShape, interpolator: Type[ProtocolInterpolator] | None = None, interpolator_kwargs: dict | None = None, ) -> Self: """ Interpolate the spectral distribution in-place according to *CIE 167:2005* recommendation (if the interpolator has not been changed at instantiation time) or specified interpolation arguments. The logic for choosing the interpolator class when ``interpolator`` is not specified is as follows: .. code-block:: python if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator = self.interpolator elif self.is_uniform(): interpolator = SpragueInterpolator else: interpolator = CubicSplineInterpolator The logic for choosing the interpolator keyword arguments when ``interpolator_kwargs`` is not specified is as follows: .. code-block:: python if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator_kwargs = self.interpolator_kwargs else: interpolator_kwargs = {} Parameters ---------- shape Spectral shape used for interpolation. interpolator Interpolator class type to use as interpolating function. interpolator_kwargs Arguments to use when instantiating the interpolating function. Returns ------- :class:`colour.SpectralDistribution` Interpolated spectral distribution. Notes ----- - Interpolation will be performed over boundaries range, if it is required to extend the range of the spectral distribution use the :meth:`colour.SpectralDistribution.extrapolate` or :meth:`colour.SpectralDistribution.align` methods. Warnings -------- - *Cubic Spline* interpolator requires at least 3 wavelengths :math:`\\lambda_n` for interpolation. - *Sprague (1880)* interpolator requires at least 6 wavelengths :math:`\\lambda_n` for interpolation. References ---------- :cite:`CIETC1-382005e` Examples -------- Spectral distribution with a uniformly spaced independent variable uses *Sprague (1880)* interpolation: >>> from colour.utilities import numpy_print_options >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... } >>> sd = SpectralDistribution(data) >>> with numpy_print_options(suppress=True): ... print(sd.interpolate(SpectralShape(500, 600, 1))) ... # doctest: +ELLIPSIS [[ 500. 0.0651 ...] [ 501. 0.0653522...] [ 502. 0.0656105...] [ 503. 0.0658715...] [ 504. 0.0661328...] [ 505. 0.0663929...] [ 506. 0.0666509...] [ 507. 0.0669069...] [ 508. 0.0671613...] [ 509. 0.0674150...] [ 510. 0.0676692...] [ 511. 0.0679253...] [ 512. 0.0681848...] [ 513. 0.0684491...] [ 514. 0.0687197...] [ 515. 0.0689975...] [ 516. 0.0692832...] [ 517. 0.0695771...] [ 518. 0.0698787...] [ 519. 0.0701870...] [ 520. 0.0705 ...] [ 521. 0.0708155...] [ 522. 0.0711336...] [ 523. 0.0714547...] [ 524. 0.0717789...] [ 525. 0.0721063...] [ 526. 0.0724367...] [ 527. 0.0727698...] [ 528. 0.0731051...] [ 529. 0.0734423...] [ 530. 0.0737808...] [ 531. 0.0741203...] [ 532. 0.0744603...] [ 533. 0.0748006...] [ 534. 0.0751409...] [ 535. 0.0754813...] [ 536. 0.0758220...] [ 537. 0.0761633...] [ 538. 0.0765060...] [ 539. 0.0768511...] [ 540. 0.0772 ...] [ 541. 0.0775527...] [ 542. 0.0779042...] [ 543. 0.0782507...] [ 544. 0.0785908...] [ 545. 0.0789255...] [ 546. 0.0792576...] [ 547. 0.0795917...] [ 548. 0.0799334...] [ 549. 0.0802895...] [ 550. 0.0806671...] [ 551. 0.0810740...] [ 552. 0.0815176...] [ 553. 0.0820049...] [ 554. 0.0825423...] [ 555. 0.0831351...] [ 556. 0.0837873...] [ 557. 0.0845010...] [ 558. 0.0852763...] [ 559. 0.0861110...] [ 560. 0.087 ...] [ 561. 0.0879383...] [ 562. 0.0889300...] [ 563. 0.0899793...] [ 564. 0.0910876...] [ 565. 0.0922541...] [ 566. 0.0934760...] [ 567. 0.0947487...] [ 568. 0.0960663...] [ 569. 0.0974220...] [ 570. 0.0988081...] [ 571. 0.1002166...] [ 572. 0.1016394...] [ 573. 0.1030687...] [ 574. 0.1044972...] [ 575. 0.1059186...] [ 576. 0.1073277...] [ 577. 0.1087210...] [ 578. 0.1100968...] [ 579. 0.1114554...] [ 580. 0.1128 ...] [ 581. 0.1141333...] [ 582. 0.1154495...] [ 583. 0.1167424...] [ 584. 0.1180082...] [ 585. 0.1192452...] [ 586. 0.1204536...] [ 587. 0.1216348...] [ 588. 0.1227915...] [ 589. 0.1239274...] [ 590. 0.1250465...] [ 591. 0.1261531...] [ 592. 0.1272517...] [ 593. 0.1283460...] [ 594. 0.1294393...] [ 595. 0.1305340...] [ 596. 0.1316310...] [ 597. 0.1327297...] [ 598. 0.1338277...] [ 599. 0.1349201...] [ 600. 0.136 ...]] Spectral distribution with a non-uniformly spaced independent variable uses *Cubic Spline* interpolation: >>> sd = SpectralDistribution(data) >>> sd[510] = np.pi / 10 >>> with numpy_print_options(suppress=True): ... print(sd.interpolate(SpectralShape(500, 600, 1))) ... # doctest: +ELLIPSIS [[ 500. 0.0651 ...] [ 501. 0.1365202...] [ 502. 0.1953263...] [ 503. 0.2423724...] [ 504. 0.2785126...] [ 505. 0.3046010...] [ 506. 0.3214916...] [ 507. 0.3300387...] [ 508. 0.3310962...] [ 509. 0.3255184...] [ 510. 0.3141592...] [ 511. 0.2978729...] [ 512. 0.2775135...] [ 513. 0.2539351...] [ 514. 0.2279918...] [ 515. 0.2005378...] [ 516. 0.1724271...] [ 517. 0.1445139...] [ 518. 0.1176522...] [ 519. 0.0926962...] [ 520. 0.0705 ...] [ 521. 0.0517370...] [ 522. 0.0363589...] [ 523. 0.0241365...] [ 524. 0.0148407...] [ 525. 0.0082424...] [ 526. 0.0041126...] [ 527. 0.0022222...] [ 528. 0.0023421...] [ 529. 0.0042433...] [ 530. 0.0076966...] [ 531. 0.0124729...] [ 532. 0.0183432...] [ 533. 0.0250785...] [ 534. 0.0324496...] [ 535. 0.0402274...] [ 536. 0.0481829...] [ 537. 0.0560870...] [ 538. 0.0637106...] [ 539. 0.0708246...] [ 540. 0.0772 ...] [ 541. 0.0826564...] [ 542. 0.0872086...] [ 543. 0.0909203...] [ 544. 0.0938549...] [ 545. 0.0960760...] [ 546. 0.0976472...] [ 547. 0.0986321...] [ 548. 0.0990942...] [ 549. 0.0990971...] [ 550. 0.0987043...] [ 551. 0.0979794...] [ 552. 0.0969861...] [ 553. 0.0957877...] [ 554. 0.0944480...] [ 555. 0.0930304...] [ 556. 0.0915986...] [ 557. 0.0902161...] [ 558. 0.0889464...] [ 559. 0.0878532...] [ 560. 0.087 ...] [ 561. 0.0864371...] [ 562. 0.0861623...] [ 563. 0.0861600...] [ 564. 0.0864148...] [ 565. 0.0869112...] [ 566. 0.0876336...] [ 567. 0.0885665...] [ 568. 0.0896945...] [ 569. 0.0910020...] [ 570. 0.0924735...] [ 571. 0.0940936...] [ 572. 0.0958467...] [ 573. 0.0977173...] [ 574. 0.0996899...] [ 575. 0.1017491...] [ 576. 0.1038792...] [ 577. 0.1060649...] [ 578. 0.1082906...] [ 579. 0.1105408...] [ 580. 0.1128 ...] [ 581. 0.1150526...] [ 582. 0.1172833...] [ 583. 0.1194765...] [ 584. 0.1216167...] [ 585. 0.1236884...] [ 586. 0.1256760...] [ 587. 0.1275641...] [ 588. 0.1293373...] [ 589. 0.1309798...] [ 590. 0.1324764...] [ 591. 0.1338114...] [ 592. 0.1349694...] [ 593. 0.1359349...] [ 594. 0.1366923...] [ 595. 0.1372262...] [ 596. 0.1375211...] [ 597. 0.1375614...] [ 598. 0.1373316...] [ 599. 0.1368163...] [ 600. 0.136 ...]] """ shape_start, shape_end, shape_interval = as_float_array( [ self.shape.start, self.shape.end, self.shape.interval, ] ) shape = SpectralShape( *[ x[0] if x[0] is not None else x[1] for x in zip( (shape.start, shape.end, shape.interval), (shape_start, shape_end, shape_interval), strict=True, ) ] ) shape.start = max([shape.start, shape_start]) shape.end = min([shape.end, shape_end]) if interpolator is None: if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator = self.interpolator elif self.is_uniform(): interpolator = SpragueInterpolator else: interpolator = CubicSplineInterpolator if interpolator_kwargs is None: if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator_kwargs = self.interpolator_kwargs else: interpolator_kwargs = {} self_interpolator, self.interpolator = self.interpolator, interpolator self_interpolator_kwargs, self.interpolator_kwargs = ( self.interpolator_kwargs, interpolator_kwargs, ) values = self[shape.wavelengths] self.domain = shape.wavelengths self.values = values self.interpolator = self_interpolator self.interpolator_kwargs = self_interpolator_kwargs return self
[docs] def extrapolate( self, shape: SpectralShape, extrapolator: Type[ProtocolExtrapolator] | None = None, extrapolator_kwargs: dict | None = None, ) -> Self: """ Extrapolate the spectral distribution in-place according to *CIE 15:2004* and *CIE 167:2005* recommendations or specified extrapolation arguments. Parameters ---------- shape Spectral shape used for extrapolation. extrapolator Extrapolator class type to use as extrapolating function. extrapolator_kwargs Arguments to use when instantiating the extrapolating function. Returns ------- :class:`colour.SpectralDistribution` Extrapolated spectral distribution. References ---------- :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l` Examples -------- >>> from colour.utilities import numpy_print_options >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... } >>> sd = SpectralDistribution(data) >>> sd.extrapolate(SpectralShape(400, 700, 20)).shape SpectralShape(400.0, 700.0, 20.0) >>> with numpy_print_options(suppress=True): ... print(sd) [[ 400. 0.0651] [ 420. 0.0651] [ 440. 0.0651] [ 460. 0.0651] [ 480. 0.0651] [ 500. 0.0651] [ 520. 0.0705] [ 540. 0.0772] [ 560. 0.087 ] [ 580. 0.1128] [ 600. 0.136 ] [ 620. 0.136 ] [ 640. 0.136 ] [ 660. 0.136 ] [ 680. 0.136 ] [ 700. 0.136 ]] """ shape_start, shape_end, shape_interval = as_float_array( [ self.shape.start, self.shape.end, self.shape.interval, ] ) wavelengths = np.hstack( [ np.arange(shape.start, shape_start, shape_interval), np.arange(shape_end, shape.end, shape_interval) + shape_interval, ] ) extrapolator = optional(extrapolator, Extrapolator) extrapolator_kwargs = optional( extrapolator_kwargs, {"method": "Constant", "left": None, "right": None}, ) self_extrapolator = self.extrapolator self_extrapolator_kwargs = self.extrapolator_kwargs self.extrapolator = extrapolator self.extrapolator_kwargs = extrapolator_kwargs # The following self-assignment is written as intended and triggers the # extrapolation. self[wavelengths] = self[wavelengths] self.extrapolator = self_extrapolator self.extrapolator_kwargs = self_extrapolator_kwargs return self
[docs] def align( self, shape: SpectralShape, interpolator: Type[ProtocolInterpolator] | None = None, interpolator_kwargs: dict | None = None, extrapolator: Type[ProtocolExtrapolator] | None = None, extrapolator_kwargs: dict | None = None, ) -> Self: """ Align the spectral distribution in-place to the specified spectral shape: Interpolate first then extrapolate to fit the specified range. Interpolation is performed according to *CIE 167:2005* recommendation (if the interpolator has not been changed at instantiation time) or specified interpolation arguments. The logic for choosing the interpolator class when ``interpolator`` is not specified is as follows: .. code-block:: python if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator = self.interpolator elif self.is_uniform(): interpolator = SpragueInterpolator else: interpolator = CubicSplineInterpolator The logic for choosing the interpolator keyword arguments when ``interpolator_kwargs`` is not specified is as follows: .. code-block:: python if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator_kwargs = self.interpolator_kwargs else: interpolator_kwargs = {} Parameters ---------- shape Spectral shape used for alignment. interpolator Interpolator class type to use as interpolating function. interpolator_kwargs Arguments to use when instantiating the interpolating function. extrapolator Extrapolator class type to use as extrapolating function. extrapolator_kwargs Arguments to use when instantiating the extrapolating function. Returns ------- :class:`colour.SpectralDistribution` Aligned spectral distribution. Examples -------- >>> from colour.utilities import numpy_print_options >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... } >>> sd = SpectralDistribution(data) >>> with numpy_print_options(suppress=True): ... print(sd.align(SpectralShape(505, 565, 1))) ... # doctest: +ELLIPSIS [[ 505. 0.0663929...] [ 506. 0.0666509...] [ 507. 0.0669069...] [ 508. 0.0671613...] [ 509. 0.0674150...] [ 510. 0.0676692...] [ 511. 0.0679253...] [ 512. 0.0681848...] [ 513. 0.0684491...] [ 514. 0.0687197...] [ 515. 0.0689975...] [ 516. 0.0692832...] [ 517. 0.0695771...] [ 518. 0.0698787...] [ 519. 0.0701870...] [ 520. 0.0705 ...] [ 521. 0.0708155...] [ 522. 0.0711336...] [ 523. 0.0714547...] [ 524. 0.0717789...] [ 525. 0.0721063...] [ 526. 0.0724367...] [ 527. 0.0727698...] [ 528. 0.0731051...] [ 529. 0.0734423...] [ 530. 0.0737808...] [ 531. 0.0741203...] [ 532. 0.0744603...] [ 533. 0.0748006...] [ 534. 0.0751409...] [ 535. 0.0754813...] [ 536. 0.0758220...] [ 537. 0.0761633...] [ 538. 0.0765060...] [ 539. 0.0768511...] [ 540. 0.0772 ...] [ 541. 0.0775527...] [ 542. 0.0779042...] [ 543. 0.0782507...] [ 544. 0.0785908...] [ 545. 0.0789255...] [ 546. 0.0792576...] [ 547. 0.0795917...] [ 548. 0.0799334...] [ 549. 0.0802895...] [ 550. 0.0806671...] [ 551. 0.0810740...] [ 552. 0.0815176...] [ 553. 0.0820049...] [ 554. 0.0825423...] [ 555. 0.0831351...] [ 556. 0.0837873...] [ 557. 0.0845010...] [ 558. 0.0852763...] [ 559. 0.0861110...] [ 560. 0.087 ...] [ 561. 0.0879383...] [ 562. 0.0889300...] [ 563. 0.0899793...] [ 564. 0.0910876...] [ 565. 0.0922541...]] """ self.interpolate(shape, interpolator, interpolator_kwargs) self.extrapolate(shape, extrapolator, extrapolator_kwargs) return self
[docs] def trim(self, shape: SpectralShape) -> Self: """ Trim the spectral distribution wavelengths to the specified spectral shape. Parameters ---------- shape Spectral shape used for trimming. Returns ------- :class:`colour.SpectralDistribution` Trimmed spectral distribution. Examples -------- >>> from colour.utilities import numpy_print_options >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... } >>> sd = SpectralDistribution(data) >>> sd = sd.interpolate(SpectralShape(500, 600, 1)) >>> with numpy_print_options(suppress=True): ... print(sd.trim(SpectralShape(520, 580, 5))) ... # doctest: +ELLIPSIS [[ 520. 0.0705 ...] [ 521. 0.0708155...] [ 522. 0.0711336...] [ 523. 0.0714547...] [ 524. 0.0717789...] [ 525. 0.0721063...] [ 526. 0.0724367...] [ 527. 0.0727698...] [ 528. 0.0731051...] [ 529. 0.0734423...] [ 530. 0.0737808...] [ 531. 0.0741203...] [ 532. 0.0744603...] [ 533. 0.0748006...] [ 534. 0.0751409...] [ 535. 0.0754813...] [ 536. 0.0758220...] [ 537. 0.0761633...] [ 538. 0.0765060...] [ 539. 0.0768511...] [ 540. 0.0772 ...] [ 541. 0.0775527...] [ 542. 0.0779042...] [ 543. 0.0782507...] [ 544. 0.0785908...] [ 545. 0.0789255...] [ 546. 0.0792576...] [ 547. 0.0795917...] [ 548. 0.0799334...] [ 549. 0.0802895...] [ 550. 0.0806671...] [ 551. 0.0810740...] [ 552. 0.0815176...] [ 553. 0.0820049...] [ 554. 0.0825423...] [ 555. 0.0831351...] [ 556. 0.0837873...] [ 557. 0.0845010...] [ 558. 0.0852763...] [ 559. 0.0861110...] [ 560. 0.087 ...] [ 561. 0.0879383...] [ 562. 0.0889300...] [ 563. 0.0899793...] [ 564. 0.0910876...] [ 565. 0.0922541...] [ 566. 0.0934760...] [ 567. 0.0947487...] [ 568. 0.0960663...] [ 569. 0.0974220...] [ 570. 0.0988081...] [ 571. 0.1002166...] [ 572. 0.1016394...] [ 573. 0.1030687...] [ 574. 0.1044972...] [ 575. 0.1059186...] [ 576. 0.1073277...] [ 577. 0.1087210...] [ 578. 0.1100968...] [ 579. 0.1114554...] [ 580. 0.1128 ...]] """ start = max([shape.start, self.shape.start]) end = min([shape.end, self.shape.end]) indexes = np.where(np.logical_and(self.domain >= start, self.domain <= end)) wavelengths = self.wavelengths[indexes] values = self.values[indexes] self.wavelengths = wavelengths self.values = values if self.shape.boundaries != shape.boundaries: runtime_warning( f'"{shape}" shape could not be honoured, using "{self.shape}"!' ) return self
[docs] def normalise(self, factor: Real = 1) -> Self: """ Normalise the spectral distribution with the specified normalization factor. Parameters ---------- factor Normalisation factor. Returns ------- :class:`colour.SpectralDistribution` Normalised spectral distribution. Examples -------- >>> from colour.utilities import numpy_print_options >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... } >>> sd = SpectralDistribution(data) >>> with numpy_print_options(suppress=True): ... print(sd.normalise()) # doctest: +ELLIPSIS [[ 500. 0.4786764...] [ 520. 0.5183823...] [ 540. 0.5676470...] [ 560. 0.6397058...] [ 580. 0.8294117...] [ 600. 1. ...]] """ with sdiv_mode(): self *= sdiv(1, max(self.values)) * factor return self
[docs] class MultiSpectralDistributions(MultiSignals): """ Define multi-spectral distributions: the base object for multi-spectral computations. Model colour matching functions, display primaries, camera sensitivities, and related spectral data sets. Initialise multi-spectral distributions according to *CIE 15:2004* recommendation: use the method developed by *Sprague (1880)* for interpolating functions having uniformly spaced independent variables and the *Cubic Spline* method for non-uniformly spaced independent variables. Perform extrapolation according to *CIE 167:2005* recommendation. .. important:: Specific documentation about getting, setting, indexing and slicing the multi-spectral power distributions values is available in the :ref:`spectral-representation-and-continuous-signal` section. Parameters ---------- data Data to be stored in the multi-spectral distributions. domain Values to initialise the multiple :class:`colour.SpectralDistribution` class instances :attr:`colour.continuous.Signal.wavelengths` attribute with. If both ``data`` and ``domain`` arguments are defined, the latter will be used to initialise the :attr:`colour.continuous.Signal.wavelengths` property. labels Names to use for the :class:`colour.SpectralDistribution` class instances. Other Parameters ---------------- extrapolator Extrapolator class type to use as extrapolating function for the :class:`colour.SpectralDistribution` class instances. extrapolator_kwargs Arguments to use when instantiating the extrapolating function of the :class:`colour.SpectralDistribution` class instances. interpolator Interpolator class type to use as interpolating function for the :class:`colour.SpectralDistribution` class instances. interpolator_kwargs Arguments to use when instantiating the interpolating function of the :class:`colour.SpectralDistribution` class instances. name Multi-spectral distributions name. display_labels Multi-spectral distributions labels for figures, default to :attr:`colour.MultiSpectralDistributions.labels` property value. Warnings -------- The *Cubic Spline* method might produce unexpected results with exceptionally noisy or non-uniformly spaced data. Attributes ---------- - :attr:`~colour.MultiSpectralDistributions.display_name` - :attr:`~colour.MultiSpectralDistributions.display_labels` - :attr:`~colour.MultiSpectralDistributions.wavelengths` - :attr:`~colour.MultiSpectralDistributions.values` - :attr:`~colour.MultiSpectralDistributions.shape` Methods ------- - :meth:`~colour.MultiSpectralDistributions.__init__` - :meth:`~colour.MultiSpectralDistributions.interpolate` - :meth:`~colour.MultiSpectralDistributions.extrapolate` - :meth:`~colour.MultiSpectralDistributions.align` - :meth:`~colour.MultiSpectralDistributions.trim` - :meth:`~colour.MultiSpectralDistributions.normalise` - :meth:`~colour.MultiSpectralDistributions.to_sds` References ---------- :cite:`CIETC1-382005e`, :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l` Examples -------- Instantiating the multi-spectral distributions with a uniformly spaced independent variable: >>> from colour.utilities import numpy_print_options >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> labels = ("x_bar", "y_bar", "z_bar") >>> with numpy_print_options(suppress=True): ... MultiSpectralDistributions(data, labels=labels) ... # doctest: +ELLIPSIS ... MultiSpectral...([[ 500. , 0.0049 , 0.323 , 0.272 ], ... [ 510. , 0.0093 , 0.503 , 0.1582 ], ... [ 520. , 0.06327, 0.71 , 0.07825], ... [ 530. , 0.1655 , 0.862 , 0.04216], ... [ 540. , 0.2904 , 0.954 , 0.0203 ], ... [ 550. , 0.43345, 0.99495, 0.00875], ... [ 560. , 0.5945 , 0.995 , 0.0039 ]], ... [...'x_bar', ...'y_bar', ...'z_bar'], ... SpragueInterpolator, ... {}, ... Extrapolator, ... {'method': 'Constant', 'left': None, 'right': None}) Instantiating a spectral distribution with a non-uniformly spaced independent variable: >>> data[511] = (0.00314, 0.31416, 0.03142) >>> with numpy_print_options(suppress=True): ... MultiSpectralDistributions(data, labels=labels) ... # doctest: +ELLIPSIS ... MultiSpectral...([[ 500. , 0.0049 , 0.323 , 0.272 ], ... [ 510. , 0.0093 , 0.503 , 0.1582 ], ... [ 511. , 0.00314, 0.31416, 0.03142], ... [ 520. , 0.06327, 0.71 , 0.07825], ... [ 530. , 0.1655 , 0.862 , 0.04216], ... [ 540. , 0.2904 , 0.954 , 0.0203 ], ... [ 550. , 0.43345, 0.99495, 0.00875], ... [ 560. , 0.5945 , 0.995 , 0.0039 ]], ... [...'x_bar', ...'y_bar', ...'z_bar'], ... CubicSplineInterpolator, ... {}, ... Extrapolator, ... {'method': 'Constant', 'left': None, 'right': None}) Instantiation with a *Pandas* `DataFrame`: >>> from colour.utilities import is_pandas_installed >>> if is_pandas_installed(): ... from pandas import DataFrame ... ... x_bar = [data[key][0] for key in sorted(data.keys())] ... y_bar = [data[key][1] for key in sorted(data.keys())] ... z_bar = [data[key][2] for key in sorted(data.keys())] ... print( ... MultiSignals( # doctest: +SKIP ... DataFrame( ... dict(zip(labels, [x_bar, y_bar, z_bar])), data.keys() ... ) ... ) ... ) ... [[ 5.0000000...e+02 4.9000000...e-03 3.2300000...e-01 \ 2.7200000...e-01] [ 5.1000000...e+02 9.3000000...e-03 5.0300000...e-01 \ 1.5820000...e-01] [ 5.2000000...e+02 3.1400000...e-03 3.1416000...e-01 \ 3.1420000...e-02] [ 5.3000000...e+02 6.3270000...e-02 7.1000000...e-01 \ 7.8250000...e-02] [ 5.4000000...e+02 1.6550000...e-01 8.6200000...e-01 \ 4.2160000...e-02] [ 5.5000000...e+02 2.9040000...e-01 9.5400000...e-01 \ 2.0300000...e-02] [ 5.6000000...e+02 4.3345000...e-01 9.9495000...e-01 \ 8.7500000...e-03] [ 5.1100000...e+02 5.9450000...e-01 9.9500000...e-01 \ 3.9000000...e-03]] """
[docs] def __init__( self, data: ( ArrayLike | DataFrame | dict | MultiSignals | Sequence | Series | Signal | SpectralDistribution | ValuesView | None ) = None, domain: ArrayLike | SpectralShape | KeysView | None = None, labels: Sequence | None = None, **kwargs: Any, ) -> None: domain = domain.wavelengths if isinstance(domain, SpectralShape) else domain signals = self.multi_signals_unpack_data(data, domain, labels) domain = signals[next(iter(signals.keys()))].domain if signals else None uniform = is_uniform(domain) if domain is not None and len(domain) > 0 else True # Initialising with *CIE 15:2004* and *CIE 167:2005* recommendations # defaults. kwargs["interpolator"] = kwargs.get( "interpolator", SpragueInterpolator if uniform else CubicSplineInterpolator, ) kwargs["interpolator_kwargs"] = kwargs.get("interpolator_kwargs", {}) kwargs["extrapolator"] = kwargs.get("extrapolator", Extrapolator) kwargs["extrapolator_kwargs"] = kwargs.get( "extrapolator_kwargs", {"method": "Constant", "left": None, "right": None}, ) super().__init__(signals, domain, signal_type=SpectralDistribution, **kwargs) self._display_name: str = self.name self.display_name = kwargs.get("display_name", self._display_name) self._display_labels: list = list(self.signals.keys()) self.display_labels = kwargs.get("display_labels", self._display_labels)
@property def display_name(self) -> str: """ Getter and setter for the multi-spectral distributions' display name. The display name provides a human-readable identifier for the multi-spectral distribution collection, used for visualization and reporting purposes. Parameters ---------- value Value to set the multi-spectral distributions' display name with. Returns ------- :class:`str` Multi-spectral distributions' display name. """ return self._display_name @display_name.setter def display_name(self, value: str) -> None: """Setter for the **self.display_name** property.""" attest( isinstance(value, str), f'"display_name" property: "{value}" type is not "str"!', ) self._display_name = value @property def display_labels(self) -> List[str]: """ Getter and setter for the display labels of the multi-spectral distributions. The display labels provide human-readable identifiers for each spectral distribution in the multi-spectral collection, facilitating data visualization and interpretation. Parameters ---------- value Value to set the multi-spectral distributions display labels with. Returns ------- :class:`list` Multi-spectral distributions display labels. """ return self._display_labels @display_labels.setter def display_labels(self, value: Sequence) -> None: """Setter for the **self.display_labels** property.""" attest( is_iterable(value), f'"display_labels" property: "{value}" is not an "iterable" like object!', ) attest( len(set(value)) == len(value), '"display_labels" property: values must be unique!', ) attest( len(value) == len(self.labels), f'"display_labels" property: length must be "{len(self.labels)}"!', ) self._display_labels = [str(label) for label in value] for i, signal in enumerate(self.signals.values()): cast("SpectralDistribution", signal).display_name = self._display_labels[i] @property def wavelengths(self) -> NDArrayFloat: """ Getter and setter for the multi-spectral distributions wavelengths :math:`\\lambda_n`. Parameters ---------- value Value to set the multi-spectral distributions wavelengths :math:`\\lambda_n` with. Returns ------- :class:`numpy.ndarray` Multi-spectral distributions wavelengths :math:`\\lambda_n`. """ return self.domain @wavelengths.setter def wavelengths(self, value: ArrayLike) -> None: """Setter for the **self.wavelengths** property.""" self.domain = as_float_array(value, self.dtype) @property def values(self) -> NDArrayFloat: """ Getter and setter for the multi-spectral distributions values. Parameters ---------- value Value to set the multi-spectral distributions wavelengths values with. Returns ------- :class:`numpy.ndarray` Multi-spectral distributions values. """ return self.range @values.setter def values(self, value: ArrayLike) -> None: """Setter for the **self.values** property.""" self.range = as_float_array(value, self.dtype) @property def shape(self) -> SpectralShape: """ Getter property for the multi-spectral distributions shape. Returns ------- :class:`colour.SpectralShape` Multi-spectral distributions shape. Notes ----- - Multi-spectral distributions with a non-uniformly spaced independent variable have multiple intervals, in that case :attr:`colour.MultiSpectralDistributions.shape` property returns the *minimum* interval size. Examples -------- Shape of the multi-spectral distributions with a uniformly spaced independent variable: >>> from colour.utilities import numpy_print_options >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> MultiSpectralDistributions(data).shape SpectralShape(500.0, 560.0, 10.0) Shape of the multi-spectral distributions with a non-uniformly spaced independent variable: >>> data[511] = (0.00314, 0.31416, 0.03142) >>> MultiSpectralDistributions(data).shape SpectralShape(500.0, 560.0, 1.0) """ return first_item(self._signals.values()).shape
[docs] def interpolate( self, shape: SpectralShape, interpolator: Type[ProtocolInterpolator] | None = None, interpolator_kwargs: dict | None = None, ) -> Self: """ Interpolate the multi-spectral distributions in-place according to *CIE 167:2005* recommendation (if the interpolator has not been changed at instantiation time) or specified interpolation arguments. The logic for choosing the interpolator class when ``interpolator`` is not specified is as follows: .. code-block:: python if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator = self.interpolator elif self.is_uniform(): interpolator = SpragueInterpolator else: interpolator = CubicSplineInterpolator The logic for choosing the interpolator keyword arguments when ``interpolator_kwargs`` is not specified is as follows: .. code-block:: python if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator_kwargs = self.interpolator_kwargs else: interpolator_kwargs = {} Parameters ---------- shape Spectral shape used for interpolation. interpolator Interpolator class type to use as interpolating function. interpolator_kwargs Arguments to use when instantiating the interpolating function. Returns ------- :class:`colour.MultiSpectralDistributions` Interpolated multi-spectral distributions. Notes ----- - See :meth:`colour.SpectralDistribution.interpolate` method notes section. Warnings -------- See :meth:`colour.SpectralDistribution.interpolate` method warning section. References ---------- :cite:`CIETC1-382005e` Examples -------- Multi-spectral distributions with a uniformly spaced independent variable uses *Sprague (1880)* interpolation: >>> from colour.utilities import numpy_print_options >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> msds = MultiSpectralDistributions(data) >>> with numpy_print_options(suppress=True): ... print(msds.interpolate(SpectralShape(500, 560, 1))) ... # doctest: +ELLIPSIS [[ 500. 0.0049 ... 0.323 ... 0.272 ...] [ 501. 0.0043252... 0.3400642... 0.2599848...] [ 502. 0.0037950... 0.3572165... 0.2479849...] [ 503. 0.0033761... 0.3744030... 0.2360688...] [ 504. 0.0031397... 0.3916650... 0.2242878...] [ 505. 0.0031582... 0.4091067... 0.2126801...] [ 506. 0.0035019... 0.4268629... 0.2012748...] [ 507. 0.0042365... 0.4450668... 0.1900968...] [ 508. 0.0054192... 0.4638181... 0.1791709...] [ 509. 0.0070965... 0.4831505... 0.1685260...] [ 510. 0.0093 ... 0.503 ... 0.1582 ...] [ 511. 0.0120562... 0.5232543... 0.1482365...] [ 512. 0.0154137... 0.5439717... 0.1386625...] [ 513. 0.0193991... 0.565139 ... 0.1294993...] [ 514. 0.0240112... 0.5866255... 0.1207676...] [ 515. 0.0292289... 0.6082226... 0.1124864...] [ 516. 0.0350192... 0.6296821... 0.1046717...] [ 517. 0.0413448... 0.6507558... 0.0973361...] [ 518. 0.0481727... 0.6712346... 0.0904871...] [ 519. 0.0554816... 0.6909873... 0.0841267...] [ 520. 0.06327 ... 0.71 ... 0.07825 ...] [ 521. 0.0715642... 0.7283456... 0.0728614...] [ 522. 0.0803970... 0.7459679... 0.0680051...] [ 523. 0.0897629... 0.7628184... 0.0636823...] [ 524. 0.0996227... 0.7789004... 0.0598449...] [ 525. 0.1099142... 0.7942533... 0.0564111...] [ 526. 0.1205637... 0.8089368... 0.0532822...] [ 527. 0.1314973... 0.8230153... 0.0503588...] [ 528. 0.1426523... 0.8365417... 0.0475571...] [ 529. 0.1539887... 0.8495422... 0.0448253...] [ 530. 0.1655 ... 0.862 ... 0.04216 ...] [ 531. 0.1772055... 0.8738585... 0.0395936...] [ 532. 0.1890877... 0.8850940... 0.0371046...] [ 533. 0.2011304... 0.8957073... 0.0346733...] [ 534. 0.2133310... 0.9057092... 0.0323006...] [ 535. 0.2256968... 0.9151181... 0.0300011...] [ 536. 0.2382403... 0.9239560... 0.0277974...] [ 537. 0.2509754... 0.9322459... 0.0257131...] [ 538. 0.2639130... 0.9400080... 0.0237668...] [ 539. 0.2770569... 0.9472574... 0.0219659...] [ 540. 0.2904 ... 0.954 ... 0.0203 ...] [ 541. 0.3039194... 0.9602409... 0.0187414...] [ 542. 0.3175893... 0.9660106... 0.0172748...] [ 543. 0.3314022... 0.9713260... 0.0158947...] [ 544. 0.3453666... 0.9761850... 0.0146001...] [ 545. 0.3595019... 0.9805731... 0.0133933...] [ 546. 0.3738324... 0.9844703... 0.0122777...] [ 547. 0.3883818... 0.9878583... 0.0112562...] [ 548. 0.4031674... 0.9907270... 0.0103302...] [ 549. 0.4181943... 0.9930817... 0.0094972...] [ 550. 0.43345 ... 0.99495 ... 0.00875 ...] [ 551. 0.4489082... 0.9963738... 0.0080748...] [ 552. 0.4645599... 0.9973682... 0.0074580...] [ 553. 0.4803950... 0.9979568... 0.0068902...] [ 554. 0.4963962... 0.9981802... 0.0063660...] [ 555. 0.5125410... 0.9980910... 0.0058818...] [ 556. 0.5288034... 0.9977488... 0.0054349...] [ 557. 0.5451560... 0.9972150... 0.0050216...] [ 558. 0.5615719... 0.9965479... 0.0046357...] [ 559. 0.5780267... 0.9957974... 0.0042671...] [ 560. 0.5945 ... 0.995 ... 0.0039 ...]] Multi-spectral distributions with a non-uniformly spaced independent variable uses *Cubic Spline* interpolation: >>> data[511] = (0.00314, 0.31416, 0.03142) >>> msds = MultiSpectralDistributions(data) >>> with numpy_print_options(suppress=True): ... print(msds.interpolate(SpectralShape(500, 560, 1))) ... # doctest: +ELLIPSIS [[ 500. 0.0049 ... 0.323 ... 0.272 ...] [ 501. 0.0300110... 0.9455153... 0.5985102...] [ 502. 0.0462136... 1.3563103... 0.8066498...] [ 503. 0.0547925... 1.5844039... 0.9126502...] [ 504. 0.0570325... 1.6588148... 0.9327429...] [ 505. 0.0542183... 1.6085619... 0.8831594...] [ 506. 0.0476346... 1.4626640... 0.7801312...] [ 507. 0.0385662... 1.2501401... 0.6398896...] [ 508. 0.0282978... 1.0000089... 0.4786663...] [ 509. 0.0181142... 0.7412892... 0.3126925...] [ 510. 0.0093 ... 0.503 ... 0.1582 ...] [ 511. 0.00314 ... 0.31416 ... 0.03142 ...] [ 512. 0.0006228... 0.1970419... -0.0551709...] [ 513. 0.0015528... 0.1469341... -0.1041165...] [ 514. 0.0054381... 0.1523785... -0.1217152...] [ 515. 0.0117869... 0.2019173... -0.1142659...] [ 516. 0.0201073... 0.2840925... -0.0880670...] [ 517. 0.0299077... 0.3874463... -0.0494174...] [ 518. 0.0406961... 0.5005208... -0.0046156...] [ 519. 0.0519808... 0.6118579... 0.0400397...] [ 520. 0.06327 ... 0.71 ... 0.07825 ...] [ 521. 0.0741690... 0.7859059... 0.1050384...] [ 522. 0.0846726... 0.8402033... 0.1207164...] [ 523. 0.0948728... 0.8759363... 0.1269173...] [ 524. 0.1048614... 0.8961496... 0.1252743...] [ 525. 0.1147305... 0.9038874... 0.1174207...] [ 526. 0.1245719... 0.9021942... 0.1049899...] [ 527. 0.1344776... 0.8941145... 0.0896151...] [ 528. 0.1445395... 0.8826926... 0.0729296...] [ 529. 0.1548497... 0.8709729... 0.0565668...] [ 530. 0.1655 ... 0.862 ... 0.04216 ...] [ 531. 0.1765618... 0.858179 ... 0.0309976...] [ 532. 0.1880244... 0.8593588... 0.0229897...] [ 533. 0.1998566... 0.8647493... 0.0177013...] [ 534. 0.2120269... 0.8735601... 0.0146975...] [ 535. 0.2245042... 0.8850011... 0.0135435...] [ 536. 0.2372572... 0.8982820... 0.0138044...] [ 537. 0.2502546... 0.9126126... 0.0150454...] [ 538. 0.2634650... 0.9272026... 0.0168315...] [ 539. 0.2768572... 0.9412618... 0.0187280...] [ 540. 0.2904 ... 0.954 ... 0.0203 ...] [ 541. 0.3040682... 0.9647869... 0.0211987...] [ 542. 0.3178617... 0.9736329... 0.0214207...] [ 543. 0.3317865... 0.9807080... 0.0210486...] [ 544. 0.3458489... 0.9861825... 0.0201650...] [ 545. 0.3600548... 0.9902267... 0.0188525...] [ 546. 0.3744103... 0.9930107... 0.0171939...] [ 547. 0.3889215... 0.9947048... 0.0152716...] [ 548. 0.4035944... 0.9954792... 0.0131685...] [ 549. 0.4184352... 0.9955042... 0.0109670...] [ 550. 0.43345 ... 0.99495 ... 0.00875 ...] [ 551. 0.4486447... 0.9939867... 0.0065999...] [ 552. 0.4640255... 0.9927847... 0.0045994...] [ 553. 0.4795984... 0.9915141... 0.0028313...] [ 554. 0.4953696... 0.9903452... 0.0013781...] [ 555. 0.5113451... 0.9894483... 0.0003224...] [ 556. 0.5275310... 0.9889934... -0.0002530...] [ 557. 0.5439334... 0.9891509... -0.0002656...] [ 558. 0.5605583... 0.9900910... 0.0003672...] [ 559. 0.5774118... 0.9919840... 0.0017282...] [ 560. 0.5945 ... 0.995 ... 0.0039 ...]] """ for signal in self.signals.values(): cast("SpectralDistribution", signal).interpolate( shape, interpolator, interpolator_kwargs ) return self
[docs] def extrapolate( self, shape: SpectralShape, extrapolator: Type[ProtocolExtrapolator] | None = None, extrapolator_kwargs: dict | None = None, ) -> Self: """ Extrapolate the multi-spectral distributions in-place according to *CIE 15:2004* and *CIE 167:2005* recommendations or specified extrapolation arguments. Parameters ---------- shape Spectral shape used for extrapolation. extrapolator Extrapolator class type to use as extrapolating function. extrapolator_kwargs Arguments to use when instantiating the extrapolating function. Returns ------- :class:`colour.MultiSpectralDistributions` Extrapolated multi-spectral distributions. References ---------- :cite:`CIETC1-382005g`, :cite:`CIETC1-482004l` Examples -------- >>> from colour.utilities import numpy_print_options >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> msds = MultiSpectralDistributions(data) >>> msds.extrapolate(SpectralShape(400, 700, 10)).shape SpectralShape(400.0, 700.0, 10.0) >>> with numpy_print_options(suppress=True): ... print(msds) [[ 400. 0.0049 0.323 0.272 ] [ 410. 0.0049 0.323 0.272 ] [ 420. 0.0049 0.323 0.272 ] [ 430. 0.0049 0.323 0.272 ] [ 440. 0.0049 0.323 0.272 ] [ 450. 0.0049 0.323 0.272 ] [ 460. 0.0049 0.323 0.272 ] [ 470. 0.0049 0.323 0.272 ] [ 480. 0.0049 0.323 0.272 ] [ 490. 0.0049 0.323 0.272 ] [ 500. 0.0049 0.323 0.272 ] [ 510. 0.0093 0.503 0.1582 ] [ 520. 0.06327 0.71 0.07825] [ 530. 0.1655 0.862 0.04216] [ 540. 0.2904 0.954 0.0203 ] [ 550. 0.43345 0.99495 0.00875] [ 560. 0.5945 0.995 0.0039 ] [ 570. 0.5945 0.995 0.0039 ] [ 580. 0.5945 0.995 0.0039 ] [ 590. 0.5945 0.995 0.0039 ] [ 600. 0.5945 0.995 0.0039 ] [ 610. 0.5945 0.995 0.0039 ] [ 620. 0.5945 0.995 0.0039 ] [ 630. 0.5945 0.995 0.0039 ] [ 640. 0.5945 0.995 0.0039 ] [ 650. 0.5945 0.995 0.0039 ] [ 660. 0.5945 0.995 0.0039 ] [ 670. 0.5945 0.995 0.0039 ] [ 680. 0.5945 0.995 0.0039 ] [ 690. 0.5945 0.995 0.0039 ] [ 700. 0.5945 0.995 0.0039 ]] """ for signal in self.signals.values(): cast("SpectralDistribution", signal).extrapolate( shape, extrapolator, extrapolator_kwargs ) return self
[docs] def align( self, shape: SpectralShape, interpolator: Type[ProtocolInterpolator] | None = None, interpolator_kwargs: dict | None = None, extrapolator: Type[ProtocolExtrapolator] | None = None, extrapolator_kwargs: dict | None = None, ) -> Self: """ Align the multi-spectral distributions in-place to the specified spectral shape: Interpolates first then extrapolates to fit the specified range. Interpolation is performed according to *CIE 167:2005* recommendation (if the interpolator has not been changed at instantiation time) or specified interpolation arguments. The logic for choosing the interpolator class when ``interpolator`` is not specified is as follows: .. code-block:: python if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator = self.interpolator elif self.is_uniform(): interpolator = SpragueInterpolator else: interpolator = CubicSplineInterpolator The logic for choosing the interpolator keyword arguments when ``interpolator_kwargs`` is not specified is as follows: .. code-block:: python if self.interpolator not in ( SpragueInterpolator, CubicSplineInterpolator, ): interpolator_kwargs = self.interpolator_kwargs else: interpolator_kwargs = {} Parameters ---------- shape Spectral shape used for alignment. interpolator Interpolator class type to use as interpolating function. interpolator_kwargs Arguments to use when instantiating the interpolating function. extrapolator Extrapolator class type to use as extrapolating function. extrapolator_kwargs Arguments to use when instantiating the extrapolating function. Returns ------- :class:`colour.MultiSpectralDistributions` Aligned multi-spectral distributions. Examples -------- >>> from colour.utilities import numpy_print_options >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> msds = MultiSpectralDistributions(data) >>> with numpy_print_options(suppress=True): ... print(msds.align(SpectralShape(505, 565, 1))) ... # doctest: +ELLIPSIS [[ 505. 0.0031582... 0.4091067... 0.2126801...] [ 506. 0.0035019... 0.4268629... 0.2012748...] [ 507. 0.0042365... 0.4450668... 0.1900968...] [ 508. 0.0054192... 0.4638181... 0.1791709...] [ 509. 0.0070965... 0.4831505... 0.1685260...] [ 510. 0.0093 ... 0.503 ... 0.1582 ...] [ 511. 0.0120562... 0.5232543... 0.1482365...] [ 512. 0.0154137... 0.5439717... 0.1386625...] [ 513. 0.0193991... 0.565139 ... 0.1294993...] [ 514. 0.0240112... 0.5866255... 0.1207676...] [ 515. 0.0292289... 0.6082226... 0.1124864...] [ 516. 0.0350192... 0.6296821... 0.1046717...] [ 517. 0.0413448... 0.6507558... 0.0973361...] [ 518. 0.0481727... 0.6712346... 0.0904871...] [ 519. 0.0554816... 0.6909873... 0.0841267...] [ 520. 0.06327 ... 0.71 ... 0.07825 ...] [ 521. 0.0715642... 0.7283456... 0.0728614...] [ 522. 0.0803970... 0.7459679... 0.0680051...] [ 523. 0.0897629... 0.7628184... 0.0636823...] [ 524. 0.0996227... 0.7789004... 0.0598449...] [ 525. 0.1099142... 0.7942533... 0.0564111...] [ 526. 0.1205637... 0.8089368... 0.0532822...] [ 527. 0.1314973... 0.8230153... 0.0503588...] [ 528. 0.1426523... 0.8365417... 0.0475571...] [ 529. 0.1539887... 0.8495422... 0.0448253...] [ 530. 0.1655 ... 0.862 ... 0.04216 ...] [ 531. 0.1772055... 0.8738585... 0.0395936...] [ 532. 0.1890877... 0.8850940... 0.0371046...] [ 533. 0.2011304... 0.8957073... 0.0346733...] [ 534. 0.2133310... 0.9057092... 0.0323006...] [ 535. 0.2256968... 0.9151181... 0.0300011...] [ 536. 0.2382403... 0.9239560... 0.0277974...] [ 537. 0.2509754... 0.9322459... 0.0257131...] [ 538. 0.2639130... 0.9400080... 0.0237668...] [ 539. 0.2770569... 0.9472574... 0.0219659...] [ 540. 0.2904 ... 0.954 ... 0.0203 ...] [ 541. 0.3039194... 0.9602409... 0.0187414...] [ 542. 0.3175893... 0.9660106... 0.0172748...] [ 543. 0.3314022... 0.9713260... 0.0158947...] [ 544. 0.3453666... 0.9761850... 0.0146001...] [ 545. 0.3595019... 0.9805731... 0.0133933...] [ 546. 0.3738324... 0.9844703... 0.0122777...] [ 547. 0.3883818... 0.9878583... 0.0112562...] [ 548. 0.4031674... 0.9907270... 0.0103302...] [ 549. 0.4181943... 0.9930817... 0.0094972...] [ 550. 0.43345 ... 0.99495 ... 0.00875 ...] [ 551. 0.4489082... 0.9963738... 0.0080748...] [ 552. 0.4645599... 0.9973682... 0.0074580...] [ 553. 0.4803950... 0.9979568... 0.0068902...] [ 554. 0.4963962... 0.9981802... 0.0063660...] [ 555. 0.5125410... 0.9980910... 0.0058818...] [ 556. 0.5288034... 0.9977488... 0.0054349...] [ 557. 0.5451560... 0.9972150... 0.0050216...] [ 558. 0.5615719... 0.9965479... 0.0046357...] [ 559. 0.5780267... 0.9957974... 0.0042671...] [ 560. 0.5945 ... 0.995 ... 0.0039 ...] [ 561. 0.5945 ... 0.995 ... 0.0039 ...] [ 562. 0.5945 ... 0.995 ... 0.0039 ...] [ 563. 0.5945 ... 0.995 ... 0.0039 ...] [ 564. 0.5945 ... 0.995 ... 0.0039 ...] [ 565. 0.5945 ... 0.995 ... 0.0039 ...]] """ for signal in self.signals.values(): cast("SpectralDistribution", signal).align( shape, interpolator, interpolator_kwargs, extrapolator, extrapolator_kwargs, ) return self
[docs] def trim(self, shape: SpectralShape) -> Self: """ Trim the multi-spectral distributions wavelengths to the specified shape. Parameters ---------- shape Spectral shape used for trimming. Returns ------- :class:`colour.MultiSpectralDistributions` Trimmed multi-spectral distributions. Examples -------- >>> from colour.utilities import numpy_print_options >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> msds = MultiSpectralDistributions(data) >>> msds = msds.interpolate(SpectralShape(500, 560, 1)) >>> with numpy_print_options(suppress=True): ... print(msds.trim(SpectralShape(520, 580, 5))) ... # doctest: +ELLIPSIS [[ 520. 0.06327 ... 0.71 ... 0.07825 ...] [ 521. 0.0715642... 0.7283456... 0.0728614...] [ 522. 0.0803970... 0.7459679... 0.0680051...] [ 523. 0.0897629... 0.7628184... 0.0636823...] [ 524. 0.0996227... 0.7789004... 0.0598449...] [ 525. 0.1099142... 0.7942533... 0.0564111...] [ 526. 0.1205637... 0.8089368... 0.0532822...] [ 527. 0.1314973... 0.8230153... 0.0503588...] [ 528. 0.1426523... 0.8365417... 0.0475571...] [ 529. 0.1539887... 0.8495422... 0.0448253...] [ 530. 0.1655 ... 0.862 ... 0.04216 ...] [ 531. 0.1772055... 0.8738585... 0.0395936...] [ 532. 0.1890877... 0.8850940... 0.0371046...] [ 533. 0.2011304... 0.8957073... 0.0346733...] [ 534. 0.2133310... 0.9057092... 0.0323006...] [ 535. 0.2256968... 0.9151181... 0.0300011...] [ 536. 0.2382403... 0.9239560... 0.0277974...] [ 537. 0.2509754... 0.9322459... 0.0257131...] [ 538. 0.2639130... 0.9400080... 0.0237668...] [ 539. 0.2770569... 0.9472574... 0.0219659...] [ 540. 0.2904 ... 0.954 ... 0.0203 ...] [ 541. 0.3039194... 0.9602409... 0.0187414...] [ 542. 0.3175893... 0.9660106... 0.0172748...] [ 543. 0.3314022... 0.9713260... 0.0158947...] [ 544. 0.3453666... 0.9761850... 0.0146001...] [ 545. 0.3595019... 0.9805731... 0.0133933...] [ 546. 0.3738324... 0.9844703... 0.0122777...] [ 547. 0.3883818... 0.9878583... 0.0112562...] [ 548. 0.4031674... 0.9907270... 0.0103302...] [ 549. 0.4181943... 0.9930817... 0.0094972...] [ 550. 0.43345 ... 0.99495 ... 0.00875 ...] [ 551. 0.4489082... 0.9963738... 0.0080748...] [ 552. 0.4645599... 0.9973682... 0.0074580...] [ 553. 0.4803950... 0.9979568... 0.0068902...] [ 554. 0.4963962... 0.9981802... 0.0063660...] [ 555. 0.5125410... 0.9980910... 0.0058818...] [ 556. 0.5288034... 0.9977488... 0.0054349...] [ 557. 0.5451560... 0.9972150... 0.0050216...] [ 558. 0.5615719... 0.9965479... 0.0046357...] [ 559. 0.5780267... 0.9957974... 0.0042671...] [ 560. 0.5945 ... 0.995 ... 0.0039 ...]] """ for signal in self.signals.values(): cast("SpectralDistribution", signal).trim(shape) return self
[docs] def normalise(self, factor: Real = 1) -> Self: """ Normalise the multi-spectral distributions with the specified normalization factor. Parameters ---------- factor Normalization factor. Returns ------- :class:`colour.MultiSpectralDistributions` Normalised multi- spectral distribution. Notes ----- - The implementation uses the maximum value for each :class:`colour.SpectralDistribution` class instances. Examples -------- >>> from colour.utilities import numpy_print_options >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> msds = MultiSpectralDistributions(data) >>> with numpy_print_options(suppress=True): ... print(msds.normalise()) # doctest: +ELLIPSIS [[ 500. 0.0082422... 0.3246231... 1. ...] [ 510. 0.0156434... 0.5055276... 0.5816176...] [ 520. 0.1064255... 0.7135678... 0.2876838...] [ 530. 0.2783852... 0.8663316... 0.155 ...] [ 540. 0.4884777... 0.9587939... 0.0746323...] [ 550. 0.7291000... 0.9999497... 0.0321691...] [ 560. 1. ... 1. ... 0.0143382...]] """ for signal in self.signals.values(): cast("SpectralDistribution", signal).normalise(factor) return self
[docs] def to_sds(self) -> List[SpectralDistribution]: """ Convert the multi-spectral distributions to a list of spectral distributions. Returns ------- :class:`list` List of spectral distributions. Examples -------- >>> from colour.utilities import numpy_print_options >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> msds = MultiSpectralDistributions(data) >>> with numpy_print_options(suppress=True): ... for sd in msds.to_sds(): ... print(sd) # doctest: +ELLIPSIS [[ 500. 0.0049 ...] [ 510. 0.0093 ...] [ 520. 0.06327...] [ 530. 0.1655 ...] [ 540. 0.2904 ...] [ 550. 0.43345...] [ 560. 0.5945 ...]] [[ 500. 0.323 ...] [ 510. 0.503 ...] [ 520. 0.71 ...] [ 530. 0.862 ...] [ 540. 0.954 ...] [ 550. 0.99495...] [ 560. 0.995 ...]] [[ 500. 0.272 ...] [ 510. 0.1582 ...] [ 520. 0.07825...] [ 530. 0.04216...] [ 540. 0.0203 ...] [ 550. 0.00875...] [ 560. 0.0039 ...]] """ return [ cast("SpectralDistribution", signal.copy()) for signal in self.signals.values() ]
_CACHE_RESHAPED_SDS_AND_MSDS: dict = CACHE_REGISTRY.register_cache( f"{__name__}._CACHE_RESHAPED_SDS_AND_MSDS" ) TypeSpectralDistribution = TypeVar( "TypeSpectralDistribution", bound="SpectralDistribution" )
[docs] def reshape_sd( sd: TypeSpectralDistribution, shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT, method: (Literal["Align", "Extrapolate", "Interpolate", "Trim"] | str) = "Align", copy: bool = True, **kwargs: Any, ) -> TypeSpectralDistribution: """ Reshape the specified spectral distribution to match the specified spectral shape. The reshaped object is cached, thus another call to the definition with the same arguments will yield the cached object immediately. Parameters ---------- sd Spectral distribution to reshape. shape Target spectral shape for reshaping the spectral distribution. method Method to use for reshaping. copy Whether to return a copy of the cached spectral distribution. Default is *True*. Other Parameters ---------------- kwargs {:meth:`colour.SpectralDistribution.align`, :meth:`colour.SpectralDistribution.extrapolate`, :meth:`colour.SpectralDistribution.interpolate`, :meth:`colour.SpectralDistribution.trim`}, See the documentation of the previously listed methods. Returns ------- :class:`colour.SpectralDistribution` Warnings -------- Contrary to *Numpy*, reshaping a spectral distribution alters its data! """ method = validate_method( method, valid_methods=("Align", "Extrapolate", "Interpolate", "Trim") ) # Handling dict-like keyword arguments. kwargs_items = list(kwargs.items()) for i, (keyword, value) in enumerate(kwargs_items): if isinstance(value, Mapping): kwargs_items[i] = (keyword, tuple(value.items())) hash_key = hash((sd, shape, method, tuple(kwargs_items))) if is_caching_enabled() and hash_key in _CACHE_RESHAPED_SDS_AND_MSDS: reshaped_sd = _CACHE_RESHAPED_SDS_AND_MSDS[hash_key] return reshaped_sd.copy() if copy else reshaped_sd function = getattr(sd, method) reshaped_sd = getattr(sd.copy(), method)(shape, **filter_kwargs(function, **kwargs)) _CACHE_RESHAPED_SDS_AND_MSDS[hash_key] = reshaped_sd return reshaped_sd
TypeMultiSpectralDistributions = TypeVar( "TypeMultiSpectralDistributions", bound="MultiSpectralDistributions" )
[docs] def reshape_msds( msds: TypeMultiSpectralDistributions, shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT, method: (Literal["Align", "Extrapolate", "Interpolate", "Trim"] | str) = "Align", copy: bool = True, **kwargs: Any, ) -> TypeMultiSpectralDistributions: """ Reshape the specified multi-spectral distributions to match the specified spectral shape. The reshaped object is cached, thus another call to the definition with the same arguments will yield the cached object immediately. Parameters ---------- msds Multi-spectral distributions to reshape. shape Target spectral shape for reshaping the multi-spectral distributions. method Method to use for reshaping. copy Whether to return a copy of the cached multi-spectral distributions. Default is *True*. Other Parameters ---------------- kwargs {:meth:`colour.MultiSpectralDistributions.align`, :meth:`colour.MultiSpectralDistributions.extrapolate`, :meth:`colour.MultiSpectralDistributions.interpolate`, :meth:`colour.MultiSpectralDistributions.trim`}, See the documentation of the previously listed methods. Returns ------- :class:`colour.MultiSpectralDistributions` Warnings -------- Contrary to *Numpy*, reshaping multi-spectral distributions alters their data! """ return reshape_sd(msds, shape, method, copy, **kwargs) # pyright: ignore
[docs] def sds_and_msds_to_sds( sds: ( Sequence[SpectralDistribution | MultiSpectralDistributions] | SpectralDistribution | MultiSpectralDistributions | ValuesView ), ) -> List[SpectralDistribution]: """ Convert specified spectral and multi-spectral distributions to a list of spectral distributions. Parameters ---------- sds Spectral and multi-spectral distributions to convert to a list of spectral distributions. Each multi-spectral distribution is expanded into its constituent spectral distributions. Returns ------- :class:`list` List of spectral distributions where multi-spectral distributions have been expanded into individual spectral distributions. Examples -------- >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... } >>> sd_1 = SpectralDistribution(data) >>> sd_2 = SpectralDistribution(data) >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> multi_sds_1 = MultiSpectralDistributions(data) >>> multi_sds_2 = MultiSpectralDistributions(data) >>> len(sds_and_msds_to_sds([sd_1, sd_2, multi_sds_1, multi_sds_2])) 8 """ if isinstance(sds, SpectralDistribution): return sds_and_msds_to_sds([sds]) if isinstance(sds, MultiSpectralDistributions): sds_converted = sds.to_sds() else: sds_converted = [] for sd in sds: sds_converted += ( sd.to_sds() if isinstance(sd, MultiSpectralDistributions) else [sd] ) return sds_converted
[docs] def sds_and_msds_to_msds( sds: ( Sequence[SpectralDistribution | MultiSpectralDistributions] | SpectralDistribution | MultiSpectralDistributions | ValuesView ), ) -> MultiSpectralDistributions: """ Convert specified spectral and multi-spectral distributions to multi-spectral distributions. The spectral and multi-spectral distributions will be aligned to the intersection of their spectral shapes. Parameters ---------- sds Spectral and multi-spectral distributions to convert to multi-spectral distributions. Returns ------- :class:`colour.MultiSpectralDistributions` Multi-spectral distributions. Examples -------- >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... } >>> sd_1 = SpectralDistribution(data) >>> sd_2 = SpectralDistribution(data) >>> data = { ... 500: (0.004900, 0.323000, 0.272000), ... 510: (0.009300, 0.503000, 0.158200), ... 520: (0.063270, 0.710000, 0.078250), ... 530: (0.165500, 0.862000, 0.042160), ... 540: (0.290400, 0.954000, 0.020300), ... 550: (0.433450, 0.994950, 0.008750), ... 560: (0.594500, 0.995000, 0.003900), ... } >>> multi_sds_1 = MultiSpectralDistributions(data) >>> multi_sds_2 = MultiSpectralDistributions(data) >>> from colour.utilities import numpy_print_options >>> with numpy_print_options(suppress=True, linewidth=160): ... sds_and_msds_to_msds( # doctest: +SKIP ... [sd_1, sd_2, multi_sds_1, multi_sds_2] ... ) ... MultiSpectralDistributions([[ 500. , 0.0651 ...,\ 0.0651 ..., 0.0049 ..., 0.323 ..., 0.272 ...,\ 0.0049 ..., 0.323 ..., 0.272 ...], [ 510. , 0.0676692...,\ 0.0676692..., 0.0093 ..., 0.503 ..., 0.1582 ...,\ 0.0093 ..., 0.503 ..., 0.1582 ...], [ 520. , 0.0705 ...,\ 0.0705 ..., 0.06327 ..., 0.71 ..., 0.07825 ...,\ 0.06327 ..., 0.71 ..., 0.07825 ...], [ 530. , 0.0737808...,\ 0.0737808..., 0.1655 ..., 0.862 ..., 0.04216 ...,\ 0.1655 ..., 0.862 ..., 0.04216 ...], [ 540. , 0.0772 ...,\ 0.0772 ..., 0.2904 ..., 0.954 ..., 0.0203 ...,\ 0.2904 ..., 0.954 ..., 0.0203 ...], [ 550. , 0.0806671...,\ 0.0806671..., 0.43345 ..., 0.99495 ..., 0.00875 ...,\ 0.43345 ..., 0.99495 ..., 0.00875 ...], [ 560. , 0.087 ...,\ 0.087 ..., 0.5945 ..., 0.995 ..., 0.0039 ...,\ 0.5945 ..., 0.995 ..., 0.0039 ...]], labels=['SpectralDistribution (...)', \ 'SpectralDistribution (...)', '0 - SpectralDistribution (...)', \ '1 - SpectralDistribution (...)', '2 - SpectralDistribution (...)', \ '0 - SpectralDistribution (...)', '1 - SpectralDistribution (...)', \ '2 - SpectralDistribution (...)'], interpolator=SpragueInterpolator, interpolator_kwargs={}, extrapolator=Extrapolator, extrapolator_kwargs={...}) """ if isinstance(sds, SpectralDistribution): return sds_and_msds_to_msds([sds]) if isinstance(sds, MultiSpectralDistributions): msds_converted = sds else: sds_converted = sds_and_msds_to_sds(sds) shapes = tuple({sd.shape for sd in sds_converted}) shape = SpectralShape( max(shape.start for shape in shapes), min(shape.end for shape in shapes), min(shape.interval for shape in shapes), ) values = [] labels = [] display_labels = [] for sd in sds_converted: if sd.shape != shape: sd = sd.copy().align(shape) # noqa: PLW2901 values.append(sd.values) labels.append(sd.name if sd.name not in labels else f"{sd.name} ({id(sd)})") display_labels.append( sd.display_name if sd.display_name not in display_labels else f"{sd.display_name} ({id(sd)})" ) msds_converted = MultiSpectralDistributions( tstack(values), shape.wavelengths, labels, display_labels=display_labels, ) return msds_converted