Coverage for colour/algebra/extrapolation.py: 100%
79 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2Extrapolation
3=============
5Define classes for extrapolating one-dimensional functions beyond their
6original domain.
8- :class:`colour.Extrapolator`: Extrapolate 1-D functions using various
9 methods to extend function values beyond the original interpolation range.
11References
12----------
13- :cite:`Sastanina` : sastanin. (n.d.). How to make scipy.interpolate give an
14 extrapolated result beyond the input range? Retrieved August 8, 2014, from
15 http://stackoverflow.com/a/2745496/931625
16- :cite:`Westland2012i` : Westland, S., Ripamonti, C., & Cheung, V. (2012).
17 Extrapolation Methods. In Computational Colour Science Using MATLAB (2nd
18 ed., p. 38). ISBN:978-0-470-66569-5
19"""
21from __future__ import annotations
23import typing
25import numpy as np
27from colour.algebra import NullInterpolator, sdiv, sdiv_mode
28from colour.constants import DTYPE_FLOAT_DEFAULT
30if typing.TYPE_CHECKING:
31 from colour.hints import (
32 Any,
33 ArrayLike,
34 DTypeReal,
35 Literal,
36 NDArrayFloat,
37 ProtocolInterpolator,
38 Real,
39 Type,
40 )
42from colour.utilities import (
43 as_float,
44 as_float_array,
45 attest,
46 is_numeric,
47 optional,
48 validate_method,
49)
51__author__ = "Colour Developers"
52__copyright__ = "Copyright 2013 Colour Developers"
53__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
54__maintainer__ = "Colour Developers"
55__email__ = "colour-developers@colour-science.org"
56__status__ = "Production"
58__all__ = [
59 "Extrapolator",
60]
63class Extrapolator:
64 """
65 Extrapolate 1-D function values beyond the specified interpolator's
66 domain boundaries.
68 The :class:`colour.Extrapolator` class wraps a specified *Colour* or
69 *scipy* interpolator instance with compatible signature to provide
70 controlled extrapolation behaviour. Two extrapolation methods are
71 supported:
73 - *Linear*: Extrapolate values linearly using the slope defined by
74 boundary points (xi[0], xi[1]) for x < xi[0] and (xi[-1], xi[-2])
75 for x > xi[-1].
76 - *Constant*: Assign boundary values xi[0] for x < xi[0] and xi[-1]
77 for x > xi[-1].
79 Specifying *left* and *right* arguments overrides the chosen
80 extrapolation method, assigning these values to points outside the
81 interpolator's domain.
83 Parameters
84 ----------
85 interpolator
86 Interpolator object.
87 method
88 Extrapolation method.
89 left
90 Value to return for x < xi[0].
91 right
92 Value to return for x > xi[-1].
93 dtype
94 Data type used for internal conversions.
96 Methods
97 -------
98 - :meth:`~colour.Extrapolator.__init__`
99 - :meth:`~colour.Extrapolator.__class__`
101 Notes
102 -----
103 - The interpolator must define ``x`` and ``y`` properties.
105 References
106 ----------
107 :cite:`Sastanina`, :cite:`Westland2012i`
109 Examples
110 --------
111 Extrapolating a single numeric variable:
113 >>> from colour.algebra import LinearInterpolator
114 >>> x = np.array([3, 4, 5])
115 >>> y = np.array([1, 2, 3])
116 >>> interpolator = LinearInterpolator(x, y)
117 >>> extrapolator = Extrapolator(interpolator)
118 >>> extrapolator(1)
119 -1.0
121 Extrapolating an `ArrayLike` variable:
123 >>> extrapolator(np.array([6, 7, 8]))
124 array([ 4., 5., 6.])
126 Using the *Constant* extrapolation method:
128 >>> x = np.array([3, 4, 5])
129 >>> y = np.array([1, 2, 3])
130 >>> interpolator = LinearInterpolator(x, y)
131 >>> extrapolator = Extrapolator(interpolator, method="Constant")
132 >>> extrapolator(np.array([0.1, 0.2, 8, 9]))
133 array([ 1., 1., 3., 3.])
135 Using defined *left* boundary and *Constant* extrapolation method:
137 >>> x = np.array([3, 4, 5])
138 >>> y = np.array([1, 2, 3])
139 >>> interpolator = LinearInterpolator(x, y)
140 >>> extrapolator = Extrapolator(interpolator, method="Constant", left=0)
141 >>> extrapolator(np.array([0.1, 0.2, 8, 9]))
142 array([ 0., 0., 3., 3.])
143 """
145 def __init__(
146 self,
147 interpolator: ProtocolInterpolator | None = None,
148 method: Literal["Linear", "Constant"] | str = "Linear",
149 left: Real | None = None,
150 right: Real | None = None,
151 dtype: Type[DTypeReal] | None = None,
152 *args: Any, # noqa: ARG002
153 **kwargs: Any, # noqa: ARG002
154 ) -> None:
155 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
157 self._interpolator: ProtocolInterpolator = NullInterpolator(
158 np.array([-np.inf, np.inf]), np.array([-np.inf, np.inf])
159 )
160 self.interpolator = optional(interpolator, self._interpolator)
161 self._method: Literal["Linear", "Constant"] | str = "Linear"
162 self.method = optional(method, self._method)
163 self._right: Real | None = None
164 self.right = right
165 self._left: Real | None = None
166 self.left = left
168 self._dtype: Type[DTypeReal] = dtype
170 @property
171 def interpolator(self) -> ProtocolInterpolator:
172 """
173 Getter and setter for the interpolator.
175 The interpolator must implement the interpolator protocol with an
176 `x` attribute containing the independent variable data.
178 Parameters
179 ----------
180 value
181 Value to set the interpolator instance implementing the required
182 protocol with an `x` attribute for wavelength or frequency values
183 with.
185 Returns
186 -------
187 ProtocolInterpolator
188 Interpolator instance implementing the required protocol with
189 an `x` attribute for wavelength or frequency values.
190 """
192 return self._interpolator
194 @interpolator.setter
195 def interpolator(self, value: ProtocolInterpolator) -> None:
196 """Setter for the **self.interpolator** property."""
198 attest(
199 hasattr(value, "x"),
200 f'"{value}" interpolator has no "x" attribute!',
201 )
203 attest(
204 hasattr(value, "y"),
205 f'"{value}" interpolator has no "y" attribute!',
206 )
208 self._interpolator = value
210 @property
211 def method(self) -> Literal["Linear", "Constant"] | str:
212 """
213 Getter and setter for the extrapolation method for the interpolator.
215 This property controls the behaviour of the interpolator when
216 extrapolating values outside the interpolation domain. The method
217 determines how values are computed beyond the specified boundaries.
219 Parameters
220 ----------
221 value
222 Value to set the extrapolation method to use, either ``'Linear'``
223 for linear extrapolation or ``'Constant'`` for constant value
224 extrapolation at the boundaries.
226 Returns
227 -------
228 :class:`str`
229 Extrapolation method to use.
230 """
232 return self._method
234 @method.setter
235 def method(self, value: Literal["Linear", "Constant"] | str) -> None:
236 """Setter for the **self.method** property."""
238 attest(
239 isinstance(value, str),
240 f'"method" property: "{value}" type is not "str"!',
241 )
243 value = validate_method(value, ("Linear", "Constant"))
245 self._method = value
247 @property
248 def left(self) -> Real | None:
249 """
250 Getter and setter for the left boundary value.
252 Specifies the value to return when evaluating the interpolant at
253 points beyond the leftmost data point ( x < xi[0]).
255 Parameters
256 ----------
257 value
258 Value to return for x < xi[0] for extrapolation beyond the
259 leftmost data point.
261 Returns
262 -------
263 Real or :py:data:`None`
264 Value to return for x < xi[0] for extrapolation beyond the
265 leftmost data point.
266 """
268 return self._left
270 @left.setter
271 def left(self, value: Real | None) -> None:
272 """Setter for the **self.left** property."""
274 if value is not None:
275 attest(
276 is_numeric(value),
277 f'"left" property: "{value}" is not a "number"!',
278 )
280 self._left = value
282 @property
283 def right(self) -> Real | None:
284 """
285 Getter and setter for the right boundary value.
287 Specifies the value to return when evaluating the interpolant at
288 points beyond the rightmost data point (x > xi[-1]).
290 Parameters
291 ----------
292 value
293 Value to return for x > xi[-1] for extrapolation beyond the
294 rightmost data point.
296 Returns
297 -------
298 :class:`numbers.Real` or :py:data:`None`
299 Value to return for x > xi[-1] for extrapolation beyond the
300 rightmost data point.
301 """
303 return self._right
305 @right.setter
306 def right(self, value: Real | None) -> None:
307 """Setter for the **self.right** property."""
309 if value is not None:
310 attest(
311 is_numeric(value),
312 f'"right" property: "{value}" is not a "number"!',
313 )
315 self._right = value
317 def __call__(self, x: ArrayLike) -> NDArrayFloat:
318 """
319 Evaluate the extrapolator at specified point(s).
321 Parameters
322 ----------
323 x
324 Point(s) to evaluate the extrapolator at.
326 Returns
327 -------
328 :class:`numpy.ndarray`
329 Extrapolated point value(s).
330 """
332 x = as_float_array(x)
334 xe = self._evaluate(x)
336 return as_float(xe)
338 def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat:
339 """
340 Perform the extrapolating evaluation at specified points.
342 Parameters
343 ----------
344 x
345 Points to evaluate the extrapolator at.
347 Returns
348 -------
349 :class:`numpy.ndarray`
350 Extrapolated point values.
351 """
353 xi = self._interpolator.x
354 yi = self._interpolator.y
356 y = np.empty_like(x)
358 if self._method == "linear":
359 with sdiv_mode():
360 y[x < xi[0]] = yi[0] + (x[x < xi[0]] - xi[0]) * sdiv(
361 yi[1] - yi[0], xi[1] - xi[0]
362 )
363 y[x > xi[-1]] = yi[-1] + (x[x > xi[-1]] - xi[-1]) * sdiv(
364 yi[-1] - yi[-2], xi[-1] - xi[-2]
365 )
366 elif self._method == "constant":
367 y[x < xi[0]] = yi[0]
368 y[x > xi[-1]] = yi[-1]
370 if self._left is not None:
371 y[x < xi[0]] = self._left
372 if self._right is not None:
373 y[x > xi[-1]] = self._right
375 in_range = np.logical_and(x >= xi[0], x <= xi[-1])
376 y[in_range] = self._interpolator(x[in_range])
378 return y