Coverage for algebra/interpolation.py: 69%

415 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2Interpolation 

3============= 

4 

5Provide classes and functions for interpolating variables in colour science 

6computations. 

7 

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. 

12 

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. 

34 

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""" 

64 

65from __future__ import annotations 

66 

67import sys 

68import typing 

69from functools import reduce 

70from unittest.mock import MagicMock 

71 

72import numpy as np 

73 

74from colour.utilities.requirements import is_scipy_installed 

75from colour.utilities.verbose import usage_warning 

76 

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

82 

83 mock = MagicMock() 

84 mock.__name__ = "" 

85 

86 for module in ( 

87 "scipy", 

88 "scipy.interpolate", 

89 ): 

90 sys.modules[module] = mock 

91 

92import scipy.interpolate 

93 

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) 

101 

102if typing.TYPE_CHECKING: 

103 from colour.hints import ( 

104 Any, 

105 ArrayLike, 

106 Callable, 

107 DTypeReal, 

108 Literal, 

109 Type, 

110 ) 

111 

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) 

128 

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" 

135 

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] 

155 

156 

157def kernel_nearest_neighbour(x: ArrayLike) -> NDArrayFloat: 

158 """ 

159 Return the *nearest-neighbour* kernel evaluated at specified samples. 

160 

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. 

165 

166 Parameters 

167 ---------- 

168 x 

169 Samples at which to evaluate the *nearest-neighbour* kernel. 

170 

171 Returns 

172 ------- 

173 :class:`numpy.ndarray` 

174 The *nearest-neighbour* kernel evaluated at specified samples. 

175 

176 References 

177 ---------- 

178 :cite:`Burger2009b` 

179 

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 """ 

185 

186 return np.where(np.abs(x) < 0.5, 1, 0) 

187 

188 

189def kernel_linear(x: ArrayLike) -> NDArrayFloat: 

190 """ 

191 Evaluate the *linear* kernel at specified samples. 

192 

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. 

196 

197 Parameters 

198 ---------- 

199 x 

200 Samples at which to evaluate the *linear* kernel. 

201 

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. 

207 

208 References 

209 ---------- 

210 :cite:`Burger2009b` 

211 

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 """ 

220 

221 return np.where(np.abs(x) < 1, 1 - np.abs(x), 0) 

222 

223 

224def kernel_sinc(x: ArrayLike, a: float = 3) -> NDArrayFloat: 

225 """ 

226 Evaluate the *sinc* kernel at specified sample positions. 

227 

228 Compute the *sinc* kernel function, commonly used in signal processing 

229 and interpolation applications, for the specified sample positions. 

230 

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. 

238 

239 Returns 

240 ------- 

241 :class:`numpy.ndarray` 

242 *Sinc* kernel values evaluated at the specified sample positions. 

243 

244 Raises 

245 ------ 

246 AssertionError 

247 If ``a`` is less than 1. 

248 

249 References 

250 ---------- 

251 :cite:`Burger2009b` 

252 

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 """ 

261 

262 x = as_float_array(x) 

263 

264 attest(bool(a >= 1), '"a" must be equal or superior to 1!') 

265 

266 return np.where(np.abs(x) < a, np.sinc(x), 0) 

267 

268 

269def kernel_lanczos(x: ArrayLike, a: float = 3) -> NDArrayFloat: 

270 """ 

271 Return the *Lanczos* kernel evaluated at specified samples. 

272 

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. 

277 

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]`. 

285 

286 Returns 

287 ------- 

288 :class:`numpy.ndarray` 

289 The *Lanczos* kernel evaluated at specified samples. 

290 

291 References 

292 ---------- 

293 :cite:`Wikipedia2005b` 

294 

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 """ 

303 

304 x = as_float_array(x) 

305 

306 attest(bool(a >= 1), '"a" must be equal or superior to 1!') 

307 

308 return np.where(np.abs(x) < a, np.sinc(x) * np.sinc(x / a), 0) 

309 

310 

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. 

316 

317 Notable *cardinal spline* :math:`a` and :math:`b` parameterizations: 

318 

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})` 

323 

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. 

332 

333 Returns 

334 ------- 

335 :class:`numpy.ndarray` 

336 The *cardinal spline* kernel evaluated at specified samples. 

337 

338 References 

339 ---------- 

340 :cite:`Burger2009b` 

341 

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 """ 

350 

351 x = as_float_array(x) 

352 

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 

364 

365 return 1 / 6 * y 

366 

367 

368class KernelInterpolator: 

369 """ 

370 Perform kernel-based interpolation of a 1-D function. 

371 

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)`: 

376 

377 :math:`\\hat{g}(w_0) = [k * g](w_0) = \ 

378\\sum_{x=-\\infty}^{\\infty}k(w_0 - x)\\cdot g(x)` 

379 

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. 

398 

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` 

407 

408 Methods 

409 ------- 

410 - :meth:`~colour.KernelInterpolator.__init__` 

