Coverage for colour/algebra/interpolation.py: 100%
415 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
1"""
2Interpolation
3=============
5Provide classes and functions for interpolating variables in colour science
6computations.
8This module implements various interpolation methods for one-dimensional
9functions and multi-dimensional table-based interpolation. These methods
10support spectral data processing, colour transformations, and general
11numerical interpolation tasks in colour science applications.
13- :class:`colour.KernelInterpolator`: 1-D function generic interpolation
14 with arbitrary kernel.
15- :class:`colour.NearestNeighbourInterpolator`: 1-D function
16 nearest-neighbour interpolation.
17- :class:`colour.LinearInterpolator`: 1-D function linear interpolation.
18- :class:`colour.SpragueInterpolator`: 1-D function fifth-order polynomial
19 interpolation using *Sprague (1880)* method.
20- :class:`colour.CubicSplineInterpolator`: 1-D function cubic spline
21 interpolation.
22- :class:`colour.PchipInterpolator`: 1-D function piecewise cube Hermite
23 interpolation.
24- :class:`colour.NullInterpolator`: 1-D function null interpolation.
25- :func:`colour.lagrange_coefficients`: Compute *Lagrange Coefficients*.
26- :func:`colour.algebra.table_interpolation_trilinear`: Perform trilinear
27 interpolation with table.
28- :func:`colour.algebra.table_interpolation_tetrahedral`: Perform
29 tetrahedral interpolation with table.
30- :attr:`colour.TABLE_INTERPOLATION_METHODS`: Supported table interpolation
31 methods.
32- :func:`colour.table_interpolation`: Perform interpolation with table using
33 specified method.
35References
36----------
37- :cite:`Bourkeb` : Bourke, P. (n.d.). Trilinear Interpolation. Retrieved
38 January 13, 2018, from http://paulbourke.net/miscellaneous/interpolation/
39- :cite:`Burger2009b` : Burger, W., & Burge, M. J. (2009). Principles of
40 Digital Image Processing. Springer London. doi:10.1007/978-1-84800-195-4
41- :cite:`CIETC1-382005f` : CIE TC 1-38. (2005). 9.2.4 Method of
42 interpolation for uniformly spaced independent variable. In CIE 167:2005
43 Recommended Practice for Tabulating Spectral Data for Use in Colour
44 Computations (pp. 1-27). ISBN:978-3-901906-41-1
45- :cite:`CIETC1-382005h` : CIE TC 1-38. (2005). Table V. Values of the
46 c-coefficients of Equ.s 6 and 7. In CIE 167:2005 Recommended Practice for
47 Tabulating Spectral Data for Use in Colour Computations (p. 19).
48 ISBN:978-3-901906-41-1
49- :cite:`Fairman1985b` : Fairman, H. S. (1985). The calculation of weight
50 factors for tristimulus integration. Color Research & Application, 10(4),
51 199-203. doi:10.1002/col.5080100407
52- :cite:`Kirk2006` : Kirk, R. (2006). Truelight Software Library 2.0.
53 Retrieved July 8, 2017, from
54 https://www.filmlight.ltd.uk/pdf/whitepapers/FL-TL-TN-0057-SoftwareLib.pdf
55- :cite:`Westland2012h` : Westland, S., Ripamonti, C., & Cheung, V. (2012).
56 Interpolation Methods. In Computational Colour Science Using MATLAB (2nd
57 ed., pp. 29-37). ISBN:978-0-470-66569-5
58- :cite:`Wikipedia2003a` : Wikipedia. (2003). Lagrange polynomial -
59 Definition. Retrieved January 20, 2016, from
60 https://en.wikipedia.org/wiki/Lagrange_polynomial#Definition
61- :cite:`Wikipedia2005b` : Wikipedia. (2005). Lanczos resampling. Retrieved
62 October 14, 2017, from https://en.wikipedia.org/wiki/Lanczos_resampling
63"""
65from __future__ import annotations
67import sys
68import typing
69from functools import reduce
70from unittest.mock import MagicMock
72import numpy as np
74from colour.utilities.requirements import is_scipy_installed
75from colour.utilities.verbose import usage_warning
77if not is_scipy_installed(): # pragma: no cover
78 try:
79 is_scipy_installed(raise_exception=True)
80 except ImportError as error:
81 usage_warning(str(error))
83 mock = MagicMock()
84 mock.__name__ = ""
86 for module in (
87 "scipy",
88 "scipy.interpolate",
89 ):
90 sys.modules[module] = mock
92import scipy.interpolate
94from colour.algebra import sdiv, sdiv_mode
95from colour.constants import (
96 DTYPE_FLOAT_DEFAULT,
97 DTYPE_INT_DEFAULT,
98 TOLERANCE_ABSOLUTE_DEFAULT,
99 TOLERANCE_RELATIVE_DEFAULT,
100)
102if typing.TYPE_CHECKING:
103 from colour.hints import (
104 Any,
105 ArrayLike,
106 Callable,
107 DTypeReal,
108 Literal,
109 Type,
110 )
112from colour.hints import NDArrayFloat, cast
113from colour.utilities import (
114 CanonicalMapping,
115 as_array,
116 as_float,
117 as_float_array,
118 as_float_scalar,
119 as_int_array,
120 attest,
121 closest_indexes,
122 interval,
123 is_numeric,
124 optional,
125 runtime_warning,
126 validate_method,
127)
129__author__ = "Colour Developers"
130__copyright__ = "Copyright 2013 Colour Developers"
131__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
132__maintainer__ = "Colour Developers"
133__email__ = "colour-developers@colour-science.org"
134__status__ = "Production"
136__all__ = [
137 "kernel_nearest_neighbour",
138 "kernel_linear",
139 "kernel_sinc",
140 "kernel_lanczos",
141 "kernel_cardinal_spline",
142 "KernelInterpolator",
143 "NearestNeighbourInterpolator",
144 "LinearInterpolator",
145 "SpragueInterpolator",
146 "CubicSplineInterpolator",
147 "PchipInterpolator",
148 "NullInterpolator",
149 "lagrange_coefficients",
150 "table_interpolation_trilinear",
151 "table_interpolation_tetrahedral",
152 "TABLE_INTERPOLATION_METHODS",
153 "table_interpolation",
154]
157def kernel_nearest_neighbour(x: ArrayLike) -> NDArrayFloat:
158 """
159 Return the *nearest-neighbour* kernel evaluated at specified samples.
161 The *nearest-neighbour* kernel is a discontinuous kernel function that
162 equals 1 for samples within the range [-0.5, 0.5) and 0 elsewhere. This
163 kernel represents the simplest interpolation method where each output
164 value is determined by the closest input sample.
166 Parameters
167 ----------
168 x
169 Samples at which to evaluate the *nearest-neighbour* kernel.
171 Returns
172 -------
173 :class:`numpy.ndarray`
174 The *nearest-neighbour* kernel evaluated at specified samples.
176 References
177 ----------
178 :cite:`Burger2009b`
180 Examples
181 --------
182 >>> kernel_nearest_neighbour(np.linspace(0, 1, 10))
183 array([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
184 """
186 return np.where(np.abs(x) < 0.5, 1, 0)
189def kernel_linear(x: ArrayLike) -> NDArrayFloat:
190 """
191 Evaluate the *linear* kernel at specified samples.
193 The *linear* kernel is a triangular function that returns 1 when
194 :math:`|x| < 0.5` and 0 otherwise, providing a simple binary response
195 based on the absolute value of the input.
197 Parameters
198 ----------
199 x
200 Samples at which to evaluate the *linear* kernel.
202 Returns
203 -------
204 :class:`numpy.ndarray`
205 The *linear* kernel evaluated at specified samples, with values of 1
206 for :math:`|x| < 0.5` and 0 otherwise.
208 References
209 ----------
210 :cite:`Burger2009b`
212 Examples
213 --------
214 >>> kernel_linear(np.linspace(0, 1, 10)) # doctest: +ELLIPSIS
215 array([ 1. , 0.8888888..., 0.7777777..., \
2160.6666666..., 0.5555555...,
217 0.4444444..., 0.3333333..., 0.2222222..., \
2180.1111111..., 0. ])
219 """
221 return np.where(np.abs(x) < 1, 1 - np.abs(x), 0)
224def kernel_sinc(x: ArrayLike, a: float = 3) -> NDArrayFloat:
225 """
226 Evaluate the *sinc* kernel at specified sample positions.
228 Compute the *sinc* kernel function, commonly used in signal processing
229 and interpolation applications, for the specified sample positions.
231 Parameters
232 ----------
233 x
234 Sample positions at which to evaluate the *sinc* kernel.
235 a
236 Size parameter of the *sinc* kernel, controlling the function's
237 support width.
239 Returns
240 -------
241 :class:`numpy.ndarray`
242 *Sinc* kernel values evaluated at the specified sample positions.
244 Raises
245 ------
246 AssertionError
247 If ``a`` is less than 1.
249 References
250 ----------
251 :cite:`Burger2009b`
253 Examples
254 --------
255 >>> kernel_sinc(np.linspace(0, 1, 10)) # doctest: +ELLIPSIS
256 array([ 1.0000000...e+00, 9.7981553...e-01, 9.2072542...e-01,
257 8.2699334...e-01, 7.0531659...e-01, 5.6425327...e-01,
258 4.1349667...e-01, 2.6306440...e-01, 1.2247694...e-01,
259 3.8981718...e-17])
260 """
262 x = as_float_array(x)
264 attest(bool(a >= 1), '"a" must be equal or superior to 1!')
266 return np.where(np.abs(x) < a, np.sinc(x), 0)
269def kernel_lanczos(x: ArrayLike, a: float = 3) -> NDArrayFloat:
270 """
271 Return the *Lanczos* kernel evaluated at specified samples.
273 The *Lanczos* kernel is a sinc-based windowing function commonly used
274 in signal processing and image resampling applications. It is defined
275 as :math:`L(x) = \\text{sinc}(x) \\cdot \\text{sinc}(x/a)` for
276 :math:`|x| < a`, and zero otherwise.
278 Parameters
279 ----------
280 x
281 Samples at which to evaluate the *Lanczos* kernel.
282 a
283 Size of the *Lanczos* kernel, defining the support region
284 :math:`[-a, a]`.
286 Returns
287 -------
288 :class:`numpy.ndarray`
289 The *Lanczos* kernel evaluated at specified samples.
291 References
292 ----------
293 :cite:`Wikipedia2005b`
295 Examples
296 --------
297 >>> kernel_lanczos(np.linspace(0, 1, 10)) # doctest: +ELLIPSIS
298 array([ 1.0000000...e+00, 9.7760615...e-01, 9.1243770...e-01,
299 8.1030092...e-01, 6.8012706...e-01, 5.3295773...e-01,
300 3.8071690...e-01, 2.3492839...e-01, 1.0554054...e-01,
301 3.2237621...e-17])
302 """
304 x = as_float_array(x)
306 attest(bool(a >= 1), '"a" must be equal or superior to 1!')
308 return np.where(np.abs(x) < a, np.sinc(x) * np.sinc(x / a), 0)
311def kernel_cardinal_spline(
312 x: ArrayLike, a: float = 0.5, b: float = 0.0
313) -> NDArrayFloat:
314 """
315 Return the *cardinal spline* kernel evaluated at specified samples.
317 Notable *cardinal spline* :math:`a` and :math:`b` parameterizations:
319 - *Catmull-Rom*: :math:`(a=0.5, b=0)`
320 - *Cubic B-Spline*: :math:`(a=0, b=1)`
321 - *Mitchell-Netravalli*:
322 :math:`(a=\\cfrac{1}{3}, b=\\cfrac{1}{3})`
324 Parameters
325 ----------
326 x
327 Samples at which to evaluate the *cardinal spline* kernel.
328 a
329 :math:`a` control parameter.
330 b
331 :math:`b` control parameter.
333 Returns
334 -------
335 :class:`numpy.ndarray`
336 The *cardinal spline* kernel evaluated at specified samples.
338 References
339 ----------
340 :cite:`Burger2009b`
342 Examples
343 --------
344 >>> kernel_cardinal_spline(np.linspace(0, 1, 10)) # doctest: +ELLIPSIS
345 array([ 1. , 0.9711934..., 0.8930041..., \
3460.7777777..., 0.6378600...,
347 0.4855967..., 0.3333333..., 0.1934156..., \
3480.0781893..., 0. ])
349 """
351 x = as_float_array(x)
353 x_abs = np.abs(x)
354 y = np.where(
355 x_abs < 1,
356 (-6 * a - 9 * b + 12) * x_abs**3 + (6 * a + 12 * b - 18) * x_abs**2 - 2 * b + 6,
357 (-6 * a - b) * x_abs**3
358 + (30 * a + 6 * b) * x_abs**2
359 + (-48 * a - 12 * b) * x_abs
360 + 24 * a
361 + 8 * b,
362 )
363 y[x_abs >= 2] = 0
365 return 1 / 6 * y
368class KernelInterpolator:
369 """
370 Perform kernel-based interpolation of a 1-D function.
372 Reconstruct a continuous signal from discrete samples using linear
373 convolution. Express interpolation as the convolution of the specified
374 discrete function :math:`g(x)` with a continuous interpolation kernel
375 :math:`k(w)`:
377 :math:`\\hat{g}(w_0) = [k * g](w_0) = \
378\\sum_{x=-\\infty}^{\\infty}k(w_0 - x)\\cdot g(x)`
380 Parameters
381 ----------
382 x
383 Independent :math:`x` variable values corresponding with :math:`y`
384 variable.
385 y
386 Dependent and already known :math:`y` variable values to interpolate.
387 window
388 Width of the window in samples on each side.
389 kernel
390 Kernel to use for interpolation.
391 kernel_kwargs
392 Arguments to use when calling the kernel.
393 padding_kwargs
394 Arguments to use when padding :math:`y` variable values with the
395 :func:`np.pad` definition.
396 dtype
397 Data type used for internal conversions.
399 Attributes
400 ----------
401 - :attr:`~colour.KernelInterpolator.x`
402 - :attr:`~colour.KernelInterpolator.y`
403 - :attr:`~colour.KernelInterpolator.window`
404 - :attr:`~colour.KernelInterpolator.kernel`
405 - :attr:`~colour.KernelInterpolator.kernel_kwargs`
406 - :attr:`~colour.KernelInterpolator.padding_kwargs`
408 Methods
409 -------
410 - :meth:`~colour.KernelInterpolator.__init__`
411 - :meth:`~colour.KernelInterpolator.__call__`
413 References
414 ----------
415 :cite:`Burger2009b`, :cite:`Wikipedia2005b`
417 Examples
418 --------
419 Interpolating a single numeric variable:
421 >>> y = np.array(
422 ... [5.9200, 9.3700, 10.8135, 4.5100, 69.5900, 27.8007, 86.0500]
423 ... )
424 >>> x = np.arange(len(y))
425 >>> f = KernelInterpolator(x, y)
426 >>> f(0.5) # doctest: +ELLIPSIS
427 6.9411400...
429 Interpolating an `ArrayLike` variable:
431 >>> f([0.25, 0.75]) # doctest: +ELLIPSIS
432 array([ 6.1806208..., 8.0823848...])
434 Using a different *lanczos* kernel:
436 >>> f = KernelInterpolator(x, y, kernel=kernel_sinc)
437 >>> f([0.25, 0.75]) # doctest: +ELLIPSIS
438 array([ 6.5147317..., 8.3965466...])
440 Using a different window size:
442 >>> f = KernelInterpolator(
443 ... x, y, window=16, kernel=kernel_lanczos, kernel_kwargs={"a": 16}
444 ... )
445 >>> f([0.25, 0.75]) # doctest: +ELLIPSIS
446 array([ 5.3961792..., 5.6521093...])
447 """
449 def __init__(
450 self,
451 x: ArrayLike,
452 y: ArrayLike,
453 window: float = 3,
454 kernel: Callable = kernel_lanczos,
455 kernel_kwargs: dict | None = None,
456 padding_kwargs: dict | None = None,
457 dtype: Type[DTypeReal] | None = None,
458 *args: Any, # noqa: ARG002
459 **kwargs: Any, # noqa: ARG002
460 ) -> None:
461 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
463 self._x_p: NDArrayFloat = np.array([])
464 self._y_p: NDArrayFloat = np.array([])
466 self._x: NDArrayFloat = np.array([])
467 self._y: NDArrayFloat = np.array([])
468 self._window: float = 3
469 self._padding_kwargs: dict = {
470 "pad_width": (window, window),
471 "mode": "reflect",
472 }
473 self._kernel: Callable = kernel_lanczos
474 self._kernel_kwargs: dict = {}
475 self._dtype: Type[DTypeReal] = dtype
477 self.x = x
478 self.y = y
479 self.window = window
480 self.padding_kwargs = optional(padding_kwargs, self._padding_kwargs)
481 self.kernel = kernel
482 self.kernel_kwargs = optional(kernel_kwargs, self._kernel_kwargs)
484 self._validate_dimensions()
486 @property
487 def x(self) -> NDArrayFloat:
488 """
489 Getter and setter for the independent :math:`x` variable.
491 Parameters
492 ----------
493 value
494 Value to set the independent :math:`x` variable with.
496 Returns
497 -------
498 :class:`numpy.ndarray`
499 Independent :math:`x` variable.
500 """
502 return self._x
504 @x.setter
505 def x(self, value: ArrayLike) -> None:
506 """Setter for the **self.x** property."""
508 value = np.atleast_1d(value).astype(self._dtype)
510 attest(
511 value.ndim == 1,
512 '"x" independent variable must have exactly one dimension!',
513 )
515 value_interval = interval(value)
517 if value_interval.size != 1:
518 runtime_warning(
519 '"x" independent variable is not uniform, '
520 "unpredictable results may occur!"
521 )
523 self._x = as_array(value, self._dtype)
525 self._x_p = np.pad(
526 self._x,
527 as_int_array([self._window, self._window]),
528 "linear_ramp",
529 end_values=(
530 np.min(self._x) - self._window * value_interval[0],
531 np.max(self._x) + self._window * value_interval[0],
532 ),
533 )
535 @property
536 def y(self) -> NDArrayFloat:
537 """
538 Getter and setter for the dependent and already known :math:`y` variable.
540 Parameters
541 ----------
542 value
543 Value to set the dependent and already known :math:`y` variable
544 with.
546 Returns
547 -------
548 :class:`numpy.ndarray`
549 Dependent and already known :math:`y` variable.
550 """
552 return self._y
554 @y.setter
555 def y(self, value: ArrayLike) -> None:
556 """Setter for the **self.y** property."""
558 value = np.atleast_1d(value).astype(self._dtype)
560 attest(
561 value.ndim == 1,
562 '"y" dependent variable must have exactly one dimension!',
563 )
565 self._y = as_array(value, self._dtype)
567 if self._window is not None:
568 self._y_p = np.pad(self._y, **self._padding_kwargs)
570 @property
571 def window(self) -> float:
572 """
573 Getter and setter for the filtering window size for the moving average.
575 The window determines the number of samples used in the moving
576 average calculation. A larger window produces smoother results
577 with greater lag, while a smaller window yields more responsive
578 but potentially noisier output.
580 Parameters
581 ----------
582 value
583 Value to set the window with.
585 Returns
586 -------
587 :class:`float`
588 Window size for the moving average filter.
589 """
591 return self._window
593 @window.setter
594 def window(self, value: float) -> None:
595 """Setter for the **self.window** property."""
597 attest(bool(value >= 1), '"window" must be equal to or greater than 1!')
599 self._window = value
601 # Triggering "self._x_p" update.
602 if self._x is not None:
603 self.x = self._x
605 # Triggering "self._y_p" update.
606 if self._y is not None:
607 self.y = self._y
609 @property
610 def kernel(self) -> Callable:
611 """
612 Getter and setter for the kernel callable for the interpolator.
614 Parameters
615 ----------
616 value
617 Value to set the callable object to use as the interpolation kernel
618 with. Must be a callable that accepts numeric arguments.
620 Returns
621 -------
622 Callable
623 Callable object to use as the interpolation kernel.
625 Raises
626 ------
627 AssertionError
628 If the provided value is not callable.
629 """
631 return self._kernel
633 @kernel.setter
634 def kernel(self, value: Callable) -> None:
635 """Setter for the **self.kernel** property."""
637 attest(
638 callable(value),
639 f'"kernel" property: "{value}" is not callable!',
640 )
642 self._kernel = value
644 @property
645 def kernel_kwargs(self) -> dict:
646 """
647 Getter and setter for the kernel keyword arguments for the convolution
648 operation.
650 Parameters
651 ----------
652 value
653 Value to set the keyword arguments to pass to the kernel function
654 with.
656 Returns
657 -------
658 :class:`dict`
659 Keyword arguments to pass to the kernel function.
661 Raises
662 ------
663 AssertionError
664 If the provided value is not a :class:'dict` class instance.
665 """
667 return self._kernel_kwargs
669 @kernel_kwargs.setter
670 def kernel_kwargs(self, value: dict) -> None:
671 """Setter for the **self.kernel_kwargs** property."""
673 attest(
674 isinstance(value, dict),
675 f'"kernel_kwargs" property: "{value}" type is not "dict"!',
676 )
678 self._kernel_kwargs = value
680 @property
681 def padding_kwargs(self) -> dict:
682 """
683 Getter and setter for the padding keyword arguments for edge handling.
685 Parameters
686 ----------
687 value
688 Value to set the keyword arguments to pass to the padding function
689 when handling edges during interpolation.
691 Returns
692 -------
693 :class:`dict`
694 Keyword arguments to pass to the padding function when handling
695 edges during interpolation.
697 Raises
698 ------
699 AssertionError
700 If the provided value is not a :class:`dict` class instance.
701 """
703 return self._padding_kwargs
705 @padding_kwargs.setter
706 def padding_kwargs(self, value: dict) -> None:
707 """Setter for the **self.padding_kwargs** property."""
709 attest(
710 isinstance(value, dict),
711 f'"padding_kwargs" property: "{value}" type is not a "dict" instance!',
712 )
714 self._padding_kwargs = value
716 # Triggering "self._y_p" update.
717 if self._y is not None:
718 self.y = self._y
720 def __call__(self, x: ArrayLike) -> NDArrayFloat:
721 """
722 Evaluate the interpolator at specified point(s).
724 Parameters
725 ----------
726 x
727 Point(s) to evaluate the interpolant at.
729 Returns
730 -------
731 :class:`numpy.ndarray`
732 Interpolated value(s).
733 """
735 x = as_float_array(x)
737 xi = self._evaluate(x)
739 return as_float(xi)
741 def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat:
742 """
743 Evaluate the interpolating polynomial at the specified point.
745 Parameters
746 ----------
747 x
748 Point at which to evaluate the interpolant.
750 Returns
751 -------
752 :class:`numpy.ndarray`
753 Interpolated values at the specified point.
754 """
756 self._validate_dimensions()
757 self._validate_interpolation_range(x)
759 x_interval = interval(self._x)[0]
760 x_f = np.floor(x / x_interval)
762 windows = x_f[..., None] + np.arange(-self._window + 1, self._window + 1)
763 clip_l = min(self._x_p) / x_interval
764 clip_h = max(self._x_p) / x_interval
765 windows = np.clip(windows, clip_l, clip_h) - clip_l
766 windows = as_int_array(np.around(windows))
768 return np.sum(
769 self._y_p[windows]
770 * self._kernel(
771 x[..., None] / x_interval - windows - min(self._x_p) / x_interval,
772 **self._kernel_kwargs,
773 ),
774 axis=-1,
775 )
777 def _validate_dimensions(self) -> None:
778 """
779 Validate that the dimensions of the variables are equal.
781 Raises
782 ------
783 ValueError
784 If the x and y variable dimensions do not match.
785 """
787 if len(self._x) != len(self._y):
788 error = (
789 '"x" independent and "y" dependent variables have different '
790 f'dimensions: "{len(self._x)}", "{len(self._y)}"'
791 )
793 raise ValueError(error)
795 def _validate_interpolation_range(self, x: NDArrayFloat) -> None:
796 """
797 Validate that the specified interpolation point is within the valid
798 interpolation range.
800 The interpolation point must be within the bounds defined by the first
801 and last x-coordinates of the interpolator's data.
803 Parameters
804 ----------
805 x
806 Point to validate for interpolation range compliance.
808 Raises
809 ------
810 ValueError
811 If the point is outside the valid interpolation range.
812 """
814 below_interpolation_range = x < self._x[0]
815 above_interpolation_range = x > self._x[-1]
817 if below_interpolation_range.any():
818 error = f'"{x}" is below interpolation range.'
820 raise ValueError(error)
822 if above_interpolation_range.any():
823 error = f'"{x}" is above interpolation range.'
825 raise ValueError(error)
828class NearestNeighbourInterpolator(KernelInterpolator):
829 """
830 Perform nearest-neighbour interpolation on discrete data.
832 Implement a kernel-based interpolator that selects the closest known
833 data point for each query position. This interpolator provides fast,
834 discontinuous interpolation suitable for categorical data or when
835 preserving exact measured values is required.
837 Other Parameters
838 ----------------
839 dtype
840 Data type used for internal conversions.
841 padding_kwargs
842 Arguments to use when padding :math:`y` variable values with the
843 :func:`np.pad` definition.
844 window
845 Width of the window in samples on each side.
846 x
847 Independent :math:`x` variable values corresponding with :math:`y`
848 variable.
849 y
850 Dependent and already known :math:`y` variable values to
851 interpolate.
853 Methods
854 -------
855 - :meth:`~colour.NearestNeighbourInterpolator.__init__`
856 """
858 def __init__(self, *args: Any, **kwargs: Any) -> None:
859 kwargs["kernel"] = kernel_nearest_neighbour
860 kwargs.pop("kernel_kwargs", None)
862 super().__init__(*args, **kwargs)
865class LinearInterpolator:
866 """
867 Perform linear interpolation of a 1-D function.
869 This class provides a wrapper around NumPy's linear interpolation
870 functionality for interpolating between specified data points.
872 Parameters
873 ----------
874 x
875 Independent :math:`x` variable values corresponding with :math:`y`
876 variable.
877 y
878 Dependent and already known :math:`y` variable values to
879 interpolate.
880 dtype
881 Data type used for internal conversions.
883 Attributes
884 ----------
885 - :attr:`~colour.LinearInterpolator.x`
886 - :attr:`~colour.LinearInterpolator.y`
888 Methods
889 -------
890 - :meth:`~colour.LinearInterpolator.__init__`
891 - :meth:`~colour.LinearInterpolator.__call__`
893 Notes
894 -----
895 - This class is a wrapper around *numpy.interp* definition.
897 Examples
898 --------
899 Interpolating a single numeric variable:
901 >>> y = np.array([5.9200, 9.3700, 10.8135, 4.5100, 69.5900, 27.8007, 86.0500])
902 >>> x = np.arange(len(y))
903 >>> f = LinearInterpolator(x, y)
904 >>> f(0.5) # doctest: +ELLIPSIS
905 7.64...
907 Interpolating an `ArrayLike` variable:
909 >>> f([0.25, 0.75])
910 array([ 6.7825, 8.5075])
911 """
913 def __init__(
914 self,
915 x: ArrayLike,
916 y: ArrayLike,
917 dtype: Type[DTypeReal] | None = None,
918 *args: Any, # noqa: ARG002
919 **kwargs: Any, # noqa: ARG002
920 ) -> None:
921 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
923 self._x: NDArrayFloat = np.array([])
924 self._y: NDArrayFloat = np.array([])
925 self._dtype: Type[DTypeReal] = dtype
927 self.x = x
928 self.y = y
930 self._validate_dimensions()
932 @property
933 def x(self) -> NDArrayFloat:
934 """
935 Getter and setter for the independent :math:`x` variable.
937 Parameters
938 ----------
939 value
940 Value to set the independent :math:`x` variable with.
942 Returns
943 -------
944 :class:`numpy.ndarray`
945 Independent :math:`x` variable.
947 Raises
948 ------
949 AssertionError
950 If the provided value has not exactly one dimension.
951 """
953 return self._x
955 @x.setter
956 def x(self, value: ArrayLike) -> None:
957 """Setter for the **self.x** property."""
959 value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype))
961 attest(
962 value.ndim == 1,
963 '"x" independent variable must have exactly one dimension!',
964 )
966 self._x = value
968 @property
969 def y(self) -> NDArrayFloat:
970 """
971 Getter and setter for the dependent and already known
972 :math:`y` variable.
974 Parameters
975 ----------
976 value
977 Value to set the dependent and already known :math:`y` variable
978 with.
980 Returns
981 -------
982 :class:`numpy.ndarray`
983 Dependent and already known :math:`y` variable.
985 Raises
986 ------
987 AssertionError
988 If the provided value has not exactly one dimension.
989 """
991 return self._y
993 @y.setter
994 def y(self, value: ArrayLike) -> None:
995 """Setter for the **self.y** property."""
997 value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype))
999 attest(
1000 value.ndim == 1,
1001 '"y" dependent variable must have exactly one dimension!',
1002 )
1004 self._y = value
1006 def __call__(self, x: ArrayLike) -> NDArrayFloat:
1007 """
1008 Evaluate the interpolating polynomial at specified point(s).
1011 Parameters
1012 ----------
1013 x
1014 Point(s) to evaluate the interpolant at.
1016 Returns
1017 -------
1018 :class:`numpy.ndarray`
1019 Interpolated value(s).
1020 """
1022 x = as_float_array(x)
1024 xi = self._evaluate(x)
1026 return as_float(xi)
1028 def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat:
1029 """
1030 Perform the interpolating polynomial evaluation at specified points.
1032 Parameters
1033 ----------
1034 x
1035 Points to evaluate the interpolant at.
1037 Returns
1038 -------
1039 :class:`numpy.ndarray`
1040 Interpolated points values.
1041 """
1043 self._validate_dimensions()
1044 self._validate_interpolation_range(x)
1046 return np.interp(x, self._x, self._y)
1048 def _validate_dimensions(self) -> None:
1049 """Validate that the variables dimensions are the same."""
1051 if len(self._x) != len(self._y):
1052 error = (
1053 '"x" independent and "y" dependent variables have different '
1054 f'dimensions: "{len(self._x)}", "{len(self._y)}"'
1055 )
1057 raise ValueError(error)
1059 def _validate_interpolation_range(self, x: NDArrayFloat) -> None:
1060 """Validate specified point to be in interpolation range."""
1062 below_interpolation_range = x < self._x[0]
1063 above_interpolation_range = x > self._x[-1]
1065 if below_interpolation_range.any():
1066 error = f'"{x}" is below interpolation range.'
1068 raise ValueError(error)
1070 if above_interpolation_range.any():
1071 error = f'"{x}" is above interpolation range.'
1073 raise ValueError(error)
1076class SpragueInterpolator:
1077 """
1078 Perform fifth-order polynomial interpolation using the *Sprague (1880)*
1079 method for uniformly spaced data.
1081 Implement the *Sprague (1880)* interpolation method recommended by the
1082 *CIE* for interpolating functions with uniformly spaced independent
1083 variables. This interpolator constructs a fifth-order polynomial that
1084 passes through specified dependent variable values, providing smooth
1085 interpolation suitable for spectral data and other colour science
1086 applications.
1088 Parameters
1089 ----------
1090 x
1091 Independent :math:`x` variable values corresponding with :math:`y`
1092 variable.
1093 y
1094 Dependent and already known :math:`y` variable values to
1095 interpolate.
1096 dtype
1097 Data type used for internal conversions.
1099 Attributes
1100 ----------
1101 - :attr:`~colour.SpragueInterpolator.x`
1102 - :attr:`~colour.SpragueInterpolator.y`
1104 Methods
1105 -------
1106 - :meth:`~colour.SpragueInterpolator.__init__`
1107 - :meth:`~colour.SpragueInterpolator.__call__`
1109 Notes
1110 -----
1111 - The minimum number :math:`k` of data points required along the
1112 interpolation axis is :math:`k=6`.
1114 References
1115 ----------
1116 :cite:`CIETC1-382005f`, :cite:`Westland2012h`
1118 Examples
1119 --------
1120 Interpolating a single numeric variable:
1122 >>> y = np.array([5.9200, 9.3700, 10.8135, 4.5100, 69.5900, 27.8007, 86.0500])
1123 >>> x = np.arange(len(y))
1124 >>> f = SpragueInterpolator(x, y)
1125 >>> f(0.5) # doctest: +ELLIPSIS
1126 7.2185025...
1128 Interpolating an `ArrayLike` variable:
1130 >>> f([0.25, 0.75]) # doctest: +ELLIPSIS
1131 array([ 6.7295161..., 7.8140625...])
1132 """
1134 SPRAGUE_C_COEFFICIENTS = np.array(
1135 [
1136 [884, -1960, 3033, -2648, 1080, -180],
1137 [508, -540, 488, -367, 144, -24],
1138 [-24, 144, -367, 488, -540, 508],
1139 [-180, 1080, -2648, 3033, -1960, 884],
1140 ]
1141 )
1142 """
1143 Defines the coefficients used to generate extra points for boundaries
1144 interpolation.
1146 SPRAGUE_C_COEFFICIENTS, (4, 6)
1148 References
1149 ----------
1150 :cite:`CIETC1-382005h`
1151 """
1153 def __init__(
1154 self,
1155 x: ArrayLike,
1156 y: ArrayLike,
1157 dtype: Type[DTypeReal] | None = None,
1158 *args: Any, # noqa: ARG002
1159 **kwargs: Any, # noqa: ARG002
1160 ) -> None:
1161 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1163 self._xp: NDArrayFloat = np.array([])
1164 self._yp: NDArrayFloat = np.array([])
1166 self._x: NDArrayFloat = np.array([])
1167 self._y: NDArrayFloat = np.array([])
1168 self._dtype: Type[DTypeReal] = dtype
1170 self.x = x
1171 self.y = y
1173 self._validate_dimensions()
1175 @property
1176 def x(self) -> NDArrayFloat:
1177 """
1178 Getter and setter for the independent :math:`x` variable.
1180 Parameters
1181 ----------
1182 value
1183 Value to set the independent :math:`x` variable with.
1185 Returns
1186 -------
1187 :class:`numpy.ndarray`
1188 Independent :math:`x` variable.
1190 Raises
1191 ------
1192 AssertionError
1193 If the provided value has not exactly one dimension.
1194 """
1196 return self._x
1198 @x.setter
1199 def x(self, value: ArrayLike) -> None:
1200 """Setter for the **self.x** property."""
1202 value = as_array(np.atleast_1d(value), self._dtype)
1204 attest(
1205 value.ndim == 1,
1206 '"x" independent variable must have exactly one dimension!',
1207 )
1209 self._x = value
1211 value_interval = interval(self._x)[0]
1213 xp1 = self._x[0] - value_interval * 2
1214 xp2 = self._x[0] - value_interval
1215 xp3 = self._x[-1] + value_interval
1216 xp4 = self._x[-1] + value_interval * 2
1218 self._xp = np.concatenate(
1219 [
1220 as_array([xp1, xp2], self._dtype),
1221 value,
1222 as_array([xp3, xp4], self._dtype),
1223 ]
1224 )
1226 @property
1227 def y(self) -> NDArrayFloat:
1228 """
1229 Getter and setter for the dependent and already known
1230 :math:`y` variable.
1232 Parameters
1233 ----------
1234 value
1235 Value to set the dependent and already known :math:`y` variable
1236 with.
1238 Returns
1239 -------
1240 :class:`numpy.ndarray`
1241 Dependent and already known :math:`y` variable.
1243 Raises
1244 ------
1245 AssertionError
1246 If the provided value has not exactly one dimension and its value
1247 count is less than 6.
1248 """
1250 return self._y
1252 @y.setter
1253 def y(self, value: ArrayLike) -> None:
1254 """Setter for the **self.y** property."""
1256 value = as_array(np.atleast_1d(value), self._dtype)
1258 attest(
1259 value.ndim == 1,
1260 '"y" dependent variable must have exactly one dimension!',
1261 )
1263 attest(
1264 len(value) >= 6,
1265 '"y" dependent variable values count must be equal to or greater than 6!',
1266 )
1268 self._y = value
1270 yp1, yp2, yp3, yp4 = (
1271 np.sum(
1272 self.SPRAGUE_C_COEFFICIENTS
1273 * np.asarray((value[0:6], value[0:6], value[-6:], value[-6:])),
1274 axis=1,
1275 )
1276 / 209
1277 )
1279 self._yp = np.concatenate(
1280 [
1281 as_array([yp1, yp2], self._dtype),
1282 value,
1283 as_array([yp3, yp4], self._dtype),
1284 ]
1285 )
1287 def __call__(self, x: ArrayLike) -> NDArrayFloat:
1288 """
1289 Evaluate the interpolating polynomial at specified point(s).
1291 Parameters
1292 ----------
1293 x
1294 Point(s) to evaluate the interpolant at.
1296 Returns
1297 -------
1298 :class:`numpy.ndarray`
1299 Interpolated value(s).
1300 """
1302 x = as_float_array(x)
1304 xi = self._evaluate(x)
1306 return as_float(xi)
1308 def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat:
1309 """
1310 Perform the interpolating polynomial evaluation at specified point.
1312 Parameters
1313 ----------
1314 x
1315 Point to evaluate the interpolant at.
1317 Returns
1318 -------
1319 :class:`numpy.ndarray`
1320 Interpolated point values.
1321 """
1323 self._validate_dimensions()
1324 self._validate_interpolation_range(x)
1326 i = np.searchsorted(self._xp, x) - 1
1327 with sdiv_mode():
1328 X = sdiv(x - self._xp[i], self._xp[i + 1] - self._xp[i])
1330 r = self._yp
1332 r_s = np.asarray((r[i - 2], r[i - 1], r[i], r[i + 1], r[i + 2], r[i + 3]))
1333 w_s = np.asarray(
1334 (
1335 (2, -16, 0, 16, -2, 0),
1336 (-1, 16, -30, 16, -1, 0),
1337 (-9, 39, -70, 66, -33, 7),
1338 (13, -64, 126, -124, 61, -12),
1339 (-5, 25, -50, 50, -25, 5),
1340 )
1341 )
1342 a = np.dot(w_s, r_s) / 24
1344 # Fancy vector code here... use underlying numpy structures to accelerate
1345 # parts of the linear algebra.
1347 y = r[i] + (a.reshape(5, -1) * X ** np.arange(1, 6).reshape(-1, 1)).sum(axis=0)
1349 if y.size == 1:
1350 return y[0]
1352 return y
1354 def _validate_dimensions(self) -> None:
1355 """Validate that the variables dimensions are the same."""
1357 if len(self._x) != len(self._y):
1358 error = (
1359 '"x" independent and "y" dependent variables have different '
1360 f'dimensions: "{len(self._x)}", "{len(self._y)}"'
1361 )
1363 raise ValueError(error)
1365 def _validate_interpolation_range(self, x: NDArrayFloat) -> None:
1366 """Validate specified point to be in interpolation range."""
1368 below_interpolation_range = x < self._x[0]
1369 above_interpolation_range = x > self._x[-1]
1371 if below_interpolation_range.any():
1372 error = f'"{x}" is below interpolation range.'
1374 raise ValueError(error)
1376 if above_interpolation_range.any():
1377 error = f'"{x}" is above interpolation range.'
1379 raise ValueError(error)
1382class CubicSplineInterpolator(scipy.interpolate.interp1d):
1383 """
1384 Perform cubic spline interpolation on one-dimensional data.
1386 Provide smooth interpolation through specified data points using
1387 piecewise cubic polynomials. The resulting interpolant maintains
1388 continuity in the function and its first two derivatives at data
1389 points, making it suitable for spectral data and colour science
1390 applications requiring smooth transitions between measured values.
1392 Methods
1393 -------
1394 - :meth:`~colour.CubicSplineInterpolator.__init__`
1396 Notes
1397 -----
1398 - This class is a wrapper around *scipy.interpolate.interp1d* class.
1399 """
1401 def __init__(self, *args: Any, **kwargs: Any) -> None:
1402 kwargs["kind"] = "cubic"
1403 super().__init__(*args, **kwargs)
1406class PchipInterpolator(scipy.interpolate.PchipInterpolator):
1407 """
1408 Interpolate a 1-D function using Piecewise Cubic Hermite Interpolating
1409 Polynomial (PCHIP) interpolation.
1411 PCHIP interpolation constructs a smooth curve through specified data
1412 points while preserving monotonicity between consecutive points. This
1413 method ensures that the interpolated values do not exhibit spurious
1414 oscillations, making it particularly suitable for colour science
1415 applications where physical constraints must be respected.
1417 Attributes
1418 ----------
1419 - :attr:`~colour.PchipInterpolator.y`
1421 Methods
1422 -------
1423 - :meth:`~colour.PchipInterpolator.__init__`
1425 Notes
1426 -----
1427 - This class is a wrapper around *scipy.interpolate.PchipInterpolator*
1428 class.
1429 """
1431 def __init__(self, x: ArrayLike, y: ArrayLike, *args: Any, **kwargs: Any) -> None:
1432 super().__init__(as_float_array(x), as_float_array(y), *args, **kwargs)
1434 self._y: NDArrayFloat = as_float_array(y)
1436 @property
1437 def y(self) -> NDArrayFloat:
1438 """
1439 Getter and setter for the dependent and already known
1440 :math:`y` variable.
1442 Parameters
1443 ----------
1444 value
1445 Value to set the dependent and already known :math:`y` variable
1446 with.
1448 Returns
1449 -------
1450 :class:`numpy.ndarray`
1451 Dependent and already known :math:`y` variable.
1452 """
1454 return self._y
1456 @y.setter
1457 def y(self, value: ArrayLike) -> None:
1458 """Setter for the **self.y** property."""
1460 self._y = as_float_array(value)
1463class NullInterpolator:
1464 """
1465 Implement 1-D function null interpolation.
1467 This interpolator returns existing :math:`y` values when called with
1468 :math:`x` values within specified tolerances, and returns a default
1469 value when outside tolerances. Unlike traditional interpolators that
1470 estimate intermediate values, this null interpolator only returns exact
1471 matches within tolerance bounds.
1473 Parameters
1474 ----------
1475 x
1476 Independent :math:`x` variable values corresponding with :math:`y`
1477 variable.
1478 y
1479 Dependent and already known :math:`y` variable values to
1480 interpolate.
1481 absolute_tolerance
1482 Absolute tolerance.
1483 relative_tolerance
1484 Relative tolerance.
1485 default
1486 Default value for interpolation outside tolerances.
1487 dtype
1488 Data type used for internal conversions.
1490 Attributes
1491 ----------
1492 - :attr:`~colour.NullInterpolator.x`
1493 - :attr:`~colour.NullInterpolator.y`
1494 - :attr:`~colour.NullInterpolator.relative_tolerance`
1495 - :attr:`~colour.NullInterpolator.absolute_tolerance`
1496 - :attr:`~colour.NullInterpolator.default`
1498 Methods
1499 -------
1500 - :meth:`~colour.NullInterpolator.__init__`
1501 - :meth:`~colour.NullInterpolator.__call__`
1503 Examples
1504 --------
1505 >>> y = np.array([5.9200, 9.3700, 10.8135, 4.5100, 69.5900, 27.8007, 86.0500])
1506 >>> x = np.arange(len(y))
1507 >>> f = NullInterpolator(x, y)
1508 >>> f(0.5)
1509 nan
1510 >>> f(1.0) # doctest: +ELLIPSIS
1511 9.3699999...
1512 >>> f = NullInterpolator(x, y, absolute_tolerance=0.01)
1513 >>> f(1.01) # doctest: +ELLIPSIS
1514 9.3699999...
1515 """
1517 def __init__(
1518 self,
1519 x: ArrayLike,
1520 y: ArrayLike,
1521 absolute_tolerance: float = TOLERANCE_ABSOLUTE_DEFAULT,
1522 relative_tolerance: float = TOLERANCE_RELATIVE_DEFAULT,
1523 default: float = np.nan,
1524 dtype: Type[DTypeReal] | None = None,
1525 *args: Any, # noqa: ARG002
1526 **kwargs: Any, # noqa: ARG002
1527 ) -> None:
1528 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1530 self._x: NDArrayFloat = np.array([])
1531 self._y: NDArrayFloat = np.array([])
1532 self._absolute_tolerance: float = TOLERANCE_ABSOLUTE_DEFAULT
1533 self._relative_tolerance: float = TOLERANCE_RELATIVE_DEFAULT
1534 self._default: float = np.nan
1535 self._dtype: Type[DTypeReal] = dtype
1537 self.x = x
1538 self.y = y
1539 self.absolute_tolerance = absolute_tolerance
1540 self.relative_tolerance = relative_tolerance
1541 self.default = default
1543 self._validate_dimensions()
1545 @property
1546 def x(self) -> NDArrayFloat:
1547 """
1548 Getter and setter for the independent :math:`x` variable.
1550 Parameters
1551 ----------
1552 value
1553 Value to set the independent :math:`x` variable with.
1555 Returns
1556 -------
1557 :class:`numpy.ndarray`
1558 Independent :math:`x` variable.
1560 Raises
1561 ------
1562 AssertionError
1563 If the provided value has not exactly one dimension.
1564 """
1566 return self._x
1568 @x.setter
1569 def x(self, value: ArrayLike) -> None:
1570 """Setter for the **self.x** property."""
1572 value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype))
1574 attest(
1575 value.ndim == 1,
1576 '"x" independent variable must have exactly one dimension!',
1577 )
1579 self._x = value
1581 @property
1582 def y(self) -> NDArrayFloat:
1583 """
1584 Getter and setter for the dependent and already known
1585 :math:`y` variable.
1587 Parameters
1588 ----------
1589 value
1590 Value to set the dependent and already known :math:`y` variable
1591 with.
1593 Returns
1594 -------
1595 :class:`numpy.ndarray`
1596 Dependent and already known :math:`y` variable.
1598 Raises
1599 ------
1600 AssertionError
1601 If the provided value has not exactly one dimension.
1602 """
1604 return self._y
1606 @y.setter
1607 def y(self, value: ArrayLike) -> None:
1608 """Setter for the **self.y** property."""
1610 value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype))
1612 attest(
1613 value.ndim == 1,
1614 '"y" dependent variable must have exactly one dimension!',
1615 )
1617 self._y = value
1619 @property
1620 def relative_tolerance(self) -> float:
1621 """
1622 Getter and setter property for the relative tolerance for numerical
1623 comparisons.
1625 Parameters
1626 ----------
1627 value
1628 Value to set the relative tolerance for numerical comparisons with.
1630 Returns
1631 -------
1632 :class:`float`
1633 Relative tolerance for numerical comparisons.
1635 Raises
1636 ------
1637 AssertionError
1638 If the value is not numeric.
1639 """
1641 return self._relative_tolerance
1643 @relative_tolerance.setter
1644 def relative_tolerance(self, value: float) -> None:
1645 """Setter for the **self.relative_tolerance** property."""
1647 attest(
1648 is_numeric(value),
1649 '"relative_tolerance" variable must be a "numeric"!',
1650 )
1652 self._relative_tolerance = as_float_scalar(value)
1654 @property
1655 def absolute_tolerance(self) -> float:
1656 """
1657 Getter and setter property for the absolute tolerance for numerical
1658 comparisons.
1660 Parameters
1661 ----------
1662 value
1663 Value to set the absolute tolerance for numerical comparisons with.
1665 Returns
1666 -------
1667 :class:`float`
1668 Absolute tolerance for numerical comparisons.
1670 Raises
1671 ------
1672 AssertionError
1673 If the value is not numeric.
1674 """
1676 return self._absolute_tolerance
1678 @absolute_tolerance.setter
1679 def absolute_tolerance(self, value: float) -> None:
1680 """Setter for the **self.absolute_tolerance** property."""
1682 attest(
1683 is_numeric(value),
1684 '"absolute_tolerance" variable must be a "numeric"!',
1685 )
1687 self._absolute_tolerance = as_float_scalar(value)
1689 @property
1690 def default(self) -> float:
1691 """
1692 Getter and setter property for the default value for call outside
1693 tolerances.
1695 Parameters
1696 ----------
1697 value
1698 Value to set the default value with for call outside tolerances.
1700 Returns
1701 -------
1702 :class:`float`
1703 Default value for call outside tolerances.
1705 Raises
1706 ------
1707 AssertionError
1708 If the value is not numeric.
1709 """
1711 return self._default
1713 @default.setter
1714 def default(self, value: float) -> None:
1715 """Setter for the **self.default** property."""
1717 attest(is_numeric(value), '"default" variable must be a "numeric"!')
1719 self._default = value
1721 def __call__(self, x: ArrayLike) -> NDArrayFloat:
1722 """
1723 Evaluate the interpolator at specified point(s).
1726 Parameters
1727 ----------
1728 x
1729 Point(s) to evaluate the interpolant at.
1731 Returns
1732 -------
1733 :class:`numpy.ndarray`
1734 Interpolated value(s).
1735 """
1737 x = as_float_array(x)
1739 xi = self._evaluate(x)
1741 return as_float(xi)
1743 def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat:
1744 """
1745 Perform the interpolator evaluation at specified points.
1747 Parameters
1748 ----------
1749 x
1750 Points to evaluate the interpolant at.
1752 Returns
1753 -------
1754 :class:`numpy.ndarray`
1755 Interpolated points values.
1756 """
1758 self._validate_dimensions()
1759 self._validate_interpolation_range(x)
1761 indexes = closest_indexes(self._x, x)
1762 values = self._y[indexes]
1763 values[
1764 ~np.isclose(
1765 self._x[indexes],
1766 x,
1767 rtol=self._absolute_tolerance,
1768 atol=self._relative_tolerance,
1769 )
1770 ] = self._default
1772 return np.squeeze(values)
1774 def _validate_dimensions(self) -> None:
1775 """Validate that the variables dimensions are the same."""
1777 if len(self._x) != len(self._y):
1778 error = (
1779 '"x" independent and "y" dependent variables have different '
1780 f'dimensions: "{len(self._x)}", "{len(self._y)}"'
1781 )
1783 raise ValueError(error)
1785 def _validate_interpolation_range(self, x: NDArrayFloat) -> None:
1786 """Validate specified point to be in interpolation range."""
1788 below_interpolation_range = x < self._x[0]
1789 above_interpolation_range = x > self._x[-1]
1791 if below_interpolation_range.any():
1792 error = f'"{x}" is below interpolation range.'
1794 raise ValueError(error)
1796 if above_interpolation_range.any():
1797 error = f'"{x}" is above interpolation range.'
1799 raise ValueError(error)
1802def lagrange_coefficients(r: float, n: int = 4) -> NDArrayFloat:
1803 """
1804 Compute *Lagrange coefficients* at specified point :math:`r` for
1805 polynomial interpolation of degree :math:`n`.
1807 Parameters
1808 ----------
1809 r
1810 Point at which to compute the *Lagrange coefficients*.
1811 n
1812 Degree of the polynomial interpolation. The number of coefficients
1813 returned will be :math:`n + 1`.
1815 Returns
1816 -------
1817 :class:`numpy.ndarray`
1818 Array of *Lagrange coefficients* computed at point :math:`r`.
1820 References
1821 ----------
1822 :cite:`Fairman1985b`, :cite:`Wikipedia2003a`
1824 Examples
1825 --------
1826 >>> lagrange_coefficients(0.1)
1827 array([ 0.8265, 0.2755, -0.1305, 0.0285])
1828 """
1830 r_i = np.arange(n)
1831 L_n = []
1832 for j in range(len(r_i)):
1833 basis = [(r - r_i[i]) / (r_i[j] - r_i[i]) for i in range(len(r_i)) if i != j]
1834 L_n.append(reduce(lambda x, y: x * y, basis))
1836 return np.array(L_n)
1839def table_interpolation_trilinear(V_xyz: ArrayLike, table: ArrayLike) -> NDArrayFloat:
1840 """
1841 Perform trilinear interpolation of the specified :math:`V_{xyz}` values using
1842 the specified interpolation table.
1844 Parameters
1845 ----------
1846 V_xyz
1847 :math:`V_{xyz}` values to interpolate.
1848 table
1849 4-Dimensional (NxNxNx3) interpolation table.
1851 Returns
1852 -------
1853 :class:`numpy.ndarray`
1854 Interpolated :math:`V_{xyz}` values.
1856 References
1857 ----------
1858 :cite:`Bourkeb`
1860 Examples
1861 --------
1862 >>> import os
1863 >>> import colour
1864 >>> path = os.path.join(
1865 ... os.path.dirname(__file__),
1866 ... "..",
1867 ... "io",
1868 ... "luts",
1869 ... "tests",
1870 ... "resources",
1871 ... "iridas_cube",
1872 ... "Colour_Correct.cube",
1873 ... )
1874 >>> LUT = colour.read_LUT(path)
1875 >>> table = LUT.table
1876 >>> prng = np.random.RandomState(4)
1877 >>> V_xyz = colour.algebra.random_triplet_generator(3, random_state=prng)
1878 >>> print(V_xyz) # doctest: +ELLIPSIS
1879 [[ 0.9670298... 0.7148159... 0.9762744...]
1880 [ 0.5472322... 0.6977288... 0.0062302...]
1881 [ 0.9726843... 0.2160895... 0.2529823...]]
1882 >>> table_interpolation_trilinear(V_xyz, table) # doctest: +ELLIPSIS
1883 array([[ 1.0120664..., 0.7539146..., 1.0228540...],
1884 [ 0.5075794..., 0.6479459..., 0.1066404...],
1885 [ 1.0976519..., 0.1785998..., 0.2299897...]])
1886 """
1888 V_xyz = cast("NDArrayFloat", V_xyz)
1889 original_shape = V_xyz.shape
1890 V_xyz = cast("NDArrayFloat", np.clip(V_xyz, 0, 1).reshape(-1, 3))
1892 # Index computation
1893 table = cast("NDArrayFloat", table)
1894 i_m = np.array(table.shape[:-1]) - 1
1895 V_xyz_s = V_xyz * i_m
1897 i_f = V_xyz_s.astype(DTYPE_INT_DEFAULT)
1898 i_f = np.clip(i_f, 0, i_m)
1899 i_c = np.minimum(i_f + 1, i_m)
1901 # Relative coordinates (fractional part)
1902 frac = V_xyz_s - i_f
1904 # Extract indices for direct lookup
1905 fx, fy, fz = i_f[:, 0], i_f[:, 1], i_f[:, 2]
1906 cx, cy, cz = i_c[:, 0], i_c[:, 1], i_c[:, 2]
1908 # Extract fractional coordinates
1909 dx, dy, dz = frac[:, 0:1], frac[:, 1:2], frac[:, 2:3]
1910 dx1, dy1, dz1 = 1.0 - dx, 1.0 - dy, 1.0 - dz
1912 # Direct vertex lookups (8 corners of cube)
1913 v000 = table[fx, fy, fz]
1914 v001 = table[fx, fy, cz]
1915 v010 = table[fx, cy, fz]
1916 v011 = table[fx, cy, cz]
1917 v100 = table[cx, fy, fz]
1918 v101 = table[cx, fy, cz]
1919 v110 = table[cx, cy, fz]
1920 v111 = table[cx, cy, cz]
1922 # Trilinear interpolation (vectorized)
1923 result = (
1924 v000 * (dx1 * dy1 * dz1)
1925 + v001 * (dx1 * dy1 * dz)
1926 + v010 * (dx1 * dy * dz1)
1927 + v011 * (dx1 * dy * dz)
1928 + v100 * (dx * dy1 * dz1)
1929 + v101 * (dx * dy1 * dz)
1930 + v110 * (dx * dy * dz1)
1931 + v111 * (dx * dy * dz)
1932 )
1934 return result.reshape(original_shape)
1937def table_interpolation_tetrahedral(V_xyz: ArrayLike, table: ArrayLike) -> NDArrayFloat:
1938 """
1939 Perform tetrahedral interpolation of the specified :math:`V_{xyz}` values
1940 using the specified 4-dimensional interpolation table.
1942 Parameters
1943 ----------
1944 V_xyz
1945 :math:`V_{xyz}` values to interpolate.
1946 table
1947 4-Dimensional (NxNxNx3) interpolation table.
1949 Returns
1950 -------
1951 :class:`numpy.ndarray`
1952 Interpolated :math:`V_{xyz}` values.
1954 References
1955 ----------
1956 :cite:`Kirk2006`
1958 Examples
1959 --------
1960 >>> import os
1961 >>> import colour
1962 >>> path = os.path.join(
1963 ... os.path.dirname(__file__),
1964 ... "..",
1965 ... "io",
1966 ... "luts",
1967 ... "tests",
1968 ... "resources",
1969 ... "iridas_cube",
1970 ... "Colour_Correct.cube",
1971 ... )
1972 >>> LUT = colour.read_LUT(path)
1973 >>> table = LUT.table
1974 >>> prng = np.random.RandomState(4)
1975 >>> V_xyz = colour.algebra.random_triplet_generator(3, random_state=prng)
1976 >>> print(V_xyz) # doctest: +ELLIPSIS
1977 [[ 0.9670298... 0.7148159... 0.9762744...]
1978 [ 0.5472322... 0.6977288... 0.0062302...]
1979 [ 0.9726843... 0.2160895... 0.2529823...]]
1980 >>> table_interpolation_tetrahedral(V_xyz, table) # doctest: +ELLIPSIS
1981 array([[ 1.0196197..., 0.7674062..., 1.0311751...],
1982 [ 0.5105603..., 0.6466722..., 0.1077296...],
1983 [ 1.1178206..., 0.1762039..., 0.2209534...]])
1984 """
1986 V_xyz = cast("NDArrayFloat", V_xyz)
1987 original_shape = V_xyz.shape
1988 V_xyz = cast("NDArrayFloat", np.clip(V_xyz, 0, 1).reshape(-1, 3))
1990 # Index computation
1991 table = cast("NDArrayFloat", table)
1992 i_m = np.array(table.shape[:-1]) - 1
1993 V_xyz_s = V_xyz * i_m
1995 i_f = V_xyz_s.astype(DTYPE_INT_DEFAULT)
1996 i_f = np.clip(i_f, 0, i_m)
1997 i_c = np.minimum(i_f + 1, i_m)
1999 # Relative coordinates
2000 r = V_xyz_s - i_f
2001 x, y, z = r[:, 0], r[:, 1], r[:, 2]
2003 # Extract indices for direct lookup
2004 fx, fy, fz = i_f[:, 0], i_f[:, 1], i_f[:, 2]
2005 cx, cy, cz = i_c[:, 0], i_c[:, 1], i_c[:, 2]
2007 # Look up 8 corner vertices
2008 V000 = table[fx, fy, fz]
2009 V001 = table[fx, fy, cz]
2010 V010 = table[fx, cy, fz]
2011 V011 = table[fx, cy, cz]
2012 V100 = table[cx, fy, fz]
2013 V101 = table[cx, fy, cz]
2014 V110 = table[cx, cy, fz]
2015 V111 = table[cx, cy, cz]
2017 # Expand dimensions for broadcasting
2018 x = x[:, np.newaxis]
2019 y = y[:, np.newaxis]
2020 z = z[:, np.newaxis]
2022 # Tetrahedral interpolation - select tetrahedron based on position
2023 xyz_o = np.select(
2024 [
2025 np.logical_and(x > y, y > z),
2026 np.logical_and(x > z, z >= y),
2027 np.logical_and(z >= x, x > y),
2028 np.logical_and(y >= x, x > z),
2029 np.logical_and(y >= z, z >= x),
2030 np.logical_and(z > y, y >= x),
2031 ],
2032 [
2033 (1 - x) * V000 + (x - y) * V100 + (y - z) * V110 + z * V111,
2034 (1 - x) * V000 + (x - z) * V100 + (z - y) * V101 + y * V111,
2035 (1 - z) * V000 + (z - x) * V001 + (x - y) * V101 + y * V111,
2036 (1 - y) * V000 + (y - x) * V010 + (x - z) * V110 + z * V111,
2037 (1 - y) * V000 + (y - z) * V010 + (z - x) * V011 + x * V111,
2038 (1 - z) * V000 + (z - y) * V001 + (y - x) * V011 + x * V111,
2039 ],
2040 )
2042 return xyz_o.reshape(original_shape)
2045TABLE_INTERPOLATION_METHODS = CanonicalMapping(
2046 {
2047 "Trilinear": table_interpolation_trilinear,
2048 "Tetrahedral": table_interpolation_tetrahedral,
2049 }
2050)
2051TABLE_INTERPOLATION_METHODS.__doc__ = """
2052Supported table interpolation methods.
2054References
2055----------
2056:cite:`Bourkeb`, :cite:`Kirk2006`
2057"""
2060def table_interpolation(
2061 V_xyz: ArrayLike,
2062 table: ArrayLike,
2063 method: Literal["Trilinear", "Tetrahedral"] | str = "Trilinear",
2064) -> NDArrayFloat:
2065 """
2066 Perform interpolation of the specified :math:`V_{xyz}` values using a
2067 4-dimensional interpolation table.
2069 Interpolate the input :math:`V_{xyz}` values through either trilinear
2070 or tetrahedral interpolation methods using the specified lookup table.
2072 Parameters
2073 ----------
2074 V_xyz
2075 :math:`V_{xyz}` values to interpolate, where each row represents
2076 a three-dimensional coordinate within the interpolation table's
2077 domain.
2078 table
2079 4-dimensional (NxNxNx3) interpolation table defining the mapping
2080 from input coordinates to output values.
2081 method
2082 Interpolation method to use. Either "Trilinear" for trilinear
2083 interpolation or "Tetrahedral" for tetrahedral interpolation.
2085 Returns
2086 -------
2087 :class:`numpy.ndarray`
2088 Interpolated :math:`V_{xyz}` values with the same shape as the
2089 input array.
2091 References
2092 ----------
2093 :cite:`Bourkeb`, :cite:`Kirk2006`
2095 Examples
2096 --------
2097 >>> import os
2098 >>> import colour
2099 >>> path = os.path.join(
2100 ... os.path.dirname(__file__),
2101 ... "..",
2102 ... "io",
2103 ... "luts",
2104 ... "tests",
2105 ... "resources",
2106 ... "iridas_cube",
2107 ... "Colour_Correct.cube",
2108 ... )
2109 >>> LUT = colour.read_LUT(path)
2110 >>> table = LUT.table
2111 >>> prng = np.random.RandomState(4)
2112 >>> V_xyz = colour.algebra.random_triplet_generator(3, random_state=prng)
2113 >>> print(V_xyz) # doctest: +ELLIPSIS
2114 [[ 0.9670298... 0.7148159... 0.9762744...]
2115 [ 0.5472322... 0.6977288... 0.0062302...]
2116 [ 0.9726843... 0.2160895... 0.2529823...]]
2117 >>> table_interpolation(V_xyz, table) # doctest: +ELLIPSIS
2118 array([[ 1.0120664..., 0.7539146..., 1.0228540...],
2119 [ 0.5075794..., 0.6479459..., 0.1066404...],
2120 [ 1.0976519..., 0.1785998..., 0.2299897...]])
2121 >>> table_interpolation(V_xyz, table, method="Tetrahedral")
2122 ... # doctest: +ELLIPSIS
2123 array([[ 1.0196197..., 0.7674062..., 1.0311751...],
2124 [ 0.5105603..., 0.6466722..., 0.1077296...],
2125 [ 1.1178206..., 0.1762039..., 0.2209534...]])
2126 """
2128 method = validate_method(method, tuple(TABLE_INTERPOLATION_METHODS))
2130 return TABLE_INTERPOLATION_METHODS[method](V_xyz, table)