Module transformnd.transforms

Implementations of some common transforms.

Expand source code
"""Implementations of some common transforms."""
from .affine import Affine
from .reflection import Reflect
from .simple import Identity, Scale, Translate

__all__ = ["Affine", "Identity", "Reflect", "Scale", "Translate"]

Sub-modules

transformnd.transforms.affine

Rigid transformations implemented as affine multiplications.

transformnd.transforms.moving_least_squares

Implementation of Moving Least Squares transformation …

transformnd.transforms.reflection
transformnd.transforms.simple

Simple transformations like rigid translation and scaling.

transformnd.transforms.thinplate

Thin plate splines transformations.

Classes

class Affine (matrix: ArrayLike, *, spaces: SpaceTuple = (None, None))

Helper class that provides a standard way to create an ABC using inheritance.

Affine transformation using an augmented matrix.

Matrix must have shape (D+1, D+1) or (D, D+1); the bottom row is assumed to be [0, 0, …, 0, 1].

Parameters

matrix : ArrayLike
Affine transformation matrix.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Raises

ValueError
Malformed matrix.
Expand source code
class Affine(Transform):
    def __init__(
        self,
        matrix: ArrayLike,
        *,
        spaces: SpaceTuple = (None, None),
    ):
        """Affine transformation using an augmented matrix.

        Matrix must have shape (D+1, D+1) or (D, D+1);
        the bottom row is assumed to be [0, 0, ..., 0, 1].

        Parameters
        ----------
        matrix : ArrayLike
            Affine transformation matrix.
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Raises
        ------
        ValueError
            Malformed matrix.
        """
        super().__init__(spaces=spaces)
        m = np.asarray(matrix)

        if m.ndim != 2:
            raise ValueError("Transformation matrix must be 2D")

        if m.shape[1] == m.shape[0] + 1:
            base = np.eye(m.shape[1])
            base[:-1, :] = m
            m = base
        elif not is_square(m):
            raise ValueError("Transformation matrix must be square")

        self.matrix = m
        self.ndim = {len(self.matrix) - 1}

    def apply(self, coords: np.ndarray) -> np.ndarray:
        coords = self._validate_coords(coords)
        # todo: replace with writing into full ones?
        coords = np.concatenate(
            [coords, np.ones((coords.shape[0], 1), dtype=coords.dtype)], axis=1
        )
        return (self.matrix @ coords.T).T[:, :-1]

    def __invert__(self) -> Transform:
        return type(self)(
            np.linalg.inv(self.matrix),
            spaces=self.spaces[::-1],
        )

    def __matmul__(self, rhs: Affine) -> Affine:
        """Compose two affine transforms by matrix multiplication.

        Parameters
        ----------
        rhs : AffineTransform

        Returns
        -------
        AffineTransform

        Raises
        ------
        ValueError
            Incompatible transforms.
        """
        if not isinstance(rhs, Affine):
            return NotImplemented
        if self.matrix.shape != rhs.matrix.shape:
            raise ValueError(
                "Cannot multiply affine matrices of different dimensionality"
            )
        if not none_eq(self.target_space, rhs.source_space):
            raise ValueError("Affine transforms do not share a space")
        return Affine(
            self.matrix @ rhs.matrix,
            spaces=(self.source_space, rhs.target_space),
        )

    @classmethod
    def from_linear_map(
        cls,
        linear_map: ArrayLike,
        translation=0,
        *,
        spaces: SpaceTuple = (None, None),
    ) -> Affine:
        """Create an augmented affine matrix from a linear map,
        with an optional translation.

        Parameters
        ----------
        linear_map : ArrayLike
            Shape (D, D)
        translation : ArrayLike, optional
            Translation to add to the matrix, by default None
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        AffineTransform
        """
        lin_map = np.asarray(linear_map)

        side = len(lin_map) + 1
        matrix = np.eye(side, dtype=lin_map.dtype)
        matrix[:-1, :] = lin_map
        matrix[:-1, -1] = translation

        return cls(matrix, spaces=spaces)

    @classmethod
    def identity(
        cls,
        ndim: int,
        *,
        spaces: SpaceTuple = (None, None),
    ) -> Affine:
        """Create an identity affine transformation.

        Parameters
        ----------
        ndim : int
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        AffineTransform
        """
        return cls(
            np.eye(ndim + 1),
            spaces=spaces,
        )

    @classmethod
    def translation(
        cls,
        translation: ArrayLike,
        ndim: Optional[int] = None,
        *,
        spaces: SpaceTuple = (None, None),
    ) -> Affine:
        """Create an affine translation.

        Parameters
        ----------
        translation : ArrayLike
            If scalar, broadcast to ndim.
        ndim : int, optional
            If translation is scalar, how many dims to use.
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        AffineTransform
        """
        t = arg_as_array(translation, ndim)
        m = np.eye(len(t) + 1, dtype=t.dtype)
        m[:-1, -1] = t

        return cls(m, spaces=spaces)

    @classmethod
    def scaling(
        cls,
        scale: ArrayLike,
        ndim: Optional[int] = None,
        *,
        spaces: SpaceTuple = (None, None),
    ) -> Affine:
        """Create an affine scaling.

        Parameters
        ----------
        scale : ArrayLike
            If scalar, broadcast to ndim.
        ndim : Optional[int], optional
            If scale is scalar, how many dimensions to use
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        AffineTransform
            [description]
        """
        s = arg_as_array(scale, ndim)
        m = np.eye(len(s) + 1, dtype=s.dtype)
        m[:-1, :-1] *= s

        return cls(m, spaces=spaces)

    @classmethod
    def reflection(
        cls,
        axis: Union[int, Container[int]],
        ndim: int,
        *,
        spaces: SpaceTuple = (None, None),
    ) -> Affine:
        """Create an affine reflection.

        Parameters
        ----------
        axis : Union[int, Container[int]]
            A single axis or multiple to reflect in.
        ndim : int
            How many dimensions to work in.
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        AffineTransform
        """
        if np.isscalar(axis):
            axis = [axis]  # type: ignore
        values = [-1 if idx in axis else 1 for idx in range(ndim)]  # type:ignore
        m = np.eye(ndim + 1)
        m[:-1, :-1] *= values

        return cls(m, spaces=spaces)

    @classmethod
    def rotation2(
        cls,
        rotation: float,
        degrees=True,
        clockwise=False,
        *,
        spaces: SpaceTuple = (None, None),
    ) -> Affine:
        """Create a 2D affine rotation.

        Parameters
        ----------
        rotation : float
            Angle to rotate.
        degrees : bool, optional
            Whether rotation is in degrees (rather than radians), by default True
        clockwise : bool, optional
            Whether rotation is clockwise, by default False
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        AffineTransform
        """
        if degrees:
            rotation = np.deg2rad(rotation)
        if clockwise:
            rotation *= -1

        vals = [
            [np.cos(rotation), -np.sin(rotation)],
            [np.sin(rotation), np.cos(rotation)],
        ]
        m = np.eye(3)
        m[:-1, :-1] = vals

        return cls(m, spaces=spaces)

    @classmethod
    def rotation3(
        cls,
        rotation: Union[float, Tuple[float, float, float]],
        degrees=True,
        clockwise=False,
        order=(0, 1, 2),
        *,
        spaces: SpaceTuple = (None, None),
    ) -> Affine:
        """Create a 3D affine rotation.

        Parameters
        ----------
        rotation : Union[float, Tuple[float, float, float]]
            Either a single rotation for all axes, or 1 for each.
        degrees : bool, optional
            Whether rotation is in degrees (rather than radians), by default True
        clockwise : bool, optional
            Whether rotation is clockwise, by default False
        order : tuple, optional
            What order to apply the rotations, by default (0, 1, 2)
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        AffineTransform

        Raises
        ------
        ValueError
            Incompatible order.
        """
        if np.isscalar(rotation):
            r = np.array([rotation] * 3)
        else:
            r = np.asarray(rotation)

        if degrees:
            r = np.deg2rad(r)
        if clockwise:
            r *= -1

        if len(order) != 3 or set(order) != {0, 1, 2}:
            raise ValueError("Order must contain only 0, 1, 2 in any order.")

        order = list(order)

        rots = [
            np.array(
                [
                    [1, 0, 0],
                    [0, np.cos(r[0]), -np.sin(r[0])],
                    [0, np.sin(r[0]), np.cos(r[0])],
                ]
            ),
            np.array(
                [
                    [np.cos(r[1]), 0, np.sin(r[1])],
                    [0, 1, 0],
                    [-np.sin(r[1]), 0, np.cos(r[1])],
                ]
            ),
            np.array(
                [
                    [np.cos(r[2]), -np.sin(r[2]), 0],
                    [np.sin(r[2]), np.cos(r[2]), 0],
                    [0, 0, 1],
                ]
            ),
        ]

        rot = rots[order.pop(0)]
        rot @= rots[order.pop(0)]
        rot @= rots[order.pop(0)]

        m = np.eye(4)
        m[:-1, :-1] = rot

        return cls(m, spaces=spaces)

    @classmethod
    def shearing(
        cls,
        factor: Union[float, np.ndarray],
        ndim: Optional[int] = None,
        *,
        spaces: SpaceTuple = (None, None),
    ) -> Affine:
        """Create an affine shear.

        `factor` can be a scalar to broadcast to all dimensions,
        or a D-length list of D-1 lists.
        The first inner list contains the shear factors in the first dimension
        for all *but* the first dimension.
        The second inner list contains the shear factors in the second dimension
        for all the *but* the second dimension, etc.

        Parameters
        ----------
        factor : Union[float, np.ndarray]
            Shear scale factors; see above for more details.
        ndim : Optional[int], optional
            If factor is scalar, broadcast to this many dimensions, by default None
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        AffineTransform

        Raises
        ------
        ValueError
            Incompatible factor.
        """
        if np.isscalar(factor):
            if ndim is None:
                raise ValueError("If factor is scalar, ndim must be defined")
            s = np.full((ndim, ndim - 1), factor)
        else:
            s = np.asarray(factor)
            if s.shape[0] != s.shape[1] + 1:
                raise ValueError("Factor must be of shape (D, D-1)")
            ndim = s.shape[0]

        m = np.eye(ndim, dtype=s.dtype)
        for col_idx in range(m.shape[1]):
            it = iter(factor[col_idx])  # type: ignore
            for row_idx in range(m.shape[0] - 1):
                if m[row_idx, col_idx] == 0:
                    m[row_idx, col_idx] = next(it)

        return cls(m, spaces=spaces)

