Source code for ttfemesh.domain.curve

import warnings
from abc import ABC, abstractmethod
from typing import Callable, Union

import numpy as np

from ttfemesh.utils.array import ensure_1d


[docs] class Curve(ABC): """ Abstract base class for a curve in the domain. Defines the interface for all curve implementations. """
[docs] def get_start(self) -> np.ndarray: """Get the start point of the curve.""" return self.evaluate(-1.0)[0]
[docs] def get_end(self) -> np.ndarray: """Get the end point of the curve.""" return self.evaluate(1.0)[0]
def __call__(self, *args, **kwargs) -> np.ndarray: return self.evaluate(*args, **kwargs) def _validate(self, t: Union[np.ndarray, float], tol: float = 1e-6): """ Ensure that parameter values are in the interval [-1, 1] within a specified tolerance. Args: t (np.ndarray): Array of parameter values to validate. tol (float): Tolerance for boundary checks. Raises: ValueError: If any parameter values are outside the interval [-1, 1] (with tolerance). """ t_ = ensure_1d(t) if not np.all(-1 - tol <= t_) or not np.all(t_ <= 1 + tol): warnings.warn( f"Parameter values are not in the interval [-1, 1]" f" within tolerance {tol}." f" May lead to unexpected behavior." )
[docs] @abstractmethod def evaluate(self, t: Union[np.ndarray, float]) -> np.ndarray: # pragma: no cover """ Evaluate the curve at parameter values t. Args: t (Union[np.ndarray, float]): Array of or scalar parameter values in [-1, 1]. Returns: np.ndarray: Array of shape (len(t), 2) with (x, y) coordinates. """ pass
[docs] @abstractmethod def tangent(self, t: Union[np.ndarray, float]) -> np.ndarray: # pragma: no cover """ Compute the tangent vector (not normalized) to the curve at parameter values t. Args: t (Union[np.ndarray, float]): Array of or scalar parameter values in [-1, 1]. Returns: np.ndarray: Array of shape (len(t), 2) with tangent vectors. """ pass
[docs] def equals(self, other: "Curve", num_points: int = 100, tol: float = 1e-6) -> bool: """ Check if two curves are approximately equal by sampling. Args: other (Curve): Another curve to compare. num_points (int): Number of points to sample along the curve. tol (float): Tolerance for point-wise comparison. Returns: bool: True if the curves are approximately equal, False otherwise. """ reverse = False start = self.get_start() start_other = other.get_start() if not np.allclose(start, start_other, atol=tol): start_other = other.get_end() if not np.allclose(start, start_other, atol=tol): return False reverse = True ts = np.linspace(-1, 1, num_points) for t in ts: t_other = t if not reverse else -t if not np.allclose(self.evaluate(t), other.evaluate(t_other), atol=tol): return False return True
[docs] class Line2D(Curve): """ A line segment in 2D space. Example: >>> from ttfemesh.domain import Line2D >>> from ttfemesh.utils import plot_curve_with_tangents >>> line = Line2D((0, 0), (1, 1)) >>> print(line) >>> plot_curve_with_tangents(line, "Line") """
[docs] def __init__(self, start: tuple[float, float], end: tuple[float, float]): """ Initialize a line segment from `start` to `end`. Args: start (tuple): Coordinates of the start point (x, y). end (tuple): Coordinates of the end point (x, y). """ self.start = np.array(start) self.end = np.array(end)
[docs] def evaluate(self, t: Union[np.ndarray, float]) -> np.ndarray: t_ = ensure_1d(t) self._validate(t_) return np.outer((1 - t_) * 0.5, self.start) + np.outer((1 + t_) * 0.5, self.end)
[docs] def tangent(self, t: Union[np.ndarray, float]) -> np.ndarray: t_ = ensure_1d(t) self._validate(t_) return np.tile(0.5 * (self.end - self.start), (len(t_), 1))
def __repr__(self): return f"Line2D(start={tuple(self.start)}, end={tuple(self.end)})"
[docs] class CircularArc2D(Curve): """ A circular arc in 2D space. Example: >>> import numpy as np >>> from ttfemesh.domain import CircularArc2D >>> from ttfemesh.utils import plot_curve_with_tangents >>> circular_arc = CircularArc2D((0, 0), 1, np.pi/2., 0.5*np.pi) >>> print(circular_arc) >>> plot_curve_with_tangents(circular_arc, "Circular Arc") """
[docs] def __init__( self, center: tuple[float, float], radius: float, start_angle: float = 0.0, angle_sweep: float = np.pi, ): """ Initialize a circular arc defined by a center, radius, and angle sweep. Args: center (tuple): Coordinates of the center (x, y). radius (float): Radius of the half-circle. start_angle (float): Starting angle in radians. Default is 0. angle_sweep (float): Angle sweep in radians. Default is π. """ self.center = np.array(center) self.radius = radius self.start_angle = start_angle self.angle_sweep = angle_sweep
[docs] def evaluate(self, t: Union[np.ndarray, float]) -> np.ndarray: t_ = ensure_1d(t) self._validate(t_) angle = self.start_angle + 0.5 * (t_ + 1) * self.angle_sweep x = self.center[0] + self.radius * np.cos(angle) y = self.center[1] + self.radius * np.sin(angle) return np.stack((x, y), axis=-1)
[docs] def tangent(self, t: Union[np.ndarray, float]) -> np.ndarray: t_ = ensure_1d(t) self._validate(t_) angle = self.start_angle + 0.5 * (t_ + 1) * self.angle_sweep mul_factor = 0.5 * self.angle_sweep * self.radius dx = -np.sin(angle) * mul_factor dy = np.cos(angle) * mul_factor tangent = np.stack((dx, dy), axis=-1) return tangent
def __repr__(self): return ( f"CircularArc2D(center={tuple(self.center)}, " f"radius={self.radius}, start_angle={self.start_angle}, " f"angle_sweep={self.angle_sweep})" )
[docs] class ParametricCurve2D(Curve): """ A parametric curve in 2D space. Example: >>> from ttfemesh.domain import ParametricCurve2D >>> from ttfemesh.utils import plot_curve_with_tangents >>> parametric_curve = ParametricCurve2D( ... lambda t: np.sin(t * np.pi), ... lambda t: np.cos(t * np.pi) ... ) >>> print(parametric_curve) >>> plot_curve_with_tangents(parametric_curve, "Parametric Curve") """
[docs] def __init__( self, x_func: Callable[[np.ndarray], np.ndarray], y_func: Callable[[np.ndarray], np.ndarray] ): """ Initialize a parametric curve defined by functions x(t) and y(t). Uses a finite difference approximation to compute the tangent. Args: x_func (Callable[[np.ndarray], np.ndarray]): Function x(t) where t is in [-1, 1]. y_func (Callable[[np.ndarray], np.ndarray]): Function y(t) where t is in [-1, 1]. """ self.x_func = x_func self.y_func = y_func
[docs] def evaluate(self, t: Union[np.ndarray, float]) -> np.ndarray: t_ = ensure_1d(t) self._validate(t_) x = self.x_func(t_) y = self.y_func(t_) return np.stack((x, y), axis=-1)
[docs] def tangent(self, t: Union[np.ndarray, float]) -> np.ndarray: """ Compute the tangent vector using a finite difference approximation. Args: t (Union[np.ndarray, float]): Array of or scalar parameter values in [-1, 1]. Returns: np.ndarray: Array of shape (len(t), 2) with tangent. """ t_ = ensure_1d(t) self._validate(t_) dt = 1e-5 dx = (self.x_func(t_ + dt) - self.x_func(t_)) / dt dy = (self.y_func(t_ + dt) - self.y_func(t_)) / dt tangent = np.stack((dx, dy), axis=-1) return tangent
def __repr__(self): return f"ParametricCurve2D(x_func={self.x_func}, y_func={self.y_func})"