Source code for buffalo_wings.airfoil.internal.polyline_curve

"""Piecewise-linear open-curve runtime for authored section geometry."""

from __future__ import annotations

from typing import override

import numpy as np
from buffalo_core.numeric import as_float_array, as_float_scalar
from buffalo_core.typing import FloatArray, FloatInput, FloatScalar

from .curve import Curve
from .schema.common import Point2DSpec

_MIN_POLYLINE_POINTS = 2


[docs] class PolylineCurve(Curve): """ Piecewise-linear open-curve runtime for authored section geometry. Notes ----- The native parameter ``u`` is the normalized cumulative arc-length fraction along the polyline, so every segment occupies a parameter span proportional to its physical length. """ def __init__( self, *, points: list[Point2DSpec], ) -> None: """Store one polyline curve after validating its segment geometry.""" if len(points) < _MIN_POLYLINE_POINTS: msg = "PolylineCurve requires at least two points." raise ValueError(msg) coordinates = np.asarray(points, dtype=np.float64) deltas = np.diff(coordinates, axis=0) segment_lengths = np.hypot(deltas[:, 0], deltas[:, 1]) if np.any(segment_lengths <= 0.0): msg = "PolylineCurve requires strictly positive segment lengths." raise ValueError(msg) total_length = float(np.sum(segment_lengths)) cumulative = np.concatenate(( np.array([0.0], dtype=np.float64), np.cumsum(segment_lengths, dtype=np.float64) / total_length, )) self._coordinates = coordinates self._deltas = deltas self._segment_lengths = segment_lengths self._breakpoint_values = cumulative self._total_length = total_length @property def points(self) -> FloatArray: """Return the stored polyline vertices as an ``(n, 2)`` array.""" return as_float_array(self._coordinates)
[docs] @override def xy_from_u(self, u: FloatInput) -> tuple[FloatArray, FloatArray]: """Evaluate polyline coordinates by arc-fraction interpolation.""" u_array = as_float_array(u) x = np.interp(u_array, self._breakpoint_values, self._coordinates[:, 0]) y = np.interp(u_array, self._breakpoint_values, self._coordinates[:, 1]) return as_float_array(x), as_float_array(y)
[docs] @override def xy_u(self, u: FloatInput) -> tuple[FloatArray, FloatArray]: """Return the piecewise-constant derivative on each segment.""" u_array = as_float_array(u) segment_index = ( np.searchsorted( self._breakpoint_values, u_array, side="left", ) - 1 ) segment_index = np.clip(segment_index, 0, self._deltas.shape[0] - 1) du = np.diff(self._breakpoint_values)[segment_index] dx = self._deltas[segment_index, 0] / du dy = self._deltas[segment_index, 1] / du return as_float_array(dx), as_float_array(dy)
[docs] @override def xy_uu(self, u: FloatInput) -> tuple[FloatArray, FloatArray]: """Return zero second derivatives away from breakpoints.""" u_array = as_float_array(u) return np.zeros_like(u_array), np.zeros_like(u_array)
[docs] @override def breakpoints(self) -> list[FloatScalar]: """Return normalized arc-fraction breakpoints along the polyline.""" return [as_float_scalar(value) for value in self._breakpoint_values]