Source code for buffalo_wings.airfoil.internal.analytic.cst.cst_geometry_side

"""One-side CST geometry helpers for airfoil runtimes."""

from __future__ import annotations

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

from buffalo_wings.airfoil.internal.bezier import (
    BezierCurve1D,
    BezierDemotionContinuity,
)


def _power_term(base: FloatArray, exponent: FloatScalar) -> FloatArray:
    """
    Evaluate one power-law class factor.

    Parameters
    ----------
    base : FloatArray
        Nonnegative abscissa factors such as ``x`` or ``1 - x``.
    exponent : FloatScalar
        Class-function exponent.

    Returns
    -------
    FloatArray
        ``base**exponent`` with the exponent-zero case normalized to ones.
    """
    if exponent == 0.0:  # noqa: RUF069
        return np.ones_like(base)
    return as_float_array(np.power(base, exponent))


def _power_term_x(base: FloatArray, exponent: FloatScalar) -> FloatArray:
    """
    Evaluate the first derivative of one power-law class factor.

    Parameters
    ----------
    base : FloatArray
        Nonnegative abscissa factors such as ``x`` or ``1 - x``.
    exponent : FloatScalar
        Class-function exponent.

    Returns
    -------
    FloatArray
        First derivative of ``base**exponent`` with respect to ``base``.
    """
    if exponent == 0.0:  # noqa: RUF069
        return np.zeros_like(base)
    with np.errstate(divide="ignore", invalid="ignore"):
        return as_float_array(exponent * np.power(base, exponent - 1.0))


def _power_term_xx(base: FloatArray, exponent: FloatScalar) -> FloatArray:
    """
    Evaluate the second derivative of one power-law class factor.

    Parameters
    ----------
    base : FloatArray
        Nonnegative abscissa factors such as ``x`` or ``1 - x``.
    exponent : FloatScalar
        Class-function exponent.

    Returns
    -------
    FloatArray
        Second derivative of ``base**exponent`` with respect to ``base``.
    """
    if exponent in {0.0, 1.0}:
        return np.zeros_like(base)
    with np.errstate(divide="ignore", invalid="ignore"):
        return as_float_array(
            exponent * (exponent - 1.0) * np.power(base, exponent - 2.0)
        )