411 - :meth:`~colour.KernelInterpolator.__call__` 

412 

413 References 

414 ---------- 

415 :cite:`Burger2009b`, :cite:`Wikipedia2005b` 

416 

417 Examples 

418 -------- 

419 Interpolating a single numeric variable: 

420 

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... 

428 

429 Interpolating an `ArrayLike` variable: 

430 

431 >>> f([0.25, 0.75]) # doctest: +ELLIPSIS 

432 array([ 6.1806208..., 8.0823848...]) 

433 

434 Using a different *lanczos* kernel: 

435 

436 >>> f = KernelInterpolator(x, y, kernel=kernel_sinc) 

437 >>> f([0.25, 0.75]) # doctest: +ELLIPSIS 

438 array([ 6.5147317..., 8.3965466...]) 

439 

440 Using a different window size: 

441 

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 """ 

448 

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) 

462 

463 self._x_p: NDArrayFloat = np.array([]) 

464 self._y_p: NDArrayFloat = np.array([]) 

465 

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 

476 

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) 

483 

484 self._validate_dimensions() 

485 

486 @property 

487 def x(self) -> NDArrayFloat: 

488 """ 

489 Getter and setter for the independent :math:`x` variable. 

490 

491 Parameters 

492 ---------- 

493 value 

494 Value to set the independent :math:`x` variable with. 

495 

496 Returns 

497 ------- 

498 :class:`numpy.ndarray` 

499 Independent :math:`x` variable. 

500 """ 

501 

502 return self._x 

503 

504 @x.setter 

505 def x(self, value: ArrayLike) -> None: 

506 """Setter for the **self.x** property.""" 

507 

508 value = np.atleast_1d(value).astype(self._dtype) 

509 

510 attest( 

511 value.ndim == 1, 

512 '"x" independent variable must have exactly one dimension!', 

513 ) 

514 

515 value_interval = interval(value) 

516 

517 if value_interval.size != 1: 

518 runtime_warning( 

519 '"x" independent variable is not uniform, ' 

520 "unpredictable results may occur!" 

521 ) 

522 

523 self._x = as_array(value, self._dtype) 

524 

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 ) 

534 

535 @property 

536 def y(self) -> NDArrayFloat: 

537 """ 

538 Getter and setter for the dependent and already known :math:`y` variable. 

539 

540 Parameters 

541 ---------- 

542 value 

543 Value to set the dependent and already known :math:`y` variable 

544 with. 

545 

546 Returns 

547 ------- 

548 :class:`numpy.ndarray` 

549 Dependent and already known :math:`y` variable. 

550 """ 

551 

552 return self._y 

553 

554 @y.setter 

555 def y(self, value: ArrayLike) -> None: 

556 """Setter for the **self.y** property.""" 

557 

558 value = np.atleast_1d(value).astype(self._dtype) 

559 

560 attest( 

561 value.ndim == 1, 

562 '"y" dependent variable must have exactly one dimension!', 

563 ) 

564 

565 self._y = as_array(value, self._dtype) 

566 

567 if self._window is not None: 

568 self._y_p = np.pad(self._y, **self._padding_kwargs) 

569 

570 @property 

571 def window(self) -> float: 

572 """ 

573 Getter and setter for the filtering window size for the moving average. 

574 

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. 

579 

580 Parameters 

581 ---------- 

582 value 

583 Value to set the window with. 

584 

585 Returns 

586 ------- 

587 :class:`float` 

588 Window size for the moving average filter. 

589 """ 

590 

591 return self._window 

592 

593 @window.setter 

594 def window(self, value: float) -> None: 

595 """Setter for the **self.window** property.""" 

596 

597 attest(bool(value >= 1), '"window" must be equal to or greater than 1!') 

598 

599 self._window = value 

600 

601 # Triggering "self._x_p" update. 

602 if self._x is not None: 

603 self.x = self._x 

604 

605 # Triggering "self._y_p" update. 

606 if self._y is not None: 

607 self.y = self._y 

608 

609 @property 

610 def kernel(self) -> Callable: 

611 """ 

612 Getter and setter for the kernel callable for the interpolator. 

613 

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. 

619 

620 Returns 

621 ------- 

622 Callable 

623 Callable object to use as the interpolation kernel. 

624 

625 Raises 

626 ------ 

627 AssertionError 

628 If the provided value is not callable. 

629 """ 

630 

631 return self._kernel 

632 

633 @kernel.setter 

634 def kernel(self, value: Callable) -> None: 

635 """Setter for the **self.kernel** property.""" 

636 

637 attest( 

638 callable(value), 

639 f'"kernel" property: "{value}" is not callable!', 

640 ) 

641 

642 self._kernel = value 

643 

644 @property 

645 def kernel_kwargs(self) -> dict: 

646 """ 

647 Getter and setter for the kernel keyword arguments for the convolution 

648 operation. 

649 

650 Parameters 

651 ---------- 

652 value 

653 Value to set the keyword arguments to pass to the kernel function 

654 with. 

655 