Ancestors

Static methods

def from_linear_map(linear_map: ArrayLike, translation=0, *, spaces: SpaceTuple = (None, None)) ‑> Affine

Create an augmented affine matrix from a linear map, with an optional translation.

Parameters

linear_map : ArrayLike
Shape (D, D)
translation : ArrayLike, optional
Translation to add to the matrix, by default None
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

AffineTransform
 
Expand source code
@classmethod
def from_linear_map(
    cls,
    linear_map: ArrayLike,
    translation=0,
    *,
    spaces: SpaceTuple = (None, None),
) -> Affine:
    """Create an augmented affine matrix from a linear map,
    with an optional translation.

    Parameters
    ----------
    linear_map : ArrayLike
        Shape (D, D)
    translation : ArrayLike, optional
        Translation to add to the matrix, by default None
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    AffineTransform
    """
    lin_map = np.asarray(linear_map)

    side = len(lin_map) + 1
    matrix = np.eye(side, dtype=lin_map.dtype)
    matrix[:-1, :] = lin_map
    matrix[:-1, -1] = translation

    return cls(matrix, spaces=spaces)
def identity(ndim: int, *, spaces: SpaceTuple = (None, None)) ‑> Affine

Create an identity affine transformation.

Parameters

ndim : int
 
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

AffineTransform
 
