Source code for colour.algebra.extrapolation
"""
Extrapolation
=============
Defines the classes for extrapolating variables:
- :class:`colour.Extrapolator`: 1-D function extrapolation.
References
----------
- :cite:`Sastanina` : sastanin. (n.d.). How to make scipy.interpolate give an
extrapolated result beyond the input range? Retrieved August 8, 2014, from
http://stackoverflow.com/a/2745496/931625
- :cite:`Westland2012i` : Westland, S., Ripamonti, C., & Cheung, V. (2012).
Extrapolation Methods. In Computational Colour Science Using MATLAB (2nd
ed., p. 38). ISBN:978-0-470-66569-5
"""
from __future__ import annotations
import numpy as np
from colour.algebra import NullInterpolator
from colour.constants import DEFAULT_FLOAT_DTYPE
from colour.hints import (
DTypeNumber,
FloatingOrArrayLike,
FloatingOrNDArray,
Literal,
NDArray,
Number,
Optional,
Type,
TypeInterpolator,
Union,
cast,
)
from colour.utilities import (
as_float,
attest,
is_numeric,
is_string,
optional,
validate_method,
)
__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "New BSD License - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"Extrapolator",
]
[docs]class Extrapolator:
"""
Extrapolate the 1-D function of given interpolator.
The :class:`colour.Extrapolator` class acts as a wrapper around a given
*Colour* or *scipy* interpolator class instance with compatible signature.
Two extrapolation methods are available:
- *Linear*: Linearly extrapolates given points using the slope defined by
the interpolator boundaries (xi[0], xi[1]) if x < xi[0] and
(xi[-1], xi[-2]) if x > xi[-1].
- *Constant*: Extrapolates given points by assigning the interpolator
boundaries values xi[0] if x < xi[0] and xi[-1] if x > xi[-1].
Specifying the *left* and *right* arguments takes precedence on the chosen
extrapolation method and will assign the respective *left* and *right*
values to the given points.
Parameters
----------
interpolator
Interpolator object.
method
Extrapolation method.
left
Value to return for x < xi[0].
right
Value to return for x > xi[-1].
dtype
Data type used for internal conversions.
Methods
-------
- :meth:`~colour.Extrapolator.__init__`
- :meth:`~colour.Extrapolator.__class__`
Notes
-----
- The interpolator must define ``x`` and ``y`` properties.
References
----------
:cite:`Sastanina`, :cite:`Westland2012i`
Examples
--------
Extrapolating a single numeric variable:
>>> from colour.algebra import LinearInterpolator
>>> x = np.array([3, 4, 5])
>>> y = np.array([1, 2, 3])
>>> interpolator = LinearInterpolator(x, y)
>>> extrapolator = Extrapolator(interpolator)
>>> extrapolator(1)
-1.0
Extrapolating an `ArrayLike` variable:
>>> extrapolator(np.array([6, 7 , 8]))
array([ 4., 5., 6.])
Using the *Constant* extrapolation method:
>>> x = np.array([3, 4, 5])
>>> y = np.array([1, 2, 3])
>>> interpolator = LinearInterpolator(x, y)
>>> extrapolator = Extrapolator(interpolator, method='Constant')
>>> extrapolator(np.array([0.1, 0.2, 8, 9]))
array([ 1., 1., 3., 3.])
Using defined *left* boundary and *Constant* extrapolation method:
>>> x = np.array([3, 4, 5])
>>> y = np.array([1, 2, 3])
>>> interpolator = LinearInterpolator(x, y)
>>> extrapolator = Extrapolator(interpolator, method='Constant', left=0)
>>> extrapolator(np.array([0.1, 0.2, 8, 9]))
array([ 0., 0., 3., 3.])
"""
[docs] def __init__(
self,
interpolator: Optional[TypeInterpolator] = None,
method: Union[Literal["Linear", "Constant"], str] = "Linear",
left: Optional[Number] = None,
right: Optional[Number] = None,
dtype: Optional[Type[DTypeNumber]] = None,
):
dtype = cast(Type[DTypeNumber], optional(dtype, DEFAULT_FLOAT_DTYPE))
self._interpolator: TypeInterpolator = NullInterpolator(
np.array([-np.inf, np.inf]), np.array([-np.inf, np.inf])
)
self.interpolator = optional(interpolator, self._interpolator)
self._method: Union[Literal["Linear", "Constant"], str] = "Linear"
self.method = cast(
Union[Literal["Linear", "Constant"], str],
optional(method, self._method),
)
self._right: Optional[Number] = None
self.right = right
self._left: Optional[Number] = None
self.left = left
self._dtype: Type[DTypeNumber] = dtype
@property
def interpolator(self) -> TypeInterpolator:
"""
Getter and setter property for the *Colour* or *scipy* interpolator
class instance.
Parameters
----------
value
Value to set the *Colour* or *scipy* interpolator class instance
with.
Returns
-------
TypeInterpolator
*Colour* or *scipy* interpolator class instance.
"""
return self._interpolator
@interpolator.setter
def interpolator(self, value: TypeInterpolator):
"""Setter for the **self.interpolator** property."""
attest(
hasattr(value, "x"),
f'"{value}" interpolator has no "x" attribute!',
)
attest(
hasattr(value, "y"),
f'"{value}" interpolator has no "y" attribute!',
)
self._interpolator = value
@property
def method(self) -> Union[Literal["Linear", "Constant"], str]:
"""
Getter and setter property for the extrapolation method.
Parameters
----------
value
Value to set the extrapolation method. with.
Returns
-------
:class:`str`
Extrapolation method.
"""
return self._method
@method.setter
def method(self, value: Union[Literal["Linear", "Constant"], str]):
"""Setter for the **self.method** property."""
attest(
is_string(value),
f'"method" property: "{value}" type is not "str"!',
)
value = validate_method(value, ["Linear", "Constant"])
self._method = value
@property
def left(self) -> Optional[Number]:
"""
Getter and setter property for left value to return for x < xi[0].
Parameters
----------
value
Left value to return for x < xi[0].
Returns
-------
:py:data:`None` or Number
Left value to return for x < xi[0].
"""
return self._left
@left.setter
def left(self, value: Optional[Number]):
"""Setter for the **self.left** property."""
if value is not None:
attest(
is_numeric(value),
f'"left" property: "{value}" is not a "number"!',
)
self._left = value
@property
def right(self) -> Optional[Number]:
"""
Getter and setter property for right value to return for x > xi[-1].
Parameters
----------
value
Right value to return for x > xi[-1].
Returns
-------
:py:data:`None` or Number
Right value to return for x > xi[-1].
"""
return self._right
@right.setter
def right(self, value: Optional[Number]):
"""Setter for the **self.right** property."""
if value is not None:
attest(
is_numeric(value),
f'"right" property: "{value}" is not a "number"!',
)
self._right = value
[docs] def __call__(self, x: FloatingOrArrayLike) -> FloatingOrNDArray:
"""
Evaluate the Extrapolator at given point(s).
Parameters
----------
x
Point(s) to evaluate the Extrapolator at.
Returns
-------
:class:`numpy.floating` or :class:`numpy.ndarray`
Extrapolated points value(s).
"""
x = np.atleast_1d(x).astype(self._dtype)
xe = as_float(self._evaluate(x))
return xe
def _evaluate(self, x: NDArray) -> NDArray:
"""
Perform the extrapolating evaluation at given points.
Parameters
----------
x
Points to evaluate the Extrapolator at.
Returns
-------
:class:`numpy.ndarray`
Extrapolated points values.
"""
xi = self._interpolator.x
yi = self._interpolator.y
y = np.empty_like(x)
if self._method == "linear":
y[x < xi[0]] = yi[0] + (x[x < xi[0]] - xi[0]) * (yi[1] - yi[0]) / (
xi[1] - xi[0]
)
y[x > xi[-1]] = yi[-1] + (x[x > xi[-1]] - xi[-1]) * (
yi[-1] - yi[-2]
) / (xi[-1] - xi[-2])
elif self._method == "constant":
y[x < xi[0]] = yi[0]
y[x > xi[-1]] = yi[-1]
if self._left is not None:
y[x < xi[0]] = self._left
if self._right is not None:
y[x > xi[-1]] = self._right
in_range = np.logical_and(x >= xi[0], x <= xi[-1])
y[in_range] = self._interpolator(x[in_range])
return y