656 Returns 

657 ------- 

658 :class:`dict` 

659 Keyword arguments to pass to the kernel function. 

660 

661 Raises 

662 ------ 

663 AssertionError 

664 If the provided value is not a :class:'dict` class instance. 

665 """ 

666 

667 return self._kernel_kwargs 

668 

669 @kernel_kwargs.setter 

670 def kernel_kwargs(self, value: dict) -> None: 

671 """Setter for the **self.kernel_kwargs** property.""" 

672 

673 attest( 

674 isinstance(value, dict), 

675 f'"kernel_kwargs" property: "{value}" type is not "dict"!', 

676 ) 

677 

678 self._kernel_kwargs = value 

679 

680 @property 

681 def padding_kwargs(self) -> dict: 

682 """ 

683 Getter and setter for the padding keyword arguments for edge handling. 

684 

685 Parameters 

686 ---------- 

687 value 

688 Value to set the keyword arguments to pass to the padding function 

689 when handling edges during interpolation. 

690 

691 Returns 

692 ------- 

693 :class:`dict` 

694 Keyword arguments to pass to the padding function when handling 

695 edges during interpolation. 

696 

697 Raises 

698 ------ 

699 AssertionError 

700 If the provided value is not a :class:`dict` class instance. 

701 """ 

702 

703 return self._padding_kwargs 

704 

705 @padding_kwargs.setter 

706 def padding_kwargs(self, value: dict) -> None: 

707 """Setter for the **self.padding_kwargs** property.""" 

708 

709 attest( 

710 isinstance(value, dict), 

711 f'"padding_kwargs" property: "{value}" type is not a "dict" instance!', 

712 ) 

713 

714 self._padding_kwargs = value 

715 

716 # Triggering "self._y_p" update. 

717 if self._y is not None: 

718 self.y = self._y 

719 

720 def __call__(self, x: ArrayLike) -> NDArrayFloat: 

721 """ 

722 Evaluate the interpolator at specified point(s). 

723 

724 Parameters 

725 ---------- 

726 x 

727 Point(s) to evaluate the interpolant at. 

728 

729 Returns 

730 ------- 

731 :class:`numpy.ndarray` 

732 Interpolated value(s). 

733 """ 

734 

735 x = as_float_array(x) 

736 

737 xi = self._evaluate(x) 

738 

739 return as_float(xi) 

740 

741 def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: 

742 """ 

743 Evaluate the interpolating polynomial at the specified point. 

744 

745 Parameters 

746 ---------- 

747 x 

748 Point at which to evaluate the interpolant. 

749 

750 Returns 

751 ------- 

752 :class:`numpy.ndarray` 

753 Interpolated values at the specified point. 

754 """ 

755 

756 self._validate_dimensions() 

757 self._validate_interpolation_range(x) 

758 

759 x_interval = interval(self._x)[0] 

760 x_f = np.floor(x / x_interval) 

761 

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

767 

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 ) 

776 

777 def _validate_dimensions(self) -> None: 

778 """ 

779 Validate that the dimensions of the variables are equal. 

780 

781 Raises 

782 ------ 

783 ValueError 

784 If the x and y variable dimensions do not match. 

785 """ 

786 

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 ) 

792 

793 raise ValueError(error) 

794 

795 def _validate_interpolation_range(self, x: NDArrayFloat) -> None: 

796 """ 

797 Validate that the specified interpolation point is within the valid 

798 interpolation range. 

799 

800 The interpolation point must be within the bounds defined by the first 

801 and last x-coordinates of the interpolator's data. 

802 

803 Parameters 

804 ---------- 

805 x 

806 Point to validate for interpolation range compliance. 

807 

808 Raises 

809 ------ 

810 ValueError 

811 If the point is outside the valid interpolation range. 

812 """ 

813 

814 below_interpolation_range = x < self._x[0] 

815 above_interpolation_range = x > self._x[-1] 

816 

817 if below_interpolation_range.any(): 

818 error = f'"{x}" is below interpolation range.' 

819 

820 raise ValueError(error) 

821 

822 if above_interpolation_range.any(): 

823 error = f'"{x}" is above interpolation range.' 

824 

825 raise ValueError(error) 

826 

827 

828class NearestNeighbourInterpolator(KernelInterpolator): 

829 """ 

830 Perform nearest-neighbour interpolation on discrete data. 

831 

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. 

836 

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. 

852 

853 Methods 

854 ------- 

855 - :meth:`~colour.NearestNeighbourInterpolator.__init__` 

856 """ 

857 

858 def __init__(self, *args: Any, **kwargs: Any) -> None: 

859 kwargs["kernel"] = kernel_nearest_neighbour 

860 kwargs.pop("kernel_kwargs", None) 

861 

862 super().__init__(*args, **kwargs) 

863 

864 

865class LinearInterpolator: 

866 """ 

867 Perform linear interpolation of a 1-D function. 

868 

869 This class provides a wrapper around NumPy's linear interpolation 

