use crate::context::compute::ComputeContext;
use crate::context::error::OxiflowError;
use crate::context::value::ContextValue;
use crate::context::variable::ContextVariable;
use crate::mesh::Mesh;
pub trait RequiresContext {
fn required_variables(&self) -> Vec<ContextVariable>;
fn optional_variables(&self) -> Vec<ContextVariable> {
vec![]
}
fn depends_on(&self) -> Vec<ContextVariable> {
vec![]
}
fn priority(&self) -> u32 {
100
}
}
pub trait PhysicalModel: RequiresContext + Send + Sync {
fn compute_physics(
&self,
state: &ContextValue,
ctx: &ComputeContext,
) -> Result<ContextValue, OxiflowError>;
fn initial_state(&self, mesh: &dyn Mesh) -> ContextValue;
fn name(&self) -> &str;
fn description(&self) -> Option<&str> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::structured::UniformGrid1D;
use nalgebra::DVector;
struct FullModel;
impl RequiresContext for FullModel {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![
ContextVariable::Time,
ContextVariable::SpatialGradient {
dimension: 0,
component: None,
},
]
}
fn optional_variables(&self) -> Vec<ContextVariable> {
vec![ContextVariable::External {
name: "T_amb".into(),
}]
}
fn depends_on(&self) -> Vec<ContextVariable> {
vec![ContextVariable::Time]
}
fn priority(&self) -> u32 {
200
}
}
impl PhysicalModel for FullModel {
fn compute_physics(
&self,
state: &ContextValue,
_ctx: &ComputeContext,
) -> Result<ContextValue, OxiflowError> {
let u = state.as_scalar_field()?;
Ok(ContextValue::ScalarField(u.clone()))
}
fn initial_state(&self, mesh: &dyn Mesh) -> ContextValue {
ContextValue::ScalarField(DVector::from_element(mesh.n_dof(), 0.0))
}
fn name(&self) -> &str {
"full_model"
}
fn description(&self) -> Option<&str> {
Some("test model")
}
}
struct MinimalModel;
impl RequiresContext for MinimalModel {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![]
}
}
impl PhysicalModel for MinimalModel {
fn compute_physics(
&self,
state: &ContextValue,
_ctx: &ComputeContext,
) -> Result<ContextValue, OxiflowError> {
Ok(state.clone())
}
fn initial_state(&self, mesh: &dyn Mesh) -> ContextValue {
ContextValue::ScalarField(DVector::from_element(mesh.n_dof(), 1.0))
}
fn name(&self) -> &str {
"minimal"
}
}
#[test]
fn required_variables_returns_declared() {
let vars = FullModel.required_variables();
assert_eq!(vars.len(), 2);
assert!(vars.contains(&ContextVariable::Time));
}
#[test]
fn empty_required_variables_is_valid() {
assert!(MinimalModel.required_variables().is_empty());
}
#[test]
fn optional_and_depends_on_defaults_are_empty() {
assert!(MinimalModel.optional_variables().is_empty());
assert!(MinimalModel.depends_on().is_empty());
}
#[test]
fn priority_default_is_100() {
assert_eq!(MinimalModel.priority(), 100);
}
#[test]
fn priority_custom_value() {
assert_eq!(FullModel.priority(), 200);
}
#[test]
fn compute_physics_returns_derivative() {
let ctx = ComputeContext::new(0.0, 0.01);
let state = ContextValue::ScalarField(DVector::from_vec(vec![1.0, 2.0, 3.0]));
let result = FullModel.compute_physics(&state, &ctx).unwrap();
assert!(result.is_scalar_field());
assert_eq!(result.as_scalar_field().unwrap().len(), 3);
}
#[test]
fn compute_physics_wrong_state_type_returns_error() {
let ctx = ComputeContext::new(0.0, 0.01);
let state = ContextValue::Scalar(1.0);
let err = FullModel.compute_physics(&state, &ctx).unwrap_err();
assert!(matches!(err, OxiflowError::TypeMismatch { .. }));
}
#[test]
fn initial_state_matches_mesh_n_dof() {
let mesh = UniformGrid1D::new(10, 0.0, 1.0).unwrap();
let state = FullModel.initial_state(&mesh);
assert_eq!(state.as_scalar_field().unwrap().len(), 10);
}
#[test]
fn name_returns_identifier() {
assert_eq!(FullModel.name(), "full_model");
}
#[test]
fn description_returns_some_or_none() {
assert_eq!(FullModel.description(), Some("test model"));
assert_eq!(MinimalModel.description(), None);
}
#[test]
fn physical_model_is_object_safe() {
let models: Vec<Box<dyn PhysicalModel>> = vec![Box::new(FullModel), Box::new(MinimalModel)];
assert_eq!(models[0].name(), "full_model");
assert_eq!(models[1].name(), "minimal");
}
}