Coverage for utilities/array.py: 72%
404 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Array Utilities
3===============
5Provide utilities for array manipulation and computational operations.
7References
8----------
9- :cite:`Castro2014a` : Castro, S. (2014). Numpy: Fastest way of computing
10 diagonal for each row of a 2d array. Retrieved August 22, 2014, from
11 http://stackoverflow.com/questions/26511401/\
12numpy-fastest-way-of-computing-diagonal-for-each-row-of-a-2d-array/\
1326517247#26517247
14- :cite:`Yorke2014a` : Yorke, R. (2014). Python: Change format of np.array or
15 allow tolerance in in1d function. Retrieved March 27, 2015, from
16 http://stackoverflow.com/a/23521245/931625
17"""
19from __future__ import annotations
21import functools
22import re
23import sys
24import typing
25from collections.abc import KeysView, ValuesView
26from contextlib import contextmanager
27from dataclasses import fields, is_dataclass, replace
28from operator import add, mul, pow, sub, truediv # noqa: A004
29from typing import Union, get_args, get_origin, get_type_hints
31import numpy as np
33from colour.constants import (
34 DTYPE_COMPLEX_DEFAULT,
35 DTYPE_FLOAT_DEFAULT,
36 DTYPE_INT_DEFAULT,
37 EPSILON,
38)
40if typing.TYPE_CHECKING:
41 from colour.hints import (
42 Any,
43 Callable,
44 DType,
45 DTypeBoolean,
46 DTypeComplex,
47 DTypeReal,
48 Dataclass,
49 Generator,
50 Literal,
51 NDArray,
52 NDArrayComplex,
53 NDArrayFloat,
54 NDArrayInt,
55 Real,
56 Self,
57 Sequence,
58 Type,
59 )
61from colour.hints import ArrayLike, DTypeComplex, DTypeFloat, DTypeInt, cast
62from colour.utilities import (
63 CACHE_REGISTRY,
64 attest,
65 int_digest,
66 is_caching_enabled,
67 optional,
68 suppress_warnings,
69 validate_method,
70)
72__author__ = "Colour Developers"
73__copyright__ = "Copyright 2013 Colour Developers"
74__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
75__maintainer__ = "Colour Developers"
76__email__ = "colour-developers@colour-science.org"
77__status__ = "Production"
79__all__ = [
80 "MixinDataclassFields",
81 "MixinDataclassIterable",
82 "MixinDataclassArray",
83 "MixinDataclassArithmetic",
84 "as_array",
85 "as_int",
86 "as_float",
87 "as_int_array",
88 "as_float_array",
89 "as_int_scalar",
90 "as_float_scalar",
91 "as_complex_array",
92 "set_default_int_dtype",
93 "set_default_float_dtype",
94 "get_domain_range_scale",
95 "set_domain_range_scale",
96 "domain_range_scale",
97 "get_domain_range_scale_metadata",
98 "to_domain_1",
99 "to_domain_10",
100 "to_domain_100",
101 "to_domain_degrees",
102 "to_domain_int",
103 "from_range_1",
104 "from_range_10",
105 "from_range_100",
106 "from_range_degrees",
107 "from_range_int",
108 "is_ndarray_copy_enabled",
109 "set_ndarray_copy_enable",
110 "ndarray_copy_enable",
111 "ndarray_copy",
112 "closest_indexes",
113 "closest",
114 "interval",
115 "is_uniform",
116 "in_array",
117 "tstack",
118 "tsplit",
119 "row_as_diagonal",
120 "orient",
121 "centroid",
122 "fill_nan",
123 "has_only_nan",
124 "ndarray_write",
125 "zeros",
126 "ones",
127 "full",
128 "index_along_last_axis",
129 "format_array_as_row",
130]
133class MixinDataclassFields:
134 """
135 Provide fields introspection for :class:`dataclass`-like classes.
137 This mixin extends dataclass functionality to enable introspection
138 capabilities, allowing programmatic access to field metadata and
139 properties.
141 Attributes
142 ----------
143 - :attr:`~colour.utilities.MixinDataclassFields.fields`
144 """
146 @property
147 def fields(self) -> tuple:
148 """
149 Getter for the fields of the :class:`dataclass`-like class.
151 Returns
152 -------
153 :class:`tuple`
154 :class:`dataclass`-like class fields.
155 """
157 return fields(self) # pyright: ignore
160class MixinDataclassIterable(MixinDataclassFields):
161 """
162 Provide iteration capabilities over :class:`dataclass`-like classes.
164 This mixin extends dataclass functionality to enable dictionary-like
165 iteration over fields, allowing access to field names, values, and
166 name-value pairs through standard iteration protocols.
168 Attributes
169 ----------
170 - :attr:`~colour.utilities.MixinDataclassIterable.keys`
171 - :attr:`~colour.utilities.MixinDataclassIterable.values`
172 - :attr:`~colour.utilities.MixinDataclassIterable.items`
174 Methods
175 -------
176 - :meth:`~colour.utilities.MixinDataclassIterable.__iter__`
178 Notes
179 -----
180 - The :class:`colour.utilities.MixinDataclassIterable` class inherits
181 the methods from the following class:
183 - :class:`colour.utilities.MixinDataclassFields`
184 """
186 @property
187 def keys(self) -> tuple:
188 """
189 Getter for the :class:`dataclass`-like class keys, i.e., the field
190 names.
192 Returns
193 -------
194 :class:`tuple`
195 :class:`dataclass`-like class keys.
196 """
198 return tuple(field for field, _value in self)
200 @property
201 def values(self) -> tuple:
202 """
203 Getter for the :class:`dataclass`-like class field values.
205 Returns
206 -------
207 :class:`tuple`
208 :class:`dataclass`-like class field values.
209 """
211 return tuple(value for _field, value in self)
213 @property
214 def items(self) -> tuple:
215 """
216 Getter for the :class:`dataclass`-like class items, i.e., the field
217 names and values.
219 Returns
220 -------
221 :class:`tuple`
222 :class:`dataclass`-like class items.
223 """
225 return tuple((field, value) for field, value in self)
227 def __iter__(self) -> Generator:
228 """
229 Yield the :class:`dataclass`-like class fields.
231 Yields
232 ------
233 Generator
234 :class:`dataclass`-like class field generator.
235 """
237 yield from {
238 field.name: getattr(self, field.name) for field in self.fields
239 }.items()
242class MixinDataclassArray(MixinDataclassIterable):
243 """
244 Provide conversion methods for :class:`dataclass`-like classes to
245 :class:`numpy.ndarray` objects.
247 This mixin extends dataclass functionality to enable seamless conversion
248 to NumPy arrays, facilitating numerical operations on structured data.
250 Methods
251 -------
252 - :meth:`~colour.utilities.MixinDataclassArray.__array__`
254 Notes
255 -----
256 - The :class:`colour.utilities.MixinDataclassArray` class
257 inherits the methods from the following classes:
259 - :class:`colour.utilities.MixinDataclassIterable`
260 - :class:`colour.utilities.MixinDataclassFields`
261 """
263 def __array__(
264 self, dtype: Type[DTypeReal] | None = None, copy: bool = True
265 ) -> NDArray:
266 """
267 Implement support for :class:`dataclass`-like class conversion to
268 :class:`numpy.ndarray` class.
270 A field set to *None* will be filled with `np.nan` according to the
271 shape of the first field not set with *None*.
273 Parameters
274 ----------
275 dtype
276 :class:`numpy.dtype` to use for conversion to `np.ndarray`,
277 default to the :class:`numpy.dtype` defined by
278 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
279 copy
280 Whether to return a copy of the underlying data, will always be
281 `True`, irrespective of the parameter value.
283 Returns
284 -------
285 :class:`numpy.ndarray`
286 :class:`dataclass`-like class converted to
287 :class:`numpy.ndarray`.
288 """
290 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
292 default = None
293 for _field, value in self:
294 if value is not None:
295 default = full(as_float_array(value).shape, np.nan)
296 break
298 return tstack(
299 cast(
300 "ArrayLike",
301 [value if value is not None else default for value in self.values],
302 ),
303 dtype=dtype,
304 )
307class MixinDataclassArithmetic(MixinDataclassArray):
308 """
309 Provide mathematical operations for :class:`dataclass`-like classes.
311 This mixin extends dataclass functionality to enable arithmetic
312 operations, facilitating mathematical computations on dataclass instances
313 containing array-like data.
315 Methods
316 -------
317 - :meth:`~colour.utilities.MixinDataclassArray.__iadd__`
318 - :meth:`~colour.utilities.MixinDataclassArray.__add__`
319 - :meth:`~colour.utilities.MixinDataclassArray.__isub__`
320 - :meth:`~colour.utilities.MixinDataclassArray.__sub__`
321 - :meth:`~colour.utilities.MixinDataclassArray.__imul__`
322 - :meth:`~colour.utilities.MixinDataclassArray.__mul__`
323 - :meth:`~colour.utilities.MixinDataclassArray.__idiv__`
324 - :meth:`~colour.utilities.MixinDataclassArray.__div__`
325 - :meth:`~colour.utilities.MixinDataclassArray.__ipow__`
326 - :meth:`~colour.utilities.MixinDataclassArray.__pow__`
327 - :meth:`~colour.utilities.MixinDataclassArray.arithmetical_operation`
329 Notes
330 -----
331 - The :class:`colour.utilities.MixinDataclassArithmetic` class inherits
332 the methods from the following classes:
334 - :class:`colour.utilities.MixinDataclassArray`
335 - :class:`colour.utilities.MixinDataclassIterable`
336 - :class:`colour.utilities.MixinDataclassFields`
337 """
339 def __add__(self, a: Any) -> Self:
340 """
341 Implement support for addition.
343 Parameters
344 ----------
345 a
346 Variable :math:`a` to add.
348 Returns
349 -------
350 :class:`dataclass`
351 Variable added :class:`dataclass`-like class.
352 """
354 return self.arithmetical_operation(a, "+")
356 def __iadd__(self, a: Any) -> Self:
357 """
358 Implement support for in-place addition.
360 Parameters
361 ----------
362 a
363 Variable :math:`a` to add in-place.
365 Returns
366 -------
367 :class:`dataclass`
368 In-place variable added :class:`dataclass`-like class.
369 """
371 return self.arithmetical_operation(a, "+", True)
373 def __sub__(self, a: Any) -> Self:
374 """
375 Implement support for subtraction.
377 Parameters
378 ----------
379 a
380 Variable :math:`a` to subtract.
382 Returns
383 -------
384 :class:`dataclass`
385 Variable subtracted :class:`dataclass`-like class.
386 """
388 return self.arithmetical_operation(a, "-")
390 def __isub__(self, a: Any) -> Self:
391 """
392 Implement support for in-place subtraction.
394 Parameters
395 ----------
396 a
397 Variable :math:`a` to subtract in-place.
399 Returns
400 -------
401 :class:`dataclass`
402 In-place variable subtracted :class:`dataclass`-like class.
403 """
405 return self.arithmetical_operation(a, "-", True)
407 def __mul__(self, a: Any) -> Self:
408 """
409 Implement support for multiplication.
411 Parameters
412 ----------
413 a
414 Variable :math:`a` to multiply by.
416 Returns
417 -------
418 :class:`dataclass`
419 Variable multiplied :class:`dataclass`-like class.
420 """
422 return self.arithmetical_operation(a, "*")
424 def __imul__(self, a: Any) -> Self:
425 """
426 Implement support for in-place multiplication.
428 Parameters
429 ----------
430 a
431 Variable :math:`a` to multiply by in-place.
433 Returns
434 -------
435 :class:`dataclass`
436 In-place variable multiplied :class:`dataclass`-like class.
437 """
439 return self.arithmetical_operation(a, "*", True)
441 def __div__(self, a: Any) -> Self:
442 """
443 Implement support for division.
445 Parameters
446 ----------
447 a
448 Variable :math:`a` to divide by.
450 Returns
451 -------
452 :class:`dataclass`
453 Variable divided :class:`dataclass`-like class.
454 """
456 return self.arithmetical_operation(a, "/")
458 def __idiv__(self, a: Any) -> Self:
459 """
460 Implement support for in-place division.
462 Parameters
463 ----------
464 a
465 Variable :math:`a` to divide by in-place.
467 Returns
468 -------
469 :class:`dataclass`
470 In-place variable divided :class:`dataclass`-like class.
471 """
473 return self.arithmetical_operation(a, "/", True)
475 __itruediv__ = __idiv__
476 __truediv__ = __div__
478 def __pow__(self, a: Any) -> Self:
479 """
480 Implement support for exponentiation.
482 Parameters
483 ----------
484 a
485 Variable :math:`a` to exponentiate by.
487 Returns
488 -------
489 :class:`dataclass`
490 Variable exponentiated :class:`dataclass`-like class.
491 """
493 return self.arithmetical_operation(a, "**")
495 def __ipow__(self, a: Any) -> Self:
496 """
497 Implement support for in-place exponentiation.
499 Parameters
500 ----------
501 a
502 Variable :math:`a` to exponentiate by in-place.
504 Returns
505 -------
506 :class:`dataclass`
507 In-place variable exponentiated :class:`dataclass`-like
508 class.
509 """
511 return self.arithmetical_operation(a, "**", True)
513 def arithmetical_operation(
514 self, a: Any, operation: str, in_place: bool = False
515 ) -> Dataclass:
516 """
517 Perform the specified arithmetical operation with the :math:`a`
518 operand on the :class:`dataclass`-like class.
520 Parameters
521 ----------
522 a
523 Operand.
524 operation
525 Operation to perform.
526 in_place
527 Operation happens in place.
529 Returns
530 -------
531 :class:`dataclass`
532 :class:`dataclass`-like class with the arithmetical operation
533 performed.
534 """
536 callable_operation = {
537 "+": add,
538 "-": sub,
539 "*": mul,
540 "/": truediv,
541 "**": pow,
542 }[operation]
544 if is_dataclass(a):
545 a = as_float_array(a) # pyright: ignore
547 values = tsplit(callable_operation(as_float_array(self), a))
548 field_values = {field: values[i] for i, field in enumerate(self.keys)}
549 field_values.update({field: None for field, value in self if value is None})
551 dataclass = replace(self, **field_values) # pyright: ignore
553 if in_place:
554 for field in self.keys:
555 setattr(self, field, getattr(dataclass, field))
557 return self
559 return dataclass
562# NOTE : The following messages are pre-generated for performance reasons.
563_ASSERTION_MESSAGE_DTYPE_INT = (
564 f'"dtype" must be one of the following types: "{DTypeInt.__args__}"'
565)
567_ASSERTION_MESSAGE_DTYPE_FLOAT = (
568 f'"dtype" must be one of the following types: "{DTypeFloat.__args__}"'
569)
571_ASSERTION_MESSAGE_DTYPE_COMPLEX = (
572 f'"dtype" must be one of the following types: "{DTypeComplex.__args__}"'
573)
576def as_array(
577 a: ArrayLike | KeysView | ValuesView,
578 dtype: Type[DType] | None = None,
579) -> NDArray:
580 """
581 Convert the specified variable :math:`a` to :class:`numpy.ndarray` using
582 the specified :class:`numpy.dtype`.
584 Parameters
585 ----------
586 a
587 Variable :math:`a` to convert.
588 dtype
589 :class:`numpy.dtype` to use for conversion, default to the
590 :class:`numpy.dtype` defined by the
591 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
593 Returns
594 -------
595 :class:`numpy.ndarray`
596 Variable :math:`a` converted to :class:`numpy.ndarray`.
598 Examples
599 --------
600 >>> as_array([1, 2, 3]) # doctest: +ELLIPSIS
601 array([1, 2, 3]...)
602 >>> as_array([1, 2, 3], dtype=DTYPE_FLOAT_DEFAULT)
603 array([ 1., 2., 3.])
604 """
606 # TODO: Remove when https://github.com/numpy/numpy/issues/5718 is
607 # addressed.
608 if isinstance(a, (KeysView, ValuesView)):
609 a = list(a)
611 return np.asarray(a, dtype)
614@typing.overload
615def as_int(a: float | DTypeFloat, dtype: Type[DTypeInt] | None = None) -> DTypeInt: ...
616@typing.overload
617def as_int(
618 a: NDArray | Sequence[int], dtype: Type[DTypeInt] | None = None
619) -> NDArrayInt: ...
620@typing.overload
621def as_int(
622 a: ArrayLike, dtype: Type[DTypeInt] | None = None
623) -> DTypeInt | NDArrayInt: ...
624def as_int(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> DTypeInt | NDArrayInt:
625 """
626 Convert the specified variable :math:`a` to :class:`numpy.integer` using
627 the specified :class:`numpy.dtype`.
629 The function converts variable :math:`a` to an integer type. If variable
630 :math:`a` is not a scalar or 0-dimensional array, it is converted to
631 :class:`numpy.ndarray`.
633 Parameters
634 ----------
635 a
636 Variable :math:`a` to convert.
637 dtype
638 :class:`numpy.dtype` to use for conversion, default to the
639 :class:`numpy.dtype` defined by the
640 :attr:`colour.constant.DTYPE_INT_DEFAULT` attribute.
642 Returns
643 -------
644 :class:`numpy.ndarray`
645 Variable :math:`a` converted to :class:`numpy.integer`.
647 Examples
648 --------
649 >>> as_int(np.array(1))
650 1
651 >>> as_int(np.array([1])) # doctest: +SKIP
652 array([1])
653 >>> as_int(np.arange(10)) # doctest: +SKIP
654 array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]...)
655 """
657 dtype = optional(dtype, DTYPE_INT_DEFAULT)
659 attest(dtype in DTypeInt.__args__, _ASSERTION_MESSAGE_DTYPE_INT)
661 return dtype(a) # pyright: ignore
664@typing.overload
665def as_float(
666 a: float | DTypeFloat, dtype: Type[DTypeFloat] | None = None
667) -> DTypeFloat: ...
668@typing.overload
669def as_float(
670 a: NDArray | Sequence[float], dtype: Type[DTypeFloat] | None = None
671) -> NDArrayFloat: ...
672@typing.overload
673def as_float(
674 a: ArrayLike, dtype: Type[DTypeFloat] | None = None
675) -> DTypeFloat | NDArrayFloat: ...
676def as_float(
677 a: ArrayLike, dtype: Type[DTypeFloat] | None = None
678) -> DTypeFloat | NDArrayFloat:
679 """
680 Convert the specified variable :math:`a` to :class:`numpy.floating` using
681 the specified :class:`numpy.dtype`.
683 If variable :math:`a` is not a scalar or 0-dimensional, it is converted
684 to :class:`numpy.ndarray`.
686 Parameters
687 ----------
688 a
689 Variable :math:`a` to convert.
690 dtype
691 :class:`numpy.dtype` to use for conversion, default to the
692 :class:`numpy.dtype` defined by the
693 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
695 Returns
696 -------
697 :class:`numpy.ndarray`
698 Variable :math:`a` converted to :class:`numpy.floating`.
700 Examples
701 --------
702 >>> as_float(np.array(1))
703 1.0
704 >>> as_float(np.array([1]))
705 array([ 1.])
706 >>> as_float(np.arange(10))
707 array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
708 """
710 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
712 attest(dtype in DTypeFloat.__args__, _ASSERTION_MESSAGE_DTYPE_FLOAT)
714 # NOTE: "np.float64" reduces dimensionality:
715 # >>> np.int64(np.array([[1]]))
716 # array([[1]])
717 # >>> np.float64(np.array([[1]]))
718 # 1.0
719 # See for more information https://github.com/numpy/numpy/issues/24283
720 if isinstance(a, np.ndarray) and a.size == 1 and a.ndim != 0:
721 return as_float_array(a, dtype)
723 return dtype(a) # pyright: ignore
726def as_int_array(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> NDArrayInt:
727 """
728 Convert the specified variable :math:`a` to :class:`numpy.ndarray` using
729 the specified integer :class:`numpy.dtype`.
731 Parameters
732 ----------
733 a
734 Variable :math:`a` to convert.
735 dtype
736 :class:`numpy.dtype` to use for conversion, default to the
737 :class:`numpy.dtype` defined by the
738 :attr:`colour.constant.DTYPE_INT_DEFAULT` attribute.
740 Returns
741 -------
742 :class:`numpy.ndarray`
743 Variable :math:`a` converted to integer :class:`numpy.ndarray`.
745 Examples
746 --------
747 >>> as_int_array([1.0, 2.0, 3.0]) # doctest: +ELLIPSIS
748 array([1, 2, 3]...)
749 """
751 dtype = optional(dtype, DTYPE_INT_DEFAULT)
753 attest(dtype in DTypeInt.__args__, _ASSERTION_MESSAGE_DTYPE_INT)
755 return as_array(a, dtype)
758def as_float_array(a: ArrayLike, dtype: Type[DTypeFloat] | None = None) -> NDArrayFloat:
759 """
760 Convert the specified variable :math:`a` to :class:`numpy.ndarray` using
761 the specified floating-point :class:`numpy.dtype`.
763 Parameters
764 ----------
765 a
766 Variable :math:`a` to convert.
767 dtype
768 Floating-point :class:`numpy.dtype` to use for conversion, default
769 to the :class:`numpy.dtype` defined by the
770 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
772 Returns
773 -------
774 :class:`numpy.ndarray`
775 Variable :math:`a` converted to floating-point
776 :class:`numpy.ndarray`.
778 Examples
779 --------
780 >>> as_float_array([1, 2, 3])
781 array([ 1., 2., 3.])
782 """
784 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
786 attest(dtype in DTypeFloat.__args__, _ASSERTION_MESSAGE_DTYPE_FLOAT)
788 return as_array(a, dtype)
791def as_int_scalar(a: ArrayLike, dtype: Type[DTypeInt] | None = None) -> int:
792 """
793 Convert the specified variable :math:`a` to :class:`numpy.integer` using
794 the specified :class:`numpy.dtype`.
796 Parameters
797 ----------
798 a
799 Variable :math:`a` to convert.
800 dtype
801 :class:`numpy.dtype` to use for conversion, default to the
802 :class:`numpy.dtype` defined by the
803 :attr:`colour.constant.DTYPE_INT_DEFAULT` attribute.
805 Returns
806 -------
807 :class:`int`
808 Variable :math:`a` converted to :class:`numpy.integer`.
810 Warnings
811 --------
812 - The return type is effectively annotated as :class:`int` and not
813 :class:`numpy.integer`.
815 Examples
816 --------
817 >>> as_int_scalar(np.array(1))
818 1
819 """
821 a = np.squeeze(as_int_array(a, dtype))
823 attest(a.ndim == 0, f'"{a}" cannot be converted to "int" scalar!')
825 # TODO: Revisit when Numpy types are well established.
826 return cast("int", as_int(a, dtype))
829def as_float_scalar(a: ArrayLike, dtype: Type[DTypeFloat] | None = None) -> float:
830 """
831 Convert the specified variable :math:`a` to :class:`numpy.floating` using
832 the specified :class:`numpy.dtype`.
834 Parameters
835 ----------
836 a
837 Variable :math:`a` to convert.
838 dtype
839 :class:`numpy.dtype` to use for conversion, default to the
840 :class:`numpy.dtype` defined by the
841 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
843 Returns
844 -------
845 :class:`float`
846 Variable :math:`a` converted to :class:`numpy.floating`.
848 Warnings
849 --------
850 - The return type is effectively annotated as :class:`float` and not
851 :class:`numpy.floating`.
853 Examples
854 --------
855 >>> as_float_scalar(np.array(1))
856 1.0
857 """
859 a = np.squeeze(as_float_array(a, dtype))
861 attest(a.ndim == 0, f'"{a}" cannot be converted to "float" scalar!')
863 # TODO: Revisit when Numpy types are well established.
864 return cast("float", as_float(a, dtype))
867def as_complex_array(
868 a: ArrayLike,
869 dtype: Type[DTypeComplex] | None = None,
870) -> NDArrayComplex:
871 """
872 Convert the specified variable :math:`a` to :class:`numpy.ndarray` using
873 the specified complex :class:`numpy.dtype`.
875 Parameters
876 ----------
877 a
878 Variable :math:`a` to convert.
879 dtype
880 Complex :class:`numpy.dtype` to use for conversion, default
881 to the :class:`numpy.dtype` defined by the
882 :attr:`colour.constant.DTYPE_COMPLEX_DEFAULT` attribute.
884 Returns
885 -------
886 :class:`numpy.ndarray`
887 Variable :math:`a` converted to complex
888 :class:`numpy.ndarray`.
890 Examples
891 --------
892 >>> as_complex_array([1, 2, 3])
893 array([ 1.+0.j, 2.+0.j, 3.+0.j])
894 >>> as_complex_array([1 + 2j, 3 + 4j])
895 array([ 1.+2.j, 3.+4.j])
896 """
898 dtype = optional(dtype, DTYPE_COMPLEX_DEFAULT)
900 attest(dtype in DTypeComplex.__args__, _ASSERTION_MESSAGE_DTYPE_COMPLEX)
902 return as_array(a, dtype)
905def set_default_int_dtype(
906 dtype: Type[DTypeInt] = DTYPE_INT_DEFAULT,
907) -> None:
908 """
909 Set the *Colour* default :class:`numpy.integer` precision by setting
910 :attr:`colour.constant.DTYPE_INT_DEFAULT` attribute with the specified
911 :class:`numpy.dtype` wherever the attribute is imported.
913 Parameters
914 ----------
915 dtype
916 :class:`numpy.dtype` to set
917 :attr:`colour.constant.DTYPE_INT_DEFAULT` with.
919 Notes
920 -----
921 - It is possible to define the integer precision at import time by
922 setting the *COLOUR_SCIENCE__DEFAULT_INT_DTYPE* environment
923 variable, for example `set COLOUR_SCIENCE__DEFAULT_INT_DTYPE=int32`.
925 Warnings
926 --------
927 This definition is mostly given for consistency purposes with
928 :func:`colour.utilities.set_default_float_dtype` definition but contrary
929 to the latter, changing *integer* precision will almost certainly
930 completely break *Colour*. With great power comes great responsibility.
932 Examples
933 --------
934 >>> as_int_array(np.ones(3)).dtype # doctest: +SKIP
935 dtype('int64')
936 >>> set_default_int_dtype(np.int32) # doctest: +SKIP
937 >>> as_int_array(np.ones(3)).dtype # doctest: +SKIP
938 dtype('int32')
939 >>> set_default_int_dtype(np.int64)
940 >>> as_int_array(np.ones(3)).dtype # doctest: +SKIP
941 dtype('int64')
942 """
944 # TODO: Investigate behaviour on Windows.
945 with suppress_warnings(colour_usage_warnings=True):
946 for module in sys.modules.values():
947 if not hasattr(module, "DTYPE_INT_DEFAULT"):
948 continue
950 module.DTYPE_INT_DEFAULT = dtype # pyright: ignore
952 CACHE_REGISTRY.clear_all_caches()
955def set_default_float_dtype(
956 dtype: Type[DTypeFloat] = DTYPE_FLOAT_DEFAULT,
957) -> None:
958 """
959 Set the *Colour* default :class:`numpy.floating` precision by setting
960 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute with the
961 specified :class:`numpy.dtype` wherever the attribute is imported.
963 Parameters
964 ----------
965 dtype
966 :class:`numpy.dtype` to set
967 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` with.
969 Notes
970 -----
971 - It is possible to define the *float* precision at import time by
972 setting the *COLOUR_SCIENCE__DEFAULT_FLOAT_DTYPE* environment
973 variable, for example
974 `set COLOUR_SCIENCE__DEFAULT_FLOAT_DTYPE=float32`.
975 - Some definition returning a single-scalar ndarray might not
976 honour the specified *float* precision:
977 https://github.com/numpy/numpy/issues/16353
979 Warnings
980 --------
981 Changing *float* precision might result in various *Colour*
982 functionality breaking entirely:
983 https://github.com/numpy/numpy/issues/6860. With great power comes
984 great responsibility.
986 Examples
987 --------
988 >>> as_float_array(np.ones(3)).dtype
989 dtype('float64')
990 >>> set_default_float_dtype(np.float16) # doctest: +SKIP
991 >>> as_float_array(np.ones(3)).dtype # doctest: +SKIP
992 dtype('float16')
993 >>> set_default_float_dtype(np.float64)
994 >>> as_float_array(np.ones(3)).dtype
995 dtype('float64')
996 """
998 with suppress_warnings(colour_usage_warnings=True):
999 for module in sys.modules.values():
1000 if not hasattr(module, "DTYPE_FLOAT_DEFAULT"):
1001 continue
1003 module.DTYPE_FLOAT_DEFAULT = dtype # pyright: ignore
1005 CACHE_REGISTRY.clear_all_caches()
1008# TODO: Annotate with "Union[Literal['ignore', 'reference', '1', '100'], str]"
1009# when Python 3.7 is dropped.
1010_DOMAIN_RANGE_SCALE = "reference"
1011"""
1012Global variable storing the current *Colour* domain-range scale.
1014_DOMAIN_RANGE_SCALE
1015"""
1018def get_domain_range_scale() -> Literal["ignore", "reference", "1", "100"] | str:
1019 """
1020 Return the current *Colour* domain-range scale.
1022 The following scales are available:
1024 - **'Reference'**, the default *Colour* domain-range scale which
1025 varies depending on the referenced algorithm, e.g., [0, 1],
1026 [0, 10], [0, 100], [0, 255], etc...
1027 - **'1'**, a domain-range scale normalised to [0, 1], it is
1028 important to acknowledge that this is a soft normalisation
1029 and it is possible to use negative out of gamut values or
1030 high dynamic range data exceeding 1.
1032 Returns
1033 -------
1034 :class:`str`
1035 *Colour* domain-range scale.
1037 Warnings
1038 --------
1039 - The **'Ignore'** and **'100'** domain-range scales are for
1040 internal usage only!
1041 """
1043 return _DOMAIN_RANGE_SCALE
1046def set_domain_range_scale(
1047 scale: (
1048 Literal["ignore", "reference", "Ignore", "Reference", "1", "100"] | str
1049 ) = "reference",
1050) -> None:
1051 """
1052 Set the current *Colour* domain-range scale.
1054 The following scales are available:
1056 - **'Reference'**, the default *Colour* domain-range scale which
1057 varies depending on the referenced algorithm, e.g., [0, 1],
1058 [0, 10], [0, 100], [0, 255], etc...
1059 - **'1'**, a domain-range scale normalised to [0, 1], it is
1060 important to acknowledge that this is a soft normalisation and it
1061 is possible to use negative out of gamut values or high dynamic
1062 range data exceeding 1.
1064 Parameters
1065 ----------
1066 scale
1067 *Colour* domain-range scale to set.
1069 Warnings
1070 --------
1071 - The **'Ignore'** and **'100'** domain-range scales are for
1072 internal usage only!
1073 """
1075 global _DOMAIN_RANGE_SCALE # noqa: PLW0603
1077 _DOMAIN_RANGE_SCALE = validate_method(
1078 str(scale),
1079 ("ignore", "reference", "1", "100"),
1080 '"{0}" scale is invalid, it must be one of {1}!',
1081 )
1084class domain_range_scale:
1085 """
1086 Define a context manager and decorator to temporarily set the *Colour*
1087 domain-range scale.
1089 The following scales are available:
1091 - **'Reference'**, the default *Colour* domain-range scale which
1092 varies depending on the referenced algorithm, e.g., [0, 1],
1093 [0, 10], [0, 100], [0, 255], etc...
1094 - **'1'**, a domain-range scale normalised to [0, 1], it is
1095 important to acknowledge that this is a soft normalisation and it
1096 is possible to use negative out of gamut values or high dynamic
1097 range data exceeding 1.
1099 Parameters
1100 ----------
1101 scale
1102 *Colour* domain-range scale to set.
1104 Warnings
1105 --------
1106 - The **'Ignore'** and **'100'** domain-range scales are for
1107 internal usage only!
1109 Examples
1110 --------
1111 With *Colour* domain-range scale set to **'Reference'**:
1113 >>> with domain_range_scale("1"):
1114 ... to_domain_1(1)
1115 array(1.0)
1116 >>> with domain_range_scale("Reference"):
1117 ... from_range_1(1)
1118 array(1.0)
1120 With *Colour* domain-range scale set to **'1'**:
1122 >>> with domain_range_scale("1"):
1123 ... to_domain_1(1)
1124 array(1.0)
1125 >>> with domain_range_scale("1"):
1126 ... from_range_1(1)
1127 array(1.0)
1129 With *Colour* domain-range scale set to **'100'** (unsupported):
1131 >>> with domain_range_scale("100"):
1132 ... to_domain_1(1)
1133 array(0.01)
1134 >>> with domain_range_scale("100"):
1135 ... from_range_1(1)
1136 array(100.0)
1137 """
1139 def __init__(
1140 self,
1141 scale: (
1142 Literal["ignore", "reference", "Ignore", "Reference", "1", "100"] | str
1143 ),
1144 ) -> None:
1145 self._scale = scale
1146 self._previous_scale = get_domain_range_scale()
1148 def __enter__(self) -> Self:
1149 """Set the new domain-range scale upon entering the context manager."""
1151 set_domain_range_scale(self._scale)
1153 return self
1155 def __exit__(self, *args: Any) -> None:
1156 """
1157 Restore the previous domain-range scale upon exiting the context
1158 manager.
1159 """
1161 set_domain_range_scale(self._previous_scale)
1163 def __call__(self, function: Callable) -> Any:
1164 """
1165 Call the wrapped definition with domain-range scale management.
1166 """
1168 @functools.wraps(function)
1169 def wrapper(*args: Any, **kwargs: Any) -> Any:
1170 with self:
1171 return function(*args, **kwargs)
1173 return wrapper
1176_CACHE_DOMAIN_RANGE_SCALE_METADATA: dict = CACHE_REGISTRY.register_cache(
1177 f"{__name__}._CACHE_DOMAIN_RANGE_SCALE_METADATA"
1178)
1181def get_domain_range_scale_metadata(function: Callable) -> dict[str, Any]:
1182 """
1183 Extract domain-range scale metadata from function type hints.
1185 Extracts scale factors from PEP 593 ``Annotated`` type hints on function
1186 parameters and return values. This metadata indicates which scale factors
1187 to use when converting between 'Reference' and '1' modes.
1189 Parameters
1190 ----------
1191 function
1192 Function to extract metadata from.
1194 Returns
1195 -------
1196 :class:`dict`
1197 Dictionary with keys:
1199 - ``domain``: Dict mapping parameter names to their scale factors
1200 - ``range``: Scale factor for return value (int, tuple, or None)
1202 Examples
1203 --------
1204 >>> from colour.hints import Annotated, ArrayLike, NDArrayFloat
1205 >>> def example_function(
1206 ... XYZ: Domain1,
1207 ... illuminant: ArrayLike = None,
1208 ... ) -> Range100:
1209 ... pass
1210 >>> metadata = get_domain_range_scale_metadata(example_function)
1211 >>> metadata["domain"]
1212 {'XYZ': 1}
1213 >>> metadata["range"]
1214 100
1215 """
1217 # Unwrap functools.partial to get the underlying function
1218 if hasattr(function, "func"):
1219 function = function.func # pyright: ignore
1221 cache_key = id(function)
1223 if is_caching_enabled() and cache_key in _CACHE_DOMAIN_RANGE_SCALE_METADATA:
1224 return _CACHE_DOMAIN_RANGE_SCALE_METADATA[cache_key]
1226 metadata: dict[str, Any] = {"domain": {}, "range": None}
1228 def extract_scale_from_hint(hint: Any) -> Any | None:
1229 """
1230 Extract scale metadata from a type hint, handling Union types.
1232 Parameters
1233 ----------
1234 hint
1235 Type hint to extract scale from.
1237 Returns
1238 -------
1239 :class:`int` | :class:`tuple` | :class:`None`
1240 Scale metadata if found, None otherwise.
1241 """
1243 # Direct Annotated type with __metadata__
1244 if hasattr(hint, "__metadata__") and hint.__metadata__:
1245 return next(iter(hint.__metadata__))
1247 # Union type: check if any arg is Annotated
1248 origin = get_origin(hint)
1249 if origin is Union:
1250 for arg in get_args(hint):
1251 if hasattr(arg, "__metadata__") and arg.__metadata__:
1252 return next(iter(arg.__metadata__))
1254 return None
1256 try:
1257 hints = get_type_hints(function, include_extras=True)
1258 # Process hints from get_type_hints (actual types with __metadata__)
1259 for parameter_name, hint in hints.items():
1260 scale = extract_scale_from_hint(hint)
1261 if scale is not None:
1262 if parameter_name == "return":
1263 metadata["range"] = scale
1264 else:
1265 metadata["domain"][parameter_name] = scale
1266 except (AttributeError, TypeError, NameError):
1267 # Fallback: parse string annotations (when `from __future__ import annotations`)
1268 # Mapping of type alias names to their scale values
1269 type_alias_scales = {
1270 "Domain1": 1,
1271 "Domain10": 10,
1272 "Domain100": 100,
1273 "Domain360": 360,
1274 "Domain100_100_360": (100, 100, 360),
1275 "Range1": 1,
1276 "Range10": 10,
1277 "Range100": 100,
1278 "Range360": 360,
1279 "Range100_100_360": (100, 100, 360),
1280 }
1282 hints = getattr(function, "__annotations__", {})
1283 for parameter_name, hint in hints.items():
1284 scale = None
1286 # Check if hint is a type alias name
1287 if isinstance(hint, str) and hint in type_alias_scales:
1288 scale = type_alias_scales[hint]
1289 # Extract scale from string: "Annotated[Type, scale]" -> scale
1290 elif (
1291 isinstance(hint, str)
1292 and "Annotated[" in hint
1293 and (match := re.search(r"Annotated\[[^,]+,\s*([^\]]+)\]", hint))
1294 ):
1295 scale_string = match.group(1).strip()
1296 # Evaluate scale (could be int, tuple, etc.)
1297 try:
1298 scale = eval(scale_string) # noqa: S307
1299 except (SyntaxError, NameError, ValueError):
1300 scale = scale_string
1302 if scale is not None:
1303 if parameter_name == "return":
1304 metadata["range"] = scale
1305 else:
1306 metadata["domain"][parameter_name] = scale
1308 if is_caching_enabled():
1309 _CACHE_DOMAIN_RANGE_SCALE_METADATA[cache_key] = metadata
1311 return metadata
1314def to_domain_1(
1315 a: ArrayLike,
1316 scale_factor: ArrayLike = 100,
1317 dtype: Type[DTypeFloat] | None = None,
1318) -> NDArray:
1319 """
1320 Scale the specified array :math:`a` to domain **'1'**.
1322 The behaviour is as follows:
1324 - If *Colour* domain-range scale is **'Reference'** or **'1'**, the
1325 definition is almost entirely by-passed and will conveniently
1326 convert array :math:`a` to :class:`np.ndarray`.
1327 - If *Colour* domain-range scale is **'100'** (currently unsupported
1328 private value only used for unit tests), array :math:`a` is divided
1329 by ``scale_factor``, typically 100.
1331 Parameters
1332 ----------
1333 a
1334 Array :math:`a` to scale to domain **'1'**.
1335 scale_factor
1336 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray`
1337 if some axes need different scaling to be brought to domain **'1'**.
1338 dtype
1339 Data type used for the conversion to :class:`np.ndarray`.
1341 Returns
1342 -------
1343 :class:`numpy.ndarray`
1344 Array :math:`a` scaled to domain **'1'**.
1346 Examples
1347 --------
1348 With *Colour* domain-range scale set to **'Reference'**:
1350 >>> with domain_range_scale("Reference"):
1351 ... to_domain_1(1)
1352 array(1.0)
1354 With *Colour* domain-range scale set to **'1'**:
1356 >>> with domain_range_scale("1"):
1357 ... to_domain_1(1)
1358 array(1.0)
1360 With *Colour* domain-range scale set to **'100'** (unsupported):
1362 >>> with domain_range_scale("100"):
1363 ... to_domain_1(1)
1364 array(0.01)
1365 """
1367 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1369 a = as_float_array(a, dtype).copy()
1371 if _DOMAIN_RANGE_SCALE == "100":
1372 a /= as_float_array(scale_factor)
1374 return a
1377def to_domain_10(
1378 a: ArrayLike,
1379 scale_factor: ArrayLike = 10,
1380 dtype: Type[DTypeFloat] | None = None,
1381) -> NDArray:
1382 """
1383 Scale the specified array :math:`a` to domain **'10'**, used by the
1384 *Munsell Renotation System*.
1386 The behaviour is as follows:
1388 - If *Colour* domain-range scale is **'Reference'**, the definition
1389 is almost entirely by-passed and will conveniently convert array
1390 :math:`a` to :class:`np.ndarray`.
1391 - If *Colour* domain-range scale is **'1'**, array :math:`a` is
1392 multiplied by ``scale_factor``, typically 10.
1393 - If *Colour* domain-range scale is **'100'** (currently unsupported
1394 private value only used for unit tests), array :math:`a` is
1395 divided by ``scale_factor``, typically 10.
1397 Parameters
1398 ----------
1399 a
1400 Array :math:`a` to scale to domain **'10'**.
1401 scale_factor
1402 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray`
1403 if some axes need different scaling to be brought to domain
1404 **'10'**.
1405 dtype
1406 Data type used for the conversion to :class:`np.ndarray`.
1408 Returns
1409 -------
1410 :class:`numpy.ndarray`
1411 Array :math:`a` scaled to domain **'10'**.
1413 Examples
1414 --------
1415 With *Colour* domain-range scale set to **'Reference'**:
1417 >>> with domain_range_scale("Reference"):
1418 ... to_domain_10(1)
1419 array(1.0)
1421 With *Colour* domain-range scale set to **'1'**:
1423 >>> with domain_range_scale("1"):
1424 ... to_domain_10(1)
1425 array(10.0)
1427 With *Colour* domain-range scale set to **'100'** (unsupported):
1429 >>> with domain_range_scale("100"):
1430 ... to_domain_10(1)
1431 array(0.1)
1432 """
1434 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1436 a = as_float_array(a, dtype).copy()
1438 if _DOMAIN_RANGE_SCALE == "1":
1439 a *= as_float_array(scale_factor)
1441 if _DOMAIN_RANGE_SCALE == "100":
1442 a /= as_float_array(scale_factor)
1444 return a
1447def to_domain_100(
1448 a: ArrayLike,
1449 scale_factor: ArrayLike = 100,
1450 dtype: Type[DTypeFloat] | None = None,
1451) -> NDArray:
1452 """
1453 Scale the specified array :math:`a` to domain **'100'**.
1455 The behaviour is as follows:
1457 - If *Colour* domain-range scale is **'Reference'** or **'100'**
1458 (currently unsupported private value only used for unit tests), the
1459 definition is almost entirely by-passed and will conveniently
1460 convert array :math:`a` to :class:`np.ndarray`.
1461 - If *Colour* domain-range scale is **'1'**, array :math:`a` is
1462 multiplied by ``scale_factor``, typically 100.
1464 Parameters
1465 ----------
1466 a
1467 Array :math:`a` to scale to domain **'100'**.
1468 scale_factor
1469 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray`
1470 if some axes need different scaling to be brought to domain
1471 **'100'**.
1472 dtype
1473 Data type used for the conversion to :class:`np.ndarray`.
1475 Returns
1476 -------
1477 :class:`numpy.ndarray`
1478 Array :math:`a` scaled to domain **'100'**.
1480 Examples
1481 --------
1482 With *Colour* domain-range scale set to **'Reference'**:
1484 >>> with domain_range_scale("Reference"):
1485 ... to_domain_100(1)
1486 array(1.0)
1488 With *Colour* domain-range scale set to **'1'**:
1490 >>> with domain_range_scale("1"):
1491 ... to_domain_100(1)
1492 array(100.0)
1494 With *Colour* domain-range scale set to **'100'** (unsupported):
1496 >>> with domain_range_scale("100"):
1497 ... to_domain_100(1)
1498 array(1.0)
1499 """
1501 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1503 a = as_float_array(a, dtype).copy()
1505 if _DOMAIN_RANGE_SCALE == "1":
1506 a *= as_float_array(scale_factor)
1508 return a
1511def to_domain_degrees(
1512 a: ArrayLike,
1513 scale_factor: ArrayLike = 360,
1514 dtype: Type[DTypeFloat] | None = None,
1515) -> NDArray:
1516 """
1517 Scale the specified array :math:`a` to degrees domain.
1519 The behaviour is as follows:
1521 - If *Colour* domain-range scale is **'Reference'**, the definition
1522 is almost entirely by-passed and will conveniently convert array
1523 :math:`a` to :class:`np.ndarray`.
1524 - If *Colour* domain-range scale is **'1'**, array :math:`a` is
1525 multiplied by ``scale_factor``, typically 360.
1526 - If *Colour* domain-range scale is **'100'** (currently unsupported
1527 private value only used for unit tests), array :math:`a` is
1528 multiplied by ``scale_factor`` / 100, typically 360 / 100.
1530 Parameters
1531 ----------
1532 a
1533 Array :math:`a` to scale to degrees domain.
1534 scale_factor
1535 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray`
1536 if some axes need different scaling to be brought to degrees domain.
1537 dtype
1538 Data type used for the conversion to :class:`np.ndarray`.
1540 Returns
1541 -------
1542 :class:`numpy.ndarray`
1543 Array :math:`a` scaled to degrees domain.
1545 Examples
1546 --------
1547 With *Colour* domain-range scale set to **'Reference'**:
1549 >>> with domain_range_scale("Reference"):
1550 ... to_domain_degrees(1)
1551 array(1.0)
1553 With *Colour* domain-range scale set to **'1'**:
1555 >>> with domain_range_scale("1"):
1556 ... to_domain_degrees(1)
1557 array(360.0)
1559 With *Colour* domain-range scale set to **'100'** (unsupported):
1561 >>> with domain_range_scale("100"):
1562 ... to_domain_degrees(1)
1563 array(3.6)
1564 """
1566 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1568 a = as_float_array(a, dtype).copy()
1570 if _DOMAIN_RANGE_SCALE == "1":
1571 a *= as_float_array(scale_factor)
1573 if _DOMAIN_RANGE_SCALE == "100":
1574 a *= as_float_array(scale_factor) / 100
1576 return a
1579def to_domain_int(
1580 a: ArrayLike,
1581 bit_depth: ArrayLike = 8,
1582 dtype: Type[DTypeFloat] | None = None,
1583) -> NDArray:
1584 """
1585 Scale the specified array :math:`a` to integer domain.
1587 The behaviour is as follows:
1589 - If *Colour* domain-range scale is **'Reference'**, the definition
1590 is almost entirely by-passed and will conveniently convert array
1591 :math:`a` to :class:`np.ndarray`.
1592 - If *Colour* domain-range scale is **'1'**, array :math:`a` is
1593 multiplied by :math:`2^{bit\\_depth} - 1`.
1594 - If *Colour* domain-range scale is **'100'** (currently unsupported
1595 private value only used for unit tests), array :math:`a` is
1596 multiplied by :math:`2^{bit\\_depth} - 1`.
1598 Parameters
1599 ----------
1600 a
1601 Array :math:`a` to scale to integer domain.
1602 bit_depth
1603 Bit-depth, usually *int* but can be a :class:`numpy.ndarray` if
1604 some axis need different scaling to be brought to integer domain.
1605 dtype
1606 Data type used for the conversion to :class:`np.ndarray`.
1608 Returns
1609 -------
1610 :class:`numpy.ndarray`
1611 Array :math:`a` scaled to integer domain.
1613 Notes
1614 -----
1615 - To avoid precision issues and rounding, the scaling is performed
1616 on *float* numbers.
1618 Examples
1619 --------
1620 With *Colour* domain-range scale set to **'Reference'**:
1622 >>> with domain_range_scale("Reference"):
1623 ... to_domain_int(1)
1624 array(1.0)
1626 With *Colour* domain-range scale set to **'1'**:
1628 >>> with domain_range_scale("1"):
1629 ... to_domain_int(1)
1630 array(255.0)
1632 With *Colour* domain-range scale set to **'100'** (unsupported):
1634 >>> with domain_range_scale("100"):
1635 ... to_domain_int(1)
1636 array(2.55)
1637 """
1639 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1641 a = as_float_array(a, dtype).copy()
1643 maximum_code_value: NDArray[DTypeInt] = np.power(2, bit_depth) - 1
1644 if _DOMAIN_RANGE_SCALE == "1":
1645 a *= maximum_code_value
1647 if _DOMAIN_RANGE_SCALE == "100":
1648 a *= maximum_code_value / 100
1650 return a
1653def from_range_1(
1654 a: ArrayLike,
1655 scale_factor: ArrayLike = 100,
1656 dtype: Type[DTypeFloat] | None = None,
1657) -> NDArray:
1658 """
1659 Scale the specified array :math:`a` from range **'1'**.
1661 The behaviour is as follows:
1663 - If *Colour* domain-range scale is **'Reference'** or **'1'**, the
1664 definition is entirely by-passed.
1665 - If *Colour* domain-range scale is **'100'** (currently unsupported
1666 private value only used for unit tests), array :math:`a` is
1667 multiplied by ``scale_factor``, typically 100.
1669 Parameters
1670 ----------
1671 a
1672 Array :math:`a` to scale from range **'1'**.
1673 scale_factor
1674 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray`
1675 if some axis need different scaling to be brought from range
1676 **'1'**.
1677 dtype
1678 Data type used for the conversion to :class:`np.ndarray`.
1680 Returns
1681 -------
1682 :class:`numpy.ndarray`
1683 Array :math:`a` scaled from range **'1'**.
1685 Warnings
1686 --------
1687 The scale conversion of variable :math:`a` happens in-place, i.e.,
1688 :math:`a` will be mutated!
1690 Examples
1691 --------
1692 With *Colour* domain-range scale set to **'Reference'**:
1694 >>> with domain_range_scale("Reference"):
1695 ... from_range_1(1)
1696 array(1.0)
1698 With *Colour* domain-range scale set to **'1'**:
1700 >>> with domain_range_scale("1"):
1701 ... from_range_1(1)
1702 array(1.0)
1704 With *Colour* domain-range scale set to **'100'** (unsupported):
1706 >>> with domain_range_scale("100"):
1707 ... from_range_1(1)
1708 array(100.0)
1709 """
1711 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1713 a = as_float_array(a, dtype)
1715 if _DOMAIN_RANGE_SCALE == "100":
1716 a *= as_float_array(scale_factor)
1718 return a
1721def from_range_10(
1722 a: ArrayLike,
1723 scale_factor: ArrayLike = 10,
1724 dtype: Type[DTypeFloat] | None = None,
1725) -> NDArray:
1726 """
1727 Scale the specified array :math:`a` from range **'10'**, used by the
1728 *Munsell Renotation System*.
1730 The behaviour is as follows:
1732 - If *Colour* domain-range scale is **'Reference'**, the definition
1733 is entirely by-passed.
1734 - If *Colour* domain-range scale is **'1'**, array :math:`a` is
1735 divided by ``scale_factor``, typically 10.
1736 - If *Colour* domain-range scale is **'100'** (currently unsupported
1737 private value only used for unit tests), array :math:`a` is
1738 multiplied by ``scale_factor``, typically 10.
1740 Parameters
1741 ----------
1742 a
1743 Array :math:`a` to scale from range **'10'**.
1744 scale_factor
1745 Scale factor, usually *numeric* but can be a
1746 :class:`numpy.ndarray` if some axis need different scaling to be
1747 brought from range **'10'**.
1748 dtype
1749 Data type used for the conversion to :class:`np.ndarray`.
1751 Returns
1752 -------
1753 :class:`numpy.ndarray`
1754 Array :math:`a` scaled from range **'10'**.
1756 Warnings
1757 --------
1758 The scale conversion of variable :math:`a` happens in-place, i.e.,
1759 :math:`a` will be mutated!
1761 Examples
1762 --------
1763 With *Colour* domain-range scale set to **'Reference'**:
1765 >>> with domain_range_scale("Reference"):
1766 ... from_range_10(1)
1767 array(1.0)
1769 With *Colour* domain-range scale set to **'1'**:
1771 >>> with domain_range_scale("1"):
1772 ... from_range_10(1)
1773 array(0.1)
1775 With *Colour* domain-range scale set to **'100'** (unsupported):
1777 >>> with domain_range_scale("100"):
1778 ... from_range_10(1)
1779 array(10.0)
1780 """
1782 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1784 a = as_float_array(a, dtype)
1786 if _DOMAIN_RANGE_SCALE == "1":
1787 a /= as_float_array(scale_factor)
1789 if _DOMAIN_RANGE_SCALE == "100":
1790 a *= as_float_array(scale_factor)
1792 return a
1795def from_range_100(
1796 a: ArrayLike,
1797 scale_factor: ArrayLike = 100,
1798 dtype: Type[DTypeFloat] | None = None,
1799) -> NDArray:
1800 """
1801 Scale the specified array :math:`a` from range **'100'**.
1803 The behaviour is as follows:
1805 - If *Colour* domain-range scale is **'Reference'** or **'100'**
1806 (currently unsupported private value only used for unit tests), the
1807 definition is entirely by-passed.
1808 - If *Colour* domain-range scale is **'1'**, array :math:`a` is
1809 divided by ``scale_factor``, typically 100.
1811 Parameters
1812 ----------
1813 a
1814 Array :math:`a` to scale from range **'100'**.
1815 scale_factor
1816 Scale factor, usually *numeric* but can be a :class:`numpy.ndarray`
1817 if some axes require different scaling to be brought from range
1818 **'100'**.
1819 dtype
1820 Data type used for the conversion to :class:`numpy.ndarray`.
1822 Returns
1823 -------
1824 :class:`numpy.ndarray`
1825 Array :math:`a` scaled from range **'100'**.
1827 Warnings
1828 --------
1829 The scale conversion of variable :math:`a` happens in-place, i.e.,
1830 :math:`a` will be mutated!
1832 Examples
1833 --------
1834 With *Colour* domain-range scale set to **'Reference'**:
1836 >>> with domain_range_scale("Reference"):
1837 ... from_range_100(1)
1838 array(1.0)
1840 With *Colour* domain-range scale set to **'1'**:
1842 >>> with domain_range_scale("1"):
1843 ... from_range_100(1)
1844 array(0.01)
1846 With *Colour* domain-range scale set to **'100'** (unsupported):
1848 >>> with domain_range_scale("100"):
1849 ... from_range_100(1)
1850 array(1.0)
1851 """
1853 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1855 a = as_float_array(a, dtype)
1857 if _DOMAIN_RANGE_SCALE == "1":
1858 a /= as_float_array(scale_factor)
1860 return a
1863def from_range_degrees(
1864 a: ArrayLike,
1865 scale_factor: ArrayLike = 360,
1866 dtype: Type[DTypeFloat] | None = None,
1867) -> NDArray:
1868 """
1869 Scale the specified array :math:`a` from degrees range.
1871 The behaviour is as follows:
1873 - If *Colour* domain-range scale is **'Reference'**, the definition
1874 is entirely by-passed.
1875 - If *Colour* domain-range scale is **'1'**, array :math:`a` is
1876 divided by ``scale_factor``, typically 360.
1877 - If *Colour* domain-range scale is **'100'** (currently unsupported
1878 private value only used for unit tests), array :math:`a` is
1879 divided by ``scale_factor`` / 100, typically 360 / 100.
1881 Parameters
1882 ----------
1883 a
1884 Array :math:`a` to scale from degrees range.
1885 scale_factor
1886 Scale factor, usually *numeric* but can be a
1887 :class:`numpy.ndarray` if some axes need different scaling to be
1888 brought from degrees range.
1889 dtype
1890 Data type used for the conversion to :class:`numpy.ndarray`.
1892 Returns
1893 -------
1894 :class:`numpy.ndarray`
1895 Array :math:`a` scaled from degrees range.
1897 Warnings
1898 --------
1899 The scale conversion of variable :math:`a` happens in-place, i.e.,
1900 :math:`a` will be mutated!
1902 Examples
1903 --------
1904 With *Colour* domain-range scale set to **'Reference'**:
1906 >>> with domain_range_scale("Reference"):
1907 ... from_range_degrees(1)
1908 array(1.0)
1910 With *Colour* domain-range scale set to **'1'**:
1912 >>> with domain_range_scale("1"):
1913 ... from_range_degrees(1) # doctest: +ELLIPSIS
1914 array(0.0027777...)
1916 With *Colour* domain-range scale set to **'100'** (unsupported):
1918 >>> with domain_range_scale("100"):
1919 ... from_range_degrees(1) # doctest: +ELLIPSIS
1920 array(0.2777777...)
1921 """
1923 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
1925 a = as_float_array(a, dtype)
1927 if _DOMAIN_RANGE_SCALE == "1":
1928 a /= as_float_array(scale_factor)
1930 if _DOMAIN_RANGE_SCALE == "100":
1931 a /= as_float_array(scale_factor) / 100
1933 return a
1936def from_range_int(
1937 a: ArrayLike,
1938 bit_depth: ArrayLike = 8,
1939 dtype: Type[DTypeFloat] | None = None,
1940) -> NDArray:
1941 """
1942 Scale the specified array :math:`a` from integer range.
1944 The behaviour is as follows:
1946 - If *Colour* domain-range scale is **'Reference'**, the definition
1947 is entirely by-passed.
1948 - If *Colour* domain-range scale is **'1'**, array :math:`a` is
1949 converted to :class:`np.ndarray` and divided by
1950 :math:`2^{bit\\_depth} - 1`.
1951 - If *Colour* domain-range scale is **'100'** (currently unsupported
1952 private value only used for unit tests), array :math:`a` is
1953 converted to :class:`np.ndarray` and divided by
1954 :math:`2^{bit\\_depth} - 1`.
1956 Parameters
1957 ----------
1958 a
1959 Array :math:`a` to scale from integer range.
1960 bit_depth
1961 Bit-depth, usually *int* but can be a :class:`numpy.ndarray` if
1962 some axes need different scaling to be brought from integer range.
1963 dtype
1964 Data type used for the conversion to :class:`np.ndarray`.
1966 Returns
1967 -------
1968 :class:`numpy.ndarray`
1969 Array :math:`a` scaled from integer range.
1971 Warnings
1972 --------
1973 The scale conversion of variable :math:`a` happens in-place, i.e.,
1974 :math:`a` will be mutated!
1976 Notes
1977 -----
1978 - To avoid precision issues and rounding, the scaling is performed on
1979 *float* numbers.
1981 Examples
1982 --------
1983 With *Colour* domain-range scale set to **'Reference'**:
1985 >>> with domain_range_scale("Reference"):
1986 ... from_range_int(1)
1987 array(1.0)
1989 With *Colour* domain-range scale set to **'1'**:
1991 >>> with domain_range_scale("1"):
1992 ... from_range_int(1) # doctest: +ELLIPSIS
1993 array(0.0039215...)
1995 With *Colour* domain-range scale set to **'100'** (unsupported):
1997 >>> with domain_range_scale("100"):
1998 ... from_range_int(1) # doctest: +ELLIPSIS
1999 array(0.3921568...)
2000 """
2002 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
2004 a = as_float_array(a, dtype)
2006 maximum_code_value: NDArray[DTypeInt] = np.power(2, bit_depth) - 1
2007 if _DOMAIN_RANGE_SCALE == "1":
2008 a /= maximum_code_value
2010 if _DOMAIN_RANGE_SCALE == "100":
2011 a /= maximum_code_value / 100
2013 return a
2016_NDARRAY_COPY_ENABLED: bool = True
2017"""
2018Global variable storing the current *Colour* state for
2019:class:`numpy.ndarray` copy.
2020"""
2023def is_ndarray_copy_enabled() -> bool:
2024 """
2025 Determine whether *Colour* :class:`numpy.ndarray` copy is enabled.
2027 Various API objects return a copy of their internal
2028 :class:`numpy.ndarray` for safety purposes, but this can be a slow
2029 operation impacting performance.
2031 Returns
2032 -------
2033 :class:`bool`
2034 Whether *Colour* :class:`numpy.ndarray` copy is enabled.
2036 Examples
2037 --------
2038 >>> with ndarray_copy_enable(False):
2039 ... is_ndarray_copy_enabled()
2040 False
2041 >>> with ndarray_copy_enable(True):
2042 ... is_ndarray_copy_enabled()
2043 True
2044 """
2046 return _NDARRAY_COPY_ENABLED
2049def set_ndarray_copy_enable(enable: bool) -> None:
2050 """
2051 Set the *Colour* :class:`numpy.ndarray` copy enabled state.
2053 Parameters
2054 ----------
2055 enable
2056 Whether to enable *Colour* :class:`numpy.ndarray` copy.
2058 Examples
2059 --------
2060 >>> with ndarray_copy_enable(is_ndarray_copy_enabled()):
2061 ... print(is_ndarray_copy_enabled())
2062 ... set_ndarray_copy_enable(False)
2063 ... print(is_ndarray_copy_enabled())
2064 True
2065 False
2066 """
2068 global _NDARRAY_COPY_ENABLED # noqa: PLW0603
2070 _NDARRAY_COPY_ENABLED = enable
2073class ndarray_copy_enable:
2074 """
2075 Define a context manager and decorator to temporarily set the *Colour*
2076 :class:`numpy.ndarray` copy enabled state.
2078 Parameters
2079 ----------
2080 enable
2081 Whether to enable or disable *Colour* :class:`numpy.ndarray` copy.
2082 """
2084 def __init__(self, enable: bool) -> None:
2085 self._enable = enable
2086 self._previous_state = is_ndarray_copy_enabled()
2088 def __enter__(self) -> Self:
2089 """
2090 Set the *Colour* :class:`numpy.ndarray` copy enabled state upon
2091 entering the context manager.
2092 """
2094 set_ndarray_copy_enable(self._enable)
2096 return self
2098 def __exit__(self, *args: Any) -> None:
2099 """
2100 Restore the *Colour* :class:`numpy.ndarray` copy enabled state upon
2101 exiting the context manager.
2102 """
2104 set_ndarray_copy_enable(self._previous_state)
2106 def __call__(self, function: Callable) -> Callable:
2107 """
2108 Decorate and call the specified function with array copy control.
2110 Parameters
2111 ----------
2112 function
2113 Function to be decorated with array copy state management.
2115 Returns
2116 -------
2117 :class:`Callable`
2118 Decorated function that executes within the configured array copy
2119 state context.
2120 """
2122 @functools.wraps(function)
2123 def wrapper(*args: Any, **kwargs: Any) -> Any:
2124 with self:
2125 return function(*args, **kwargs)
2127 return wrapper
2130def ndarray_copy(a: NDArray) -> NDArray:
2131 """
2132 Return a :class:`numpy.ndarray` copy if the relevant *Colour* state is
2133 enabled.
2135 Various API objects return a copy of their internal
2136 :class:`numpy.ndarray` for safety purposes, but this can be a slow
2137 operation impacting performance.
2139 Parameters
2140 ----------
2141 a
2142 Array :math:`a` to return a copy of.
2144 Returns
2145 -------
2146 :class:`numpy.ndarray`
2147 Array :math:`a` copy according to *Colour* state.
2149 Examples
2150 --------
2151 >>> a = np.linspace(0, 1, 10)
2152 >>> id(a) == id(ndarray_copy(a))
2153 False
2154 >>> with ndarray_copy_enable(False):
2155 ... id(a) == id(ndarray_copy(a))
2156 True
2157 """
2159 if _NDARRAY_COPY_ENABLED:
2160 return np.copy(a)
2161 return a
2164def closest_indexes(a: ArrayLike, b: ArrayLike) -> NDArray:
2165 """
2166 Return the closest element indexes from array :math:`a` to reference array
2167 :math:`b` elements.
2169 Parameters
2170 ----------
2171 a
2172 Array :math:`a` to search for the closest elements.
2173 b
2174 Reference array :math:`b`.
2176 Returns
2177 -------
2178 :class:`numpy.ndarray`
2179 Closest array :math:`a` element indexes.
2181 Examples
2182 --------
2183 >>> a = np.array(
2184 ... [
2185 ... 24.31357115,
2186 ... 63.62396289,
2187 ... 55.71528816,
2188 ... 62.70988028,
2189 ... 46.84480573,
2190 ... 25.40026416,
2191 ... ]
2192 ... )
2193 >>> print(closest_indexes(a, 63))
2194 [3]
2195 >>> print(closest_indexes(a, [63, 25]))
2196 [3 5]
2197 """
2199 a = np.ravel(a)[:, None]
2200 b = np.ravel(b)[None, :]
2202 return np.abs(a - b).argmin(axis=0)
2205def closest(a: ArrayLike, b: ArrayLike) -> NDArray:
2206 """
2207 Return the closest array :math:`a` elements to reference array
2208 :math:`b` elements.
2210 Parameters
2211 ----------
2212 a
2213 Array :math:`a` to search for the closest elements.
2214 b
2215 Reference array :math:`b`.
2217 Returns
2218 -------
2219 :class:`numpy.ndarray`
2220 Closest array :math:`a` elements.
2222 Examples
2223 --------
2224 >>> a = np.array(
2225 ... [
2226 ... 24.31357115,
2227 ... 63.62396289,
2228 ... 55.71528816,
2229 ... 62.70988028,
2230 ... 46.84480573,
2231 ... 25.40026416,
2232 ... ]
2233 ... )
2234 >>> closest(a, 63)
2235 array([ 62.70988028])
2236 >>> closest(a, [63, 25])
2237 array([ 62.70988028, 25.40026416])
2238 """
2240 a = np.array(a)
2242 return a[closest_indexes(a, b)]
2245_CACHE_DISTRIBUTION_INTERVAL: dict = CACHE_REGISTRY.register_cache(
2246 f"{__name__}._CACHE_DISTRIBUTION_INTERVAL"
2247)
2250def interval(distribution: ArrayLike, unique: bool = True) -> NDArray:
2251 """
2252 Return the interval size of the specified distribution.
2254 Parameters
2255 ----------
2256 distribution
2257 Distribution to retrieve the interval from.
2258 unique
2259 Whether to return unique intervals if the distribution is
2260 non-uniformly spaced or the complete intervals.
2262 Returns
2263 -------
2264 :class:`numpy.ndarray`
2265 Distribution interval.
2267 Examples
2268 --------
2269 Uniformly spaced variable:
2271 >>> y = np.array([1, 2, 3, 4, 5])
2272 >>> interval(y)
2273 array([ 1.])
2274 >>> interval(y, False)
2275 array([ 1., 1., 1., 1.])
2277 Non-uniformly spaced variable:
2279 >>> y = np.array([1, 2, 3, 4, 8])
2280 >>> interval(y)
2281 array([ 1., 4.])
2282 >>> interval(y, False)
2283 array([ 1., 1., 1., 4.])
2284 """
2286 distribution = as_float_array(distribution)
2287 hash_key = hash(
2288 (
2289 int_digest(distribution.tobytes()),
2290 distribution.shape,
2291 unique,
2292 )
2293 )
2295 if is_caching_enabled() and hash_key in _CACHE_DISTRIBUTION_INTERVAL:
2296 return np.copy(_CACHE_DISTRIBUTION_INTERVAL[hash_key])
2298 differences = np.abs(distribution[1:] - distribution[:-1])
2300 if unique and np.all(differences == differences[0]):
2301 interval_ = np.array([differences[0]])
2302 elif unique:
2303 interval_ = np.unique(differences)
2304 else:
2305 interval_ = differences
2307 _CACHE_DISTRIBUTION_INTERVAL[hash_key] = np.copy(interval_)
2309 return interval_
2312def is_uniform(distribution: ArrayLike) -> bool:
2313 """
2314 Determine whether the specified distribution is uniform.
2316 Parameters
2317 ----------
2318 distribution
2319 Distribution to check for uniformity.
2321 Returns
2322 -------
2323 :class:`bool`
2324 Whether the distribution is uniform.
2326 Examples
2327 --------
2328 Uniformly spaced variable:
2330 >>> a = np.array([1, 2, 3, 4, 5])
2331 >>> is_uniform(a)
2332 True
2334 Non-uniformly spaced variable:
2336 >>> a = np.array([1, 2, 3.1415, 4, 5])
2337 >>> is_uniform(a)
2338 False
2339 """
2341 return bool(interval(distribution).size == 1)
2344def in_array(a: ArrayLike, b: ArrayLike, tolerance: Real = EPSILON) -> NDArray:
2345 """
2346 Determine whether each element of array :math:`a` is present in array
2347 :math:`b` within the specified tolerance.
2349 Parameters
2350 ----------
2351 a
2352 Array :math:`a` to test the elements from.
2353 b
2354 Array :math:`b` against which to test the elements of array
2355 :math:`a`.
2356 tolerance
2357 Tolerance value.
2359 Returns
2360 -------
2361 :class:`numpy.ndarray`
2362 Boolean array with array :math:`a` shape indicating whether each
2363 element of array :math:`a` is present in array :math:`b` within the
2364 specified tolerance.
2366 References
2367 ----------
2368 :cite:`Yorke2014a`
2370 Examples
2371 --------
2372 >>> a = np.array([0.50, 0.60])
2373 >>> b = np.linspace(0, 10, 101)
2374 >>> np.isin(a, b)
2375 array([ True, False], dtype=bool)
2376 >>> in_array(a, b)
2377 array([ True, True], dtype=bool)
2378 """
2380 a = as_float_array(a)
2381 b = as_float_array(b)
2383 d = np.abs(np.ravel(a) - b[..., None])
2385 return np.reshape(np.any(d <= tolerance, axis=0), a.shape)
2388def tstack(
2389 a: ArrayLike,
2390 dtype: Type[DTypeBoolean] | Type[DTypeReal] | None = None,
2391) -> NDArray:
2392 """
2393 Stack the specified array of arrays :math:`a` along the last axis (tail)
2394 to produce a stacked array.
2396 Used to stack an array of arrays produced by the
2397 :func:`colour.utilities.tsplit` definition.
2399 Parameters
2400 ----------
2401 a
2402 Array of arrays :math:`a` to stack along the last axis.
2403 dtype
2404 :class:`numpy.dtype` to use for initial conversion to
2405 :class:`numpy.ndarray`, default to the :class:`numpy.dtype` defined
2406 by :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
2408 Returns
2409 -------
2410 :class:`numpy.ndarray`
2411 Stacked array.
2413 Examples
2414 --------
2415 >>> a = 0
2416 >>> tstack([a, a, a])
2417 array([ 0., 0., 0.])
2418 >>> a = np.arange(0, 6)
2419 >>> tstack([a, a, a])
2420 array([[ 0., 0., 0.],
2421 [ 1., 1., 1.],
2422 [ 2., 2., 2.],
2423 [ 3., 3., 3.],
2424 [ 4., 4., 4.],
2425 [ 5., 5., 5.]])
2426 >>> a = np.reshape(a, (1, 6))
2427 >>> tstack([a, a, a])
2428 array([[[ 0., 0., 0.],
2429 [ 1., 1., 1.],
2430 [ 2., 2., 2.],
2431 [ 3., 3., 3.],
2432 [ 4., 4., 4.],
2433 [ 5., 5., 5.]]])
2434 >>> a = np.reshape(a, (1, 1, 6))
2435 >>> tstack([a, a, a])
2436 array([[[[ 0., 0., 0.],
2437 [ 1., 1., 1.],
2438 [ 2., 2., 2.],
2439 [ 3., 3., 3.],
2440 [ 4., 4., 4.],
2441 [ 5., 5., 5.]]]])
2442 """
2444 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
2446 a = as_array(a, dtype)
2448 return np.concatenate([x[..., np.newaxis] for x in a], axis=-1)
2451def tsplit(
2452 a: ArrayLike,
2453 dtype: Type[DTypeBoolean] | Type[DTypeReal] | None = None,
2454) -> NDArray:
2455 """
2456 Split the specified stacked array :math:`a` along the last axis (tail)
2457 to produce an array of arrays.
2459 Used to split a stacked array produced by the :func:`colour.utilities.tstack`
2460 definition.
2462 Parameters
2463 ----------
2464 a
2465 Stacked array :math:`a` to split.
2466 dtype
2467 :class:`numpy.dtype` to use for initial conversion to
2468 :class:`numpy.ndarray`, default to the :class:`numpy.dtype` defined
2469 by :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
2471 Returns
2472 -------
2473 :class:`numpy.ndarray`
2474 Array of arrays.
2476 Examples
2477 --------
2478 >>> a = np.array([0, 0, 0])
2479 >>> tsplit(a)
2480 array([ 0., 0., 0.])
2481 >>> a = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2], [3, 3, 3], [4, 4, 4], [5, 5, 5]])
2482 >>> tsplit(a)
2483 array([[ 0., 1., 2., 3., 4., 5.],
2484 [ 0., 1., 2., 3., 4., 5.],
2485 [ 0., 1., 2., 3., 4., 5.]])
2486 >>> a = np.array(
2487 ... [
2488 ... [
2489 ... [0, 0, 0],
2490 ... [1, 1, 1],
2491 ... [2, 2, 2],
2492 ... [3, 3, 3],
2493 ... [4, 4, 4],
2494 ... [5, 5, 5],
2495 ... ]
2496 ... ]
2497 ... )
2498 >>> tsplit(a)
2499 array([[[ 0., 1., 2., 3., 4., 5.]],
2500 <BLANKLINE>
2501 [[ 0., 1., 2., 3., 4., 5.]],
2502 <BLANKLINE>
2503 [[ 0., 1., 2., 3., 4., 5.]]])
2504 """
2506 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
2508 a = as_array(a, dtype)
2510 return np.array([a[..., x] for x in range(a.shape[-1])])
2513def row_as_diagonal(a: ArrayLike) -> NDArray:
2514 """
2515 Return the rows of the specified array :math:`a` as diagonal matrices.
2517 Parameters
2518 ----------
2519 a
2520 Array :math:`a` to return the rows of as diagonal matrices.
2522 Returns
2523 -------
2524 :class:`numpy.ndarray`
2525 Array :math:`a` rows as diagonal matrices.
2527 References
2528 ----------
2529 :cite:`Castro2014a`
2531 Examples
2532 --------
2533 >>> a = np.array(
2534 ... [
2535 ... [0.25891593, 0.07299478, 0.36586996],
2536 ... [0.30851087, 0.37131459, 0.16274825],
2537 ... [0.71061831, 0.67718718, 0.09562581],
2538 ... [0.71588836, 0.76772047, 0.15476079],
2539 ... [0.92985142, 0.22263399, 0.88027331],
2540 ... ]
2541 ... )
2542 >>> row_as_diagonal(a)
2543 array([[[ 0.25891593, 0. , 0. ],
2544 [ 0. , 0.07299478, 0. ],
2545 [ 0. , 0. , 0.36586996]],
2546 <BLANKLINE>
2547 [[ 0.30851087, 0. , 0. ],
2548 [ 0. , 0.37131459, 0. ],
2549 [ 0. , 0. , 0.16274825]],
2550 <BLANKLINE>
2551 [[ 0.71061831, 0. , 0. ],
2552 [ 0. , 0.67718718, 0. ],
2553 [ 0. , 0. , 0.09562581]],
2554 <BLANKLINE>
2555 [[ 0.71588836, 0. , 0. ],
2556 [ 0. , 0.76772047, 0. ],
2557 [ 0. , 0. , 0.15476079]],
2558 <BLANKLINE>
2559 [[ 0.92985142, 0. , 0. ],
2560 [ 0. , 0.22263399, 0. ],
2561 [ 0. , 0. , 0.88027331]]])
2562 """
2564 d = as_array(a)
2566 d = np.expand_dims(d, -2)
2568 return np.eye(d.shape[-1]) * d
2571def orient(
2572 a: ArrayLike,
2573 orientation: (
2574 Literal["Ignore", "Flip", "Flop", "90 CW", "90 CCW", "180"] | str
2575 ) = "Ignore",
2576) -> NDArray:
2577 """
2578 Orient the specified array :math:`a` using the specified orientation.
2580 Parameters
2581 ----------
2582 a
2583 Array :math:`a` to orient.
2584 orientation
2585 Orientation to perform.
2587 Returns
2588 -------
2589 :class:`numpy.ndarray`
2590 Oriented array.
2592 Examples
2593 --------
2594 >>> a = np.tile(np.arange(5), (5, 1))
2595 >>> a
2596 array([[0, 1, 2, 3, 4],
2597 [0, 1, 2, 3, 4],
2598 [0, 1, 2, 3, 4],
2599 [0, 1, 2, 3, 4],
2600 [0, 1, 2, 3, 4]])
2601 >>> orient(a, "90 CW")
2602 array([[ 0., 0., 0., 0., 0.],
2603 [ 1., 1., 1., 1., 1.],
2604 [ 2., 2., 2., 2., 2.],
2605 [ 3., 3., 3., 3., 3.],
2606 [ 4., 4., 4., 4., 4.]])
2607 >>> orient(a, "Flip")
2608 array([[ 4., 3., 2., 1., 0.],
2609 [ 4., 3., 2., 1., 0.],
2610 [ 4., 3., 2., 1., 0.],
2611 [ 4., 3., 2., 1., 0.],
2612 [ 4., 3., 2., 1., 0.]])
2613 """
2615 a = as_float_array(a)
2617 orientation = validate_method(
2618 orientation, ("Ignore", "Flip", "Flop", "90 CW", "90 CCW", "180")
2619 )
2621 if orientation == "ignore":
2622 oriented = a
2623 elif orientation == "flip":
2624 oriented = np.fliplr(a)
2625 elif orientation == "flop":
2626 oriented = np.flipud(a)
2627 elif orientation == "90 cw":
2628 oriented = np.rot90(a, 3)
2629 elif orientation == "90 ccw":
2630 oriented = np.rot90(a)
2631 elif orientation == "180":
2632 oriented = np.rot90(a, 2)
2634 return oriented
2637def centroid(a: ArrayLike) -> NDArrayInt:
2638 """
2639 Return the centroid indexes of the specified array :math:`a`.
2641 Parameters
2642 ----------
2643 a
2644 Array :math:`a` to return the centroid indexes of.
2646 Returns
2647 -------
2648 :class:`numpy.ndarray`
2649 Centroid indexes of array :math:`a`.
2651 Examples
2652 --------
2653 >>> a = np.tile(np.arange(0, 5), (5, 1))
2654 >>> centroid(a) # doctest: +ELLIPSIS
2655 array([2, 3]...)
2656 """
2658 a = as_float_array(a)
2660 a_s = np.sum(a)
2662 ranges = [np.arange(0, a.shape[i]) for i in range(a.ndim)]
2663 coordinates = np.meshgrid(*ranges)
2665 a_ci = []
2666 for axis in coordinates:
2667 axis = np.transpose(axis) # noqa: PLW2901
2668 # Aligning axis for N-D arrays where N is normalised to
2669 # range [3, :math:`\\\infty`]
2670 for i in range(axis.ndim - 2, 0, -1):
2671 axis = np.rollaxis(axis, i - 1, axis.ndim) # noqa: PLW2901
2673 a_ci.append(np.sum(axis * a) // a_s)
2675 # NOTE: Cannot use `as_int_array` as presence of NaN will raise a ValueError
2676 # exception.
2677 return np.array(a_ci).astype(DTYPE_INT_DEFAULT)
2680def fill_nan(
2681 a: ArrayLike,
2682 method: Literal["Interpolation", "Constant"] | str = "Interpolation",
2683 default: Real = 0,
2684) -> NDArray:
2685 """
2686 Fill the NaN values in the specified array :math:`a` using the specified
2687 method.
2689 Parameters
2690 ----------
2691 a
2692 Array :math:`a` to fill the NaNs of.
2693 method
2694 *Interpolation* method linearly interpolates through the NaN values,
2695 *Constant* method replaces NaN values with ``default``.
2696 default
2697 Value to use with the *Constant* method.
2699 Returns
2700 -------
2701 :class:`numpy.ndarray`
2702 NaN-filled array :math:`a`.
2704 Examples
2705 --------
2706 >>> a = np.array([0.1, 0.2, np.nan, 0.4, 0.5])
2707 >>> fill_nan(a)
2708 array([ 0.1, 0.2, 0.3, 0.4, 0.5])
2709 >>> fill_nan(a, method="Constant")
2710 array([ 0.1, 0.2, 0. , 0.4, 0.5])
2711 """
2713 a = np.array(a, copy=True)
2714 method = validate_method(method, ("Interpolation", "Constant"))
2716 mask = np.isnan(a)
2718 if method == "interpolation":
2719 a[mask] = np.interp(np.flatnonzero(mask), np.flatnonzero(~mask), a[~mask])
2720 elif method == "constant":
2721 a[mask] = default
2723 return a
2726def has_only_nan(a: ArrayLike) -> bool:
2727 """
2728 Return whether the specified array :math:`a` contains only *NaN* values.
2730 Parameters
2731 ----------
2732 a
2733 Array :math:`a` to check whether it contains only *NaN* values.
2735 Returns
2736 -------
2737 :class:`bool`
2738 Whether array :math:`a` contains only *NaN* values.
2740 Examples
2741 --------
2742 >>> has_only_nan(None)
2743 True
2744 >>> has_only_nan([None, None])
2745 True
2746 >>> has_only_nan([True, None])
2747 False
2748 >>> has_only_nan([0.1, np.nan, 0.3])
2749 False
2750 """
2752 a = as_float_array(a)
2754 return bool(np.all(np.isnan(a)))
2757@contextmanager
2758def ndarray_write(a: ArrayLike) -> Generator:
2759 """
2760 Define a context manager that temporarily sets the specified array
2761 :math:`a` to writeable for operations, then restores it to read-only.
2763 Parameters
2764 ----------
2765 a
2766 Array :math:`a` to operate on.
2768 Yields
2769 ------
2770 Generator
2771 Array :math:`a` made temporarily writeable.
2773 Examples
2774 --------
2775 >>> a = np.linspace(0, 1, 10)
2776 >>> a.setflags(write=False)
2777 >>> try:
2778 ... a += 1
2779 ... except ValueError:
2780 ... pass
2781 >>> with ndarray_write(a):
2782 ... a += 1
2783 """
2785 a = as_float_array(a)
2787 a.setflags(write=True)
2789 try:
2790 yield a
2791 finally:
2792 a.setflags(write=False)
2795def zeros(
2796 shape: int | Sequence[int],
2797 dtype: Type[DTypeReal] | None = None,
2798 order: Literal["C", "F"] = "C",
2799) -> NDArray:
2800 """
2801 Create an array of zeros with the active dtype.
2803 Wrap :func:`np.zeros` definition to create an array with the active
2804 :class:`numpy.dtype` defined by the
2805 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
2807 Parameters
2808 ----------
2809 shape
2810 Shape of the new array, e.g., ``(2, 3)`` or ``2``.
2811 dtype
2812 :class:`numpy.dtype` to use for conversion, default to the
2813 :class:`numpy.dtype` defined by the
2814 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
2815 order
2816 Whether to store multi-dimensional data in row-major
2817 (C-style) or column-major (Fortran-style) order in memory.
2819 Returns
2820 -------
2821 :class:`numpy.ndarray`
2822 Array of the specified shape and :class:`numpy.dtype`, filled
2823 with zeros.
2825 Examples
2826 --------
2827 >>> zeros(3)
2828 array([ 0., 0., 0.])
2829 """
2831 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
2833 return np.zeros(shape, dtype, order)
2836def ones(
2837 shape: int | Sequence[int],
2838 dtype: Type[DTypeReal] | None = None,
2839 order: Literal["C", "F"] = "C",
2840) -> NDArray:
2841 """
2842 Create an array of ones with the active dtype.
2844 Wrap :func:`np.ones` definition to create an array with the active
2845 :class:`numpy.dtype` defined by the
2846 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
2848 Parameters
2849 ----------
2850 shape
2851 Shape of the new array, e.g., ``(2, 3)`` or ``2``.
2852 dtype
2853 :class:`numpy.dtype` to use for conversion, default to the
2854 :class:`numpy.dtype` defined by the
2855 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
2856 order
2857 Whether to store multi-dimensional data in row-major (C-style) or
2858 column-major (Fortran-style) order in memory.
2860 Returns
2861 -------
2862 :class:`numpy.ndarray`
2863 Array of the specified shape and :class:`numpy.dtype`, filled with ones.
2865 Examples
2866 --------
2867 >>> ones(3)
2868 array([ 1., 1., 1.])
2869 """
2871 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
2873 return np.ones(shape, dtype, order)
2876def full(
2877 shape: int | Sequence[int],
2878 fill_value: Real,
2879 dtype: Type[DTypeReal] | None = None,
2880 order: Literal["C", "F"] = "C",
2881) -> NDArray:
2882 """
2883 Create an array of the specified value with the active dtype.
2885 Wrap :func:`np.full` definition to create an array with the active
2886 :class:`numpy.dtype` defined by the
2887 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
2889 Parameters
2890 ----------
2891 shape
2892 Shape of the new array, e.g., ``(2, 3)`` or ``2``.
2893 fill_value
2894 Fill value.
2895 dtype
2896 :class:`numpy.dtype` to use for conversion, default to the
2897 :class:`numpy.dtype` defined by the
2898 :attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
2899 order
2900 Whether to store multi-dimensional data in row-major (C-style) or
2901 column-major (Fortran-style) order in memory.
2903 Returns
2904 -------
2905 :class:`numpy.ndarray`
2906 Array of the specified shape and :class:`numpy.dtype`, filled with
2907 the specified value.
2909 Examples
2910 --------
2911 >>> full(3, 2.5)
2912 array([ 2.5, 2.5, 2.5])
2913 """
2915 dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
2917 return np.full(shape, fill_value, dtype, order)
2920def index_along_last_axis(a: ArrayLike, indexes: ArrayLike) -> NDArray:
2921 """
2922 Reduce the dimension of array :math:`a` by one, using an array of
2923 indexes to select elements from the last axis.
2925 Parameters
2926 ----------
2927 a
2928 Array :math:`a` to be indexed.
2929 indexes
2930 *Integer* array with the same shape as :math:`a` but with one
2931 dimension fewer, containing indices to the last dimension of
2932 :math:`a`. All elements must be numbers between 0 and
2933 :math:`m - 1`.
2935 Returns
2936 -------
2937 :class:`numpy.ndarray`
2938 Indexed array :math:`a`.
2940 Raises
2941 ------
2942 :class:`ValueError`
2943 If the array :math:`a` and ``indexes`` have incompatible shapes.
2944 :class:`IndexError`
2945 If ``indexes`` has elements outside of the allowed range of 0 to
2946 :math:`m - 1` or if it is not an *integer* array.
2948 Examples
2949 --------
2950 >>> a = np.array(
2951 ... [
2952 ... [
2953 ... [0.3, 0.5, 6.9],
2954 ... [3.3, 4.4, 1.6],
2955 ... [4.4, 7.5, 2.3],
2956 ... [2.3, 1.6, 7.4],
2957 ... ],
2958 ... [
2959 ... [2.0, 5.9, 2.8],
2960 ... [6.2, 4.9, 8.6],
2961 ... [3.7, 9.7, 7.3],
2962 ... [6.3, 4.3, 3.2],
2963 ... ],
2964 ... [
2965 ... [0.8, 1.9, 0.7],
2966 ... [5.6, 4.0, 1.7],
2967 ... [6.7, 8.2, 1.7],
2968 ... [1.2, 7.1, 1.4],
2969 ... ],
2970 ... [
2971 ... [4.0, 4.8, 8.9],
2972 ... [4.0, 0.3, 6.9],
2973 ... [3.5, 7.1, 4.5],
2974 ... [1.4, 1.9, 1.6],
2975 ... ],
2976 ... ]
2977 ... )
2978 >>> indexes = np.array([[2, 0, 1, 1], [2, 1, 1, 0], [0, 0, 1, 2], [0, 0, 1, 2]])
2979 >>> index_along_last_axis(a, indexes)
2980 array([[ 6.9, 3.3, 7.5, 1.6],
2981 [ 2.8, 4.9, 9.7, 6.3],
2982 [ 0.8, 5.6, 8.2, 1.4],
2983 [ 4. , 4. , 7.1, 1.6]])
2985 This function can be used to compute the result of :func:`np.min` along
2986 the last axis given the corresponding :func:`np.argmin` indexes.
2988 >>> indexes = np.argmin(a, axis=-1)
2989 >>> np.array_equal(index_along_last_axis(a, indexes), np.min(a, axis=-1))
2990 True
2992 In particular, this can be used to manipulate the indexes specified by
2993 functions like :func:`np.min` before indexing the array. For example, to
2994 get elements directly following the smallest elements:
2996 >>> index_along_last_axis(a, (indexes + 1) % 3)
2997 array([[ 0.5, 3.3, 4.4, 7.4],
2998 [ 5.9, 8.6, 9.7, 6.3],
2999 [ 0.8, 5.6, 6.7, 7.1],
3000 [ 4.8, 6.9, 7.1, 1.9]])
3001 """
3003 a = np.array(a)
3004 indexes = np.array(indexes)
3006 if a.shape[:-1] != indexes.shape:
3007 error = (
3008 f"Array and indexes have incompatible shapes: {a.shape} and {indexes.shape}"
3009 )
3011 raise ValueError(error)
3013 return np.take_along_axis(a, indexes[..., None], axis=-1).squeeze(axis=-1)
3016def format_array_as_row(a: ArrayLike, decimals: int = 7, separator: str = " ") -> str:
3017 """
3018 Format the specified array :math:`a` as a row.
3020 Parameters
3021 ----------
3022 a
3023 Array to format.
3024 decimals
3025 Decimal count to use when formatting as a row.
3026 separator
3027 Separator used to join the array :math:`a` items.
3029 Returns
3030 -------
3031 :class:`str`
3032 Array formatted as a row.
3034 Examples
3035 --------
3036 >>> format_array_as_row([1.25, 2.5, 3.75])
3037 '1.2500000 2.5000000 3.7500000'
3038 >>> format_array_as_row([1.25, 2.5, 3.75], 3)
3039 '1.250 2.500 3.750'
3040 >>> format_array_as_row([1.25, 2.5, 3.75], 3, ", ")
3041 '1.250, 2.500, 3.750'
3042 """
3044 a = np.ravel(a)
3046 return separator.join(
3047 "{1:0.{0}f}".format(decimals, x)
3048 for x in a # noqa: PLE1300, RUF100
3049 )