870 functionality for interpolating between specified data points. 

871 

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. 

882 

883 Attributes 

884 ---------- 

885 - :attr:`~colour.LinearInterpolator.x` 

886 - :attr:`~colour.LinearInterpolator.y` 

887 

888 Methods 

889 ------- 

890 - :meth:`~colour.LinearInterpolator.__init__` 

891 - :meth:`~colour.LinearInterpolator.__call__` 

892 

893 Notes 

894 ----- 

895 - This class is a wrapper around *numpy.interp* definition. 

896 

897 Examples 

898 -------- 

899 Interpolating a single numeric variable: 

900 

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... 

906 

907 Interpolating an `ArrayLike` variable: 

908 

909 >>> f([0.25, 0.75]) 

910 array([ 6.7825, 8.5075]) 

911 """ 

912 

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) 

922 

923 self._x: NDArrayFloat = np.array([]) 

924 self._y: NDArrayFloat = np.array([]) 

925 self._dtype: Type[DTypeReal] = dtype 

926 

927 self.x = x 

928 self.y = y 

929 

930 self._validate_dimensions() 

931 

932 @property 

933 def x(self) -> NDArrayFloat: 

934 """ 

935 Getter and setter for the independent :math:`x` variable. 

936 

937 Parameters 

938 ---------- 

939 value 

940 Value to set the independent :math:`x` variable with. 

941 

942 Returns 

943 ------- 

944 :class:`numpy.ndarray` 

945 Independent :math:`x` variable. 

946 

947 Raises 

948 ------ 

949 AssertionError 

950 If the provided value has not exactly one dimension. 

951 """ 

952 

953 return self._x 

954 

955 @x.setter 

956 def x(self, value: ArrayLike) -> None: 

957 """Setter for the **self.x** property.""" 

958 

959 value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype)) 

960 

961 attest( 

962 value.ndim == 1, 

963 '"x" independent variable must have exactly one dimension!', 

964 ) 

965 

966 self._x = value 

967 

968 @property 

969 def y(self) -> NDArrayFloat: 

970 """ 

971 Getter and setter for the dependent and already known 

972 :math:`y` variable. 

973 

974 Parameters 

975 ---------- 

976 value 

977 Value to set the dependent and already known :math:`y` variable 

978 with. 

979 

980 Returns 

981 ------- 

982 :class:`numpy.ndarray` 

983 Dependent and already known :math:`y` variable. 

984 

985 Raises 

986 ------ 

987 AssertionError 

988 If the provided value has not exactly one dimension. 

989 """ 

990 

991 return self._y 

992 

993 @y.setter 

994 def y(self, value: ArrayLike) -> None: 

995 """Setter for the **self.y** property.""" 

996 

997 value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype)) 

998 

999 attest( 

1000 value.ndim == 1, 

1001 '"y" dependent variable must have exactly one dimension!', 

1002 ) 

1003 

1004 self._y = value 

1005 

1006 def __call__(self, x: ArrayLike) -> NDArrayFloat: 

1007 """ 

1008 Evaluate the interpolating polynomial at specified point(s). 

1009 

1010 

1011 Parameters 

1012 ---------- 

1013 x 

1014 Point(s) to evaluate the interpolant at. 

1015 

1016 Returns 

1017 ------- 

1018 :class:`numpy.ndarray` 

1019 Interpolated value(s). 

1020 """ 

1021 

1022 x = as_float_array(x) 

1023 

1024 xi = self._evaluate(x) 

1025 

1026 return as_float(xi) 

1027 

1028 def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: 

1029 """ 

1030 Perform the interpolating polynomial evaluation at specified points. 

1031 

1032 Parameters 

1033 ---------- 

1034 x 

1035 Points to evaluate the interpolant at. 

1036 

1037 Returns 

1038 ------- 

1039 :class:`numpy.ndarray` 

1040 Interpolated points values. 

1041 """ 

1042 

1043 self._validate_dimensions() 

1044 self._validate_interpolation_range(x) 

1045 

1046 return np.interp(x, self._x, self._y) 

1047 

1048 def _validate_dimensions(self) -> None: 

1049 """Validate that the variables dimensions are the same.""" 

1050 

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 ) 

1056 

1057 raise ValueError(error) 

1058 

1059 def _validate_interpolation_range(self, x: NDArrayFloat) -> None: 

1060 """Validate specified point to be in interpolation range.""" 

1061 

1062 below_interpolation_range = x < self._x[0] 

1063 above_interpolation_range = x > self._x[-1] 

1064 

1065 if below_interpolation_range.any(): 

1066 error = f'"{x}" is below interpolation range.' 

1067 

1068 raise ValueError(error) 

1069 

1070 if above_interpolation_range.any(): 

1071 error = f'"{x}" is above interpolation range.' 

1072 

1073 raise ValueError(error) 

1074 

1075 

1076class SpragueInterpolator: 

1077 """ 

1078 Perform fifth-order polynomial interpolation using the *Sprague (1880)* 

