pub mod chain;
pub mod config;
pub mod linear;
pub mod methods;
pub mod orchestrator;
pub mod scenario;
pub mod snapshot;
#[cfg(feature = "sparse")]
pub mod sparse;
#[cfg(feature = "sparse")]
pub mod spec;
pub use config::{IntegratorKind, SolverConfiguration, StepControl, TimeConfiguration};
pub use scenario::{Domain, DomainId, Scenario};
pub use snapshot::SimulationSnapshot;
#[cfg(feature = "sparse")]
pub use spec::IntegratorSpec;
use crate::context::error::OxiflowError;
#[non_exhaustive]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SimulationResult {
pub states: Vec<crate::context::value::ContextValue>,
pub times: Vec<f64>,
pub n_steps: usize,
pub metadata: std::collections::HashMap<String, f64>,
}
impl SimulationResult {
pub fn len(&self) -> usize {
self.states.len()
}
pub fn is_empty(&self) -> bool {
self.states.is_empty()
}
pub fn t_final(&self) -> Option<f64> {
self.times.last().copied()
}
#[cfg(feature = "vtk")]
pub fn write_vtk(
&self,
mesh: &dyn crate::mesh::Mesh,
path: &std::path::Path,
) -> Result<(), OxiflowError> {
use crate::context::value::ContextValue;
use vtkio::model::{
Attribute, Attributes, ByteOrder, CellType, Cells, DataSet, IOBuffer,
UnstructuredGridPiece, Version, VertexNumbers, Vtk,
};
if mesh.spatial_dimension() != 1 {
return Err(OxiflowError::Persistence(format!(
"write_vtk currently only supports 1D meshes (Line cells), \
got spatial_dimension() = {}",
mesh.spatial_dimension()
)));
}
let n = mesh.n_dof();
if n == 0 {
return Err(OxiflowError::Persistence(
"write_vtk: mesh has zero degrees of freedom".to_string(),
));
}
let last_state = self.states.last().ok_or_else(|| {
OxiflowError::Persistence("write_vtk: SimulationResult has no saved states".to_string())
})?;
let values: Vec<f64> = match last_state {
ContextValue::ScalarField(v) => v.iter().copied().collect(),
other => {
return Err(OxiflowError::Persistence(format!(
"write_vtk currently only supports ScalarField states, got {other:?}"
)));
}
};
if values.len() != n {
return Err(OxiflowError::Persistence(format!(
"write_vtk: state has {} values, mesh has {n} degrees of freedom",
values.len()
)));
}
let mut points = Vec::with_capacity(n * 3);
for i in 0..n {
let c = mesh.coordinates(i);
points.push(c[0]);
points.push(0.0);
points.push(0.0);
}
let (connectivity, offsets, types) = if n >= 2 {
let mut connectivity = Vec::with_capacity((n - 1) * 2);
let mut offsets = Vec::with_capacity(n - 1);
for i in 0..n - 1 {
connectivity.push(i as u64);
connectivity.push((i + 1) as u64);
offsets.push(((i + 1) * 2) as u64);
}
(connectivity, offsets, vec![CellType::Line; n - 1])
} else {
(vec![0u64], vec![1u64], vec![CellType::Vertex])
};
let mut attributes = Attributes::new();
attributes
.point
.push(Attribute::scalars("field", 1).with_data(IOBuffer::F64(values)));
let vtk = Vtk {
version: Version::new((2, 0)),
title: String::from("oxiflow SimulationResult"),
byte_order: ByteOrder::BigEndian,
file_path: Some(path.to_path_buf()),
data: DataSet::inline(UnstructuredGridPiece {
points: IOBuffer::F64(points),
cells: Cells {
cell_verts: VertexNumbers::XML {
connectivity,
offsets,
},
types,
},
data: attributes,
}),
};
vtk.export(path)
.map_err(|e| OxiflowError::Persistence(format!("VTK export failed: {e}")))
}
}
pub trait Solver: Send + Sync {
fn solve(
&self,
scenario: &Scenario,
config: &SolverConfiguration,
) -> Result<SimulationResult, OxiflowError>;
fn on_divergence(&self, snapshot: &snapshot::SimulationSnapshot) {
let _ = snapshot;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::value::ContextValue;
use nalgebra::DVector;
#[test]
fn simulation_result_len() {
let r = SimulationResult {
states: vec![ContextValue::ScalarField(DVector::from_element(5, 0.0))],
times: vec![1.0],
n_steps: 10,
metadata: std::collections::HashMap::new(),
};
assert_eq!(r.len(), 1);
assert!(!r.is_empty());
}
#[test]
fn simulation_result_t_final() {
let r = SimulationResult {
states: vec![
ContextValue::ScalarField(DVector::from_element(5, 0.0)),
ContextValue::ScalarField(DVector::from_element(5, 1.0)),
],
times: vec![0.0, 1.0],
n_steps: 2,
metadata: std::collections::HashMap::new(),
};
assert_eq!(r.t_final(), Some(1.0));
}
#[test]
fn empty_result() {
let r = SimulationResult {
states: vec![],
times: vec![],
n_steps: 0,
metadata: std::collections::HashMap::new(),
};
assert!(r.is_empty());
assert_eq!(r.t_final(), None);
}
#[test]
fn solver_is_object_safe() {
fn assert_object_safe<T: Solver + ?Sized>() {}
assert_object_safe::<dyn Solver>();
}
#[cfg(feature = "vtk")]
#[test]
fn write_vtk_round_trip() {
use crate::mesh::structured::UniformGrid1D;
let mesh = UniformGrid1D::new(5, 0.0, 4.0).unwrap();
let result = SimulationResult {
states: vec![ContextValue::ScalarField(DVector::from_vec(vec![
0.0, 1.0, 2.0, 1.0, 0.0,
]))],
times: vec![0.4],
n_steps: 4,
metadata: std::collections::HashMap::new(),
};
let path = std::env::temp_dir().join("oxiflow_test_write_vtk_round_trip.vtu");
result.write_vtk(&mesh, &path).unwrap();
let reimported = vtkio::model::Vtk::import(&path).unwrap();
let vtkio::model::DataSet::UnstructuredGrid { pieces, .. } = reimported.data else {
panic!("expected an UnstructuredGrid dataset");
};
let piece = pieces[0]
.load_piece_data(None)
.expect("failed to load piece data");
assert_eq!(piece.points.len(), 5 * 3); let field = piece
.data
.point
.iter()
.find(|a| a.name() == "field")
.expect("missing 'field' point attribute");
let vtkio::model::Attribute::DataArray(da) = field else {
panic!("expected a DataArray attribute");
};
let values: Vec<f64> = da.data.clone().cast_into().unwrap();
assert_eq!(values, vec![0.0, 1.0, 2.0, 1.0, 0.0]);
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "vtk")]
#[test]
fn write_vtk_rejects_2d_mesh() {
struct FakeMesh2D;
impl crate::mesh::Mesh for FakeMesh2D {
fn n_dof(&self) -> usize {
1
}
fn coordinates(&self, _i: usize) -> &[f64] {
&[0.0, 0.0]
}
fn spatial_dimension(&self) -> usize {
2
}
fn characteristic_length(&self) -> f64 {
1.0
}
}
let result = SimulationResult {
states: vec![ContextValue::ScalarField(DVector::from_element(1, 0.0))],
times: vec![0.0],
n_steps: 0,
metadata: std::collections::HashMap::new(),
};
let path = std::env::temp_dir().join("oxiflow_test_write_vtk_rejects_2d.vtu");
let result = result.write_vtk(&FakeMesh2D, &path);
assert!(matches!(result, Err(OxiflowError::Persistence(_))));
}
#[cfg(feature = "vtk")]
#[test]
fn write_vtk_rejects_empty_states() {
use crate::mesh::structured::UniformGrid1D;
let mesh = UniformGrid1D::new(3, 0.0, 1.0).unwrap();
let result = SimulationResult {
states: vec![],
times: vec![],
n_steps: 0,
metadata: std::collections::HashMap::new(),
};
let path = std::env::temp_dir().join("oxiflow_test_write_vtk_rejects_empty.vtu");
let outcome = result.write_vtk(&mesh, &path);
assert!(matches!(outcome, Err(OxiflowError::Persistence(_))));
}
}