[docs] class CstGeometrySide: """One CST side geometry expressed in class-shape form.""" def __init__( self, *, shape: BezierCurve1D, n1: FloatScalar = 0.5, n2: FloatScalar = 1.0, delta_te: FloatScalar = 0.0, ) -> None: """ Initialize one CST side geometry. Parameters ---------- shape : BezierCurve1D One-dimensional Bezier curve used by the CST side. n1 : FloatScalar, default=0.5 Leading-edge class exponent. n2 : FloatScalar, default=1.0 Trailing-edge class exponent. delta_te : FloatScalar, default=0.0 Linear trailing-edge term for this side. """ self._shape = shape self._n1 = n1 self._n2 = n2 self._delta_te = delta_te @property def shape(self) -> BezierCurve1D: """ Return the one-dimensional Bezier curve. This property exposes the one-dimensional Bezier curve used by the CST side. """ return self._shape @property def coefficients(self) -> FloatArray: """ Return the Bernstein coefficients of the shape curve. This property exposes the stored Bernstein coefficients for the one-dimensional Bezier curve. """ return self.shape.coefficients @property def n1(self) -> FloatScalar: """ Return the leading-edge class exponent. This property stores the leading-edge class exponent. """ return self._n1 @property def n2(self) -> FloatScalar: """ Return the trailing-edge class exponent. This property stores the trailing-edge class exponent. """ return self._n2 @property def delta_te(self) -> FloatScalar: """ Return the linear trailing-edge term. This property stores the linear trailing-edge term for this side. """ return self._delta_te
[docs] def rebuild_with_shape( self, shape: BezierCurve1D, *, delta_te: FloatScalar | None = None, ) -> CstGeometrySide: """ Return one side rebuilt with a replacement Bezier shape curve. Parameters ---------- shape : BezierCurve1D Replacement one-dimensional Bezier shape curve. delta_te : FloatScalar | None, default=None Replacement side-local trailing-edge term. When omitted, the current side-local value is reused. Returns ------- CstGeometrySide Rebuilt CST side with the same exponents and trailing-edge term. """ return CstGeometrySide( shape=shape, n1=self.n1, n2=self.n2, delta_te=self.delta_te if delta_te is None else delta_te, )
[docs] def promote_degree(self, *, count: int = 1) -> CstGeometrySide: """ Raise the Bezier shape degree without changing the side geometry. Parameters ---------- count : int, default=1 Number of Bezier degree-elevation steps to apply to the stored shape curve. Returns ------- CstGeometrySide Rebuilt CST side with an exact elevated Bezier shape curve. """ return self.rebuild_with_shape(self.shape.promote_degree(count=count))
[docs] def demote_degree( self, *, count: int = 1, continuity: BezierDemotionContinuity = "NOT_CONNECTED", ) -> CstGeometrySide: """ Lower the Bezier shape degree with constrained demotion. Parameters ---------- count : int, default=1 Number of Bezier degree-reduction steps to apply to the stored shape curve. continuity : {"NOT_CONNECTED", "C0", "C1", "C2"}, default="NOT_CONNECTED" Symmetric endpoint continuity preserved during each Bezier demotion step. Returns ------- CstGeometrySide Rebuilt CST side with a reduced-degree Bezier shape curve. Notes ----- This operation is intentionally approximate unless the original shape curve is exactly reducible to the requested lower degree. """ return self.rebuild_with_shape( self.shape.demote_degree( count=count, continuity=continuity, ) )
[docs] def class_value(self, x: FloatInput) -> FloatArray: """ Evaluate the CST class function. Parameters ---------- x : FloatInput Chordwise coordinates, typically in ``[0, 1]``. Returns ------- FloatArray Class-function values at ``x``. """ x_array = as_float_array(x) leading = _power_term(x_array, self.n1) trailing = _power_term(1.0 - x_array, self.n2) return as_float_array(leading * trailing)
[docs] def class_x(self, x: FloatInput) -> FloatArray: """ Evaluate the first derivative of the CST class function. Parameters ---------- x : FloatInput Chordwise coordinates, typically in ``[0, 1]``. Returns ------- FloatArray First derivative values of the class function at ``x``. """ x_array = as_float_array(x) leading = _power_term(x_array, self.n1) trailing = _power_term(1.0 - x_array, self.n2) leading_x = _power_term_x(x_array, self.n1) trailing_x = -_power_term_x(1.0 - x_array, self.n2) return as_float_array(leading_x * trailing + leading * trailing_x)
[docs] def class_xx(self, x: FloatInput) -> FloatArray: """ Evaluate the second derivative of the CST class function. Parameters ---------- x : FloatInput Chordwise coordinates, typically in ``[0, 1]``. Returns ------- FloatArray Second derivative values of the class function at ``x``. """ x_array = as_float_array(x) return as_float_array( _power_term_xx(x_array, self.n1) * _power_term(1.0 - x_array, self.n2) + 2.0 * _power_term_x(x_array, self.n1) * (-_power_term_x(1.0 - x_array, self.n2)) + _power_term(x_array, self.n1) * _power_term_xx(1.0 - x_array, self.n2) )
[docs] def shape_value(self, x: FloatInput) -> FloatArray: """ Evaluate the Bezier shape curve. Parameters ---------- x : FloatInput Chordwise coordinates, typically in ``[0, 1]``. Returns ------- FloatArray Shape-function values at ``x``. """ return self.shape.evaluate(x)
[docs] def shape_x(self, x: FloatInput) -> FloatArray: """ Evaluate the first derivative of the Bezier shape curve. Parameters ---------- x : FloatInput Chordwise coordinates, typically in ``[0, 1]``. Returns ------- FloatArray First derivative values of the shape curve at ``x``. """ return self.shape.evaluate_u(x)
[docs] def shape_xx(self, x: FloatInput) -> FloatArray: """ Evaluate the second derivative of the Bezier shape curve. Parameters ---------- x : FloatInput Chordwise coordinates, typically in ``[0, 1]``. Returns ------- FloatArray Second derivative values of the shape curve at ``x``. """ return self.shape.evaluate_uu(x)
[docs] def y(self, x: FloatInput) -> FloatArray: """ Evaluate the CST side ordinate. Parameters ---------- x : FloatInput Chordwise coordinates, typically in ``[0, 1]``. Returns ------- FloatArray Surface ordinate values for this side at ``x``. """ x_array = as_float_array(x) return as_float_array( self.class_value(x_array) * self.shape_value(x_array) + self.delta_te * x_array )
[docs] def y_x(self, x: FloatInput) -> FloatArray: """ Evaluate the first derivative of the CST side ordinate. Parameters ---------- x : FloatInput Chordwise coordinates, typically in ``[0, 1]``. Returns ------- FloatArray First derivative values ``dy/dx`` for this side at ``x``. """ x_array = as_float_array(x) class_values = self.class_value(x_array) shape_values = self.shape_value(x_array) return as_float_array( self.class_x(x_array) * shape_values + class_values * self.shape_x(x_array) + self.delta_te )
[docs] def y_xx(self, x: FloatInput) -> FloatArray: """ Evaluate the second derivative of the CST side ordinate. Parameters ---------- x : FloatInput Chordwise coordinates, typically in ``[0, 1]``. Returns ------- FloatArray Second derivative values ``d2y/dx2`` for this side at ``x``. """ x_array = as_float_array(x) class_values = self.class_value(x_array) shape_values = self.shape_value(x_array) class_x = self.class_x(x_array) shape_x = self.shape_x(x_array) return as_float_array( self.class_xx(x_array) * shape_values + 2.0 * class_x * shape_x + class_values * self.shape_xx(x_array) )