1079 method for uniformly spaced data. 

1080 

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. 

1087 

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. 

1098 

1099 Attributes 

1100 ---------- 

1101 - :attr:`~colour.SpragueInterpolator.x` 

1102 - :attr:`~colour.SpragueInterpolator.y` 

1103 

1104 Methods 

1105 ------- 

1106 - :meth:`~colour.SpragueInterpolator.__init__` 

1107 - :meth:`~colour.SpragueInterpolator.__call__` 

1108 

1109 Notes 

1110 ----- 

1111 - The minimum number :math:`k` of data points required along the 

1112 interpolation axis is :math:`k=6`. 

1113 

1114 References 

1115 ---------- 

1116 :cite:`CIETC1-382005f`, :cite:`Westland2012h` 

1117 

1118 Examples 

1119 -------- 

1120 Interpolating a single numeric variable: 

1121 

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... 

1127 

1128 Interpolating an `ArrayLike` variable: 

1129 

1130 >>> f([0.25, 0.75]) # doctest: +ELLIPSIS 

1131 array([ 6.7295161..., 7.8140625...]) 

1132 """ 

1133 

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. 

1145 

1146 SPRAGUE_C_COEFFICIENTS, (4, 6) 

1147 

1148 References 

1149 ---------- 

1150 :cite:`CIETC1-382005h` 

1151 """ 

1152 

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) 

1162 

1163 self._xp: NDArrayFloat = np.array([]) 

1164 self._yp: NDArrayFloat = np.array([]) 

1165 

1166 self._x: NDArrayFloat = np.array([]) 

1167 self._y: NDArrayFloat = np.array([]) 

1168 self._dtype: Type[DTypeReal] = dtype 

1169 

1170 self.x = x 

1171 self.y = y 

1172 

1173 self._validate_dimensions() 

1174 

1175 @property 

1176 def x(self) -> NDArrayFloat: 

1177 """ 

1178 Getter and setter for the independent :math:`x` variable. 

1179 

1180 Parameters 

1181 ---------- 

1182 value 

1183 Value to set the independent :math:`x` variable with. 

1184 

1185 Returns 

1186 ------- 

1187 :class:`numpy.ndarray` 

1188 Independent :math:`x` variable. 

1189 

1190 Raises 

1191 ------ 

1192 AssertionError 

1193 If the provided value has not exactly one dimension. 

1194 """ 

1195 

1196 return self._x 

1197 

1198 @x.setter 

1199 def x(self, value: ArrayLike) -> None: 

1200 """Setter for the **self.x** property.""" 

1201 

1202 value = as_array(np.atleast_1d(value), self._dtype) 

1203 

1204 attest( 

1205 value.ndim == 1, 

1206 '"x" independent variable must have exactly one dimension!', 

1207 ) 

1208 

1209 self._x = value 

1210 

1211 value_interval = interval(self._x)[0] 

1212 

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 

1217 

1218 self._xp = np.concatenate( 

1219 [ 

1220 as_array([xp1, xp2], self._dtype), 

1221 value, 

1222 as_array([xp3, xp4], self._dtype), 

1223 ] 

1224 ) 

1225 

1226 @property 

1227 def y(self) -> NDArrayFloat: 

1228 """ 

1229 Getter and setter for the dependent and already known 

1230 :math:`y` variable. 

1231 

1232 Parameters 

1233 ---------- 

1234 value 

1235 Value to set the dependent and already known :math:`y` variable 

1236 with. 

1237 

1238 Returns 

1239 ------- 

1240 :class:`numpy.ndarray` 

1241 Dependent and already known :math:`y` variable. 

1242 

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 """ 

1249 

1250 return self._y 

1251 

1252 @y.setter 

1253 def y(self, value: ArrayLike) -> None: 

1254 """Setter for the **self.y** property.""" 

1255 

1256 value = as_array(np.atleast_1d(value), self._dtype) 

1257 

1258 attest( 

1259 value.ndim == 1, 

1260 '"y" dependent variable must have exactly one dimension!', 

1261 ) 

1262 

1263 attest( 

1264 len(value) >= 6, 

1265 '"y" dependent variable values count must be equal to or greater than 6!', 

1266 ) 

1267 

1268 self._y = value 

1269 

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 ) 

1278 

1279 self._yp = np.concatenate( 

1280 [ 

1281 as_array([yp1, yp2], self._dtype), 

1282 value, 

1283 as_array([yp3, yp4], self._dtype), 

1284 ] 

1285 ) 

1286 

1287 def __call__(self, x: ArrayLike) -> NDArrayFloat: 

1288 """ 

1289 Evaluate the interpolating polynomial at specified point(s). 

1290 

1291 Parameters 

1292 ---------- 

1293 x 

1294 Point(s) to evaluate the interpolant at. 

1295 

1296 Returns 

1297 ------- 

1298 :class:`numpy.ndarray` 

1299 Interpolated value(s). 

1300 """ 

1301 

1302 x = as_float_array(x) 

