use crate::boundary::BoundaryCondition;
use crate::context::error::OxiflowError;
use crate::context::variable::ContextVariable;
use crate::coupling::{CouplingOperator, Interface};
use crate::mesh::Mesh;
use crate::model::traits::PhysicalModel;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DomainId(String);
impl DomainId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for DomainId {
fn default() -> Self {
Self("default".to_string())
}
}
impl std::fmt::Display for DomainId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for DomainId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[non_exhaustive]
pub struct Domain {
pub id: DomainId,
pub model: Box<dyn PhysicalModel>,
pub mesh: Box<dyn Mesh>,
pub boundary_conditions: Vec<Box<dyn BoundaryCondition>>,
}
impl Domain {
pub fn new(
id: impl Into<DomainId>,
model: Box<dyn PhysicalModel>,
mesh: Box<dyn Mesh>,
) -> Self {
Self {
id: id.into(),
model,
mesh,
boundary_conditions: vec![],
}
}
pub fn with_boundary_conditions(mut self, bcs: Vec<Box<dyn BoundaryCondition>>) -> Self {
self.boundary_conditions = bcs;
self
}
}
pub struct Scenario {
domains: Vec<Domain>,
couplings: Vec<Box<dyn CouplingOperator>>,
interfaces: Vec<Interface>,
pub t_start: f64,
}
impl Scenario {
pub fn single(model: Box<dyn PhysicalModel>, mesh: Box<dyn Mesh>) -> Self {
Self {
domains: vec![Domain::new(DomainId::default(), model, mesh)],
couplings: vec![],
interfaces: vec![],
t_start: 0.0,
}
}
pub fn single_from(model: Box<dyn PhysicalModel>, mesh: Box<dyn Mesh>, t_start: f64) -> Self {
Self {
domains: vec![Domain::new(DomainId::default(), model, mesh)],
couplings: vec![],
interfaces: vec![],
t_start,
}
}
pub fn multi(domains: Vec<Domain>) -> Result<Self, OxiflowError> {
if domains.is_empty() {
return Err(OxiflowError::InvalidDomain(
"Scenario requires at least one domain".into(),
));
}
Ok(Self {
domains,
couplings: vec![],
interfaces: vec![],
t_start: 0.0,
})
}
pub fn with_t_start(mut self, t_start: f64) -> Self {
self.t_start = t_start;
self
}
pub fn with_coupling(mut self, coupling: Box<dyn CouplingOperator>) -> Self {
self.interfaces.push(coupling.interface().clone());
self.couplings.push(coupling);
self
}
pub fn n_domains(&self) -> usize {
self.domains.len()
}
pub fn n_couplings(&self) -> usize {
self.couplings.len()
}
pub fn couplings(&self) -> &[Box<dyn CouplingOperator>] {
&self.couplings
}
pub fn interfaces(&self) -> &[Interface] {
&self.interfaces
}
pub fn domains(&self) -> &[Domain] {
&self.domains
}
pub fn single_domain(&self) -> Result<&Domain, OxiflowError> {
if self.domains.len() != 1 {
return Err(OxiflowError::InvalidDomain(format!(
"expected 1 domain, found {}",
self.domains.len()
)));
}
Ok(&self.domains[0])
}
pub fn context_requirements(&self) -> Vec<ContextVariable> {
let mut requirements: Vec<ContextVariable> = self
.domains
.iter()
.flat_map(|d| d.model.required_variables())
.collect();
self.domains.iter().for_each(|d| {
requirements.extend(
d.boundary_conditions
.iter()
.flat_map(|bc| bc.required_variables()),
);
});
self.couplings.iter().for_each(|c| {
requirements.extend(c.required_variables());
});
requirements.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
requirements.dedup();
requirements
}
pub fn validate(&self) -> Result<(), OxiflowError> {
if self.domains.is_empty() {
return Err(OxiflowError::InvalidDomain(
"Scenario has no domains".into(),
));
}
for domain in &self.domains {
if domain.mesh.n_dof() == 0 {
return Err(OxiflowError::InvalidDomain(format!(
"domain '{}' has empty mesh",
domain.id
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::compute::ComputeContext;
use crate::context::error::OxiflowError;
use crate::context::value::ContextValue;
use crate::context::variable::ContextVariable;
use crate::mesh::structured::UniformGrid1D;
use crate::model::traits::{PhysicalModel, RequiresContext};
use nalgebra::DVector;
struct NeedsTime;
impl RequiresContext for NeedsTime {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![ContextVariable::Time]
}
}
impl PhysicalModel for NeedsTime {
fn compute_physics(
&self,
s: &ContextValue,
_: &ComputeContext,
) -> Result<ContextValue, OxiflowError> {
Ok(s.clone())
}
fn initial_state(&self, mesh: &dyn Mesh) -> ContextValue {
ContextValue::ScalarField(DVector::from_element(mesh.n_dof(), 0.0))
}
fn name(&self) -> &str {
"needs_time"
}
}
struct NeedsGradient;
impl RequiresContext for NeedsGradient {
fn required_variables(&self) -> Vec<ContextVariable> {
vec![ContextVariable::SpatialGradient {
dimension: 0,
component: None,
}]
}
}
impl PhysicalModel for NeedsGradient {
fn compute_physics(
&self,
s: &ContextValue,
_: &ComputeContext,
) -> Result<ContextValue, OxiflowError> {
Ok(s.clone())
}
fn initial_state(&self, mesh: &dyn Mesh) -> ContextValue {
ContextValue::ScalarField(DVector::from_element(mesh.n_dof(), 0.0))
}
fn name(&self) -> &str {
"needs_gradient"
}
}
fn make_mesh() -> Box<dyn Mesh> {
Box::new(UniformGrid1D::new(10, 0.0, 1.0).unwrap())
}
#[test]
fn domain_id_new() {
let id = DomainId::new("col_a");
assert_eq!(id.as_str(), "col_a");
}
#[test]
fn domain_id_default_is_default() {
assert_eq!(DomainId::default().as_str(), "default");
}
#[test]
fn domain_id_from_str() {
let id: DomainId = "lake".into();
assert_eq!(id.as_str(), "lake");
}
#[test]
fn domain_id_display() {
assert_eq!(format!("{}", DomainId::new("river")), "river");
}
#[test]
fn domain_id_equality() {
assert_eq!(DomainId::new("a"), DomainId::new("a"));
assert_ne!(DomainId::new("a"), DomainId::new("b"));
}
#[test]
fn single_creates_one_domain() {
let s = Scenario::single(Box::new(NeedsTime), make_mesh());
assert_eq!(s.n_domains(), 1);
}
#[test]
fn single_t_start_defaults_to_zero() {
let s = Scenario::single(Box::new(NeedsTime), make_mesh());
assert_eq!(s.t_start, 0.0);
}
#[test]
fn single_from_sets_t_start() {
let s = Scenario::single_from(Box::new(NeedsTime), make_mesh(), 5.0);
assert_eq!(s.t_start, 5.0);
}
#[test]
fn with_t_start_builder() {
let s = Scenario::single(Box::new(NeedsTime), make_mesh()).with_t_start(2.5);
assert_eq!(s.t_start, 2.5);
}
#[test]
fn multi_with_two_domains() {
let d1 = Domain::new("a", Box::new(NeedsTime), make_mesh());
let d2 = Domain::new("b", Box::new(NeedsGradient), make_mesh());
let s = Scenario::multi(vec![d1, d2]).unwrap();
assert_eq!(s.n_domains(), 2);
}
#[test]
fn multi_empty_returns_error() {
assert!(Scenario::multi(vec![]).is_err());
}
#[test]
fn single_domain_ok_for_one_domain() {
let s = Scenario::single(Box::new(NeedsTime), make_mesh());
assert!(s.single_domain().is_ok());
assert_eq!(s.single_domain().unwrap().id, DomainId::default());
}
#[test]
fn single_domain_err_for_multi() {
let d1 = Domain::new("a", Box::new(NeedsTime), make_mesh());
let d2 = Domain::new("b", Box::new(NeedsGradient), make_mesh());
let s = Scenario::multi(vec![d1, d2]).unwrap();
assert!(s.single_domain().is_err());
}
#[test]
fn requirements_from_single_domain() {
let s = Scenario::single(Box::new(NeedsTime), make_mesh());
let reqs = s.context_requirements();
assert!(reqs.contains(&ContextVariable::Time));
}
#[test]
fn requirements_aggregated_and_deduped_across_domains() {
let d1 = Domain::new("a", Box::new(NeedsTime), make_mesh());
let d2 = Domain::new("b", Box::new(NeedsTime), make_mesh());
let s = Scenario::multi(vec![d1, d2]).unwrap();
let reqs = s.context_requirements();
assert_eq!(
reqs.iter().filter(|v| **v == ContextVariable::Time).count(),
1
);
}
#[test]
fn requirements_union_of_all_domains() {
let d1 = Domain::new("a", Box::new(NeedsTime), make_mesh());
let d2 = Domain::new("b", Box::new(NeedsGradient), make_mesh());
let s = Scenario::multi(vec![d1, d2]).unwrap();
let reqs = s.context_requirements();
assert!(reqs.contains(&ContextVariable::Time));
assert!(reqs.contains(&ContextVariable::SpatialGradient {
dimension: 0,
component: None
}));
}
#[test]
fn validate_ok_for_valid_scenario() {
let s = Scenario::single(Box::new(NeedsTime), make_mesh());
assert!(s.validate().is_ok());
}
use crate::boundary::BoundaryCondition;
use crate::context::compute::ComputeContext as Ctx;
#[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) -> crate::boundary::BoundaryType {
crate::boundary::BoundaryType::Dirichlet
}
fn apply(
&self,
_state: &mut DVector<f64>,
_ctx: &Ctx,
_mesh: &dyn Mesh,
) -> Result<(), OxiflowError> {
Ok(())
}
}
#[test]
fn domain_with_boundary_conditions_stores_bcs() {
let bc: Box<dyn BoundaryCondition> = Box::new(TimeDependentBC);
let domain =
Domain::new("col", Box::new(NeedsTime), make_mesh()).with_boundary_conditions(vec![bc]);
assert_eq!(domain.boundary_conditions.len(), 1);
}
#[test]
fn domain_new_has_empty_bcs() {
let domain = Domain::new("col", Box::new(NeedsTime), make_mesh());
assert!(domain.boundary_conditions.is_empty());
}
#[test]
fn context_requirements_includes_bc_variables() {
let bc: Box<dyn BoundaryCondition> = Box::new(TimeDependentBC);
let domain = Domain::new("col", Box::new(NeedsGradient), make_mesh())
.with_boundary_conditions(vec![bc]);
let scenario = Scenario::multi(vec![domain]).unwrap();
let reqs = scenario.context_requirements();
assert!(reqs.contains(&ContextVariable::Time));
assert!(reqs.contains(&ContextVariable::SpatialGradient {
dimension: 0,
component: None
}));
}
#[test]
fn context_requirements_deduplicates_bc_and_model_variables() {
let bc: Box<dyn BoundaryCondition> = Box::new(TimeDependentBC);
let domain =
Domain::new("col", Box::new(NeedsTime), make_mesh()).with_boundary_conditions(vec![bc]);
let scenario = Scenario::multi(vec![domain]).unwrap();
let reqs = scenario.context_requirements();
assert_eq!(
reqs.iter().filter(|v| **v == ContextVariable::Time).count(),
1
);
}
}