Source code for arlunio.math

from __future__ import annotations

from typing import Callable

import numpy as np

import arlunio as ar


[docs]def clamp(vs, min_=0, max_=1): """Force an array of values to stay within a range of values. Parameters ---------- vs: The array of values to clamp min_: The minimum value the result should contain max_: The maximum value the result should contain Examples -------- By default values will be limited to between :code:`0` and :code:`1` >>> from arlunio.math import clamp >>> import numpy as np >>> vs = np.linspace(-1, 2, 6) >>> clamp(vs) array([0. , 0. , 0.2, 0.8, 1. , 1. ]) But this can be changed with extra arguments to the :code:`clamp` function >>> clamp(vs, min_=-1, max_=0.5) array([-1. , -0.4, 0.2, 0.5, 0.5, 0.5]) """ vs = np.array(vs) vs[vs > max_] = max_ vs[vs < min_] = min_ return vs
[docs]def dot(us, vs): """Return the dot product of two arrays of vectors.""" return np.sum(us * vs, axis=-1)
def length(vs): return np.sqrt(np.sum(vs * vs, axis=-1))
[docs]def lerp(start: float = 0, stop: float = 1) -> Callable[[float], float]: """Return a function that will linerarly interpolate between a and b. Parameters ---------- start: The value the interpolation should start from. stop: The value the interpolation should stop at. Examples -------- By default this function will interpolate between :code:`0` and :code:`1` >>> from arlunio.math import lerp >>> f = lerp() >>> f(0) 0 >>> f(1) 1 However by passing arguments to the :code:`lerp` function we can change the bounds of the interpolation. >>> import numpy as np >>> ts = np.linspace(0, 1, 4) >>> f = lerp(start=3, stop=-1) >>> f(ts) array([ 3. , 1.66666667, 0.33333333, -1. ]) """ def f(t: float) -> float: return (1 - t) * start + t * stop return f
[docs]def normalise(vs): """Normalise an array into the range :math:`[0, 1]` Parameters ---------- vs: The array to normalise. """ if len(vs.shape) == 1: return vs / length(vs) if len(vs.shape) == 2: return vs / length(vs)[:, np.newaxis]
[docs]@ar.definition def X(width: int, height: int, *, x0=0, scale=1, stretch=False): """Cartesian :math:`x` coordinates. .. arlunio-image:: X Coordinates :align: right :: from arlunio.math import X from arlunio.image import colorramp x = X() image = colorramp(x(width=256, height=256)) Attributes ---------- x0: Shift all the coordinate values by :code:`x0` scale: Controls the magnitude of the extreme values. stretch: If :code:`True` and the image is wider than it is tall then the grid will be stretched so that :code:`x = scale` falls on the image border. Otherwise the image's aspect ratio will be taken into account and :code:`x = scale` will fall somewhere within the boundaries of the image. Examples -------- By default values will be generated between :math:`\\pm 1`:: >>> from arlunio.math import X >>> x = X() >>> x(width=4, height=4) array([[-1. , -0.33333333, 0.33333333, 1. ], [-1. , -0.33333333, 0.33333333, 1. ], [-1. , -0.33333333, 0.33333333, 1. ], [-1. , -0.33333333, 0.33333333, 1. ]]) If however the image is wider than it is tall this range will be extended so that the resulting image is not stretched:: >>> x(width=4, height=2) array([[-2. , -0.66666667, 0.66666667, 2. ], [-2. , -0.66666667, 0.66666667, 2. ]]) This behaviour can be disabled with the :code:`stretch` attribute:: >>> x.stretch = True >>> x(width=4, height=2) array([[-1. , -0.33333333, 0.33333333, 1. ], [-1. , -0.33333333, 0.33333333, 1. ]]) Additionally the :code:`scale` attribute can be used to adjust the magnitude of the extreme values generated, while the :code:`x0` attribute can be used to shift all the values by a given amount:: >>> x = X(x0=-2, scale=2) >>> x(width=4, height=4) array([[0. , 1.33333333, 2.66666667, 4. ], [0. , 1.33333333, 2.66666667, 4. ], [0. , 1.33333333, 2.66666667, 4. ], [0. , 1.33333333, 2.66666667, 4. ]]) """ ratio = width / height if not stretch and ratio > 1: scale = scale * ratio x = np.linspace(-scale, scale, width) x = np.array([x for _ in range(height)]) return x - x0
[docs]@ar.definition def Y(width: int, height: int, *, y0=0, scale=1, stretch=False): """Cartesian :math:`y` coordinates .. arlunio-image:: Y Coordinates :align: right :: from arlunio.math import Y from arlunio.image import colorramp y = Y() image = colorramp(y(width=256, height=256)) Attributes ---------- y0: Shift all the coordinate values by :code:`y0` scale: Controls the size of the extreme values stretch: If :code:`True` and the image is taller than it is wide then the grid will be stretched so that :code:`y = scale` falls on the border. Otherwise the image's aspect ratio will be taken into account and :code:`y = scale` will fall somewhere within the boundaries of the image. Examples -------- By default values will be generated between :math:`\\pm 1`:: >>> from arlunio.math import Y >>> y = Y() >>> y(width=4, height=4) array([[ 1. , 1. , 1. , 1. ], [ 0.33333333, 0.33333333, 0.33333333, 0.33333333], [-0.33333333, -0.33333333, -0.33333333, -0.33333333], [-1. , -1. , -1. , -1. ]]) If however the image is taller than it is wide this range will be extended so that the resulting image is not stretched:: >>> y(width=2, height=4) array([[ 2. , 2. ], [ 0.66666667, 0.66666667], [-0.66666667, -0.66666667], [-2. , -2. ]]) This behaviour can be disabled with the :code:`stretch` attribute:: >>> y.stretch = True >>> y(width=2, height=4) array([[ 1. , 1. ], [ 0.33333333, 0.33333333], [-0.33333333, -0.33333333], [-1. , -1. ]]) Additionally the :code:`scale` attribute can be used to adjust the magnitude of the extreme values generated, while the :code:`y0` attribute can be used to shift all the values by a given amount:: >>> y = Y(y0=-2, scale=2) >>> y(width=4, height=4) array([[4. , 4. , 4. , 4. ], [2.66666667, 2.66666667, 2.66666667, 2.66666667], [1.33333333, 1.33333333, 1.33333333, 1.33333333], [0. , 0. , 0. , 0. ]]) """ ratio = height / width if not stretch and ratio > 1: scale = scale * ratio y = np.linspace(scale, -scale, height) y = np.array([y for _ in range(width)]).transpose() return y - y0
[docs]@ar.definition def R(x: X, y: Y): """Polar :math:`r` coordinates. .. arlunio-image:: R Coordinates :align: right :: from arlunio.math import R from arlunio.image import colorramp r = R() image = colorramp(r(width=256, height=256)) This definition corresponds with the distance a given point is from the origin and can be calculated from the point's equivalent Cartesian coordinate representation .. math:: r = \\sqrt{x^2 + y^2} Examples -------- :: >>> from arlunio.math import R >>> r = R() >>> r(width=5, height=5) array([[1.41421356, 1.11803399, 1. , 1.11803399, 1.41421356], [1.11803399, 0.70710678, 0.5 , 0.70710678, 1.11803399], [1. , 0.5 , 0. , 0.5 , 1. ], [1.11803399, 0.70710678, 0.5 , 0.70710678, 1.11803399], [1.41421356, 1.11803399, 1. , 1.11803399, 1.41421356]]) While this definition does not currently have any attributes of its own, since it's derived from the :class:`arlunio.math.X` and :class:`arlunio.math.Y` definitions it automatically inherits the attributes from these base definitions:: >>> r = R(x0=-2, y0=-2, scale=2) >>> r(width=5, height=5) array([[4. , 4.12310563, 4.47213595, 5. , 5.65685425], [3. , 3.16227766, 3.60555128, 4.24264069, 5. ], [2. , 2.23606798, 2.82842712, 3.60555128, 4.47213595], [1. , 1.41421356, 2.23606798, 3.16227766, 4.12310563], [0. , 1. , 2. , 3. , 4. ]]) Notice how in the case where the base definitions share a attribute (:code:`scale` in this case) they both share the value that is set when creating an instance of the :code:`R` definition. """ return np.sqrt(x * x + y * y)
[docs]@ar.definition def T(x: X, y: Y, *, t0=0): """Polar :math:`\\theta` coordinates. .. arlunio-image:: T Coordinates :align: right :: from arlunio.math import T from arlunio.image import colorramp t = T() image = colorramp(t(width=256, height=256)) This definition corresponds with the angle a given point is around from the positive :math:`x`-axis. This can be calculated from the point's equivalent Cartesian coordinate representation. All angles are given in :term:`radians` .. math:: t = atan2\\left(\\frac{y}{x}\\right) Attributes ---------- t0: Shift all the coordinate values by :code:`t0` Examples -------- By default all point on the :math:`x`-axis will have a value of :code:`t = 0`:: >>> from arlunio.math import T >>> t = T() >>> t(width=5, height=5) array([[ 2.35619449, 2.03444394, 1.57079633, 1.10714872, 0.78539816], [ 2.67794504, 2.35619449, 1.57079633, 0.78539816, 0.46364761], [ 3.14159265, 3.14159265, 0. , 0. , 0. ], [-2.67794504, -2.35619449, -1.57079633, -0.78539816, -0.46364761], [-2.35619449, -2.03444394, -1.57079633, -1.10714872, -0.78539816]]) This can be changed however with the :code:`t0` attribute:: >>> from math import pi >>> t.t0 = pi >>> t(width=5, height=5) array([[-0.78539816, -1.10714872, -1.57079633, -2.03444394, -2.35619449], [-0.46364761, -0.78539816, -1.57079633, -2.35619449, -2.67794504], [ 0. , 0. , -3.14159265, -3.14159265, -3.14159265], [-5.8195377 , -5.49778714, -4.71238898, -3.92699082, -3.60524026], [-5.49778714, -5.17603659, -4.71238898, -4.24874137, -3.92699082]]) Also, being a definition derived from :class:`arlunio.math.X` and :class:`arlunio.math.Y` the attributes for these definitions are also available to control the output:: >>> t = T(x0=-2, y0=-2, scale=2) >>> t(width=5, height=5) array([[1.57079633, 1.32581766, 1.10714872, 0.92729522, 0.78539816], [1.57079633, 1.24904577, 0.98279372, 0.78539816, 0.64350111], [1.57079633, 1.10714872, 0.78539816, 0.5880026 , 0.46364761], [1.57079633, 0.78539816, 0.46364761, 0.32175055, 0.24497866], [0. , 0. , 0. , 0. , 0. ]]) """ t = np.arctan2(y, x) return t - t0
[docs]@ar.definition def Cartesian(x: X, y: Y): """Cartesian coordinates.""" return np.dstack([x, y])
[docs]@ar.definition def Polar(r: R, t: T): """Polar coordinates.""" return np.dstack([r, t])
[docs]@ar.definition def Barycentric(x: X, y: Y, *, a=(0.5, -0.5), b=(0, 0.5), c=(-0.5, -0.5)): """Barycentric coordinates. .. arlunio-image:: Barycentric Demo :align: right :: import arlunio.math as math import arlunio.image as img import numpy as np coords = math.Barycentric()(width=256, height=256) l1 = np.abs(coords[:, :, 0]) l2 = np.abs(coords[:, :, 1]) l3 = np.abs(coords[:, :, 2]) r = img.colorramp(l1, start='red', stop='black') g = img.colorramp(l2, start='green', stop='black') b = img.colorramp(l3, start='blue', stop='black') rs = np.asarray(r) gs = np.asarray(g) bs = np.asarray(b) image = img.fromarray(rs + gs + bs) Returns the Barycentric coordinate grid as defined by the given triangle. Attributes ---------- a: The cartesian :math:`(x, y)` coordinates of the point :math:`a` b: The cartesian :math:`(x, y)` coordinates of the point :math:`b` c: The cartesian :math:`(x, y)` coordinates of the point :math:`c` """ h, w = x.shape # Construct the transform matrix T t1 = a[0] - c[0] t2 = b[0] - c[0] t3 = a[1] - c[1] t4 = b[1] - c[1] # Compute the inverse determinant d = 1 / ((t1 * t4) - (t2 * t3)) p = np.dstack([x, y]).reshape(w * h, 2) - c p1 = p[:, 0] p2 = p[:, 1] # Compute the coordinates l1 = d * ((p1 * t4) - (p2 * t2)) l2 = d * ((p2 * t1) - (p1 * t3)) l3 = 1 - l1 - l2 return np.dstack([l1, l2, l3])[0].reshape(h, w, 3)