use crate::fields::field::Field;
use crate::primitives::{SymmTensor, Tensor, Vector3};
#[derive(Debug, Clone)]
pub enum BoundaryCondition<T: Clone> {
FixedValue(T),
FixedField(Field<T>),
ZeroGradient,
FixedGradient(T),
Mixed {
value_fraction: f64,
ref_value: T,
ref_grad: T,
},
MixedField {
value_fraction: Field<f64>,
ref_value: Field<T>,
ref_grad: Field<T>,
},
InletOutlet {
inlet_value: T,
},
OutletInlet {
outlet_value: T,
},
Symmetry,
Slip,
NoSlip,
Wedge,
Empty,
Calculated(Field<T>),
Freestream {
freestream_value: T,
},
PressureInletOutletVelocity,
FixedFluxPressure {
gradient: T,
},
TotalPressure {
p0: T,
},
FlowRateInletVelocity {
volumetric_flow_rate: f64,
},
}
impl<T: Clone + Default> BoundaryCondition<T> {
pub fn is_fixed_value(&self) -> bool {
matches!(
self,
Self::FixedValue(_)
| Self::FixedField(_)
| Self::NoSlip
| Self::TotalPressure { .. }
| Self::FlowRateInletVelocity { .. }
)
}
}
impl<T: Clone> BoundaryCondition<T> {
pub fn flux_value_fraction(&self, phi_f: f64) -> Option<f64> {
match self {
Self::InletOutlet { .. }
| Self::Freestream { .. }
| Self::PressureInletOutletVelocity => Some(if phi_f < 0.0 { 1.0 } else { 0.0 }),
Self::OutletInlet { .. } => Some(if phi_f >= 0.0 { 1.0 } else { 0.0 }),
_ => None,
}
}
pub fn flux_ref_value(&self) -> Option<&T> {
match self {
Self::InletOutlet { inlet_value } => Some(inlet_value),
Self::OutletInlet { outlet_value } => Some(outlet_value),
Self::Freestream { freestream_value } => Some(freestream_value),
_ => None,
}
}
}
impl BoundaryCondition<f64> {
pub fn total_pressure_value(p0: f64, rho: f64, u_mag: f64) -> f64 {
p0 - 0.5 * rho * u_mag * u_mag
}
pub fn fixed_flux_pressure_sn_grad(
phi_hbya: f64,
phi_target: f64,
dp: f64,
mag_sf: f64,
) -> f64 {
let denom = dp * mag_sf;
if denom.abs() < 1e-300 {
0.0
} else {
(phi_hbya - phi_target) / denom
}
}
}
impl BoundaryCondition<Vector3> {
pub fn slip_face_value(internal: Vector3, unit_normal: Vector3) -> Vector3 {
internal - unit_normal * internal.dot(unit_normal)
}
pub fn pressure_inlet_outlet_velocity_value(phi_f: f64, area_vector: Vector3) -> Vector3 {
let mag = area_vector.mag();
if mag < 1e-300 {
Vector3::ZERO
} else {
area_vector * (phi_f / (mag * mag))
}
}
pub fn flow_rate_inlet_velocity_value(
q: f64,
area_patch: f64,
area_vector: Vector3,
) -> Vector3 {
let mag = area_vector.mag();
if mag < 1e-300 || area_patch.abs() < 1e-300 {
Vector3::ZERO
} else {
let n = area_vector * (1.0 / mag);
n * (-(q / area_patch))
}
}
}
#[derive(Debug, Clone)]
pub struct PatchField<T: Clone> {
pub bc: BoundaryCondition<T>,
pub values: Field<T>,
}
impl PatchField<f64> {
pub fn fixed_value(size: usize, v: f64) -> Self {
Self {
bc: BoundaryCondition::FixedValue(v),
values: Field::uniform(size, v),
}
}
pub fn zero_gradient(size: usize) -> Self {
Self {
bc: BoundaryCondition::ZeroGradient,
values: Field::zeros(size),
}
}
pub fn empty() -> Self {
Self {
bc: BoundaryCondition::Empty,
values: Field::new(vec![]),
}
}
pub fn update_total_pressure(&mut self, rho: &[f64], u_mag: &[f64]) {
if let BoundaryCondition::TotalPressure { p0 } = self.bc {
let n = self.values.len();
let vals: Vec<f64> = (0..n)
.map(|i| BoundaryCondition::<f64>::total_pressure_value(p0, rho[i], u_mag[i]))
.collect();
self.values = Field::new(vals);
}
}
}
impl PatchField<Vector3> {
pub fn fixed_value_vec(size: usize, v: Vector3) -> Self {
Self {
bc: BoundaryCondition::FixedValue(v),
values: Field::uniform(size, v),
}
}
pub fn zero_gradient_vec(size: usize) -> Self {
Self {
bc: BoundaryCondition::ZeroGradient,
values: Field::zero_vec(size),
}
}
pub fn empty_vec() -> Self {
Self {
bc: BoundaryCondition::Empty,
values: Field::new(vec![]),
}
}
pub fn update_flow_rate_inlet_velocity(&mut self, area_vectors: &[Vector3]) {
if let BoundaryCondition::FlowRateInletVelocity {
volumetric_flow_rate,
} = self.bc
{
let area_patch: f64 = area_vectors.iter().map(|s| s.mag()).sum();
let vals: Vec<Vector3> = area_vectors
.iter()
.map(|s| {
BoundaryCondition::flow_rate_inlet_velocity_value(
volumetric_flow_rate,
area_patch,
*s,
)
})
.collect();
self.values = Field::new(vals);
}
}
pub fn update_pressure_inlet_outlet_velocity(&mut self, phi: &[f64], area_vectors: &[Vector3]) {
if matches!(self.bc, BoundaryCondition::PressureInletOutletVelocity) {
let vals: Vec<Vector3> = area_vectors
.iter()
.zip(phi.iter())
.map(|(s, p)| BoundaryCondition::pressure_inlet_outlet_velocity_value(*p, *s))
.collect();
self.values = Field::new(vals);
}
}
}
impl PatchField<Tensor> {
pub fn fixed_value_tensor(size: usize, v: Tensor) -> Self {
Self {
bc: BoundaryCondition::FixedValue(v),
values: Field::uniform(size, v),
}
}
pub fn zero_gradient_tensor(size: usize) -> Self {
Self {
bc: BoundaryCondition::ZeroGradient,
values: Field::uniform(size, Tensor::ZERO),
}
}
}
impl PatchField<SymmTensor> {
pub fn fixed_value_symm_tensor(size: usize, v: SymmTensor) -> Self {
Self {
bc: BoundaryCondition::FixedValue(v),
values: Field::uniform(size, v),
}
}
pub fn zero_gradient_symm_tensor(size: usize) -> Self {
Self {
bc: BoundaryCondition::ZeroGradient,
values: Field::uniform(size, SymmTensor::ZERO),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vv_slip_zeroes_normal_preserves_tangential() {
let internal = Vector3::new(3.0, 4.0, 0.0);
let n = Vector3::new(1.0, 0.0, 0.0);
let v = BoundaryCondition::slip_face_value(internal, n);
assert!(
v.dot(n).abs() < 1e-15,
"normal component not removed: {}",
v.dot(n)
);
assert!((v.y - 4.0).abs() < 1e-15);
assert!(v.z.abs() < 1e-15);
let n2 = Vector3::new(1.0, 1.0, 0.0) * (1.0 / 2.0_f64.sqrt());
let v2 = BoundaryCondition::slip_face_value(Vector3::new(2.0, 0.0, 0.0), n2);
assert!(
v2.dot(n2).abs() < 1e-15,
"oblique normal not removed: {}",
v2.dot(n2)
);
}
#[test]
fn vv_flux_value_fraction_switch() {
let io = BoundaryCondition::InletOutlet {
inlet_value: 5.0_f64,
};
assert_eq!(io.flux_value_fraction(-1.0), Some(1.0)); assert_eq!(io.flux_value_fraction(1.0), Some(0.0)); assert_eq!(io.flux_ref_value(), Some(&5.0));
let oi = BoundaryCondition::OutletInlet {
outlet_value: 7.0_f64,
};
assert_eq!(oi.flux_value_fraction(-1.0), Some(0.0)); assert_eq!(oi.flux_value_fraction(1.0), Some(1.0)); assert_eq!(oi.flux_ref_value(), Some(&7.0));
assert_eq!(
BoundaryCondition::<f64>::ZeroGradient.flux_value_fraction(1.0),
None
);
}
#[test]
fn vv_freestream_switches_like_inlet_outlet() {
let fs = BoundaryCondition::Freestream {
freestream_value: 3.5_f64,
};
assert_eq!(fs.flux_value_fraction(-1.0), Some(1.0)); assert_eq!(fs.flux_value_fraction(1.0), Some(0.0)); assert_eq!(fs.flux_ref_value(), Some(&3.5));
}
#[test]
fn vv_pressure_inlet_outlet_velocity_from_flux() {
let sf = Vector3::new(2.0, 0.0, 0.0);
let u = BoundaryCondition::pressure_inlet_outlet_velocity_value(3.0, sf);
assert!(
(u.x - 1.5).abs() < 1e-15,
"|U| should be φ_f/|S_f| = 1.5, got {}",
u.x
);
assert!(
u.y.abs() < 1e-15 && u.z.abs() < 1e-15,
"velocity must be purely normal"
);
let bc = BoundaryCondition::<Vector3>::PressureInletOutletVelocity;
assert_eq!(bc.flux_value_fraction(-1.0), Some(1.0)); assert_eq!(bc.flux_value_fraction(1.0), Some(0.0)); let out = BoundaryCondition::pressure_inlet_outlet_velocity_value(4.0, sf);
let inn = BoundaryCondition::pressure_inlet_outlet_velocity_value(-4.0, sf);
assert!(out.x > 0.0 && inn.x < 0.0);
}
#[test]
fn vv_fixed_flux_pressure_sn_grad_reproduces_flux() {
let (phi_hbya, phi_target, dp, mag_sf) = (0.30_f64, 0.20, 0.5, 2.0);
let g = BoundaryCondition::fixed_flux_pressure_sn_grad(phi_hbya, phi_target, dp, mag_sf);
assert!(
(g - 0.1).abs() < 1e-12,
"snGrad should be 0.1 Pa/m, got {g}"
);
let phi = phi_hbya - dp * g * mag_sf;
assert!(
(phi - phi_target).abs() < 1e-12,
"corrected flux must equal target"
);
assert_eq!(
BoundaryCondition::fixed_flux_pressure_sn_grad(0.3, 0.2, 0.0, 2.0),
0.0
);
}
#[test]
fn vv_total_pressure_dynamic_head() {
let p0 = 1.0e5_f64;
let rho = 1.2;
assert!((BoundaryCondition::total_pressure_value(p0, rho, 0.0) - p0).abs() < 1e-6);
assert!(
(BoundaryCondition::total_pressure_value(p0, rho, 10.0) - (p0 - 60.0)).abs() < 1e-6
);
let mut pf = PatchField {
bc: BoundaryCondition::TotalPressure { p0 },
values: Field::new(vec![0.0, 0.0]),
};
pf.update_total_pressure(&[rho, rho], &[0.0, 10.0]);
assert!((pf.values[0] - p0).abs() < 1e-6);
assert!((pf.values[1] - (p0 - 60.0)).abs() < 1e-6);
}
#[test]
fn vv_flow_rate_inlet_velocity_integral_matches_q() {
let q = 8.0_f64;
let sfs = vec![
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(2.0, 0.0, 0.0),
];
let mut pf = PatchField {
bc: BoundaryCondition::FlowRateInletVelocity {
volumetric_flow_rate: q,
},
values: Field::new(vec![Vector3::ZERO; 3]),
};
pf.update_flow_rate_inlet_velocity(&sfs);
let area: f64 = sfs.iter().map(|s| s.mag()).sum();
let measured: f64 = pf
.values
.as_slice()
.iter()
.zip(&sfs)
.map(|(u, s)| -u.dot(*s))
.sum();
assert!(
(measured - q).abs() < 1e-12,
"patch flux {measured} must equal Q = {q}"
);
for u in pf.values.as_slice() {
assert!((u.mag() - q / area).abs() < 1e-12); assert!(u.x < 0.0); }
}
}