1303 

1304 xi = self._evaluate(x) 

1305 

1306 return as_float(xi) 

1307 

1308 def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: 

1309 """ 

1310 Perform the interpolating polynomial evaluation at specified point. 

1311 

1312 Parameters 

1313 ---------- 

1314 x 

1315 Point to evaluate the interpolant at. 

1316 

1317 Returns 

1318 ------- 

1319 :class:`numpy.ndarray` 

1320 Interpolated point values. 

1321 """ 

1322 

1323 self._validate_dimensions() 

1324 self._validate_interpolation_range(x) 

1325 

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

1329 

1330 r = self._yp 

1331 

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 

1343 

1344 # Fancy vector code here... use underlying numpy structures to accelerate 

1345 # parts of the linear algebra. 

1346 

1347 y = r[i] + (a.reshape(5, -1) * X ** np.arange(1, 6).reshape(-1, 1)).sum(axis=0) 

1348 

1349 if y.size == 1: 

1350 return y[0] 

1351 

1352 return y 

1353 

1354 def _validate_dimensions(self) -> None: 

1355 """Validate that the variables dimensions are the same.""" 

1356 

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 ) 

1362 

1363 raise ValueError(error) 

1364 

1365 def _validate_interpolation_range(self, x: NDArrayFloat) -> None: 

1366 """Validate specified point to be in interpolation range.""" 

1367 

1368 below_interpolation_range = x < self._x[0] 

1369 above_interpolation_range = x > self._x[-1] 

1370 

1371 if below_interpolation_range.any(): 

1372 error = f'"{x}" is below interpolation range.' 

1373 

1374 raise ValueError(error) 

1375 

1376 if above_interpolation_range.any(): 

1377 error = f'"{x}" is above interpolation range.' 

1378 

1379 raise ValueError(error) 

1380 

1381 

1382class CubicSplineInterpolator(scipy.interpolate.interp1d): 

1383 """ 

1384 Perform cubic spline interpolation on one-dimensional data. 

1385 

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. 

1391 

1392 Methods 

1393 ------- 

1394 - :meth:`~colour.CubicSplineInterpolator.__init__` 

1395 

1396 Notes 

1397 ----- 

1398 - This class is a wrapper around *scipy.interpolate.interp1d* class. 

1399 """ 

1400 

1401 def __init__(self, *args: Any, **kwargs: Any) -> None: 

1402 kwargs["kind"] = "cubic" 

1403 super().__init__(*args, **kwargs) 

1404 

1405 

1406class PchipInterpolator(scipy.interpolate.PchipInterpolator): 

1407 """ 

1408 Interpolate a 1-D function using Piecewise Cubic Hermite Interpolating 

1409 Polynomial (PCHIP) interpolation. 

1410 

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. 

1416 

1417 Attributes 

1418 ---------- 

1419 - :attr:`~colour.PchipInterpolator.y` 

1420 

1421 Methods 

1422 ------- 

1423 - :meth:`~colour.PchipInterpolator.__init__` 

1424 

1425 Notes 

1426 ----- 

1427 - This class is a wrapper around *scipy.interpolate.PchipInterpolator* 

1428 class. 

1429 """ 

1430 

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) 

1433 

1434 self._y: NDArrayFloat = as_float_array(y) 

1435 

1436 @property 

1437 def y(self) -> NDArrayFloat: 

1438 """ 

1439 Getter and setter for the dependent and already known 

1440 :math:`y` variable. 

1441 

1442 Parameters 

1443 ---------- 

1444 value 

1445 Value to set the dependent and already known :math:`y` variable 

1446 with. 

1447 

1448 Returns 

1449 ------- 

1450 :class:`numpy.ndarray` 

1451 Dependent and already known :math:`y` variable. 

1452 """ 

1453 

1454 return self._y 

1455 

1456 @y.setter 

1457 def y(self, value: ArrayLike) -> None: 

1458 """Setter for the **self.y** property.""" 

1459 

1460 self._y = as_float_array(value) 

1461 

1462 

1463class NullInterpolator: 

1464 """ 

1465 Implement 1-D function null interpolation. 

1466 

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. 

1472 

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. 

1489 

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` 

1497 

1498 Methods 

1499 ------- 

1500 - :meth:`~colour.NullInterpolator.__init__` 

1501 - :meth:`~colour.NullInterpolator.__call__` 

1502 

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 """ 

1516 

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) 

1529 

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 

1536 

1537 self.x = x 

1538 self.y = y 

1539 self.absolute_tolerance = absolute_tolerance 

1540 self.relative_tolerance = relative_tolerance 

1541 self.default = default 

1542 

1543 self._validate_dimensions() 

1544 

1545 @property 

1546 def x(self) -> NDArrayFloat: 

