chore: sync project state and current artifacts
This commit is contained in:
232
crates/solver/src/strategies/mod.rs
Normal file
232
crates/solver/src/strategies/mod.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
//! 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 newton_raphson;
|
||||
mod sequential_substitution;
|
||||
|
||||
pub use fallback::{FallbackConfig, FallbackSolver};
|
||||
pub use newton_raphson::NewtonConfig;
|
||||
pub use sequential_substitution::PicardConfig;
|
||||
|
||||
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<ConvergedState, SolverError> {
|
||||
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
|
||||
// Convert Vec<f64> to SystemState for validation methods
|
||||
let system_state: entropyk_components::SystemState = state.state.clone().into();
|
||||
system.check_mass_balance(&system_state)?;
|
||||
system.check_energy_balance(&system_state)?;
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user