"""
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 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 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