Package transformnd

transformnd

Code style: black GitHub Test Status Docs Status

A library providing an API for coordinate transformations, as well as some common transforms. The goal is to allow downstream applications which require such transformations (e.g. image registration) to be generic over anything inheriting from Transform.

The base classes and utilities are very lightweight with few dependencies, for use as an API; additional transforms and features use extras.

Heavily inspired by/ cribbed directly from Philipp Schlegel's work in navis; co-developed with xform as a red team prototype.

N coordinates in D dimensions are given as a numpy array of shape (N, D).

Transform subclasses which are restricted to certain dimensionalities can specify this in their ndim class variable. Instances of Transform subclasses can further restrict their ndim. Use self._validate_coords(coords) in __call__ to ensure the coordinates are of valid type and dimensions.

Additionally, transformnd provides an interface for transforming types other than NxD numpy arrays, and implements these adapters for a few common types.

See the tutorial here.

Implemented transforms

Arbitrary transforms can be composed into a TransformSequence with transform1 | transform2. A graph of transforms between defined spaces can be traversed using the TransformGraph.

Implemented adapters

Additional transforms and adapters

Contributions of additional transforms and adapters are welcome! Even if they're only thin wrappers around an external library, the downstream ecosystem benefits from a consistent API.

Such external transformation libraries should be specified as "extras", and be contained in a submodule so that they are not immediately imported with transformnd. Dependencies for new adapters do not need to be included in transformnd's dependencies, but should be specified in the requirements.txt for tests.

Alternatively, consider adopting transformnd's base classes in your own library, and have your transformation instantly compatible for downstream users.

Expand source code
"""
.. include:: ../../README.md
"""
from .base import Transform, TransformSequence, TransformWrapper
from .util import SpaceRef, TransformSignature, check_ndim
from .version import version as __version__  # noqa: F401
from .version import version_tuple as __version_info__  # noqa: F401

__all__ = [
    "Transform",
    "TransformSequence",
    "TransformWrapper",
    "TransformSignature",
    "SpaceRef",
    "check_ndim",
]

Sub-modules

transformnd.adapters

Adapters for transforming objects which are not well-behaved numpy arrays …

transformnd.base

Base classes and wrappers for transforms.

transformnd.graph

Bridging transforms between known spaces.

transformnd.transforms

Implementations of some common transforms.

transformnd.util

Utilities used elsewhere in the package.

transformnd.version

Functions

def check_ndim(given_ndim: int, supported_ndim: Optional[Set[int]])

Raise a ValueError if dimensionality is unsupported.

Parameters

given_ndim : int
The dimensionality to check.
supported_ndim : Optional[Set[int]]
Which dimensions are supported. If None, the check passes.

Raises

ValueError
If supported dimensions are defined and given_ndim is not in them.
Expand source code
def check_ndim(given_ndim: int, supported_ndim: Optional[Set[int]]):
    """Raise a ValueError if dimensionality is unsupported.

    Parameters
    ----------
    given_ndim : int
        The dimensionality to check.
    supported_ndim : Optional[Set[int]]
        Which dimensions are supported.
        If None, the check passes.

    Raises
    ------
    ValueError
        If supported dimensions are defined and given_ndim is not in them.
    """
    if supported_ndim is not None and given_ndim not in supported_ndim:
        raise ValueError(
            f"Transform supported for {format_dims(supported_ndim)}, not {given_ndim}"
        )

Classes

class Transform (*, spaces: SpaceTuple = (None, None))

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

Base class for transformations.

Parameters

spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Expand source code
class Transform(ABC):
    ndim: Optional[Set[int]] = None

    def __init__(
        self,
        *,
        spaces: SpaceTuple = (None, None),
    ):
        """Base class for transformations.

        Parameters
        ----------
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces
        """
        self.spaces = spaces

    @property
    def source_space(self):
        return self.spaces[0]

    @property
    def target_space(self):
        return self.spaces[1]

    def _validate_coords(self, coords) -> np.ndarray:
        """Check that dimension of coords are supported.

        Also ensure that coords is a 2D numpy array.

        Parameters
        ----------
        coords : np.ndarray
            NxD array of N D-dimensional coordinates.

        Raises
        ------
        ValueError
            If dimensions are not supported.
        """
        coords = np.asarray(coords)
        if coords.ndim != 2:
            raise ValueError("Coords must be a 2D array")
        check_ndim(coords.shape[1], self.ndim)
        return coords

    @abstractmethod
    def apply(self, coords: np.ndarray) -> np.ndarray:
        """Apply transformation.

        Parameters
        ----------
        coords : np.ndarray
            NxD array of N D-dimensional coordinates.

        Returns
        -------
        np.ndarray
            Transformed coordinates in the same shape.
        """
        pass

    def __invert__(self) -> Transform:
        """Invert transformation if possible.

        Returns
        -------
        Transform
            Inverted transformation.
        """
        return NotImplemented

    def __or__(self, other) -> TransformSequence:
        """Compose transformations into a sequence.

        If other is a TransformSequence, prepend this transform to the others.

        Parameters
        ----------
        other : Transform

        Returns
        -------
        TransformSequence
        """
        if not isinstance(other, Transform):
            return NotImplemented
        transforms = get_transform_list(self) + get_transform_list(other)
        return TransformSequence(
            transforms,
            spaces=(self.source_space, other.target_space),
        )

    def __ror__(self, other) -> TransformSequence:
        """Compose transformations into a sequence.

        If other is a TransformSequence, append this transform to the others.

        Parameters
        ----------
        other : Transform

        Returns
        -------
        TransformSequence
        """
        if not isinstance(other, Transform):
            return NotImplemented
        transforms = get_transform_list(other) + get_transform_list(self)
        return TransformSequence(
            transforms,
            spaces=(other.source_space, self.target_space),
        )

    def __str__(self) -> str:
        cls_name = type(self).__name__
        src = space_str(self.source_space)
        tgt = space_str(self.target_space)
        return f"{cls_name}[{src}->{tgt}]"

Ancestors

  • abc.ABC

Subclasses

Class variables

var ndim : Optional[Set[int]]

Instance variables

var source_space
Expand source code
@property
def source_space(self):
    return self.spaces[0]
var target_space
Expand source code
@property
def target_space(self):
    return self.spaces[1]

Methods

def apply(self, coords: np.ndarray) ‑> numpy.ndarray

Apply transformation.

Parameters

coords : np.ndarray
NxD array of N D-dimensional coordinates.

Returns

np.ndarray
Transformed coordinates in the same shape.
Expand source code
@abstractmethod
def apply(self, coords: np.ndarray) -> np.ndarray:
    """Apply transformation.

    Parameters
    ----------
    coords : np.ndarray
        NxD array of N D-dimensional coordinates.

    Returns
    -------
    np.ndarray
        Transformed coordinates in the same shape.
    """
    pass
class TransformSequence (transforms: Sequence[Transform], *, spaces: SpaceTuple = (None, None))

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

Combine transforms by chaining them.

Also checks for consistent dimensionality and space references, inferring if None.

Parameters

transforms : List[Transform]
Items which are a TransformSequences will each still be treated as a single transform.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces. Can also be inferred from the first and last transforms.

Raises

