Source code for nrv.utils.geom._ellipse

import numpy as np
from copy import deepcopy

from ._cshape import CShape
from .._units import to_nrv_unit


[docs] class Ellipse(CShape): """ Ellipse class that inherits from Cshape. Represents an ellipse with a center, semi-major axis, and semi-minor axis. """
[docs] def __init__( self, center: tuple[float, float] = (0, 0), radius: tuple[float, float] = (10, 10), rot: float = None, degree: bool = False, ): """ Initializes the Ellipse Parameters ---------- center : tuple[float, float], optional Center of the shape, by default (0,0) radius : tuple[float,float], optional Radius of the shape, for none circular shape set as an average distance between trace points and center, by default (10,10) rot : float, optional rotation of the shape, by default None degree : bool, optional if True `rot` is in degree, if False in radian, by default False """ super().__init__(center, radius, rot=rot, degree=degree) assert np.iterable( radius ), "Ellipse radius must be iterable (of lenght at least equal to 2)" self.r1 = self.radius[0] self.r2 = self.radius[1]
@property def c(self) -> np.ndarray: """ Center of the ellipse as an array. Returns ------- np.ndarray Ellipse center coordinates. """ return np.array(self.center, dtype=float) @property def r(self) -> np.ndarray: """ Semi-axis lengths of the ellipse. Returns ------- np.ndarray Array containing ``r1`` and ``r2``. """ return np.array(self.radius, dtype=float) @property def area(self) -> float: """ Area of the ellipse. Returns ------- float Ellipse area. """ return np.pi * self.r1 * self.r2 @property def perimeter(self) -> float: """ Perimeter of the shape in $\\mu m^2$ Warning ------- For ellipse perimeter is only the `Simple arithmetic-geometric mean approximation <https://en.wikipedia.org/wiki/Perimeter_of_an_ellipse>`_. To use with cation as it can be limited for eccentric ellipses. """ return 2 * np.pi * (np.sum(self.r**2) / 2) ** 0.5 @property def bbox_size(self) -> tuple[float, float]: """ Size of the axis-aligned bounding box. Returns ------- tuple[float, float] Bounding-box width and height. """ return ( np.hypot(2 * self.r1 * np.cos(-self.rot), 2 * self.r2 * np.sin(-self.rot)), np.hypot(2 * self.r1 * np.sin(-self.rot), 2 * self.r2 * np.cos(-self.rot)), ) @property def bbox(self): """ Coordinates of the axis-aligned bounding box. Returns ------- np.ndarray Bounding box as ``[ymin, zmin, ymax, zmax]``. """ b_s = self.bbox_size return np.array( [ self.center[0] - b_s[0] / 2, self.center[1] - b_s[1] / 2, self.center[0] + b_s[0] / 2, self.center[1] + b_s[1] / 2, ] ) @property def rot_mat(self) -> np.ndarray: """ rotation matrix Returns ------- np.ndarray """ return np.array( [ [np.cos(self.rot), -np.sin(self.rot)], [np.sin(self.rot), np.cos(self.rot)], ] ) @property def rot_mat_inverse(self) -> np.ndarray: """ Inverse rotation matrix Returns ------- np.ndarray """ return np.array( [ [np.cos(-self.rot), -np.sin(-self.rot)], [np.sin(-self.rot), np.cos(-self.rot)], ] ) @property def is_rot(self) -> bool: """ Indicate whether the ellipse is rotated. Returns ------- bool ``True`` if the rotation angle is non-zero. """ return bool(self.rot)
[docs] def is_inside( self, point: tuple[np.ndarray, np.ndarray], delta: float = 0, for_all: bool = True, ) -> bool: """ Check whether one or several points lie inside the ellipse. Parameters ---------- point : tuple[np.ndarray, np.ndarray] | np.ndarray Point or set of points to test. delta : float, optional Additional margin added to the semi-axis lengths. for_all : bool, optional If ``True``, return a single boolean for all points. Returns ------- bool | np.ndarray Inclusion test result. """ if isinstance(point, np.ndarray): X = deepcopy(point) else: X = np.array(point).astype(float).T # Translate back to the ellipse's coordinate system X -= self.c # Rotate back to the ellipse's coordinate system if self.is_rot: X @= self.rot_mat_inverse # Normalize the coordinate X /= np.array((self.r1 + delta, self.r2 + delta)) X_norm = np.sum(X**2, axis=-1) # Check if the normalized point is inside the unit circle if for_all: return (X_norm <= 1).all() return X_norm <= 1
[docs] def rotate(self, angle: float, degree: bool = False): """ Rotate the ellipse around its center. Parameters ---------- angle : float Rotation angle. degree : bool, optional If ``True``, ``angle`` is given in degrees. """ if degree: angle = to_nrv_unit(angle, "deg") self.rot = (self.rot + angle) % (2 * np.pi)
[docs] def get_trace(self, n_theta=100) -> tuple[list[float], list[float]]: """ Sample the ellipse boundary. Parameters ---------- n_theta : int, optional Number of boundary points to generate. Returns ------- tuple[np.ndarray, np.ndarray] Y and Z coordinates of the sampled boundary. """ _theta = np.linspace(0, 2 * np.pi, n_theta, endpoint=True) y_trace = self.r1 * np.cos(_theta) z_trace = self.r2 * np.sin(_theta) X = np.vstack( ( y_trace, z_trace, ) ).T # Apply rotation if needed if self.is_rot: X = X @ self.rot_mat # Apply translation X += self.c return X[:, 0], X[:, 1]
[docs] def get_point_inside(self, n_pts: int = 1, delta: float = 0) -> np.ndarray: """ Draw random points inside the ellipse. Parameters ---------- n_pts : int, optional Number of points to generate. delta : float, optional Minimum distance to keep from the boundary. Returns ------- np.ndarray Array of sampled points of shape ``(n_pts, 2)``. """ _theta = np.random.random(n_pts) * 2 * np.pi _rf = np.sqrt(np.random.random(n_pts)) # Get a random point inside an unit circle X = np.vstack( ( _rf * np.cos(_theta), _rf * np.sin(_theta), ) ).T # Scale to ellipse dimension X *= np.array((self.r1 - delta, self.r2 - delta)) # Rotate and translate if self.is_rot: X = X @ self.rot_mat X += self.c return X