use crate::math::Fix128;
pub const SMAGORINSKY_CS: Fix128 = Fix128 {
hi: 0,
lo: 0x2B85_1EB8_51EB_851F, };
#[cfg(feature = "std")]
pub(crate) const KE_C_MU: Fix128 = Fix128 {
hi: 0,
lo: 0x170A3D70A3D70A3D, };
#[cfg(feature = "std")]
pub(crate) const KE_SIGMA_K: Fix128 = Fix128::ONE;
#[cfg(feature = "std")]
pub(crate) const KE_SIGMA_EPS: Fix128 = Fix128 {
hi: 1,
lo: 0x4CCC_CCCC_CCCC_CCCD,
};
#[cfg(feature = "std")]
pub(crate) const KE_C1_EPS: Fix128 = Fix128 {
hi: 1,
lo: 0x70A3_D70A_3D70_A3D7,
};
#[cfg(feature = "std")]
pub(crate) const KE_C2_EPS: Fix128 = Fix128 {
hi: 1,
lo: 0xEB85_1EB8_51EB_851F,
};
#[cfg(feature = "std")]
pub(crate) const KW_BETA_STAR: Fix128 = KE_C_MU;
#[cfg(feature = "std")]
pub(crate) const KW_BETA: Fix128 = Fix128 {
hi: 0,
lo: 0x1333_3333_3333_3333,
};
#[cfg(feature = "std")]
pub(crate) const KW_ALPHA: Fix128 = Fix128 {
hi: 0,
lo: 0x8E38_E38E_38E3_8E38,
};
#[cfg(feature = "std")]
pub(crate) const KW_SIGMA: Fix128 = Fix128::from_int(2);
#[must_use]
pub fn smagorinsky_eddy_viscosity(filter_width_m: Fix128, strain_rate_magnitude: Fix128) -> Fix128 {
smagorinsky_eddy_viscosity_with(SMAGORINSKY_CS, filter_width_m, strain_rate_magnitude)
}
#[must_use]
pub(crate) fn smagorinsky_eddy_viscosity_with(
cs: Fix128,
filter_width_m: Fix128,
strain_rate_magnitude: Fix128,
) -> Fix128 {
let cs_delta = cs * filter_width_m;
cs_delta * cs_delta * strain_rate_magnitude
}
#[must_use]
pub fn strain_rate_magnitude(
s11: Fix128,
s22: Fix128,
s33: Fix128,
s12: Fix128,
s13: Fix128,
s23: Fix128,
) -> Fix128 {
let diag = s11 * s11 + s22 * s22 + s33 * s33;
let off = Fix128::from_int(2) * (s12 * s12 + s13 * s13 + s23 * s23);
(Fix128::from_int(2) * (diag + off)).sqrt()
}
#[cfg(feature = "std")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub(crate) struct KEpsilonState {
pub(crate) k: Fix128,
pub(crate) epsilon: Fix128,
}
#[cfg(feature = "std")]
impl KEpsilonState {
#[must_use]
pub(crate) fn eddy_viscosity(&self) -> Fix128 {
if self.epsilon.is_zero() {
return Fix128::ZERO;
}
KE_C_MU * self.k * self.k / self.epsilon
}
pub(crate) fn advance_k(&mut self, production_k: Fix128, dt_s: Fix128) {
self.k = self.k + (production_k - self.epsilon) * dt_s;
if self.k < Fix128::ZERO {
self.k = Fix128::ZERO;
}
}
pub(crate) fn advance(&mut self, production_k: Fix128, dt_s: Fix128) {
let start = *self;
self.advance_k(production_k, dt_s);
let mut eps = start;
eps.advance_epsilon(production_k, dt_s);
self.epsilon = eps.epsilon;
}
pub(crate) fn advance_epsilon(&mut self, production_k: Fix128, dt_s: Fix128) {
if self.k.is_zero() {
return;
}
let factor = self.epsilon / self.k;
let d_eps = factor * (KE_C1_EPS * production_k - KE_C2_EPS * self.epsilon);
self.epsilon = self.epsilon + d_eps * dt_s;
if self.epsilon < Fix128::ZERO {
self.epsilon = Fix128::ZERO;
}
}
}
#[cfg(feature = "std")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub(crate) struct KOmegaState {
pub(crate) k: Fix128,
pub(crate) omega: Fix128,
}
#[cfg(feature = "std")]
impl KOmegaState {
#[must_use]
pub(crate) fn eddy_viscosity(&self) -> Fix128 {
if self.omega.is_zero() {
return Fix128::ZERO;
}
self.k / self.omega
}
#[must_use]
pub(crate) fn from_k_epsilon(state: &KEpsilonState) -> Self {
let omega = if state.k.is_zero() {
Fix128::ZERO
} else {
state.epsilon / (KW_BETA_STAR * state.k)
};
Self { k: state.k, omega }
}
#[must_use]
pub(crate) fn to_k_epsilon(self) -> KEpsilonState {
KEpsilonState {
k: self.k,
epsilon: KW_BETA_STAR * self.k * self.omega,
}
}
pub(crate) fn advance(&mut self, production_k: Fix128, dt_s: Fix128) {
let (k, w) = (self.k, self.omega);
let dk = production_k - KW_BETA_STAR * k * w;
let dw = if k.is_zero() {
Fix128::ZERO - KW_BETA * w * w
} else {
KW_ALPHA * (w / k) * production_k - KW_BETA * w * w
};
self.k = k + dk * dt_s;
if self.k < Fix128::ZERO {
self.k = Fix128::ZERO;
}
self.omega = w + dw * dt_s;
if self.omega < Fix128::ZERO {
self.omega = Fix128::ZERO;
}
}
}
#[cfg(feature = "std")]
#[must_use]
pub(crate) fn dynamic_smagorinsky_cs(strain_grid: Fix128, strain_test: Fix128) -> Fix128 {
let min_cs = Fix128::from_ratio(5, 100);
let max_cs = Fix128::from_ratio(25, 100);
if strain_grid.is_zero() || strain_test.is_zero() {
return SMAGORINSKY_CS;
}
let ratio = strain_test / strain_grid;
let unclamped = SMAGORINSKY_CS * ratio;
if unclamped < min_cs {
min_cs
} else if unclamped > max_cs {
max_cs
} else {
unclamped
}
}
pub(crate) const VON_KARMAN: Fix128 = Fix128 {
hi: 0,
lo: 0x68F5_C28F_5C28_F5C3,
};
pub(crate) const LOG_LAW_B: Fix128 = Fix128 {
hi: 5,
lo: 0x8000_0000_0000_0000,
};
pub(crate) const Y_PLUS_TRANSITION: Fix128 = Fix128 {
hi: 11,
lo: 0x72006EE534E20000, };
pub const FRICTION_VELOCITY_BISECTIONS: u32 = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WallFunctionError {
NonPositiveWallDistance,
NonPositiveViscosity,
NegativeSpeed,
}
impl core::fmt::Display for WallFunctionError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NonPositiveWallDistance => write!(f, "the wall distance must be positive"),
Self::NonPositiveViscosity => write!(f, "the kinematic viscosity must be positive"),
Self::NegativeSpeed => write!(f, "the speed relative to the wall must not be negative"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for WallFunctionError {}
pub fn friction_velocity(
u_rel: Fix128,
y_p: Fix128,
nu: Fix128,
) -> Result<Fix128, WallFunctionError> {
if y_p <= Fix128::ZERO {
return Err(WallFunctionError::NonPositiveWallDistance);
}
if nu <= Fix128::ZERO {
return Err(WallFunctionError::NonPositiveViscosity);
}
if u_rel.is_negative() {
return Err(WallFunctionError::NegativeSpeed);
}
Ok(friction_velocity_checked(u_rel, y_p, nu))
}
pub(crate) fn friction_velocity_checked(u_rel: Fix128, y_p: Fix128, nu: Fix128) -> Fix128 {
if u_rel.is_zero() {
return Fix128::ZERO;
}
let sublayer_root = (nu * u_rel / y_p).sqrt();
let mut lo = Fix128::ZERO;
let mut hi = if sublayer_root > u_rel {
sublayer_root
} else {
u_rel
};
if hi * u_plus(y_p * hi / nu) < u_rel {
hi = hi.double();
}
for _ in 0..FRICTION_VELOCITY_BISECTIONS {
let mid = (lo + hi).half();
if mid * u_plus(y_p * mid / nu) < u_rel {
lo = mid;
} else {
hi = mid;
}
}
(lo + hi).half()
}
#[cfg(feature = "std")]
#[must_use]
pub(crate) fn y_plus(
density: Fix128,
u_tau: Fix128,
y_m: Fix128,
dynamic_viscosity_pas: Fix128,
) -> Fix128 {
if dynamic_viscosity_pas.is_zero() {
return Fix128::ZERO;
}
density * u_tau * y_m / dynamic_viscosity_pas
}
#[must_use]
pub(crate) fn u_plus(y_plus_val: Fix128) -> Fix128 {
if y_plus_val <= Fix128::ZERO {
return Fix128::ZERO;
}
if y_plus_val < Y_PLUS_TRANSITION {
return y_plus_val;
}
let ln_yp = y_plus_val.ln();
ln_yp / VON_KARMAN + LOG_LAW_B
}
#[cfg(feature = "std")]
#[must_use]
pub(crate) fn wall_k_epsilon(u_tau_m_per_s: Fix128, y_m: Fix128) -> (Fix128, Fix128) {
if y_m.is_zero() || u_tau_m_per_s.is_zero() {
return (Fix128::ZERO, Fix128::ZERO);
}
let sqrt_cmu = KE_C_MU.sqrt();
let k = if sqrt_cmu.is_zero() {
Fix128::ZERO
} else {
u_tau_m_per_s * u_tau_m_per_s / sqrt_cmu
};
let u_tau_cube = u_tau_m_per_s * u_tau_m_per_s * u_tau_m_per_s;
let eps = u_tau_cube / (VON_KARMAN * y_m);
(k, eps)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: Fix128, b: Fix128, tol: Fix128) -> bool {
let d = if a > b { a - b } else { b - a };
d <= tol
}
#[test]
fn smagorinsky_cs_constant() {
assert!(approx_eq(
SMAGORINSKY_CS,
Fix128::from_ratio(17, 100),
Fix128::from_ratio(1, 1000)
));
}
#[test]
fn ke_c_mu_constant() {
assert!(approx_eq(
KE_C_MU,
Fix128::from_ratio(9, 100),
Fix128::from_raw(0, 16)
));
}
#[test]
fn smagorinsky_zero_strain_zero_viscosity() {
let nu = smagorinsky_eddy_viscosity(Fix128::from_ratio(1, 100), Fix128::ZERO);
assert_eq!(nu, Fix128::ZERO);
}
#[test]
fn smagorinsky_scales_with_delta_squared() {
let s = Fix128::from_int(10);
let nu1 = smagorinsky_eddy_viscosity(Fix128::from_ratio(1, 100), s);
let nu2 = smagorinsky_eddy_viscosity(Fix128::from_ratio(2, 100), s);
let ratio = nu2 / nu1;
assert!(approx_eq(
ratio,
Fix128::from_int(4),
Fix128::from_ratio(1, 100)
));
}
#[test]
fn strain_rate_isotropic_diagonal() {
let s = strain_rate_magnitude(
Fix128::ONE,
Fix128::ZERO,
Fix128::ZERO,
Fix128::ZERO,
Fix128::ZERO,
Fix128::ZERO,
);
let expected = Fix128::from_int(2).sqrt();
assert!(approx_eq(s, expected, Fix128::from_ratio(1, 100)));
}
#[test]
fn strain_rate_zero_tensor() {
let s = strain_rate_magnitude(
Fix128::ZERO,
Fix128::ZERO,
Fix128::ZERO,
Fix128::ZERO,
Fix128::ZERO,
Fix128::ZERO,
);
assert_eq!(s, Fix128::ZERO);
}
#[test]
fn ke_eddy_viscosity_formula() {
let st = KEpsilonState {
k: Fix128::ONE,
epsilon: Fix128::from_ratio(9, 100),
};
let nu = st.eddy_viscosity();
assert!(approx_eq(nu, Fix128::ONE, Fix128::from_ratio(1, 100)));
}
#[test]
fn ke_zero_epsilon_zero_viscosity() {
let st = KEpsilonState {
k: Fix128::ONE,
epsilon: Fix128::ZERO,
};
assert_eq!(st.eddy_viscosity(), Fix128::ZERO);
}
#[test]
fn ke_k_transport_reduces_k_no_production() {
let mut st = KEpsilonState {
k: Fix128::from_int(10),
epsilon: Fix128::from_int(1),
};
st.advance_k(Fix128::ZERO, Fix128::from_ratio(1, 10));
assert!(st.k < Fix128::from_int(10));
assert!(st.k > Fix128::from_int(9));
}
#[test]
fn ke_k_never_goes_negative() {
let mut st = KEpsilonState {
k: Fix128::ONE,
epsilon: Fix128::from_int(100),
};
st.advance_k(Fix128::ZERO, Fix128::ONE);
assert!(st.k >= Fix128::ZERO);
}
#[test]
fn ke_epsilon_transport_zero_k_stays() {
let mut st = KEpsilonState {
k: Fix128::ZERO,
epsilon: Fix128::from_int(1),
};
let eps_before = st.epsilon;
st.advance_epsilon(Fix128::ONE, Fix128::ONE);
assert_eq!(st.epsilon, eps_before);
}
#[test]
fn kw_from_ke_conversion() {
let ke = KEpsilonState {
k: Fix128::ONE,
epsilon: Fix128::from_ratio(9, 100),
};
let kw = KOmegaState::from_k_epsilon(&ke);
assert_eq!(kw.k, ke.k);
assert!(approx_eq(kw.omega, Fix128::ONE, Fix128::from_ratio(1, 100)));
}
#[test]
fn kw_eddy_viscosity_reduces_when_omega_high() {
let low = KOmegaState {
k: Fix128::ONE,
omega: Fix128::ONE,
};
let high = KOmegaState {
k: Fix128::ONE,
omega: Fix128::from_int(10),
};
assert!(low.eddy_viscosity() > high.eddy_viscosity());
}
#[test]
fn kw_zero_omega_zero_viscosity() {
let st = KOmegaState {
k: Fix128::ONE,
omega: Fix128::ZERO,
};
assert_eq!(st.eddy_viscosity(), Fix128::ZERO);
}
#[test]
fn von_karman_and_b_constants() {
assert!(approx_eq(
VON_KARMAN,
Fix128::from_ratio(41, 100),
Fix128::from_ratio(1, 100)
));
assert!(approx_eq(
LOG_LAW_B,
Fix128::from_ratio(55, 10),
Fix128::from_ratio(1, 100)
));
}
#[test]
fn y_plus_zero_viscosity_returns_zero() {
let v = y_plus(
Fix128::from_int(1000),
Fix128::ONE,
Fix128::from_ratio(1, 100),
Fix128::ZERO,
);
assert_eq!(v, Fix128::ZERO);
}
#[test]
fn u_plus_sublayer_linear() {
let up = u_plus(Fix128::from_int(5));
assert_eq!(up, Fix128::from_int(5));
}
#[test]
fn u_plus_log_region_increases_slowly() {
let up = u_plus(Fix128::from_int(100));
assert!(approx_eq(
up,
Fix128::from_ratio(167, 10),
Fix128::from_int(1)
));
}
#[test]
fn u_plus_zero_returns_zero() {
assert_eq!(u_plus(Fix128::ZERO), Fix128::ZERO);
}
#[test]
fn wall_k_epsilon_positive() {
let (k, eps) = wall_k_epsilon(Fix128::from_ratio(5, 10), Fix128::from_ratio(1, 100));
assert!(k > Fix128::ZERO);
assert!(eps > Fix128::ZERO);
}
#[test]
fn dynamic_cs_default_when_strain_zero() {
let c = dynamic_smagorinsky_cs(Fix128::ZERO, Fix128::from_int(1));
assert_eq!(c, SMAGORINSKY_CS);
}
#[test]
fn dynamic_cs_clamped_to_range() {
let c = dynamic_smagorinsky_cs(Fix128::ONE, Fix128::from_int(100));
assert!(c <= Fix128::from_ratio(25, 100));
let c2 = dynamic_smagorinsky_cs(Fix128::from_int(100), Fix128::ONE);
assert!(c2 >= Fix128::from_ratio(5, 100));
}
#[test]
fn wall_k_epsilon_zero_y_returns_zero() {
let (k, eps) = wall_k_epsilon(Fix128::ONE, Fix128::ZERO);
assert_eq!(k, Fix128::ZERO);
assert_eq!(eps, Fix128::ZERO);
}
}