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