1547 """ 

1548 Getter and setter for the independent :math:`x` variable. 

1549 

1550 Parameters 

1551 ---------- 

1552 value 

1553 Value to set the independent :math:`x` variable with. 

1554 

1555 Returns 

1556 ------- 

1557 :class:`numpy.ndarray` 

1558 Independent :math:`x` variable. 

1559 

1560 Raises 

1561 ------ 

1562 AssertionError 

1563 If the provided value has not exactly one dimension. 

1564 """ 

1565 

1566 return self._x 

1567 

1568 @x.setter 

1569 def x(self, value: ArrayLike) -> None: 

1570 """Setter for the **self.x** property.""" 

1571 

1572 value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype)) 

1573 

1574 attest( 

1575 value.ndim == 1, 

1576 '"x" independent variable must have exactly one dimension!', 

1577 ) 

1578 

1579 self._x = value 

1580 

1581 @property 

1582 def y(self) -> NDArrayFloat: 

1583 """ 

1584 Getter and setter for the dependent and already known 

1585 :math:`y` variable. 

1586 

1587 Parameters 

1588 ---------- 

1589 value 

1590 Value to set the dependent and already known :math:`y` variable 

1591 with. 

1592 

1593 Returns 

1594 ------- 

1595 :class:`numpy.ndarray` 

1596 Dependent and already known :math:`y` variable. 

1597 

1598 Raises 

1599 ------ 

1600 AssertionError 

1601 If the provided value has not exactly one dimension. 

1602 """ 

1603 

1604 return self._y 

1605 

1606 @y.setter 

1607 def y(self, value: ArrayLike) -> None: 

1608 """Setter for the **self.y** property.""" 

1609 

1610 value = cast("NDArrayFloat", np.atleast_1d(value).astype(self._dtype)) 

1611 

1612 attest( 

1613 value.ndim == 1, 

1614 '"y" dependent variable must have exactly one dimension!', 

1615 ) 

1616 

1617 self._y = value 

1618 

1619 @property 

1620 def relative_tolerance(self) -> float: 

1621 """ 

1622 Getter and setter property for the relative tolerance for numerical 

1623 comparisons. 

1624 

1625 Parameters 

1626 ---------- 

1627 value 

1628 Value to set the relative tolerance for numerical comparisons with. 

1629 

1630 Returns 

1631 ------- 

1632 :class:`float` 

1633 Relative tolerance for numerical comparisons. 

1634 

1635 Raises 

1636 ------ 

1637 AssertionError 

1638 If the value is not numeric. 

1639 """ 

1640 

1641 return self._relative_tolerance 

1642 

1643 @relative_tolerance.setter 

1644 def relative_tolerance(self, value: float) -> None: 

1645 """Setter for the **self.relative_tolerance** property.""" 

1646 

1647 attest( 

1648 is_numeric(value), 

1649 '"relative_tolerance" variable must be a "numeric"!', 

1650 ) 

1651 

1652 self._relative_tolerance = as_float_scalar(value) 

1653 

1654 @property 

1655 def absolute_tolerance(self) -> float: 

1656 """ 

1657 Getter and setter property for the absolute tolerance for numerical 

1658 comparisons. 

1659 

1660 Parameters 

1661 ---------- 

1662 value 

1663 Value to set the absolute tolerance for numerical comparisons with. 

1664 

1665 Returns 

1666 ------- 

1667 :class:`float` 

1668 Absolute tolerance for numerical comparisons. 

1669 

1670 Raises 

1671 ------ 

1672 AssertionError 

1673 If the value is not numeric. 

1674 """ 

1675 

1676 return self._absolute_tolerance 

1677 

1678 @absolute_tolerance.setter 

1679 def absolute_tolerance(self, value: float) -> None: 

1680 """Setter for the **self.absolute_tolerance** property.""" 

1681 

1682 attest( 

1683 is_numeric(value), 

1684 '"absolute_tolerance" variable must be a "numeric"!', 

1685 ) 

1686 

1687 self._absolute_tolerance = as_float_scalar(value) 

1688 

1689 @property 

1690 def default(self) -> float: 

1691 """ 

1692 Getter and setter property for the default value for call outside 

1693 tolerances. 

1694 

1695 Parameters 

1696 ---------- 

1697 value 

1698 Value to set the default value with for call outside tolerances. 

1699 

1700 Returns 

1701 ------- 

1702 :class:`float` 

1703 Default value for call outside tolerances. 

1704 

1705 Raises 

1706 ------ 

1707 AssertionError 

1708 If the value is not numeric. 

1709 """ 

1710 

1711 return self._default 

1712 

1713 @default.setter 

1714 def default(self, value: float) -> None: 

1715 """Setter for the **self.default** property.""" 

1716 

1717 attest(is_numeric(value), '"default" variable must be a "numeric"!') 

1718 

1719 self._default = value 

1720 

1721 def __call__(self, x: ArrayLike) -> NDArrayFloat: 

1722 """ 

1723 Evaluate the interpolator at specified point(s). 

1724 

1725 

1726 Parameters 

1727 ---------- 

1728 x 

1729 Point(s) to evaluate the interpolant at. 

1730 

1731 Returns 

1732 ------- 

1733 :class:`numpy.ndarray` 

1734 Interpolated value(s). 

1735 """ 

