chore: sync project state and current artifacts

This commit is contained in:
Sepehr
2026-02-22 23:27:31 +01:00
parent 1b6415776e
commit dd77089b22
232 changed files with 37056 additions and 4296 deletions

View 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),
}
}
}