#![allow(dead_code)]
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("{0}")]
General(String),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq)]
pub enum PhysicsError {
NumericalDivergence {
message: String,
},
InvalidInput {
field: String,
reason: String,
},
OutOfBounds {
index: usize,
max: usize,
},
ConvergenceFailed {
iterations: usize,
tolerance: f64,
residual: f64,
},
MeshError {
message: String,
},
MaterialError {
message: String,
},
CollisionError {
message: String,
},
IoError {
message: String,
},
FemElementError {
element_id: usize,
message: String,
},
FemAssemblyError {
message: String,
},
FemBoundaryConditionError {
node_id: usize,
message: String,
},
LbmLatticeError {
message: String,
},
LbmDistributionError {
cell_id: usize,
message: String,
},
LbmStabilityError {
parameter: String,
value: f64,
limit: f64,
},
SphKernelError {
message: String,
},
SphNeighborError {
particle_id: usize,
message: String,
},
SphDensityError {
particle_id: usize,
density: f64,
},
MdForceFieldError {
atom_types: (String, String),
message: String,
},
MdIntegrationError {
step: usize,
message: String,
},
MdNeighborListError {
message: String,
},
WithContext {
source: Box<PhysicsError>,
context: String,
},
}
impl std::fmt::Display for PhysicsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PhysicsError::NumericalDivergence { message } => {
write!(f, "Numerical divergence: {}", message)
}
PhysicsError::InvalidInput { field, reason } => {
write!(f, "Invalid input for '{}': {}", field, reason)
}
PhysicsError::OutOfBounds { index, max } => {
write!(f, "Index {} out of bounds (max {})", index, max)
}
PhysicsError::ConvergenceFailed {
iterations,
tolerance,
residual,
} => write!(
f,
"Convergence failed after {} iterations (tolerance={}, residual={})",
iterations, tolerance, residual
),
PhysicsError::MeshError { message } => write!(f, "Mesh error: {}", message),
PhysicsError::MaterialError { message } => write!(f, "Material error: {}", message),
PhysicsError::CollisionError { message } => write!(f, "Collision error: {}", message),
PhysicsError::IoError { message } => write!(f, "I/O error: {}", message),
PhysicsError::FemElementError {
element_id,
message,
} => write!(f, "FEM element {} error: {}", element_id, message),
PhysicsError::FemAssemblyError { message } => {
write!(f, "FEM assembly error: {}", message)
}
PhysicsError::FemBoundaryConditionError { node_id, message } => {
write!(f, "FEM BC error at node {}: {}", node_id, message)
}
PhysicsError::LbmLatticeError { message } => {
write!(f, "LBM lattice error: {}", message)
}
PhysicsError::LbmDistributionError { cell_id, message } => {
write!(f, "LBM distribution error at cell {}: {}", cell_id, message)
}
PhysicsError::LbmStabilityError {
parameter,
value,
limit,
} => write!(
f,
"LBM stability violation: {} = {} exceeds limit {}",
parameter, value, limit
),
PhysicsError::SphKernelError { message } => {
write!(f, "SPH kernel error: {}", message)
}
PhysicsError::SphNeighborError {
particle_id,
message,
} => write!(
f,
"SPH neighbor error for particle {}: {}",
particle_id, message
),
PhysicsError::SphDensityError {
particle_id,
density,
} => write!(
f,
"SPH density error: particle {} has invalid density {}",
particle_id, density
),
PhysicsError::MdForceFieldError {
atom_types,
message,
} => write!(
f,
"MD force field error for pair ({}, {}): {}",
atom_types.0, atom_types.1, message
),
PhysicsError::MdIntegrationError { step, message } => {
write!(f, "MD integration error at step {}: {}", step, message)
}
PhysicsError::MdNeighborListError { message } => {
write!(f, "MD neighbor list error: {}", message)
}
PhysicsError::WithContext { source, context } => {
write!(f, "{}: {}", context, source)
}
}
}
}
impl std::error::Error for PhysicsError {}
impl PhysicsError {
pub fn with_context(self, context: impl Into<String>) -> Self {
PhysicsError::WithContext {
source: Box::new(self),
context: context.into(),
}
}
pub fn root_cause(&self) -> &PhysicsError {
match self {
PhysicsError::WithContext { source, .. } => source.root_cause(),
other => other,
}
}
pub fn context_chain(&self) -> Vec<&str> {
let mut chain = Vec::new();
let mut current = self;
while let PhysicsError::WithContext { source, context } = current {
chain.push(context.as_str());
current = source;
}
chain
}
pub fn domain(&self) -> &'static str {
match self {
PhysicsError::FemElementError { .. }
| PhysicsError::FemAssemblyError { .. }
| PhysicsError::FemBoundaryConditionError { .. } => "FEM",
PhysicsError::LbmLatticeError { .. }
| PhysicsError::LbmDistributionError { .. }
| PhysicsError::LbmStabilityError { .. } => "LBM",
PhysicsError::SphKernelError { .. }
| PhysicsError::SphNeighborError { .. }
| PhysicsError::SphDensityError { .. } => "SPH",
PhysicsError::MdForceFieldError { .. }
| PhysicsError::MdIntegrationError { .. }
| PhysicsError::MdNeighborListError { .. } => "MD",
PhysicsError::MeshError { .. } => "Mesh",
PhysicsError::MaterialError { .. } => "Material",
PhysicsError::CollisionError { .. } => "Collision",
PhysicsError::IoError { .. } => "IO",
PhysicsError::NumericalDivergence { .. } => "Numerical",
PhysicsError::InvalidInput { .. } => "Input",
PhysicsError::OutOfBounds { .. } => "Bounds",
PhysicsError::ConvergenceFailed { .. } => "Solver",
PhysicsError::WithContext { source, .. } => source.domain(),
}
}
pub fn recovery_suggestion(&self) -> &'static str {
match self {
PhysicsError::NumericalDivergence { .. } => {
"Try reducing the time step or using a more stable integration scheme."
}
PhysicsError::InvalidInput { .. } => {
"Check the input parameters against the documentation."
}
PhysicsError::OutOfBounds { .. } => "Verify array sizes and index calculations.",
PhysicsError::ConvergenceFailed { .. } => {
"Try increasing max iterations, relaxing tolerance, or improving the initial guess."
}
PhysicsError::MeshError { .. } => {
"Check mesh quality metrics and repair degenerate elements."
}
PhysicsError::MaterialError { .. } => {
"Verify material parameters (e.g., positive moduli, valid Poisson ratio)."
}
PhysicsError::CollisionError { .. } => {
"Check collision geometry and broadphase settings."
}
PhysicsError::IoError { .. } => "Check file paths, permissions, and disk space.",
PhysicsError::FemElementError { .. } => {
"Check element connectivity and node positions for degenerate configurations."
}
PhysicsError::FemAssemblyError { .. } => {
"Verify element stiffness matrices are positive semi-definite."
}
PhysicsError::FemBoundaryConditionError { .. } => {
"Ensure boundary conditions are consistent and the system is not over-constrained."
}
PhysicsError::LbmLatticeError { .. } => {
"Check lattice dimensions and velocity model configuration."
}
PhysicsError::LbmDistributionError { .. } => {
"Reduce the time step or check inlet/outlet boundary conditions."
}
PhysicsError::LbmStabilityError { .. } => {
"Reduce flow velocity or increase lattice resolution to satisfy stability constraints."
}
PhysicsError::SphKernelError { .. } => {
"Check smoothing length and kernel radius parameters."
}
PhysicsError::SphNeighborError { .. } => {
"Increase neighbor search radius or check spatial hashing configuration."
}
PhysicsError::SphDensityError { .. } => {
"Add tensile instability correction or increase particle count in sparse regions."
}
PhysicsError::MdForceFieldError { .. } => {
"Check force field parameter files and atom type definitions."
}
PhysicsError::MdIntegrationError { .. } => {
"Reduce time step, check for overlapping atoms, or use a thermostat."
}
PhysicsError::MdNeighborListError { .. } => {
"Increase skin distance or rebuild neighbor list more frequently."
}
PhysicsError::WithContext { source, .. } => source.recovery_suggestion(),
}
}
pub fn severity(&self) -> ErrorSeverity {
match self {
PhysicsError::NumericalDivergence { .. } => ErrorSeverity::Critical,
PhysicsError::ConvergenceFailed { .. } => ErrorSeverity::Warning,
PhysicsError::InvalidInput { .. } => ErrorSeverity::Error,
PhysicsError::OutOfBounds { .. } => ErrorSeverity::Error,
PhysicsError::LbmStabilityError { .. } => ErrorSeverity::Critical,
PhysicsError::MdIntegrationError { .. } => ErrorSeverity::Critical,
PhysicsError::SphDensityError { .. } => ErrorSeverity::Warning,
PhysicsError::FemElementError { .. } => ErrorSeverity::Error,
PhysicsError::WithContext { source, .. } => source.severity(),
_ => ErrorSeverity::Error,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ErrorSeverity {
Info,
Warning,
Error,
Critical,
}
impl std::fmt::Display for ErrorSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ErrorSeverity::Info => write!(f, "INFO"),
ErrorSeverity::Warning => write!(f, "WARNING"),
ErrorSeverity::Error => write!(f, "ERROR"),
ErrorSeverity::Critical => write!(f, "CRITICAL"),
}
}
}
pub type PhysicsResult<T> = std::result::Result<T, PhysicsError>;
pub fn check_positive(value: f64, name: &str) -> PhysicsResult<f64> {
if value > 0.0 {
Ok(value)
} else {
Err(PhysicsError::InvalidInput {
field: name.to_string(),
reason: format!("expected positive value, got {}", value),
})
}
}
pub fn check_non_negative(value: f64, name: &str) -> PhysicsResult<f64> {
if value >= 0.0 {
Ok(value)
} else {
Err(PhysicsError::InvalidInput {
field: name.to_string(),
reason: format!("expected non-negative value, got {}", value),
})
}
}
pub fn check_range(value: f64, min: f64, max: f64, name: &str) -> PhysicsResult<f64> {
if value >= min && value <= max {
Ok(value)
} else {
Err(PhysicsError::InvalidInput {
field: name.to_string(),
reason: format!("expected value in [{}, {}], got {}", min, max, value),
})
}
}
pub fn check_finite(value: f64, name: &str) -> PhysicsResult<f64> {
if value.is_finite() {
Ok(value)
} else {
Err(PhysicsError::NumericalDivergence {
message: format!("'{}' is not finite: {}", name, value),
})
}
}
pub fn check_index(index: usize, max: usize) -> PhysicsResult<usize> {
if index < max {
Ok(index)
} else {
Err(PhysicsError::OutOfBounds { index, max })
}
}
pub fn check_finite_vec3(v: [f64; 3], name: &str) -> PhysicsResult<[f64; 3]> {
for (i, &c) in v.iter().enumerate() {
if !c.is_finite() {
return Err(PhysicsError::NumericalDivergence {
message: format!("'{}'[{}] is not finite: {}", name, i, c),
});
}
}
Ok(v)
}
pub fn check_finite_slice(values: &[f64], name: &str) -> PhysicsResult<()> {
for (i, &v) in values.iter().enumerate() {
if !v.is_finite() {
return Err(PhysicsError::NumericalDivergence {
message: format!("'{}'[{}] is not finite: {}", name, i, v),
});
}
}
Ok(())
}
pub fn check_poisson_ratio(nu: f64) -> PhysicsResult<f64> {
if nu > -1.0 && nu < 0.5 {
Ok(nu)
} else {
Err(PhysicsError::MaterialError {
message: format!("Poisson's ratio must be in (-1, 0.5) for 3D, got {}", nu),
})
}
}
pub fn check_lbm_mach(velocity: f64, cs: f64, limit: f64) -> PhysicsResult<f64> {
let mach = velocity / cs;
if mach <= limit {
Ok(mach)
} else {
Err(PhysicsError::LbmStabilityError {
parameter: "Mach number".to_string(),
value: mach,
limit,
})
}
}
pub fn check_sph_density(particle_id: usize, density: f64) -> PhysicsResult<f64> {
if density > 0.0 {
Ok(density)
} else {
Err(PhysicsError::SphDensityError {
particle_id,
density,
})
}
}
pub fn check_md_timestep(dt: f64, max_dt: f64) -> PhysicsResult<f64> {
if dt <= 0.0 {
Err(PhysicsError::InvalidInput {
field: "timestep".to_string(),
reason: format!("must be positive, got {}", dt),
})
} else if dt > max_dt {
Err(PhysicsError::MdIntegrationError {
step: 0,
message: format!("timestep {} exceeds maximum {}", dt, max_dt),
})
} else {
Ok(dt)
}
}
#[derive(Debug, Clone)]
pub struct SolverDiagnostics {
pub iterations: usize,
pub final_residual: f64,
pub converged: bool,
pub history: Vec<f64>,
}
impl SolverDiagnostics {
pub fn new() -> Self {
Self {
iterations: 0,
final_residual: f64::INFINITY,
converged: false,
history: Vec::new(),
}
}
pub fn record(&mut self, residual: f64) {
self.history.push(residual);
self.iterations = self.history.len();
self.final_residual = residual;
}
pub fn check_convergence(&self, tolerance: f64) -> PhysicsResult<()> {
if self.converged || self.final_residual <= tolerance {
Ok(())
} else {
Err(PhysicsError::ConvergenceFailed {
iterations: self.iterations,
tolerance,
residual: self.final_residual,
})
}
}
pub fn convergence_rate(&self) -> Option<f64> {
let n = self.history.len();
if n < 2 {
return None;
}
let window_size = 5.min(n);
let window = &self.history[n - window_size..];
let steps = window_size - 1;
let log_sum: f64 = window
.windows(2)
.map(|w| {
if w[0].abs() < f64::EPSILON {
0.0
} else {
(w[1].abs() / w[0].abs()).ln()
}
})
.sum();
Some((log_sum / steps as f64).exp())
}
pub fn is_diverging(&self) -> bool {
let n = self.history.len();
if n < 3 {
return false;
}
let last3 = &self.history[n - 3..];
last3[0] < last3[1] && last3[1] < last3[2]
}
pub fn summary(&self) -> String {
let status = if self.converged {
"CONVERGED"
} else if self.is_diverging() {
"DIVERGING"
} else {
"NOT CONVERGED"
};
format!(
"[{}] {} iterations, residual = {:.6e}",
status, self.iterations, self.final_residual
)
}
pub fn reset(&mut self) {
self.iterations = 0;
self.final_residual = f64::INFINITY;
self.converged = false;
self.history.clear();
}
}
impl Default for SolverDiagnostics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticInfo {
pub error: PhysicsError,
pub time: Option<f64>,
pub step: Option<usize>,
pub solver_diag: Option<SolverDiagnostics>,
pub metadata: Vec<(String, String)>,
}
impl DiagnosticInfo {
pub fn from_error(error: PhysicsError) -> Self {
Self {
error,
time: None,
step: None,
solver_diag: None,
metadata: Vec::new(),
}
}
pub fn at_time(mut self, t: f64) -> Self {
self.time = Some(t);
self
}
pub fn at_step(mut self, step: usize) -> Self {
self.step = Some(step);
self
}
pub fn with_solver_diag(mut self, diag: SolverDiagnostics) -> Self {
self.solver_diag = Some(diag);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
pub fn report(&self) -> String {
let mut lines = Vec::new();
lines.push(format!("[{}] {}", self.error.severity(), self.error));
lines.push(format!("Domain: {}", self.error.domain()));
lines.push(format!("Suggestion: {}", self.error.recovery_suggestion()));
if let Some(t) = self.time {
lines.push(format!("Time: {:.6e}", t));
}
if let Some(s) = self.step {
lines.push(format!("Step: {}", s));
}
if let Some(ref diag) = self.solver_diag {
lines.push(format!("Solver: {}", diag.summary()));
}
for (k, v) in &self.metadata {
lines.push(format!("{}: {}", k, v));
}
lines.join("\n")
}
}
#[derive(Debug, Clone, Default)]
pub struct ErrorCollector {
pub errors: Vec<PhysicsError>,
pub max_errors: usize,
}
impl ErrorCollector {
pub fn new() -> Self {
Self {
errors: Vec::new(),
max_errors: 100,
}
}
pub fn with_limit(max: usize) -> Self {
Self {
errors: Vec::new(),
max_errors: max,
}
}
pub fn push(&mut self, error: PhysicsError) -> bool {
self.errors.push(error);
self.errors.len() >= self.max_errors
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn count(&self) -> usize {
self.errors.len()
}
pub fn drain(&mut self) -> Vec<PhysicsError> {
std::mem::take(&mut self.errors)
}
pub fn into_result(self) -> PhysicsResult<()> {
if self.errors.is_empty() {
Ok(())
} else {
let count = self.count();
let first = self
.errors
.into_iter()
.next()
.expect("errors is non-empty after check");
Err(first.with_context(format!("First of {} errors", count)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_positive() {
assert!(check_positive(1.0, "x").is_ok());
assert!(check_positive(-1.0, "x").is_err());
assert!(check_positive(0.0, "x").is_err());
}
#[test]
fn test_check_range() {
assert!(check_range(0.5, 0.0, 1.0, "t").is_ok());
assert!(check_range(0.0, 0.0, 1.0, "t").is_ok());
assert!(check_range(1.0, 0.0, 1.0, "t").is_ok());
assert!(check_range(-0.1, 0.0, 1.0, "t").is_err());
assert!(check_range(1.1, 0.0, 1.0, "t").is_err());
}
#[test]
fn test_check_finite() {
assert!(check_finite(f64::NAN, "v").is_err());
assert!(check_finite(f64::INFINITY, "v").is_err());
assert!(check_finite(f64::NEG_INFINITY, "v").is_err());
assert!(check_finite(1.0, "v").is_ok());
}
#[test]
fn test_check_index() {
assert!(check_index(0, 5).is_ok());
assert!(check_index(4, 5).is_ok());
assert!(check_index(5, 5).is_err());
assert!(check_index(100, 5).is_err());
}
#[test]
fn test_solver_diagnostics_convergence() {
let mut diag = SolverDiagnostics::new();
let tolerance = 1e-6;
let mut residual = 1.0_f64;
for _ in 0..10 {
residual *= 0.1;
diag.record(residual);
}
assert!(diag.check_convergence(tolerance).is_ok());
}
#[test]
fn test_solver_diagnostics_not_converged() {
let mut diag = SolverDiagnostics::new();
diag.record(1.0);
diag.record(0.9);
assert!(diag.check_convergence(1e-6).is_err());
}
#[test]
fn test_convergence_rate_none_when_too_few() {
let mut diag = SolverDiagnostics::new();
assert!(diag.convergence_rate().is_none());
diag.record(1.0);
assert!(diag.convergence_rate().is_none());
}
#[test]
fn test_convergence_rate_some() {
let mut diag = SolverDiagnostics::new();
for i in 1..=6 {
diag.record(1.0 / (10_f64.powi(i)));
}
let rate = diag.convergence_rate();
assert!(rate.is_some());
let r = rate.unwrap();
assert!((r - 0.1).abs() < 1e-9, "rate was {}", r);
}
#[test]
fn test_display_physics_error() {
let e = PhysicsError::InvalidInput {
field: "density".to_string(),
reason: "must be positive".to_string(),
};
let s = format!("{}", e);
assert!(s.contains("density"));
assert!(s.contains("must be positive"));
}
#[test]
fn test_check_non_negative() {
assert!(check_non_negative(0.0, "x").is_ok());
assert!(check_non_negative(1.0, "x").is_ok());
assert!(check_non_negative(-0.01, "x").is_err());
}
#[test]
fn test_check_finite_vec3() {
assert!(check_finite_vec3([1.0, 2.0, 3.0], "v").is_ok());
assert!(check_finite_vec3([f64::NAN, 0.0, 0.0], "v").is_err());
assert!(check_finite_vec3([0.0, f64::INFINITY, 0.0], "v").is_err());
}
#[test]
fn test_check_finite_slice() {
assert!(check_finite_slice(&[1.0, 2.0, 3.0], "arr").is_ok());
assert!(check_finite_slice(&[1.0, f64::NAN], "arr").is_err());
}
#[test]
fn test_check_poisson_ratio() {
assert!(check_poisson_ratio(0.3).is_ok());
assert!(check_poisson_ratio(0.0).is_ok());
assert!(check_poisson_ratio(-0.5).is_ok());
assert!(check_poisson_ratio(0.5).is_err()); assert!(check_poisson_ratio(-1.0).is_err()); assert!(check_poisson_ratio(0.6).is_err());
}
#[test]
fn test_check_lbm_mach() {
assert!(check_lbm_mach(0.1, 1.0 / 3.0_f64.sqrt(), 0.3).is_ok());
let result = check_lbm_mach(10.0, 1.0 / 3.0_f64.sqrt(), 0.3);
assert!(result.is_err());
if let Err(PhysicsError::LbmStabilityError { parameter, .. }) = result {
assert_eq!(parameter, "Mach number");
}
}
#[test]
fn test_check_sph_density() {
assert!(check_sph_density(0, 1000.0).is_ok());
assert!(check_sph_density(5, -1.0).is_err());
assert!(check_sph_density(5, 0.0).is_err());
}
#[test]
fn test_check_md_timestep() {
assert!(check_md_timestep(0.001, 0.01).is_ok());
assert!(check_md_timestep(-0.001, 0.01).is_err());
assert!(check_md_timestep(0.1, 0.01).is_err());
}
#[test]
fn test_error_with_context() {
let e = PhysicsError::InvalidInput {
field: "mass".to_string(),
reason: "negative".to_string(),
};
let e2 = e.with_context("during particle initialization");
let s = format!("{}", e2);
assert!(s.contains("during particle initialization"));
assert!(s.contains("mass"));
}
#[test]
fn test_error_root_cause() {
let e = PhysicsError::NumericalDivergence {
message: "blowup".to_string(),
};
let e2 = e.with_context("in solver").with_context("in simulation");
let root = e2.root_cause();
match root {
PhysicsError::NumericalDivergence { message } => {
assert_eq!(message, "blowup");
}
other => panic!("expected NumericalDivergence, got {:?}", other),
}
}
#[test]
fn test_error_context_chain() {
let e = PhysicsError::MeshError {
message: "bad".to_string(),
};
let e2 = e.with_context("step 1").with_context("step 2");
let chain = e2.context_chain();
assert_eq!(chain.len(), 2);
assert_eq!(chain[0], "step 2");
assert_eq!(chain[1], "step 1");
}
#[test]
fn test_error_domain() {
assert_eq!(
PhysicsError::FemElementError {
element_id: 0,
message: "x".into()
}
.domain(),
"FEM"
);
assert_eq!(
PhysicsError::LbmLatticeError {
message: "x".into()
}
.domain(),
"LBM"
);
assert_eq!(
PhysicsError::SphKernelError {
message: "x".into()
}
.domain(),
"SPH"
);
assert_eq!(
PhysicsError::MdForceFieldError {
atom_types: ("A".into(), "B".into()),
message: "x".into()
}
.domain(),
"MD"
);
}
#[test]
fn test_error_severity() {
assert_eq!(
PhysicsError::NumericalDivergence {
message: "x".into()
}
.severity(),
ErrorSeverity::Critical
);
assert_eq!(
PhysicsError::ConvergenceFailed {
iterations: 10,
tolerance: 1e-6,
residual: 1e-3
}
.severity(),
ErrorSeverity::Warning
);
assert_eq!(
PhysicsError::InvalidInput {
field: "x".into(),
reason: "y".into()
}
.severity(),
ErrorSeverity::Error
);
}
#[test]
fn test_error_recovery_suggestion() {
let e = PhysicsError::ConvergenceFailed {
iterations: 100,
tolerance: 1e-6,
residual: 1e-3,
};
let suggestion = e.recovery_suggestion();
assert!(suggestion.contains("iterations") || suggestion.contains("tolerance"));
}
#[test]
fn test_severity_display() {
assert_eq!(format!("{}", ErrorSeverity::Critical), "CRITICAL");
assert_eq!(format!("{}", ErrorSeverity::Warning), "WARNING");
assert_eq!(format!("{}", ErrorSeverity::Error), "ERROR");
assert_eq!(format!("{}", ErrorSeverity::Info), "INFO");
}
#[test]
fn test_severity_ordering() {
assert!(ErrorSeverity::Info < ErrorSeverity::Warning);
assert!(ErrorSeverity::Warning < ErrorSeverity::Error);
assert!(ErrorSeverity::Error < ErrorSeverity::Critical);
}
#[test]
fn test_solver_diagnostics_is_diverging() {
let mut diag = SolverDiagnostics::new();
diag.record(1.0);
diag.record(2.0);
diag.record(3.0);
assert!(diag.is_diverging());
let mut diag2 = SolverDiagnostics::new();
diag2.record(3.0);
diag2.record(2.0);
diag2.record(1.0);
assert!(!diag2.is_diverging());
}
#[test]
fn test_solver_diagnostics_summary() {
let mut diag = SolverDiagnostics::new();
diag.record(1.0);
diag.converged = true;
let s = diag.summary();
assert!(s.contains("CONVERGED"));
}
#[test]
fn test_solver_diagnostics_reset() {
let mut diag = SolverDiagnostics::new();
diag.record(1.0);
diag.record(0.5);
diag.converged = true;
diag.reset();
assert_eq!(diag.iterations, 0);
assert!(diag.history.is_empty());
assert!(!diag.converged);
}
#[test]
fn test_diagnostic_info_report() {
let e = PhysicsError::MdIntegrationError {
step: 1000,
message: "energy drift".to_string(),
};
let report = DiagnosticInfo::from_error(e)
.at_time(1.5e-12)
.at_step(1000)
.with_metadata("ensemble", "NVT")
.report();
assert!(report.contains("CRITICAL"));
assert!(report.contains("MD"));
assert!(report.contains("energy drift"));
assert!(report.contains("ensemble: NVT"));
}
#[test]
fn test_error_collector_basic() {
let mut collector = ErrorCollector::new();
assert!(!collector.has_errors());
assert_eq!(collector.count(), 0);
collector.push(PhysicsError::InvalidInput {
field: "x".into(),
reason: "bad".into(),
});
assert!(collector.has_errors());
assert_eq!(collector.count(), 1);
}
#[test]
fn test_error_collector_limit() {
let mut collector = ErrorCollector::with_limit(3);
collector.push(PhysicsError::MeshError {
message: "e1".into(),
});
collector.push(PhysicsError::MeshError {
message: "e2".into(),
});
let limit_reached = collector.push(PhysicsError::MeshError {
message: "e3".into(),
});
assert!(limit_reached);
}
#[test]
fn test_error_collector_drain() {
let mut collector = ErrorCollector::new();
collector.push(PhysicsError::IoError {
message: "test".into(),
});
let errors = collector.drain();
assert_eq!(errors.len(), 1);
assert!(!collector.has_errors());
}
#[test]
fn test_error_collector_into_result_ok() {
let collector = ErrorCollector::new();
assert!(collector.into_result().is_ok());
}
#[test]
fn test_error_collector_into_result_err() {
let mut collector = ErrorCollector::new();
collector.push(PhysicsError::IoError {
message: "fail".into(),
});
let result = collector.into_result();
assert!(result.is_err());
}
#[test]
fn test_display_fem_errors() {
let e = PhysicsError::FemElementError {
element_id: 42,
message: "negative Jacobian".to_string(),
};
let s = format!("{}", e);
assert!(s.contains("42"));
assert!(s.contains("negative Jacobian"));
}
#[test]
fn test_display_lbm_errors() {
let e = PhysicsError::LbmStabilityError {
parameter: "CFL".to_string(),
value: 1.5,
limit: 1.0,
};
let s = format!("{}", e);
assert!(s.contains("CFL"));
assert!(s.contains("1.5"));
assert!(s.contains("1"));
}
#[test]
fn test_display_sph_errors() {
let e = PhysicsError::SphDensityError {
particle_id: 7,
density: -0.5,
};
let s = format!("{}", e);
assert!(s.contains("7"));
assert!(s.contains("-0.5"));
}
#[test]
fn test_display_md_errors() {
let e = PhysicsError::MdForceFieldError {
atom_types: ("C".to_string(), "O".to_string()),
message: "missing LJ parameters".to_string(),
};
let s = format!("{}", e);
assert!(s.contains("C"));
assert!(s.contains("O"));
assert!(s.contains("missing LJ parameters"));
}
#[test]
fn test_context_chain_preserves_domain() {
let e = PhysicsError::SphKernelError {
message: "bad kernel".into(),
};
let e2 = e.with_context("in step 5");
assert_eq!(e2.domain(), "SPH");
}
}