Source code for nekmeshpy.core.affine

"""Affine maps behind the rung-preserving ``translate`` / ``rotate`` / ``scale``."""

from __future__ import annotations

from collections.abc import Sequence
from typing import Optional

import numpy as np

from .._typing import FloatArray, Point, PointArray, Vec3

#: The world origin -- default centre for :func:`rotation` / :func:`scaling`.
ORIGIN: Point = np.array([0.0, 0.0, 0.0])
#: The ``+z`` axis -- default rotation axis, matching the sweep factories' ``axis=``.
Z_AXIS: Vec3 = np.array([0.0, 0.0, 1.0])

# An affine map: ``(matrix, offset)``, with ``matrix=None`` meaning pure translation.
# This is a *runtime* assignment, not an annotation, so ``from __future__ import
# annotations`` does not defer it: spelling the union ``FloatArray | None`` here would
# evaluate PEP 604 eagerly and raise ``TypeError`` on the Python 3.9 CI leg.
Affine = tuple[Optional[FloatArray], Vec3]


[docs] def apply(P: PointArray, matrix: FloatArray | None, offset: Vec3) -> PointArray: """Map every coordinate of ``P`` (any leading shape, trailing axis 3) through the affine pair, returning a new array of the same shape. With ``matrix=None`` this is the exact translation ``P + offset``.""" A: PointArray = np.asarray(P, dtype=float) if matrix is None: out: PointArray = A + offset return out out = A @ np.asarray(matrix, dtype=float).T + offset return out
[docs] def translation(vector: Vec3 | Sequence[float]) -> Affine: """The affine map that shifts by ``vector`` ``(3,)``.""" v: Vec3 = np.asarray(vector, dtype=float).reshape(-1) if v.shape != (3,): raise ValueError("translate: vector must be a (3,) displacement, got %s" % (v.shape,)) return None, v
[docs] def scaling(factor: float | Vec3 | Sequence[float], center: Point | Sequence[float] = ORIGIN) -> Affine: """The affine map that scales about ``center`` by ``factor`` -- a scalar (uniform) or a ``(3,)`` per-axis vector. A zero or negative component is rejected: it would collapse or invert every element.""" f: FloatArray = np.asarray(factor, dtype=float).reshape(-1) if f.shape == (1,): f = np.repeat(f, 3) if f.shape != (3,): raise ValueError( "scale: factor must be a scalar or a (3,) per-axis vector, got %s" % (np.shape(factor),)) if not np.all(f > 0.0): raise ValueError("scale: every factor must be positive (got %s); a zero or " "negative factor collapses or inverts every element" % (f,)) return np.diag(f), _fixed_point_offset(np.diag(f), center)
[docs] def rotation(angle: float, axis: Vec3 | Sequence[float] = Z_AXIS, center: Point | Sequence[float] = ORIGIN) -> Affine: """The affine map that rotates by ``angle`` **radians** about the line through ``center`` with direction ``axis`` (right-handed; ``axis`` need not be normalized). """ k: Vec3 = np.asarray(axis, dtype=float).reshape(-1) if k.shape != (3,): raise ValueError("rotate: axis must be a (3,) direction, got %s" % (k.shape,)) n = float(np.linalg.norm(k)) if n == 0.0: raise ValueError("rotate: axis must be non-zero") k = k / n c, s = np.cos(float(angle)), np.sin(float(angle)) K: FloatArray = np.array([[0.0, -k[2], k[1]], [k[2], 0.0, -k[0]], [-k[1], k[0], 0.0]]) R: FloatArray = c * np.eye(3) + s * K + (1.0 - c) * np.outer(k, k) return R, _fixed_point_offset(R, center)
[docs] def reflection(normal: Vec3 | Sequence[float], point: Point | Sequence[float] = ORIGIN) -> Affine: """The affine map that mirrors through the plane with unit ``normal`` (need not be normalized) passing through ``point`` -- the Householder matrix ``I - 2 n n^T``. Its determinant is ``-1``, so it turns every element inside out: this is the map ``mirror`` applies to the coordinates, *paired* with a re-winding of the connectivity. Handing it to ``transform`` alone leaves an inverted mesh.""" n: Vec3 = np.asarray(normal, dtype=float).reshape(-1) if n.shape != (3,): raise ValueError("mirror: normal must be a (3,) direction, got %s" % (n.shape,)) norm = float(np.linalg.norm(n)) if norm == 0.0: raise ValueError("mirror: normal must be non-zero -- it names a plane") n = n / norm M: FloatArray = np.eye(3) - 2.0 * np.outer(n, n) return M, _fixed_point_offset(M, point)
def _fixed_point_offset(matrix: FloatArray, center: Point | Sequence[float]) -> Vec3: """The offset that keeps ``center`` fixed under ``matrix``: ``c - matrix @ c``.""" c: Point = np.asarray(center, dtype=float).reshape(-1) if c.shape != (3,): raise ValueError("center must be a (3,) point, got %s" % (c.shape,)) off: Vec3 = c - matrix @ c return off