# 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 numpy as np

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

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013 - 2015 - 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']

[docs]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
[docs]    def start(self, value):
"""
Setter for **self.__start** private attribute.

Parameters
----------
value : 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
[docs]    def end(self, value):
"""
Setter for **self.__end** private attribute.

Parameters
----------
value : 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
[docs]    def steps(self, value):
"""
Setter for **self.__steps** private attribute.

Parameters
----------
value : 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

[docs]    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)

[docs]    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)

[docs]    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.0
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
10.0
"""

return iter(self.range())

[docs]    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()

[docs]    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())

[docs]    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())

[docs]    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)

[docs]    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:
samples = round(
(self.__steps + self.__end - self.__start) / self.__steps)
self.__range, current_steps = np.linspace(self.__start,
self.__end,
samples,
retstep=True)

if current_steps != self.__steps:
self.__steps = current_steps
warning(('"{0}" shape could not be honored, using '
'"{1}"!').format((self.__start,
self.__end,
self.__steps),
self))
return self.__range

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

Parameters
----------
name : 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}}
title : unicode, optional
Spectral power distribution title for figures.

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

Methods
-------
__getitem__
__init__
__setitem__
__iter__
__contains__
__len__
__eq__
__ne__
__add__
__sub__
__mul__
__div__
__truediv__
__pow__
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)
"""

[docs]    def __init__(self, name, data, title=None):
self.__name = None
self.name = name
self.__data = None
self.data = data
self.__title = None
self.title = title

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

Returns
-------
unicode
self.__name.
"""

return self.__name

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

Parameters
----------
value : 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
[docs]    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 title(self):
"""
Property for **self.__title** private attribute.

Returns
-------
unicode
self.__title.
"""

if self.__title is not None:
return self.__title
else:
return self.__name

@title.setter
[docs]    def title(self, value):
"""
Setter for **self.__title** private attribute.

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

if value is not None:
assert type(value) in (str, unicode), (
('"{0}" attribute: "{1}" type is not '
'"str" or "unicode"!').format('title', value))
self.__title = 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
[docs]    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
[docs]    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
[docs]    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
[docs]    def shape(self, value):
"""
Setter for **self.shape** attribute.

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

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

[docs]    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]  Hettinger, R. (n.d.). Python hashable dicts. Retrieved August
08, 2014, from http://stackoverflow.com/a/16162138/931625
"""

return hash(frozenset(self.__data))

[docs]    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)

[docs]    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)

[docs]    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()))

[docs]    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

[docs]    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)

[docs]    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

[docs]    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 = as_array(x)

return x

[docs]    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

[docs]    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))

[docs]    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

[docs]    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__

[docs]    def __pow__(self, x):
"""
Implements support for spectral power distribution exponentiation.

Parameters
----------
x : numeric or array_like or SpectralPowerDistribution
Variable to exponentiate by.

Returns
-------
SpectralPowerDistribution
Spectral power distribution raised by power of x.

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

Notes
-----
-   Reimplements the :meth:object.__pow__ method.

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

Examples
--------
Exponentiation by a single *numeric* variable:

>>> data = {510: 1.67, 520: 2.59, 530: 3.73, 540: 4.19}
>>> spd = SpectralPowerDistribution('Spd', data)
>>> spd ** 2  # doctest: +ELLIPSIS
<...SpectralPowerDistribution object at 0x...>
>>> spd.values
array([  2.7889,   6.7081,  13.9129,  17.5561])

Exponentiation by an *array_like* variable:

>>> spd ** [1, 2, 3, 4]  # doctest: +ELLIPSIS
<...SpectralPowerDistribution object at 0x...>
>>> spd.values  # doctest: +ELLIPSIS
array([  2.7889000...e+00,   4.4998605...e+01,   2.6931031...e+03,
9.4997501...e+04])

Exponentiation by a :class:SpectralPowerDistribution class variable:

>>> spd_alternate = SpectralPowerDistribution('Spd', data)
>>> spd ** spd_alternate  # doctest: +ELLIPSIS
<...SpectralPowerDistribution object at 0x...>
>>> spd.values  # doctest: +ELLIPSIS
array([  5.5446356...e+00,   1.9133109...e+04,   6.2351033...e+12,
7.1880990...e+20])
"""

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

return self

[docs]    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

[docs]    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)

[docs]    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
{'Constant', 'Linear'},
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 TC 1-48. (2004). Extrapolation. In CIE 015:2004
Colorimetry, 3rd Edition (p. 24). ISBN:978-3-901-90633-6
.. [3]  CIE TC 1-38. (2005). EXTRAPOLATION. In CIE 167:2005
Recommended Practice for Tabulating Spectral Data for Use in
Colour Computations (pp. 19â€“20). ISBN:978-3-901-90641-1

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

[docs]    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
{None, 'Sprague', 'Cubic Spline', 'Linear'},
Enforce given interpolation method.

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

Raises
------
RuntimeError
If Sprague (1880) 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 (1880) 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 (1880) interpolator requires at least 6 wavelengths
:math:\lambda_n for interpolation.

References
----------
.. [4]  CIE TC 1-38. (2005). 9. INTERPOLATION. In CIE 167:2005
Recommended Practice for Tabulating Spectral Data for Use in
Colour Computations (pp. 14â€“19). ISBN:978-3-901-90641-1

Examples
--------
Uniform data is using Sprague (1880) 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, np.ceil(spd_shape.start))
shape.end = min(shape.end, np.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

[docs]    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
{'Constant', 'Linear'},
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

[docs]    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

[docs]    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

[docs]    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)

[docs]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 : unicode
Tri-spectral power distribution name.
data : dict
Tri-spectral power distribution data.
mapping : dict
Tri-spectral power distribution attributes mapping.
title : unicode, optional
Tri-spectral power distribution title for figures.
labels : dict, optional
Tri-spectral power distribution axis labels mapping for figures.

Attributes
----------
name
mapping
data
title
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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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)
"""

[docs]    def __init__(self, name, data, mapping, title=None, labels=None):
self.__name = None
self.name = name
self.__mapping = mapping
self.__data = None
self.data = data
self.__title = None
self.title = title
self.__labels = None
self.labels = labels

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

Returns
-------
unicode
self.__name.
"""

return self.__name

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

Parameters
----------
value : 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
[docs]    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
[docs]    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 title(self):
"""
Property for **self.__title** private attribute.

Returns
-------
unicode
self.__title.
"""

if self.__title is not None:
return self.__title
else:
return self.__name

@title.setter
[docs]    def title(self, value):
"""
Setter for **self.__title** private attribute.

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

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

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

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

if self.__labels is not None:
return self.__labels
else:
return self.__mapping

@labels.setter
[docs]    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
[docs]    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
[docs]    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
[docs]    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
[docs]    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
[docs]    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
[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> tri_spd.shape
SpectralShape(510, 540, 10)
"""

return self.x.shape

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

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

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

[docs]    def __hash__(self):
"""
Returns the spectral power distribution hash value. [1]_

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

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

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

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

[docs]    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}
>>> mapping  = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> tri_spd[510]
array([ 49.67,  90.56,  12.43])
"""

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

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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)

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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)

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 510 in tri_spd
True

"""

return wavelength in self.x

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> len(tri_spd)
4
"""

return len(self.x)

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd1 = TriSpectralPowerDistribution('Tri Spd', data1, mapping)
>>> tri_spd2 = TriSpectralPowerDistribution('Tri Spd', data2, mapping)
>>> tri_spd3 = TriSpectralPowerDistribution('Tri Spd', data1, mapping)
>>> 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)

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd1 = TriSpectralPowerDistribution('Tri Spd', data1, mapping)
>>> tri_spd2 = TriSpectralPowerDistribution('Tri Spd', data2, mapping)
>>> tri_spd3 = TriSpectralPowerDistribution('Tri Spd', data1, mapping)
>>> 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 = as_array(x)

return x

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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, mapping)
>>> 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

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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, mapping)
>>> 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))

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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, mapping)
>>> 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

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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, mapping)
>>> 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__

[docs]    def __pow__(self, x):
"""
Implements support for tri-spectral power distribution exponentiation.

