"""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