Expand source code
@classmethod
def identity(
    cls,
    ndim: int,
    *,
    spaces: SpaceTuple = (None, None),
) -> Affine:
    """Create an identity affine transformation.

    Parameters
    ----------
    ndim : int
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    AffineTransform
    """
    return cls(
        np.eye(ndim + 1),
        spaces=spaces,
    )
def reflection(axis: Union[int, Container[int]], ndim: int, *, spaces: SpaceTuple = (None, None)) ‑> Affine

Create an affine reflection.

Parameters

axis : Union[int, Container[int]]
A single axis or multiple to reflect in.
ndim : int
How many dimensions to work in.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

AffineTransform
 
Expand source code
@classmethod
def reflection(
    cls,
    axis: Union[int, Container[int]],
    ndim: int,
    *,
    spaces: SpaceTuple = (None, None),
) -> Affine:
    """Create an affine reflection.

    Parameters
    ----------
    axis : Union[int, Container[int]]
        A single axis or multiple to reflect in.
    ndim : int
        How many dimensions to work in.
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    AffineTransform
    """
    if np.isscalar(axis):
        axis = [axis]  # type: ignore
    values = [-1 if idx in axis else 1 for idx in range(ndim)]  # type:ignore
    m = np.eye(ndim + 1)
    m[:-1, :-1] *= values

    return cls(m, spaces=spaces)
