Some checks failed
CI / check (push) Has been cancelled
Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop. Co-authored-by: Cursor <cursoragent@cursor.com>
240 lines
8.6 KiB
Rust
240 lines
8.6 KiB
Rust
//! 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<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. 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),
|
|
}
|
|
}
|
|
}
|