use crate::boundary::BoundaryCondition;
use crate::context::compute::ComputeContext;
use crate::context::error::OxiflowError;
use crate::context::variable::ContextVariable;
use crate::mesh::Mesh;
use crate::model::traits::RequiresContext;
use nalgebra::DVector;
#[non_exhaustive]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DanckwertsInlet {
pub dispersion: f64,
pub velocity: f64,
pub feed_variable: ContextVariable,
}
impl DanckwertsInlet {
pub fn new(dispersion: f64, velocity: f64, feed_variable: ContextVariable) -> Self {
Self {
dispersion,
velocity,
feed_variable,
}
}
}
impl RequiresContext for DanckwertsInlet {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![
ContextVariable::SpatialGradient {
dimension: 0,
component: None,
},
self.feed_variable.clone(),
]
}
fn priority(&self) -> u32 {
50
}
}
impl BoundaryCondition for DanckwertsInlet {
fn boundary_type(&self) -> crate::boundary::BoundaryType {
crate::boundary::BoundaryType::Robin
}
fn location(&self) -> Option<crate::boundary::BoundaryLocation> {
Some(crate::boundary::BoundaryLocation::Inlet)
}
fn apply(
&self,
state: &mut DVector<f64>,
ctx: &ComputeContext,
mesh: &dyn Mesh,
) -> Result<(), OxiflowError> {
if mesh.n_dof() == 0 {
return Err(OxiflowError::PreconditionFailed {
context: "DanckwertsInlet",
message: "mesh has no degrees of freedom".into(),
});
}
if self.velocity == 0.0 {
return Err(OxiflowError::PreconditionFailed {
context: "DanckwertsInlet",
message: "velocity must be non-zero".into(),
});
}
let gradient = ctx.gradient(0)?;
let du_dx_0 = gradient[0];
let u_feed = ctx.external(self.feed_variable.clone())?.as_scalar()?;
state[0] = u_feed + (self.dispersion / self.velocity) * du_dx_0;
Ok(())
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DanckwertsOutlet;
impl DanckwertsOutlet {
pub fn new() -> Self {
Self
}
}
impl Default for DanckwertsOutlet {
fn default() -> Self {
Self::new()
}
}
impl RequiresContext for DanckwertsOutlet {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
fn priority(&self) -> u32 {
60
}
}
impl BoundaryCondition for DanckwertsOutlet {
fn boundary_type(&self) -> crate::boundary::BoundaryType {
crate::boundary::BoundaryType::Neumann
}
fn location(&self) -> Option<crate::boundary::BoundaryLocation> {
Some(crate::boundary::BoundaryLocation::Outlet)
}
fn apply(
&self,
state: &mut DVector<f64>,
_ctx: &ComputeContext,
mesh: &dyn Mesh,
) -> Result<(), OxiflowError> {
let n = mesh.n_dof();
if n < 2 {
return Err(OxiflowError::PreconditionFailed {
context: "DanckwertsOutlet",
message: "mesh must have at least 2 nodes".into(),
});
}
state[n - 1] = state[n - 2];
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::value::ContextValue;
use crate::mesh::structured::UniformGrid1D;
use nalgebra::DVector;
fn feed_var() -> ContextVariable {
ContextVariable::External {
name: "feed_concentration".into(),
}
}
fn make_inlet(d: f64, v: f64) -> DanckwertsInlet {
DanckwertsInlet::new(d, v, feed_var())
}
fn make_mesh(n: usize) -> UniformGrid1D {
UniformGrid1D::new(n, 0.0, 1.0).unwrap()
}
fn make_ctx_with_gradient_and_feed(gradient_at_0: f64, u_feed: f64) -> ComputeContext {
let mut ctx = ComputeContext::new(0.0, 0.01);
let n = 5;
let mut grad = DVector::zeros(n);
grad[0] = gradient_at_0;
ctx.insert(
ContextVariable::SpatialGradient {
dimension: 0,
component: None,
},
ContextValue::ScalarField(grad),
);
ctx.insert(feed_var(), ContextValue::Scalar(u_feed));
ctx
}
#[test]
fn inlet_boundary_type_is_robin() {
assert_eq!(
make_inlet(1e-7, 1e-3).boundary_type(),
crate::boundary::BoundaryType::Robin
);
}
#[test]
fn inlet_location_is_inlet() {
assert_eq!(
make_inlet(1e-7, 1e-3).location(),
Some(crate::boundary::BoundaryLocation::Inlet)
);
}
#[test]
fn outlet_boundary_type_is_neumann() {
assert_eq!(
DanckwertsOutlet::new().boundary_type(),
crate::boundary::BoundaryType::Neumann
);
}
#[test]
fn outlet_location_is_outlet() {
assert_eq!(
DanckwertsOutlet::new().location(),
Some(crate::boundary::BoundaryLocation::Outlet)
);
}
#[test]
fn inlet_required_variables_contains_gradient_and_feed() {
let inlet = make_inlet(1e-7, 1e-3);
let reqs = inlet.required_variables();
assert!(reqs.contains(&ContextVariable::SpatialGradient {
dimension: 0,
component: None,
}));
assert!(reqs.contains(&feed_var()));
assert_eq!(reqs.len(), 2);
}
#[test]
fn inlet_optional_variables_default_empty() {
assert!(make_inlet(1e-7, 1e-3).optional_variables().is_empty());
}
#[test]
fn inlet_depends_on_default_empty() {
assert!(make_inlet(1e-7, 1e-3).depends_on().is_empty());
}
#[test]
fn inlet_priority_is_50() {
assert_eq!(make_inlet(1e-7, 1e-3).priority(), 50);
}
#[test]
fn outlet_required_variables_is_empty() {
assert!(DanckwertsOutlet::new().required_variables().is_empty());
}
#[test]
fn outlet_priority_is_60() {
assert_eq!(DanckwertsOutlet::new().priority(), 60);
}
#[test]
fn inlet_apply_zero_gradient_equals_u_feed() {
let inlet = make_inlet(1e-7, 1e-3);
let mesh = make_mesh(5);
let ctx = make_ctx_with_gradient_and_feed(0.0, 1.0);
let mut state = DVector::zeros(5);
inlet.apply(&mut state, &ctx, &mesh).unwrap();
assert!((state[0] - 1.0).abs() < 1e-12);
}
#[test]
fn inlet_apply_formula_is_correct() {
let inlet = make_inlet(1e-4, 1e-2);
let mesh = make_mesh(5);
let ctx = make_ctx_with_gradient_and_feed(0.5, 0.8);
let mut state = DVector::zeros(5);
inlet.apply(&mut state, &ctx, &mesh).unwrap();
let expected = 0.8 + (1e-4 / 1e-2) * 0.5;
assert!((state[0] - expected).abs() < 1e-12);
}
#[test]
fn inlet_apply_does_not_modify_other_nodes() {
let inlet = make_inlet(1e-7, 1e-3);
let mesh = make_mesh(5);
let ctx = make_ctx_with_gradient_and_feed(0.0, 1.0);
let mut state = DVector::from_element(5, 2.0);
inlet.apply(&mut state, &ctx, &mesh).unwrap();
for i in 1..5 {
assert!((state[i] - 2.0).abs() < 1e-12, "node {i} was modified");
}
}
#[test]
fn inlet_apply_missing_gradient_returns_error() {
let inlet = make_inlet(1e-7, 1e-3);
let mesh = make_mesh(5);
let mut ctx = ComputeContext::new(0.0, 0.01);
ctx.insert(feed_var(), ContextValue::Scalar(1.0));
let mut state = DVector::zeros(5);
let err = inlet.apply(&mut state, &ctx, &mesh).unwrap_err();
assert!(matches!(err, OxiflowError::MissingCalculator(_)));
}
#[test]
fn inlet_apply_missing_feed_returns_error() {
let inlet = make_inlet(1e-7, 1e-3);
let mesh = make_mesh(5);
let mut ctx = ComputeContext::new(0.0, 0.01);
let grad = ContextValue::ScalarField(DVector::zeros(5));
ctx.insert(
ContextVariable::SpatialGradient {
dimension: 0,
component: None,
},
grad,
);
let mut state = DVector::zeros(5);
let err = inlet.apply(&mut state, &ctx, &mesh).unwrap_err();
assert!(matches!(err, OxiflowError::MissingCalculator(_)));
}
#[test]
fn inlet_apply_zero_velocity_returns_error() {
let inlet = make_inlet(1e-7, 0.0);
let mesh = make_mesh(5);
let ctx = make_ctx_with_gradient_and_feed(0.0, 1.0);
let mut state = DVector::zeros(5);
let err = inlet.apply(&mut state, &ctx, &mesh).unwrap_err();
assert!(matches!(err, OxiflowError::PreconditionFailed { .. }));
}
#[test]
fn inlet_apply_empty_mesh_returns_error() {
let inlet = make_inlet(1e-7, 1e-3);
struct EmptyMesh;
impl Mesh for EmptyMesh {
fn n_dof(&self) -> usize {
0
}
fn coordinates(&self, _: usize) -> &[f64] {
&[]
}
fn spatial_dimension(&self) -> usize {
1
}
fn characteristic_length(&self) -> f64 {
0.0
}
}
impl std::fmt::Debug for EmptyMesh {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "EmptyMesh")
}
}
let mesh = EmptyMesh;
let ctx = make_ctx_with_gradient_and_feed(0.0, 1.0);
let mut state = DVector::zeros(0);
let err = inlet.apply(&mut state, &ctx, &mesh).unwrap_err();
assert!(matches!(err, OxiflowError::PreconditionFailed { .. }));
}
#[test]
fn outlet_apply_sets_last_node_to_predecessor() {
let mesh = make_mesh(5);
let ctx = ComputeContext::new(0.0, 0.01);
let mut state = DVector::from_vec(vec![1.0, 2.0, 3.0, 4.0, 9.9]);
DanckwertsOutlet::new()
.apply(&mut state, &ctx, &mesh)
.unwrap();
assert!((state[4] - state[3]).abs() < 1e-12);
assert!((state[4] - 4.0).abs() < 1e-12);
}
#[test]
fn outlet_apply_does_not_modify_other_nodes() {
let mesh = make_mesh(5);
let ctx = ComputeContext::new(0.0, 0.01);
let values = vec![1.0, 2.0, 3.0, 4.0, 9.9];
let mut state = DVector::from_vec(values.clone());
DanckwertsOutlet::new()
.apply(&mut state, &ctx, &mesh)
.unwrap();
for i in 0..4 {
assert!(
(state[i] - values[i]).abs() < 1e-12,
"node {i} was modified"
);
}
}
#[test]
fn outlet_apply_two_node_mesh() {
let mesh = make_mesh(2);
let ctx = ComputeContext::new(0.0, 0.01);
let mut state = DVector::from_vec(vec![3.0, 0.0]);
DanckwertsOutlet::new()
.apply(&mut state, &ctx, &mesh)
.unwrap();
assert!((state[1] - 3.0).abs() < 1e-12);
}
#[test]
fn outlet_apply_single_node_returns_error() {
struct SingleMesh;
impl Mesh for SingleMesh {
fn n_dof(&self) -> usize {
1
}
fn coordinates(&self, _: usize) -> &[f64] {
&[0.0]
}
fn spatial_dimension(&self) -> usize {
1
}
fn characteristic_length(&self) -> f64 {
0.0
}
}
impl std::fmt::Debug for SingleMesh {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SingleMesh")
}
}
let mesh = SingleMesh;
let ctx = ComputeContext::new(0.0, 0.01);
let mut state = DVector::from_element(1, 1.0);
let err = DanckwertsOutlet::new()
.apply(&mut state, &ctx, &mesh)
.unwrap_err();
assert!(matches!(err, OxiflowError::PreconditionFailed { .. }));
}
#[test]
fn both_bcs_are_object_safe() {
let bcs: Vec<Box<dyn BoundaryCondition>> = vec![
Box::new(make_inlet(1e-7, 1e-3)),
Box::new(DanckwertsOutlet::new()),
];
assert_eq!(bcs.len(), 2);
}
#[test]
fn debug_inlet_is_non_empty() {
let s = format!("{:?}", make_inlet(1e-7, 1e-3));
assert!(s.contains("DanckwertsInlet"));
assert!(s.contains("dispersion"));
}
#[test]
fn debug_outlet_is_non_empty() {
assert!(!format!("{:?}", DanckwertsOutlet::new()).is_empty());
}
}