def rotation2(rotation: float, degrees=True, clockwise=False, *, spaces: SpaceTuple = (None, None)) ‑> Affine

Create a 2D affine rotation.

Parameters

rotation : float
Angle to rotate.
degrees : bool, optional
Whether rotation is in degrees (rather than radians), by default True
clockwise : bool, optional
Whether rotation is clockwise, by default False
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

AffineTransform
 
Expand source code
@classmethod
def rotation2(
    cls,
    rotation: float,
    degrees=True,
    clockwise=False,
    *,
    spaces: SpaceTuple = (None, None),
) -> Affine:
    """Create a 2D affine rotation.

    Parameters
    ----------
    rotation : float
        Angle to rotate.
    degrees : bool, optional
        Whether rotation is in degrees (rather than radians), by default True
    clockwise : bool, optional
        Whether rotation is clockwise, by default False
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    AffineTransform
    """
    if degrees:
        rotation = np.deg2rad(rotation)
    if clockwise:
        rotation *= -1

    vals = [
        [np.cos(rotation), -np.sin(rotation)],
        [np.sin(rotation), np.cos(rotation)],
    ]
    m = np.eye(3)
    m[:-1, :-1] = vals

    return cls(m, spaces=spaces)
def rotation3(rotation: Union[float, Tuple[float, float, float]], degrees=True, clockwise=False, order=(0, 1, 2), *, spaces: SpaceTuple = (None, None)) ‑> Affine

Create a 3D affine rotation.

Parameters

rotation : Union[float, Tuple[float, float, float]]
Either a single rotation for all axes, or 1 for each.
degrees : bool, optional
Whether rotation is in degrees (rather than radians), by default True
clockwise : bool, optional
Whether rotation is clockwise, by default False
order : tuple, optional
What order to apply the rotations, by default (0, 1, 2)
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

AffineTransform
 

Raises

