Coverage for algebra/common.py: 69%
148 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"""
2Common Utilities
3================
5Define common algebra utility objects that do not fall within any specific
6category.
8The *Common* sub-package provides general-purpose mathematical and
9computational utilities used throughout the colour science library.
10"""
12from __future__ import annotations
14import functools
15import typing
17import numpy as np
19if typing.TYPE_CHECKING:
20 from colour.hints import (
21 Any,
22 ArrayLike,
23 Callable,
24 DTypeFloat,
25 NDArray,
26 NDArrayFloat,
27 Self,
28 Tuple,
29 )
31from colour.constants import EPSILON
32from colour.hints import Literal, cast
33from colour.utilities import (
34 as_float,
35 as_float_array,
36 optional,
37 runtime_warning,
38 tsplit,
39 validate_method,
40)
42__author__ = "Colour Developers"
43__copyright__ = "Copyright 2013 Colour Developers"
44__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
45__maintainer__ = "Colour Developers"
46__email__ = "colour-developers@colour-science.org"
47__status__ = "Production"
49__all__ = [
50 "get_sdiv_mode",
51 "set_sdiv_mode",
52 "sdiv_mode",
53 "sdiv",
54 "is_spow_enabled",
55 "set_spow_enable",
56 "spow_enable",
57 "spow",
58 "normalise_vector",
59 "normalise_maximum",
60 "vecmul",
61 "euclidean_distance",
62 "manhattan_distance",
63 "linear_conversion",
64 "linstep_function",
65 "lerp",
66 "smoothstep_function",
67 "smooth",
68 "is_identity",
69 "eigen_decomposition",
70]
72_SDIV_MODE: Literal[
73 "Numpy",
74 "Ignore",
75 "Warning",
76 "Raise",
77 "Ignore Zero Conversion",
78 "Warning Zero Conversion",
79 "Ignore Limit Conversion",
80 "Warning Limit Conversion",
81 "Replace With Epsilon",
82 "Warning Replace With Epsilon",
83] = "Ignore Zero Conversion"
84"""
85Global variable storing the current *Colour* safe division function mode.
86"""
89def get_sdiv_mode() -> Literal[
90 "Numpy",
91 "Ignore",
92 "Warning",
93 "Raise",
94 "Ignore Zero Conversion",
95 "Warning Zero Conversion",
96 "Ignore Limit Conversion",
97 "Warning Limit Conversion",
98 "Replace With Epsilon",
99 "Warning Replace With Epsilon",
100]:
101 """
102 Return the current *Colour* safe division mode.
104 Returns
105 -------
106 :class:`str`
107 Current *Colour* safe division mode. See
108 :func:`colour.algebra.sdiv` definition for an explanation of
109 the possible modes.
111 Examples
112 --------
113 >>> with sdiv_mode("Numpy"):
114 ... get_sdiv_mode()
115 'numpy'
116 >>> with sdiv_mode("Ignore Zero Conversion"):
117 ... get_sdiv_mode()
118 'ignore zero conversion'
119 """
121 return _SDIV_MODE
124def set_sdiv_mode(
125 mode: (
126 Literal[
127 "Numpy",
128 "Ignore",
129 "Warning",
130 "Raise",
131 "Ignore Zero Conversion",
132 "Warning Zero Conversion",
133 "Ignore Limit Conversion",
134 "Warning Limit Conversion",
135 "Replace With Epsilon",
136 "Warning Replace With Epsilon",
137 ]
138 | str
139 ),
140) -> None:
141 """
142 Set the *Colour* safe division function mode.
144 Parameters
145 ----------
146 mode
147 *Colour* safe division mode. See :func:`colour.algebra.sdiv`
148 definition for an explanation of the possible modes.
150 Examples
151 --------
152 >>> with sdiv_mode(get_sdiv_mode()):
153 ... print(get_sdiv_mode())
154 ... set_sdiv_mode("Raise")
155 ... print(get_sdiv_mode())
156 ignore zero conversion
157 raise
158 """
160 global _SDIV_MODE # noqa: PLW0603
162 _SDIV_MODE = cast(
163 "Literal['Numpy', 'Ignore', 'Warning', 'Raise', " # pyright: ignore
164 "'Ignore Zero Conversion', 'Warning Zero Conversion', "
165 "'Ignore Limit Conversion', 'Warning Limit Conversion', "
166 "'Replace With Epsilon', 'Warning Replace With Epsilon']",
167 validate_method(
168 mode,
169 (
170 "Numpy",
171 "Ignore",
172 "Warning",
173 "Raise",
174 "Ignore Zero Conversion",
175 "Warning Zero Conversion",
176 "Ignore Limit Conversion",
177 "Warning Limit Conversion",
178 "Replace With Epsilon",
179 "Warning Replace With Epsilon",
180 ),
181 ),
182 )
185class sdiv_mode:
186 """
187 Context manager and decorator for temporarily modifying *Colour* safe
188 division function mode.
190 This utility enables temporary modification of the safe division behavior
191 in *Colour* computations, allowing control over how division operations
192 handle edge cases such as division by zero or near-zero values. The
193 context manager ensures automatic restoration of the original mode upon
194 exit.
196 Parameters
197 ----------
198 mode
199 *Colour* safe division function mode, see :func:`colour.algebra.sdiv`
200 definition for an explanation about the possible modes.
201 """
203 def __init__(
204 self,
205 mode: (
206 Literal[
207 "Numpy",
208 "Ignore",
209 "Warning",
210 "Raise",
211 "Ignore Zero Conversion",
212 "Warning Zero Conversion",
213 "Ignore Limit Conversion",
214 "Warning Limit Conversion",
215 "Replace With Epsilon",
216 "Warning Replace With Epsilon",
217 ]
218 | None
219 ) = None,
220 ) -> None:
221 self._mode = optional(mode, get_sdiv_mode())
222 self._previous_mode = get_sdiv_mode()
224 def __enter__(self) -> Self:
225 """
226 Set the *Colour* safe/symmetrical power function state to the
227 specified value upon entering the context manager.
228 """
230 set_sdiv_mode(self._mode)
232 return self
234 def __exit__(self, *args: Any) -> None:
235 """
236 Restore the *Colour* safe / symmetrical power function enabled state
237 upon exiting the context manager.
238 """
240 set_sdiv_mode(self._previous_mode)
242 def __call__(self, function: Callable) -> Callable:
243 """
244 Call the wrapped definition.
246 The decorator applies the specified spectral power distribution
247 state to the wrapped function during its execution.
248 """
250 @functools.wraps(function)
251 def wrapper(*args: Any, **kwargs: Any) -> Any:
252 with self:
253 return function(*args, **kwargs)
255 return wrapper
258def sdiv(a: ArrayLike, b: ArrayLike) -> NDArrayFloat:
259 """
260 Perform safe division of array :math:`a` by array :math:`b` while
261 handling zero-division cases.
263 Avoid NaN and +/- inf generation when array :math:`b` contains zero
264 values. The zero-division handling behaviour is controlled by the
265 :func:`colour.algebra.set_sdiv_mode` definition or the
266 :func:`sdiv_mode` context manager. The following modes are available:
268 - ``Numpy``: The current *Numpy* zero-division handling occurs.
269 - ``Ignore``: Zero-division occurs silently.
270 - ``Warning``: Zero-division occurs with a warning.
271 - ``Ignore Zero Conversion``: Zero-division occurs silently and NaNs
272 or +/- infs values are converted to zeros. See
273 :func:`numpy.nan_to_num` definition for more details.
274 - ``Warning Zero Conversion``: Zero-division occurs with a warning
275 and NaNs or +/- infs values are converted to zeros. See
276 :func:`numpy.nan_to_num` definition for more details.
277 - ``Ignore Limit Conversion``: Zero-division occurs silently and
278 NaNs or +/- infs values are converted to zeros or the largest +/-
279 finite floating point values representable by the division result
280 :class:`numpy.dtype`. See :func:`numpy.nan_to_num` definition for
281 more details.
282 - ``Warning Limit Conversion``: Zero-division occurs with a warning
283 and NaNs or +/- infs values are converted to zeros or the largest
284 +/- finite floating point values representable by the division
285 result :class:`numpy.dtype`.
286 - ``Replace With Epsilon``: Zero-division is avoided by replacing
287 zero denominators with the machine epsilon value from
288 :attr:`colour.constants.EPSILON`.
289 - ``Warning Replace With Epsilon``: Zero-division is avoided by
290 replacing zero denominators with the machine epsilon value from
291 :attr:`colour.constants.EPSILON` with a warning.
293 Parameters
294 ----------
295 a
296 Numerator array :math:`a`.
297 b
298 Denominator array :math:`b`.
300 Returns
301 -------
302 :class:`np.float` or :class:`numpy.ndarray`
303 Array :math:`a` safely divided by :math:`b`.
305 Examples
306 --------
307 >>> a = np.array([0, 1, 2])
308 >>> b = np.array([2, 1, 0])
309 >>> sdiv(a, b)
310 array([ 0., 1., 0.])
311 >>> try:
312 ... with sdiv_mode("Raise"):
313 ... sdiv(a, b)
314 ... except Exception as error:
315 ... error # doctest: +ELLIPSIS
316 FloatingPointError('divide by zero encountered in...divide')
317 >>> with sdiv_mode("Ignore Zero Conversion"):
318 ... sdiv(a, b)
319 array([ 0., 1., 0.])
320 >>> with sdiv_mode("Warning Zero Conversion"):
321 ... sdiv(a, b)
322 array([ 0., 1., 0.])
323 >>> with sdiv_mode("Ignore Limit Conversion"):
324 ... sdiv(a, b) # doctest: +SKIP
325 array([ 0.00000000e+000, 1.00000000e+000, 1.79769313e+308])
326 >>> with sdiv_mode("Warning Limit Conversion"):
327 ... sdiv(a, b) # doctest: +SKIP
328 array([ 0.00000000e+000, 1.00000000e+000, 1.79769313e+308])
329 >>> with sdiv_mode("Replace With Epsilon"):
330 ... sdiv(a, b) # doctest: +ELLIPSIS
331 array([ 0.00000000e+00, 1.00000000e+00, ...])
332 >>> with sdiv_mode("Warning Replace With Epsilon"):
333 ... sdiv(a, b) # doctest: +ELLIPSIS
334 array([ 0.00000000e+00, 1.00000000e+00, ...])
335 """
337 a = as_float_array(a)
338 b = as_float_array(b)
340 mode = validate_method(
341 _SDIV_MODE,
342 (
343 "Numpy",
344 "Ignore",
345 "Warning",
346 "Raise",
347 "Ignore Zero Conversion",
348 "Warning Zero Conversion",
349 "Ignore Limit Conversion",
350 "Warning Limit Conversion",
351 "Replace With Epsilon",
352 "Warning Replace With Epsilon",
353 ),
354 )
356 if mode == "numpy":
357 c = a / b
358 elif mode == "ignore":
359 with np.errstate(divide="ignore", invalid="ignore"):
360 c = a / b
361 elif mode == "warning":
362 with np.errstate(divide="warn", invalid="warn"):
363 c = a / b
364 elif mode == "raise":
365 with np.errstate(divide="raise", invalid="raise"):
366 c = a / b
367 elif mode == "ignore zero conversion":
368 with np.errstate(divide="ignore", invalid="ignore"):
369 c = np.nan_to_num(a / b, nan=0, posinf=0, neginf=0)
370 elif mode == "warning zero conversion":
371 with np.errstate(divide="warn", invalid="warn"):
372 c = np.nan_to_num(a / b, nan=0, posinf=0, neginf=0)
373 elif mode == "ignore limit conversion":
374 with np.errstate(divide="ignore", invalid="ignore"):
375 c = np.nan_to_num(a / b)
376 elif mode == "warning limit conversion":
377 with np.errstate(divide="warn", invalid="warn"):
378 c = np.nan_to_num(a / b)
379 elif mode == "replace with epsilon":
380 b = np.where(b == 0, EPSILON, b)
381 c = a / b
382 elif mode == "warning replace with epsilon":
383 if np.any(b == 0):
384 runtime_warning("Zero(s) detected in denominator, replacing with EPSILON.")
385 b = np.where(b == 0, EPSILON, b)
386 c = a / b
388 return c
391_SPOW_ENABLED: bool = True
392"""
393Global variable storing the current *Colour* safe / symmetrical power function
394enabled state.
395"""
398def is_spow_enabled() -> bool:
399 """
400 Return whether *Colour* safe / symmetrical power function is enabled.
402 Returns
403 -------
404 :class:`bool`
405 Whether *Colour* safe / symmetrical power function is enabled.
407 Examples
408 --------
409 >>> with spow_enable(False):
410 ... is_spow_enabled()
411 False
412 >>> with spow_enable(True):
413 ... is_spow_enabled()
414 True
415 """
417 return _SPOW_ENABLED
420def set_spow_enable(enable: bool) -> None:
421 """
422 Set the *Colour* safe/symmetrical power function enabled state.
424 Parameters
425 ----------
426 enable
427 Whether to enable the *Colour* safe/symmetrical power function.
429 Examples
430 --------
431 >>> with spow_enable(is_spow_enabled()):
432 ... print(is_spow_enabled())
433 ... set_spow_enable(False)
434 ... print(is_spow_enabled())
435 True
436 False
437 """
439 global _SPOW_ENABLED # noqa: PLW0603
441 _SPOW_ENABLED = enable
444class spow_enable:
445 """
446 Context manager and decorator for temporarily setting the state of *Colour*
447 safe/symmetrical power function.
449 This utility provides both context manager and decorator functionality to
450 temporarily enable or disable the safe/symmetrical power function used
451 throughout the *Colour* library. When enabled, power operations use a
452 symmetrical implementation that handles negative values appropriately for
453 colour science computations.
455 Parameters
456 ----------
457 enable
458 Whether to enable or disable the *Colour* safe/symmetrical power
459 function for the duration of the context or decorated function.
460 """
462 def __init__(self, enable: bool) -> None:
463 self._enable = enable
464 self._previous_state = is_spow_enabled()
466 def __enter__(self) -> Self:
467 """
468 Set the *Colour* safe / symmetrical power function enabled state
469 upon entering the context manager.
470 """
472 set_spow_enable(self._enable)
474 return self
476 def __exit__(self, *args: Any) -> None:
477 """
478 Set the *Colour* safe / symmetrical power function enabled state
479 upon exiting the context manager.
480 """
482 set_spow_enable(self._previous_state)
484 def __call__(self, function: Callable) -> Callable:
485 """Call the wrapped definition."""
487 @functools.wraps(function)
488 def wrapper(*args: Any, **kwargs: Any) -> Any:
489 with self:
490 return function(*args, **kwargs)
492 return wrapper
495@typing.overload
496def spow(a: float | DTypeFloat, p: float | DTypeFloat) -> DTypeFloat: ...
497@typing.overload
498def spow(a: NDArray, p: ArrayLike) -> NDArrayFloat: ...
499@typing.overload
500def spow(a: ArrayLike, p: NDArray) -> NDArrayFloat: ...
501@typing.overload
502def spow(a: ArrayLike, p: ArrayLike) -> DTypeFloat | NDArrayFloat: ...
503def spow(a: ArrayLike, p: ArrayLike) -> DTypeFloat | NDArrayFloat:
504 """
505 Raise specified array :math:`a` to the power :math:`p` as follows:
506 :math:`\\text{sign}(a) \\cdot |a|^p`.
508 This definition avoids NaN generation when array :math:`a` is negative
509 and power :math:`p` is fractional. This behaviour can be enabled or
510 disabled with the :func:`colour.algebra.set_spow_enable` definition or
511 with the :func:`spow_enable` context manager.
513 Parameters
514 ----------
515 a
516 Array :math:`a`.
517 p
518 Power :math:`p`.
520 Returns
521 -------
522 :class:`np.float` or :class:`numpy.ndarray`
523 Array :math:`a` safely raised to the power :math:`p`.
525 Examples
526 --------
527 >>> np.power(-2, 0.15)
528 nan
529 >>> spow(-2, 0.15) # doctest: +ELLIPSIS
530 -1.1095694...
531 >>> spow(0, 0)
532 0.0
533 """
535 if not _SPOW_ENABLED:
536 return np.power(a, p)
538 a = as_float_array(a)
539 p = as_float_array(p)
541 a_p = np.sign(a) * np.abs(a) ** p
543 return as_float(0 if a_p.ndim == 0 and np.isnan(a_p) else a_p)
546def normalise_vector(a: ArrayLike) -> NDArrayFloat:
547 """
548 Normalise the specified vector :math:`a`.
550 The normalisation process scales the vector to have unit length, ensuring
551 that the magnitude of the resulting vector equals 1.
553 Parameters
554 ----------
555 a
556 Vector :math:`a` to normalise.
558 Returns
559 -------
560 :class:`numpy.ndarray`
561 Normalised vector :math:`a` with unit length.
563 Examples
564 --------
565 >>> a = np.array([0.20654008, 0.12197225, 0.05136952])
566 >>> normalise_vector(a) # doctest: +ELLIPSIS
567 array([ 0.8419703..., 0.4972256..., 0.2094102...])
568 """
570 a = as_float_array(a)
572 with sdiv_mode():
573 return sdiv(a, np.linalg.norm(a))
576def normalise_maximum(
577 a: ArrayLike,
578 axis: int | None = None,
579 factor: float = 1,
580 clip: bool = True,
581) -> NDArrayFloat:
582 """
583 Normalise specified array :math:`a` values by :math:`a` maximum value
584 and optionally clip them between [0, factor].
586 Parameters
587 ----------
588 a
589 Array :math:`a` to normalise.
590 axis
591 Normalization axis.
592 factor
593 Normalization factor.
594 clip
595 Clip values to domain [0, 'factor'].
597 Returns
598 -------
599 :class:`numpy.ndarray`
600 Maximum normalised array :math:`a`.
602 Examples
603 --------
604 >>> a = np.array([0.48222001, 0.31654775, 0.22070353])
605 >>> normalise_maximum(a) # doctest: +ELLIPSIS
606 array([ 1. , 0.6564384..., 0.4576822...])
607 """
609 a = as_float_array(a)
611 maximum = np.max(a, axis=axis)
613 with sdiv_mode():
614 a = a * sdiv(1, maximum[..., None]) * factor
616 return np.clip(a, 0, factor) if clip else a
619def vecmul(m: ArrayLike, v: ArrayLike) -> NDArrayFloat:
620 """
621 Perform batched multiplication between the matrix array :math:`m` and
622 vector array :math:`v`.
624 This function is equivalent to :func:`numpy.matmul` but specifically
625 designed for vector multiplication by a matrix. Vector dimensionality is
626 automatically increased to enable broadcasting. The operation can be
627 expressed using :func:`numpy.einsum` with subscripts
628 *'...ij,...j->...i'*.
630 Parameters
631 ----------
632 m
633 Matrix array :math:`m`.
634 v
635 Vector array :math:`v`.
637 Returns
638 -------
639 :class:`numpy.ndarray`
640 Multiplied vector array :math:`v`.
642 Examples
643 --------
644 >>> m = np.array(
645 ... [
646 ... [0.7328, 0.4296, -0.1624],
647 ... [-0.7036, 1.6975, 0.0061],
648 ... [0.0030, 0.0136, 0.9834],
649 ... ]
650 ... )
651 >>> m = np.reshape(np.tile(m, (6, 1)), (6, 3, 3))
652 >>> v = np.array([0.20654008, 0.12197225, 0.05136952])
653 >>> v = np.tile(v, (6, 1))
654 >>> vecmul(m, v) # doctest: +ELLIPSIS
655 array([[ 0.1954094..., 0.0620396..., 0.0527952...],
656 [ 0.1954094..., 0.0620396..., 0.0527952...],
657 [ 0.1954094..., 0.0620396..., 0.0527952...],
658 [ 0.1954094..., 0.0620396..., 0.0527952...],
659 [ 0.1954094..., 0.0620396..., 0.0527952...],
660 [ 0.1954094..., 0.0620396..., 0.0527952...]])
661 """
663 return np.matmul(as_float_array(m), as_float_array(v)[..., None]).squeeze(-1)
666def euclidean_distance(a: ArrayLike, b: ArrayLike) -> NDArrayFloat:
667 """
668 Calculate the *Euclidean* distance between the specified point arrays
669 :math:`a` and :math:`b`.
671 For a two-dimensional space, the metric is as follows:
673 :math:`E_D = [(x_a - x_b)^2 + (y_a - y_b)^2]^{1/2}`
675 Parameters
676 ----------
677 a
678 Point array :math:`a`.
679 b
680 Point array :math:`b`.
682 Returns
683 -------
684 :class:`numpy.float64` or :class:`numpy.ndarray`
685 *Euclidean* distance between the two point arrays.
687 Examples
688 --------
689 >>> a = np.array([100.00000000, 21.57210357, 272.22819350])
690 >>> b = np.array([100.00000000, 426.67945353, 72.39590835])
691 >>> euclidean_distance(a, b) # doctest: +ELLIPSIS
692 451.7133019...
693 """
695 return as_float(np.linalg.norm(as_float_array(a) - as_float_array(b), axis=-1))
698def manhattan_distance(a: ArrayLike, b: ArrayLike) -> NDArrayFloat:
699 """
700 Compute the *Manhattan* (or *City-Block*) distance between point array
701 :math:`a` and point array :math:`b`.
703 For a two-dimensional space, the metric is defined as:
705 :math:`M_D = |x_a - x_b| + |y_a - y_b|`
707 Parameters
708 ----------
709 a
710 Point array :math:`a`.
711 b
712 Point array :math:`b`.
714 Returns
715 -------
716 :class:`np.float` or :class:`numpy.ndarray`
717 *Manhattan* distance.
719 Examples
720 --------
721 >>> a = np.array([100.00000000, 21.57210357, 272.22819350])
722 >>> b = np.array([100.00000000, 426.67945353, 72.39590835])
723 >>> manhattan_distance(a, b) # doctest: +ELLIPSIS
724 604.9396351...
725 """
727 return as_float(np.sum(np.abs(as_float_array(a) - as_float_array(b)), axis=-1))
730def linear_conversion(
731 a: ArrayLike, old_range: ArrayLike, new_range: ArrayLike
732) -> NDArrayFloat:
733 """
734 Perform simple linear conversion of the specified array :math:`a` between the
735 old and new ranges.
737 Parameters
738 ----------
739 a
740 Array :math:`a` to perform the linear conversion onto.
741 old_range
742 Old range.
743 new_range
744 New range.
746 Returns
747 -------
748 :class:`numpy.ndarray`
749 Linear conversion result.
751 Examples
752 --------
753 >>> a = np.linspace(0, 1, 10)
754 >>> linear_conversion(a, np.array([0, 1]), np.array([1, 10]))
755 array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.])
756 """
758 a = as_float_array(a)
760 in_min, in_max = tsplit(old_range)
761 out_min, out_max = tsplit(new_range)
763 return ((a - in_min) / (in_max - in_min)) * (out_max - out_min) + out_min
766def linstep_function(
767 x: ArrayLike,
768 a: ArrayLike = 0,
769 b: ArrayLike = 1,
770 clip: bool = False,
771) -> NDArrayFloat:
772 """
773 Perform linear interpolation between specified arrays :math:`a` and
774 :math:`b` using array :math:`x`.
776 Parameters
777 ----------
778 x
779 Array :math:`x` containing values to use for interpolation between
780 array :math:`a` and array :math:`b`.
781 a
782 Array :math:`a`, the start of the interpolation range.
783 b
784 Array :math:`b`, the end of the interpolation range.
785 clip
786 Whether to clip the output values to range [:math:`a`, :math:`b`].
788 Returns
789 -------
790 :class:`numpy.ndarray`
791 Linear interpolation result.
793 Examples
794 --------
795 >>> a = 0
796 >>> b = 2
797 >>> linstep_function(0.5, a, b)
798 1.0
799 """
801 x = as_float_array(x)
802 a = as_float_array(a)
803 b = as_float_array(b)
805 y = (1.0 - x) * a + x * b
807 return np.clip(y, a, b) if clip else y
810lerp = linstep_function
813def smoothstep_function(
814 x: ArrayLike,
815 a: ArrayLike = 0,
816 b: ArrayLike = 1,
817 clip: bool = False,
818) -> NDArrayFloat:
819 """
820 Apply the *smoothstep* cubic Hermite interpolation function to
821 array :math:`x`.
823 The *smoothstep* function creates a smooth S-shaped curve between
824 specified edge values, commonly used for smooth transitions in
825 colour interpolation and rendering operations.
827 Parameters
828 ----------
829 x
830 Input array :math:`x` containing values to be transformed.
831 a
832 Lower edge value for the interpolation domain.
833 b
834 Upper edge value for the interpolation domain.
835 clip
836 Whether to normalize and constrain input values to the domain
837 [:math:`a`, :math:`b`] before applying the *smoothstep* function.
839 Returns
840 -------
841 :class:`numpy.ndarray`
842 Transformed array with values smoothly interpolated using the
843 cubic Hermite polynomial :math:`3x^2 - 2x^3`.
845 Examples
846 --------
847 >>> x = np.linspace(-2, 2, 5)
848 >>> smoothstep_function(x, -2, 2, clip=True)
849 array([ 0. , 0.15625, 0.5 , 0.84375, 1. ])
850 """
852 x = as_float_array(x)
853 a = as_float_array(a)
854 b = as_float_array(b)
856 i = np.clip((x - a) / (b - a), 0, 1) if clip else x
858 return (i**2) * (3.0 - 2.0 * i)
861smooth = smoothstep_function
864def is_identity(a: ArrayLike) -> bool:
865 """
866 Determine whether the specified array :math:`a` is an identity matrix.
868 An identity matrix is a square matrix with ones on the main diagonal
869 and zeros elsewhere, satisfying :math:`I \\cdot A = A \\cdot I = A`
870 for any compatible matrix :math:`A`.
872 Parameters
873 ----------
874 a
875 Array :math:`a` to test for identity matrix properties.
877 Returns
878 -------
879 :class:`bool`
880 Whether the specified array :math:`a` is an identity matrix.
882 Examples
883 --------
884 >>> is_identity(np.reshape(np.array([1, 0, 0, 0, 1, 0, 0, 0, 1]), (3, 3)))
885 True
886 >>> is_identity(np.reshape(np.array([1, 2, 0, 0, 1, 0, 0, 0, 1]), (3, 3)))
887 False
888 """
890 return np.array_equal(np.identity(len(np.diag(a))), a)
893def eigen_decomposition(
894 a: ArrayLike,
895 eigen_w_v_count: int | None = None,
896 descending_order: bool = True,
897 covariance_matrix: bool = False,
898) -> Tuple[NDArrayFloat, NDArrayFloat]:
899 """
900 Compute the eigenvalues :math:`w` and eigenvectors :math:`v` of the
901 specified array :math:`a` in the specified order.
903 Parameters
904 ----------
905 a
906 Array to compute the eigenvalues :math:`w` and eigenvectors :math:`v`
907 for.
908 eigen_w_v_count
909 Number of eigenvalues :math:`w` and eigenvectors :math:`v` to return.
910 descending_order
911 Whether to return the eigenvalues :math:`w` and eigenvectors :math:`v`
912 in descending order.
913 covariance_matrix
914 Whether to compute the eigenvalues :math:`w` and eigenvectors
915 :math:`v` of the array :math:`a` covariance matrix
916 :math:`A = a^T \\cdot a`.
918 Returns
919 -------
920 :class:`tuple`
921 Tuple of eigenvalues :math:`w` and eigenvectors :math:`v`. The
922 eigenvalues are in the specified order, each repeated according to
923 its multiplicity. The column ``v[:, i]`` is the normalized eigenvector
924 corresponding to the eigenvalue ``w[i]``.
926 Examples
927 --------
928 >>> a = np.diag([1, 2, 3])
929 >>> w, v = eigen_decomposition(a)
930 >>> w
931 array([ 3., 2., 1.])
932 >>> v
933 array([[ 0., 0., 1.],
934 [ 0., 1., 0.],
935 [ 1., 0., 0.]])
936 >>> w, v = eigen_decomposition(a, 1)
937 >>> w
938 array([ 3.])
939 >>> v
940 array([[ 0.],
941 [ 0.],
942 [ 1.]])
943 >>> w, v = eigen_decomposition(a, descending_order=False)
944 >>> w
945 array([ 1., 2., 3.])
946 >>> v
947 array([[ 1., 0., 0.],
948 [ 0., 1., 0.],
949 [ 0., 0., 1.]])
950 >>> w, v = eigen_decomposition(a, covariance_matrix=True)
951 >>> w
952 array([ 9., 4., 1.])
953 >>> v
954 array([[ 0., 0., 1.],
955 [ 0., 1., 0.],
956 [ 1., 0., 0.]])
957 """
959 A = as_float_array(a)
961 if covariance_matrix:
962 A = np.dot(np.transpose(A), A)
964 w, v = np.linalg.eigh(A)
966 if eigen_w_v_count is not None:
967 w = w[-eigen_w_v_count:]
968 v = v[..., -eigen_w_v_count:]
970 if descending_order:
971 w = np.flipud(w)
972 v = np.fliplr(v)
974 return w, v