Entropyk/crates/solver/tests/verbose_mode.rs

480 lines
15 KiB
Rust

//! Tests for verbose mode diagnostics (Story 7.4).
//!
//! Covers:
//! - VerboseConfig default behavior
//! - IterationDiagnostics collection
//! - Jacobian condition number estimation
//! - ConvergenceDiagnostics summary
use entropyk_solver::jacobian::JacobianMatrix;
use entropyk_solver::{
ConvergenceDiagnostics, IterationDiagnostics, SolverSwitchEvent, SolverType, SwitchReason,
VerboseConfig, VerboseOutputFormat,
};
// =============================================================================
// Task 1: VerboseConfig Tests
// =============================================================================
#[test]
fn test_verbose_config_default_is_disabled() {
let config = VerboseConfig::default();
// All features should be disabled by default for backward compatibility
assert!(!config.enabled, "enabled should be false by default");
assert!(!config.log_residuals, "log_residuals should be false by default");
assert!(
!config.log_jacobian_condition,
"log_jacobian_condition should be false by default"
);
assert!(
!config.log_solver_switches,
"log_solver_switches should be false by default"
);
assert!(
!config.dump_final_state,
"dump_final_state should be false by default"
);
assert_eq!(
config.output_format,
VerboseOutputFormat::Both,
"output_format should default to Both"
);
}
#[test]
fn test_verbose_config_all_enabled() {
let config = VerboseConfig::all_enabled();
assert!(config.enabled, "enabled should be true");
assert!(config.log_residuals, "log_residuals should be true");
assert!(config.log_jacobian_condition, "log_jacobian_condition should be true");
assert!(config.log_solver_switches, "log_solver_switches should be true");
assert!(config.dump_final_state, "dump_final_state should be true");
}
#[test]
fn test_verbose_config_is_any_enabled() {
// All disabled
let config = VerboseConfig::default();
assert!(!config.is_any_enabled(), "no features should be enabled");
// Master switch off but features on
let config = VerboseConfig {
enabled: false,
log_residuals: true,
..Default::default()
};
assert!(
!config.is_any_enabled(),
"should be false when master switch is off"
);
// Master switch on but all features off
let config = VerboseConfig {
enabled: true,
..Default::default()
};
assert!(
!config.is_any_enabled(),
"should be false when no features are enabled"
);
// Master switch on and one feature on
let config = VerboseConfig {
enabled: true,
log_residuals: true,
..Default::default()
};
assert!(config.is_any_enabled(), "should be true when one feature is enabled");
}
// =============================================================================
// Task 2: IterationDiagnostics Tests
// =============================================================================
#[test]
fn test_iteration_diagnostics_creation() {
let diag = IterationDiagnostics {
iteration: 5,
residual_norm: 1e-4,
delta_norm: 1e-5,
alpha: Some(0.5),
jacobian_frozen: true,
jacobian_condition: Some(1e3),
};
assert_eq!(diag.iteration, 5);
assert!((diag.residual_norm - 1e-4).abs() < 1e-15);
assert!((diag.delta_norm - 1e-5).abs() < 1e-15);
assert_eq!(diag.alpha, Some(0.5));
assert!(diag.jacobian_frozen);
assert_eq!(diag.jacobian_condition, Some(1e3));
}
#[test]
fn test_iteration_diagnostics_without_alpha() {
// Sequential Substitution doesn't use line search
let diag = IterationDiagnostics {
iteration: 3,
residual_norm: 1e-3,
delta_norm: 1e-4,
alpha: None,
jacobian_frozen: false,
jacobian_condition: None,
};
assert_eq!(diag.alpha, None);
assert!(!diag.jacobian_frozen);
assert_eq!(diag.jacobian_condition, None);
}
// =============================================================================
// Task 3: Jacobian Condition Number Tests
// =============================================================================
#[test]
fn test_jacobian_condition_number_well_conditioned() {
// Identity-like matrix (well-conditioned)
let entries = vec![(0, 0, 2.0), (1, 1, 1.0)];
let j = JacobianMatrix::from_builder(&entries, 2, 2);
let cond = j.estimate_condition_number().expect("should compute condition number");
// Condition number of diagonal matrix is max/min diagonal entry
assert!(
cond < 10.0,
"Expected low condition number for well-conditioned matrix, got {}",
cond
);
}
#[test]
fn test_jacobian_condition_number_ill_conditioned() {
// Nearly singular matrix
let entries = vec![
(0, 0, 1.0),
(0, 1, 1.0),
(1, 0, 1.0),
(1, 1, 1.0000001),
];
let j = JacobianMatrix::from_builder(&entries, 2, 2);
let cond = j.estimate_condition_number().expect("should compute condition number");
assert!(
cond > 1e6,
"Expected high condition number for ill-conditioned matrix, got {}",
cond
);
}
#[test]
fn test_jacobian_condition_number_identity() {
// Identity matrix has condition number 1
let entries = vec![(0, 0, 1.0), (1, 1, 1.0), (2, 2, 1.0)];
let j = JacobianMatrix::from_builder(&entries, 3, 3);
let cond = j.estimate_condition_number().expect("should compute condition number");
assert!(
(cond - 1.0).abs() < 1e-10,
"Expected condition number 1 for identity matrix, got {}",
cond
);
}
#[test]
fn test_jacobian_condition_number_empty_matrix() {
// Empty matrix (0x0)
let j = JacobianMatrix::zeros(0, 0);
let cond = j.estimate_condition_number();
assert!(
cond.is_none(),
"Expected None for empty matrix"
);
}
// =============================================================================
// Task 4: SolverSwitchEvent Tests
// =============================================================================
#[test]
fn test_solver_switch_event_creation() {
let event = SolverSwitchEvent {
from_solver: SolverType::NewtonRaphson,
to_solver: SolverType::SequentialSubstitution,
reason: SwitchReason::Divergence,
iteration: 10,
residual_at_switch: 1e6,
};
assert_eq!(event.from_solver, SolverType::NewtonRaphson);
assert_eq!(event.to_solver, SolverType::SequentialSubstitution);
assert_eq!(event.reason, SwitchReason::Divergence);
assert_eq!(event.iteration, 10);
assert!((event.residual_at_switch - 1e6).abs() < 1e-6);
}
#[test]
fn test_solver_type_display() {
assert_eq!(
format!("{}", SolverType::NewtonRaphson),
"Newton-Raphson"
);
assert_eq!(
format!("{}", SolverType::SequentialSubstitution),
"Sequential Substitution"
);
}
#[test]
fn test_switch_reason_display() {
assert_eq!(format!("{}", SwitchReason::Divergence), "divergence detected");
assert_eq!(
format!("{}", SwitchReason::SlowConvergence),
"slow convergence"
);
assert_eq!(format!("{}", SwitchReason::UserRequested), "user requested");
assert_eq!(
format!("{}", SwitchReason::ReturnToNewton),
"returning to Newton after stabilization"
);
}
// =============================================================================
// Task 5: ConvergenceDiagnostics Tests
// =============================================================================
#[test]
fn test_convergence_diagnostics_default() {
let diag = ConvergenceDiagnostics::default();
assert_eq!(diag.iterations, 0);
assert!((diag.final_residual - 0.0).abs() < 1e-15);
assert!(!diag.converged);
assert!(diag.iteration_history.is_empty());
assert!(diag.solver_switches.is_empty());
assert!(diag.final_state.is_none());
assert!(diag.jacobian_condition_final.is_none());
assert_eq!(diag.timing_ms, 0);
assert!(diag.final_solver.is_none());
}
#[test]
fn test_convergence_diagnostics_with_capacity() {
let diag = ConvergenceDiagnostics::with_capacity(100);
// Capacity should be pre-allocated
assert!(diag.iteration_history.capacity() >= 100);
assert!(diag.iteration_history.is_empty());
}
#[test]
fn test_convergence_diagnostics_push_iteration() {
let mut diag = ConvergenceDiagnostics::new();
diag.push_iteration(IterationDiagnostics {
iteration: 0,
residual_norm: 1.0,
delta_norm: 0.0,
alpha: None,
jacobian_frozen: false,
jacobian_condition: None,
});
diag.push_iteration(IterationDiagnostics {
iteration: 1,
residual_norm: 0.5,
delta_norm: 0.5,
alpha: Some(1.0),
jacobian_frozen: false,
jacobian_condition: Some(100.0),
});
assert_eq!(diag.iteration_history.len(), 2);
assert_eq!(diag.iteration_history[0].iteration, 0);
assert_eq!(diag.iteration_history[1].iteration, 1);
}
#[test]
fn test_convergence_diagnostics_push_switch() {
let mut diag = ConvergenceDiagnostics::new();
diag.push_switch(SolverSwitchEvent {
from_solver: SolverType::NewtonRaphson,
to_solver: SolverType::SequentialSubstitution,
reason: SwitchReason::Divergence,
iteration: 5,
residual_at_switch: 1e10,
});
assert_eq!(diag.solver_switches.len(), 1);
assert_eq!(diag.solver_switches[0].iteration, 5);
}
#[test]
fn test_convergence_diagnostics_summary_converged() {
let mut diag = ConvergenceDiagnostics::new();
diag.iterations = 25;
diag.final_residual = 1e-8;
diag.best_residual = 1e-8;
diag.converged = true;
diag.timing_ms = 150;
diag.final_solver = Some(SolverType::NewtonRaphson);
diag.jacobian_condition_final = Some(1e4);
let summary = diag.summary();
assert!(summary.contains("Converged: YES"));
assert!(summary.contains("Iterations: 25"));
// The format uses {:.3e} which produces like "1.000e-08"
assert!(summary.contains("Final Residual:"));
assert!(summary.contains("Solver Switches: 0"));
assert!(summary.contains("Timing: 150 ms"));
assert!(summary.contains("Jacobian Condition:"));
assert!(summary.contains("Final Solver: Newton-Raphson"));
// Should NOT contain ill-conditioned warning
assert!(!summary.contains("WARNING"));
}
#[test]
fn test_convergence_diagnostics_summary_ill_conditioned() {
let mut diag = ConvergenceDiagnostics::new();
diag.iterations = 100;
diag.final_residual = 1e-2;
diag.best_residual = 1e-3;
diag.converged = false;
diag.timing_ms = 500;
diag.jacobian_condition_final = Some(1e12);
let summary = diag.summary();
assert!(summary.contains("Converged: NO"));
assert!(summary.contains("WARNING: ill-conditioned"));
}
#[test]
fn test_convergence_diagnostics_summary_with_switches() {
let mut diag = ConvergenceDiagnostics::new();
diag.iterations = 50;
diag.final_residual = 1e-6;
diag.best_residual = 1e-6;
diag.converged = true;
diag.timing_ms = 200;
diag.push_switch(SolverSwitchEvent {
from_solver: SolverType::NewtonRaphson,
to_solver: SolverType::SequentialSubstitution,
reason: SwitchReason::Divergence,
iteration: 10,
residual_at_switch: 1e10,
});
let summary = diag.summary();
assert!(summary.contains("Solver Switches: 1"));
}
// =============================================================================
// VerboseOutputFormat Tests
// =============================================================================
#[test]
fn test_verbose_output_format_default() {
let format = VerboseOutputFormat::default();
assert_eq!(format, VerboseOutputFormat::Both);
}
// =============================================================================
// JSON Serialization Tests (Story 7.4 - AC4)
// =============================================================================
#[test]
fn test_convergence_diagnostics_json_serialization() {
let mut diag = ConvergenceDiagnostics::new();
diag.iterations = 50;
diag.final_residual = 1e-6;
diag.best_residual = 1e-7;
diag.converged = true;
diag.timing_ms = 250;
diag.final_solver = Some(SolverType::NewtonRaphson);
diag.jacobian_condition_final = Some(1e5);
diag.push_iteration(IterationDiagnostics {
iteration: 1,
residual_norm: 1.0,
delta_norm: 0.5,
alpha: Some(1.0),
jacobian_frozen: false,
jacobian_condition: Some(100.0),
});
diag.push_switch(SolverSwitchEvent {
from_solver: SolverType::NewtonRaphson,
to_solver: SolverType::SequentialSubstitution,
reason: SwitchReason::Divergence,
iteration: 10,
residual_at_switch: 1e6,
});
// Test JSON serialization
let json = serde_json::to_string(&diag).expect("Should serialize to JSON");
assert!(json.contains("\"iterations\":50"));
assert!(json.contains("\"converged\":true"));
assert!(json.contains("\"NewtonRaphson\""));
assert!(json.contains("\"Divergence\""));
}
#[test]
fn test_convergence_diagnostics_round_trip() {
let mut diag = ConvergenceDiagnostics::new();
diag.iterations = 25;
diag.final_residual = 1e-8;
diag.converged = true;
diag.timing_ms = 100;
diag.final_solver = Some(SolverType::SequentialSubstitution);
// Serialize to JSON
let json = serde_json::to_string(&diag).expect("Should serialize");
// Deserialize back
let restored: ConvergenceDiagnostics =
serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(restored.iterations, 25);
assert!((restored.final_residual - 1e-8).abs() < 1e-20);
assert!(restored.converged);
assert_eq!(restored.timing_ms, 100);
assert_eq!(restored.final_solver, Some(SolverType::SequentialSubstitution));
}
#[test]
fn test_dump_diagnostics_json_format() {
let mut diag = ConvergenceDiagnostics::new();
diag.iterations = 10;
diag.final_residual = 1e-4;
diag.converged = false;
let json_output = diag.dump_diagnostics(VerboseOutputFormat::Json);
assert!(json_output.starts_with('{'));
// to_string_pretty adds spaces after colons
assert!(json_output.contains("\"iterations\"") && json_output.contains("10"));
assert!(json_output.contains("\"converged\"") && json_output.contains("false"));
}
#[test]
fn test_dump_diagnostics_log_format() {
let mut diag = ConvergenceDiagnostics::new();
diag.iterations = 10;
diag.final_residual = 1e-4;
diag.converged = false;
let log_output = diag.dump_diagnostics(VerboseOutputFormat::Log);
assert!(log_output.contains("Convergence Diagnostics Summary"));
assert!(log_output.contains("Converged: NO"));
assert!(log_output.contains("Iterations: 10"));
}