use std::borrow::Cow;
use crate::context::compute::ComputeContext;
use crate::context::error::OxiflowError;
use crate::mesh::Mesh;
use crate::model::traits::RequiresContext;
use nalgebra::DVector;
pub mod danckwerts;
pub use danckwerts::{DanckwertsInlet, DanckwertsOutlet};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BoundaryType {
Dirichlet,
Neumann,
Robin,
Periodic,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BoundaryLocation {
Inlet,
Outlet,
Wall,
Symmetry,
Periodic,
Interface,
PhaseInterface,
CouplingInterface,
Custom(Cow<'static, str>),
}
pub trait BoundaryCondition: RequiresContext + std::fmt::Debug {
fn boundary_type(&self) -> BoundaryType;
fn location(&self) -> Option<BoundaryLocation> {
None
}
fn apply(
&self,
state: &mut DVector<f64>,
ctx: &ComputeContext,
mesh: &dyn Mesh,
) -> Result<(), OxiflowError>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::variable::ContextVariable;
use crate::mesh::structured::UniformGrid1D;
#[derive(Debug)]
struct ZeroFluxBC;
impl RequiresContext for ZeroFluxBC {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
}
impl BoundaryCondition for ZeroFluxBC {
fn boundary_type(&self) -> BoundaryType {
BoundaryType::Neumann
}
fn apply(
&self,
_state: &mut DVector<f64>,
_ctx: &ComputeContext,
_mesh: &dyn Mesh,
) -> Result<(), OxiflowError> {
Ok(())
}
}
#[derive(Debug)]
struct FixedInletBC {
value: f64,
}
impl RequiresContext for FixedInletBC {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
fn priority(&self) -> u32 {
50
}
}
impl BoundaryCondition for FixedInletBC {
fn boundary_type(&self) -> BoundaryType {
BoundaryType::Dirichlet
}
fn location(&self) -> Option<BoundaryLocation> {
Some(BoundaryLocation::Inlet)
}
fn apply(
&self,
state: &mut DVector<f64>,
_ctx: &ComputeContext,
_mesh: &dyn Mesh,
) -> Result<(), OxiflowError> {
if !state.is_empty() {
state[0] = self.value;
}
Ok(())
}
}
#[derive(Debug)]
struct TimeDependentBC;
impl RequiresContext for TimeDependentBC {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![ContextVariable::Time]
}
}
impl BoundaryCondition for TimeDependentBC {
fn boundary_type(&self) -> BoundaryType {
BoundaryType::Dirichlet
}
fn apply(
&self,
state: &mut DVector<f64>,
ctx: &ComputeContext,
_mesh: &dyn Mesh,
) -> Result<(), OxiflowError> {
if !state.is_empty() {
state[0] = ctx.time();
}
Ok(())
}
}
fn make_mesh() -> UniformGrid1D {
UniformGrid1D::new(5, 0.0, 1.0).unwrap()
}
fn make_state(n: usize) -> DVector<f64> {
DVector::from_element(n, 0.0)
}
#[test]
fn boundary_type_variants_are_distinct() {
assert_ne!(BoundaryType::Dirichlet, BoundaryType::Neumann);
assert_ne!(BoundaryType::Neumann, BoundaryType::Robin);
assert_ne!(BoundaryType::Robin, BoundaryType::Periodic);
}
#[test]
fn boundary_type_clone_preserves_equality() {
assert_eq!(BoundaryType::Robin.clone(), BoundaryType::Robin);
}
#[test]
fn boundary_type_debug_non_empty() {
for t in [
BoundaryType::Dirichlet,
BoundaryType::Neumann,
BoundaryType::Robin,
BoundaryType::Periodic,
] {
assert!(!format!("{:?}", t).is_empty());
}
}
#[test]
fn boundary_location_variants_are_distinct() {
assert_ne!(BoundaryLocation::Inlet, BoundaryLocation::Outlet);
assert_ne!(BoundaryLocation::Wall, BoundaryLocation::Symmetry);
assert_ne!(
BoundaryLocation::Interface,
BoundaryLocation::PhaseInterface
);
assert_ne!(
BoundaryLocation::PhaseInterface,
BoundaryLocation::CouplingInterface,
);
}
#[test]
fn boundary_location_custom_equality_by_name() {
let a = BoundaryLocation::Custom("membrane".into());
let b = BoundaryLocation::Custom("membrane".into());
let c = BoundaryLocation::Custom("wall_left".into());
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn boundary_location_clone_preserves_equality() {
let loc = BoundaryLocation::Custom("top".into());
assert_eq!(loc.clone(), loc);
}
#[test]
fn boundary_location_debug_non_empty() {
let locations = [
BoundaryLocation::Inlet,
BoundaryLocation::Outlet,
BoundaryLocation::Wall,
BoundaryLocation::Symmetry,
BoundaryLocation::Periodic,
BoundaryLocation::Interface,
BoundaryLocation::PhaseInterface,
BoundaryLocation::CouplingInterface,
BoundaryLocation::Custom("test".into()),
];
for loc in &locations {
assert!(!format!("{:?}", loc).is_empty());
}
}
#[test]
fn trait_is_object_safe() {
let bcs: Vec<Box<dyn BoundaryCondition>> =
vec![Box::new(ZeroFluxBC), Box::new(FixedInletBC { value: 1.0 })];
assert_eq!(bcs.len(), 2);
}
#[test]
fn boxed_bc_can_be_applied() {
let bc: Box<dyn BoundaryCondition> = Box::new(FixedInletBC { value: 3.14 });
let mesh = make_mesh();
let mut state = make_state(mesh.n_dof());
let ctx = ComputeContext::new(0.0, 0.01);
assert!(bc.apply(&mut state, &ctx, &mesh).is_ok());
assert!((state[0] - 3.14).abs() < 1e-12);
}
#[test]
fn boundary_type_returned_correctly() {
assert_eq!(ZeroFluxBC.boundary_type(), BoundaryType::Neumann);
assert_eq!(
FixedInletBC { value: 0.0 }.boundary_type(),
BoundaryType::Dirichlet
);
assert_eq!(TimeDependentBC.boundary_type(), BoundaryType::Dirichlet);
}
#[test]
fn boundary_type_accessible_via_dyn() {
let bc: &dyn BoundaryCondition = &ZeroFluxBC;
assert_eq!(bc.boundary_type(), BoundaryType::Neumann);
}
#[test]
fn location_default_is_none() {
assert!(ZeroFluxBC.location().is_none());
assert!(TimeDependentBC.location().is_none());
}
#[test]
fn location_override_returned_correctly() {
assert_eq!(
FixedInletBC { value: 0.0 }.location(),
Some(BoundaryLocation::Inlet)
);
}
#[test]
fn location_accessible_via_dyn() {
let bc: &dyn BoundaryCondition = &FixedInletBC { value: 0.0 };
assert_eq!(bc.location(), Some(BoundaryLocation::Inlet));
}
#[test]
fn required_variables_forwarded_via_dyn() {
let bc: &dyn BoundaryCondition = &TimeDependentBC;
assert!(bc.required_variables().contains(&ContextVariable::Time));
}
#[test]
fn optional_variables_default_is_empty() {
assert!(ZeroFluxBC.optional_variables().is_empty());
}
#[test]
fn depends_on_default_is_empty() {
assert!(ZeroFluxBC.depends_on().is_empty());
}
#[test]
fn priority_default_is_100() {
assert_eq!(ZeroFluxBC.priority(), 100);
}
#[test]
fn priority_can_be_overridden() {
assert_eq!(FixedInletBC { value: 0.0 }.priority(), 50);
}
#[test]
fn zero_flux_bc_does_not_modify_state() {
let mesh = make_mesh();
let mut state = DVector::from_element(mesh.n_dof(), 2.0);
let ctx = ComputeContext::new(0.0, 0.01);
ZeroFluxBC.apply(&mut state, &ctx, &mesh).unwrap();
assert!(state.iter().all(|&v| (v - 2.0).abs() < 1e-12));
}
#[test]
fn fixed_inlet_bc_sets_first_dof() {
let mesh = make_mesh();
let mut state = make_state(mesh.n_dof());
let ctx = ComputeContext::new(0.0, 0.01);
FixedInletBC { value: 42.0 }
.apply(&mut state, &ctx, &mesh)
.unwrap();
assert!((state[0] - 42.0).abs() < 1e-12);
assert!(state.iter().skip(1).all(|&v| v == 0.0));
}
#[test]
fn time_dependent_bc_reads_context_time() {
let mesh = make_mesh();
let mut state = make_state(mesh.n_dof());
let ctx = ComputeContext::new(3.5, 0.01);
TimeDependentBC.apply(&mut state, &ctx, &mesh).unwrap();
assert!((state[0] - 3.5).abs() < 1e-12);
}
#[test]
fn debug_output_is_non_empty() {
let bc: &dyn BoundaryCondition = &ZeroFluxBC;
assert!(!format!("{:?}", bc).is_empty());
}
}