673 lines
22 KiB
Rust
673 lines
22 KiB
Rust
//! 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());
|
|
}
|