use super::equation::GoverningEquation;
use super::experiment::ExperimentSpec;
use super::falsifiable::FalsifiableSimulation;
use super::model_card::EquationModelCard;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigError {
pub message: String,
pub field: Option<String>,
pub cause: Option<String>,
}
impl ConfigError {
#[must_use]
pub fn new(message: &str) -> Self {
Self {
message: message.to_string(),
field: None,
cause: None,
}
}
#[must_use]
pub fn field_error(field: &str, message: &str) -> Self {
Self {
message: message.to_string(),
field: Some(field.to_string()),
cause: None,
}
}
#[must_use]
pub fn with_cause(mut self, cause: &str) -> Self {
self.cause = Some(cause.to_string());
self
}
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(ref field) = self.field {
write!(f, "Config error in '{}': {}", field, self.message)?;
} else {
write!(f, "Config error: {}", self.message)?;
}
if let Some(ref cause) = self.cause {
write!(f, " (caused by: {cause})")?;
}
Ok(())
}
}
impl std::error::Error for ConfigError {}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub validated_params: Vec<String>,
}
impl ValidationResult {
#[must_use]
pub fn success(validated_params: Vec<String>) -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
validated_params,
}
}
#[must_use]
pub fn failure(errors: Vec<String>) -> Self {
Self {
valid: false,
errors,
warnings: Vec::new(),
validated_params: Vec::new(),
}
}
#[must_use]
pub fn with_warning(mut self, warning: &str) -> Self {
self.warnings.push(warning.to_string());
self
}
}
#[derive(Debug, Clone)]
pub struct VerificationResult {
pub passed: bool,
pub total_tests: usize,
pub passed_tests: usize,
pub failed_tests: usize,
pub test_results: Vec<TestResult>,
}
impl VerificationResult {
#[must_use]
pub fn from_tests(test_results: Vec<TestResult>) -> Self {
let passed_tests = test_results.iter().filter(|t| t.passed).count();
let failed_tests = test_results.len() - passed_tests;
Self {
passed: failed_tests == 0,
total_tests: test_results.len(),
passed_tests,
failed_tests,
test_results,
}
}
#[must_use]
pub fn empty() -> Self {
Self {
passed: true,
total_tests: 0,
passed_tests: 0,
failed_tests: 0,
test_results: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct TestResult {
pub name: String,
pub passed: bool,
pub expected: f64,
pub actual: f64,
pub tolerance: f64,
pub message: String,
}
impl TestResult {
#[must_use]
pub fn pass(name: &str, expected: f64, actual: f64, tolerance: f64) -> Self {
Self {
name: name.to_string(),
passed: true,
expected,
actual,
tolerance,
message: format!(
"Test '{name}' PASSED: expected {expected:.6}, got {actual:.6} (tol: {tolerance:.6})"
),
}
}
#[must_use]
pub fn fail(name: &str, expected: f64, actual: f64, tolerance: f64) -> Self {
Self {
name: name.to_string(),
passed: false,
expected,
actual,
tolerance,
message: format!(
"Test '{name}' FAILED: expected {expected:.6}, got {actual:.6} (tol: {tolerance:.6})"
),
}
}
}
pub trait Reproducible {
fn set_seed(&mut self, seed: u64);
fn rng_state(&self) -> [u8; 32];
fn restore_rng_state(&mut self, state: &[u8; 32]);
fn current_seed(&self) -> u64;
}
pub trait YamlConfigurable: Sized {
fn from_yaml(spec: &ExperimentSpec) -> Result<Self, ConfigError>;
fn validate_against_emc(&self, emc: &EquationModelCard) -> ValidationResult;
fn parameters(&self) -> HashMap<String, f64>;
}
pub trait EddSimulation:
GoverningEquation + FalsifiableSimulation + Reproducible + YamlConfigurable
{
fn emc(&self) -> &EquationModelCard;
fn verify_against_emc(&self) -> VerificationResult;
fn simulation_name(&self) -> &str {
&self.emc().name
}
fn simulation_version(&self) -> &str {
&self.emc().version
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_error_new() {
let err = ConfigError::new("test error");
assert_eq!(err.message, "test error");
assert!(err.field.is_none());
assert!(err.cause.is_none());
}
#[test]
fn test_config_error_field() {
let err = ConfigError::field_error("omega", "must be positive");
assert_eq!(err.field, Some("omega".to_string()));
assert!(err.message.contains("positive"));
}
#[test]
fn test_config_error_with_cause() {
let err = ConfigError::new("parse failed").with_cause("invalid number");
assert!(err.cause.is_some());
assert!(err.cause.unwrap().contains("invalid"));
}
#[test]
fn test_config_error_display() {
let err = ConfigError::field_error("seed", "required").with_cause("missing key");
let display = format!("{err}");
assert!(display.contains("seed"));
assert!(display.contains("required"));
assert!(display.contains("missing key"));
}
#[test]
fn test_validation_result_success() {
let result = ValidationResult::success(vec!["omega".to_string(), "amplitude".to_string()]);
assert!(result.valid);
assert!(result.errors.is_empty());
assert_eq!(result.validated_params.len(), 2);
}
#[test]
fn test_validation_result_failure() {
let result = ValidationResult::failure(vec!["omega out of range".to_string()]);
assert!(!result.valid);
assert_eq!(result.errors.len(), 1);
}
#[test]
fn test_validation_result_with_warning() {
let result = ValidationResult::success(vec![]).with_warning("deprecated parameter");
assert!(result.valid);
assert_eq!(result.warnings.len(), 1);
}
#[test]
fn test_verification_result_from_tests() {
let tests = vec![
TestResult::pass("test1", 10.0, 10.0, 0.01),
TestResult::fail("test2", 5.0, 6.0, 0.01),
];
let result = VerificationResult::from_tests(tests);
assert!(!result.passed);
assert_eq!(result.total_tests, 2);
assert_eq!(result.passed_tests, 1);
assert_eq!(result.failed_tests, 1);
}
#[test]
fn test_verification_result_empty() {
let result = VerificationResult::empty();
assert!(result.passed);
assert_eq!(result.total_tests, 0);
}
#[test]
fn test_test_result_pass() {
let result = TestResult::pass("energy_conservation", 100.0, 100.001, 0.01);
assert!(result.passed);
assert!(result.message.contains("PASSED"));
}
#[test]
fn test_test_result_fail() {
let result = TestResult::fail("energy_conservation", 100.0, 110.0, 0.01);
assert!(!result.passed);
assert!(result.message.contains("FAILED"));
}
}