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

"""Canonical CST airfoil-side helper."""

from __future__ import annotations

from functools import cached_property

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

from buffalo_wings.airfoil.internal.bezier import (
    BezierCurve1D,
    BezierCurve2D,
    bernstein_to_monomial_coefficients,
    monomial_to_bernstein_coefficients,
)

from .cst_geometry_side import CstGeometrySide

CANONICAL_CST_N1: FloatScalar = 0.5
CANONICAL_CST_N2: FloatScalar = 1.0


def _canonical_exact_curve(
    *,
    shape: BezierCurve1D,
    delta_te: FloatScalar,
) -> BezierCurve2D:
    """
    Build the exact Bezier curve for one canonical CST airfoil side.

    Parameters
    ----------
    shape : BezierCurve1D
        Canonical CST shape term in Bernstein form.
    delta_te : FloatScalar
        Side-local linear trailing-edge term.

    Returns
    -------
    BezierCurve2D
        Exact canonical airfoil-side Bezier curve in the parameter
        ``s in [0, 1]``.

    Notes
    -----
    This uses the exact canonical CST transformation from Marshall 2013.
    For a canonical side with shape-function degree ``n``, the resulting
    exact Bezier curve has degree ``2n + 3`` and satisfies
    ``x(s) = s**2``.
    """
    monomial_shape = bernstein_to_monomial_coefficients(shape.coefficients)
    shape_degree = shape.degree
    exact_degree = 2 * shape_degree + 3

    x_monomial = np.zeros(exact_degree + 1, dtype=np.float64)
    x_monomial[2] = 1.0

    y_monomial = np.zeros(exact_degree + 1, dtype=np.float64)
    y_monomial[1] = monomial_shape[0]
    y_monomial[2] = as_float_scalar(delta_te)

    for index in range(1, shape_degree + 1):
        y_monomial[2 * index + 1] = (
            monomial_shape[index] - monomial_shape[index - 1]
        )

    y_monomial[exact_degree] = -monomial_shape[-1]

    x_bernstein = monomial_to_bernstein_coefficients(as_float_array(x_monomial))
    y_bernstein = monomial_to_bernstein_coefficients(as_float_array(y_monomial))

    return BezierCurve2D.from_coordinate_curves(
        x_curve=BezierCurve1D(coefficients=x_bernstein),
        y_curve=BezierCurve1D(coefficients=y_bernstein),
    )


[docs] class CstAirfoilSide(CstGeometrySide): """ Canonical CST side helper with fixed airfoil class exponents. Notes ----- This helper represents the airfoil-specific CST case with ``n1 = 0.5`` and ``n2 = 1.0``. It also provides the exact canonical Bezier curve representation for the side in the auxiliary parameter ``s in [0, 1]``. """ def __init__( self, *, shape: BezierCurve1D, delta_te: FloatScalar = 0.0, ) -> None: """ Initialize a canonical CST side from one Bezier shape curve. Parameters ---------- shape : BezierCurve1D Bezier curve used for the canonical CST shape term. delta_te : FloatScalar, default=0.0 Side-local linear trailing-edge contribution. """ super().__init__( shape=shape, n1=as_float_scalar(CANONICAL_CST_N1), n2=as_float_scalar(CANONICAL_CST_N2), delta_te=as_float_scalar(delta_te), ) @cached_property def exact_curve(self) -> BezierCurve2D: """ Return the exact canonical Bezier curve for this CST side. Returns ------- BezierCurve2D Exact Bezier curve in the auxiliary canonical airfoil parameterization ``x = s**2``. """ return _canonical_exact_curve( shape=self.shape, delta_te=self.delta_te, )
[docs] def rebuild_with_shape( self, shape: BezierCurve1D, *, delta_te: FloatScalar | None = None, ) -> CstAirfoilSide: """ Return one canonical side rebuilt with a replacement shape curve. Parameters ---------- shape : BezierCurve1D Replacement canonical CST shape curve. delta_te : FloatScalar | None, default=None Replacement side-local trailing-edge term. When omitted, the current side-local value is reused. Returns ------- CstAirfoilSide Rebuilt canonical CST side with the same trailing-edge term. """ return CstAirfoilSide( shape=shape, delta_te=self.delta_te if delta_te is None else delta_te, )
[docs] def x_canonical(self, s: FloatInput) -> FloatArray: # noqa: PLR6301 """ Return the canonical side chord coordinate ``x = s**2``. Parameters ---------- s : FloatInput Canonical side parameter in ``[0, 1]``. Returns ------- FloatArray Chordwise coordinates for the canonical side parameter. """ s_array = as_float_array(s) return as_float_array(s_array**2)
[docs] def xy_canonical(self, s: FloatInput) -> tuple[FloatArray, FloatArray]: """ Return canonical side coordinates in the ``x = s**2`` parameter. Parameters ---------- s : FloatInput Canonical side parameter in ``[0, 1]``. Returns ------- tuple[FloatArray, FloatArray] Arrays ``(x(s), y(s))`` for the canonical side parameter. """ x_array = self.x_canonical(s) return x_array, self.y(x_array)
[docs] def xy_canonical_t(self, s: FloatInput) -> tuple[FloatArray, FloatArray]: """ Return first derivatives in the canonical side parameter. Parameters ---------- s : FloatInput Canonical side parameter in ``[0, 1]``. Returns ------- tuple[FloatArray, FloatArray] Arrays ``(dx/ds, dy/ds)``. Notes ----- These derivatives are evaluated directly in the canonical side parameter rather than through ``dy/dx * dx/ds`` so the leading-edge value remains finite. """ s_array = as_float_array(s) x_array = self.x_canonical(s_array) shape = self.shape_value(x_array) shape_x = self.shape_x(x_array) dx_ds = as_float_array(2.0 * s_array) class_s = as_float_array(s_array * (1.0 - x_array)) class_s_t = as_float_array(1.0 - 3.0 * x_array) dy_ds = as_float_array( class_s_t * shape + class_s * shape_x * dx_ds + self.delta_te * dx_ds ) return dx_ds, dy_ds
[docs] def xy_canonical_tt(self, s: FloatInput) -> tuple[FloatArray, FloatArray]: """ Return second derivatives in the canonical side parameter. Parameters ---------- s : FloatInput Canonical side parameter in ``[0, 1]``. Returns ------- tuple[FloatArray, FloatArray] Arrays ``(d2x/ds2, d2y/ds2)``. Notes ----- These derivatives are evaluated in closed form in the canonical side parameter so the leading-edge value remains finite. """ s_array = as_float_array(s) x_array = self.x_canonical(s_array) shape = self.shape_value(x_array) shape_x = self.shape_x(x_array) shape_xx = self.shape_xx(x_array) dx_ds = as_float_array(2.0 * s_array) d2x_ds2 = as_float_array(2.0 * np.ones_like(s_array)) class_s = as_float_array(s_array * (1.0 - x_array)) class_s_t = as_float_array(1.0 - 3.0 * x_array) class_s_tt = as_float_array(-6.0 * s_array) dy_dss = as_float_array( class_s_tt * shape + 2.0 * class_s_t * shape_x * dx_ds + class_s * shape_xx * dx_ds**2 + class_s * shape_x * d2x_ds2 + self.delta_te * d2x_ds2 ) return d2x_ds2, dy_dss