Source code for colour.algebra.common

"""
Common Utilities
================

Defines the common algebra utilities objects that don't fall in any specific
category.
"""

from __future__ import annotations

import functools
import numpy as np

from colour.hints import (
    Any,
    ArrayLike,
    Boolean,
    Callable,
    Floating,
    FloatingOrArrayLike,
    FloatingOrNDArray,
    Integer,
    NDArray,
    Optional,
)
from colour.utilities import as_float_array, as_float, tsplit

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "New BSD License - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "is_spow_enabled",
    "set_spow_enable",
    "spow_enable",
    "spow",
    "normalise_maximum",
    "vector_dot",
    "matrix_dot",
    "linear_conversion",
    "linstep_function",
    "lerp",
    "smoothstep_function",
    "smooth",
    "is_identity",
]

# TODO: Annotate with "bool" when Python 3.7 is dropped.
_SPOW_ENABLED = True
"""
Global variable storing the current *Colour* safe / symmetrical power function
enabled state.
"""


[docs]def is_spow_enabled() -> bool: """ Return whether *Colour* safe / symmetrical power function is enabled. Returns ------- :class:`bool` Whether *Colour* safe / symmetrical power function is enabled. Examples -------- >>> with spow_enable(False): ... is_spow_enabled() False >>> with spow_enable(True): ... is_spow_enabled() True """ return _SPOW_ENABLED
[docs]def set_spow_enable(enable: bool): """ Set *Colour* safe / symmetrical power function enabled state. Parameters ---------- enable Whether to enable *Colour* safe / symmetrical power function. Examples -------- >>> with spow_enable(is_spow_enabled()): ... print(is_spow_enabled()) ... set_spow_enable(False) ... print(is_spow_enabled()) True False """ global _SPOW_ENABLED _SPOW_ENABLED = enable
[docs]class spow_enable: """ Define a context manager and decorator temporarily setting *Colour* safe / symmetrical power function enabled state. Parameters ---------- enable Whether to enable or disable *Colour* safe / symmetrical power function. """
[docs] def __init__(self, enable: bool): self._enable = enable self._previous_state = is_spow_enabled()
def __enter__(self) -> spow_enable: """ Set the *Colour* safe / symmetrical power function enabled state upon entering the context manager. """ set_spow_enable(self._enable) return self def __exit__(self, *args: Any): """ Set the *Colour* safe / symmetrical power function enabled state upon exiting the context manager. """ set_spow_enable(self._previous_state) def __call__(self, function: Callable) -> Callable: """Call the wrapped definition.""" @functools.wraps(function) def wrapper(*args: Any, **kwargs: Any) -> Any: with self: return function(*args, **kwargs) return wrapper
[docs]def spow(a: FloatingOrArrayLike, p: FloatingOrArrayLike) -> FloatingOrNDArray: """ Raise given array :math:`a` to the power :math:`p` as follows: :math:`sign(a) * |a|^p`. This definition avoids NaNs generation when array :math:`a` is negative and the power :math:`p` is fractional. This behaviour can be enabled or disabled with the :func:`colour.algebra.set_spow_enable` definition or with the :func:`spow_enable` context manager. Parameters ---------- a Array :math:`a`. p Power :math:`p`. Returns ------- :class:`np.floating` or :class:`numpy.ndarray` Array :math:`a` safely raised to the power :math:`p`. Examples -------- >>> np.power(-2, 0.15) nan >>> spow(-2, 0.15) # doctest: +ELLIPSIS -1.1095694... >>> spow(0, 0) 0.0 """ if not _SPOW_ENABLED: return np.power(a, p) a = np.atleast_1d(a) p = as_float_array(p) a_p = np.sign(a) * np.abs(a) ** p a_p[np.isnan(a_p)] = 0 return as_float(a_p)
[docs]def normalise_maximum( a: ArrayLike, axis: Optional[Integer] = None, factor: Floating = 1, clip: Boolean = True, ) -> NDArray: """ Normalise given array :math:`a` values by :math:`a` maximum value and optionally clip them between. Parameters ---------- a Array :math:`a` to normalise. axis Normalization axis. factor Normalization factor. clip Clip values to domain [0, 'factor']. Returns ------- :class:`numpy.ndarray` Maximum normalised array :math:`a`. Examples -------- >>> a = np.array([0.48222001, 0.31654775, 0.22070353]) >>> normalise_maximum(a) # doctest: +ELLIPSIS array([ 1. , 0.6564384..., 0.4576822...]) """ a = as_float_array(a) maximum = np.max(a, axis=axis) a = a * (1 / maximum[..., np.newaxis]) * factor return np.clip(a, 0, factor) if clip else a
[docs]def vector_dot(m: ArrayLike, v: ArrayLike) -> NDArray: """ Perform the dot product of the matrix array :math:`m` with the vector array :math:`v`. This definition is a convenient wrapper around :func:`np.einsum` with the following subscripts: *'...ij,...j->...i'*. Parameters ---------- m Matrix array :math:`m`. v Vector array :math:`v`. Returns ------- :class:`numpy.ndarray` Transformed vector array :math:`v`. Examples -------- >>> m = np.array( ... [[0.7328, 0.4296, -0.1624], ... [-0.7036, 1.6975, 0.0061], ... [0.0030, 0.0136, 0.9834]] ... ) >>> m = np.reshape(np.tile(m, (6, 1)), (6, 3, 3)) >>> v = np.array([0.20654008, 0.12197225, 0.05136952]) >>> v = np.tile(v, (6, 1)) >>> vector_dot(m, v) # doctest: +ELLIPSIS array([[ 0.1954094..., 0.0620396..., 0.0527952...], [ 0.1954094..., 0.0620396..., 0.0527952...], [ 0.1954094..., 0.0620396..., 0.0527952...], [ 0.1954094..., 0.0620396..., 0.0527952...], [ 0.1954094..., 0.0620396..., 0.0527952...], [ 0.1954094..., 0.0620396..., 0.0527952...]]) """ return np.einsum("...ij,...j->...i", as_float_array(m), as_float_array(v))
[docs]def matrix_dot(a: ArrayLike, b: ArrayLike) -> NDArray: """ Perform the dot product of the matrix array :math:`a` with the matrix array :math:`b`. This definition is a convenient wrapper around :func:`np.einsum` with the following subscripts: *'...ij,...jk->...ik'*. Parameters ---------- a Matrix array :math:`a`. b Matrix array :math:`b`. Returns ------- :class:`numpy.ndarray` Examples -------- >>> a = np.array( ... [[0.7328, 0.4296, -0.1624], ... [-0.7036, 1.6975, 0.0061], ... [0.0030, 0.0136, 0.9834]] ... ) >>> a = np.reshape(np.tile(a, (6, 1)), (6, 3, 3)) >>> b = a >>> matrix_dot(a, b) # doctest: +ELLIPSIS array([[[ 0.2342420..., 1.0418482..., -0.2760903...], [-1.7099407..., 2.5793226..., 0.1306181...], [-0.0044203..., 0.0377490..., 0.9666713...]], <BLANKLINE> [[ 0.2342420..., 1.0418482..., -0.2760903...], [-1.7099407..., 2.5793226..., 0.1306181...], [-0.0044203..., 0.0377490..., 0.9666713...]], <BLANKLINE> [[ 0.2342420..., 1.0418482..., -0.2760903...], [-1.7099407..., 2.5793226..., 0.1306181...], [-0.0044203..., 0.0377490..., 0.9666713...]], <BLANKLINE> [[ 0.2342420..., 1.0418482..., -0.2760903...], [-1.7099407..., 2.5793226..., 0.1306181...], [-0.0044203..., 0.0377490..., 0.9666713...]], <BLANKLINE> [[ 0.2342420..., 1.0418482..., -0.2760903...], [-1.7099407..., 2.5793226..., 0.1306181...], [-0.0044203..., 0.0377490..., 0.9666713...]], <BLANKLINE> [[ 0.2342420..., 1.0418482..., -0.2760903...], [-1.7099407..., 2.5793226..., 0.1306181...], [-0.0044203..., 0.0377490..., 0.9666713...]]]) """ return np.einsum( "...ij,...jk->...ik", as_float_array(a), as_float_array(b) )
[docs]def linear_conversion( a: ArrayLike, old_range: ArrayLike, new_range: ArrayLike ) -> NDArray: """ Perform a simple linear conversion of given array :math:`a` between the old and new ranges. Parameters ---------- a Array :math:`a` to perform the linear conversion onto. old_range Old range. new_range New range. Returns ------- :class:`numpy.ndarray` Linear conversion result. Examples -------- >>> a = np.linspace(0, 1, 10) >>> linear_conversion(a, np.array([0, 1]), np.array([1, 10])) array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.]) """ a = as_float_array(a) in_min, in_max = tsplit(old_range) out_min, out_max = tsplit(new_range) return ((a - in_min) / (in_max - in_min)) * (out_max - out_min) + out_min
[docs]def linstep_function( x: FloatingOrArrayLike, a: FloatingOrArrayLike = 0, b: FloatingOrArrayLike = 1, clip: Boolean = False, ) -> NDArray: """ Perform a simple linear interpolation between given array :math:`a` and array :math:`b` using :math:`x` array. Parameters ---------- x Array :math:`x` value to use to interpolate between array :math:`a` and array :math:`b`. a Array :math:`a`, the start of the range in which to interpolate. b Array :math:`b`, the end of the range in which to interpolate. clip Whether to clip the output values to range [``a``, ``b``]. Returns ------- :class:`numpy.ndarray` Linear interpolation result. Examples -------- >>> a = 0 >>> b = 2 >>> linstep_function(0.5, a, b) 1.0 """ x = as_float_array(x) a = as_float_array(a) b = as_float_array(b) y = (1 - x) * a + x * b return np.clip(y, a, b) if clip else y
lerp = linstep_function
[docs]def smoothstep_function( x: FloatingOrArrayLike, a: FloatingOrArrayLike = 0, b: FloatingOrArrayLike = 1, clip: Boolean = False, ) -> NDArray: """ Evaluate the *smoothstep* sigmoid-like function on array :math:`x`. Parameters ---------- x Array :math:`x`. a Low input domain limit, i.e. the left edge. b High input domain limit, i.e. the right edge. clip Whether to scale, bias and clip input values to domain [``a``, ``b``]. Returns ------- :class:`numpy.ndarray` Array :math:`x` after *smoothstep* sigmoid-like function evaluation. Examples -------- >>> x = np.linspace(-2, 2, 5) >>> smoothstep_function(x, -2, 2, clip=True) array([ 0. , 0.15625, 0.5 , 0.84375, 1. ]) """ x = as_float_array(x) a = as_float_array(a) b = as_float_array(b) i = np.clip((x - a) / (b - a), 0, 1) if clip else x return (i**2) * (3 - 2 * i)
smooth = smoothstep_function
[docs]def is_identity(a: ArrayLike) -> Boolean: """ Return whether :math:`a` array is an identity matrix. Parameters ---------- a Array :math:`a` to test. Returns ------- :class:`bool` Whether :math:`a` array is an identity matrix. Examples -------- >>> is_identity(np.array([1, 0, 0, 0, 1, 0, 0, 0, 1]).reshape(3, 3)) True >>> is_identity(np.array([1, 2, 0, 0, 1, 0, 0, 0, 1]).reshape(3, 3)) False """ return np.array_equal(np.identity(len(np.diag(a))), a)