//! 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")); }