| Crates.io | newton_faer |
| lib.rs | newton_faer |
| version | 0.1.2 |
| created_at | 2025-08-08 02:24:07.258464+00 |
| updated_at | 2025-08-08 02:46:22.64256+00 |
| description | Newton's method for solving systems of nonlinear equations using the Faer library. |
| homepage | https://github.com/alexlatif/newton-faer |
| repository | https://github.com/alexlatif/newton-faer |
| max_upload_size | |
| id | 1786094 |
| size | 85,223 |
Most Rust numerical computing is still young. The existing options:
This crate is a thin, reusable Newton core that leans on faer for world-class sparse linear algebra, while keeping all domain logic (residual/Jacobian) outside the engine. You get:
Architecture at a glance
Your model implements NonlinearSystem (residual + Jacobian/refresh), optionally with its own Jacobian cache.
The engine doesn’t know your math. You provide two tiny pieces:
1/ NonlinearSystem (your model)
2/ JacobianCache (your storage)
use newton_faer::{NonlinearSystem, RowMap, JacobianCache, NewtonCfg, solve};
use faer::sparse::{SymbolicSparseColMat, Pair};
struct Layout;
impl RowMap for Layout {
type Var = ();
fn dim(&self) -> usize { 2 }
fn row(&self, _i: usize, _v: ()) -> Option<usize> { None }
}
struct Jc { sym: SymbolicSparseColMat<usize>, vals: Vec<f64> }
impl JacobianCache<f64> for Jc {
fn symbolic(&self) -> &SymbolicSparseColMat<usize> { &self.sym }
fn values(&self) -> &[f64] { &self.vals }
fn values_mut(&mut self) -> &mut [f64] { &mut self.vals }
}
struct Model { lay: Layout, jac: Jc }
impl Model {
fn new() -> Self {
let pairs = [
Pair{row:0, col:0}, Pair{row:1, col:0},
Pair{row:0, col:1}, Pair{row:1, col:1},
];
let (sym, _) = SymbolicSparseColMat::try_new_from_indices(2, 2, &pairs).unwrap();
let nnz = sym.col_ptr()[sym.ncols()];
Self { lay: Layout, jac: Jc { sym, vals: vec![0.0; nnz] } }
}
}
impl NonlinearSystem for Model {
type Real = f64;
type Layout = Layout;
fn layout(&self) -> &Self::Layout { &self.lay }
fn jacobian(&self) -> &dyn JacobianCache<Self::Real> { &self.jac }
fn jacobian_mut(&mut self) -> &mut dyn JacobianCache<Self::Real> { &mut self.jac }
fn residual(&self, x: &[f64], out: &mut [f64]) {
// f1 = sin(x) + y
// f2 = x + exp(y) - 1
out[0] = x[0].sin() + x[1];
out[1] = x[0] + x[1].exp() - 1.0;
}
fn refresh_jacobian(&mut self, x: &[f64]) {
// J = [[cos(x), 1],
// [ 1, exp(y)]] in CSC (col-major): (0,0),(1,0),(0,1),(1,1)
let v = self.jac.values_mut();
v[0] = x[0].cos();
v[1] = 1.0;
v[2] = 1.0;
v[3] = x[1].exp();
}
}
// usage
fn main() -> Result<(), Box<dyn std::error::Error>> {
newton_faer::init_global_parallelism(0); // use all cores
let mut model = Model::new();
let mut x = [0.2, 0.0]; // initial guess
let iters = solve(&mut model, &mut x, NewtonCfg::sparse().with_adaptive(true))?;
println!("iters={iters}, x={:.6}, y={:.6}", x[0], x[1]);
Ok(())
}
Parallelism is a policy choice: inside LU for one/few huge systems, or over cases for large batches. We auto-init Rayon once; you can override the global thread count via config.
use rayon::prelude::*;
use crate::solver::NewtonCfg;
// batch mode: many cases, maximize throughput
let cfg_batch = NewtonCfg::<f64>::default().with_threads(1); // LU single-thread
let results: Vec<_> = contingencies
.par_iter()
.map(|&br| {
let mut pf = base_pf.clone();
pf.update_for_outage(br)?;
pf.solve_with_cfg(cfg_batch) // your wrapper that passes cfg in
})
.collect();
// big single case mode: few huge systems
let cfg_big = NewtonCfg::<f64>::default().with_threads(0); // use all cores in LU
let mut pf = big_system_pf.clone();
let res = pf.solve_with_cfg(cfg_big)?;
Guideline:
Tons of scenarios? with_threads(1) + par_iter() over cases.
One gigantic system? with_threads(0) (all cores) + sequential cases.
Divergence guard + backtracking are enabled when adaptive=true and steered by: min_damping, max_damping, grow, shrink, divergence_ratio, ls_backtrack, ls_max_steps.
let cfg = NewtonCfg::default().with_adaptive(true);
Keep a solver instance and reuse the symbolic factorization across solves with the same sparsity. Only the numeric phase is recomputed.
let mut lu = FaerLu::<f64>::default();
for case in cases {
let mut x = init_state(&case);
let _iters = solver::solve_sparse_cb(&mut model, &mut x, &mut lu, cfg, |_| Control::Continue)?;
}
Stream iteration stats to your UI, support cancellation, and run the solve on a worker thread.
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, mpsc,
};
use crate::solver::{Control, IterationStats};
let (tx, rx) = mpsc::channel::<IterationStats<f64>>();
let cancel = Arc::new(AtomicBool::new(false));
let cancel_flag = cancel.clone();
let mut system = /* build system */;
std::thread::spawn(move || {
let _ = solver::solve_cb(&mut model, &mut x, cfg.with_adaptive(true), |st| {
let _ = tx.send(st.clone());
if cancel_flag.load(Ordering::Relaxed) { Control::Cancel } else { Control::Continue }
});
});
while let Ok(st) = rx.recv() {
println!("iter={} residual={:.3e} damping={:.2}", st.iter, st.residual, st.damping);
// if user hits "Stop": cancel.store(true, Ordering::Relaxed);
}
Big thanks to the faer team. newton-faer leans on faer’s fast, well-designed sparse linear algebra.