//! Solver strategy implementations for thermodynamic system solving. //! //! This module provides the concrete solver implementations that can be used //! via the [`Solver`] trait or the [`SolverStrategy`] enum for zero-cost //! static dispatch. //! //! # Available Strategies //! //! - [`NewtonRaphson`] โ€” Newton-Raphson solver with quadratic convergence //! - [`SequentialSubstitution`] โ€” Picard iteration solver, more robust for non-linear systems //! - [`FallbackSolver`] โ€” Intelligent fallback between Newton and Picard //! //! # Example //! //! ```rust //! use entropyk_solver::solver::{Solver, SolverStrategy}; //! use std::time::Duration; //! //! let solver = SolverStrategy::default() //! .with_timeout(Duration::from_millis(500)); //! ``` mod fallback; mod homotopy; mod newton_raphson; mod pseudo_transient; mod sequential_substitution; mod trust_region; pub use fallback::{FallbackConfig, FallbackSolver}; pub use homotopy::HomotopyConfig; pub use newton_raphson::NewtonConfig; pub use pseudo_transient::PtcConfig; pub use sequential_substitution::PicardConfig; pub use trust_region::TrustRegionConfig; use crate::solver::{ConvergedState, Solver, SolverError}; use crate::system::System; use std::time::Duration; /// Enum-based solver strategy dispatcher. /// /// Provides zero-cost static dispatch to the selected solver strategy via /// `match` (monomorphization), avoiding vtable overhead while still allowing /// runtime strategy selection. /// /// # Default /// /// `SolverStrategy::default()` returns `NewtonRaphson(NewtonConfig::default())`. /// /// # Example /// /// ```rust /// use entropyk_solver::solver::{Solver, SolverStrategy, PicardConfig}; /// use std::time::Duration; /// /// let strategy = SolverStrategy::SequentialSubstitution( /// PicardConfig { relaxation_factor: 0.3, ..Default::default() } /// ).with_timeout(Duration::from_secs(1)); /// ``` #[derive(Debug, Clone, PartialEq)] pub enum SolverStrategy { /// Newton-Raphson solver (quadratic convergence, requires Jacobian). NewtonRaphson(NewtonConfig), /// Sequential Substitution / Picard iteration (robust, no Jacobian needed). SequentialSubstitution(PicardConfig), } impl Default for SolverStrategy { /// Returns `SolverStrategy::NewtonRaphson(NewtonConfig::default())`. fn default() -> Self { SolverStrategy::NewtonRaphson(NewtonConfig::default()) } } impl Solver for SolverStrategy { fn solve(&mut self, system: &mut System) -> Result { tracing::info!( strategy = match self { SolverStrategy::NewtonRaphson(_) => "NewtonRaphson", SolverStrategy::SequentialSubstitution(_) => "SequentialSubstitution", }, "SolverStrategy::solve dispatching" ); let result = match self { SolverStrategy::NewtonRaphson(cfg) => cfg.solve(system), SolverStrategy::SequentialSubstitution(cfg) => cfg.solve(system), }; if let Ok(state) = &result { if state.is_converged() { // Post-solve validation checks. Components index the state slice by // global index, so pass the raw (แน, P, h)-strided vector directly // rather than through the stride-2 SystemState conversion (CM1.2). let state_slice: &[f64] = &state.state; system.check_mass_balance(state_slice)?; system.check_energy_balance(state_slice)?; } } result } fn with_timeout(self, timeout: Duration) -> Self { match self { SolverStrategy::NewtonRaphson(cfg) => { SolverStrategy::NewtonRaphson(cfg.with_timeout(timeout)) } SolverStrategy::SequentialSubstitution(cfg) => { SolverStrategy::SequentialSubstitution(cfg.with_timeout(timeout)) } } } } #[cfg(test)] mod tests { use super::*; use crate::system::System; use std::time::Duration; /// Verify that `SolverStrategy::default()` returns Newton-Raphson. #[test] fn test_solver_strategy_default_is_newton_raphson() { let strategy = SolverStrategy::default(); assert!( matches!(strategy, SolverStrategy::NewtonRaphson(_)), "Default strategy must be NewtonRaphson, got {:?}", strategy ); } /// Verify that the Newton-Raphson variant wraps a `NewtonConfig`. #[test] fn test_solver_strategy_newton_raphson_variant() { let strategy = SolverStrategy::NewtonRaphson(NewtonConfig::default()); match strategy { SolverStrategy::NewtonRaphson(cfg) => { assert_eq!(cfg.max_iterations, 100); assert!((cfg.tolerance - 1e-6).abs() < 1e-15); assert!(!cfg.line_search); assert!(cfg.timeout.is_none()); } other => panic!("Expected NewtonRaphson, got {:?}", other), } } /// Verify that the Sequential Substitution variant wraps a `PicardConfig`. #[test] fn test_solver_strategy_sequential_substitution_variant() { let strategy = SolverStrategy::SequentialSubstitution(PicardConfig::default()); match strategy { SolverStrategy::SequentialSubstitution(cfg) => { assert_eq!(cfg.max_iterations, 100); assert!((cfg.tolerance - 1e-6).abs() < 1e-15); assert!((cfg.relaxation_factor - 0.5).abs() < 1e-15); assert!(cfg.timeout.is_none()); } other => panic!("Expected SequentialSubstitution, got {:?}", other), } } /// Verify that `with_timeout` on `SolverStrategy::NewtonRaphson` propagates to inner config. #[test] fn test_solver_strategy_newton_with_timeout() { let timeout = Duration::from_millis(500); let strategy = SolverStrategy::default().with_timeout(timeout); match strategy { SolverStrategy::NewtonRaphson(cfg) => { assert_eq!(cfg.timeout, Some(timeout)); } other => panic!("Expected NewtonRaphson after with_timeout, got {:?}", other), } } /// Verify that `with_timeout` on `SolverStrategy::SequentialSubstitution` propagates. #[test] fn test_solver_strategy_picard_with_timeout() { let timeout = Duration::from_secs(1); let strategy = SolverStrategy::SequentialSubstitution(PicardConfig::default()).with_timeout(timeout); match strategy { SolverStrategy::SequentialSubstitution(cfg) => { assert_eq!(cfg.timeout, Some(timeout)); } other => panic!( "Expected SequentialSubstitution after with_timeout, got {:?}", other ), } } /// Verify that `SolverStrategy::NewtonRaphson` dispatches to the Newton implementation. #[test] fn test_solver_strategy_newton_dispatch_reaches_stub() { let mut strategy = SolverStrategy::default(); // NewtonRaphson let mut system = System::new(); system.finalize().unwrap(); let result = strategy.solve(&mut system); // Empty system should return InvalidSystem assert!( result.is_err(), "Newton solver must return Err for empty system" ); match result { Err(SolverError::InvalidSystem { ref message }) => { assert!( message.contains("Empty") || message.contains("no state"), "Newton dispatch must detect empty system, got: {}", message ); } other => panic!("Expected InvalidSystem from Newton solver, got {:?}", other), } } /// Verify that `SolverStrategy::SequentialSubstitution` dispatches to the Picard implementation. #[test] fn test_solver_strategy_picard_dispatch_reaches_implementation() { let mut strategy = SolverStrategy::SequentialSubstitution(PicardConfig::default()); let mut system = System::new(); system.finalize().unwrap(); let result = strategy.solve(&mut system); assert!( result.is_err(), "Picard solver must return Err for empty system" ); match result { Err(SolverError::InvalidSystem { ref message }) => { assert!( message.contains("Empty") || message.contains("no state"), "Picard dispatch must detect empty system, got: {}", message ); } other => panic!("Expected InvalidSystem from Picard solver, got {:?}", other), } } }