ValueError
Incompatible order.
Expand source code
@classmethod
def rotation3(
    cls,
    rotation: Union[float, Tuple[float, float, float]],
    degrees=True,
    clockwise=False,
    order=(0, 1, 2),
    *,
    spaces: SpaceTuple = (None, None),
) -> Affine:
    """Create a 3D affine rotation.

    Parameters
    ----------
    rotation : Union[float, Tuple[float, float, float]]
        Either a single rotation for all axes, or 1 for each.
    degrees : bool, optional
        Whether rotation is in degrees (rather than radians), by default True
    clockwise : bool, optional
        Whether rotation is clockwise, by default False
    order : tuple, optional
        What order to apply the rotations, by default (0, 1, 2)
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    AffineTransform

    Raises
    ------
    ValueError
        Incompatible order.
    """
    if np.isscalar(rotation):
        r = np.array([rotation] * 3)
    else:
        r = np.asarray(rotation)

    if degrees:
        r = np.deg2rad(r)
    if clockwise:
        r *= -1

    if len(order) != 3 or set(order) != {0, 1, 2}:
        raise ValueError("Order must contain only 0, 1, 2 in any order.")

    order = list(order)

    rots = [
        np.array(
            [
                [1, 0, 0],
                [0, np.cos(r[0]), -np.sin(r[0])],
                [0, np.sin(r[0]), np.cos(r[0])],
            ]
        ),
        np.array(
            [
                [np.cos(r[1]), 0, np.sin(r[1])],
                [0, 1, 0],
                [-np.sin(r[1]), 0, np.cos(r[1])],
            ]
        ),
        np.array(
            [
                [np.cos(r[2]), -np.sin(r[2]), 0],
                [np.sin(r[2]), np.cos(r[2]), 0],
                [0, 0, 1],
            ]
        ),
    ]

    rot = rots[order.pop(0)]
    rot @= rots[order.pop(0)]
    rot @= rots[order.pop(0)]

    m = np.eye(4)
    m[:-1, :-1] = rot

    return cls(m, spaces=spaces)
def scaling(scale: ArrayLike, ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None)) ‑> Affine

Create an affine scaling.

Parameters

scale : ArrayLike
If scalar, broadcast to ndim.
ndim : Optional[int], optional
If scale is scalar, how many dimensions to use
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

AffineTransform
[description]
Expand source code
@classmethod
def scaling(
    cls,
    scale: ArrayLike,
    ndim: Optional[int] = None,
    *,
    spaces: SpaceTuple = (None, None),
) -> Affine:
    """Create an affine scaling.

    Parameters
    ----------
    scale : ArrayLike
        If scalar, broadcast to ndim.
    ndim : Optional[int], optional
        If scale is scalar, how many dimensions to use
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    AffineTransform
        [description]
    """
    s = arg_as_array(scale, ndim)
    m = np.eye(len(s) + 1, dtype=s.dtype)
    m[:-1, :-1] *= s

    return cls(m, spaces=spaces)
def shearing(factor: Union[float, np.ndarray], ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None)) ‑> Affine

Create an affine shear.

factor can be a scalar to broadcast to all dimensions, or a D-length list of D-1 lists. The first inner list contains the shear factors in the first dimension for all but the first dimension. The second inner list contains the shear factors in the second dimension for all the but the second dimension, etc.

Parameters

factor : Union[float, np.ndarray]
Shear scale factors; see above for more details.
ndim : Optional[int], optional
If factor is scalar, broadcast to this many dimensions, by default None
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

AffineTransform
 

Raises

ValueError
Incompatible factor.
Expand source code
@classmethod
def shearing(
    cls,
    factor: Union[float, np.ndarray],
    ndim: Optional[int] = None,
    *,
    spaces: SpaceTuple = (None, None),
) -> Affine:
    """Create an affine shear.

    `factor` can be a scalar to broadcast to all dimensions,
    or a D-length list of D-1 lists.
    The first inner list contains the shear factors in the first dimension
    for all *but* the first dimension.
    The second inner list contains the shear factors in the second dimension
    for all the *but* the second dimension, etc.

    Parameters
    ----------
    factor : Union[float, np.ndarray]
        Shear scale factors; see above for more details.
    ndim : Optional[int], optional
        If factor is scalar, broadcast to this many dimensions, by default None
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    AffineTransform

    Raises
    ------
    ValueError
        Incompatible factor.
    """
    if np.isscalar(factor):
        if ndim is None:
            raise ValueError("If factor is scalar, ndim must be defined")
        s = np.full((ndim, ndim - 1), factor)
    else:
        s = np.asarray(factor)
        if s.shape[0] != s.shape[1] + 1:
            raise ValueError("Factor must be of shape (D, D-1)")
        ndim = s.shape[0]

    m = np.eye(ndim, dtype=s.dtype)
    for col_idx in range(m.shape[1]):
        it = iter(factor[col_idx])  # type: ignore
        for row_idx in range(m.shape[0] - 1):
            if m[row_idx, col_idx] == 0:
                m[row_idx, col_idx] = next(it)

    return cls(m, spaces=spaces)
