1#![allow(dead_code)]
11
12use thiserror::Error;
13
14#[derive(Debug, Error)]
16pub enum Error {
17 #[error("{0}")]
19 General(String),
20}
21
22pub type Result<T> = std::result::Result<T, Error>;
24
25#[derive(Debug, Clone, PartialEq)]
27pub enum PhysicsError {
28 NumericalDivergence {
30 message: String,
32 },
33 InvalidInput {
35 field: String,
37 reason: String,
39 },
40 OutOfBounds {
42 index: usize,
44 max: usize,
46 },
47 ConvergenceFailed {
49 iterations: usize,
51 tolerance: f64,
53 residual: f64,
55 },
56 MeshError {
58 message: String,
60 },
61 MaterialError {
63 message: String,
65 },
66 CollisionError {
68 message: String,
70 },
71 IoError {
73 message: String,
75 },
76
77 FemElementError {
80 element_id: usize,
82 message: String,
84 },
85
86 FemAssemblyError {
88 message: String,
90 },
91
92 FemBoundaryConditionError {
94 node_id: usize,
96 message: String,
98 },
99
100 LbmLatticeError {
103 message: String,
105 },
106
107 LbmDistributionError {
109 cell_id: usize,
111 message: String,
113 },
114
115 LbmStabilityError {
117 parameter: String,
119 value: f64,
121 limit: f64,
123 },
124
125 SphKernelError {
128 message: String,
130 },
131
132 SphNeighborError {
134 particle_id: usize,
136 message: String,
138 },
139
140 SphDensityError {
142 particle_id: usize,
144 density: f64,
146 },
147
148 MdForceFieldError {
151 atom_types: (String, String),
153 message: String,
155 },
156
157 MdIntegrationError {
159 step: usize,
161 message: String,
163 },
164
165 MdNeighborListError {
167 message: String,
169 },
170
171 WithContext {
174 source: Box<PhysicsError>,
176 context: String,
178 },
179}
180
181impl std::fmt::Display for PhysicsError {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 match self {
184 PhysicsError::NumericalDivergence { message } => {
185 write!(f, "Numerical divergence: {}", message)
186 }
187 PhysicsError::InvalidInput { field, reason } => {
188 write!(f, "Invalid input for '{}': {}", field, reason)
189 }
190 PhysicsError::OutOfBounds { index, max } => {
191 write!(f, "Index {} out of bounds (max {})", index, max)
192 }
193 PhysicsError::ConvergenceFailed {
194 iterations,
195 tolerance,
196 residual,
197 } => write!(
198 f,
199 "Convergence failed after {} iterations (tolerance={}, residual={})",
200 iterations, tolerance, residual
201 ),
202 PhysicsError::MeshError { message } => write!(f, "Mesh error: {}", message),
203 PhysicsError::MaterialError { message } => write!(f, "Material error: {}", message),
204 PhysicsError::CollisionError { message } => write!(f, "Collision error: {}", message),
205 PhysicsError::IoError { message } => write!(f, "I/O error: {}", message),
206
207 PhysicsError::FemElementError {
209 element_id,
210 message,
211 } => write!(f, "FEM element {} error: {}", element_id, message),
212 PhysicsError::FemAssemblyError { message } => {
213 write!(f, "FEM assembly error: {}", message)
214 }
215 PhysicsError::FemBoundaryConditionError { node_id, message } => {
216 write!(f, "FEM BC error at node {}: {}", node_id, message)
217 }
218
219 PhysicsError::LbmLatticeError { message } => {
221 write!(f, "LBM lattice error: {}", message)
222 }
223 PhysicsError::LbmDistributionError { cell_id, message } => {
224 write!(f, "LBM distribution error at cell {}: {}", cell_id, message)
225 }
226 PhysicsError::LbmStabilityError {
227 parameter,
228 value,
229 limit,
230 } => write!(
231 f,
232 "LBM stability violation: {} = {} exceeds limit {}",
233 parameter, value, limit
234 ),
235
236 PhysicsError::SphKernelError { message } => {
238 write!(f, "SPH kernel error: {}", message)
239 }
240 PhysicsError::SphNeighborError {
241 particle_id,
242 message,
243 } => write!(
244 f,
245 "SPH neighbor error for particle {}: {}",
246 particle_id, message
247 ),
248 PhysicsError::SphDensityError {
249 particle_id,
250 density,
251 } => write!(
252 f,
253 "SPH density error: particle {} has invalid density {}",
254 particle_id, density
255 ),
256
257 PhysicsError::MdForceFieldError {
259 atom_types,
260 message,
261 } => write!(
262 f,
263 "MD force field error for pair ({}, {}): {}",
264 atom_types.0, atom_types.1, message
265 ),
266 PhysicsError::MdIntegrationError { step, message } => {
267 write!(f, "MD integration error at step {}: {}", step, message)
268 }
269 PhysicsError::MdNeighborListError { message } => {
270 write!(f, "MD neighbor list error: {}", message)
271 }
272
273 PhysicsError::WithContext { source, context } => {
275 write!(f, "{}: {}", context, source)
276 }
277 }
278 }
279}
280
281impl std::error::Error for PhysicsError {}
282
283impl PhysicsError {
284 pub fn with_context(self, context: impl Into<String>) -> Self {
286 PhysicsError::WithContext {
287 source: Box::new(self),
288 context: context.into(),
289 }
290 }
291
292 pub fn root_cause(&self) -> &PhysicsError {
294 match self {
295 PhysicsError::WithContext { source, .. } => source.root_cause(),
296 other => other,
297 }
298 }
299
300 pub fn context_chain(&self) -> Vec<&str> {
302 let mut chain = Vec::new();
303 let mut current = self;
304 while let PhysicsError::WithContext { source, context } = current {
305 chain.push(context.as_str());
306 current = source;
307 }
308 chain
309 }
310
311 pub fn domain(&self) -> &'static str {
313 match self {
314 PhysicsError::FemElementError { .. }
315 | PhysicsError::FemAssemblyError { .. }
316 | PhysicsError::FemBoundaryConditionError { .. } => "FEM",
317
318 PhysicsError::LbmLatticeError { .. }
319 | PhysicsError::LbmDistributionError { .. }
320 | PhysicsError::LbmStabilityError { .. } => "LBM",
321
322 PhysicsError::SphKernelError { .. }
323 | PhysicsError::SphNeighborError { .. }
324 | PhysicsError::SphDensityError { .. } => "SPH",
325
326 PhysicsError::MdForceFieldError { .. }
327 | PhysicsError::MdIntegrationError { .. }
328 | PhysicsError::MdNeighborListError { .. } => "MD",
329
330 PhysicsError::MeshError { .. } => "Mesh",
331 PhysicsError::MaterialError { .. } => "Material",
332 PhysicsError::CollisionError { .. } => "Collision",
333 PhysicsError::IoError { .. } => "IO",
334
335 PhysicsError::NumericalDivergence { .. } => "Numerical",
336 PhysicsError::InvalidInput { .. } => "Input",
337 PhysicsError::OutOfBounds { .. } => "Bounds",
338 PhysicsError::ConvergenceFailed { .. } => "Solver",
339
340 PhysicsError::WithContext { source, .. } => source.domain(),
341 }
342 }
343
344 pub fn recovery_suggestion(&self) -> &'static str {
346 match self {
347 PhysicsError::NumericalDivergence { .. } => {
348 "Try reducing the time step or using a more stable integration scheme."
349 }
350 PhysicsError::InvalidInput { .. } => {
351 "Check the input parameters against the documentation."
352 }
353 PhysicsError::OutOfBounds { .. } => "Verify array sizes and index calculations.",
354 PhysicsError::ConvergenceFailed { .. } => {
355 "Try increasing max iterations, relaxing tolerance, or improving the initial guess."
356 }
357 PhysicsError::MeshError { .. } => {
358 "Check mesh quality metrics and repair degenerate elements."
359 }
360 PhysicsError::MaterialError { .. } => {
361 "Verify material parameters (e.g., positive moduli, valid Poisson ratio)."
362 }
363 PhysicsError::CollisionError { .. } => {
364 "Check collision geometry and broadphase settings."
365 }
366 PhysicsError::IoError { .. } => "Check file paths, permissions, and disk space.",
367 PhysicsError::FemElementError { .. } => {
368 "Check element connectivity and node positions for degenerate configurations."
369 }
370 PhysicsError::FemAssemblyError { .. } => {
371 "Verify element stiffness matrices are positive semi-definite."
372 }
373 PhysicsError::FemBoundaryConditionError { .. } => {
374 "Ensure boundary conditions are consistent and the system is not over-constrained."
375 }
376 PhysicsError::LbmLatticeError { .. } => {
377 "Check lattice dimensions and velocity model configuration."
378 }
379 PhysicsError::LbmDistributionError { .. } => {
380 "Reduce the time step or check inlet/outlet boundary conditions."
381 }
382 PhysicsError::LbmStabilityError { .. } => {
383 "Reduce flow velocity or increase lattice resolution to satisfy stability constraints."
384 }
385 PhysicsError::SphKernelError { .. } => {
386 "Check smoothing length and kernel radius parameters."
387 }
388 PhysicsError::SphNeighborError { .. } => {
389 "Increase neighbor search radius or check spatial hashing configuration."
390 }
391 PhysicsError::SphDensityError { .. } => {
392 "Add tensile instability correction or increase particle count in sparse regions."
393 }
394 PhysicsError::MdForceFieldError { .. } => {
395 "Check force field parameter files and atom type definitions."
396 }
397 PhysicsError::MdIntegrationError { .. } => {
398 "Reduce time step, check for overlapping atoms, or use a thermostat."
399 }
400 PhysicsError::MdNeighborListError { .. } => {
401 "Increase skin distance or rebuild neighbor list more frequently."
402 }
403 PhysicsError::WithContext { source, .. } => source.recovery_suggestion(),
404 }
405 }
406
407 pub fn severity(&self) -> ErrorSeverity {
409 match self {
410 PhysicsError::NumericalDivergence { .. } => ErrorSeverity::Critical,
411 PhysicsError::ConvergenceFailed { .. } => ErrorSeverity::Warning,
412 PhysicsError::InvalidInput { .. } => ErrorSeverity::Error,
413 PhysicsError::OutOfBounds { .. } => ErrorSeverity::Error,
414 PhysicsError::LbmStabilityError { .. } => ErrorSeverity::Critical,
415 PhysicsError::MdIntegrationError { .. } => ErrorSeverity::Critical,
416 PhysicsError::SphDensityError { .. } => ErrorSeverity::Warning,
417 PhysicsError::FemElementError { .. } => ErrorSeverity::Error,
418 PhysicsError::WithContext { source, .. } => source.severity(),
419 _ => ErrorSeverity::Error,
420 }
421 }
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
426pub enum ErrorSeverity {
427 Info,
429 Warning,
431 Error,
433 Critical,
435}
436
437impl std::fmt::Display for ErrorSeverity {
438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439 match self {
440 ErrorSeverity::Info => write!(f, "INFO"),
441 ErrorSeverity::Warning => write!(f, "WARNING"),
442 ErrorSeverity::Error => write!(f, "ERROR"),
443 ErrorSeverity::Critical => write!(f, "CRITICAL"),
444 }
445 }
446}
447
448pub type PhysicsResult<T> = std::result::Result<T, PhysicsError>;
450
451pub fn check_positive(value: f64, name: &str) -> PhysicsResult<f64> {
453 if value > 0.0 {
454 Ok(value)
455 } else {
456 Err(PhysicsError::InvalidInput {
457 field: name.to_string(),
458 reason: format!("expected positive value, got {}", value),
459 })
460 }
461}
462
463pub fn check_non_negative(value: f64, name: &str) -> PhysicsResult<f64> {
465 if value >= 0.0 {
466 Ok(value)
467 } else {
468 Err(PhysicsError::InvalidInput {
469 field: name.to_string(),
470 reason: format!("expected non-negative value, got {}", value),
471 })
472 }
473}
474
475pub fn check_range(value: f64, min: f64, max: f64, name: &str) -> PhysicsResult<f64> {
477 if value >= min && value <= max {
478 Ok(value)
479 } else {
480 Err(PhysicsError::InvalidInput {
481 field: name.to_string(),
482 reason: format!("expected value in [{}, {}], got {}", min, max, value),
483 })
484 }
485}
486
487pub fn check_finite(value: f64, name: &str) -> PhysicsResult<f64> {
489 if value.is_finite() {
490 Ok(value)
491 } else {
492 Err(PhysicsError::NumericalDivergence {
493 message: format!("'{}' is not finite: {}", name, value),
494 })
495 }
496}
497
498pub fn check_index(index: usize, max: usize) -> PhysicsResult<usize> {
500 if index < max {
501 Ok(index)
502 } else {
503 Err(PhysicsError::OutOfBounds { index, max })
504 }
505}
506
507pub fn check_finite_vec3(v: [f64; 3], name: &str) -> PhysicsResult<[f64; 3]> {
509 for (i, &c) in v.iter().enumerate() {
510 if !c.is_finite() {
511 return Err(PhysicsError::NumericalDivergence {
512 message: format!("'{}'[{}] is not finite: {}", name, i, c),
513 });
514 }
515 }
516 Ok(v)
517}
518
519pub fn check_finite_slice(values: &[f64], name: &str) -> PhysicsResult<()> {
521 for (i, &v) in values.iter().enumerate() {
522 if !v.is_finite() {
523 return Err(PhysicsError::NumericalDivergence {
524 message: format!("'{}'[{}] is not finite: {}", name, i, v),
525 });
526 }
527 }
528 Ok(())
529}
530
531pub fn check_poisson_ratio(nu: f64) -> PhysicsResult<f64> {
533 if nu > -1.0 && nu < 0.5 {
534 Ok(nu)
535 } else {
536 Err(PhysicsError::MaterialError {
537 message: format!("Poisson's ratio must be in (-1, 0.5) for 3D, got {}", nu),
538 })
539 }
540}
541
542pub fn check_lbm_mach(velocity: f64, cs: f64, limit: f64) -> PhysicsResult<f64> {
544 let mach = velocity / cs;
545 if mach <= limit {
546 Ok(mach)
547 } else {
548 Err(PhysicsError::LbmStabilityError {
549 parameter: "Mach number".to_string(),
550 value: mach,
551 limit,
552 })
553 }
554}
555
556pub fn check_sph_density(particle_id: usize, density: f64) -> PhysicsResult<f64> {
558 if density > 0.0 {
559 Ok(density)
560 } else {
561 Err(PhysicsError::SphDensityError {
562 particle_id,
563 density,
564 })
565 }
566}
567
568pub fn check_md_timestep(dt: f64, max_dt: f64) -> PhysicsResult<f64> {
570 if dt <= 0.0 {
571 Err(PhysicsError::InvalidInput {
572 field: "timestep".to_string(),
573 reason: format!("must be positive, got {}", dt),
574 })
575 } else if dt > max_dt {
576 Err(PhysicsError::MdIntegrationError {
577 step: 0,
578 message: format!("timestep {} exceeds maximum {}", dt, max_dt),
579 })
580 } else {
581 Ok(dt)
582 }
583}
584
585#[derive(Debug, Clone)]
587pub struct SolverDiagnostics {
588 pub iterations: usize,
590 pub final_residual: f64,
592 pub converged: bool,
594 pub history: Vec<f64>,
596}
597
598impl SolverDiagnostics {
599 pub fn new() -> Self {
601 Self {
602 iterations: 0,
603 final_residual: f64::INFINITY,
604 converged: false,
605 history: Vec::new(),
606 }
607 }
608
609 pub fn record(&mut self, residual: f64) {
611 self.history.push(residual);
612 self.iterations = self.history.len();
613 self.final_residual = residual;
614 }
615
616 pub fn check_convergence(&self, tolerance: f64) -> PhysicsResult<()> {
618 if self.converged || self.final_residual <= tolerance {
619 Ok(())
620 } else {
621 Err(PhysicsError::ConvergenceFailed {
622 iterations: self.iterations,
623 tolerance,
624 residual: self.final_residual,
625 })
626 }
627 }
628
629 pub fn convergence_rate(&self) -> Option<f64> {
632 let n = self.history.len();
633 if n < 2 {
634 return None;
635 }
636 let window_size = 5.min(n);
637 let window = &self.history[n - window_size..];
638 let steps = window_size - 1;
640 let log_sum: f64 = window
641 .windows(2)
642 .map(|w| {
643 if w[0].abs() < f64::EPSILON {
644 0.0
645 } else {
646 (w[1].abs() / w[0].abs()).ln()
647 }
648 })
649 .sum();
650 Some((log_sum / steps as f64).exp())
651 }
652
653 pub fn is_diverging(&self) -> bool {
655 let n = self.history.len();
656 if n < 3 {
657 return false;
658 }
659 let last3 = &self.history[n - 3..];
661 last3[0] < last3[1] && last3[1] < last3[2]
662 }
663
664 pub fn summary(&self) -> String {
666 let status = if self.converged {
667 "CONVERGED"
668 } else if self.is_diverging() {
669 "DIVERGING"
670 } else {
671 "NOT CONVERGED"
672 };
673 format!(
674 "[{}] {} iterations, residual = {:.6e}",
675 status, self.iterations, self.final_residual
676 )
677 }
678
679 pub fn reset(&mut self) {
681 self.iterations = 0;
682 self.final_residual = f64::INFINITY;
683 self.converged = false;
684 self.history.clear();
685 }
686}
687
688impl Default for SolverDiagnostics {
689 fn default() -> Self {
690 Self::new()
691 }
692}
693
694#[derive(Debug, Clone)]
696pub struct DiagnosticInfo {
697 pub error: PhysicsError,
699 pub time: Option<f64>,
701 pub step: Option<usize>,
703 pub solver_diag: Option<SolverDiagnostics>,
705 pub metadata: Vec<(String, String)>,
707}
708
709impl DiagnosticInfo {
710 pub fn from_error(error: PhysicsError) -> Self {
712 Self {
713 error,
714 time: None,
715 step: None,
716 solver_diag: None,
717 metadata: Vec::new(),
718 }
719 }
720
721 pub fn at_time(mut self, t: f64) -> Self {
723 self.time = Some(t);
724 self
725 }
726
727 pub fn at_step(mut self, step: usize) -> Self {
729 self.step = Some(step);
730 self
731 }
732
733 pub fn with_solver_diag(mut self, diag: SolverDiagnostics) -> Self {
735 self.solver_diag = Some(diag);
736 self
737 }
738
739 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
741 self.metadata.push((key.into(), value.into()));
742 self
743 }
744
745 pub fn report(&self) -> String {
747 let mut lines = Vec::new();
748 lines.push(format!("[{}] {}", self.error.severity(), self.error));
749 lines.push(format!("Domain: {}", self.error.domain()));
750 lines.push(format!("Suggestion: {}", self.error.recovery_suggestion()));
751 if let Some(t) = self.time {
752 lines.push(format!("Time: {:.6e}", t));
753 }
754 if let Some(s) = self.step {
755 lines.push(format!("Step: {}", s));
756 }
757 if let Some(ref diag) = self.solver_diag {
758 lines.push(format!("Solver: {}", diag.summary()));
759 }
760 for (k, v) in &self.metadata {
761 lines.push(format!("{}: {}", k, v));
762 }
763 lines.join("\n")
764 }
765}
766
767#[derive(Debug, Clone, Default)]
769pub struct ErrorCollector {
770 pub errors: Vec<PhysicsError>,
772 pub max_errors: usize,
774}
775
776impl ErrorCollector {
777 pub fn new() -> Self {
779 Self {
780 errors: Vec::new(),
781 max_errors: 100,
782 }
783 }
784
785 pub fn with_limit(max: usize) -> Self {
787 Self {
788 errors: Vec::new(),
789 max_errors: max,
790 }
791 }
792
793 pub fn push(&mut self, error: PhysicsError) -> bool {
795 self.errors.push(error);
796 self.errors.len() >= self.max_errors
797 }
798
799 pub fn has_errors(&self) -> bool {
801 !self.errors.is_empty()
802 }
803
804 pub fn count(&self) -> usize {
806 self.errors.len()
807 }
808
809 pub fn drain(&mut self) -> Vec<PhysicsError> {
811 std::mem::take(&mut self.errors)
812 }
813
814 pub fn into_result(self) -> PhysicsResult<()> {
816 if self.errors.is_empty() {
817 Ok(())
818 } else {
819 let count = self.count();
820 let first = self
821 .errors
822 .into_iter()
823 .next()
824 .expect("errors is non-empty after check");
825 Err(first.with_context(format!("First of {} errors", count)))
826 }
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 #[test]
835 fn test_check_positive() {
836 assert!(check_positive(1.0, "x").is_ok());
837 assert!(check_positive(-1.0, "x").is_err());
838 assert!(check_positive(0.0, "x").is_err());
839 }
840
841 #[test]
842 fn test_check_range() {
843 assert!(check_range(0.5, 0.0, 1.0, "t").is_ok());
844 assert!(check_range(0.0, 0.0, 1.0, "t").is_ok());
845 assert!(check_range(1.0, 0.0, 1.0, "t").is_ok());
846 assert!(check_range(-0.1, 0.0, 1.0, "t").is_err());
847 assert!(check_range(1.1, 0.0, 1.0, "t").is_err());
848 }
849
850 #[test]
851 fn test_check_finite() {
852 assert!(check_finite(f64::NAN, "v").is_err());
853 assert!(check_finite(f64::INFINITY, "v").is_err());
854 assert!(check_finite(f64::NEG_INFINITY, "v").is_err());
855 assert!(check_finite(1.0, "v").is_ok());
856 }
857
858 #[test]
859 fn test_check_index() {
860 assert!(check_index(0, 5).is_ok());
861 assert!(check_index(4, 5).is_ok());
862 assert!(check_index(5, 5).is_err());
863 assert!(check_index(100, 5).is_err());
864 }
865
866 #[test]
867 fn test_solver_diagnostics_convergence() {
868 let mut diag = SolverDiagnostics::new();
869 let tolerance = 1e-6;
870 let mut residual = 1.0_f64;
872 for _ in 0..10 {
873 residual *= 0.1;
874 diag.record(residual);
875 }
876 assert!(diag.check_convergence(tolerance).is_ok());
878 }
879
880 #[test]
881 fn test_solver_diagnostics_not_converged() {
882 let mut diag = SolverDiagnostics::new();
883 diag.record(1.0);
884 diag.record(0.9);
885 assert!(diag.check_convergence(1e-6).is_err());
886 }
887
888 #[test]
889 fn test_convergence_rate_none_when_too_few() {
890 let mut diag = SolverDiagnostics::new();
891 assert!(diag.convergence_rate().is_none());
892 diag.record(1.0);
893 assert!(diag.convergence_rate().is_none());
894 }
895
896 #[test]
897 fn test_convergence_rate_some() {
898 let mut diag = SolverDiagnostics::new();
899 for i in 1..=6 {
900 diag.record(1.0 / (10_f64.powi(i)));
901 }
902 let rate = diag.convergence_rate();
903 assert!(rate.is_some());
904 let r = rate.unwrap();
906 assert!((r - 0.1).abs() < 1e-9, "rate was {}", r);
907 }
908
909 #[test]
910 fn test_display_physics_error() {
911 let e = PhysicsError::InvalidInput {
912 field: "density".to_string(),
913 reason: "must be positive".to_string(),
914 };
915 let s = format!("{}", e);
916 assert!(s.contains("density"));
917 assert!(s.contains("must be positive"));
918 }
919
920 #[test]
923 fn test_check_non_negative() {
924 assert!(check_non_negative(0.0, "x").is_ok());
925 assert!(check_non_negative(1.0, "x").is_ok());
926 assert!(check_non_negative(-0.01, "x").is_err());
927 }
928
929 #[test]
930 fn test_check_finite_vec3() {
931 assert!(check_finite_vec3([1.0, 2.0, 3.0], "v").is_ok());
932 assert!(check_finite_vec3([f64::NAN, 0.0, 0.0], "v").is_err());
933 assert!(check_finite_vec3([0.0, f64::INFINITY, 0.0], "v").is_err());
934 }
935
936 #[test]
937 fn test_check_finite_slice() {
938 assert!(check_finite_slice(&[1.0, 2.0, 3.0], "arr").is_ok());
939 assert!(check_finite_slice(&[1.0, f64::NAN], "arr").is_err());
940 }
941
942 #[test]
943 fn test_check_poisson_ratio() {
944 assert!(check_poisson_ratio(0.3).is_ok());
945 assert!(check_poisson_ratio(0.0).is_ok());
946 assert!(check_poisson_ratio(-0.5).is_ok());
947 assert!(check_poisson_ratio(0.5).is_err()); assert!(check_poisson_ratio(-1.0).is_err()); assert!(check_poisson_ratio(0.6).is_err());
950 }
951
952 #[test]
953 fn test_check_lbm_mach() {
954 assert!(check_lbm_mach(0.1, 1.0 / 3.0_f64.sqrt(), 0.3).is_ok());
955 let result = check_lbm_mach(10.0, 1.0 / 3.0_f64.sqrt(), 0.3);
957 assert!(result.is_err());
958 if let Err(PhysicsError::LbmStabilityError { parameter, .. }) = result {
959 assert_eq!(parameter, "Mach number");
960 }
961 }
962
963 #[test]
964 fn test_check_sph_density() {
965 assert!(check_sph_density(0, 1000.0).is_ok());
966 assert!(check_sph_density(5, -1.0).is_err());
967 assert!(check_sph_density(5, 0.0).is_err());
968 }
969
970 #[test]
971 fn test_check_md_timestep() {
972 assert!(check_md_timestep(0.001, 0.01).is_ok());
973 assert!(check_md_timestep(-0.001, 0.01).is_err());
974 assert!(check_md_timestep(0.1, 0.01).is_err());
975 }
976
977 #[test]
978 fn test_error_with_context() {
979 let e = PhysicsError::InvalidInput {
980 field: "mass".to_string(),
981 reason: "negative".to_string(),
982 };
983 let e2 = e.with_context("during particle initialization");
984 let s = format!("{}", e2);
985 assert!(s.contains("during particle initialization"));
986 assert!(s.contains("mass"));
987 }
988
989 #[test]
990 fn test_error_root_cause() {
991 let e = PhysicsError::NumericalDivergence {
992 message: "blowup".to_string(),
993 };
994 let e2 = e.with_context("in solver").with_context("in simulation");
995 let root = e2.root_cause();
996 match root {
997 PhysicsError::NumericalDivergence { message } => {
998 assert_eq!(message, "blowup");
999 }
1000 other => panic!("expected NumericalDivergence, got {:?}", other),
1001 }
1002 }
1003
1004 #[test]
1005 fn test_error_context_chain() {
1006 let e = PhysicsError::MeshError {
1007 message: "bad".to_string(),
1008 };
1009 let e2 = e.with_context("step 1").with_context("step 2");
1010 let chain = e2.context_chain();
1011 assert_eq!(chain.len(), 2);
1012 assert_eq!(chain[0], "step 2");
1013 assert_eq!(chain[1], "step 1");
1014 }
1015
1016 #[test]
1017 fn test_error_domain() {
1018 assert_eq!(
1019 PhysicsError::FemElementError {
1020 element_id: 0,
1021 message: "x".into()
1022 }
1023 .domain(),
1024 "FEM"
1025 );
1026 assert_eq!(
1027 PhysicsError::LbmLatticeError {
1028 message: "x".into()
1029 }
1030 .domain(),
1031 "LBM"
1032 );
1033 assert_eq!(
1034 PhysicsError::SphKernelError {
1035 message: "x".into()
1036 }
1037 .domain(),
1038 "SPH"
1039 );
1040 assert_eq!(
1041 PhysicsError::MdForceFieldError {
1042 atom_types: ("A".into(), "B".into()),
1043 message: "x".into()
1044 }
1045 .domain(),
1046 "MD"
1047 );
1048 }
1049
1050 #[test]
1051 fn test_error_severity() {
1052 assert_eq!(
1053 PhysicsError::NumericalDivergence {
1054 message: "x".into()
1055 }
1056 .severity(),
1057 ErrorSeverity::Critical
1058 );
1059 assert_eq!(
1060 PhysicsError::ConvergenceFailed {
1061 iterations: 10,
1062 tolerance: 1e-6,
1063 residual: 1e-3
1064 }
1065 .severity(),
1066 ErrorSeverity::Warning
1067 );
1068 assert_eq!(
1069 PhysicsError::InvalidInput {
1070 field: "x".into(),
1071 reason: "y".into()
1072 }
1073 .severity(),
1074 ErrorSeverity::Error
1075 );
1076 }
1077
1078 #[test]
1079 fn test_error_recovery_suggestion() {
1080 let e = PhysicsError::ConvergenceFailed {
1081 iterations: 100,
1082 tolerance: 1e-6,
1083 residual: 1e-3,
1084 };
1085 let suggestion = e.recovery_suggestion();
1086 assert!(suggestion.contains("iterations") || suggestion.contains("tolerance"));
1087 }
1088
1089 #[test]
1090 fn test_severity_display() {
1091 assert_eq!(format!("{}", ErrorSeverity::Critical), "CRITICAL");
1092 assert_eq!(format!("{}", ErrorSeverity::Warning), "WARNING");
1093 assert_eq!(format!("{}", ErrorSeverity::Error), "ERROR");
1094 assert_eq!(format!("{}", ErrorSeverity::Info), "INFO");
1095 }
1096
1097 #[test]
1098 fn test_severity_ordering() {
1099 assert!(ErrorSeverity::Info < ErrorSeverity::Warning);
1100 assert!(ErrorSeverity::Warning < ErrorSeverity::Error);
1101 assert!(ErrorSeverity::Error < ErrorSeverity::Critical);
1102 }
1103
1104 #[test]
1105 fn test_solver_diagnostics_is_diverging() {
1106 let mut diag = SolverDiagnostics::new();
1107 diag.record(1.0);
1108 diag.record(2.0);
1109 diag.record(3.0);
1110 assert!(diag.is_diverging());
1111
1112 let mut diag2 = SolverDiagnostics::new();
1113 diag2.record(3.0);
1114 diag2.record(2.0);
1115 diag2.record(1.0);
1116 assert!(!diag2.is_diverging());
1117 }
1118
1119 #[test]
1120 fn test_solver_diagnostics_summary() {
1121 let mut diag = SolverDiagnostics::new();
1122 diag.record(1.0);
1123 diag.converged = true;
1124 let s = diag.summary();
1125 assert!(s.contains("CONVERGED"));
1126 }
1127
1128 #[test]
1129 fn test_solver_diagnostics_reset() {
1130 let mut diag = SolverDiagnostics::new();
1131 diag.record(1.0);
1132 diag.record(0.5);
1133 diag.converged = true;
1134 diag.reset();
1135 assert_eq!(diag.iterations, 0);
1136 assert!(diag.history.is_empty());
1137 assert!(!diag.converged);
1138 }
1139
1140 #[test]
1141 fn test_diagnostic_info_report() {
1142 let e = PhysicsError::MdIntegrationError {
1143 step: 1000,
1144 message: "energy drift".to_string(),
1145 };
1146 let report = DiagnosticInfo::from_error(e)
1147 .at_time(1.5e-12)
1148 .at_step(1000)
1149 .with_metadata("ensemble", "NVT")
1150 .report();
1151 assert!(report.contains("CRITICAL"));
1152 assert!(report.contains("MD"));
1153 assert!(report.contains("energy drift"));
1154 assert!(report.contains("ensemble: NVT"));
1155 }
1156
1157 #[test]
1158 fn test_error_collector_basic() {
1159 let mut collector = ErrorCollector::new();
1160 assert!(!collector.has_errors());
1161 assert_eq!(collector.count(), 0);
1162
1163 collector.push(PhysicsError::InvalidInput {
1164 field: "x".into(),
1165 reason: "bad".into(),
1166 });
1167 assert!(collector.has_errors());
1168 assert_eq!(collector.count(), 1);
1169 }
1170
1171 #[test]
1172 fn test_error_collector_limit() {
1173 let mut collector = ErrorCollector::with_limit(3);
1174 collector.push(PhysicsError::MeshError {
1175 message: "e1".into(),
1176 });
1177 collector.push(PhysicsError::MeshError {
1178 message: "e2".into(),
1179 });
1180 let limit_reached = collector.push(PhysicsError::MeshError {
1181 message: "e3".into(),
1182 });
1183 assert!(limit_reached);
1184 }
1185
1186 #[test]
1187 fn test_error_collector_drain() {
1188 let mut collector = ErrorCollector::new();
1189 collector.push(PhysicsError::IoError {
1190 message: "test".into(),
1191 });
1192 let errors = collector.drain();
1193 assert_eq!(errors.len(), 1);
1194 assert!(!collector.has_errors());
1195 }
1196
1197 #[test]
1198 fn test_error_collector_into_result_ok() {
1199 let collector = ErrorCollector::new();
1200 assert!(collector.into_result().is_ok());
1201 }
1202
1203 #[test]
1204 fn test_error_collector_into_result_err() {
1205 let mut collector = ErrorCollector::new();
1206 collector.push(PhysicsError::IoError {
1207 message: "fail".into(),
1208 });
1209 let result = collector.into_result();
1210 assert!(result.is_err());
1211 }
1212
1213 #[test]
1214 fn test_display_fem_errors() {
1215 let e = PhysicsError::FemElementError {
1216 element_id: 42,
1217 message: "negative Jacobian".to_string(),
1218 };
1219 let s = format!("{}", e);
1220 assert!(s.contains("42"));
1221 assert!(s.contains("negative Jacobian"));
1222 }
1223
1224 #[test]
1225 fn test_display_lbm_errors() {
1226 let e = PhysicsError::LbmStabilityError {
1227 parameter: "CFL".to_string(),
1228 value: 1.5,
1229 limit: 1.0,
1230 };
1231 let s = format!("{}", e);
1232 assert!(s.contains("CFL"));
1233 assert!(s.contains("1.5"));
1234 assert!(s.contains("1"));
1235 }
1236
1237 #[test]
1238 fn test_display_sph_errors() {
1239 let e = PhysicsError::SphDensityError {
1240 particle_id: 7,
1241 density: -0.5,
1242 };
1243 let s = format!("{}", e);
1244 assert!(s.contains("7"));
1245 assert!(s.contains("-0.5"));
1246 }
1247
1248 #[test]
1249 fn test_display_md_errors() {
1250 let e = PhysicsError::MdForceFieldError {
1251 atom_types: ("C".to_string(), "O".to_string()),
1252 message: "missing LJ parameters".to_string(),
1253 };
1254 let s = format!("{}", e);
1255 assert!(s.contains("C"));
1256 assert!(s.contains("O"));
1257 assert!(s.contains("missing LJ parameters"));
1258 }
1259
1260 #[test]
1261 fn test_context_chain_preserves_domain() {
1262 let e = PhysicsError::SphKernelError {
1263 message: "bad kernel".into(),
1264 };
1265 let e2 = e.with_context("in step 5");
1266 assert_eq!(e2.domain(), "SPH");
1267 }
1268}