ValueError
If spaces are incompatible.
Expand source code
class TransformSequence(Transform, Sequence[Transform]):
    def __init__(
        self,
        transforms: Sequence[Transform],
        *,
        spaces: SpaceTuple = (None, None),
    ) -> None:
        """Combine transforms by chaining them.

        Also checks for consistent dimensionality and space references,
        inferring if None.

        Parameters
        ----------
        transforms : List[Transform]
            Items which are a TransformSequences
            will each still be treated as a single transform.
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces.
            Can also be inferred from the first and last transforms.

        Raises
        ------
        ValueError
            If spaces are incompatible.
        """
        ts = infer_spaces(transforms, *spaces)

        super().__init__(
            spaces=(
                ts[0].source_space,
                ts[-1].target_space,
            ),
        )

        self.transforms: List[Transform] = ts

        self.ndim = None
        for t in self.transforms:
            self.ndim = dim_intersection(self.ndim, t.ndim)

        if self.ndim is not None and len(self.ndim) == 0:
            raise ValueError("Transforms have incompatible dimensionalities")

    def __iter__(self) -> Iterator[Transform]:
        """Iterate through component transforms.

        Yields
        -------
        Transform
        """
        yield from self.transforms

    def __len__(self) -> int:
        """Number of transforms.

        Returns
        -------
        int
        """
        return len(self.transforms)

    def __invert__(self) -> Transform:
        try:
            transforms = [~t for t in reversed(self.transforms)]
        except NotImplementedError:
            return NotImplemented
        return type(self)(
            transforms,
            spaces=self.spaces[::-1],
        )

    def apply(self, coords: np.ndarray) -> np.ndarray:
        for t in self.transforms:
            coords = t.apply(coords)
        return coords

    def list_spaces(self, skip_none=False) -> List[SpaceRef]:
        """List spaces in this transform.

        Parameters
        ----------
        skip_none : bool, optional
            Whether to skip undefined spaces, default False.

        Returns
        -------
        List[SpaceRef]
        """
        spaces = [self.source_space] + [t.target_space for t in self.transforms]
        if skip_none:
            spaces = [s for s in spaces if s is not None]
        return spaces

    def __str__(self) -> str:
        cls_name = type(self).__name__
        spaces_str = "->".join(space_str(s) for s in self.list_spaces())
        return f"{cls_name}[{spaces_str}]"

    def __getitem__(self, idx: Union[slice, int]):
        if isinstance(idx, int):
            return self.transforms[idx]
        return type(self)(self.transforms[idx])

Ancestors

  • Transform
  • abc.ABC
  • collections.abc.Sequence
  • collections.abc.Reversible
  • collections.abc.Collection
  • collections.abc.Sized
  • collections.abc.Iterable
  • collections.abc.Container
  • typing.Generic

Methods

def list_spaces(self, skip_none=False) ‑> List[Hashable]

List spaces in this transform.

Parameters

skip_none : bool, optional
Whether to skip undefined spaces, default False.

Returns

List[SpaceRef]
 
Expand source code
def list_spaces(self, skip_none=False) -> List[SpaceRef]:
    """List spaces in this transform.

    Parameters
    ----------
    skip_none : bool, optional
        Whether to skip undefined spaces, default False.

    Returns
    -------
    List[SpaceRef]
    """
    spaces = [self.source_space] + [t.target_space for t in self.transforms]
    if skip_none:
        spaces = [s for s in spaces if s is not None]
    return spaces

Inherited members

class TransformWrapper (fn: TransformSignature, ndim: Optional[Union[Set[int], int]] = None, *, spaces: SpaceTuple = (None, None))

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

Wrapper around an arbitrary function.

Callable should take and return an identically-shaped NxD numpy array of N D-dimensional coordinates.

Parameters

fn : TransformSignature
Callable.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Expand source code
class TransformWrapper(Transform):
    def __init__(
        self,
        fn: TransformSignature,
        ndim: Optional[Union[Set[int], int]] = None,
        *,
        spaces: SpaceTuple = (None, None),
    ):
        """Wrapper around an arbitrary function.

        Callable should take and return an identically-shaped
        NxD numpy array of N D-dimensional coordinates.

        Parameters
        ----------
        fn : TransformSignature
            Callable.
        spaces : tuple[SpaceRef, SpaceRef]
            Optional source and target spaces
        """
        super().__init__(spaces=spaces)
        self.fn = fn
        if ndim is not None:
            if isinstance(ndim, int):
                self.ndim = {ndim}
            else:
                self.ndim = set(ndim)

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

Ancestors

Inherited members