def translation(translation: ArrayLike, ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None)) ‑> Affine

Create an affine translation.

Parameters

translation : ArrayLike
If scalar, broadcast to ndim.
ndim : int, optional
If translation is scalar, how many dims to use.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

AffineTransform
 
Expand source code
@classmethod
def translation(
    cls,
    translation: ArrayLike,
    ndim: Optional[int] = None,
    *,
    spaces: SpaceTuple = (None, None),
) -> Affine:
    """Create an affine translation.

    Parameters
    ----------
    translation : ArrayLike
        If scalar, broadcast to ndim.
    ndim : int, optional
        If translation is scalar, how many dims to use.
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    AffineTransform
    """
    t = arg_as_array(translation, ndim)
    m = np.eye(len(t) + 1, dtype=t.dtype)
    m[:-1, -1] = t

    return cls(m, spaces=spaces)

Inherited members

class Identity (*, spaces: Tuple[Optional[Hashable], Optional[Hashable]] = (None, None))

Helper class that provides a standard way to create an ABC using inheritance.

Transform which does nothing.

Parameters

spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Raises

ValueError
[description]
Expand source code
class Identity(Transform):
    def __init__(
        self,
        *,
        spaces: SpaceTuple = (None, None),
    ):
        """
        Transform which does nothing.

        Parameters
        ----------
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Raises
        ------
        ValueError
            [description]
        """
        src = chain_or(*spaces, default=None)
        tgt = chain_or(*spaces[::-1], default=None)
        if src != tgt:
            raise ValueError("Source and target spaces are different")
        super().__init__(spaces=(src, src))

    def __invert__(self) -> Transform:
        return self

    def apply(self, coords: np.ndarray) -> np.ndarray:
        return coords.copy()

Ancestors

Inherited members

class Reflect (normals: Union[numpy.__array_like._SupportsArray[numpy.dtype], numpy.__nested_sequence._NestedSequence[numpy.__array_like._SupportsArray[numpy.dtype]], bool, int, float, complex, str, bytes, numpy.__nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], point=0, *, spaces: Tuple[Optional[Hashable], Optional[Hashable]] = (None, None))

Helper class that provides a standard way to create an ABC using inheritance.

Reflection about arbitrary planes.

Parameters

normals : sequence of arrays
Normal vectors to the planes of reflection. Unitised internally.
point : float or sequence of floats, optional
Intersection point of all reflection planes (can be broadcast from scalar), by default 0 (i.e. the origin)
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Raises

