Source code for pargeo.geometry

"""Geometry module.

Utility module for creating geometries. The module provides a set of classes
for creating geometries such as rectangles, circles, ellipses, and star-like
geometries. These geometries can be discretized using the `to_polygon` method,
and used in the `Domain` class.

Example:

    >>> from pargeo.domain import Domain
    >>> from pargeo.geometry import Rectangle, Circle
    <BLANKLINE>
    >>> domain = Rectangle(center=(0, 0), width=1, height=1).to_polygon()
    >>> domain = Domain(domain, holes={1})
    >>> hole = Circle(center=(.5, .5), radius=.1).to_polygon()
    >>> domain.add(polygon=hole, level=1)

"""
import math
import random
from abc import ABC, abstractmethod

from pargeo.utils.geometry_utils import (
    discretize_ellipse,
    polar_to_cartesian,
    trigonometric_function,
)
from pargeo.utils.typing_utils import Polygon, Vector


[docs] class Geometry(ABC): """Abstract class for geometries."""
[docs] @abstractmethod def to_polygon(self, *args, **kwargs) -> Polygon: """Return a shapely polygon representation of self.""" pass
[docs] class Box(Geometry): """Box geometry.""" def __init__(self, left_bottom: Vector, right_top: Vector): """Creates a rectangle. Args: left_bottom (tuple): The (x, y) coordinates of the left bottom corner. right_top (tuple): The (x, y) coordinates of the right top corner. """ self.left_bottom = left_bottom self.right_top = right_top self.center = tuple(right_top[i] - left_bottom[i] for i in range(2)) self.width = right_top[0] - left_bottom[0] self.height = right_top[1] - left_bottom[1]
[docs] @classmethod def from_center(cls, center: Vector, width: float, height: float): """ Creates a box from the center. Args: center (tuple): The (x, y) coordinates of the center of the box. width (float): The width of the box. height (float): The height of the box. Returns: Box: The created box object. """ left_bottom = (center[0] - width / 2, center[1] - height / 2) right_top = (center[0] + width / 2, center[1] + height / 2) return cls(left_bottom, right_top)
[docs] def to_polygon(self) -> Polygon: """Return the corners of the rectangle in counter-clockwise order.""" p0 = self.left_bottom p1 = (self.right_top[0], self.left_bottom[1]) p2 = self.right_top p3 = (self.left_bottom[0], self.right_top[1]) return Polygon([p0, p1, p2, p3])
[docs] class NStar(Geometry): """NStar geometry.""" def __init__( self, center: Vector, radius_in: float, radius_out: float, n_peaks: int, alpha: float = 0.0, ): """Make an NStar. Args: center: The center. radius_in: The inner radius. radius_out: The outer radius. N: Number of points. alpha: Angle offset. TODO: Explain this. """ if radius_in > radius_out: raise ValueError( "The parameter setting radius_in > radius_out not allowed." ) self._center = center self._inner_radius = radius_in self._outer_radius = radius_out self._n_peaks = n_peaks self.__alpha = alpha
[docs] def to_polygon(self) -> Polygon: aux_angles = [2 * math.pi * i / self._n_peaks for i in range(self._n_peaks)] offset = math.pi / self._n_peaks angles_in = [angle + self.__alpha + offset for angle in aux_angles] angles_out = [angle + self.__alpha for angle in aux_angles] coords_in = [ polar_to_cartesian(angle, self._inner_radius) for angle in angles_in ] coords_out = [ polar_to_cartesian(angle, self._outer_radius) for angle in angles_out ] # Translate to center mx, my = self._center coords_in = [(x + mx, y + my) for x, y in coords_in] coords_out = [(x + mx, y + my) for x, y in coords_out] # Create a shapely polygon xy = [] for xy_in, xy_out in zip(coords_in, coords_out): xy.append(xy_out) xy.append(xy_in) return Polygon(xy)
[docs] class StarLike(Geometry): """Abstract class for star-like geometries.""" def __init__(self, center: Vector) -> None: self.center = center
[docs] @abstractmethod def radius_at(self, angle: float) -> float: """Returns the distance at a given angle to the center.""" raise NotImplementedError
[docs] def to_polygon(self, refs: int) -> Polygon: """Return a shapely polygon representation of self.""" return self.discretize(refs)
[docs] def discretize(self, refs: int) -> Polygon: # TODO: also give angles as alternative. angles = [i * 2 * math.pi / refs for i in range(refs)] radii = [self.radius_at(angle) for angle in angles] mx, my = self.center coords = [polar_to_cartesian(angle, rad) for angle, rad in zip(angles, radii)] return Polygon([(x + mx, y + my) for x, y in coords])
[docs] class Circle(StarLike): def __init__(self, center: Vector, radius) -> None: self.radius = radius super().__init__(center)
[docs] def radius_at(self, angle: float) -> float: return self.radius
[docs] class Ellipse(StarLike): def __init__(self, center: Vector, axis: Vector, angle: float = 0) -> None: self.axis = axis self.angle = angle super().__init__(center)
[docs] def radius_at(self, angle: float) -> float: # TODO: Check if this is correct return math.sqrt( (self.axis[0] * math.cos(angle)) ** 2 + (self.axis[1] * math.sin(angle)) ** 2 )
[docs] def discretize(self, refs) -> Polygon: return discretize_ellipse(self.center, self.axis, self.angle, refs)
[docs] class Stellar(StarLike): """Stellar bubble.""" def __init__( self, center: Vector, radius: float, coefficient: list[tuple[float, float]] | None = None, ): """Creates a star-like geometry. Args: center: The center. radius: Approximately the average radius. coefficient: numpy array of shape (n, 2) with n > 0. Hint: for smooth stellar holes, you can çhoose k from {.1, .2} and then `coeffients = np.random.uniform(-k, k, (int(1/k), 2))` """ self.center = center self.radius = radius if coefficient is None: coefficient = self.generate_coefficients() self.coefficient = coefficient self.trig_fun = trigonometric_function(self.coefficient, self.radius)
[docs] def radius_at(self, angle: float) -> float: return self.trig_fun(angle)
[docs] def generate_coefficients(self, k: float = 0.2) -> list[tuple[float, float]]: """Generate coefficients for the stellar function. Args: k: Scaling factor. """ j = int(1 / k) coeffs_ = [random.uniform(-k, k) for _ in range(j * 2)] return list(zip(coeffs_[:j], coeffs_[j:]))
[docs] class RainDrop(StarLike): """Raindrop geometry.""" def __init__(self, center: Vector, a: float, scale: float): super().__init__(center) if not (0.5 <= a <= 1): raise ValueError(f"Value of a must be between 0.5 and 1, but got {a}.") self._a = a self._scale = scale
[docs] def radius_at(self, angle: float) -> float: raise NotImplementedError("Not implemented here")
[docs] def discretize(self, refs): angles = [i * 2 * math.pi / refs for i in range(refs)] denominators = [ (1 - self._a + self._a * math.cos(angle)) ** 2 + self._a**2 * math.sin(angle) ** 2 for angle in angles ] x_coords = [ self._a * math.sin(angle) - self._a * math.sin(angle) / den for den, angle in zip(denominators, angles) ] y_coords = [ self._a * math.cos(angle) + (1 - self._a + self._a * math.cos(angle)) / den for den, angle in zip(denominators, angles) ] mx, my = self.center return Polygon( [ (mx + self._scale * x, my + self._scale * y) for x, y in zip(x_coords, y_coords) ] )