feat(python): implement python bindings for all components and solvers
This commit is contained in:
@@ -8,8 +8,10 @@
|
||||
//! - AC #5: Divergence detection
|
||||
//! - AC #6: Pre-allocated buffers
|
||||
|
||||
use entropyk_solver::{ConvergenceStatus, JacobianMatrix, NewtonConfig, Solver, SolverError, System};
|
||||
use approx::assert_relative_eq;
|
||||
use entropyk_solver::{
|
||||
ConvergenceStatus, JacobianMatrix, NewtonConfig, Solver, SolverError, System,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -17,20 +19,20 @@ use std::time::Duration;
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Test that Newton-Raphson exhibits quadratic convergence on a simple system.
|
||||
///
|
||||
///
|
||||
/// For a well-conditioned system near the solution, the residual norm should
|
||||
/// decrease quadratically (roughly square each iteration).
|
||||
#[test]
|
||||
fn test_quadratic_convergence_simple_system() {
|
||||
// We'll test the Jacobian solve directly since we need a mock system
|
||||
// For J = [[2, 0], [0, 3]] and r = [2, 3], solution is x = [-1, -1]
|
||||
|
||||
|
||||
let entries = vec![(0, 0, 2.0), (1, 1, 3.0)];
|
||||
let jacobian = JacobianMatrix::from_builder(&entries, 2, 2);
|
||||
|
||||
|
||||
let residuals = vec![2.0, 3.0];
|
||||
let delta = jacobian.solve(&residuals).expect("non-singular");
|
||||
|
||||
|
||||
// J·Δx = -r => Δx = -J^{-1}·r
|
||||
assert_relative_eq!(delta[0], -1.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(delta[1], -1.0, epsilon = 1e-10);
|
||||
@@ -43,19 +45,19 @@ fn test_solve_2x2_linear_system() {
|
||||
// Solution: Δx = -J^{-1}·r
|
||||
let entries = vec![(0, 0, 4.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 3.0)];
|
||||
let jacobian = JacobianMatrix::from_builder(&entries, 2, 2);
|
||||
|
||||
|
||||
let residuals = vec![1.0, 2.0];
|
||||
let delta = jacobian.solve(&residuals).expect("non-singular");
|
||||
|
||||
|
||||
// Verify: J·Δx = -r
|
||||
let j00 = 4.0;
|
||||
let j01 = 1.0;
|
||||
let j10 = 1.0;
|
||||
let j11 = 3.0;
|
||||
|
||||
|
||||
let computed_r0 = j00 * delta[0] + j01 * delta[1];
|
||||
let computed_r1 = j10 * delta[0] + j11 * delta[1];
|
||||
|
||||
|
||||
assert_relative_eq!(computed_r0, -1.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(computed_r1, -2.0, epsilon = 1e-10);
|
||||
}
|
||||
@@ -66,13 +68,13 @@ fn test_diagonal_system_one_iteration() {
|
||||
// For a diagonal Jacobian, Newton should converge in 1 iteration
|
||||
// J = [[a, 0], [0, b]], r = [c, d]
|
||||
// Δx = [-c/a, -d/b]
|
||||
|
||||
|
||||
let entries = vec![(0, 0, 5.0), (1, 1, 7.0)];
|
||||
let jacobian = JacobianMatrix::from_builder(&entries, 2, 2);
|
||||
|
||||
|
||||
let residuals = vec![10.0, 21.0];
|
||||
let delta = jacobian.solve(&residuals).expect("non-singular");
|
||||
|
||||
|
||||
assert_relative_eq!(delta[0], -2.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(delta[1], -3.0, epsilon = 1e-10);
|
||||
}
|
||||
@@ -90,7 +92,7 @@ fn test_line_search_configuration() {
|
||||
line_search_max_backtracks: 20,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
assert!(cfg.line_search);
|
||||
assert_relative_eq!(cfg.line_search_armijo_c, 1e-4);
|
||||
assert_eq!(cfg.line_search_max_backtracks, 20);
|
||||
@@ -107,7 +109,7 @@ fn test_line_search_disabled_by_default() {
|
||||
#[test]
|
||||
fn test_armijo_constant_range() {
|
||||
let cfg = NewtonConfig::default();
|
||||
|
||||
|
||||
// Armijo constant should be in (0, 0.5) for typical line search
|
||||
assert!(cfg.line_search_armijo_c > 0.0);
|
||||
assert!(cfg.line_search_armijo_c < 0.5);
|
||||
@@ -124,7 +126,7 @@ fn test_numerical_jacobian_configuration() {
|
||||
use_numerical_jacobian: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
assert!(cfg.use_numerical_jacobian);
|
||||
}
|
||||
|
||||
@@ -141,18 +143,18 @@ fn test_numerical_jacobian_linear_function() {
|
||||
// r[0] = 2*x0 + 3*x1
|
||||
// r[1] = x0 - 2*x1
|
||||
// J = [[2, 3], [1, -2]]
|
||||
|
||||
|
||||
let state = vec![1.0, 2.0];
|
||||
let residuals = vec![2.0 * state[0] + 3.0 * state[1], state[0] - 2.0 * state[1]];
|
||||
|
||||
|
||||
let compute_residuals = |s: &[f64], r: &mut [f64]| {
|
||||
r[0] = 2.0 * s[0] + 3.0 * s[1];
|
||||
r[1] = s[0] - 2.0 * s[1];
|
||||
Ok(())
|
||||
};
|
||||
|
||||
|
||||
let j_num = JacobianMatrix::numerical(compute_residuals, &state, &residuals, 1e-8).unwrap();
|
||||
|
||||
|
||||
// Check against analytical Jacobian
|
||||
assert_relative_eq!(j_num.get(0, 0).unwrap(), 2.0, epsilon = 1e-5);
|
||||
assert_relative_eq!(j_num.get(0, 1).unwrap(), 3.0, epsilon = 1e-5);
|
||||
@@ -166,24 +168,24 @@ fn test_numerical_jacobian_nonlinear_function() {
|
||||
// r[0] = x0^2 + x1
|
||||
// r[1] = sin(x0) + cos(x1)
|
||||
// J = [[2*x0, 1], [cos(x0), -sin(x1)]]
|
||||
|
||||
|
||||
let state = vec![0.5_f64, 1.0_f64];
|
||||
let residuals = vec![state[0].powi(2) + state[1], state[0].sin() + state[1].cos()];
|
||||
|
||||
|
||||
let compute_residuals = |s: &[f64], r: &mut [f64]| {
|
||||
r[0] = s[0].powi(2) + s[1];
|
||||
r[1] = s[0].sin() + s[1].cos();
|
||||
Ok(())
|
||||
};
|
||||
|
||||
|
||||
let j_num = JacobianMatrix::numerical(compute_residuals, &state, &residuals, 1e-8).unwrap();
|
||||
|
||||
|
||||
// Analytical values
|
||||
let j00 = 2.0 * state[0]; // 1.0
|
||||
let j01 = 1.0;
|
||||
let j10 = state[0].cos();
|
||||
let j11 = -state[1].sin();
|
||||
|
||||
|
||||
assert_relative_eq!(j_num.get(0, 0).unwrap(), j00, epsilon = 1e-5);
|
||||
assert_relative_eq!(j_num.get(0, 1).unwrap(), j01, epsilon = 1e-5);
|
||||
assert_relative_eq!(j_num.get(1, 0).unwrap(), j10, epsilon = 1e-5);
|
||||
@@ -199,7 +201,7 @@ fn test_numerical_jacobian_nonlinear_function() {
|
||||
fn test_timeout_configuration() {
|
||||
let timeout = Duration::from_millis(500);
|
||||
let cfg = NewtonConfig::default().with_timeout(timeout);
|
||||
|
||||
|
||||
assert_eq!(cfg.timeout, Some(timeout));
|
||||
}
|
||||
|
||||
@@ -215,7 +217,7 @@ fn test_no_timeout_by_default() {
|
||||
fn test_timeout_error_contains_duration() {
|
||||
let err = SolverError::Timeout { timeout_ms: 1234 };
|
||||
let msg = err.to_string();
|
||||
|
||||
|
||||
assert!(msg.contains("1234"));
|
||||
}
|
||||
|
||||
@@ -230,7 +232,7 @@ fn test_divergence_threshold_configuration() {
|
||||
divergence_threshold: 1e8,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
assert_relative_eq!(cfg.divergence_threshold, 1e8);
|
||||
}
|
||||
|
||||
@@ -248,7 +250,7 @@ fn test_divergence_error_contains_reason() {
|
||||
reason: "Residual increased for 3 consecutive iterations".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
|
||||
|
||||
assert!(msg.contains("Residual increased"));
|
||||
assert!(msg.contains("3 consecutive"));
|
||||
}
|
||||
@@ -260,7 +262,7 @@ fn test_divergence_error_threshold_exceeded() {
|
||||
reason: "Residual norm 1e12 exceeds threshold 1e10".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
|
||||
|
||||
assert!(msg.contains("exceeds threshold"));
|
||||
}
|
||||
|
||||
@@ -276,7 +278,7 @@ fn test_preallocated_buffers_empty_system() {
|
||||
|
||||
let mut solver = NewtonConfig::default();
|
||||
let result = solver.solve(&mut sys);
|
||||
|
||||
|
||||
// Should return error without panic
|
||||
assert!(result.is_err());
|
||||
}
|
||||
@@ -299,7 +301,7 @@ fn test_preallocated_buffers_all_configs() {
|
||||
divergence_threshold: 1e8,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
let result = solver.solve(&mut sys);
|
||||
assert!(result.is_err()); // Empty system, but no panic
|
||||
}
|
||||
@@ -314,10 +316,10 @@ fn test_singular_jacobian_returns_none() {
|
||||
// Singular matrix: [[1, 1], [1, 1]]
|
||||
let entries = vec![(0, 0, 1.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 1.0)];
|
||||
let jacobian = JacobianMatrix::from_builder(&entries, 2, 2);
|
||||
|
||||
|
||||
let residuals = vec![1.0, 2.0];
|
||||
let result = jacobian.solve(&residuals);
|
||||
|
||||
|
||||
assert!(result.is_none(), "Singular matrix should return None");
|
||||
}
|
||||
|
||||
@@ -325,10 +327,10 @@ fn test_singular_jacobian_returns_none() {
|
||||
#[test]
|
||||
fn test_zero_jacobian_returns_none() {
|
||||
let jacobian = JacobianMatrix::zeros(2, 2);
|
||||
|
||||
|
||||
let residuals = vec![1.0, 2.0];
|
||||
let result = jacobian.solve(&residuals);
|
||||
|
||||
|
||||
assert!(result.is_none(), "Zero matrix should return None");
|
||||
}
|
||||
|
||||
@@ -337,7 +339,7 @@ fn test_zero_jacobian_returns_none() {
|
||||
fn test_jacobian_condition_number_well_conditioned() {
|
||||
let entries = vec![(0, 0, 1.0), (1, 1, 1.0)];
|
||||
let jacobian = JacobianMatrix::from_builder(&entries, 2, 2);
|
||||
|
||||
|
||||
let cond = jacobian.condition_number().unwrap();
|
||||
assert_relative_eq!(cond, 1.0, epsilon = 1e-10);
|
||||
}
|
||||
@@ -346,14 +348,9 @@ fn test_jacobian_condition_number_well_conditioned() {
|
||||
#[test]
|
||||
fn test_jacobian_condition_number_ill_conditioned() {
|
||||
// Nearly singular matrix
|
||||
let entries = vec![
|
||||
(0, 0, 1.0),
|
||||
(0, 1, 1.0),
|
||||
(1, 0, 1.0),
|
||||
(1, 1, 1.0 + 1e-12),
|
||||
];
|
||||
let entries = vec![(0, 0, 1.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 1.0 + 1e-12)];
|
||||
let jacobian = JacobianMatrix::from_builder(&entries, 2, 2);
|
||||
|
||||
|
||||
let cond = jacobian.condition_number();
|
||||
assert!(cond.unwrap() > 1e10, "Should be ill-conditioned");
|
||||
}
|
||||
@@ -371,12 +368,15 @@ fn test_jacobian_non_square_overdetermined() {
|
||||
(2, 1, 3.0),
|
||||
];
|
||||
let jacobian = JacobianMatrix::from_builder(&entries, 3, 2);
|
||||
|
||||
|
||||
let residuals = vec![1.0, 2.0, 3.0];
|
||||
let result = jacobian.solve(&residuals);
|
||||
|
||||
|
||||
// Should return a least-squares solution
|
||||
assert!(result.is_some(), "Non-square system should return least-squares solution");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"Non-square system should return least-squares solution"
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -387,14 +387,9 @@ fn test_jacobian_non_square_overdetermined() {
|
||||
#[test]
|
||||
fn test_convergence_status_converged() {
|
||||
use entropyk_solver::ConvergedState;
|
||||
|
||||
let state = ConvergedState::new(
|
||||
vec![1.0, 2.0],
|
||||
10,
|
||||
1e-8,
|
||||
ConvergenceStatus::Converged,
|
||||
);
|
||||
|
||||
|
||||
let state = ConvergedState::new(vec![1.0, 2.0], 10, 1e-8, ConvergenceStatus::Converged);
|
||||
|
||||
assert!(state.is_converged());
|
||||
assert_eq!(state.status, ConvergenceStatus::Converged);
|
||||
}
|
||||
@@ -403,14 +398,14 @@ fn test_convergence_status_converged() {
|
||||
#[test]
|
||||
fn test_convergence_status_timed_out() {
|
||||
use entropyk_solver::ConvergedState;
|
||||
|
||||
|
||||
let state = ConvergedState::new(
|
||||
vec![1.0],
|
||||
50,
|
||||
1e-3,
|
||||
ConvergenceStatus::TimedOutWithBestState,
|
||||
);
|
||||
|
||||
|
||||
assert!(!state.is_converged());
|
||||
assert_eq!(state.status, ConvergenceStatus::TimedOutWithBestState);
|
||||
}
|
||||
@@ -427,7 +422,7 @@ fn test_non_convergence_display() {
|
||||
final_residual: 1.23e-4,
|
||||
};
|
||||
let msg = err.to_string();
|
||||
|
||||
|
||||
assert!(msg.contains("100"));
|
||||
assert!(msg.contains("1.23"));
|
||||
}
|
||||
@@ -439,7 +434,7 @@ fn test_invalid_system_display() {
|
||||
message: "Empty system has no equations".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
|
||||
|
||||
assert!(msg.contains("Empty system"));
|
||||
}
|
||||
|
||||
@@ -465,7 +460,7 @@ fn test_tolerance_positive() {
|
||||
#[test]
|
||||
fn test_picard_relaxation_factor_range() {
|
||||
use entropyk_solver::PicardConfig;
|
||||
|
||||
|
||||
let cfg = PicardConfig::default();
|
||||
assert!(cfg.relaxation_factor > 0.0);
|
||||
assert!(cfg.relaxation_factor <= 1.0);
|
||||
@@ -477,4 +472,4 @@ fn test_line_search_max_backtracks_reasonable() {
|
||||
let cfg = NewtonConfig::default();
|
||||
assert!(cfg.line_search_max_backtracks > 0);
|
||||
assert!(cfg.line_search_max_backtracks <= 100);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user