ValueError
Inconsistent dimensionality
Expand source code
class Reflect(Transform):
    def __init__(
        self,
        normals: ArrayLike,
        point=0,
        *,
        spaces: SpaceTuple = (None, None),
    ):
        """Reflection about arbitrary planes.

        Parameters
        ----------
        normals : sequence of arrays
            Normal vectors to the planes of reflection.
            Unitised internally.
        point : float or sequence of floats, optional
            Intersection point of all reflection planes
            (can be broadcast from scalar), by default 0 (i.e. the origin)
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Raises
        ------
        ValueError
            Inconsistent dimensionality
        """
        super().__init__(spaces=spaces)
        normals = np.asarray(normals)
        if normals.ndim == 1:
            normals = [normals]

        n1 = normals[0]
        if not np.isscalar(point) and len(n1) != len(point):
            raise ValueError("Point and normals are not of the same dimensionality")
        self.point = point
        self.ndim = {len(n1)}
        self.normals = [unitise(n) for n in normals]
        # todo: matmul is associative, so turn this into an affine in 2/3D?

    def apply(self, coords: np.ndarray) -> np.ndarray:
        coords = self._validate_coords(coords)
        out = coords - self.point
        for n in self.normals:
            # mul->sum vectorises dot product
            # normals are unit, avoids unnecessary division by 1
            out -= 2 * np.sum(coords * n, axis=1) * n
        out += self.point
        return out

    @classmethod
    def from_points(
        cls,
        points: ArrayLike,
        *,
        spaces: SpaceTuple = (None, None),
    ):
        """Infer a single plane of reflection from a minimal number of points on it.

        Parameters
        ----------
        points :
            NxD array of N points in D dimensions. N == D
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        Reflection
        """
        point, normals = get_hyperplanes(np.asarray(points), unitise=False)
        return cls(normals, point, spaces=spaces)

    @classmethod
    def from_axis(
        cls,
        axis: Union[int, Sequence[int]],
        origin: ArrayLike,
        *,
        spaces: SpaceTuple = (None, None),
    ):
        """Reflect around hyperplane(s) parallel with axes.

        Parameters
        ----------
        axis : int or sequence of int
            Index (or indices) of axes in which to reflect.
        origin : array-like
            Point around which to reflect.
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Returns
        -------
        Reflection

        Raises
        ------
        ValueError
            Selected axis does not exist.
        """
        origin = np.asarray(origin)
        axis = ensure_tuple(axis)

        for a in axis:
            if a >= len(axis):
                raise ValueError(
                    "Cannot reflect in axis which does not exist (too high)"
                )

        normals = []
        for i in range(len(origin) - len(axis) + 1):
            if i not in axis:
                v = np.zeros_like(origin)
                v[i] += 1
                normals.append(v)

        return cls(normals, origin, spaces=spaces)

    def __invert__(self):
        return copy(self)

Ancestors

Static methods

def from_axis(axis: Union[int, Sequence[int]], origin: Union[numpy.__array_like._SupportsArray[numpy.dtype], numpy.__nested_sequence._NestedSequence[numpy.__array_like._SupportsArray[numpy.dtype]], bool, int, float, complex, str, bytes, numpy.__nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], *, spaces: Tuple[Optional[Hashable], Optional[Hashable]] = (None, None))

Reflect around hyperplane(s) parallel with axes.

Parameters

axis : int or sequence of int
Index (or indices) of axes in which to reflect.
origin : array-like
Point around which to reflect.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

Reflection
 

Raises

ValueError
Selected axis does not exist.
Expand source code
@classmethod
def from_axis(
    cls,
    axis: Union[int, Sequence[int]],
    origin: ArrayLike,
    *,
    spaces: SpaceTuple = (None, None),
):
    """Reflect around hyperplane(s) parallel with axes.

    Parameters
    ----------
    axis : int or sequence of int
        Index (or indices) of axes in which to reflect.
    origin : array-like
        Point around which to reflect.
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    Reflection

    Raises
    ------
    ValueError
        Selected axis does not exist.
    """
    origin = np.asarray(origin)
    axis = ensure_tuple(axis)

    for a in axis:
        if a >= len(axis):
            raise ValueError(
                "Cannot reflect in axis which does not exist (too high)"
            )

    normals = []
    for i in range(len(origin) - len(axis) + 1):
        if i not in axis:
            v = np.zeros_like(origin)
            v[i] += 1
            normals.append(v)

    return cls(normals, origin, spaces=spaces)
def from_points(points: Union[numpy.__array_like._SupportsArray[numpy.dtype], numpy.__nested_sequence._NestedSequence[numpy.__array_like._SupportsArray[numpy.dtype]], bool, int, float, complex, str, bytes, numpy.__nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], *, spaces: Tuple[Optional[Hashable], Optional[Hashable]] = (None, None))

Infer a single plane of reflection from a minimal number of points on it.

