use crate::fields::boundary::bc::{BoundaryCondition, PatchField};
use crate::fields::field::Field;
use crate::fields::vol_field::{VolScalarField, VolVectorField};
use crate::primitives::vector::Vector3;
pub const KNP_MIN_WAVE_SPEED_SPAN: f64 = 1e-10;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KnpFaceState {
pub rho: f64,
pub u: Vector3,
pub p: f64,
pub c: f64,
pub w_e: f64,
pub f_e: f64,
}
impl KnpFaceState {
pub fn total_energy(rho: f64, u: Vector3, p: f64, c: f64, e: f64) -> Self {
let e_tot = e + 0.5 * (u.x * u.x + u.y * u.y + u.z * u.z);
Self {
rho,
u,
p,
c,
w_e: rho * e_tot,
f_e: e_tot + p / rho,
}
}
pub fn static_enthalpy(rho: f64, u: Vector3, p: f64, c: f64, h: f64) -> Self {
Self {
rho,
u,
p,
c,
w_e: rho * h,
f_e: h,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KnpFaceFlux {
pub mass: f64,
pub momentum: Vector3,
pub energy: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KnpWaveSpeeds {
pub a_minus: f64,
pub a_plus: f64,
}
#[inline]
pub fn knp_wave_speeds(l: &KnpFaceState, r: &KnpFaceState, n_f: Vector3) -> KnpWaveSpeeds {
let u_n_l = l.u.dot(n_f);
let u_n_r = r.u.dot(n_f);
KnpWaveSpeeds {
a_minus: (u_n_l - l.c).min(u_n_r - r.c).min(0.0),
a_plus: (u_n_l + l.c).max(u_n_r + r.c).max(0.0),
}
}
#[inline]
pub fn physical_face_flux(s: &KnpFaceState, n_f: Vector3) -> KnpFaceFlux {
physical_flux_with_normal_velocity(s, s.u.dot(n_f), n_f)
}
#[inline]
fn physical_flux_with_normal_velocity(s: &KnpFaceState, u_n: f64, n_f: Vector3) -> KnpFaceFlux {
KnpFaceFlux {
mass: s.rho * u_n,
momentum: (s.rho * u_n) * s.u + s.p * n_f,
energy: s.rho * u_n * s.f_e,
}
}
#[inline]
fn face_flux_impl(
l: &KnpFaceState,
r: &KnpFaceState,
n_f: Vector3,
with_dissipation: bool,
) -> KnpFaceFlux {
let u_n_l = l.u.dot(n_f);
let u_n_r = r.u.dot(n_f);
let a_l = (u_n_l - l.c).min(u_n_r - r.c).min(0.0);
let a_r = (u_n_l + l.c).max(u_n_r + r.c).max(0.0);
let da = (a_r - a_l).max(KNP_MIN_WAVE_SPEED_SPAN);
let f_l = physical_flux_with_normal_velocity(l, u_n_l, n_f);
let f_r = physical_flux_with_normal_velocity(r, u_n_r, n_f);
let (j_rho, j_rhou, j_e) = if with_dissipation {
(
a_l * a_r * (r.rho - l.rho),
a_l * a_r * (r.rho * r.u - l.rho * l.u),
a_l * a_r * (r.w_e - l.w_e),
)
} else {
(0.0, Vector3::ZERO, 0.0)
};
KnpFaceFlux {
mass: (a_r * f_l.mass - a_l * f_r.mass + j_rho) / da,
momentum: (a_r * f_l.momentum - a_l * f_r.momentum + j_rhou) * (1.0 / da),
energy: (a_r * f_l.energy - a_l * f_r.energy + j_e) / da,
}
}
pub fn knp_face_flux(l: &KnpFaceState, r: &KnpFaceState, n_f: Vector3) -> KnpFaceFlux {
face_flux_impl(l, r, n_f, true)
}
pub fn central_face_flux(l: &KnpFaceState, r: &KnpFaceState, n_f: Vector3) -> KnpFaceFlux {
face_flux_impl(l, r, n_f, false)
}
pub fn local_wave_speed_max(l: &KnpFaceState, r: &KnpFaceState, n_f: Vector3) -> f64 {
let w = knp_wave_speeds(l, r, n_f);
w.a_minus.abs().max(w.a_plus.abs())
}
pub fn central_flux_speed_max(l: &KnpFaceState, r: &KnpFaceState, n_f: Vector3) -> f64 {
let w = knp_wave_speeds(l, r, n_f);
let a_pos = w.a_plus / (w.a_plus - w.a_minus).max(KNP_MIN_WAVE_SPEED_SPAN);
let a_sf = w.a_minus * a_pos;
let aphiv_pos = a_pos * l.u.dot(n_f) - a_sf;
let aphiv_neg = (1.0 - a_pos) * r.u.dot(n_f) + a_sf;
aphiv_pos.abs().max(aphiv_neg.abs())
}
#[inline]
pub fn mach_blend(ma: f64, lo: f64, hi: f64) -> f64 {
if hi > lo {
((ma - lo) / (hi - lo)).clamp(0.0, 1.0)
} else if ma >= hi {
1.0
} else {
0.0
}
}
pub fn velocity_component(u: &VolVectorField, comp: usize) -> VolScalarField {
let pick = |v: Vector3| match comp {
0 => v.x,
1 => v.y,
_ => v.z,
};
let internal: Vec<f64> = u.internal.as_slice().iter().map(|v| pick(*v)).collect();
let boundary: Vec<PatchField<f64>> = u
.boundary
.iter()
.map(|pf| {
let bc = match &pf.bc {
BoundaryCondition::FixedValue(v) => BoundaryCondition::FixedValue(pick(*v)),
BoundaryCondition::FixedField(ff) => BoundaryCondition::FixedField(Field::new(
ff.as_slice().iter().map(|v| pick(*v)).collect(),
)),
BoundaryCondition::Calculated(ff) => BoundaryCondition::Calculated(Field::new(
ff.as_slice().iter().map(|v| pick(*v)).collect(),
)),
BoundaryCondition::ZeroGradient => BoundaryCondition::ZeroGradient,
BoundaryCondition::FixedGradient(g) => BoundaryCondition::FixedGradient(pick(*g)),
BoundaryCondition::Mixed {
value_fraction,
ref_value,
ref_grad,
} => BoundaryCondition::Mixed {
value_fraction: *value_fraction,
ref_value: pick(*ref_value),
ref_grad: pick(*ref_grad),
},
BoundaryCondition::MixedField {
value_fraction,
ref_value,
ref_grad,
} => BoundaryCondition::MixedField {
value_fraction: value_fraction.clone(),
ref_value: Field::new(ref_value.as_slice().iter().map(|v| pick(*v)).collect()),
ref_grad: Field::new(ref_grad.as_slice().iter().map(|v| pick(*v)).collect()),
},
BoundaryCondition::InletOutlet { inlet_value } => BoundaryCondition::InletOutlet {
inlet_value: pick(*inlet_value),
},
BoundaryCondition::OutletInlet { outlet_value } => BoundaryCondition::OutletInlet {
outlet_value: pick(*outlet_value),
},
BoundaryCondition::Freestream { freestream_value } => {
BoundaryCondition::Freestream {
freestream_value: pick(*freestream_value),
}
}
BoundaryCondition::FixedFluxPressure { gradient } => {
BoundaryCondition::FixedFluxPressure {
gradient: pick(*gradient),
}
}
BoundaryCondition::TotalPressure { p0 } => {
BoundaryCondition::TotalPressure { p0: pick(*p0) }
}
BoundaryCondition::FlowRateInletVelocity {
volumetric_flow_rate,
} => BoundaryCondition::FlowRateInletVelocity {
volumetric_flow_rate: *volumetric_flow_rate,
},
BoundaryCondition::Symmetry => BoundaryCondition::Symmetry,
BoundaryCondition::Slip => BoundaryCondition::Slip,
BoundaryCondition::NoSlip => BoundaryCondition::NoSlip,
BoundaryCondition::Wedge => BoundaryCondition::Wedge,
BoundaryCondition::PressureInletOutletVelocity => {
BoundaryCondition::PressureInletOutletVelocity
}
BoundaryCondition::Empty => BoundaryCondition::Empty,
};
let values = Field::new(pf.values.as_slice().iter().map(|v| pick(*v)).collect());
PatchField { bc, values }
})
.collect();
VolScalarField::new(
format!("{}_{comp}", u.name),
u.mesh.clone(),
Field::new(internal),
boundary,
)
}
#[cfg(test)]
mod tests {
use super::*;
const X: Vector3 = Vector3 {
x: 1.0,
y: 0.0,
z: 0.0,
};
fn liquid() -> KnpFaceState {
KnpFaceState::static_enthalpy(950.0, Vector3::new(3.0, 0.0, 0.0), 7.0e6, 1200.0, 5.0e5)
}
#[test]
fn identical_states_give_zero_dissipation() {
let s = liquid();
let knp = knp_face_flux(&s, &s, X);
let cen = central_face_flux(&s, &s, X);
assert!(
(knp.mass - cen.mass).abs() < 1e-9,
"mass dissipation nonzero"
);
assert!(
(knp.momentum.x - cen.momentum.x).abs() < 1e-6,
"momentum dissipation nonzero"
);
assert!(
(knp.energy - cen.energy).abs() < 1e-3,
"energy dissipation nonzero"
);
}
#[test]
fn supersonic_one_sided_is_upwind() {
let l =
KnpFaceState::static_enthalpy(20.0, Vector3::new(500.0, 0.0, 0.0), 1.0e6, 300.0, 2.6e6);
let r =
KnpFaceState::static_enthalpy(18.0, Vector3::new(520.0, 0.0, 0.0), 0.9e6, 300.0, 2.5e6);
let knp = knp_face_flux(&l, &r, X);
let up = physical_face_flux(&l, X);
assert!((knp.mass - up.mass).abs() / up.mass.abs() < 1e-9);
assert!((knp.momentum.x - up.momentum.x).abs() / up.momentum.x.abs() < 1e-9);
assert!((knp.energy - up.energy).abs() / up.energy.abs() < 1e-9);
assert_eq!(knp_wave_speeds(&l, &r, X).a_minus, 0.0);
}
#[test]
fn supersonic_reverse_is_upwind_from_the_right() {
let l = KnpFaceState::static_enthalpy(
20.0,
Vector3::new(-520.0, 0.0, 0.0),
1.0e6,
300.0,
2.6e6,
);
let r = KnpFaceState::static_enthalpy(
18.0,
Vector3::new(-500.0, 0.0, 0.0),
0.9e6,
300.0,
2.5e6,
);
let knp = knp_face_flux(&l, &r, X);
let up = physical_face_flux(&r, X);
assert_eq!(knp_wave_speeds(&l, &r, X).a_plus, 0.0);
assert!((knp.mass - up.mass).abs() / up.mass.abs() < 1e-9);
assert!((knp.energy - up.energy).abs() / up.energy.abs() < 1e-9);
}
#[test]
fn mach_blend_monotone_and_clamped() {
assert_eq!(mach_blend(0.1, 0.3, 1.0), 0.0);
assert_eq!(mach_blend(0.3, 0.3, 1.0), 0.0);
assert_eq!(mach_blend(1.0, 0.3, 1.0), 1.0);
assert_eq!(mach_blend(1.5, 0.3, 1.0), 1.0);
assert!((mach_blend(0.65, 0.3, 1.0) - 0.5).abs() < 1e-12);
let mut prev = -1.0;
for i in 0..=20 {
let ma = i as f64 / 20.0 * 1.2;
let b = mach_blend(ma, 0.3, 1.0);
assert!(b >= prev - 1e-12 && (0.0..=1.0).contains(&b));
prev = b;
}
assert_eq!(mach_blend(0.4, 0.5, 0.5), 0.0);
assert_eq!(mach_blend(0.5, 0.5, 0.5), 1.0);
}
#[test]
fn energy_constructors_set_the_documented_pair() {
let u = Vector3::new(3.0, 4.0, 0.0); let te = KnpFaceState::total_energy(2.0, u, 1.0e5, 300.0, 1.0e5);
assert_eq!(te.w_e, 2.0 * (1.0e5 + 12.5));
assert_eq!(te.f_e, 1.0e5 + 12.5 + 1.0e5 / 2.0);
let se = KnpFaceState::static_enthalpy(2.0, u, 1.0e5, 300.0, 3.0e5);
assert_eq!(se.w_e, 6.0e5);
assert_eq!(se.f_e, 3.0e5);
let f = physical_face_flux(&te, X);
assert_eq!(f.mass, 6.0);
assert_eq!(f.energy, 6.0 * te.f_e);
assert_eq!(f.momentum, Vector3::new(6.0 * 3.0 + 1.0e5, 6.0 * 4.0, 0.0));
}
#[test]
fn consistent_and_antisymmetric_under_reversal() {
let s = liquid();
let k = knp_face_flux(&s, &s, X);
let f = physical_face_flux(&s, X);
assert!((k.mass - f.mass).abs() <= 1e-12 * f.mass.abs());
assert!((k.momentum.x - f.momentum.x).abs() <= 1e-12 * f.momentum.x.abs());
assert!((k.energy - f.energy).abs() <= 1e-12 * f.energy.abs());
let l = KnpFaceState::total_energy(1.0, Vector3::new(10.0, 0.0, 0.0), 1.0e5, 374.2, 2.5e5);
let r =
KnpFaceState::total_energy(0.125, Vector3::new(-5.0, 0.0, 0.0), 1.0e4, 334.7, 2.0e5);
let fwd = knp_face_flux(&l, &r, X);
let rev = knp_face_flux(&r, &l, Vector3::new(-1.0, 0.0, 0.0));
assert!((fwd.mass + rev.mass).abs() <= 1e-12 * fwd.mass.abs().max(1.0));
assert!((fwd.momentum.x + rev.momentum.x).abs() <= 1e-9 * fwd.momentum.x.abs());
assert!((fwd.energy + rev.energy).abs() <= 1e-9 * fwd.energy.abs());
}
#[test]
fn wave_speed_measures_match_hand_values() {
let q = KnpFaceState::total_energy(1.0, Vector3::ZERO, 1.0e5, 340.0, 2.5e5);
assert_eq!(local_wave_speed_max(&q, &q, X), 340.0);
assert_eq!(central_flux_speed_max(&q, &q, X), 170.0);
let s = KnpFaceState::total_energy(1.0, Vector3::new(500.0, 0.0, 0.0), 1.0e5, 340.0, 2.5e5);
assert_eq!(local_wave_speed_max(&s, &s, X), 840.0);
assert_eq!(central_flux_speed_max(&s, &s, X), 500.0);
let z = KnpFaceState::static_enthalpy(1.0, Vector3::ZERO, 0.0, 0.0, 0.0);
assert_eq!(central_flux_speed_max(&z, &z, X), 0.0);
let k = knp_face_flux(&z, &z, X);
assert!(k.mass == 0.0 && k.energy == 0.0 && k.momentum == Vector3::ZERO);
}
#[test]
fn sod_interface_flux_matches_hand_evaluation() {
let gamma = 1.4_f64;
let c_l = (gamma * 1.0e5 / 1.0).sqrt();
let c_r = (gamma * 1.0e4 / 0.125).sqrt();
let l = KnpFaceState::total_energy(1.0, Vector3::ZERO, 1.0e5, c_l, 1.0e5 / 0.4);
let r = KnpFaceState::total_energy(0.125, Vector3::ZERO, 1.0e4, c_r, 1.0e4 / (0.4 * 0.125));
let k = knp_face_flux(&l, &r, X);
let a = c_l.max(c_r);
let expect_mass = -a * a * (0.125 - 1.0) / (2.0 * a);
let expect_mom = (a * 1.0e5 + a * 1.0e4) / (2.0 * a);
let expect_e = -a * a * (r.w_e - l.w_e) / (2.0 * a);
assert!((k.mass - expect_mass).abs() < 1e-9 * expect_mass.abs());
assert!((k.momentum.x - expect_mom).abs() < 1e-9 * expect_mom);
assert!((k.energy - expect_e).abs() < 1e-9 * expect_e.abs());
assert!(k.mass > 0.0, "mass must flow from high to low pressure");
}
#[test]
fn matches_pre_refactor_hybrid_copy_bit_for_bit() {
fn legacy(
l: &KnpFaceState,
r: &KnpFaceState,
n: Vector3,
diss: bool,
) -> (f64, Vector3, f64) {
let (hl, hr) = (l.f_e, r.f_e);
let u_n_l = l.u.dot(n);
let u_n_r = r.u.dot(n);
let a_l = (u_n_l - l.c).min(u_n_r - r.c).min(0.0);
let a_r = (u_n_l + l.c).max(u_n_r + r.c).max(0.0);
let da = (a_r - a_l).max(1e-10);
let f_cont_l = l.rho * u_n_l;
let f_cont_r = r.rho * u_n_r;
let f_mom_l = (l.rho * u_n_l) * l.u + l.p * n;
let f_mom_r = (r.rho * u_n_r) * r.u + r.p * n;
let f_ener_l = l.rho * u_n_l * hl;
let f_ener_r = r.rho * u_n_r * hr;
let (j_rho, j_rhou, j_rhoe) = if diss {
(
a_l * a_r * (r.rho - l.rho),
a_l * a_r * (r.rho * r.u - l.rho * l.u),
a_l * a_r * (r.rho * hr - l.rho * hl),
)
} else {
(0.0, Vector3::ZERO, 0.0)
};
(
(a_r * f_cont_l - a_l * f_cont_r + j_rho) / da,
(a_r * f_mom_l - a_l * f_mom_r + j_rhou) * (1.0 / da),
(a_r * f_ener_l - a_l * f_ener_r + j_rhoe) / da,
)
}
let mut seed: u64 = 0x9e37_79b9_7f4a_7c15;
let mut next = || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(seed >> 11) as f64 / (1u64 << 53) as f64
};
let n_obl = Vector3::new(0.6, 0.8, 0.0);
for i in 0..2000 {
let mut st = || {
KnpFaceState::static_enthalpy(
1.0 + 999.0 * next(),
Vector3::new(400.0 * next() - 200.0, 50.0 * next() - 25.0, 0.0),
1.0e5 + 1.0e7 * next(),
1.0 + 1500.0 * next(),
1.0e5 + 2.7e6 * next(),
)
};
let (l, r) = (st(), st());
let n = if i % 2 == 0 { X } else { n_obl };
for diss in [true, false] {
let k = if diss {
knp_face_flux(&l, &r, n)
} else {
central_face_flux(&l, &r, n)
};
let (m, mo, e) = legacy(&l, &r, n, diss);
assert_eq!(k.mass.to_bits(), m.to_bits(), "mass, case {i}");
assert_eq!(k.momentum.x.to_bits(), mo.x.to_bits(), "mom x, case {i}");
assert_eq!(k.momentum.y.to_bits(), mo.y.to_bits(), "mom y, case {i}");
assert_eq!(k.momentum.z.to_bits(), mo.z.to_bits(), "mom z, case {i}");
assert_eq!(k.energy.to_bits(), e.to_bits(), "energy, case {i}");
}
}
}
#[test]
fn velocity_component_maps_interior_and_boundaries() {
use crate::interface::one_dimensional_meshing::create_one_d_mesh;
use std::sync::Arc;
use uom::si::area::square_meter;
use uom::si::f64::{Area, Length};
use uom::si::length::meter;
let mesh = Arc::new(
create_one_d_mesh(
Length::new::<meter>(1.0),
Area::new::<square_meter>(0.01),
4,
)
.expect("mesh"),
);
let mut u = VolVectorField::zero("U", mesh.clone());
for c in 0..4 {
u.internal.as_mut_slice()[c] = Vector3::new(c as f64, 10.0 + c as f64, -1.0);
}
u.boundary[0].bc = BoundaryCondition::FixedValue(Vector3::new(7.0, 8.0, 9.0));
let uy = velocity_component(&u, 1);
assert_eq!(uy.internal.as_slice(), &[10.0, 11.0, 12.0, 13.0]);
assert!(matches!(uy.boundary[0].bc, BoundaryCondition::FixedValue(v) if v == 8.0));
let uz = velocity_component(&u, 2);
assert!(uz.internal.as_slice().iter().all(|v| *v == -1.0));
assert_eq!(uz.name, "U_2");
}
}