Coverage for colour/continuous/abstract.py: 100%
123 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2Abstract Continuous Function
3============================
5Define an abstract base class for continuous mathematical functions with
6support for interpolation, extrapolation, and arithmetical operations:
8- :class:`colour.continuous.AbstractContinuousFunction`
9"""
11from __future__ import annotations
13import typing
14from abc import ABC, abstractmethod
15from copy import deepcopy
17import numpy as np
19if typing.TYPE_CHECKING:
20 from colour.hints import (
21 ArrayLike,
22 Callable,
23 DTypeFloat,
24 Generator,
25 Literal,
26 NDArrayFloat,
27 ProtocolExtrapolator,
28 ProtocolInterpolator,
29 Real,
30 Self,
31 Type,
32 )
34from colour.utilities import (
35 MixinCallback,
36 as_float,
37 attest,
38 closest,
39 is_uniform,
40 optional,
41)
43__author__ = "Colour Developers"
44__copyright__ = "Copyright 2013 Colour Developers"
45__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
46__maintainer__ = "Colour Developers"
47__email__ = "colour-developers@colour-science.org"
48__status__ = "Production"
50__all__ = [
51 "AbstractContinuousFunction",
52]
55class AbstractContinuousFunction(ABC, MixinCallback):
56 """
57 Define the base class for an abstract continuous function.
59 This is an :class:`ABCMeta` abstract class that must be inherited by
60 sub-classes.
62 The sub-classes are expected to implement the
63 :meth:`colour.continuous.AbstractContinuousFunction.function` method so
64 that evaluating the function for any independent domain variable
65 :math:`x \\in\\mathbb{R}` returns a corresponding range variable
66 :math:`y \\in\\mathbb{R}`. A conventional implementation adopts an
67 interpolating function encapsulated inside an extrapolating function.
68 The resulting function independent domain, stored as discrete values in
69 the :attr:`colour.continuous.AbstractContinuousFunction.domain` attribute,
70 corresponds with the function dependent and already known range stored in
71 the :attr:`colour.continuous.AbstractContinuousFunction.range` property.
73 Parameters
74 ----------
75 name
76 Continuous function name.
78 Attributes
79 ----------
80 - :attr:`~colour.continuous.AbstractContinuousFunction.name`
81 - :attr:`~colour.continuous.AbstractContinuousFunction.dtype`
82 - :attr:`~colour.continuous.AbstractContinuousFunction.domain`
83 - :attr:`~colour.continuous.AbstractContinuousFunction.range`
84 - :attr:`~colour.continuous.AbstractContinuousFunction.interpolator`
85 - :attr:`~colour.continuous.\
86AbstractContinuousFunction.interpolator_kwargs`
87 - :attr:`~colour.continuous.AbstractContinuousFunction.extrapolator`
88 - :attr:`~colour.continuous.\
89AbstractContinuousFunction.extrapolator_kwargs`
90 - :attr:`~colour.continuous.AbstractContinuousFunction.function`
92 Methods
93 -------
94 - :meth:`~colour.continuous.AbstractContinuousFunction.__init__`
95 - :meth:`~colour.continuous.AbstractContinuousFunction.__str__`
96 - :meth:`~colour.continuous.AbstractContinuousFunction.__repr__`
97 - :meth:`~colour.continuous.AbstractContinuousFunction.__hash__`
98 - :meth:`~colour.continuous.AbstractContinuousFunction.__getitem__`
99 - :meth:`~colour.continuous.AbstractContinuousFunction.__setitem__`
100 - :meth:`~colour.continuous.AbstractContinuousFunction.__contains__`
101 - :meth:`~colour.continuous.AbstractContinuousFunction.__iter__`
102 - :meth:`~colour.continuous.AbstractContinuousFunction.__len__`
103 - :meth:`~colour.continuous.AbstractContinuousFunction.__eq__`
104 - :meth:`~colour.continuous.AbstractContinuousFunction.__ne__`
105 - :meth:`~colour.continuous.AbstractContinuousFunction.__iadd__`
106 - :meth:`~colour.continuous.AbstractContinuousFunction.__add__`
107 - :meth:`~colour.continuous.AbstractContinuousFunction.__isub__`
108 - :meth:`~colour.continuous.AbstractContinuousFunction.__sub__`
109 - :meth:`~colour.continuous.AbstractContinuousFunction.__imul__`
110 - :meth:`~colour.continuous.AbstractContinuousFunction.__mul__`
111 - :meth:`~colour.continuous.AbstractContinuousFunction.__idiv__`
112 - :meth:`~colour.continuous.AbstractContinuousFunction.__div__`
113 - :meth:`~colour.continuous.AbstractContinuousFunction.__ipow__`
114 - :meth:`~colour.continuous.AbstractContinuousFunction.__pow__`
115 - :meth:`~colour.continuous.AbstractContinuousFunction.\
116arithmetical_operation`
117 - :meth:`~colour.continuous.AbstractContinuousFunction.fill_nan`
118 - :meth:`~colour.continuous.AbstractContinuousFunction.domain_distance`
119 - :meth:`~colour.continuous.AbstractContinuousFunction.is_uniform`
120 - :meth:`~colour.continuous.AbstractContinuousFunction.copy`
121 """
123 def __init__(self, name: str | None = None) -> None:
124 super().__init__()
126 self._name: str = f"{self.__class__.__name__} ({id(self)})"
127 self.name = optional(name, self._name)
129 @property
130 def name(self) -> str:
131 """
132 Getter and setter for the abstract continuous function name.
134 Parameters
135 ----------
136 value
137 Value to set the abstract continuous function name with.
139 Returns
140 -------
141 :class:`str`
142 Abstract continuous function name.
143 """
145 return self._name
147 @name.setter
148 def name(self, value: str) -> None:
149 """Setter for the **self.name** property."""
151 attest(
152 isinstance(value, str),
153 f'"name" property: "{value}" type is not "str"!',
154 )
156 self._name = value
158 @property
159 @abstractmethod
160 def dtype(self) -> Type[DTypeFloat]:
161 """
162 Getter and setter for the abstract continuous function dtype.
164 This property must be reimplemented by sub-classes.
166 Parameters
167 ----------
168 value
169 Value to set the abstract continuous function dtype with.
171 Returns
172 -------
173 Type[DTypeFloat]
174 Abstract continuous function dtype.
175 """
177 ... # pragma: no cover
179 @dtype.setter
180 @abstractmethod
181 def dtype(self, value: Type[DTypeFloat]) -> None:
182 """
183 Setter for the **self.dtype** property, must be reimplemented by
184 sub-classes.
185 """
187 ... # pragma: no cover
189 @property
190 @abstractmethod
191 def domain(self) -> NDArrayFloat:
192 """
193 Getter and setter for the abstract continuous function's independent
194 domain variable :math:`x`.
196 This property must be reimplemented by sub-classes.
198 Parameters
199 ----------
200 value
201 Value to set the abstract continuous function independent domain
202 variable :math:`x` with.
204 Returns
205 -------
206 :class:`numpy.ndarray`
207 Abstract continuous function independent domain variable
208 :math:`x`.
209 """
211 ... # pragma: no cover
213 @domain.setter
214 @abstractmethod
215 def domain(self, value: ArrayLike) -> None:
216 """
217 Setter for the **self.domain** property, must be reimplemented by
218 sub-classes.
219 """
221 ... # pragma: no cover
223 @property
224 @abstractmethod
225 def range(self) -> NDArrayFloat:
226 """
227 Getter and setter for the abstract continuous function's range
228 variable :math:`y`.
230 This property must be reimplemented by sub-classes.
232 Parameters
233 ----------
234 value
235 Value to set the abstract continuous function's range variable
236 :math:`y` with.
238 Returns
239 -------
240 :class:`numpy.ndarray`
241 Abstract continuous function's range variable :math:`y`.
242 """
244 ... # pragma: no cover
246 @range.setter
247 @abstractmethod
248 def range(self, value: ArrayLike) -> None:
249 """
250 Setter for the **self.range** property, must be reimplemented by
251 sub-classes.
252 """
254 ... # pragma: no cover
256 @property
257 @abstractmethod
258 def interpolator(self) -> Type[ProtocolInterpolator]:
259 """
260 Getter and setter for the abstract continuous function interpolator
261 type.
263 This property must be reimplemented by sub-classes.
265 Parameters
266 ----------
267 value
268 Value to set the abstract continuous function interpolator type
269 with.
271 Returns
272 -------
273 Type[ProtocolInterpolator]
274 Abstract continuous function interpolator type.
275 """
277 ... # pragma: no cover
279 @interpolator.setter
280 @abstractmethod
281 def interpolator(self, value: Type[ProtocolInterpolator]) -> None:
282 """
283 Setter for the **self.interpolator** property, must be reimplemented by
284 sub-classes.
285 """
287 ... # pragma: no cover
289 @property
290 @abstractmethod
291 def interpolator_kwargs(self) -> dict:
292 """
293 Getter and setter for the interpolator instantiation time arguments.
295 This property must be reimplemented by sub-classes.
297 Parameters
298 ----------
299 value
300 Value to set the abstract continuous function interpolator
301 instantiation time arguments to.
303 Returns
304 -------
305 :class:`dict`
306 Abstract continuous function interpolator instantiation time
307 arguments.
308 """
310 ... # pragma: no cover
312 @interpolator_kwargs.setter
313 @abstractmethod
314 def interpolator_kwargs(self, value: dict) -> None:
315 """
316 Setter for the **self.interpolator_kwargs** property, must be
317 reimplemented by sub-classes.
318 """
320 ... # pragma: no cover
322 @property
323 @abstractmethod
324 def extrapolator(self) -> Type[ProtocolExtrapolator]:
325 """
326 Getter and setter for the abstract continuous function extrapolator
327 type.
329 This property must be reimplemented by sub-classes.
331 Parameters
332 ----------
333 value
334 Value to set the abstract continuous function extrapolator type
335 with.
337 Returns
338 -------
339 Type[ProtocolExtrapolator]
340 Abstract continuous function extrapolator type.
341 """
343 ... # pragma: no cover
345 @extrapolator.setter
346 @abstractmethod
347 def extrapolator(self, value: Type[ProtocolExtrapolator]) -> None:
348 """
349 Setter for the **self.extrapolator** property, must be reimplemented by
350 sub-classes.
351 """
353 ... # pragma: no cover
355 @property
356 @abstractmethod
357 def extrapolator_kwargs(self) -> dict:
358 """
359 Getter and setter for the abstract continuous function extrapolator
360 instantiation time arguments.
362 This property must be reimplemented by sub-classes.
364 Parameters
365 ----------
366 value
367 Value to set the abstract continuous function extrapolator
368 instantiation time arguments to.
370 Returns
371 -------
372 :class:`dict`
373 Abstract continuous function extrapolator instantiation time
374 arguments.
375 """
377 ... # pragma: no cover
379 @extrapolator_kwargs.setter
380 @abstractmethod
381 def extrapolator_kwargs(self, value: dict) -> None:
382 """
383 Setter for the **self.extrapolator_kwargs** property, must be
384 reimplemented by sub-classes.
385 """
387 ... # pragma: no cover
389 @property
390 @abstractmethod
391 def function(self) -> Callable:
392 """
393 Getter for the abstract continuous function callable.
395 This property must be reimplemented by sub-classes.
397 Returns
398 -------
399 Callable
400 Abstract continuous function callable.
401 """
403 ... # pragma: no cover
405 @abstractmethod
406 def __str__(self) -> str:
407 """
408 Return a formatted string representation of the abstract continuous
409 function.
411 This method must be reimplemented by sub-classes.
413 Returns
414 -------
415 :class:`str`
416 Formatted string representation.
417 """
419 return super().__repr__() # pragma: no cover
421 @abstractmethod
422 def __repr__(self) -> str:
423 """
424 Return an evaluable string representation of the abstract continuous
425 function, must be reimplemented by sub-classes.
427 Returns
428 -------
429 :class:`str`
430 Evaluable string representation.
431 """
433 return super().__repr__() # pragma: no cover
435 @abstractmethod
436 def __hash__(self) -> int:
437 """
438 Compute the hash of the abstract continuous function.
440 Returns
441 -------
442 :class:`int`
443 Object hash.
444 """
446 ... # pragma: no cover
448 @abstractmethod
449 def __getitem__(self, x: ArrayLike | slice) -> NDArrayFloat:
450 """
451 Return the corresponding range variable :math:`y` for the specified
452 independent domain variable :math:`x`.
454 This abstract method must be reimplemented by sub-classes.
456 Parameters
457 ----------
458 x
459 Independent domain variable :math:`x`.
461 Returns
462 -------
463 :class:`numpy.ndarray`
464 Variable :math:`y` range value.
465 """
467 ... # pragma: no cover
469 @abstractmethod
470 def __setitem__(self, x: ArrayLike | slice, y: ArrayLike) -> None:
471 """
472 Set the corresponding range variable :math:`y` for the specified
473 independent domain variable :math:`x`.
475 This abstract method must be reimplemented by sub-classes.
477 Parameters
478 ----------
479 x
480 Independent domain variable :math:`x`.
481 y
482 Corresponding range variable :math:`y`.
483 """
485 ... # pragma: no cover
487 @abstractmethod
488 def __contains__(self, x: ArrayLike | slice) -> bool:
489 """
490 Determine whether the abstract continuous function contains the
491 specified independent domain variable :math:`x`.
493 This abstract method must be reimplemented by sub-classes.
495 Parameters
496 ----------
497 x
498 Independent domain variable :math:`x`.
500 Returns
501 -------
502 :class:`bool`
503 Whether :math:`x` domain value is contained.
504 """
506 ... # pragma: no cover
508 def __iter__(self) -> Generator:
509 """
510 Return a generator for the abstract continuous function.
512 Yields
513 ------
514 Generator
515 Abstract continuous function generator.
516 """
518 yield from np.column_stack([self.domain, self.range])
520 def __len__(self) -> int:
521 """
522 Return the number of elements in the abstract continuous function's
523 independent domain variable :math:`x`.
525 Returns
526 -------
527 :class:`int`
528 Number of elements in the independent domain variable :math:`x`.
529 """
531 return len(self.domain)
533 @abstractmethod
534 def __eq__(self, other: object) -> bool:
535 """
536 Determine whether the abstract continuous function equals the specified
537 object.
539 This abstract method must be reimplemented by sub-classes.
541 Parameters
542 ----------
543 other
544 Object to determine for equality with the abstract continuous function.
546 Returns
547 -------
548 :class:`bool`
549 Whether the specified object is equal to the abstract continuous
550 function.
551 """
553 ... # pragma: no cover
555 @abstractmethod
556 def __ne__(self, other: object) -> bool:
557 """
558 Determine whether the abstract continuous function is not equal to the
559 specified object.
561 This method must be reimplemented by sub-classes.
563 Parameters
564 ----------
565 other
566 Object to determine whether it is not equal to the abstract continuous
567 function.
569 Returns
570 -------
571 :class:`bool`
572 Whether the specified object is not equal to the abstract
573 continuous function.
574 """
576 ... # pragma: no cover
578 def __add__(self, a: ArrayLike | Self) -> Self:
579 """
580 Implement support for addition.
582 Parameters
583 ----------
584 a
585 Variable :math:`a` to add to the continuous function.
587 Returns
588 -------
589 :class:`colour.continuous.AbstractContinuousFunction`
590 Abstract continuous function with the specified variable
591 added.
592 """
594 return self.arithmetical_operation(a, "+")
596 def __iadd__(self, a: ArrayLike | Self) -> Self:
597 """
598 Implement support for in-place addition.
600 Parameters
601 ----------
602 a
603 Variable :math:`a` to add in-place to the abstract continuous
604 function.
606 Returns
607 -------
608 :class:`colour.continuous.AbstractContinuousFunction`
609 Abstract continuous function with in-place addition applied.
610 """
612 return self.arithmetical_operation(a, "+", True)
614 def __sub__(self, a: ArrayLike | Self) -> Self:
615 """
616 Implement support for subtraction.
618 Parameters
619 ----------
620 a
621 Variable :math:`a` to subtract from the continuous function.
623 Returns
624 -------
625 :class:`colour.continuous.AbstractContinuousFunction`
626 Abstract continuous function with the specified variable
627 subtracted.
628 """
630 return self.arithmetical_operation(a, "-")
632 def __isub__(self, a: ArrayLike | Self) -> Self:
633 """
634 Implement support for in-place subtraction.
636 Parameters
637 ----------
638 a
639 Variable :math:`a` to subtract in-place from the abstract continuous
640 function.
642 Returns
643 -------
644 :class:`colour.continuous.AbstractContinuousFunction`
645 Abstract continuous function with in-place subtraction applied.
646 """
648 return self.arithmetical_operation(a, "-", True)
650 def __mul__(self, a: ArrayLike | Self) -> Self:
651 """
652 Implement support for multiplication.
654 Parameters
655 ----------
656 a
657 Variable :math:`a` to multiply the continuous function by.
659 Returns
660 -------
661 :class:`colour.continuous.AbstractContinuousFunction`
662 Abstract continuous function with the specified variable
663 multiplied.
664 """
666 return self.arithmetical_operation(a, "*")
668 def __imul__(self, a: ArrayLike | Self) -> Self:
669 """
670 Implement support for in-place multiplication.
672 Parameters
673 ----------
674 a
675 Variable :math:`a` to multiply in-place by the abstract continuous
676 function.
678 Returns
679 -------
680 :class:`colour.continuous.AbstractContinuousFunction`
681 Abstract continuous function with in-place multiplication applied.
682 """
684 return self.arithmetical_operation(a, "*", True)
686 def __div__(self, a: ArrayLike | Self) -> Self:
687 """
688 Implement support for division.
690 Parameters
691 ----------
692 a
693 Variable :math:`a` to divide the continuous function by.
695 Returns
696 -------
697 :class:`colour.continuous.AbstractContinuousFunction`
698 Abstract continuous function with the specified variable
699 divided.
700 """
702 return self.arithmetical_operation(a, "/")
704 def __idiv__(self, a: ArrayLike | Self) -> Self:
705 """
706 Implement support for in-place division.
708 Parameters
709 ----------
710 a
711 Variable :math:`a` to divide in-place by the abstract continuous
712 function.
714 Returns
715 -------
716 :class:`colour.continuous.AbstractContinuousFunction`
717 Abstract continuous function with in-place division applied.
718 """
720 return self.arithmetical_operation(a, "/", True)
722 __itruediv__ = __idiv__
723 __truediv__ = __div__
725 def __pow__(self, a: ArrayLike | Self) -> Self:
726 """
727 Implement support for exponentiation.
729 Parameters
730 ----------
731 a
732 Variable :math:`a` to raise the continuous function to the power of.
734 Returns
735 -------
736 :class:`colour.continuous.AbstractContinuousFunction`
737 Abstract continuous function with the specified variable
738 exponentiated.
739 """
741 return self.arithmetical_operation(a, "**")
743 def __ipow__(self, a: ArrayLike | Self) -> Self:
744 """
745 Implement support for in-place exponentiation.
747 Parameters
748 ----------
749 a
750 Variable :math:`a` to raise in-place the abstract continuous
751 function to the power of.
753 Returns
754 -------
755 :class:`colour.continuous.AbstractContinuousFunction`
756 Abstract continuous function with in-place exponentiation applied.
757 """
759 return self.arithmetical_operation(a, "**", True)
761 @abstractmethod
762 def arithmetical_operation(
763 self,
764 a: ArrayLike | Self,
765 operation: Literal["+", "-", "*", "/", "**"],
766 in_place: bool = False,
767 ) -> Self:
768 """
769 Perform the specified arithmetical operation with operand :math:`a`,
770 either on a copy or in-place.
772 This method must be reimplemented by sub-classes.
774 Parameters
775 ----------
776 a
777 Operand :math:`a`. Can be a numeric value, array-like object, or
778 another continuous function instance.
779 operation
780 Operation to perform.
781 in_place
782 Operation happens in place.
784 Returns
785 -------
786 :class:`colour.continuous.AbstractContinuousFunction`
787 Abstract continuous function.
788 """
790 ... # pragma: no cover
792 @abstractmethod
793 def fill_nan(
794 self,
795 method: Literal["Constant", "Interpolation"] | str = "Interpolation",
796 default: Real = 0,
797 ) -> Self:
798 """
799 Fill NaNs in independent domain variable :math:`x` and corresponding
800 range variable :math:`y` using the specified method.
802 This abstract method must be reimplemented by sub-classes.
804 Parameters
805 ----------
806 method
807 *Interpolation* method linearly interpolates through the NaNs,
808 *Constant* method replaces NaNs with ``default``.
809 default
810 Value to use with the *Constant* method.
812 Returns
813 -------
814 :class:`colour.continuous.AbstractContinuousFunction`
815 NaNs filled abstract continuous function.
816 """
818 ... # pragma: no cover
820 def domain_distance(self, a: ArrayLike) -> NDArrayFloat:
821 """
822 Return the Euclidean distance between specified array and the closest
823 element of the independent domain :math:`x`.
825 Parameters
826 ----------
827 a
828 Variable :math:`a` to compute the Euclidean distance with the
829 independent domain variable :math:`x`.
831 Returns
832 -------
833 :class:`numpy.ndarray`
834 Euclidean distance between independent domain variable :math:`x`
835 and specified variable :math:`a`.
836 """
838 n = closest(self.domain, a)
840 return as_float(np.abs(a - n))
842 def is_uniform(self) -> bool:
843 """
844 Return whether the independent domain variable :math:`x` is uniform.
846 Returns
847 -------
848 :class:`bool`
849 Whether the independent domain variable :math:`x` is uniform.
850 """
852 return is_uniform(self.domain)
854 def copy(self) -> Self:
855 """
856 Return a copy of the sub-class instance.
858 Returns
859 -------
860 :class:`colour.continuous.AbstractContinuousFunction`
861 Copy of the abstract continuous function.
862 """
864 return deepcopy(self)