Parameters

points :
NxD array of N points in D dimensions. N == D
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Returns

Reflection
 
Expand source code
@classmethod
def from_points(
    cls,
    points: ArrayLike,
    *,
    spaces: SpaceTuple = (None, None),
):
    """Infer a single plane of reflection from a minimal number of points on it.

    Parameters
    ----------
    points :
        NxD array of N points in D dimensions. N == D
    spaces : tuple[SpaceRef, SpaceRef]
        Optional source and target spaces

    Returns
    -------
    Reflection
    """
    point, normals = get_hyperplanes(np.asarray(points), unitise=False)
    return cls(normals, point, spaces=spaces)

Inherited members

class Scale (scale: Union[numpy.__array_like._SupportsArray[numpy.dtype], numpy.__nested_sequence._NestedSequence[numpy.__array_like._SupportsArray[numpy.dtype]], bool, int, float, complex, str, bytes, numpy.__nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], *, spaces: Tuple[Optional[Hashable], Optional[Hashable]] = (None, None))

Helper class that provides a standard way to create an ABC using inheritance.

Simple scale transform.

All points are scaled, i.e. distance from the origin may also change.

Parameters

scale : scalar or D-length array-like
Scaling to apply in all dimensions, or each dimension.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Raises

ValueError
If scale is the wrong shape.
Expand source code
class Scale(Transform):
    def __init__(
        self,
        scale: ArrayLike,
        *,
        spaces: SpaceTuple = (None, None),
    ):
        """Simple scale transform.

        All points are scaled, i.e. distance from the origin may also change.

        Parameters
        ----------
        scale : scalar or D-length array-like
            Scaling to apply in all dimensions, or each dimension.
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Raises
        ------
        ValueError
            If scale is the wrong shape.
        """
        super().__init__(spaces=spaces)
        self.scale = np.asarray(scale)
        if self.scale.ndim > 1:
            raise ValueError("Scale must be scalar or 1D")

        if self.scale.shape not in [(), (1,)]:
            self.ndim = {self.scale.shape[0]}
        # otherwise, can be broadcast to anything

    def apply(self, coords: np.ndarray) -> np.ndarray:
        coords = self._validate_coords(coords)
        return coords * self.scale

    def __invert__(self) -> Transform:
        return type(self)(
            1 / self.scale,
            spaces=self.spaces[::-1],
        )

Ancestors

Inherited members

class Translate (translation: Union[numpy.__array_like._SupportsArray[numpy.dtype], numpy.__nested_sequence._NestedSequence[numpy.__array_like._SupportsArray[numpy.dtype]], bool, int, float, complex, str, bytes, numpy.__nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], *, spaces: Tuple[Optional[Hashable], Optional[Hashable]] = (None, None))

Helper class that provides a standard way to create an ABC using inheritance.

Simple translation.

Parameters

translation : scalar or D-length array
Translation to apply in all dimensions, or each dimension.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces

Raises

ValueError
If the translation is the wrong shape
Expand source code
class Translate(Transform):
    def __init__(
        self,
        translation: ArrayLike,
        *,
        spaces: SpaceTuple = (None, None),
    ):
        """Simple translation.

        Parameters
        ----------
        translation : scalar or D-length array
            Translation to apply in all dimensions, or each dimension.
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces

        Raises
        ------
        ValueError
            If the translation is the wrong shape
        """
        super().__init__(spaces=spaces)
        self.translation = np.asarray(translation)
        if self.translation.ndim > 1:
            raise ValueError("Translation must be scalar or 1D")

        if self.translation.shape not in [(), (1,)]:
            self.ndim = {self.translation.shape[0]}
        # otherwise, can be broadcast to anything

    def apply(self, coords: np.ndarray) -> np.ndarray:
        self._validate_coords(coords)
        return coords + self.translation

    def __invert__(self) -> Transform:
        return type(self)(-self.translation, spaces=self.spaces[::-1])

Ancestors

Inherited members