#[cfg(feature = "serde")]
use crate::context::error::OxiflowError;
use crate::context::value::ContextValue;
use crate::solver::config::{IntegratorKind, TimeConfiguration};
use crate::solver::SimulationResult;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SimulationSnapshot {
pub time_config: TimeConfiguration,
pub integrator: IntegratorKind,
pub state: ContextValue,
pub t: f64,
pub n_steps: usize,
pub error: Option<String>,
pub partial: Option<SimulationResult>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotFormat {
Json,
Bincode,
}
#[cfg(feature = "serde")]
pub fn write_snapshot(
snapshot: &SimulationSnapshot,
path: &std::path::Path,
format: SnapshotFormat,
) -> Result<(), OxiflowError> {
match format {
SnapshotFormat::Json => {
let file = std::fs::File::create(path)
.map_err(|e| OxiflowError::Persistence(format!("cannot create {path:?}: {e}")))?;
serde_json::to_writer_pretty(file, snapshot)
.map_err(|e| OxiflowError::Persistence(format!("JSON serialisation failed: {e}")))
}
SnapshotFormat::Bincode => {
let bytes = bincode::serialize(snapshot).map_err(|e| {
OxiflowError::Persistence(format!("bincode serialisation failed: {e}"))
})?;
std::fs::write(path, bytes)
.map_err(|e| OxiflowError::Persistence(format!("cannot write {path:?}: {e}")))
}
}
}
#[cfg(feature = "serde")]
pub fn read_snapshot(
path: &std::path::Path,
format: SnapshotFormat,
) -> Result<SimulationSnapshot, OxiflowError> {
match format {
SnapshotFormat::Json => {
let file = std::fs::File::open(path)
.map_err(|e| OxiflowError::Persistence(format!("cannot open {path:?}: {e}")))?;
serde_json::from_reader(file)
.map_err(|e| OxiflowError::Persistence(format!("JSON deserialisation failed: {e}")))
}
SnapshotFormat::Bincode => {
let bytes = std::fs::read(path)
.map_err(|e| OxiflowError::Persistence(format!("cannot read {path:?}: {e}")))?;
bincode::deserialize(&bytes).map_err(|e| {
OxiflowError::Persistence(format!("bincode deserialisation failed: {e}"))
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solver::config::StepControl;
use nalgebra::DVector;
fn sample_snapshot() -> SimulationSnapshot {
SimulationSnapshot {
time_config: TimeConfiguration::new(10.0, StepControl::Fixed { dt: 0.1 }),
integrator: IntegratorKind::BackwardEuler,
state: ContextValue::ScalarField(DVector::from_element(5, 1.5)),
t: 3.4,
n_steps: 34,
error: Some("non-finite value detected in state vector".to_string()),
partial: None,
}
}
#[test]
fn snapshot_fields_accessible() {
let s = sample_snapshot();
assert_eq!(s.t, 3.4);
assert_eq!(s.n_steps, 34);
assert!(s.error.is_some());
}
#[cfg(feature = "serde")]
#[test]
fn json_round_trip() {
let dir = tempdir();
let path = dir.join("snapshot.json");
let original = sample_snapshot();
write_snapshot(&original, &path, SnapshotFormat::Json).unwrap();
let restored = read_snapshot(&path, SnapshotFormat::Json).unwrap();
assert_eq!(restored.t, original.t);
assert_eq!(restored.n_steps, original.n_steps);
assert_eq!(restored.error, original.error);
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "serde")]
#[test]
fn bincode_round_trip() {
let dir = tempdir();
let path = dir.join("snapshot.bin");
let original = sample_snapshot();
write_snapshot(&original, &path, SnapshotFormat::Bincode).unwrap();
let restored = read_snapshot(&path, SnapshotFormat::Bincode).unwrap();
assert_eq!(restored.t, original.t);
assert_eq!(restored.n_steps, original.n_steps);
assert_eq!(restored.error, original.error);
std::fs::remove_file(&path).ok();
}
#[cfg(feature = "serde")]
#[test]
fn read_snapshot_missing_file_is_persistence_error() {
let path = std::path::PathBuf::from("/nonexistent/path/snapshot.json");
let err = read_snapshot(&path, SnapshotFormat::Json).unwrap_err();
assert!(matches!(err, OxiflowError::Persistence(_)));
}
#[cfg(feature = "serde")]
fn tempdir() -> std::path::PathBuf {
std::env::temp_dir()
}
}