1736 

1737 x = as_float_array(x) 

1738 

1739 xi = self._evaluate(x) 

1740 

1741 return as_float(xi) 

1742 

1743 def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: 

1744 """ 

1745 Perform the interpolator evaluation at specified points. 

1746 

1747 Parameters 

1748 ---------- 

1749 x 

1750 Points to evaluate the interpolant at. 

1751 

1752 Returns 

1753 ------- 

1754 :class:`numpy.ndarray` 

1755 Interpolated points values. 

1756 """ 

1757 

1758 self._validate_dimensions() 

1759 self._validate_interpolation_range(x) 

1760 

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 

1771 

1772 return np.squeeze(values) 

1773 

1774 def _validate_dimensions(self) -> None: 

1775 """Validate that the variables dimensions are the same.""" 

1776 

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 ) 

1782 

1783 raise ValueError(error) 

1784 

1785 def _validate_interpolation_range(self, x: NDArrayFloat) -> None: 

1786 """Validate specified point to be in interpolation range.""" 

1787 

1788 below_interpolation_range = x < self._x[0] 

1789 above_interpolation_range = x > self._x[-1] 

1790 

1791 if below_interpolation_range.any(): 

1792 error = f'"{x}" is below interpolation range.' 

1793 

1794 raise ValueError(error) 

1795 

1796 if above_interpolation_range.any(): 

1797 error = f'"{x}" is above interpolation range.' 

1798 

1799 raise ValueError(error) 

1800 

1801 

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`. 

1806 

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`. 

1814 

1815 Returns 

1816 ------- 

1817 :class:`numpy.ndarray` 

1818 Array of *Lagrange coefficients* computed at point :math:`r`. 

1819 

1820 References 

1821 ---------- 

1822 :cite:`Fairman1985b`, :cite:`Wikipedia2003a` 

1823 

1824 Examples 

1825 -------- 

1826 >>> lagrange_coefficients(0.1) 

1827 array([ 0.8265, 0.2755, -0.1305, 0.0285]) 

1828 """ 

1829 

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

1835 

1836 return np.array(L_n) 

1837 

1838 

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. 

1843 

1844 Parameters 

1845 ---------- 

1846 V_xyz 

1847 :math:`V_{xyz}` values to interpolate. 

1848 table 

1849 4-Dimensional (NxNxNx3) interpolation table. 

1850 

1851 Returns 

1852 ------- 

1853 :class:`numpy.ndarray` 

1854 Interpolated :math:`V_{xyz}` values. 

1855 

1856 References 

1857 ---------- 

1858 :cite:`Bourkeb` 

1859 

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 """ 

1887 

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

1891 

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 

1896 

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) 

1900 

1901 # Relative coordinates (fractional part) 

1902 frac = V_xyz_s - i_f 

1903 

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] 

1907 

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 

1911 

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] 

1921 

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 ) 

1933 

1934 return result.reshape(original_shape) 

1935 

1936 

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. 

1941 

1942 Parameters 

1943 ---------- 

1944 V_xyz 

1945 :math:`V_{xyz}` values to interpolate. 

1946 table 

1947 4-Dimensional (NxNxNx3) interpolation table. 

1948 

1949 Returns 

1950 ------- 

1951 :class:`numpy.ndarray` 

1952 Interpolated :math:`V_{xyz}` values. 

1953 

1954 References 

1955 ---------- 

1956 :cite:`Kirk2006` 

1957 

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 """ 

1985 

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

1989 

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 

1994 

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) 

1998 

1999 # Relative coordinates 

2000 r = V_xyz_s - i_f 

2001 x, y, z = r[:, 0], r[:, 1], r[:, 2] 

2002 

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] 

2006 

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] 

2016 

2017 # Expand dimensions for broadcasting 

2018 x = x[:, np.newaxis] 

2019 y = y[:, np.newaxis] 

2020 z = z[:, np.newaxis] 

2021 

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 ) 

2041 

2042 return xyz_o.reshape(original_shape) 

2043 

2044 

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. 

2053 

2054References 

2055---------- 

2056:cite:`Bourkeb`, :cite:`Kirk2006` 

2057""" 

2058 

2059 

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. 

2068 

2069 Interpolate the input :math:`V_{xyz}` values through either trilinear 

2070 or tetrahedral interpolation methods using the specified lookup table. 

2071 

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. 

2084 

2085 Returns 

2086 ------- 

2087 :class:`numpy.ndarray` 

2088 Interpolated :math:`V_{xyz}` values with the same shape as the 

2089 input array. 

2090 

2091 References 

2092 ---------- 

2093 :cite:`Bourkeb`, :cite:`Kirk2006` 

2094 

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 """ 

2127 

2128 method = validate_method(method, tuple(TABLE_INTERPOLATION_METHODS)) 

2129 

2130 return TABLE_INTERPOLATION_METHODS[method](V_xyz, table)