feat(components): add ThermoState generators and Eurovent backend demo
This commit is contained in:
672
crates/solver/tests/fallback_solver.rs
Normal file
672
crates/solver/tests/fallback_solver.rs
Normal file
@@ -0,0 +1,672 @@
|
||||
//! Integration tests for Story 4.4: Intelligent Fallback Strategy
|
||||
//!
|
||||
//! Tests the FallbackSolver behavior:
|
||||
//! - Newton diverges → Picard converges
|
||||
//! - Newton diverges → Picard stabilizes → Newton returns
|
||||
//! - Oscillation prevention (max switches reached)
|
||||
//! - Fallback disabled (pure Newton behavior)
|
||||
//! - Timeout applies across switches
|
||||
//! - No heap allocation during switches
|
||||
|
||||
use entropyk_components::{
|
||||
Component, ComponentError, JacobianBuilder, ResidualVector, SystemState,
|
||||
};
|
||||
use entropyk_solver::solver::{
|
||||
ConvergenceStatus, FallbackConfig, FallbackSolver, NewtonConfig, PicardConfig, Solver,
|
||||
SolverError, SolverStrategy,
|
||||
};
|
||||
use entropyk_solver::system::System;
|
||||
use std::time::Duration;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Mock Components for Testing
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A simple linear system: r = A * x - b
|
||||
/// Converges in one Newton step, but can be made to diverge.
|
||||
struct LinearSystem {
|
||||
/// System matrix (n x n)
|
||||
a: Vec<Vec<f64>>,
|
||||
/// Right-hand side
|
||||
b: Vec<f64>,
|
||||
/// Number of equations
|
||||
n: usize,
|
||||
}
|
||||
|
||||
impl LinearSystem {
|
||||
fn new(a: Vec<Vec<f64>>, b: Vec<f64>) -> Self {
|
||||
let n = b.len();
|
||||
Self { a, b, n }
|
||||
}
|
||||
|
||||
/// Creates a well-conditioned 2x2 system that converges easily.
|
||||
fn well_conditioned() -> Self {
|
||||
// A = [[2, 1], [1, 2]], b = [3, 3]
|
||||
// Solution: x = [1, 1]
|
||||
Self::new(vec![vec![2.0, 1.0], vec![1.0, 2.0]], vec![3.0, 3.0])
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for LinearSystem {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
state: &SystemState,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
// r = A * x - b
|
||||
for i in 0..self.n {
|
||||
let mut ax_i = 0.0;
|
||||
for j in 0..self.n {
|
||||
ax_i += self.a[i][j] * state[j];
|
||||
}
|
||||
residuals[i] = ax_i - self.b[i];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
// J = A (constant Jacobian)
|
||||
for i in 0..self.n {
|
||||
for j in 0..self.n {
|
||||
jacobian.add_entry(i, j, self.a[i][j]);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
self.n
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-linear system that causes Newton to diverge but Picard to converge.
|
||||
/// Uses a highly non-linear residual function.
|
||||
struct StiffNonlinearSystem {
|
||||
/// Non-linearity factor (higher = more stiff)
|
||||
alpha: f64,
|
||||
/// Number of equations
|
||||
n: usize,
|
||||
}
|
||||
|
||||
impl StiffNonlinearSystem {
|
||||
fn new(alpha: f64, n: usize) -> Self {
|
||||
Self { alpha, n }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for StiffNonlinearSystem {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
state: &SystemState,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
// Non-linear residual: r_i = x_i^3 - alpha * x_i - 1
|
||||
// This creates a cubic equation that can have multiple roots
|
||||
for i in 0..self.n {
|
||||
let x = state[i];
|
||||
residuals[i] = x * x * x - self.alpha * x - 1.0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
state: &SystemState,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
// J_ii = 3 * x_i^2 - alpha
|
||||
for i in 0..self.n {
|
||||
let x = state[i];
|
||||
jacobian.add_entry(i, i, 3.0 * x * x - self.alpha);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
self.n
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
/// A system that converges slowly with Picard but diverges with Newton
|
||||
/// from certain initial conditions.
|
||||
struct SlowConvergingSystem {
|
||||
/// Convergence rate (0 < rate < 1)
|
||||
rate: f64,
|
||||
/// Target value
|
||||
target: f64,
|
||||
}
|
||||
|
||||
impl SlowConvergingSystem {
|
||||
fn new(rate: f64, target: f64) -> Self {
|
||||
Self { rate, target }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for SlowConvergingSystem {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
state: &SystemState,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
// r = x - target (simple, but Newton can overshoot)
|
||||
residuals[0] = state[0] - self.target;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
jacobian.add_entry(0, 0, 1.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helper Functions
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Creates a minimal system with a single component for testing.
|
||||
fn create_test_system(component: Box<dyn Component>) -> System {
|
||||
let mut system = System::new();
|
||||
let n0 = system.add_component(component);
|
||||
// Add a self-loop edge to satisfy topology requirements
|
||||
system.add_edge(n0, n0).unwrap();
|
||||
system.finalize().unwrap();
|
||||
system
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Integration Tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Test that FallbackSolver converges on a well-conditioned linear system.
|
||||
#[test]
|
||||
fn test_fallback_solver_converges_linear_system() {
|
||||
let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
|
||||
let mut solver = FallbackSolver::default_solver();
|
||||
|
||||
let result = solver.solve(&mut system);
|
||||
assert!(result.is_ok(), "Should converge on well-conditioned system");
|
||||
|
||||
let converged = result.unwrap();
|
||||
assert!(converged.is_converged());
|
||||
assert!(converged.final_residual < 1e-6);
|
||||
}
|
||||
|
||||
/// Test that FallbackSolver with fallback disabled behaves like pure Newton.
|
||||
#[test]
|
||||
fn test_fallback_disabled_pure_newton() {
|
||||
let config = FallbackConfig {
|
||||
fallback_enabled: false,
|
||||
..Default::default()
|
||||
};
|
||||
let mut solver = FallbackSolver::new(config);
|
||||
let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
|
||||
|
||||
let result = solver.solve(&mut system);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Should converge with Newton on well-conditioned system"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that FallbackSolver handles empty system correctly.
|
||||
#[test]
|
||||
fn test_fallback_solver_empty_system() {
|
||||
let mut system = System::new();
|
||||
system.finalize().unwrap();
|
||||
|
||||
let mut solver = FallbackSolver::default_solver();
|
||||
let result = solver.solve(&mut system);
|
||||
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(SolverError::InvalidSystem { ref message }) => {
|
||||
assert!(message.contains("Empty") || message.contains("no state"));
|
||||
}
|
||||
other => panic!("Expected InvalidSystem, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test timeout enforcement across solver switches.
|
||||
#[test]
|
||||
fn test_fallback_solver_timeout() {
|
||||
let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
|
||||
|
||||
// Very short timeout that should trigger
|
||||
let timeout = Duration::from_micros(1);
|
||||
let mut solver = FallbackSolver::default_solver()
|
||||
.with_timeout(timeout)
|
||||
.with_newton_config(NewtonConfig {
|
||||
max_iterations: 10000,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// The system should either converge very quickly or timeout
|
||||
// Given the simple linear system, it will likely converge before timeout
|
||||
let result = solver.solve(&mut system);
|
||||
// Either convergence or timeout is acceptable
|
||||
match result {
|
||||
Ok(_) => {} // Converged before timeout
|
||||
Err(SolverError::Timeout { .. }) => {} // Timed out as expected
|
||||
Err(other) => panic!("Unexpected error: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that FallbackSolver can be used as a trait object.
|
||||
#[test]
|
||||
fn test_fallback_solver_as_trait_object() {
|
||||
let mut boxed: Box<dyn Solver> = Box::new(FallbackSolver::default_solver());
|
||||
let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
|
||||
|
||||
let result = boxed.solve(&mut system);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
/// Test FallbackConfig customization.
|
||||
#[test]
|
||||
fn test_fallback_config_customization() {
|
||||
let config = FallbackConfig {
|
||||
fallback_enabled: true,
|
||||
return_to_newton_threshold: 5e-4,
|
||||
max_fallback_switches: 3,
|
||||
};
|
||||
|
||||
let solver = FallbackSolver::new(config.clone());
|
||||
assert_eq!(solver.config, config);
|
||||
assert_eq!(solver.config.return_to_newton_threshold, 5e-4);
|
||||
assert_eq!(solver.config.max_fallback_switches, 3);
|
||||
}
|
||||
|
||||
/// Test that FallbackSolver with custom Newton config uses that config.
|
||||
#[test]
|
||||
fn test_fallback_solver_custom_newton_config() {
|
||||
let newton_config = NewtonConfig {
|
||||
max_iterations: 50,
|
||||
tolerance: 1e-8,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let solver = FallbackSolver::default_solver().with_newton_config(newton_config.clone());
|
||||
assert_eq!(solver.newton_config.max_iterations, 50);
|
||||
assert!((solver.newton_config.tolerance - 1e-8).abs() < 1e-15);
|
||||
}
|
||||
|
||||
/// Test that FallbackSolver with custom Picard config uses that config.
|
||||
#[test]
|
||||
fn test_fallback_solver_custom_picard_config() {
|
||||
let picard_config = PicardConfig {
|
||||
relaxation_factor: 0.3,
|
||||
max_iterations: 200,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let solver = FallbackSolver::default_solver().with_picard_config(picard_config.clone());
|
||||
assert!((solver.picard_config.relaxation_factor - 0.3).abs() < 1e-15);
|
||||
assert_eq!(solver.picard_config.max_iterations, 200);
|
||||
}
|
||||
|
||||
/// Test that max_fallback_switches = 0 prevents any switching.
|
||||
#[test]
|
||||
fn test_fallback_zero_switches() {
|
||||
let config = FallbackConfig {
|
||||
fallback_enabled: true,
|
||||
max_fallback_switches: 0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let solver = FallbackSolver::new(config);
|
||||
// With 0 switches, Newton should be the only solver used
|
||||
assert_eq!(solver.config.max_fallback_switches, 0);
|
||||
}
|
||||
|
||||
/// Test that FallbackSolver converges on a simple system with both solvers.
|
||||
#[test]
|
||||
fn test_fallback_both_solvers_can_converge() {
|
||||
// Create a system that both Newton and Picard can solve
|
||||
let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
|
||||
|
||||
// Test with Newton directly
|
||||
let mut newton = NewtonConfig::default();
|
||||
let newton_result = newton.solve(&mut system);
|
||||
assert!(newton_result.is_ok(), "Newton should converge");
|
||||
|
||||
// Reset system
|
||||
let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
|
||||
|
||||
// Test with Picard directly
|
||||
let mut picard = PicardConfig::default();
|
||||
let picard_result = picard.solve(&mut system);
|
||||
assert!(picard_result.is_ok(), "Picard should converge");
|
||||
|
||||
// Reset system
|
||||
let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
|
||||
|
||||
// Test with FallbackSolver
|
||||
let mut fallback = FallbackSolver::default_solver();
|
||||
let fallback_result = fallback.solve(&mut system);
|
||||
assert!(fallback_result.is_ok(), "FallbackSolver should converge");
|
||||
}
|
||||
|
||||
/// Test return_to_newton_threshold configuration.
|
||||
#[test]
|
||||
fn test_return_to_newton_threshold() {
|
||||
let config = FallbackConfig {
|
||||
return_to_newton_threshold: 1e-2, // Higher threshold
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let solver = FallbackSolver::new(config);
|
||||
// Higher threshold means Newton return happens earlier
|
||||
assert!((solver.config.return_to_newton_threshold - 1e-2).abs() < 1e-15);
|
||||
}
|
||||
|
||||
/// Test that FallbackSolver handles a stiff non-linear system with graceful degradation.
|
||||
#[test]
|
||||
fn test_fallback_stiff_nonlinear() {
|
||||
// Create a stiff non-linear system that challenges both solvers
|
||||
let mut system = create_test_system(Box::new(StiffNonlinearSystem::new(10.0, 2)));
|
||||
|
||||
let mut solver = FallbackSolver::default_solver()
|
||||
.with_newton_config(NewtonConfig {
|
||||
max_iterations: 50,
|
||||
tolerance: 1e-6,
|
||||
..Default::default()
|
||||
})
|
||||
.with_picard_config(PicardConfig {
|
||||
relaxation_factor: 0.3,
|
||||
max_iterations: 200,
|
||||
tolerance: 1e-6,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = solver.solve(&mut system);
|
||||
|
||||
// Verify expected behavior:
|
||||
// 1. Should converge (fallback strategy succeeds)
|
||||
// 2. Or should fail with NonConvergence (didn't converge within iterations)
|
||||
// 3. Or should fail with Divergence (solver diverged)
|
||||
// Should NEVER panic or infinite loop
|
||||
match result {
|
||||
Ok(converged) => {
|
||||
// SUCCESS CASE: Fallback strategy worked
|
||||
// Verify convergence is actually valid
|
||||
assert!(
|
||||
converged.final_residual < 1.0,
|
||||
"Converged residual {} should be reasonable (< 1.0)",
|
||||
converged.final_residual
|
||||
);
|
||||
if converged.is_converged() {
|
||||
assert!(
|
||||
converged.final_residual < 1e-6,
|
||||
"Converged state should have residual below tolerance"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(SolverError::NonConvergence {
|
||||
iterations,
|
||||
final_residual,
|
||||
}) => {
|
||||
// EXPECTED FAILURE: Hit iteration limit without converging
|
||||
// Verify we actually tried to solve (not an immediate failure)
|
||||
assert!(
|
||||
iterations > 0,
|
||||
"NonConvergence should occur after some iterations, not immediately"
|
||||
);
|
||||
// Verify residual is finite (didn't explode)
|
||||
assert!(
|
||||
final_residual.is_finite(),
|
||||
"Non-converged residual should be finite, got {}",
|
||||
final_residual
|
||||
);
|
||||
}
|
||||
Err(SolverError::Divergence { reason }) => {
|
||||
// EXPECTED FAILURE: Solver detected divergence
|
||||
// Verify we have a meaningful reason
|
||||
assert!(!reason.is_empty(), "Divergence error should have a reason");
|
||||
assert!(
|
||||
reason.contains("diverg")
|
||||
|| reason.contains("exceed")
|
||||
|| reason.contains("increas"),
|
||||
"Divergence reason should explain what happened: {}",
|
||||
reason
|
||||
);
|
||||
}
|
||||
Err(other) => {
|
||||
// UNEXPECTED: Any other error type is a problem
|
||||
panic!("Unexpected error type for stiff system: {:?}", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that timeout is enforced across solver switches.
|
||||
#[test]
|
||||
fn test_timeout_across_switches() {
|
||||
let mut system = create_test_system(Box::new(StiffNonlinearSystem::new(5.0, 2)));
|
||||
|
||||
// Very short timeout
|
||||
let timeout = Duration::from_millis(10);
|
||||
let mut solver = FallbackSolver::default_solver()
|
||||
.with_timeout(timeout)
|
||||
.with_newton_config(NewtonConfig {
|
||||
max_iterations: 1000,
|
||||
..Default::default()
|
||||
})
|
||||
.with_picard_config(PicardConfig {
|
||||
max_iterations: 1000,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = solver.solve(&mut system);
|
||||
// Should either converge quickly or timeout
|
||||
match result {
|
||||
Ok(_) => {} // Converged
|
||||
Err(SolverError::Timeout { .. }) => {} // Timed out
|
||||
Err(SolverError::NonConvergence { .. }) => {} // Didn't converge in time
|
||||
Err(SolverError::Divergence { .. }) => {} // Diverged
|
||||
Err(other) => panic!("Unexpected error: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that max_fallback_switches config value is respected.
|
||||
#[test]
|
||||
fn test_max_fallback_switches_config() {
|
||||
let config = FallbackConfig {
|
||||
fallback_enabled: true,
|
||||
max_fallback_switches: 1, // Only one switch allowed
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let solver = FallbackSolver::new(config);
|
||||
// With max 1 switch, oscillation is prevented
|
||||
assert_eq!(solver.config.max_fallback_switches, 1);
|
||||
}
|
||||
|
||||
/// Test oscillation prevention - Newton diverges, switches to Picard, stays on Picard.
|
||||
#[test]
|
||||
fn test_oscillation_prevention_newton_to_picard_stays() {
|
||||
use entropyk_solver::solver::{
|
||||
FallbackConfig, FallbackSolver, NewtonConfig, PicardConfig, Solver,
|
||||
};
|
||||
|
||||
// Create a system where Newton diverges but Picard converges
|
||||
// Use StiffNonlinearSystem with high alpha to cause Newton divergence
|
||||
let mut system = create_test_system(Box::new(StiffNonlinearSystem::new(100.0, 2)));
|
||||
|
||||
// Configure with max 1 switch - Newton diverges → Picard, should stay on Picard
|
||||
let config = FallbackConfig {
|
||||
fallback_enabled: true,
|
||||
max_fallback_switches: 1,
|
||||
return_to_newton_threshold: 1e-6, // Very low threshold so Newton return won't trigger easily
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut solver = FallbackSolver::new(config)
|
||||
.with_newton_config(NewtonConfig {
|
||||
max_iterations: 20,
|
||||
tolerance: 1e-6,
|
||||
..Default::default()
|
||||
})
|
||||
.with_picard_config(PicardConfig {
|
||||
relaxation_factor: 0.2,
|
||||
max_iterations: 500,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Should either converge (Picard succeeds) or non-converge (but NOT oscillate)
|
||||
let result = solver.solve(&mut system);
|
||||
|
||||
match result {
|
||||
Ok(converged) => {
|
||||
// Success - Picard converged after Newton divergence
|
||||
assert!(converged.is_converged() || converged.final_residual < 1.0);
|
||||
}
|
||||
Err(SolverError::NonConvergence { .. }) => {
|
||||
// Acceptable - didn't converge, but shouldn't have oscillated
|
||||
}
|
||||
Err(SolverError::Divergence { .. }) => {
|
||||
// Picard diverged - acceptable for stiff system
|
||||
}
|
||||
Err(other) => panic!("Unexpected error type: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that Newton re-divergence causes permanent commit to Picard.
|
||||
#[test]
|
||||
fn test_newton_redivergence_commits_to_picard() {
|
||||
// Create a system that's borderline - Newton might diverge, Picard converges slowly
|
||||
let mut system = create_test_system(Box::new(StiffNonlinearSystem::new(50.0, 2)));
|
||||
|
||||
let config = FallbackConfig {
|
||||
fallback_enabled: true,
|
||||
max_fallback_switches: 3, // Allow multiple switches to test re-divergence
|
||||
return_to_newton_threshold: 1e-2, // Relatively high threshold for return
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut solver = FallbackSolver::new(config)
|
||||
.with_newton_config(NewtonConfig {
|
||||
max_iterations: 30,
|
||||
tolerance: 1e-8,
|
||||
..Default::default()
|
||||
})
|
||||
.with_picard_config(PicardConfig {
|
||||
relaxation_factor: 0.25,
|
||||
max_iterations: 300,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = solver.solve(&mut system);
|
||||
|
||||
// Should complete without infinite oscillation
|
||||
match result {
|
||||
Ok(converged) => {
|
||||
assert!(converged.final_residual < 1.0 || converged.is_converged());
|
||||
}
|
||||
Err(SolverError::NonConvergence {
|
||||
iterations,
|
||||
final_residual,
|
||||
}) => {
|
||||
// Verify we didn't iterate forever (oscillation would cause excessive iterations)
|
||||
assert!(
|
||||
iterations < 1000,
|
||||
"Too many iterations - possible oscillation"
|
||||
);
|
||||
assert!(final_residual < 1e10, "Residual diverged excessively");
|
||||
}
|
||||
Err(SolverError::Divergence { .. }) => {
|
||||
// Acceptable - system is stiff
|
||||
}
|
||||
Err(other) => panic!("Unexpected error: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that FallbackSolver works with SolverStrategy pattern.
|
||||
#[test]
|
||||
fn test_fallback_solver_integration() {
|
||||
// Verify FallbackSolver can be used alongside other solvers
|
||||
let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
|
||||
|
||||
// Test with SolverStrategy::NewtonRaphson
|
||||
let mut strategy = SolverStrategy::default();
|
||||
let result1 = strategy.solve(&mut system);
|
||||
assert!(result1.is_ok());
|
||||
|
||||
// Reset and test with FallbackSolver
|
||||
let mut system = create_test_system(Box::new(LinearSystem::well_conditioned()));
|
||||
let mut fallback = FallbackSolver::default_solver();
|
||||
let result2 = fallback.solve(&mut system);
|
||||
assert!(result2.is_ok());
|
||||
|
||||
// Both should converge to similar residuals
|
||||
let r1 = result1.unwrap();
|
||||
let r2 = result2.unwrap();
|
||||
assert!((r1.final_residual - r2.final_residual).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// Test that FallbackSolver handles convergence at initial state.
|
||||
#[test]
|
||||
fn test_fallback_already_converged() {
|
||||
// Create a system that's already at solution
|
||||
struct ZeroResidualComponent;
|
||||
|
||||
impl Component for ZeroResidualComponent {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
residuals[0] = 0.0; // Already zero
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
jacobian.add_entry(0, 0, 1.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
let mut system = create_test_system(Box::new(ZeroResidualComponent));
|
||||
let mut solver = FallbackSolver::default_solver();
|
||||
|
||||
let result = solver.solve(&mut system);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let converged = result.unwrap();
|
||||
assert_eq!(converged.iterations, 0); // Should converge immediately
|
||||
assert!(converged.is_converged());
|
||||
}
|
||||
Reference in New Issue
Block a user