Source code for

An n-dimensional, axes-aligned cuboid

This module defines the :class:`Cuboid` class, which represents an n-dimensional
cuboid that is aligned with the axes of a Cartesian coordinate system.

.. codeauthor:: David Zwicker <>

import itertools
from typing import List, Tuple

import numpy as np

from .typing import FloatNumerical

[docs]class Cuboid: """class that represents a cuboid in :math:`n` dimensions""" def __init__(self, pos, size, mutable: bool = True): """defines a cuboid from a position and a size vector Args: pos (list): The position of the lower left corner. The length of this list determines the dimensionality of space size (list): The size of the cuboid along each dimension. mutable (bool): Flag determining whether the cuboid parameters can be changed """ self._mutable = mutable # set position and adjust mutable status later self.pos = np.array(pos, copy=True) self.size = size # implicitly sets correct shape self.pos.flags.writeable = self.mutable @property def size(self) -> np.ndarray: return self._size @size.setter def size(self, value: FloatNumerical): self._size = np.array(value, self.pos.dtype) # make copy if self.pos.shape != self._size.shape: raise ValueError( f"Size vector (dim={len(self._size)}) must have the same " f"dimension as the position vector (dim={len(self.pos)})" ) # flip Cuboid with negative size neg = self._size < 0 self.pos[neg] += self._size[neg] self._size = np.abs(self._size) self._size.flags.writeable = self.mutable @property def corners(self) -> Tuple[np.ndarray, np.ndarray]: """return coordinates of two extreme corners defining the cuboid""" return np.copy(self.pos), self.pos + self.size @property def mutable(self) -> bool: return self._mutable @mutable.setter def mutable(self, value: bool): self._mutable = bool(value) self.pos.flags.writeable = self._mutable self._size.flags.writeable = self._mutable
[docs] @classmethod def from_points(cls, p1: np.ndarray, p2: np.ndarray, **kwargs) -> "Cuboid": """create cuboid from two points Args: p1 (list): Coordinates of first corner point p2 (list): Coordinates of second corner point Returns: Cuboid: cuboid with positive size """ p1 = np.asarray(p1) p2 = np.asarray(p2) return cls(p1, p2 - p1, **kwargs)
[docs] @classmethod def from_bounds(cls, bounds: np.ndarray, **kwargs) -> "Cuboid": """create cuboid from bounds Args: bounds (list): Two dimensional array of axes bounds Returns: Cuboid: cuboid with positive size """ bounds = np.asarray(bounds).reshape(-1, 2) return cls(bounds[:, 0], bounds[:, 1] - bounds[:, 0], **kwargs)
[docs] @classmethod def from_centerpoint( cls, centerpoint: np.ndarray, size: np.ndarray, **kwargs ) -> "Cuboid": """create cuboid from two points Args: centerpoint (list): Coordinates of the center size (list): Size of the cuboid Returns: Cuboid: cuboid with positive size """ centerpoint = np.asarray(centerpoint) size = np.asarray(size) return cls(centerpoint - size / 2, size, **kwargs)
[docs] def copy(self) -> "Cuboid": return self.__class__(self.pos, self.size)
def __repr__(self): return "{cls}(pos={pos}, size={size})".format( cls=self.__class__.__name__, pos=self.pos, size=self.size ) def __add__(self, other: "Cuboid") -> "Cuboid": """The sum of two cuboids is the minimal cuboid enclosing both""" if isinstance(other, Cuboid): if self.dim != other.dim: raise RuntimeError("Incompatible dimensions") a1, a2 = self.corners b1, b2 = other.corners return self.__class__.from_points(np.minimum(a1, b1), np.maximum(a2, b2)) else: return NotImplemented def __eq__(self, other) -> bool: """override the default equality test""" if not isinstance(other, self.__class__): return NotImplemented return np.all(self.pos == other.pos) and np.all(self.size == other.size) # type: ignore @property def dim(self) -> int: return len(self.pos) @property def bounds(self) -> Tuple[Tuple[float, float], ...]: return tuple((p, p + s) for p, s in zip(self.pos, self.size)) @property def vertices(self) -> List[List[float]]: """return the coordinates of all the corners""" return list(itertools.product(*self.bounds)) # type: ignore @property def diagonal(self) -> float: """returns the length of the diagonal""" return np.linalg.norm(self.size) # type: ignore @property def surface_area(self) -> float: """surface area of a cuboid in :math:`n` dimensions. The surface area is the volume of the (:math:`n-1`)-dimensional hypercubes that bound the current cuboid: * :math:`n=1`: the number of end points (2) * :math:`n=2`: the perimeter of the rectangle * :math:`n=3`: the surface area of the cuboid """ sides = self.size null = sides == 0 null_count = null.sum() if null_count == 0: return 2 * np.sum(np.product(sides) / sides) # type: ignore elif null_count == 1: return 2 * np.product(sides[~null]) # type: ignore else: return 0 @property def centroid(self): return self.pos + self.size / 2 @centroid.setter def centroid(self, center): self.pos[:] = np.asanyarray(center) - self.size / 2 @property def volume(self) -> float: return # type: ignore
[docs] def buffer(self, amount: FloatNumerical = 0, inplace=False) -> "Cuboid": """dilate the cuboid by a certain amount in all directions""" amount = np.asarray(amount) if inplace: self.pos -= amount self.size += 2 * amount return self else: return self.__class__(self.pos - amount, self.size + 2 * amount)
[docs] def contains_point(self, points: np.ndarray) -> np.ndarray: """returns a True when `points` are within the Cuboid Args: points (:class:`~numpy.ndarray`): List of point coordinates Returns: :class:`~numpy.ndarray`: list of booleans indicating which points are inside """ points = np.asarray(points) if len(points) == 0: return points if points.shape[-1] != self.dim: raise ValueError( "Last dimension of `points` must agree with " f"cuboid dimension {self.dim}" ) c1, c2 = self.corners return np.all(c1 <= points, axis=-1) & np.all(points <= c2, axis=-1) # type: ignore
[docs]def asanyarray_flags(data: np.ndarray, dtype=None, writeable: bool = True): """turns data into an array and sets the respective flags. A copy is only made if necessary Args: data (:class:`~numpy.ndarray`): numpy array that whose flags are adjusted dtype: the resulant dtype writeable (bool): Flag determining whether the results is writable Returns: :class:`~numpy.ndarray`: array with same data as `data` but with flags adjusted. """ try: data_writeable = data.flags.writeable except AttributeError: # `data` did not have the writeable flag => it's not a numpy array result = np.array(data, dtype) else: if data_writeable != writeable: # need to make a copy because the flags differ result = np.array(data, dtype) else: # might have to make a copy to adjust the dtype result = np.asanyarray(data, dtype) # set the flags and return the array result.flags.writeable = writeable return result