Parameters
----------
x : numeric or array_like or TriSpectralPowerDistribution
Variable to exponentiate by.

Returns
-------
TriSpectralPowerDistribution
TriSpectral power distribution raised by power of x.

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

Notes
-----
-   Reimplements the :meth:object.__pow__ method.

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

Examples
--------
Exponentiation by a single *numeric* variable:

>>> x_bar = {510: 1.67, 520: 1.59, 530: 1.73, 540: 1.19}
>>> y_bar = {510: 1.56, 520: 1.34, 530: 1.76, 540: 1.45}
>>> z_bar = {510: 1.43, 520: 1.15, 530: 1.98, 540: 1.28}
>>> data = {'x_bar': x_bar, 'y_bar': y_bar, 'z_bar': z_bar}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> tri_spd ** 1.1  # doctest: +ELLIPSIS
<...TriSpectralPowerDistribution object at 0x...>
>>> tri_spd.values  # doctest: +ELLIPSIS
array([[ 1.7578755...,  1.6309365...,  1.4820731...],
[ 1.6654700...,  1.3797972...,  1.1661854...],
[ 1.8274719...,  1.8623612...,  2.1199797...],
[ 1.2108815...,  1.5048901...,  1.3119913...]])

Exponentiation by an *array_like* variable:

>>> tri_spd ** ([(1, 2, 3)] * 4)  # doctest: +ELLIPSIS
<...TriSpectralPowerDistribution object at 0x...>
>>> tri_spd.values  # doctest: +ELLIPSIS
array([[ 1.7578755...,  2.6599539...,  3.2554342...],
[ 1.6654700...,  1.9038404...,  1.5859988...],
[ 1.8274719...,  3.4683895...,  9.5278547...],
[ 1.2108815...,  2.2646943...,  2.2583585...]])

Exponentiation by a :class:TriSpectralPowerDistribution class variable:

>>> data1 = {'x_bar': z_bar, 'y_bar': x_bar, 'z_bar': y_bar}
>>> tri_spd1 = TriSpectralPowerDistribution('Tri Spd', data1, mapping)
>>> tri_spd ** tri_spd1  # doctest: +ELLIPSIS
<...TriSpectralPowerDistribution object at 0x...>
>>> tri_spd.values  # doctest: +ELLIPSIS
array([[  2.2404384...,   5.1231818...,   6.3047797...],
[  1.7979075...,   2.7836369...,   1.8552645...],
[  3.2996236...,   8.5984706...,  52.8483490...],
[  1.2775271...,   2.6452177...,   3.2583647...]])
"""

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

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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

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

Parameters
----------
shape : SpectralShape
Spectral shape used for extrapolation.
method : unicode, optional
{'Constant', 'Linear'},
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

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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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

[docs]    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. [4]_

Parameters
----------
shape : SpectralShape, optional
Spectral shape used for interpolation.
method : unicode, optional
{None, '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.

Examples
--------
Uniform data is using Sprague (1880) 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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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

[docs]    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
{'Constant', 'Linear'},
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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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

[docs]    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}
>>> mapping = {'x': 'x_bar', 'y': 'y_bar', 'z': 'z_bar'}
>>> tri_spd = TriSpectralPowerDistribution('Tri Spd', data, mapping)
>>> 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
"""

[docs]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.0, 830.0, 1.0)
>>> 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)))

[docs]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.0, 830.0, 1.0)
>>> spd[400]
0.0
"""

return constant_spd(0, shape)

[docs]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.0, 830.0, 1.0)
>>> spd[400]
1.0
"""

return constant_spd(1, shape)