use crate::Thermodynamics::User_substances_error::SubsDataError;
use log::{error, info};
use nalgebra::{DMatrix, DVector};
#[derive(Debug, Clone)]
pub enum SolveError {
MaxIterations,
SingularMatrix,
EvalError(String),
}
#[derive(Debug)]
pub enum ReactionExtentError {
SubsDataError(SubsDataError),
SolveError(SolveError),
SVDError(String),
DimensionMismatch(String),
DuplicateSpecies(usize),
SpeciesNotAssigned,
InvalidInitialResiduals(Vec<f64>),
ResidualEvaluation(String),
JacobianEvaluation(String),
InvalidSpeciesAmount { index: usize, value: f64 },
InvalidNPhase { index: usize, value: f64 },
InvalidPhi { index: usize, value: f64 },
InvalidDG0 { dg0: f64, temperature: f64 },
Other(String),
}
pub struct LMSolver<F, J, C>
where
F: Fn(&[f64]) -> Result<Vec<f64>, ReactionExtentError>,
J: Fn(&[f64]) -> Result<DMatrix<f64>, ReactionExtentError>,
C: Fn(&[f64]) -> bool,
{
pub f: F,
pub jacobian: J,
pub feasible: C,
pub lambda: f64,
pub tol: f64,
pub max_iter: usize,
pub alpha_min: f64,
}
impl<F, J, C> LMSolver<F, J, C>
where
F: Fn(&[f64]) -> Result<Vec<f64>, ReactionExtentError>,
J: Fn(&[f64]) -> Result<DMatrix<f64>, ReactionExtentError>,
C: Fn(&[f64]) -> bool,
{
pub fn solve(&mut self, mut x: Vec<f64>) -> Result<Vec<f64>, SolveError> {
let n = x.len();
let mut lambda = self.lambda;
for _iter in 0..self.max_iter {
let f_val = (self.f)(&x)
.map_err(|e| SolveError::EvalError(format!("Residual eval error: {:?}", e)))?;
info!("Iteration {}, x = {:?}, f = {:?}", _iter, x, f_val);
let f_norm = l2_norm(&f_val);
info!(" ||f|| = {}", f_norm);
if f_norm < self.tol {
return Ok(x);
}
if f_norm.is_nan() || f_norm.is_infinite() {
return Err(SolveError::SingularMatrix);
}
let j = (self.jacobian)(&x)
.map_err(|e| SolveError::EvalError(format!("Jacobian eval error: {:?}", e)))?;
info!(" Jacobian:\n{}", j);
let jt = j.transpose();
let jtj = &jt * &j;
let mut lhs = jtj.clone();
info!(" Jᵀ·J:\n{}", jtj);
for i in 0..n {
lhs[(i, i)] += lambda;
}
let rhs = -(&jt * DVector::from_vec(f_val.clone()));
let delta = lhs.lu().solve(&rhs).ok_or(SolveError::SingularMatrix)?;
info!(" Step delta: {:?}", delta);
let delta = delta.data.as_vec().clone();
let mut alpha = 1.0;
let mut accepted = false;
while alpha >= self.alpha_min {
let x_trial: Vec<f64> = x
.iter()
.zip(delta.iter())
.map(|(xi, dxi)| xi + alpha * dxi)
.collect();
info!(" Trial x (alpha={}): {:?}", alpha, x_trial);
if !(self.feasible)(&x_trial) {
alpha *= 0.5;
continue;
}
let f_trial = (self.f)(&x_trial)
.map_err(|e| SolveError::EvalError(format!("Residual eval error: {:?}", e)))?;
info!(" Trial f: {:?}", f_trial);
let f_trial_norm = l2_norm(&f_trial);
info!(" ||f_trial|| = {}", f_trial_norm);
if f_trial_norm < f_norm {
x = x_trial;
lambda *= 0.3;
accepted = true;
break;
} else {
alpha *= 0.5;
}
}
if !accepted {
lambda *= 10.0;
info!(
" No acceptable step found; increasing lambda to {}",
lambda
);
}
}
Err(SolveError::MaxIterations)
}
}
pub fn max_step_moles_nonnegative(
n: &[f64], delta_n: &[f64], safety: f64, ) -> f64 {
assert_eq!(n.len(), delta_n.len());
let mut alpha_max: f64 = 1.0;
for i in 0..n.len() {
let ni = n[i];
let dni = delta_n[i];
if dni < 0.0 {
if ni <= 0.0 {
return 0.0; }
let alpha_i = safety * ni / (-dni);
alpha_max = alpha_max.min(alpha_i);
}
}
alpha_max.clamp(0.0, 1.0)
}
pub struct NRSolver<F, J, C>
where
F: Fn(&[f64]) -> Result<Vec<f64>, ReactionExtentError>,
J: Fn(&[f64]) -> Result<DMatrix<f64>, ReactionExtentError>,
C: Fn(&[f64]) -> bool,
{
pub f: F,
pub jacobian: J,
pub feasible: C,
pub n0: Vec<f64>,
pub reactions: DMatrix<f64>,
pub tol: f64,
pub max_iter: usize,
pub alpha_min: f64,
}
impl<F, J, C> NRSolver<F, J, C>
where
F: Fn(&[f64]) -> Result<Vec<f64>, ReactionExtentError>,
J: Fn(&[f64]) -> Result<DMatrix<f64>, ReactionExtentError>,
C: Fn(&[f64]) -> bool,
{
pub fn solve(&mut self, mut x: Vec<f64>) -> Result<Vec<f64>, SolveError> {
let n = x.len();
for iter in 0..self.max_iter {
let f_val = (self.f)(&x).map_err(|e| SolveError::EvalError(format!("{:?}", e)))?;
info!("NR Iteration {}, x = {:?}, f = {:?}", iter, x, f_val);
let f_norm = l2_norm(&f_val);
info!(" ||f|| = {}", f_norm);
if f_norm < self.tol {
return Ok(x);
}
if f_norm.is_nan() || f_norm.is_infinite() {
return Err(SolveError::SingularMatrix);
}
let j = (self.jacobian)(&x).map_err(|e| SolveError::EvalError(format!("{:?}", e)))?;
info!(" Jacobian:\n{}", j);
if j.nrows() != n || j.ncols() != n {
error!(
"Jacobian nrows = {} ncols ={}, initial guess length {}",
j.nrows(),
j.ncols(),
n
);
return Err(SolveError::EvalError(
"Jacobian dimension mismatch".to_string(),
));
}
let rhs = -DVector::from_vec(f_val);
let delta_vec = j.lu().solve(&rhs).ok_or(SolveError::SingularMatrix)?;
let delta = delta_vec.data.as_vec();
info!(" Step delta: {:?}", delta_vec);
let mut alpha = 1.0;
info!("bounded step {}", alpha);
let mut accepted = false;
while alpha >= self.alpha_min {
let x_trial: Vec<f64> = x
.iter()
.zip(delta.iter())
.map(|(xi, dxi)| xi + alpha * dxi)
.collect();
info!(" Trial x (alpha={}): {:?}", alpha, x_trial);
if !(self.feasible)(&x_trial) {
alpha *= 0.5;
continue;
}
let f_trial =
(self.f)(&x_trial).map_err(|e| SolveError::EvalError(format!("{:?}", e)))?;
info!(" Trial f: {:?}", f_trial);
let f_trial_norm = l2_norm(&f_trial);
info!(" ||f_trial|| = {}", f_trial_norm);
if f_trial_norm < f_norm {
x = x_trial;
accepted = true;
break;
}
alpha *= 0.5;
if !accepted {
info!(" No acceptable step found; continuing to next iteration");
}
}
if !accepted {
return Err(SolveError::MaxIterations);
}
}
Err(SolveError::MaxIterations)
}
}
fn l2_norm(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
pub struct TrustRegionSolver<F, J, C>
where
F: Fn(&[f64]) -> Result<Vec<f64>, ReactionExtentError>,
J: Fn(&[f64]) -> Result<DMatrix<f64>, ReactionExtentError>,
C: Fn(&[f64]) -> bool,
{
pub f: F,
pub jacobian: J,
pub feasible: C,
pub tol: f64,
pub max_iter: usize,
pub delta_init: f64,
pub delta_max: f64,
pub eta: f64, }
impl<F, J, C> TrustRegionSolver<F, J, C>
where
F: Fn(&[f64]) -> Result<Vec<f64>, ReactionExtentError>,
J: Fn(&[f64]) -> Result<DMatrix<f64>, ReactionExtentError>,
C: Fn(&[f64]) -> bool,
{
pub fn solve(&self, mut x: Vec<f64>) -> Result<Vec<f64>, SolveError> {
let n = x.len();
let mut delta = self.delta_init;
for iter in 0..self.max_iter {
let f_val = (self.f)(&x).map_err(|e| SolveError::EvalError(format!("{:?}", e)))?;
let f_norm = l2_norm(&f_val);
if f_norm < self.tol {
return Ok(x);
}
let J = (self.jacobian)(&x).map_err(|e| SolveError::EvalError(format!("{:?}", e)))?;
if J.nrows() != n || J.ncols() != n {
return Err(SolveError::EvalError("Jacobian not square".into()));
}
let fvec = DVector::from_vec(f_val.clone());
let newton_step = J
.clone()
.lu()
.solve(&(-&fvec))
.unwrap_or_else(|| DVector::zeros(n));
let g = &J.transpose() * &fvec;
let g_norm_sq = g.dot(&g);
let jg = &J * &g;
let alpha = g_norm_sq / jg.dot(&jg).max(1e-16);
let cauchy_step = -alpha * g;
let p = if newton_step.norm() <= delta {
newton_step
} else if cauchy_step.norm() >= delta {
delta / cauchy_step.norm() * cauchy_step
} else {
let p_u = cauchy_step;
let p_b = newton_step;
let d = &p_b - &p_u;
let a = d.dot(&d);
let b = 2.0 * p_u.dot(&d);
let c = p_u.dot(&p_u) - delta * delta;
let tau = (-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a);
p_u + tau * d
};
let p_vec = p.data.as_vec().clone();
let x_trial: Vec<f64> = x.iter().zip(p_vec.iter()).map(|(xi, pi)| xi + pi).collect();
if !(self.feasible)(&x_trial) {
delta *= 0.25;
continue;
}
let f_trial =
(self.f)(&x_trial).map_err(|e| SolveError::EvalError(format!("{:?}", e)))?;
let actual_reduction = f_norm.powi(2) - l2_norm(&f_trial).powi(2);
let model_reduction = -2.0 * fvec.dot(&p) - p.dot(&(J.transpose() * &J * &p));
let rho = actual_reduction / model_reduction.max(1e-16);
if rho < 0.25 {
delta *= 0.25;
} else if rho > 0.75 && (p.norm() - delta).abs() < 1e-12 {
delta = (2.0 * delta).min(self.delta_max);
}
if rho > self.eta {
x = x_trial;
}
if delta < 1e-14 {
return Err(SolveError::SingularMatrix);
}
}
Err(SolveError::MaxIterations)
}
}
#[derive(Debug, Clone)]
pub struct ReactionBasis {
pub rank: usize,
pub num_reactions: usize,
pub reactions: DMatrix<f64>,
}
pub fn compute_reaction_basis(
a: &DMatrix<f64>,
tol: f64,
) -> Result<ReactionBasis, ReactionExtentError> {
use nalgebra::linalg::SVD;
let m = a.nrows();
let at = a.transpose();
let svd = SVD::new(at, true, true);
let v_t = match svd.v_t {
Some(v) => v,
None => {
let msg = "SVD failed: Váµ€".to_string();
error!("{}", msg);
return Err(ReactionExtentError::SVDError(msg));
}
};
let singular_values = svd.singular_values;
let rank = singular_values.iter().filter(|&&s| s > tol).count();
let num_reactions = if rank <= m { m - rank } else { 0 };
let mut reactions = DMatrix::<f64>::zeros(m, num_reactions);
let p = v_t.nrows();
let mut v_rows: Vec<DVector<f64>> = Vec::new();
for i in 0..p {
v_rows.push(v_t.row(i).transpose());
}
let mut col = 0;
for i in rank..p {
if col >= num_reactions {
break;
}
reactions.set_column(col, &v_rows[i]);
col += 1;
}
if col < num_reactions {
let orth_span: Vec<&DVector<f64>> = v_rows.iter().take(rank).collect();
let mut null_basis: Vec<DVector<f64>> = Vec::new();
for j in 0..m {
if col >= num_reactions {
break;
}
let mut cand = DVector::<f64>::from_element(m, 0.0);
cand[j] = 1.0;
for rvec in &orth_span {
let dot = rvec.dot(&cand);
cand -= *rvec * dot;
}
for u in &null_basis {
let dot = u.dot(&cand);
cand -= u * dot;
}
let norm = cand.norm();
if norm > tol {
let u = cand / norm;
reactions.set_column(col, &u);
null_basis.push(u);
col += 1;
}
}
}
let mut reactions = -1.0 * reactions;
let eps = 1e-3;
for i in 0..m {
for j in 0..num_reactions {
if reactions[(i, j)].abs() < eps {
reactions[(i, j)] = 0.0;
}
}
}
Ok(ReactionBasis {
rank,
num_reactions,
reactions,
})
}
#[cfg(test)]
mod tests {
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
use super::*;
#[test]
fn lm_solves_scalar_quadratic() {
let f = |x: &[f64]| Ok(vec![x[0] * x[0] - 2.0]) as Result<Vec<f64>, ReactionExtentError>;
let j = |x: &[f64]| {
Ok(nalgebra::DMatrix::from_row_slice(1, 1, &[2.0 * x[0]]))
as Result<DMatrix<f64>, ReactionExtentError>
};
let feasible = |_x: &[f64]| true;
let mut solver = LMSolver {
f,
jacobian: j,
feasible,
lambda: 1e-3,
tol: 1e-12,
max_iter: 50,
alpha_min: 1e-6,
};
let x0 = vec![1.0];
let sol = solver.solve(x0).unwrap();
assert!(approx_eq(sol[0], 2.0_f64.sqrt(), 1e-8));
}
#[test]
fn lm_solves_2d_nonlinear_system() {
let f = |x: &[f64]| {
Ok(vec![x[0] * x[0] + x[1] * x[1] - 1.0, x[0] - x[1]])
as Result<Vec<f64>, ReactionExtentError>
};
let j = |x: &[f64]| {
Ok(nalgebra::DMatrix::from_row_slice(
2,
2,
&[2.0 * x[0], 2.0 * x[1], 1.0, -1.0],
)) as Result<DMatrix<f64>, ReactionExtentError>
};
let feasible = |_x: &[f64]| true;
let mut solver = LMSolver {
f,
jacobian: j,
feasible,
lambda: 1e-3,
tol: 1e-12,
max_iter: 50,
alpha_min: 1e-6,
};
let x0 = vec![0.8, 0.3];
let sol = solver.solve(x0).unwrap();
let expected = 1.0 / 2.0_f64.sqrt();
assert!(approx_eq(sol[0], expected, 1e-8));
assert!(approx_eq(sol[1], expected, 1e-8));
}
#[test]
fn lm_respects_feasibility_constraint() {
let f = |x: &[f64]| Ok(vec![x[0] * x[0] - 1.0]) as Result<Vec<f64>, ReactionExtentError>;
let j = |x: &[f64]| {
Ok(nalgebra::DMatrix::from_row_slice(1, 1, &[2.0 * x[0]]))
as Result<DMatrix<f64>, ReactionExtentError>
};
let feasible = |x: &[f64]| x[0] >= 0.0;
let mut solver = LMSolver {
f,
jacobian: j,
feasible,
lambda: 1e-3,
tol: 1e-12,
max_iter: 50,
alpha_min: 1e-6,
};
let x0 = vec![0.1];
let sol = solver.solve(x0).unwrap();
assert!(approx_eq(sol[0], 1.0, 1e-8));
}
#[test]
fn lm_handles_flat_jacobian() {
let f = |x: &[f64]| Ok(vec![x[0].powi(3)]) as Result<Vec<f64>, ReactionExtentError>;
let j = |x: &[f64]| {
Ok(nalgebra::DMatrix::from_row_slice(
1,
1,
&[3.0 * x[0] * x[0]],
)) as Result<DMatrix<f64>, ReactionExtentError>
};
let feasible = |_x: &[f64]| true;
let mut solver = LMSolver {
f,
jacobian: j,
feasible,
lambda: 1e-4,
tol: 1e-15,
max_iter: 1000,
alpha_min: 1e-8,
};
let x0 = vec![0.5];
let sol = solver.solve(x0).unwrap();
info!("{:?}", &sol);
assert!(sol[0].abs() < 1e-5);
}
#[test]
fn lm_reports_max_iterations() {
let f = |_x: &[f64]| Ok(vec![1.0]); let j = |_x: &[f64]| Ok(nalgebra::DMatrix::identity(1, 1));
let feasible = |_x: &[f64]| true;
let mut solver = LMSolver {
f,
jacobian: j,
feasible,
lambda: 1e-3,
tol: 1e-12,
max_iter: 5,
alpha_min: 1e-6,
};
let res = solver.solve(vec![0.0]);
assert!(matches!(res, Err(SolveError::MaxIterations)));
}
}
#[cfg(test)]
mod tests_reaction_basis {
use crate::Thermodynamics::ChemEquilibrium::Chem_eq_K_eq3::compute_reaction_basis;
use log::info;
use nalgebra::DMatrix;
const TOL: f64 = 1e-10;
#[test]
fn diatomic_dissociation_o2_o() {
let a = DMatrix::from_row_slice(
2,
1,
&[
2.0, 1.0, ],
);
let basis = compute_reaction_basis(&a, TOL).unwrap();
info!("basis: {:?}", basis);
assert_eq!(basis.rank, 1);
assert_eq!(basis.num_reactions, 1);
let residual = a.transpose() * &basis.reactions;
assert!(residual.norm() < 1e-12);
}
#[test]
fn nitrogen_oxygen_system() {
let a = DMatrix::from_row_slice(
4,
2,
&[
2.0, 0.0, 0.0, 2.0, 1.0, 1.0, 1.0, 2.0, ],
);
let basis = compute_reaction_basis(&a, TOL).unwrap();
assert_eq!(basis.rank, 2);
assert_eq!(basis.num_reactions, 2);
let residual = a.transpose() * &basis.reactions;
assert!(residual.norm() < 1e-12);
}
#[test]
fn methane_combustion_species() {
let a = DMatrix::from_row_slice(
4,
3,
&[
1.0, 4.0, 0.0, 0.0, 0.0, 2.0, 1.0, 0.0, 2.0, 0.0, 2.0, 1.0, ],
);
let basis = compute_reaction_basis(&a, TOL).unwrap();
assert_eq!(basis.rank, 3);
assert_eq!(basis.num_reactions, 1);
let residual = a.transpose() * &basis.reactions;
assert!(residual.norm() < 1e-12);
}
#[test]
fn reaction_basis_o2_not_zero() {
use nalgebra::DMatrix;
let a = DMatrix::from_row_slice(2, 1, &[2.0, 1.0]);
let rb = compute_reaction_basis(&a, 1e-12).unwrap();
assert_eq!(rb.num_reactions, 1);
let nu = rb.reactions.column(0);
assert!(
nu.iter().any(|&x| x.abs() > 1e-6),
"Reaction vector is zero!"
);
assert!((2.0 * nu[0] + 1.0 * nu[1]).abs() < 1e-10);
}
}
#[cfg(test)]
mod nr_tests {
use super::*;
use nalgebra::DMatrix;
#[test]
fn max_step_limits_alpha() {
let n = vec![0.01, 0.5];
let delta = vec![-1.0, 0.1];
let alpha = max_step_moles_nonnegative(&n, &delta, 0.95);
let expected = 0.95 * 0.01 / 1.0;
assert!((alpha - expected).abs() < 1e-12);
}
#[test]
fn nr_solver_respects_bounds_and_converges() {
let f = |x: &[f64]| Ok(vec![x[0]]) as Result<Vec<f64>, ReactionExtentError>;
let j = |_: &[f64]| {
Ok(DMatrix::from_row_slice(1, 1, &[1.0])) as Result<DMatrix<f64>, ReactionExtentError>
};
let feasible = |x: &[f64]| x[0] >= 0.0;
let mut solver = NRSolver {
f,
jacobian: j,
feasible,
n0: vec![0.01],
reactions: DMatrix::zeros(1, 0),
tol: 1e-12,
max_iter: 100,
alpha_min: 1e-12,
};
let sol = solver.solve(vec![0.01]).unwrap();
assert!(sol[0] >= 0.0);
assert!(sol[0].abs() < 1e-8);
}
}
#[cfg(test)]
mod trust_region_tests {
use super::*;
use approx::assert_relative_eq;
fn vec_norm(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
fn approx_eq(a: &[f64], b: &[f64], tol: f64) -> bool {
a.iter().zip(b.iter()).all(|(x, y)| (*x - *y).abs() < tol)
}
#[test]
fn trust_region_linear_system() {
let f = |x: &[f64]| -> Result<Vec<f64>, ReactionExtentError> {
Ok(vec![3.0 * x[0] + 1.0 * x[1] - 1.0, 1.0 * x[0] + 2.0 * x[1]])
};
let j = |_x: &[f64]| -> Result<DMatrix<f64>, ReactionExtentError> {
Ok(DMatrix::from_row_slice(2, 2, &[3.0, 1.0, 1.0, 2.0]))
};
let feasible = |_x: &[f64]| true;
let solver = TrustRegionSolver {
f,
jacobian: j,
feasible,
tol: 1e-12,
max_iter: 10,
delta_init: 1.0,
delta_max: 10.0,
eta: 0.1,
};
let x0 = vec![0.0, 0.0];
let sol = solver.solve(x0).unwrap();
assert!(approx_eq(&sol, &[0.4, -0.2], 1e-10));
}
#[test]
fn trust_region_scalar_nonlinear() {
let f =
|x: &[f64]| -> Result<Vec<f64>, ReactionExtentError> { Ok(vec![x[0] * x[0] - 2.0]) };
let j = |x: &[f64]| -> Result<DMatrix<f64>, ReactionExtentError> {
Ok(DMatrix::from_row_slice(1, 1, &[2.0 * x[0]]))
};
let feasible = |_x: &[f64]| true;
let solver = TrustRegionSolver {
f,
jacobian: j,
feasible,
tol: 1e-12,
max_iter: 50,
delta_init: 0.1, delta_max: 10.0,
eta: 0.1,
};
let sol = solver.solve(vec![0.1]).unwrap();
assert!((sol[0] - 2.0_f64.sqrt()).abs() < 1e-10);
}
#[test]
fn trust_region_ill_conditioned() {
let f = |x: &[f64]| -> Result<Vec<f64>, ReactionExtentError> {
Ok(vec![1e6 * x[0] - 1.0, x[1] - 1.0])
};
let j = |_x: &[f64]| -> Result<DMatrix<f64>, ReactionExtentError> {
Ok(DMatrix::from_row_slice(2, 2, &[1e6, 0.0, 0.0, 1.0]))
};
let feasible = |_x: &[f64]| true;
let solver = TrustRegionSolver {
f,
jacobian: j,
feasible,
tol: 1e-12,
max_iter: 50,
delta_init: 0.01,
delta_max: 1.0,
eta: 0.1,
};
let sol = solver.solve(vec![0.0, 0.0]).unwrap();
assert!((sol[0] - 1e-6).abs() < 1e-10);
assert!((sol[1] - 1.0).abs() < 1e-10);
}
#[test]
fn trust_region_feasibility_constraint() {
let f = |x: &[f64]| -> Result<Vec<f64>, ReactionExtentError> { Ok(vec![x[0] - 1.0]) };
let j = |_x: &[f64]| -> Result<DMatrix<f64>, ReactionExtentError> {
Ok(DMatrix::from_row_slice(1, 1, &[1.0]))
};
let feasible = |x: &[f64]| x[0] >= 0.0;
let solver = TrustRegionSolver {
f,
jacobian: j,
feasible,
tol: 1e-12,
max_iter: 50,
delta_init: 10.0,
delta_max: 10.0,
eta: 0.1,
};
let sol = solver.solve(vec![-10.0]).unwrap();
assert!((sol[0] - 1.0).abs() < 1e-10);
}
#[test]
fn trust_region_singular_jacobian() {
let f = |_x: &[f64]| -> Result<Vec<f64>, ReactionExtentError> { Ok(vec![1.0]) };
let j =
|_x: &[f64]| -> Result<DMatrix<f64>, ReactionExtentError> { Ok(DMatrix::zeros(1, 1)) };
let feasible = |_x: &[f64]| true;
let solver = TrustRegionSolver {
f,
jacobian: j,
feasible,
tol: 1e-12,
max_iter: 5,
delta_init: 1.0,
delta_max: 1.0,
eta: 0.1,
};
let res = solver.solve(vec![0.0]);
assert!(res.is_err());
}
#[test]
fn tr_solves_scalar_quadratic() {
let f = |x: &[f64]| Ok(vec![x[0] * x[0] - 2.0]) as Result<Vec<f64>, ReactionExtentError>;
let j = |x: &[f64]| {
Ok(nalgebra::DMatrix::from_row_slice(1, 1, &[2.0 * x[0]]))
as Result<DMatrix<f64>, ReactionExtentError>
};
let feasible = |_x: &[f64]| true;
let solver = TrustRegionSolver {
f,
jacobian: j,
feasible,
tol: 1e-12,
max_iter: 50,
delta_init: 0.1,
delta_max: 10.0,
eta: 0.0,
};
let x0 = vec![0.1];
let sol = solver.solve(x0).unwrap();
assert_relative_eq!(sol[0], 2.0_f64.sqrt(), epsilon = 1e-8);
}
#[test]
fn tr_solves_2d_nonlinear_system() {
let f = |x: &[f64]| {
Ok(vec![x[0] * x[0] + x[1] * x[1] - 1.0, x[0] - x[1]])
as Result<Vec<f64>, ReactionExtentError>
};
let j = |x: &[f64]| {
Ok(nalgebra::DMatrix::from_row_slice(
2,
2,
&[2.0 * x[0], 2.0 * x[1], 1.0, -1.0],
)) as Result<DMatrix<f64>, ReactionExtentError>
};
let feasible = |_x: &[f64]| true;
let solver = TrustRegionSolver {
f,
jacobian: j,
feasible,
tol: 1e-12,
max_iter: 1000,
delta_init: 0.1,
delta_max: 10.0,
eta: 0.0,
};
let x0 = vec![0.5, 0.5];
let sol = solver.solve(x0).unwrap();
let expected = 1.0 / 2.0_f64.sqrt();
assert_relative_eq!(sol[0], expected, epsilon = 1e-8);
assert_relative_eq!(sol[1], expected, epsilon = 1e-8);
}
#[test]
fn tr_respects_feasibility_constraint() {
let f = |x: &[f64]| Ok(vec![x[0] * x[0] - 1.0]) as Result<Vec<f64>, ReactionExtentError>;
let j = |x: &[f64]| {
Ok(nalgebra::DMatrix::from_row_slice(1, 1, &[2.0 * x[0]]))
as Result<DMatrix<f64>, ReactionExtentError>
};
let feasible = |x: &[f64]| true;
let solver = TrustRegionSolver {
f,
jacobian: j,
feasible,
tol: 1e-12,
max_iter: 50,
delta_init: 1.0,
delta_max: 10.0,
eta: 0.1,
};
let x0 = vec![0.1];
let sol = solver.solve(x0).unwrap();
assert_relative_eq!(sol[0], 1.0, epsilon = 1e-8);
}
#[test]
fn tr_reports_max_iterations() {
let f = |_x: &[f64]| Ok(vec![1.0]); let j = |_x: &[f64]| Ok(nalgebra::DMatrix::identity(1, 1));
let feasible = |_x: &[f64]| true;
let solver = TrustRegionSolver {
f,
jacobian: j,
feasible,
tol: 1e-12,
max_iter: 5,
delta_init: 1.0,
delta_max: 10.0,
eta: 0.1,
};
let res = solver.solve(vec![0.0]);
assert!(matches!(res, Err(SolveError::MaxIterations)));
}
}