colour.colorimetry.spectrum — Colour 0.3.0 documentation

# Source code for colour.colorimetry.spectrum

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-

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

    Defines the classes handling spectral data computation:

    -   :class:`SpectralShape`
    -   :class:`SpectralPowerDistribution`
    -   :class:`TriSpectralPowerDistribution`

    See Also
    --------
    `Spectrum IPython Notebook
    <http://nbviewer.ipython.org/github/colour-science/colour-ipython/blob/master/notebooks/colorimetry/spectrum.ipynb>`_  # noqa
    """

    from __future__ import division, unicode_literals

    import copy
    import itertools
    import math
    import numpy as np

    from colour.algebra import (
        is_iterable,
        is_numeric,
        is_uniform,
        steps,
        to_ndarray)
    from colour.algebra import (
        Extrapolator1d,
        LinearInterpolator1d,
        SplineInterpolator,
        SpragueInterpolator)
    from colour.utilities import is_string

    __author__ = 'Colour Developers'
    __copyright__ = 'Copyright (C) 2013 - 2014 - Colour Developers'
    __license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
    __maintainer__ = 'Colour Developers'
    __email__ = 'colour-science@googlegroups.com'
    __status__ = 'Production'

    __all__ = ['SpectralShape',
               'SpectralPowerDistribution',
               'TriSpectralPowerDistribution',
               'DEFAULT_SPECTRAL_SHAPE',
               'constant_spd',
               'zeros_spd',
               'ones_spd']

    class SpectralShape(object):
        """
        Defines the base object for spectral power distribution shape.

        Parameters
        ----------
        start : numeric, optional
            Wavelengths :math:`\lambda_{i}`: range start in nm.
        end : numeric, optional
            Wavelengths :math:`\lambda_{i}`: range end in nm.
        steps : numeric, optional
            Wavelengths :math:`\lambda_{i}`: range steps.

        Attributes
        ----------
        start
        end
        steps

        Methods
        -------
        __repr__
        __contains__
        __len__
        __eq__
        __ne__
        range

        Examples
        --------
        >>> SpectralShape(360, 830, 1)
        SpectralShape(360, 830, 1)
        """

        def __init__(self, start=None, end=None, steps=None):
            # Attribute storing the spectral shape range for caching purpose.
            self.__range = None

            self.__start = None
            self.__end = None
            self.__steps = None
            self.start = start
            self.end = end
            self.steps = steps

        @property
        def start(self):
            """
            Property for **self.__start** private attribute.

            Returns
            -------
            numeric
                self.__start.
            """

            return self.__start

        @start.setter
        def start(self, value):
            """
            Setter for **self.__start** private attribute.

            Parameters
            ----------
            value : str or unicode
                Attribute value.
            """

            if value is not None:
                assert is_numeric(value), (
                    '"{0}" attribute: "{1}" type is not "numeric"!'.format(
                        'start', value))
                if self.__end is not None:
                    assert value < self.__end, (
                        '"{0}" attribute value must be strictly less than '
                        '"{1}"!'.format('start', self.__end))

            # Invalidating the *range* cache.
            if value != self.__start:
                self.__range = None

            self.__start = value

        @property
        def end(self):
            """
            Property for **self.__end** private attribute.

            Returns
            -------
            numeric
                self.__end.
            """

            return self.__end

        @end.setter
        def end(self, value):
            """
            Setter for **self.__end** private attribute.

            Parameters
            ----------
            value : str or unicode
                Attribute value.
            """

            if value is not None:
                assert is_numeric(value), (
                    '"{0}" attribute: "{1}" type is not "numeric"!'.format(
                        'end', value))
                if self.__start is not None:
                    assert value > self.__start, (
                        '"{0}" attribute value must be strictly greater than '
                        '"{1}"!'.format('end', self.__start))

            # Invalidating the *range* cache.
            if value != self.__end:
                self.__range = None

            self.__end = value

        @property
        def steps(self):
            """
            Property for **self.__steps** private attribute.

            Returns
            -------
            numeric
                self.__steps.
            """

            return self.__steps

        @steps.setter
        def steps(self, value):
            """
            Setter for **self.__steps** private attribute.

            Parameters
            ----------
            value : str or unicode
                Attribute value.
            """

            if value is not None:
                assert is_numeric(value), (
                    '"{0}" attribute: "{1}" type is not "numeric"!'.format(
                        'steps', value))

            # Invalidating the *range* cache.
            if value != self.__steps:
                self.__range = None

            self.__steps = value

        def __str__(self):
            """
            Returns a nice formatted string representation.

            Returns
            -------
            unicode
                Nice formatted string representation.
            """

            return '({0}, {1}, {2})'.format(self.__start,
                                            self.__end,
                                            self.__steps)

        def __repr__(self):
            """
            Returns a formatted string representation.

            Returns
            -------
            unicode
                Formatted string representation.
            """

            return 'SpectralShape({0}, {1}, {2})'.format(self.__start,
                                                         self.__end,
                                                         self.__steps)

        def __iter__(self):
            """
            Returns a generator for the spectral power distribution data.

            Returns
            -------
            generator
                Spectral power distribution data generator.

            Notes
            -----
            -   Reimplements the :meth:`object.__iter__` method.

            Examples
            --------
            >>> shape = SpectralShape(0, 10, 1)
            >>> for wavelength in shape: print(wavelength)
            0
            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            """

            return iter(self.range())

        def __contains__(self, wavelength):
            """
            Returns if the spectral shape contains the given wavelength
            :math:`\lambda`.

            Parameters
            ----------
            wavelength : numeric
                Wavelength :math:`\lambda`.

            Returns
            -------
            bool
                Is wavelength :math:`\lambda` in the spectral shape.

            Notes
            -----
            -   Reimplements the :meth:`object.__contains__` method.

            Examples
            --------
            >>> 0.5 in SpectralShape(0, 10, 0.1)
            True
            >>> 0.51 in SpectralShape(0, 10, 0.1)
            False
            """

            return wavelength in self.range()

        def __len__(self):
            """
            Returns the spectral shape wavelengths :math:`\lambda_n`
            count.

            Returns
            -------
            int
                Spectral shape wavelengths :math:`\lambda_n` count.

            Notes
            -----
            -   Reimplements the :meth:`object.__len__` method.

            Examples
            --------
            >>> len(SpectralShape(0, 10, 0.1))
            101
            """

            return len(self.range())

        def __eq__(self, shape):
            """
            Returns the spectral shape equality with given other spectral shape.

            Parameters
            ----------
            shape : SpectralShape
                Spectral shape to compare for equality.

            Returns
            -------
            bool
                Spectral shape equality.

            Notes
            -----
            -   Reimplements the :meth:`object.__eq__` method.

            Examples
            --------
            >>> SpectralShape(0, 10, 0.1) == SpectralShape(0, 10, 0.1)
            True
            >>> SpectralShape(0, 10, 0.1) == SpectralShape(0, 10, 1)
            False
            """

            return isinstance(shape, self.__class__) and np.array_equal(
                self.range(), shape.range())

        def __ne__(self, shape):
            """
            Returns the spectral shape inequality with given other spectral shape.

            Parameters
            ----------
            shape : SpectralShape
                Spectral shape to compare for inequality.

            Returns
            -------
            bool
                Spectral shape inequality.

            Notes
            -----
            -   Reimplements the :meth:`object.__ne__` method.

            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 == shape)

        def range(self):
            """
            Returns an iterable range for the spectral power distribution shape.

            Returns
            -------
            ndarray
                Iterable range for the spectral power distribution shape

            Raises
            ------
            RuntimeError
                If one of spectral shape *start*, *end* or *steps* attributes is
                not defined.

            Examples
            --------
            >>> SpectralShape(0, 10, 0.1).range()
            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. ])
            """

            if None in (self.__start, self.__end, self.__steps):
                raise RuntimeError(('One of the spectral shape "start", "end" or '
                                    '"steps" attributes is not defined!'))

            if self.__range is None:
                self.__range = np.arange(self.__start,
                                         self.__end + self.__steps,
                                         self.__steps)

            return self.__range

    class SpectralPowerDistribution(object):
        """
        Defines the base object for spectral data computations.

        Parameters
        ----------
        name : str or unicode
            Spectral power distribution name.
        data : dict
            Spectral power distribution data in a *dict* as follows:
            {wavelength :math:`\lambda_{i}`: value,
            wavelength :math:`\lambda_{i+1}`,
            ...,
            wavelength :math:`\lambda_{i+n}`}

        Attributes
        ----------
        name
        data
        wavelengths
        values
        items
        shape

        Methods
        -------
        __getitem__
        __init__
        __setitem__
        __iter__
        __contains__
        __len__
        __eq__
        __ne__
        __add__
        __sub__
        __mul__
        __div__
        __truediv__
        get
        is_uniform
        extrapolate
        interpolate
        align
        zeros
        normalise
        clone

        Examples
        --------
        >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
        >>> spd = SpectralPowerDistribution('Spd', data)
        >>> spd.wavelengths
        array([510, 520, 530, 540])
        >>> spd.values
        array([ 49.67,  69.59,  81.73,  88.19])
        >>> spd.shape
        SpectralShape(510, 540, 10)
        """

        def __init__(self, name, data):
            self.__name = None
            self.name = name
            self.__data = None
            self.data = data

        @property
        def name(self):
            """
            Property for **self.__name** private attribute.

            Returns
            -------
            str or unicode
                self.__name.
            """

            return self.__name

        @name.setter
        def name(self, value):
            """
            Setter for **self.__name** private attribute.

            Parameters
            ----------
            value : str or unicode
                Attribute value.
            """

            if value is not None:
                assert type(value) in (str, unicode), (
                    ('"{0}" attribute: "{1}" type is not '
                     '"str" or "unicode"!').format('name', value))
            self.__name = value

        @property
        def data(self):
            """
            Property for **self.__data** private attribute.

            Returns
            -------
            dict
                self.__data.
            """

            return self.__data

        @data.setter
        def data(self, value):
            """
            Setter for **self.__data** private attribute.

            Parameters
            ----------
            value : dict
                Attribute value.
            """

            if value is not None:
                assert type(value) is dict, (
                    '"{0}" attribute: "{1}" type is not "dict"!'.format(
                        'data', value))
            self.__data = value

        @property
        def wavelengths(self):
            """
            Property for **self.wavelengths** attribute.

            Returns
            -------
            ndarray
                Spectral power distribution wavelengths :math:`\lambda_n`.

            Warning
            -------
            :attr:`SpectralPowerDistribution.wavelengths` is read only.
            """

            return np.array(sorted(self.__data.keys()))

        @wavelengths.setter
        def wavelengths(self, value):
            """
            Setter for **self.wavelengths** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError(
                '"{0}" attribute is read only!'.format('wavelengths'))

        @property
        def values(self):
            """
            Property for **self.values** attribute.

            Returns
            -------
            ndarray
                Spectral power distribution wavelengths :math:`\lambda_n` values.

            Warning
            -------
            :attr:`SpectralPowerDistribution.values` is read only.
            """

            return np.array([self.get(wavelength)
                             for wavelength in self.wavelengths])

        @values.setter
        def values(self, value):
            """
            Setter for **self.values** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError('"{0}" attribute is read only!'.format('values'))

        @property
        def items(self):
            """
            Property for **self.items** attribute. This is a convenient attribute
            used to iterate over the spectral power distribution.

            Returns
            -------
            generator
                Spectral power distribution data generator.
            """

            return self.__iter__()

        @items.setter
        def items(self, value):
            """
            Setter for **self.items** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError('"{0}" attribute is read only!'.format('items'))

        @property
        def shape(self):
            """
            Property for **self.shape** attribute.

            Returns the shape of the spectral power distribution in the form of a
            :class:`SpectralShape` class instance.

            Returns
            -------
            SpectralShape
                Spectral power distribution shape.

            See Also
            --------
            SpectralPowerDistribution.is_uniform

            Notes
            -----
            -   A non uniform spectral power distribution may will have multiple
                different steps, in that case
                :attr:`SpectralPowerDistribution.shape` returns the *minimum* steps
                size.

            Warning
            -------
            :attr:`SpectralPowerDistribution.shape` is read only.

            Examples
            --------
            Uniform spectral power distribution:

            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> SpectralPowerDistribution('Spd', data).shape
            SpectralShape(510, 540, 10)

            Non uniform spectral power distribution:

            >>> data = {512.3: 49.67, 524.5: 69.59, 532.4: 81.73, 545.7: 88.19}
            >>> # Doctests ellipsis for Python 2.x compatibility.
            >>> SpectralPowerDistribution('Spd', data).shape  # doctest: +ELLIPSIS
            SpectralShape(512.3, 545.7, 7...)
            """

            return SpectralShape(min(self.data.keys()),
                                 max(self.data.keys()),
                                 min(steps(self.wavelengths)))

        @shape.setter
        def shape(self, value):
            """
            Setter for **self.shape** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError('"{0}" attribute is read only!'.format('shape'))

        def __hash__(self):
            """
            Returns the spectral power distribution hash value.

            Returns
            -------
            int
                Object hash.

            Notes
            -----
            -   Reimplements the :meth:`object.__hash__` method.

            Warning
            -------
            :class:`SpectralPowerDistribution` class is mutable and should not be
            hashable. However, so that it can be used as a key in some data caches,
            we provide a *__hash__* implementation, **assuming that the underlying
            data will not change for those specific cases**.

            References
            ----------
            .. [1]  http://stackoverflow.com/a/16162138/931625
                    (Last accessed 8 August 2014)
            """

            return hash(frozenset(self.__data))

        def __getitem__(self, wavelength):
            """
            Returns the value for given wavelength :math:`\lambda`.

            Parameters
            ----------
            wavelength: numeric
                Wavelength :math:`\lambda` to retrieve the value.

            Returns
            -------
            numeric
                Wavelength :math:`\lambda` value.

            See Also
            --------
            SpectralPowerDistribution.get

            Notes
            -----
            -   Reimplements the :meth:`object.__getitem__` method.

            Examples
            --------
            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> # Doctests ellipsis for Python 2.x compatibility.
            >>> spd[510]  # doctest: +ELLIPSIS
            49.67...
            """

            return self.__data.__getitem__(wavelength)

        def __setitem__(self, wavelength, value):
            """
            Sets the wavelength :math:`\lambda` with given value.

            Parameters
            ----------
            wavelength : numeric
                Wavelength :math:`\lambda` to set.
            value : numeric
                Value for wavelength :math:`\lambda`.

            Notes
            -----
            -   Reimplements the :meth:`object.__setitem__` method.

            Examples
            --------
            >>> spd = SpectralPowerDistribution('Spd', {})
            >>> spd[510] = 49.6700
            >>> spd.values
            array([ 49.67])
            """

            self.__data.__setitem__(wavelength, value)

        def __iter__(self):
            """
            Returns a generator for the spectral power distribution data.

            Returns
            -------
            generator
                Spectral power distribution data generator.

            Notes
            -----
            -   Reimplements the :meth:`object.__iter__` method.

            Examples
            --------
            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> # Doctests ellipsis for Python 2.x compatibility.
            >>> for wavelength, value in spd: print((wavelength, value))  # noqa  # doctest: +ELLIPSIS
            (510, 49.6...)
            (520, 69.5...)
            (530, 81.7...)
            (540, 88.1...)
            """

            return iter(sorted(self.__data.items()))

        def __contains__(self, wavelength):
            """
            Returns if the spectral power distribution contains the given
            wavelength :math:`\lambda`.

            Parameters
            ----------
            wavelength : numeric
                Wavelength :math:`\lambda`.

            Returns
            -------
            bool
                Is wavelength :math:`\lambda` in the spectral power distribution.

            Notes
            -----
            -   Reimplements the :meth:`object.__contains__` method.

            Examples
            --------
            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> 510 in spd
            True
            """

            return wavelength in self.__data

        def __len__(self):
            """
            Returns the spectral power distribution wavelengths :math:`\lambda_n`
            count.

            Returns
            -------
            int
                Spectral power distribution wavelengths :math:`\lambda_n` count.

            Notes
            -----
            -   Reimplements the :meth:`object.__len__` method.

            Examples
            --------
            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> len(spd)
            4
            """

            return len(self.__data)

        def __eq__(self, spd):
            """
            Returns the spectral power distribution equality with given other
            spectral power distribution.

            Parameters
            ----------
            spd : SpectralPowerDistribution
                Spectral power distribution to compare for equality.

            Returns
            -------
            bool
                Spectral power distribution equality.

            Notes
            -----
            -   Reimplements the :meth:`object.__eq__` method.

            Examples
            --------
            >>> data1 = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> data2 = {510: 48.6700, 520: 69.5900, 530: 81.7300, 540: 88.1900}
            >>> spd1 = SpectralPowerDistribution('Spd', data1)
            >>> spd2 = SpectralPowerDistribution('Spd', data2)
            >>> spd3 = SpectralPowerDistribution('Spd', data2)
            >>> spd1 == spd2
            False
            >>> spd2 == spd3
            True
            """

            return isinstance(spd, self.__class__) and spd.data == self.data

        def __ne__(self, spd):
            """
            Returns the spectral power distribution inequality with given other
            spectral power distribution.

            Parameters
            ----------
            spd : SpectralPowerDistribution
                Spectral power distribution to compare for inequality.

            Returns
            -------
            bool
                Spectral power distribution inequality.

            Notes
            -----
            -   Reimplements the :meth:`object.__ne__` method.

            Examples
            --------
            >>> data1 = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> data2 = {510: 48.6700, 520: 69.5900, 530: 81.7300, 540: 88.1900}
            >>> spd1 = SpectralPowerDistribution('Spd', data1)
            >>> spd2 = SpectralPowerDistribution('Spd', data2)
            >>> spd3 = SpectralPowerDistribution('Spd', data2)
            >>> spd1 != spd2
            True
            >>> spd2 != spd3
            False
            """

            return not (self == spd)

        def __format_operand(self, x):
            """
            Formats given :math:`x` variable operand to *numeric* or *ndarray*.

            This method is a convenient method to prepare the given :math:`x`
            variable for the arithmetic operations below.

            Parameters
            ----------
            x : numeric or ndarray or SpectralPowerDistribution
                Variable to format.

            Returns
            -------
            numeric or ndarray
                Formatted operand.
            """

            if issubclass(type(x), SpectralPowerDistribution):
                x = x.values
            elif is_iterable(x):
                x = to_ndarray(x)

            return x

        def __add__(self, x):
            """
            Implements support for spectral power distribution addition.

            Parameters
            ----------
            x : numeric or array_like or SpectralPowerDistribution
                Variable to add.

            Returns
            -------
            SpectralPowerDistribution
                Variable added spectral power distribution.

            See Also
            --------
            SpectralPowerDistribution.__sub__, SpectralPowerDistribution.__mul__,
            SpectralPowerDistribution.__div__

            Notes
            -----
            -   Reimplements the :meth:`object.__add__` method.

            Warning
            -------
            The addition operation happens in place.

            Examples
            --------
            Adding a single *numeric* variable:

            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd + 10  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([ 59.67,  79.59,  91.73,  98.19])

            Adding an *array_like* variable:

            >>> spd + [1, 2, 3, 4]  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([  60.67,   81.59,   94.73,  102.19])

            Adding a :class:`SpectralPowerDistribution` class variable:

            >>> spd_alternate = SpectralPowerDistribution('Spd', data)
            >>> spd + spd_alternate  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([ 110.34,  151.18,  176.46,  190.38])
            """

            self.__data = dict(zip(self.wavelengths,
                                   self.values + self.__format_operand(x)))

            return self

        def __sub__(self, x):
            """
            Implements support for spectral power distribution subtraction.

            Parameters
            ----------
            x : numeric or array_like or SpectralPowerDistribution
                Variable to subtract.

            Returns
            -------
            SpectralPowerDistribution
                Variable subtracted spectral power distribution.

            See Also
            --------
            SpectralPowerDistribution.__add__, SpectralPowerDistribution.__mul__,
            SpectralPowerDistribution.__div__

            Notes
            -----
            -   Reimplements the :meth:`object.__sub__` method.

            Warning
            -------
            The subtraction operation happens in place.

            Examples
            --------
            Subtracting a single *numeric* variable:

            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd - 10  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([ 39.67,  59.59,  71.73,  78.19])

            Subtracting an *array_like* variable:

            >>> spd - [1, 2, 3, 4]  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([ 38.67,  57.59,  68.73,  74.19])

            Subtracting a :class:`SpectralPowerDistribution` class variable:

            >>> spd_alternate = SpectralPowerDistribution('Spd', data)
            >>> spd - spd_alternate  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([-11., -12., -13., -14.])
            """

            return self + (-self.__format_operand(x))

        def __mul__(self, x):
            """
            Implements support for spectral power distribution multiplication.

            Parameters
            ----------
            x : numeric or array_like or SpectralPowerDistribution
                Variable to multiply.

            Returns
            -------
            SpectralPowerDistribution
                Variable multiplied spectral power distribution.

            See Also
            --------
            SpectralPowerDistribution.__add__, SpectralPowerDistribution.__sub__,
            SpectralPowerDistribution.__div__

            Notes
            -----
            -   Reimplements the :meth:`object.__mul__` method.

            Warning
            -------
            The multiplication operation happens in place.

            Examples
            --------
            Multiplying a single *numeric* variable:

            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd * 10  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([ 496.7,  695.9,  817.3,  881.9])

            Multiplying an *array_like* variable:

            >>> spd * [1, 2, 3, 4]  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([  496.7,  1391.8,  2451.9,  3527.6])

            Multiplying a :class:`SpectralPowerDistribution` class variable:

            >>> spd_alternate = SpectralPowerDistribution('Spd', data)
            >>> spd * spd_alternate  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([  24671.089,   96855.362,  200393.787,  311099.044])
            """

            self.__data = dict(zip(self.wavelengths,
                                   self.values * self.__format_operand(x)))

            return self

        def __div__(self, x):
            """
            Implements support for spectral power distribution division.

            Parameters
            ----------
            x : numeric or array_like or SpectralPowerDistribution
                Variable to divide.

            Returns
            -------
            SpectralPowerDistribution
                Variable divided spectral power distribution.

            See Also
            --------
            SpectralPowerDistribution.__add__, SpectralPowerDistribution.__sub__,
            SpectralPowerDistribution.__mul__

            Notes
            -----
            -   Reimplements the :meth:`object.__div__` method.

            Warning
            -------
            The division operation happens in place.

            Examples
            --------
            Dividing a single *numeric* variable:

            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd / 10  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([ 4.967,  6.959,  8.173,  8.819])

            Dividing an *array_like* variable:

            >>> spd / [1, 2, 3, 4]  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([ 4.967     ,  3.4795    ,  2.72433333,  2.20475   ])

            Dividing a :class:`SpectralPowerDistribution` class variable:

            >>> spd_alternate = SpectralPowerDistribution('Spd', data)
            >>> spd / spd_alternate  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values  # doctest: +ELLIPSIS
            array([ 0.1       ,  0.05      ,  0.0333333...,  0.025     ])
            """

            self.__data = dict(zip(self.wavelengths,
                                   self.values * (1 / self.__format_operand(x))))

            return self

        # Python 3 compatibility.
        __truediv__ = __div__

        def get(self, wavelength, default=None):
            """
            Returns the value for given wavelength :math:`\lambda`.

            Parameters
            ----------
            wavelength : numeric
                Wavelength :math:`\lambda` to retrieve the value.
            default : None or numeric, optional
                Wavelength :math:`\lambda` default value.

            Returns
            -------
            numeric
                Wavelength :math:`\lambda` value.

            See Also
            --------
            SpectralPowerDistribution.__getitem__

            Examples
            --------
            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> # Doctests ellipsis for Python 2.x compatibility.
            >>> spd.get(510)  # doctest: +ELLIPSIS
            49.67...
            >>> spd.get(511)  # doctest: +SKIP
            None
            """

            try:
                return self.__getitem__(wavelength)
            except KeyError:
                return default

        def is_uniform(self):
            """
            Returns if the spectral power distribution has uniformly spaced data.

            Returns
            -------
            bool
                Is uniform.

            See Also
            --------
            SpectralPowerDistribution.shape

            Examples
            --------
            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd.is_uniform()
            True

            Breaking the steps by introducing a new wavelength :math:`\lambda`
            value:

            >>> spd[511] = 3.1415
            >>> spd.is_uniform()
            False
            """

            return is_uniform(self.wavelengths)

        def extrapolate(self,
                        shape,
                        method='Constant',
                        left=None,
                        right=None):
            """
            Extrapolates the spectral power distribution following *CIE 15:2004*
            recommendation.

            Parameters
            ----------
            shape : SpectralShape
                Spectral shape used for extrapolation.
            method : unicode, optional
                ('Linear', 'Constant'),
                Extrapolation method.
            left : numeric, optional
                Value to return for low extrapolation range.
            right : numeric, optional
                Value to return for high extrapolation range.

            Returns
            -------
            SpectralPowerDistribution
                Extrapolated spectral power distribution.

            See Also
            --------
            SpectralPowerDistribution.align

            References
            ----------

            .. [2]  `CIE 015:2004 Colorimetry, 3rd edition: 7.2.2.1 Extrapolation
                    <https://law.resource.org/pub/us/cfr/ibr/003/cie.15.2004.pdf>`_
            .. [3]  `CIE 167:2005 Recommended Practice for Tabulating Spectral Data
                    for Use in Colour Computations: 10. EXTRAPOLATION
                    <http://div1.cie.co.at/?i_ca_id=551&pubid=47>`_
            Examples
            --------
            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd.extrapolate(SpectralShape(400, 700)).shape
            SpectralShape(400, 700, 10)
            >>> # Doctests ellipsis for Python 2.x compatibility.
            >>> spd[400]  # doctest: +ELLIPSIS
            49.67...
            >>> # Doctests ellipsis for Python 2.x compatibility.
            >>> spd[700]  # doctest: +ELLIPSIS
            88.1...
            """

            extrapolator = Extrapolator1d(
                LinearInterpolator1d(self.wavelengths,
                                     self.values),
                method=method,
                left=left,
                right=right)

            spd_shape = self.shape
            for i in np.arange(spd_shape.start,
                               shape.start - spd_shape.steps,
                               -spd_shape.steps):
                self[i] = extrapolator(float(i))
            for i in np.arange(spd_shape.end,
                               shape.end + spd_shape.steps,
                               spd_shape.steps):
                self[i] = extrapolator(float(i))

            return self

        def interpolate(self, shape=SpectralShape(), method=None):
            """
            Interpolates the spectral power distribution following
            *CIE 167:2005* recommendations: the method developed by
            *Sprague (1880)* should be used for interpolating functions having a
            uniformly spaced independent variable and a *Cubic Spline* method for
            non-uniformly spaced independent variable.

            Parameters
            ----------
            shape : SpectralShape, optional
                Spectral shape used for interpolation.
            method : unicode, optional
                ('Sprague', 'Cubic Spline', 'Linear'),
                Enforce given interpolation method.

            Returns
            -------
            SpectralPowerDistribution
                Interpolated spectral power distribution.

            Raises
            ------
            RuntimeError
                If the *Sprague* interpolation method is forced with a
                non-uniformly spaced independent variable.
            ValueError
                If the interpolation method is not defined.

            See Also
            --------
            SpectralPowerDistribution.align

            Notes
            -----
            -   Interpolation will be conducted over boundaries range, if you need
                to extend the range of the spectral power distribution use the
                :meth:`SpectralPowerDistribution.extrapolate` or
                :meth:`SpectralPowerDistribution.align` methods.
            -   *Sprague* interpolator cannot be used for interpolating
                functions having a non-uniformly spaced independent variable.

            Warning
            -------
            -   If *scipy* is not unavailable the *Cubic Spline* method will
                fallback to legacy *Linear* interpolation.
            -   *Linear* interpolator requires at least 2 wavelengths
                :math:`\lambda_n` for interpolation.
            -   *Cubic Spline* interpolator requires at least 3 wavelengths
                :math:`\lambda_n` for interpolation.
            -   *Sprague* interpolator requires at least 6 wavelengths
                :math:`\lambda_n` for interpolation.

            References
            ----------
            .. [4]  `CIE 167:2005 Recommended Practice for Tabulating Spectral Data
                    for Use in Colour Computations: 9. INTERPOLATION
                    <http://div1.cie.co.at/?i_ca_id=551&pubid=47>`_

            Examples
            --------
            Uniform data is using *Sprague* interpolation by default:

            >>> data = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 86.26,
            ...     560: 77.18}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd.interpolate(SpectralShape(steps=1))  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd[515]  # doctest: +ELLIPSIS
            60.3121800...

            Non uniform data is using *Cubic Spline* interpolation by default:

            >>> data = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 86.26,
            ...     560: 77.18}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd[511] = 31.41
            >>> spd.interpolate(SpectralShape(steps=1))  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd[515]  # doctest: +ELLIPSIS
            21.4792222...

            Enforcing *Linear* interpolation:

            >>> data = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 86.26,
            ...     560: 77.18}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd.interpolate(SpectralShape(steps=1), method='Linear')  # noqa  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> # Doctests ellipsis for Python 2.x compatibility.
            >>> spd[515]  # doctest: +ELLIPSIS
            59.63...
            """

            spd_shape = self.shape
            boundaries = zip((shape.start, shape.end, shape.steps),
                             (spd_shape.start, spd_shape.end, spd_shape.steps))
            boundaries = [x[0] if x[0] is not None else x[1] for x in boundaries]
            shape = SpectralShape(*boundaries)

            # Defining proper interpolation bounds.
            # TODO: Provide support for fractional steps like 0.1, etc...
            shape.start = max(shape.start, math.ceil(spd_shape.start))
            shape.end = min(shape.end, math.floor(spd_shape.end))

            wavelengths, values = self.wavelengths, self.values
            is_uniform = self.is_uniform()

            if is_string(method):
                method = method.lower()

            if method is None:
                if is_uniform:
                    interpolator = SpragueInterpolator(wavelengths, values)
                else:
                    interpolator = SplineInterpolator(wavelengths, values)
            elif method == 'sprague':
                if is_uniform:
                    interpolator = SpragueInterpolator(wavelengths, values)
                else:
                    raise RuntimeError(
                        ('"Sprague" interpolator can only be used for '
                         'interpolating functions having a uniformly spaced '
                         'independent variable!'))
            elif method == 'cubic spline':
                interpolator = SplineInterpolator(wavelengths, values)
            elif method == 'linear':
                interpolator = LinearInterpolator1d(wavelengths, values)
            else:
                raise ValueError(
                    'Undefined "{0}" interpolator!'.format(method))

            self.__data = dict([(wavelength, float(interpolator(wavelength)))
                                for wavelength in shape])
            return self

        def align(self,
                  shape,
                  method='Constant',
                  left=None,
                  right=None):
            """
            Aligns the spectral power distribution to given spectral shape:
            Interpolates first then extrapolates to fit the given range.

            Parameters
            ----------
            shape : SpectralShape
                Spectral shape used for alignment.
            method : unicode, optional
                ('Linear', 'Constant'),
                Extrapolation method.
            left : numeric, optional
                Value to return for low extrapolation range.
            right : numeric, optional
                Value to return for high extrapolation range.

            Returns
            -------
            SpectralPowerDistribution
                Aligned spectral power distribution.

            See Also
            --------
            SpectralPowerDistribution.extrapolate,
            SpectralPowerDistribution.interpolate

            Examples
            --------
            >>> data = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 86.26,
            ...     560: 77.18}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd.align(SpectralShape(505, 565, 1))  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> # Doctests skip for Python 2.x compatibility.
            >>> spd.wavelengths  # doctest: +SKIP
            array([505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517,
                   518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530,
                   531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543,
                   544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556,
                   557, 558, 559, 560, 561, 562, 563, 564, 565])
            >>> spd.values  # doctest: +ELLIPSIS
            array([ 49.67     ...,  49.67     ...,  49.67     ...,  49.67     ...,
                    49.67     ...,  49.67     ...,  51.8341162...,  53.9856467...,
                    56.1229464...,  58.2366197...,  60.3121800...,  62.3327095...,
                    64.2815187...,  66.1448055...,  67.9143153...,  69.59     ...,
                    71.1759958...,  72.6627938...,  74.0465756...,  75.3329710...,
                    76.5339542...,  77.6647421...,  78.7406907...,  79.7741932...,
                    80.7715767...,  81.73     ...,  82.6407518...,  83.507872 ...,
                    84.3326333...,  85.109696 ...,  85.8292968...,  86.47944  ...,
                    87.0480863...,  87.525344 ...,  87.9056578...,  88.19     ...,
                    88.3858347...,  88.4975634...,  88.5258906...,  88.4696570...,
                    88.3266460...,  88.0943906...,  87.7709802...,  87.3558672...,
                    86.8506741...,  86.26     ...,  85.5911699...,  84.8503430...,
                    84.0434801...,  83.1771110...,  82.2583874...,  81.2951360...,
                    80.2959122...,  79.2700525...,  78.2277286...,  77.18     ...,
                    77.18     ...,  77.18     ...,  77.18     ...,  77.18     ...,  77.18      ])
            """

            self.interpolate(shape)
            self.extrapolate(shape, method, left, right)

            return self

        def zeros(self, shape=SpectralShape()):
            """
            Zeros fills the spectral power distribution: Missing values will be
            replaced with zeros to fit the defined range.

            Parameters
            ----------
            shape : SpectralShape, optional
                Spectral shape used for zeros fill.

            Returns
            -------
            SpectralPowerDistribution
                Zeros filled spectral power distribution.

            Examples
            --------
            >>> data = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 86.26,
            ...     560: 77.18}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd.zeros(SpectralShape(505, 565, 1))  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values
            array([  0.  ,   0.  ,   0.  ,   0.  ,   0.  ,  49.67,   0.  ,   0.  ,
                     0.  ,   0.  ,   0.  ,   0.  ,   0.  ,   0.  ,   0.  ,  69.59,
                     0.  ,   0.  ,   0.  ,   0.  ,   0.  ,   0.  ,   0.  ,   0.  ,
                     0.  ,  81.73,   0.  ,   0.  ,   0.  ,   0.  ,   0.  ,   0.  ,
                     0.  ,   0.  ,   0.  ,  88.19,   0.  ,   0.  ,   0.  ,   0.  ,
                     0.  ,   0.  ,   0.  ,   0.  ,   0.  ,  86.26,   0.  ,   0.  ,
                     0.  ,   0.  ,   0.  ,   0.  ,   0.  ,   0.  ,   0.  ,  77.18,
                     0.  ,   0.  ,   0.  ,   0.  ,   0.  ])
            """

            spd_shape = self.shape
            boundaries = zip((shape.start, shape.end, shape.steps),
                             (spd_shape.start, spd_shape.end, spd_shape.end))
            boundaries = [x[0] if x[0] is not None else x[1] for x in boundaries]
            shape = SpectralShape(*boundaries)

            self.__data = dict(
                [(wavelength, self.get(wavelength, 0)) for wavelength in shape])

            return self

        def normalise(self, factor=1):
            """
            Normalises the spectral power distribution with given normalization
            factor.

            Parameters
            ----------
            factor : numeric, optional
                Normalization factor

            Returns
            -------
            SpectralPowerDistribution
                Normalised spectral power distribution.

            Examples
            --------
            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> spd.normalise()  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd.values  # doctest: +ELLIPSIS
            array([ 0.5632157...,  0.7890917...,  0.9267490...,  1.        ])
            """

            return (self * (1 / max(self.values))) * factor

        def clone(self):
            """
            Clones the spectral power distribution.

            Most of the :class:`SpectralPowerDistribution` class operations are
            conducted in-place. The :meth:`SpectralPowerDistribution.clone` method
            provides a convenient way to copy the spectral power distribution to a
            new object.

            Returns
            -------
            SpectralPowerDistribution
                Cloned spectral power distribution.

            Examples
            --------
            >>> data = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> spd = SpectralPowerDistribution('Spd', data)
            >>> print(spd)  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            >>> spd_clone = spd.clone()
            >>> print(spd_clone)  # doctest: +ELLIPSIS
            <...SpectralPowerDistribution object at 0x...>
            """

            return copy.deepcopy(self)

    class TriSpectralPowerDistribution(object):
        """
        Defines the base object for colour matching functions.

        A compound of three :class:`SpectralPowerDistribution` is used to store
        the underlying axis data.

        Parameters
        ----------
        name : str or unicode
            Tri-spectral power distribution name.
        data : dict
            Tri-spectral power distribution data.
        mapping : dict
            Tri-spectral power distribution attributes mapping.
        labels : dict
            Tri-spectral power distribution axis labels mapping.

        Attributes
        ----------
        name
        mapping
        data
        labels
        x
        y
        z
        wavelengths
        values
        items
        shape

        Methods
        -------
        __init__
        __getitem__
        __setitem__
        __iter__
        __contains__
        __len__
        __eq__
        __ne__
        __add__
        __sub__
        __mul__
        __div__
        __truediv__
        get
        is_uniform
        extrapolate
        interpolate
        align
        zeros
        normalise
        clone

        See Also
        --------
        colour.colorimetry.cmfs.LMS_ConeFundamentals,
        colour.colorimetry.cmfs.RGB_ColourMatchingFunctions,
        colour.colorimetry.cmfs.XYZ_ColourMatchingFunctions

        Examples
        --------
        >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
        >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
        >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
        >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
        >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
        >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
        >>> tri_spd.wavelengths
        array([510, 520, 530, 540])
        >>> tri_spd.values
        array([[ 49.67,  90.56,  12.43],
               [ 69.59,  87.34,  23.15],
               [ 81.73,  45.76,  67.98],
               [ 88.19,  23.45,  90.28]])
        >>> tri_spd.shape
        SpectralShape(510, 540, 10)
        """

        def __init__(self, name, data, mapping, labels):
            self.__name = None
            self.name = name
            self.__mapping = mapping
            self.__data = None
            self.data = data
            self.__labels = labels

        @property
        def name(self):
            """
            Property for **self.__name** private attribute.

            Returns
            -------
            str or unicode
                self.__name.
            """

            return self.__name

        @name.setter
        def name(self, value):
            """
            Setter for **self.__name** private attribute.

            Parameters
            ----------
            value : str or unicode
                Attribute value.
            """

            if value is not None:
                assert type(value) in (str, unicode), (
                    ('"{0}" attribute: "{1}" type is not '
                     '"str" or "unicode"!').format('name', value))
            self.__name = value

        @property
        def mapping(self):
            """
            Property for **self.__mapping** private attribute.

            Returns
            -------
            dict
                self.__mapping.
            """

            return self.__mapping

        @mapping.setter
        def mapping(self, value):
            """
            Setter for **self.__mapping** private attribute.

            Parameters
            ----------
            value : dict
                Attribute value.
            """

            if value is not None:
                assert type(value) is dict, (
                    '"{0}" attribute: "{1}" type is not "dict"!'.format(
                        'mapping', value))
                for axis in ('x', 'y', 'z'):
                    assert axis in value.keys(), (
                        '"{0}" attribute: "{1}" axis label is missing!'.format(
                            'mapping', axis))
            self.__mapping = value

        @property
        def data(self):
            """
            Property for **self.__data** private attribute.

            Returns
            -------
            dict
                self.__data.
            """

            return self.__data

        @data.setter
        def data(self, value):
            """
            Setter for **self.__data** private attribute.

            Parameters
            ----------
            value : dict
                Attribute value.
            """

            if value is not None:
                assert type(value) is dict, (
                    '"{0}" attribute: "{1}" type is not "dict"!'.format(
                        'data', value))
                for axis in ('x', 'y', 'z'):
                    assert self.__mapping.get(axis) in value.keys(), (
                        '"{0}" attribute: "{1}" axis is missing!'.format(
                            'data', axis))

                data = {}
                for axis in ('x', 'y', 'z'):
                    data[axis] = SpectralPowerDistribution(
                        self.__mapping.get(axis),
                        value.get(self.__mapping.get(axis)))

                np.testing.assert_almost_equal(
                    data['x'].wavelengths,
                    data['y'].wavelengths,
                    err_msg=('"{0}" attribute: "{1}" and "{2}" wavelengths are '
                             'different!').format('data',
                                                  self.__mapping.get('x'),
                                                  self.__mapping.get('y')))
                np.testing.assert_almost_equal(
                    data['x'].wavelengths,
                    data['z'].wavelengths,
                    err_msg=('"{0}" attribute: "{1}" and "{2}" wavelengths are '
                             'different!').format('data',
                                                  self.__mapping.get('x'),
                                                  self.__mapping.get('z')))

                self.__data = data
            else:
                self.__data = None

        @property
        def labels(self):
            """
            Property for **self.__labels** private attribute.

            Returns
            -------
            dict
                self.__labels.
            """

            return self.__labels

        @labels.setter
        def labels(self, value):
            """
            Setter for **self.__labels** private attribute.

            Parameters
            ----------
            value : dict
                Attribute value.
            """

            if value is not None:
                assert type(value) is dict, (
                    '"{0}" attribute: "{1}" type is not "dict"!'.format(
                        'labels', value))
                for axis in ('x', 'y', 'z'):
                    assert axis in value.keys(), (
                        '"{0}" attribute: "{1}" axis label is missing!'.format(
                            'labels', axis))
            self.__labels = value

        @property
        def x(self):
            """
            Property for **self.x** attribute.

            Returns
            -------
            SpectralPowerDistribution
                Spectral power distribution for *x* axis.

            Warning
            -------
            :attr:`TriSpectralPowerDistribution.x` is read only.
            """

            return self.__data.get('x')

        @x.setter
        def x(self, value):
            """
            Setter for **self.x** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError('"{0}" attribute is read only!'.format('x'))

        @property
        def y(self):
            """
            Property for **self.y** attribute.

            Returns
            -------
            SpectralPowerDistribution
                Spectral power distribution for *y* axis.

            Warning
            -------
            :attr:`TriSpectralPowerDistribution.y` is read only.
            """

            return self.__data.get('y')

        @y.setter
        def y(self, value):
            """
            Setter for **self.y** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError('"{0}" attribute is read only!'.format('y'))

        @property
        def z(self):
            """
            Property for **self.z** attribute.

            Returns
            -------
            SpectralPowerDistribution
                Spectral power distribution for *z* axis.

            Warning
            -------
            :attr:`TriSpectralPowerDistribution.z` is read only.
            """

            return self.__data.get('z')

        @z.setter
        def z(self, value):
            """
            Setter for **self.z** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError('"{0}" attribute is read only!'.format('z'))

        @property
        def wavelengths(self):
            """
            Property for **self.wavelengths** attribute.

            Returns
            -------
            ndarray
                Tri-spectral power distribution wavelengths :math:`\lambda_n`.

            Warning
            -------
            :attr:`TriSpectralPowerDistribution.wavelengths` is read only.
            """

            return self.x.wavelengths

        @wavelengths.setter
        def wavelengths(self, value):
            """
            Setter for **self.wavelengths** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError(
                '"{0}" attribute is read only!'.format('wavelengths'))

        @property
        def values(self):
            """
            Property for **self.values** attribute.

            Returns
            -------
            ndarray
                Tri-spectral power distribution wavelengths :math:`\lambda_n`
                values.

            Warning
            -------
            :attr:`TriSpectralPowerDistribution.values` is read only.
            """

            return np.array([self.get(wavelength)
                             for wavelength in self.wavelengths])

        @values.setter
        def values(self, value):
            """
            Setter for **self.values** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError('"{0}" attribute is read only!'.format('values'))

        @property
        def items(self):
            """
            Property for **self.items** attribute. This is a convenient attribute
            used to iterate over the tri-spectral power distribution.

            Returns
            -------
            generator
                Tri-spectral power distribution data generator.
            """

            return self.__iter__()

        @items.setter
        def items(self, value):
            """
            Setter for **self.items** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError('"{0}" attribute is read only!'.format('items'))

        @property
        def shape(self):
            """
            Property for **self.shape** attribute.

            Returns the shape of the tri-spectral power distribution in the form of
            a :class:`SpectralShape` class instance.

            Returns
            -------
            SpectralShape
                Tri-spectral power distribution shape.

            See Also
            --------
            SpectralPowerDistribution.is_uniform,
            TriSpectralPowerDistribution.is_uniform

            Warning
            -------
            :attr:`TriSpectralPowerDistribution.shape` is read only.

            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd.shape
            SpectralShape(510, 540, 10)
            """

            return self.x.shape

        @shape.setter
        def shape(self, value):
            """
            Setter for **self.shape** attribute.

            Parameters
            ----------
            value : object
                Attribute value.
            """

            raise AttributeError('"{0}" attribute is read only!'.format('shape'))

        def __hash__(self):
            """
            Returns the spectral power distribution hash value.

            Returns
            -------
            int
                Object hash.

            Notes
            -----
            -   Reimplements the :meth:`object.__hash__` method.

            Warning
            -------
            See :meth:`SpectralPowerDistribution.__hash__` method warning section.

            References
            ----------
            .. [5]  http://stackoverflow.com/a/16162138/931625
                    (Last accessed 8 August 2014)
            """

            return hash((frozenset(self.__data.get('x')),
                         frozenset(self.__data.get('y')),
                         frozenset(self.__data.get('z'))))

        def __getitem__(self, wavelength):
            """
            Returns the values for given wavelength :math:`\lambda`.

            Parameters
            ----------
            wavelength: numeric
                Wavelength :math:`\lambda` to retrieve the values.

            Returns
            -------
            ndarray, (3,)
                Wavelength :math:`\lambda` values.

            See Also
            --------
            TriSpectralPowerDistribution.get

            Notes
            -----
            -   Reimplements the :meth:`object.__getitem__` method.

            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd[510]
            array([ 49.67,  90.56,  12.43])
            """

            return np.array((self.x[wavelength],
                             self.y[wavelength],
                             self.z[wavelength]))

        def __setitem__(self, wavelength, value):
            """
            Sets the wavelength :math:`\lambda` with given value.

            Parameters
            ----------
            wavelength : numeric
                Wavelength :math:`\lambda` to set.
            value : array_like
                Value for wavelength :math:`\lambda`.

            Notes
            -----
            -   Reimplements the :meth:`object.__setitem__` method.

            Examples
            --------
            >>> x_bar = {}
            >>> y_bar = {}
            >>> z_bar = {}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd[510] = (49.6700, 49.6700, 49.6700)
            >>> tri_spd.values
            array([[ 49.67,  49.67,  49.67]])
            """

            x, y, z = np.ravel(value)

            self.x.__setitem__(wavelength, x)
            self.y.__setitem__(wavelength, y)
            self.z.__setitem__(wavelength, z)

        def __iter__(self):
            """
            Returns a generator for the tri-spectral power distribution data.

            Returns
            -------
            generator
                Tri-spectral power distribution data generator.

            Notes
            -----
            -   Reimplements the :meth:`object.__iter__` method.

            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> for wavelength, value in tri_spd: print((wavelength, value))
            (510, array([ 49.67,  90.56,  12.43]))
            (520, array([ 69.59,  87.34,  23.15]))
            (530, array([ 81.73,  45.76,  67.98]))
            (540, array([ 88.19,  23.45,  90.28]))
            """

            return itertools.izip(self.wavelengths, self.values)

        def __contains__(self, wavelength):
            """
            Returns if the tri-spectral power distribution contains the given
            wavelength :math:`\lambda`.

            Parameters
            ----------
            wavelength : numeric
                Wavelength :math:`\lambda`.

            Returns
            -------
            bool
                Is wavelength :math:`\lambda` in the tri-spectral power
                distribution.

            Notes
            -----
            -   Reimplements the :meth:`object.__contains__` method.

            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> 510 in tri_spd
            True

            """

            return wavelength in self.x

        def __len__(self):
            """
            Returns the tri-spectral power distribution wavelengths
            :math:`\lambda_n` count.

            Returns
            -------
            int
                Tri-Spectral power distribution wavelengths :math:`\lambda_n`
                count.

            Notes
            -----
            -   Reimplements the :meth:`object.__len__` method.

            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> len(tri_spd)
            4
            """

            return len(self.x)

        def __eq__(self, tri_spd):
            """
            Returns the tri-spectral power distribution equality with given other
            tri-spectral power distribution.

            Parameters
            ----------
            spd : TriSpectralPowerDistribution
                Tri-spectral power distribution to compare for equality.

            Returns
            -------
            bool
                Tri-spectral power distribution equality.

            Notes
            -----
            -   Reimplements the :meth:`object.__eq__` method.

            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data1 = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> data2 = {'x_bar': y_bar, 'y_bar': x_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd1 = TriSpectralPowerDistribution('Tri Spd', data1, mpg, lbl)
            >>> tri_spd2 = TriSpectralPowerDistribution('Tri Spd', data2, mpg, lbl)
            >>> tri_spd3 = TriSpectralPowerDistribution('Tri Spd', data1, mpg, lbl)
            >>> tri_spd1 == tri_spd2
            False
            >>> tri_spd1 == tri_spd3
            True
            """

            if not isinstance(tri_spd, self.__class__):
                return False

            equality = True
            for axis in self.__mapping:
                equality *= getattr(self, axis) == getattr(tri_spd, axis)

            return bool(equality)

        def __ne__(self, tri_spd):
            """
            Returns the tri-spectral power distribution inequality with given other
            tri-spectral power distribution.

            Parameters
            ----------
            spd : TriSpectralPowerDistribution
                Tri-spectral power distribution to compare for inequality.

            Returns
            -------
            bool
                Tri-spectral power distribution inequality.

            Notes
            -----
            -   Reimplements the :meth:`object.__eq__` method.

            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data1 = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> data2 = {'x_bar': y_bar, 'y_bar': x_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd1 = TriSpectralPowerDistribution('Tri Spd', data1, mpg, lbl)
            >>> tri_spd2 = TriSpectralPowerDistribution('Tri Spd', data2, mpg, lbl)
            >>> tri_spd3 = TriSpectralPowerDistribution('Tri Spd', data1, mpg, lbl)
            >>> tri_spd1 != tri_spd2
            True
            >>> tri_spd1 != tri_spd3
            False
            """

            return not (self == tri_spd)

        def __format_operand(self, x):
            """
            Formats given :math:`x` variable operand to *numeric* or *ndarray*.

            This method is a convenient method to prepare the given :math:`x`
            variable for the arithmetic operations below.

            Parameters
            ----------
            x : numeric or ndarray or TriSpectralPowerDistribution
                Variable to format.

            Returns
            -------
            numeric or ndarray
                Formatted operand.
            """

            if issubclass(type(x), TriSpectralPowerDistribution):
                x = x.values
            elif is_iterable(x):
                x = to_ndarray(x)

            return x

        def __add__(self, x):
            """
            Implements support for tri-spectral power distribution addition.

            Parameters
            ----------
            x : numeric or array_like or TriSpectralPowerDistribution
                Variable to add.

            Returns
            -------
            TriSpectralPowerDistribution
                Variable added tri-spectral power distribution.

            See Also
            --------
            TriSpectralPowerDistribution.__sub__,
            TriSpectralPowerDistribution.__mul__,
            TriSpectralPowerDistribution.__div__

            Notes
            -----
            -   Reimplements the :meth:`object.__add__` method.

            Warning
            -------
            The addition operation happens in place.

            Examples
            --------
            Adding a single *numeric* variable:

            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd + 10  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[  59.67,  100.56,   22.43],
                   [  79.59,   97.34,   33.15],
                   [  91.73,   55.76,   77.98],
                   [  98.19,   33.45,  100.28]])

            Adding an *array_like* variable:

            >>> tri_spd + [(1, 2, 3)] * 4  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[  60.67,  102.56,   25.43],
                   [  80.59,   99.34,   36.15],
                   [  92.73,   57.76,   80.98],
                   [  99.19,   35.45,  103.28]])

            Adding a :class:`TriSpectralPowerDistribution` class variable:

            >>> data1 = {'x_bar': z_bar, 'y_bar': x_bar, 'z_bar': y_bar}
            >>> tri_spd1 = TriSpectralPowerDistribution('Tri Spd', data1, mpg, lbl)
            >>> tri_spd + tri_spd1  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[  73.1 ,  152.23,  115.99],
                   [ 103.74,  168.93,  123.49],
                   [ 160.71,  139.49,  126.74],
                   [ 189.47,  123.64,  126.73]])
            """

            values = self.values + self.__format_operand(x)

            for i, axis in enumerate(('x', 'y', 'z')):
                self.__data[axis].data = dict(zip(self.wavelengths, values[:, i]))

            return self

        def __sub__(self, x):
            """
            Implements support for tri-spectral power distribution subtraction.

            Parameters
            ----------
            x : numeric or array_like or TriSpectralPowerDistribution
                Variable to subtract.

            Returns
            -------
            TriSpectralPowerDistribution
                Variable subtracted tri-spectral power distribution.

            See Also
            --------
            TriSpectralPowerDistribution.__add__,
            TriSpectralPowerDistribution.__mul__,
            TriSpectralPowerDistribution.__div__

            Notes
            -----
            -   Reimplements the :meth:`object.__sub__` method.

            Warning
            -------
            The subtraction operation happens in place.

            Examples
            --------
            Subtracting a single *numeric* variable:

            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd - 10  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[ 39.67,  80.56,   2.43],
                   [ 59.59,  77.34,  13.15],
                   [ 71.73,  35.76,  57.98],
                   [ 78.19,  13.45,  80.28]])

            Subtracting an *array_like* variable:

            >>> tri_spd - [(1, 2, 3)] * 4  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[ 38.67,  78.56,  -0.57],
                   [ 58.59,  75.34,  10.15],
                   [ 70.73,  33.76,  54.98],
                   [ 77.19,  11.45,  77.28]])

            Subtracting a :class:`TriSpectralPowerDistribution` class variable:

            >>> data1 = {'x_bar': z_bar, 'y_bar': x_bar, 'z_bar': y_bar}
            >>> tri_spd1 = TriSpectralPowerDistribution('Tri Spd', data1, mpg, lbl)
            >>> tri_spd - tri_spd1  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[ 26.24,  28.89, -91.13],
                   [ 35.44,   5.75, -77.19],
                   [  2.75, -47.97,   9.22],
                   [-13.09, -76.74,  53.83]])
            """

            return self + (-self.__format_operand(x))

        def __mul__(self, x):
            """
            Implements support for tri-spectral power distribution multiplication.

            Parameters
            ----------
            x : numeric or array_like or TriSpectralPowerDistribution
                Variable to multiply.

            Returns
            -------
            TriSpectralPowerDistribution
                Variable multiplied tri-spectral power distribution.

            See Also
            --------
            TriSpectralPowerDistribution.__add__,
            TriSpectralPowerDistribution.__sub__,
            TriSpectralPowerDistribution.__div__

            Notes
            -----
            -   Reimplements the :meth:`object.__mul__` method.

            Warning
            -------
            The multiplication operation happens in place.

            Examples
            --------
            Multiplying a single *numeric* variable:

            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd * 10  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[ 496.7,  905.6,  124.3],
                   [ 695.9,  873.4,  231.5],
                   [ 817.3,  457.6,  679.8],
                   [ 881.9,  234.5,  902.8]])

            Multiplying an *array_like* variable:

            >>> tri_spd * [(1, 2, 3)] * 4  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[  1986.8,   7244.8,   1491.6],
                   [  2783.6,   6987.2,   2778. ],
                   [  3269.2,   3660.8,   8157.6],
                   [  3527.6,   1876. ,  10833.6]])

            Multiplying a :class:`TriSpectralPowerDistribution` class variable:

            >>> data1 = {'x_bar': z_bar, 'y_bar': x_bar, 'z_bar': y_bar}
            >>> tri_spd1 = TriSpectralPowerDistribution('Tri Spd', data1, mpg, lbl)
            >>> tri_spd * tri_spd1  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[  24695.924,  359849.216,  135079.296],
                   [  64440.34 ,  486239.248,  242630.52 ],
                   [ 222240.216,  299197.184,  373291.776],
                   [ 318471.728,  165444.44 ,  254047.92 ]])
            """

            values = self.values * self.__format_operand(x)

            for i, axis in enumerate(('x', 'y', 'z')):
                self.__data[axis].data = dict(zip(self.wavelengths, values[:, i]))

            return self

        def __div__(self, x):
            """
            Implements support for tri-spectral power distribution division.

            Parameters
            ----------
            x : numeric or array_like or TriSpectralPowerDistribution
                Variable to divide.

            Returns
            -------
            TriSpectralPowerDistribution
                Variable divided tri-spectral power distribution.

            See Also
            --------
            TriSpectralPowerDistribution.__add__,
            TriSpectralPowerDistribution.__sub__,
            TriSpectralPowerDistribution.__mul__

            Notes
            -----
            -   Reimplements the :meth:`object.__mul__` method.

            Warning
            -------
            The division operation happens in place.

            Examples
            --------
            Dividing a single *numeric* variable:

            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd / 10  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[ 4.967,  9.056,  1.243],
                   [ 6.959,  8.734,  2.315],
                   [ 8.173,  4.576,  6.798],
                   [ 8.819,  2.345,  9.028]])

            Dividing an *array_like* variable:

            >>> tri_spd / [(1, 2, 3)] * 4  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values  # doctest: +ELLIPSIS
            array([[ 19.868     ,  18.112     ,   1.6573333...],
                   [ 27.836     ,  17.468     ,   3.0866666...],
                   [ 32.692     ,   9.152     ,   9.064    ...],
                   [ 35.276     ,   4.69      ,  12.0373333...]])

            Dividing a :class:`TriSpectralPowerDistribution` class variable:

            >>> data1 = {'x_bar': z_bar, 'y_bar': x_bar, 'z_bar': y_bar}
            >>> tri_spd1 = TriSpectralPowerDistribution('Tri Spd', data1, mpg, lbl)
            >>> tri_spd / tri_spd1  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values  # doctest: +ELLIPSIS
            array([[ 1.5983909...,  0.3646466...,  0.0183009...],
                   [ 1.2024190...,  0.2510130...,  0.0353408...],
                   [ 0.4809061...,  0.1119784...,  0.1980769...],
                   [ 0.3907399...,  0.0531806...,  0.5133191...]])
            """

            return self * (1 / self.__format_operand(x))

        # Python 3 compatibility.
        __truediv__ = __div__

        def get(self, wavelength, default=None):
            """
            Returns the values for given wavelength :math:`\lambda`.

            Parameters
            ----------
            wavelength : numeric
                Wavelength :math:`\lambda` to retrieve the values.
            default : None or numeric, optional
                Wavelength :math:`\lambda` default values.

            Returns
            -------
            numeric
                Wavelength :math:`\lambda` values.

            See Also
            --------
            TriSpectralPowerDistribution.__getitem__

            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd[510]
            array([ 49.67,  90.56,  12.43])
            >>> tri_spd.get(511)  # doctest: +SKIP
            None
            """

            try:
                return self.__getitem__(wavelength)
            except KeyError:
                return default

        def is_uniform(self):
            """
            Returns if the tri-spectral power distribution has uniformly spaced
            data.

            Returns
            -------
            bool
                Is uniform.

            See Also
            --------
            TriSpectralPowerDistribution.shape

            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd.is_uniform()
            True

            Breaking the steps by introducing new wavelength :math:`\lambda`
            values:

            >>> tri_spd[511] = (49.6700, 49.6700, 49.6700)
            >>> tri_spd.is_uniform()
            False
            """

            for i in self.__mapping.keys():
                if not getattr(self, i).is_uniform():
                    return False
            return True

        def extrapolate(self,
                        shape,
                        method='Constant',
                        left=None,
                        right=None):
            """
            Extrapolates the tri-spectral power distribution following
            *CIE 15:2004* recommendation.

            Parameters
            ----------
            shape : SpectralShape
                Spectral shape used for extrapolation.
            method : unicode, optional
                ('Linear', 'Constant'),
                Extrapolation method.
            left : numeric, optional
                Value to return for low extrapolation range.
            right : numeric, optional
                Value to return for high extrapolation range.

            Returns
            -------
            TriSpectralPowerDistribution
                Extrapolated tri-spectral power distribution.

            See Also
            --------
            TriSpectralPowerDistribution.align

            References
            ----------

            .. [6]  `CIE 015:2004 Colorimetry, 3rd edition: 7.2.2.1 Extrapolation
                    <https://law.resource.org/pub/us/cfr/ibr/003/cie.15.2004.pdf>`_
            .. [7]  `CIE 167:2005 Recommended Practice for Tabulating Spectral Data
                    for Use in Colour Computations: 10. EXTRAPOLATION
                    <http://div1.cie.co.at/?i_ca_id=551&pubid=47>`_
            Examples
            --------
            >>> x_bar = {510: 49.67, 520: 69.59, 530: 81.73, 540: 88.19}
            >>> y_bar = {510: 90.56, 520: 87.34, 530: 45.76, 540: 23.45}
            >>> z_bar = {510: 12.43, 520: 23.15, 530: 67.98, 540: 90.28}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd.extrapolate(SpectralShape(400, 700)).shape
            SpectralShape(400, 700, 10)
            >>> tri_spd[400]
            array([ 49.67,  90.56,  12.43])
            >>> tri_spd[700]
            array([ 88.19,  23.45,  90.28])
            """

            for i in self.__mapping.keys():
                getattr(self, i).extrapolate(shape, method, left, right)

            return self

        def interpolate(self, shape=SpectralShape(), method=None):
            """
            Interpolates the tri-spectral power distribution following
            *CIE 167:2005* recommendations: the method developed by
            *Sprague (1880)* should be used for interpolating functions having a
            uniformly spaced independent variable and a *Cubic Spline* method for
            non-uniformly spaced independent variable.

            Parameters
            ----------
            shape : SpectralShape, optional
                Spectral shape used for interpolation.
            method : unicode, optional
                ('Sprague', 'Cubic Spline', 'Linear'),
                Enforce given interpolation method.

            Returns
            -------
            TriSpectralPowerDistribution
                Interpolated tri-spectral power distribution.

            See Also
            --------
            TriSpectralPowerDistribution.align

            Notes
            -----
            -   See :meth:`SpectralPowerDistribution.interpolate` method
                notes section.

            Warning
            -------
            See :meth:`SpectralPowerDistribution.interpolate` method warning
            section.

            References
            ----------
            .. [4]  `CIE 167:2005 Recommended Practice for Tabulating Spectral Data
                    for Use in Colour Computations: 9. INTERPOLATION
                    <http://div1.cie.co.at/?i_ca_id=551&pubid=47>`_

            Examples
            --------
            Uniform data is using *Sprague* interpolation by default:

            >>> x_bar = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 89.76,
            ...     560: 90.28}
            >>> y_bar = {
            ...     510: 90.56,
            ...     520: 87.34,
            ...     530: 45.76,
            ...     540: 23.45,
            ...     550: 15.34,
            ...     560: 10.11}
            >>> z_bar = {
            ...     510: 12.43,
            ...     520: 23.15,
            ...     530: 67.98,
            ...     540: 90.28,
            ...     550: 91.61,
            ...     560: 98.24}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd.interpolate(SpectralShape(steps=1))  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd[515]
            array([ 60.30332087,  93.27163315,  13.86051361])

            Non uniform data is using *Cubic Spline* interpolation by default:

            >>> x_bar = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 89.76,
            ...     560: 90.28}
            >>> y_bar = {
            ...     510: 90.56,
            ...     520: 87.34,
            ...     530: 45.76,
            ...     540: 23.45,
            ...     550: 15.34,
            ...     560: 10.11}
            >>> z_bar = {
            ...     510: 12.43,
            ...     520: 23.15,
            ...     530: 67.98,
            ...     540: 90.28,
            ...     550: 91.61,
            ...     560: 98.24}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd[511] = (31.41, 95.27, 15.06)
            >>> tri_spd.interpolate(SpectralShape(steps=1))  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd[515]
            array([  21.47104053,  100.64300155,   18.8165196 ])

            Enforcing *Linear* interpolation:

            >>> x_bar = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 89.76,
            ...     560: 90.28}
            >>> y_bar = {
            ...     510: 90.56,
            ...     520: 87.34,
            ...     530: 45.76,
            ...     540: 23.45,
            ...     550: 15.34,
            ...     560: 10.11}
            >>> z_bar = {
            ...     510: 12.43,
            ...     520: 23.15,
            ...     530: 67.98,
            ...     540: 90.28,
            ...     550: 91.61,
            ...     560: 98.24}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd.interpolate(SpectralShape(steps=1), method='Linear')  # noqa  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd[515]
            array([ 59.63,  88.95,  17.79])
            """

            for i in self.__mapping.keys():
                getattr(self, i).interpolate(shape, method)

            return self

        def align(self,
                  shape,
                  method='Constant',
                  left=None,
                  right=None):
            """
            Aligns the tri-spectral power distribution to given shape: Interpolates
            first then extrapolates to fit the given range.

            Parameters
            ----------
            shape : SpectralShape
                Spectral shape used for alignment.
            method : unicode, optional
                ('Linear', 'Constant'),
                Extrapolation method.
            left : numeric, optional
                Value to return for low extrapolation range.
            right : numeric, optional
                Value to return for high extrapolation range.

            Returns
            -------
            TriSpectralPowerDistribution
                Aligned tri-spectral power distribution.

            See Also
            --------
            TriSpectralPowerDistribution.extrapolate,
            TriSpectralPowerDistribution.interpolate

            Examples
            --------
            >>> x_bar = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 89.76,
            ...     560: 90.28}
            >>> y_bar = {
            ...     510: 90.56,
            ...     520: 87.34,
            ...     530: 45.76,
            ...     540: 23.45,
            ...     550: 15.34,
            ...     560: 10.11}
            >>> z_bar = {
            ...     510: 12.43,
            ...     520: 23.15,
            ...     530: 67.98,
            ...     540: 90.28,
            ...     550: 91.61,
            ...     560: 98.24}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd.align(SpectralShape(505, 565, 1))  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> # Doctests skip for Python 2.x compatibility.
            >>> tri_spd.wavelengths  # doctest: +SKIP
            array([505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517,
                   518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530,
                   531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543,
                   544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556,
                   557, 558, 559, 560, 561, 562, 563, 564, 565])
            >>> tri_spd.values  # doctest: +ELLIPSIS
            array([[ 49.67     ...,  90.56     ...,  12.43     ...],
                   [ 49.67     ...,  90.56     ...,  12.43     ...],
                   [ 49.67     ...,  90.56     ...,  12.43     ...],
                   [ 49.67     ...,  90.56     ...,  12.43     ...],
                   [ 49.67     ...,  90.56     ...,  12.43     ...],
                   [ 49.67     ...,  90.56     ...,  12.43     ...],
                   [ 51.8325938...,  91.2994928...,  12.5377184...],
                   [ 53.9841952...,  91.9502387...,  12.7233193...],
                   [ 56.1205452...,  92.5395463...,  12.9651679...],
                   [ 58.2315395...,  93.0150037...,  13.3123777...],
                   [ 60.3033208...,  93.2716331...,  13.8605136...],
                   [ 62.3203719...,  93.1790455...,  14.7272944...],
                   [ 64.2676077...,  92.6085951...,  16.0282961...],
                   [ 66.1324679...,  91.4605335...,  17.8526544...],
                   [ 67.9070097...,  89.6911649...,  20.2387677...],
                   [ 69.59     ...,  87.34     ...,  23.15     ...],
                   [ 71.1837378...,  84.4868033...,  26.5150469...],
                   [ 72.6800056...,  81.0666018...,  30.3964852...],
                   [ 74.0753483...,  77.0766254...,  34.7958422...],
                   [ 75.3740343...,  72.6153870...,  39.6178858...],
                   [ 76.5856008...,  67.8490714...,  44.7026805...],
                   [ 77.7223995...,  62.9779261...,  49.8576432...],
                   [ 78.7971418...,  58.2026503...,  54.8895997...],
                   [ 79.8204447...,  53.6907852...,  59.6368406...],
                   [ 80.798376 ...,  49.5431036...,  64.0011777...],
                   [ 81.73     ...,  45.76     ...,  67.98     ...],
                   [ 82.6093606...,  42.2678534...,  71.6460893...],
                   [ 83.439232 ...,  39.10608  ...,  74.976976 ...],
                   [ 84.2220071...,  36.3063728...,  77.9450589...],
                   [ 84.956896 ...,  33.85464  ...,  80.552    ...],
                   [ 85.6410156...,  31.7051171...,  82.8203515...],
                   [ 86.27048  ...,  29.79448  ...,  84.785184 ...],
                   [ 86.8414901...,  28.0559565...,  86.4857131...],
                   [ 87.351424 ...,  26.43344  ...,  87.956928 ...],
                   [ 87.7999266...,  24.8956009...,  89.2212178...],
                   [ 88.19     ...,  23.45     ...,  90.28     ...],
                   [ 88.5265036...,  22.1424091...,  91.1039133...],
                   [ 88.8090803...,  20.9945234...,  91.6538035...],
                   [ 89.0393279...,  20.0021787...,  91.9333499...],
                   [ 89.2222817...,  19.1473370...,  91.9858818...],
                   [ 89.3652954...,  18.4028179...,  91.8811002...],
                   [ 89.4769231...,  17.7370306...,  91.7018000...],
                   [ 89.5657996...,  17.1187058...,  91.5305910...],
                   [ 89.6395227...,  16.5216272...,  91.4366204...],
                   [ 89.7035339...,  15.9293635...,  91.4622944...],
                   [ 89.76     ...,  15.34     ...,  91.61     ...],
                   [ 89.8094041...,  14.7659177...,  91.8528616...],
                   [ 89.8578890...,  14.2129190...,  92.2091737...],
                   [ 89.9096307...,  13.6795969...,  92.6929664...],
                   [ 89.9652970...,  13.1613510...,  93.2988377...],
                   [ 90.0232498...,  12.6519811...,  94.0078786...],
                   [ 90.0807467...,  12.1452800...,  94.7935995...],
                   [ 90.1351435...,  11.6366269...,  95.6278555...],
                   [ 90.1850956...,  11.1245805...,  96.4867724...],
                   [ 90.2317606...,  10.6124724...,  97.3566724...],
                   [ 90.28     ...,  10.11     ...,  98.24     ...],
                   [ 90.28     ...,  10.11     ...,  98.24     ...],
                   [ 90.28     ...,  10.11     ...,  98.24     ...],
                   [ 90.28     ...,  10.11     ...,  98.24     ...],
                   [ 90.28     ...,  10.11     ...,  98.24     ...],
                   [ 90.28     ...,  10.11     ...,  98.24     ...]])
            """

            for i in self.__mapping.keys():
                getattr(self, i).align(shape, method, left, right)

            return self

        def zeros(self, shape=SpectralShape()):
            """
            Zeros fills the tri-spectral power distribution: Missing values will be
            replaced with zeros to fit the defined range.

            Parameters
            ----------
            shape : SpectralShape, optional
                Spectral shape used for zeros fill.

            Returns
            -------
            TriSpectralPowerDistribution
                Zeros filled tri-spectral power distribution.

            Examples
            --------
            >>> x_bar = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 89.76,
            ...     560: 90.28}
            >>> y_bar = {
            ...     510: 90.56,
            ...     520: 87.34,
            ...     530: 45.76,
            ...     540: 23.45,
            ...     550: 15.34,
            ...     560: 10.11}
            >>> z_bar = {
            ...     510: 12.43,
            ...     520: 23.15,
            ...     530: 67.98,
            ...     540: 90.28,
            ...     550: 91.61,
            ...     560: 98.24}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd.zeros(SpectralShape(505, 565, 1))  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values
            array([[  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [ 49.67,  90.56,  12.43],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [ 69.59,  87.34,  23.15],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [ 81.73,  45.76,  67.98],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [ 88.19,  23.45,  90.28],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [ 89.76,  15.34,  91.61],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [ 90.28,  10.11,  98.24],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ],
                   [  0.  ,   0.  ,   0.  ]])
            """

            for i in self.__mapping.keys():
                getattr(self, i).zeros(shape)

            return self

        def normalise(self, factor=1):
            """
            Normalises the tri-spectral power distribution with given normalization
            factor.

            Parameters
            ----------
            factor : numeric, optional
                Normalization factor

            Returns
            -------
            TriSpectralPowerDistribution
                Normalised tri- spectral power distribution.

            Notes
            -----
            -   The implementation uses the maximum value for all axis.

            Examples
            --------
            >>> x_bar = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 89.76,
            ...     560: 90.28}
            >>> y_bar = {
            ...     510: 90.56,
            ...     520: 87.34,
            ...     530: 45.76,
            ...     540: 23.45,
            ...     550: 15.34,
            ...     560: 10.11}
            >>> z_bar = {
            ...     510: 12.43,
            ...     520: 23.15,
            ...     530: 67.98,
            ...     540: 90.28,
            ...     550: 91.61,
            ...     560: 98.24}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> tri_spd.normalise()  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd.values  # doctest: +ELLIPSIS
            array([[ 0.5055985...,  0.9218241...,  0.1265268...],
                   [ 0.7083672...,  0.8890472...,  0.2356473...],
                   [ 0.8319421...,  0.4657980...,  0.6919788...],
                   [ 0.8976995...,  0.2387011...,  0.9189739...],
                   [ 0.9136807...,  0.1561482...,  0.9325122...],
                   [ 0.9189739...,  0.1029112...,  1.       ...]])
            """

            maximum = max(np.ravel(self.values))
            for i in self.__mapping.keys():
                getattr(self, i) * (1 / maximum) * factor

            return self

        def clone(self):
            """
            Clones the tri-spectral power distribution.

            Most of the :class:`TriSpectralPowerDistribution` class operations are
            conducted in-place. The :meth:`TriSpectralPowerDistribution.clone`
            method provides a convenient way to copy the tri-spectral power
            distribution to a new object.

            Returns
            -------
            TriSpectralPowerDistribution
                Cloned tri-spectral power distribution.

            Examples
            --------
            >>> x_bar = {
            ...     510: 49.67,
            ...     520: 69.59,
            ...     530: 81.73,
            ...     540: 88.19,
            ...     550: 89.76,
            ...     560: 90.28}
            >>> y_bar = {
            ...     510: 90.56,
            ...     520: 87.34,
            ...     530: 45.76,
            ...     540: 23.45,
            ...     550: 15.34,
            ...     560: 10.11}
            >>> z_bar = {
            ...     510: 12.43,
            ...     520: 23.15,
            ...     530: 67.98,
            ...     540: 90.28,
            ...     550: 91.61,
            ...     560: 98.24}
            >>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
            >>> mpg = lbl = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
            >>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mpg, lbl)
            >>> print(tri_spd)  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            >>> tri_spd_clone = tri_spd.clone()
            >>> print(tri_spd_clone)  # doctest: +ELLIPSIS
            <...TriSpectralPowerDistribution object at 0x...>
            """

            return copy.deepcopy(self)

    DEFAULT_SPECTRAL_SHAPE = SpectralShape(360, 830, 1)
    """
    Default spectral shape using the shape of
    *CIE 1931 2 Degree Standard Observer*.

    DEFAULT_SPECTRAL_SHAPE : SpectralShape
    """

    def constant_spd(k,
                     shape=DEFAULT_SPECTRAL_SHAPE):
        """
        Returns a spectral power distribution of given spectral shape filled with
        constant :math:`k` values.

        Parameters
        ----------
        k : numeric
            Constant :math:`k` to fill the spectral power distribution with.
        shape : SpectralShape, optional
            Spectral shape used to create the spectral power distribution.

        Returns
        -------
        SpectralPowerDistribution
            Constant :math:`k` to filled spectral power distribution.

        Notes
        -----
        -   By default, the spectral power distribution will use the shape given
            by :attr:`DEFAULT_SPECTRAL_SHAPE` attribute.

        Examples
        --------
        >>> spd = constant_spd(100)
        >>> spd.shape
        SpectralShape(360, 830, 1)
        >>> spd[400]
        100.0
        """

        wavelengths = shape.range()
        values = np.full(len(wavelengths), k)

        name = '{0} Constant'.format(k)
        return SpectralPowerDistribution(name, dict(zip(wavelengths, values)))

    def zeros_spd(shape=DEFAULT_SPECTRAL_SHAPE):
        """
        Returns a spectral power distribution of given spectral shape filled with
        zeros.

        Parameters
        ----------
        shape : SpectralShape, optional
            Spectral shape used to create the spectral power distribution.

        Returns
        -------
        SpectralPowerDistribution
            Zeros filled spectral power distribution.

        See Also
        --------
        constant_spd

        Notes
        -----
        -   By default, the spectral power distribution will use the shape given
            by :attr:`DEFAULT_SPECTRAL_SHAPE` attribute.

        Examples
        --------
        >>> spd = zeros_spd()
        >>> spd.shape
        SpectralShape(360, 830, 1)
        >>> spd[400]
        0.0
        """

        return constant_spd(0, shape)

    def ones_spd(shape=DEFAULT_SPECTRAL_SHAPE):
        """
        Returns a spectral power distribution of given spectral shape filled with
        ones.

        Parameters
        ----------
        shape : SpectralShape, optional
            Spectral shape used to create the spectral power distribution.

        Returns
        -------
        SpectralPowerDistribution
            Ones filled spectral power distribution.

        See Also
        --------
        constant_spd

        Notes
        -----
        -   By default, the spectral power distribution will use the shape given
            by :attr:`DEFAULT_SPECTRAL_SHAPE` attribute.

        Examples
        --------
        >>> spd = ones_spd()
        >>> spd.shape
        SpectralShape(360, 830, 1)
        >>> spd[400]
        1.0
        """

        return constant_spd(1, shape)