1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3mod linalg;
4mod solver;
5
6pub use linalg::{DenseLu, FaerLu, SparseQr};
7pub use solver::{Control, IterationStats, Iterations, MatrixFormat, NewtonCfg, solve, solve_cb};
8
9use core::fmt::{self, Display, Formatter};
10use core::num::NonZeroUsize;
11use faer::Mat;
12use faer::mat::MatMut;
13use faer::sparse::SparseColMatRef;
14use faer::sparse::SymbolicSparseColMat;
15use faer_traits::ComplexField;
16use num_traits::Zero;
17use std::sync::OnceLock;
18
19pub trait RowMap {
20 type Var: Copy + Eq;
21 fn n_variables(&self) -> usize;
22 fn n_residuals(&self) -> usize;
23 fn row(&self, bus: usize, var: Self::Var) -> Option<usize>;
24}
25
26#[derive(Debug, Clone)]
27pub struct Pattern<T> {
28 pub symbolic: SymbolicSparseColMat<usize>,
29 pub values: Vec<T>,
30}
31
32impl<T> Pattern<T> {
33 #[inline]
34 pub fn attach_values(&self) -> SparseColMatRef<'_, usize, T> {
35 SparseColMatRef::new(self.symbolic.as_ref(), &self.values)
36 }
37 #[inline]
38 pub fn values_mut(&mut self) -> &mut [T] {
39 &mut self.values
40 }
41}
42
43pub trait NonlinearSystem {
44 type Real: num_traits::Float;
45 type Layout: RowMap;
46
47 fn layout(&self) -> &Self::Layout;
48 fn jacobian(&self) -> &dyn JacobianCache<Self::Real>;
49 fn jacobian_mut(&mut self) -> &mut dyn JacobianCache<Self::Real>;
50 fn residual(&self, x: &[Self::Real], out: &mut [Self::Real]);
51 fn refresh_jacobian(&mut self, x: &[Self::Real]);
52
53 fn jacobian_dense(&mut self, x: &[Self::Real], jac: &mut faer::mat::Mat<Self::Real>) {
54 self.refresh_jacobian(x);
55 let sparse = self.jacobian().attach();
56 jac.fill(Self::Real::zero());
57 let row_idx = sparse.symbolic().row_idx();
58 let vals = sparse.val();
59 for col in 0..sparse.ncols() {
60 let range = sparse.col_range(col);
61 for idx in range.clone() {
62 jac[(row_idx[idx], col)] = vals[idx];
63 }
64 }
65 }
66}
67
68pub trait LinearSolver<T: ComplexField<Real = T>, M> {
69 fn factor(&mut self, a: &M) -> SolverResult<()>;
70 fn solve_in_place(&mut self, rhs: MatMut<T>) -> SolverResult<()>;
74}
75
76pub trait JacobianCache<T > {
77 fn symbolic(&self) -> &SymbolicSparseColMat<usize>;
78 fn values(&self) -> &[T];
79 fn values_mut(&mut self) -> &mut [T];
80 #[inline]
81 fn attach(&self) -> SparseColMatRef<'_, usize, T> {
82 SparseColMatRef::new(self.symbolic().as_ref(), self.values())
83 }
84}
85
86#[derive(Debug, Clone, Copy)]
87pub struct SolverError;
88
89impl Display for SolverError {
90 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
91 f.write_str("solver error")
92 }
93}
94
95impl std::error::Error for SolverError {}
96
97pub type SolverResult<T> = Result<T, error_stack::Report<SolverError>>;
98
99static RAYON_INIT: OnceLock<usize> = OnceLock::new();
100
101pub fn init_global_parallelism(threads: usize) -> usize {
102 if let Some(n) = RAYON_INIT.get().copied() {
103 return n;
104 }
105 let target = if threads == 0 {
106 std::thread::available_parallelism()
107 .unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) })
108 .get()
109 } else {
110 threads
111 };
112
113 let _ = rayon::ThreadPoolBuilder::new()
114 .num_threads(target)
115 .build_global();
116
117 let actual = rayon::current_num_threads();
118 let _ = RAYON_INIT.set(actual);
119 actual
120}
121
122#[inline]
123pub fn current_parallelism() -> usize {
124 RAYON_INIT
125 .get()
126 .copied()
127 .unwrap_or_else(rayon::current_num_threads)
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use faer::sparse::Pair;
134 use faer::sparse::SymbolicSparseColMat;
135
136 #[derive(Clone)]
137 struct TwoVarLayout;
138 impl RowMap for TwoVarLayout {
139 type Var = ();
140 fn n_variables(&self) -> usize {
141 2
142 }
143 fn n_residuals(&self) -> usize {
144 2
145 }
146 fn row(&self, _bus: usize, _var: Self::Var) -> Option<usize> {
147 None
148 }
149 }
150
151 #[derive(Clone)]
152 struct Jc {
153 sym: SymbolicSparseColMat<usize>,
154 vals: Vec<f64>,
155 }
156 impl JacobianCache<f64> for Jc {
157 fn symbolic(&self) -> &SymbolicSparseColMat<usize> {
158 &self.sym
159 }
160 fn values(&self) -> &[f64] {
161 &self.vals
162 }
163 fn values_mut(&mut self) -> &mut [f64] {
164 &mut self.vals
165 }
166 }
167
168 struct Model {
169 layout: TwoVarLayout,
170 jac: Jc,
171 }
172
173 impl Model {
174 fn new() -> Self {
175 let pairs = vec![
176 Pair { row: 0, col: 0 },
177 Pair { row: 1, col: 0 },
178 Pair { row: 0, col: 1 },
179 Pair { row: 1, col: 1 },
180 ];
181 let (sym, _argsort) = SymbolicSparseColMat::try_new_from_indices(2, 2, &pairs).unwrap();
182 let nnz = sym.col_ptr()[sym.ncols()];
183 Self {
184 layout: TwoVarLayout,
185 jac: Jc {
186 sym,
187 vals: vec![0.0; nnz],
188 },
189 }
190 }
191 }
192
193 impl NonlinearSystem for Model {
194 type Real = f64;
195 type Layout = TwoVarLayout;
196
197 fn layout(&self) -> &Self::Layout {
198 &self.layout
199 }
200
201 fn jacobian(&self) -> &dyn JacobianCache<Self::Real> {
202 &self.jac
203 }
204 fn jacobian_mut(&mut self) -> &mut dyn JacobianCache<Self::Real> {
205 &mut self.jac
206 }
207
208 fn residual(&self, x: &[Self::Real], out: &mut [Self::Real]) {
209 let (xx, yy) = (x[0], x[1]);
210 out[0] = xx + yy - 3.0;
211 out[1] = xx * xx + yy - 3.0;
212 }
213
214 fn refresh_jacobian(&mut self, x: &[Self::Real]) {
215 let xx = x[0];
216 let v = self.jac.values_mut();
217 v[0] = 1.0;
218 v[1] = 2.0 * xx;
219 v[2] = 1.0;
220 v[3] = 1.0;
221 }
222 }
223
224 #[test]
225 fn solves_two_equations_sparse() {
226 let cfg = NewtonCfg::<f64>::sparse()
227 .with_adaptive(true)
228 .with_threads(1);
229
230 let mut model = Model::new();
231 let mut x = [0.9_f64, 2.1_f64];
232
233 let iters =
234 crate::solve_cb(&mut model, &mut x, cfg, |_| Control::Continue).expect("solver");
235
236 assert!(iters > 0 && iters <= 25);
237 assert!((x[0] - 1.0).abs() < 1e-10);
238 assert!((x[1] - 2.0).abs() < 1e-10);
239 }
240
241 #[test]
242 fn solves_non_square_system() {
243 struct NonSquareLayout;
245 impl RowMap for NonSquareLayout {
246 type Var = ();
247 fn n_variables(&self) -> usize {
248 2
249 }
250 fn n_residuals(&self) -> usize {
251 3
252 }
253 fn row(&self, _bus: usize, _var: Self::Var) -> Option<usize> {
254 None
255 }
256 }
257
258 struct NonSquareModel {
259 layout: NonSquareLayout,
260 jac: Jc,
261 }
262
263 impl NonSquareModel {
264 fn new() -> Self {
265 let pairs = vec![
267 Pair { row: 0, col: 0 },
268 Pair { row: 0, col: 1 }, Pair { row: 1, col: 0 },
270 Pair { row: 1, col: 1 }, Pair { row: 2, col: 0 },
272 Pair { row: 2, col: 1 }, ];
274 let (sym, _argsort) =
275 SymbolicSparseColMat::try_new_from_indices(3, 2, &pairs).unwrap();
276 let nnz = sym.col_ptr()[sym.ncols()];
277 Self {
278 layout: NonSquareLayout,
279 jac: Jc {
280 sym,
281 vals: vec![0.0; nnz],
282 },
283 }
284 }
285 }
286
287 impl NonlinearSystem for NonSquareModel {
288 type Real = f64;
289 type Layout = NonSquareLayout;
290
291 fn layout(&self) -> &Self::Layout {
292 &self.layout
293 }
294 fn jacobian(&self) -> &dyn JacobianCache<Self::Real> {
295 &self.jac
296 }
297 fn jacobian_mut(&mut self) -> &mut dyn JacobianCache<Self::Real> {
298 &mut self.jac
299 }
300 fn residual(&self, x: &[Self::Real], out: &mut [Self::Real]) {
301 let (xx, yy) = (x[0], x[1]);
302
303 out[0] = xx + yy - 3.0;
308 out[1] = xx - yy - 1.0;
309 out[2] = 2.0 * xx + yy - 5.0;
310 }
311 fn refresh_jacobian(&mut self, _x: &[Self::Real]) {
312 let v = self.jac.values_mut();
313 v[0] = 1.0;
322 v[1] = 1.0;
323 v[2] = 2.0;
324 v[3] = 1.0;
325 v[4] = -1.0;
326 v[5] = 1.0;
327 }
328 }
329
330 let mut model = NonSquareModel::new();
331 let mut x = [1.0_f64, 1.0_f64]; let cfg = NewtonCfg::<f64>::sparse().with_threads(1);
333
334 let result = crate::solve(&mut model, &mut x, cfg);
335
336 assert!(result.is_ok());
338 let iters = result.unwrap();
339 assert!(iters > 0 && iters <= 25);
340
341 let tol = 1e-6;
344 assert!((x[0] - 2.0).abs() < tol);
345 assert!((x[1] - 1.0).abs() < tol);
346 }
347
348 #[test]
349 fn solves_gaussian_peak_fitting() {
350 struct GaussianLayout;
354 impl RowMap for GaussianLayout {
355 type Var = ();
356 fn n_variables(&self) -> usize {
357 3
358 }
359 fn n_residuals(&self) -> usize {
360 5
361 }
362 fn row(&self, _bus: usize, _var: Self::Var) -> Option<usize> {
363 None
364 }
365 }
366
367 struct GaussianModel {
368 layout: GaussianLayout,
369 jac: Jc,
370 data: Vec<(f64, f64)>,
371 }
372
373 impl GaussianModel {
374 fn new() -> Self {
375 let pairs = vec![
377 Pair { row: 0, col: 0 },
378 Pair { row: 0, col: 1 },
379 Pair { row: 0, col: 2 },
380 Pair { row: 1, col: 0 },
381 Pair { row: 1, col: 1 },
382 Pair { row: 1, col: 2 },
383 Pair { row: 2, col: 0 },
384 Pair { row: 2, col: 1 },
385 Pair { row: 2, col: 2 },
386 Pair { row: 3, col: 0 },
387 Pair { row: 3, col: 1 },
388 Pair { row: 3, col: 2 },
389 Pair { row: 4, col: 0 },
390 Pair { row: 4, col: 1 },
391 Pair { row: 4, col: 2 },
392 ];
393 let (sym, _argsort) =
394 SymbolicSparseColMat::try_new_from_indices(5, 3, &pairs).unwrap();
395 let nnz = sym.col_ptr()[sym.ncols()];
396
397 let x_vals = [-1.0, 0.0, 1.0, 2.0, 2.5];
399 let a = 2.0;
400 let mu = 1.0;
401 let sigma = 0.8;
402
403 let data: Vec<(f64, f64)> = x_vals
404 .iter()
405 .map(|x: &f64| {
406 let y = a * (-((x - mu) / sigma).powi(2)).exp();
407 (*x, y)
408 })
409 .collect();
410
411 Self {
412 layout: GaussianLayout,
413 jac: Jc {
414 sym,
415 vals: vec![0.0; nnz],
416 },
417 data,
418 }
419 }
420 }
421
422 impl NonlinearSystem for GaussianModel {
423 type Real = f64;
424 type Layout = GaussianLayout;
425
426 fn layout(&self) -> &Self::Layout {
427 &self.layout
428 }
429 fn jacobian(&self) -> &dyn JacobianCache<Self::Real> {
430 &self.jac
431 }
432 fn jacobian_mut(&mut self) -> &mut dyn JacobianCache<Self::Real> {
433 &mut self.jac
434 }
435
436 fn residual(&self, x: &[Self::Real], out: &mut [Self::Real]) {
437 let (a, mu, sigma) = (x[0], x[1], x[2]);
438
439 for (i, &(xi, yi)) in self.data.iter().enumerate() {
440 let z = (xi - mu) / sigma;
441 let gaussian = a * (-z * z).exp();
442 out[i] = gaussian - yi;
443 }
444 }
445
446 fn refresh_jacobian(&mut self, x: &[Self::Real]) {
447 let (a, mu, sigma) = (x[0], x[1], x[2]);
448 let v = self.jac.values_mut();
449
450 for (i, &(xi, _)) in self.data.iter().enumerate() {
451 let z = (xi - mu) / sigma;
452 let exp_term = (-z * z).exp();
453 let gaussian = a * exp_term;
454 let n_eqn = 5;
455
456 v[i] = exp_term;
458
459 v[i + n_eqn] = gaussian * 2.0 * (xi - mu) / (sigma * sigma);
461
462 v[i + n_eqn * 2] =
464 gaussian * 2.0 * (xi - mu) * (xi - mu) / (sigma * sigma * sigma);
465 }
466 }
467 }
468
469 let mut model = GaussianModel::new();
470
471 let mut x = [1.8_f64, 0.5_f64, 1.2_f64];
473 let cfg = NewtonCfg::<f64>::sparse()
474 .with_adaptive(true)
475 .with_threads(1);
476
477 let callback = |stats: &IterationStats<f64>| {
478 println!(
479 "Iter: {:>2}, Residual: {:.4e}, Damping: {:.4}",
480 stats.iter, stats.residual, stats.damping
481 );
482 Control::Continue
483 };
484
485 let result = crate::solve_cb(&mut model, &mut x, cfg, callback);
487
488 assert!(result.is_ok());
490 let iters = result.unwrap();
491 assert!(iters > 0 && iters <= 50);
492
493 println!(
496 "Fitted parameters: a={:.4}, mu={:.4}, sigma={:.4}",
497 x[0], x[1], x[2]
498 );
499
500 let tol = 1e-6;
501
502 assert!(
503 (x[0] - 2.0).abs() < tol,
504 "Amplitude should be 2.0, got {}",
505 x[0]
506 );
507 assert!((x[1] - 1.0).abs() < tol, "Mean should be 1.0, got {}", x[1]);
508 assert!(
509 (x[2] - 0.8).abs() < tol,
510 "Std dev should be 0.8, got {}",
511 x[2]
512 );
513 }
514
515 #[test]
516 fn solves_simple_circle_line_system() {
517 struct SimpleLayout;
520 impl RowMap for SimpleLayout {
521 type Var = ();
522 fn n_variables(&self) -> usize {
523 2
524 }
525 fn n_residuals(&self) -> usize {
526 2
527 }
528 fn row(&self, _bus: usize, _var: Self::Var) -> Option<usize> {
529 None
530 }
531 }
532
533 struct SimpleSystem {
534 layout: SimpleLayout,
535 jac: Jc,
536 }
537
538 impl SimpleSystem {
539 fn new() -> Self {
540 let pairs = vec![
541 faer::sparse::Pair { row: 0, col: 0 },
542 faer::sparse::Pair { row: 1, col: 0 },
543 faer::sparse::Pair { row: 0, col: 1 },
544 faer::sparse::Pair { row: 1, col: 1 },
545 ];
546 let (sym, _) = SymbolicSparseColMat::try_new_from_indices(2, 2, &pairs).unwrap();
547 let nnz = sym.col_ptr()[sym.ncols()];
548 Self {
549 layout: SimpleLayout,
550 jac: Jc {
551 sym,
552 vals: vec![0.0; nnz],
553 },
554 }
555 }
556 }
557
558 impl NonlinearSystem for SimpleSystem {
559 type Real = f64;
560 type Layout = SimpleLayout;
561
562 fn layout(&self) -> &Self::Layout {
563 &self.layout
564 }
565 fn jacobian(&self) -> &dyn JacobianCache<Self::Real> {
566 &self.jac
567 }
568 fn jacobian_mut(&mut self) -> &mut dyn JacobianCache<Self::Real> {
569 &mut self.jac
570 }
571 fn residual(&self, x: &[Self::Real], out: &mut [Self::Real]) {
572 out[0] = x[0] * x[0] + x[1] * x[1] - 1.0;
573 out[1] = x[0] - x[1];
574 }
575 fn refresh_jacobian(&mut self, x: &[Self::Real]) {
576 let v = self.jac.values_mut();
577 v[0] = 2.0 * x[0];
582 v[1] = 1.0;
583 v[2] = 2.0 * x[1];
584 v[3] = -1.0;
585 }
586 }
587
588 let mut system = SimpleSystem::new();
589 let mut x = [0.5_f64, 0.5_f64];
590
591 let cfg = NewtonCfg::<f64>::sparse()
592 .with_adaptive(true)
593 .with_threads(1);
594
595 let callback = |stats: &IterationStats<f64>| {
596 println!(
597 "Iteration {}: residual = {:.2e}, damping = {:.3}",
598 stats.iter, stats.residual, stats.damping
599 );
600 Control::Continue
601 };
602
603 let result = crate::solve_cb(&mut system, &mut x, cfg, callback);
604
605 assert!(result.is_ok(), "Solver failed: {:?}", result);
606 let iters = result.unwrap();
607 assert!(iters > 0 && iters <= 25);
608
609 let expected = std::f64::consts::FRAC_1_SQRT_2;
611 let tol = 1e-8;
612 assert!(
613 (x[0] - expected).abs() < tol,
614 "x[0] = {}, expected {}",
615 x[0],
616 expected
617 );
618 assert!(
619 (x[1] - expected).abs() < tol,
620 "x[1] = {}, expected {}",
621 x[1],
622 expected
623 );
624
625 println!("Converged in {} iterations", iters);
627 println!("Solution: x = {:?}", x);
628 let mut res = [0.0; 2];
629 system.residual(&x, &mut res);
630 println!("Residual: {:?}", res);
631 }
632
633 #[test]
634 fn solves_inconsistent_system_to_least_squares() {
635 struct Layout;
643 impl RowMap for Layout {
644 type Var = ();
645 fn n_variables(&self) -> usize {
646 2
647 }
648 fn n_residuals(&self) -> usize {
649 3
650 }
651 fn row(&self, _bus: usize, _var: Self::Var) -> Option<usize> {
652 None
653 }
654 }
655
656 struct InconsistentSystem {
657 layout: Layout,
658 jac: Jc,
659 }
660
661 impl InconsistentSystem {
662 fn new() -> Self {
663 let pairs = vec![
665 faer::sparse::Pair { row: 0, col: 0 },
666 faer::sparse::Pair { row: 1, col: 0 },
667 faer::sparse::Pair { row: 2, col: 0 },
668 faer::sparse::Pair { row: 0, col: 1 },
669 faer::sparse::Pair { row: 1, col: 1 },
670 faer::sparse::Pair { row: 2, col: 1 },
671 ];
672 let (sym, _) = SymbolicSparseColMat::try_new_from_indices(3, 2, &pairs).unwrap();
673 let nnz = sym.col_ptr()[sym.ncols()];
674 Self {
675 layout: Layout,
676 jac: Jc {
677 sym,
678 vals: vec![0.0; nnz],
679 },
680 }
681 }
682 }
683
684 impl NonlinearSystem for InconsistentSystem {
685 type Real = f64;
686 type Layout = Layout;
687
688 fn layout(&self) -> &Self::Layout {
689 &self.layout
690 }
691 fn jacobian(&self) -> &dyn JacobianCache<Self::Real> {
692 &self.jac
693 }
694 fn jacobian_mut(&mut self) -> &mut dyn JacobianCache<Self::Real> {
695 &mut self.jac
696 }
697
698 fn residual(&self, x: &[Self::Real], out: &mut [Self::Real]) {
699 out[0] = x[0] * x[0] + x[1] * x[1] - 1.0;
703 out[1] = x[0] - x[1];
704 out[2] = x[0] + x[1] - 2.0;
705 }
706
707 fn refresh_jacobian(&mut self, x: &[Self::Real]) {
708 let v = self.jac.values_mut();
712 v[0] = 2.0 * x[0];
713 v[1] = 1.0; v[2] = 1.0; v[3] = 2.0 * x[1]; v[4] = -1.0; v[5] = 1.0; }
719 }
720
721 let mut system = InconsistentSystem::new();
722 let mut x = [0.5_f64, 0.5_f64]; let cfg = NewtonCfg::<f64>::sparse()
725 .with_adaptive(true)
726 .with_tol_grad(1e-8)
727 .with_tol_step(0.0) .with_threads(1);
729
730 let callback = |stats: &IterationStats<f64>| {
731 println!(
732 "Iteration {}: residual = {:.2e}, damping = {:.3}",
733 stats.iter, stats.residual, stats.damping
734 );
735 Control::Continue
736 };
737
738 let result = crate::solve_cb(&mut system, &mut x, cfg, callback);
739
740 assert!(result.is_ok(), "Solver failed: {:?}", result);
741 let iters = result.unwrap();
742 assert!(
743 iters > 0 && iters <= 50,
744 "Unexpected iteration count: {}",
745 iters
746 );
747
748 let expected = 0.5_f64.powf(1.0 / 3.0);
750 let tol = 1e-6;
751
752 assert!(
753 (x[0] - expected).abs() < tol,
754 "x[0] = {}, expected {}",
755 x[0],
756 expected
757 );
758 assert!(
759 (x[1] - expected).abs() < tol,
760 "x[1] = {}, expected {}",
761 x[1],
762 expected
763 );
764 assert!(
765 (x[0] - x[1]).abs() < 1e-8,
766 "x and y should be essentially equal"
767 );
768
769 let mut r = [0.0; 3];
771 system.residual(&x, &mut r);
772 let g_x = 2.0 * x[0] * r[0] + r[1] + r[2];
774 let g_y = 2.0 * x[1] * r[0] - r[1] + r[2];
775 let grad_norm = (g_x * g_x + g_y * g_y).sqrt();
776 assert!(
777 grad_norm < 1e-8,
778 "Not at a least-squares stationary point: ||J^T r|| = {}",
779 grad_norm
780 );
781
782 println!("Converged in {} iterations", iters);
784 println!("Solution (least squares): x = {:?}", x);
785 println!("Residuals: {:?}", r);
786 let sse = r.iter().map(|ri| ri * ri).sum::<f64>();
787 println!("Sum of squared residuals: {:.8}", sse);
788 }
789
790 #[test]
791 fn test_sparse_gradient_convergence() {
792 let ftol = 1e-12; let gtol = 1e-6; let xtol = 0.0; struct SparseTestLayout;
798 impl RowMap for SparseTestLayout {
799 type Var = ();
800 fn n_variables(&self) -> usize {
801 2
802 }
803 fn n_residuals(&self) -> usize {
804 2
805 }
806 fn row(&self, _bus: usize, _var: Self::Var) -> Option<usize> {
807 None
808 }
809 }
810
811 struct SparseTestModel {
812 layout: SparseTestLayout,
813 jac: Jc,
814 }
815
816 impl SparseTestModel {
817 fn new() -> Self {
818 let pairs = vec![Pair { row: 0, col: 0 }, Pair { row: 1, col: 1 }];
820 let (sym, _argsort) =
821 SymbolicSparseColMat::try_new_from_indices(2, 2, &pairs).unwrap();
822 let nnz = sym.col_ptr()[sym.ncols()];
823 Self {
824 layout: SparseTestLayout,
825 jac: Jc {
826 sym,
827 vals: vec![0.0; nnz],
828 },
829 }
830 }
831 }
832
833 impl NonlinearSystem for SparseTestModel {
834 type Real = f64;
835 type Layout = SparseTestLayout;
836
837 fn layout(&self) -> &Self::Layout {
838 &self.layout
839 }
840 fn jacobian(&self) -> &dyn JacobianCache<Self::Real> {
841 &self.jac
842 }
843 fn jacobian_mut(&mut self) -> &mut dyn JacobianCache<Self::Real> {
844 &mut self.jac
845 }
846
847 fn residual(&self, x: &[Self::Real], out: &mut [Self::Real]) {
848 out[0] = (x[0] - 1.0) * (x[0] - 1.0);
851 out[1] = (x[1] - 2.0) * (x[1] - 2.0);
852 }
853
854 fn refresh_jacobian(&mut self, x: &[Self::Real]) {
855 let v = self.jac.values_mut();
862 v[0] = 2.0 * (x[0] - 1.0);
863 v[1] = 2.0 * (x[1] - 2.0);
864 }
865 }
866
867 let mut model = SparseTestModel::new();
868
869 let mut x = [0.0_f64, 0.0_f64];
871
872 let cfg = NewtonCfg::<f64>::sparse()
874 .with_tol(ftol)
875 .with_tol_grad(gtol) .with_tol_step(xtol) .with_threads(1);
878
879 let callback = |stats: &IterationStats<f64>| {
880 println!(
881 "Sparse iter {}: residual = {:.2e}, damping = {:.3}",
882 stats.iter, stats.residual, stats.damping
883 );
884 Control::Continue
885 };
886
887 let result = crate::solve_cb(&mut model, &mut x, cfg, callback);
888
889 assert!(
890 result.is_ok(),
891 "Sparse solver with gradient checking failed: {:?}",
892 result
893 );
894 let iters = result.unwrap();
895 assert!(
896 iters > 0 && iters <= 50,
897 "Unexpected iteration count: {}",
898 iters
899 );
900
901 let tol = 1e-2;
903 assert!(
904 (x[0] - 1.0).abs() < tol,
905 "x[0] = {}, expected 1.0, diff = {}",
906 x[0],
907 (x[0] - 1.0).abs()
908 );
909 assert!(
910 (x[1] - 2.0).abs() < tol,
911 "x[1] = {}, expected 2.0, diff = {}",
912 x[1],
913 (x[1] - 2.0).abs()
914 );
915
916 let mut residual = [0.0; 2];
918 model.residual(&x, &mut residual);
919
920 model.refresh_jacobian(&x);
921 let jac_vals = model.jac.values();
922 let grad_x = jac_vals[0] * residual[0]; let grad_y = jac_vals[1] * residual[1]; let grad_norm = grad_x.abs().max(grad_y.abs());
926
927 assert!(
928 grad_norm < 1e-6,
929 "Gradient norm too large: {}, sparse gradient checking failed",
930 grad_norm
931 );
932
933 println!(
934 "Sparse solver converged in {} iterations to ({:.6}, {:.6})",
935 iters, x[0], x[1]
936 );
937 println!("Final gradient norm: {:.2e}", grad_norm);
938 println!(
939 "Jacobian has {} nonzeros (truly sparse)",
940 model.jac.values().len()
941 );
942 }
943
944 #[test]
945 fn test_dense_gradient_convergence() {
946 let ftol = 1e-12; let gtol = 1e-6; let xtol = 0.0; struct DenseTestLayout;
953 impl RowMap for DenseTestLayout {
954 type Var = ();
955 fn n_variables(&self) -> usize {
956 2
957 }
958 fn n_residuals(&self) -> usize {
959 2
960 }
961 fn row(&self, _bus: usize, _var: Self::Var) -> Option<usize> {
962 None
963 }
964 }
965
966 struct DenseTestModel {
967 layout: DenseTestLayout,
968 jac: Jc,
969 }
970
971 impl DenseTestModel {
972 fn new() -> Self {
973 let pairs = vec![
974 Pair { row: 0, col: 0 },
975 Pair { row: 1, col: 0 },
976 Pair { row: 0, col: 1 },
977 Pair { row: 1, col: 1 },
978 ];
979 let (sym, _argsort) =
980 SymbolicSparseColMat::try_new_from_indices(2, 2, &pairs).unwrap();
981 let nnz = sym.col_ptr()[sym.ncols()];
982 Self {
983 layout: DenseTestLayout,
984 jac: Jc {
985 sym,
986 vals: vec![0.0; nnz],
987 },
988 }
989 }
990 }
991
992 impl NonlinearSystem for DenseTestModel {
993 type Real = f64;
994 type Layout = DenseTestLayout;
995
996 fn layout(&self) -> &Self::Layout {
997 &self.layout
998 }
999 fn jacobian(&self) -> &dyn JacobianCache<Self::Real> {
1000 &self.jac
1001 }
1002 fn jacobian_mut(&mut self) -> &mut dyn JacobianCache<Self::Real> {
1003 &mut self.jac
1004 }
1005
1006 fn residual(&self, x: &[Self::Real], out: &mut [Self::Real]) {
1007 out[0] = (x[0] - 1.0) * (x[0] - 1.0);
1010 out[1] = (x[1] - 2.0) * (x[1] - 2.0);
1011 }
1012
1013 fn refresh_jacobian(&mut self, x: &[Self::Real]) {
1014 let v = self.jac.values_mut();
1024 v[0] = 2.0 * (x[0] - 1.0);
1025 v[1] = 0.0;
1026 v[2] = 0.0;
1027 v[3] = 2.0 * (x[1] - 2.0);
1028 }
1029 }
1030
1031 let mut model = DenseTestModel::new();
1032
1033 let mut x = [0.0_f64, 0.0_f64];
1035
1036 let cfg = NewtonCfg::<f64>::dense()
1038 .with_tol(ftol)
1039 .with_tol_grad(gtol)
1040 .with_tol_step(xtol)
1041 .with_threads(1);
1042
1043 let mut converged_on_gradient = false;
1044 let callback = |stats: &IterationStats<f64>| {
1045 if stats.residual > 1e-8 && stats.iter > 1 {
1047 converged_on_gradient = true;
1049 }
1050 println!(
1051 "Dense iter {}: residual = {:.2e}, damping = {:.3}",
1052 stats.iter, stats.residual, stats.damping
1053 );
1054 Control::Continue
1055 };
1056
1057 let result = crate::solve_cb(&mut model, &mut x, cfg, callback);
1058
1059 assert!(
1060 result.is_ok(),
1061 "Dense solver with gradient checking failed: {:?}",
1062 result
1063 );
1064 let iters = result.unwrap();
1065 assert!(
1066 iters > 0 && iters <= 50,
1067 "Unexpected iteration count: {}",
1068 iters
1069 );
1070
1071 let tol = 1e-2; assert!(
1074 (x[0] - 1.0).abs() < tol,
1075 "x[0] = {}, expected 1.0, diff = {}",
1076 x[0],
1077 (x[0] - 1.0).abs()
1078 );
1079 assert!(
1080 (x[1] - 2.0).abs() < tol,
1081 "x[1] = {}, expected 2.0, diff = {}",
1082 x[1],
1083 (x[1] - 2.0).abs()
1084 );
1085
1086 let mut residual = [0.0; 2];
1088 model.residual(&x, &mut residual);
1089
1090 model.refresh_jacobian(&x);
1092 let jac_vals = model.jac.values();
1093 let grad_x = jac_vals[0] * residual[0] + jac_vals[1] * residual[1];
1094 let grad_y = jac_vals[2] * residual[0] + jac_vals[3] * residual[1];
1095 let grad_norm = grad_x.abs().max(grad_y.abs());
1096
1097 assert!(
1098 grad_norm < gtol,
1099 "Gradient norm too large: {}, suggesting dense gradient checking didn't work properly",
1100 grad_norm
1101 );
1102
1103 println!(
1104 "Dense solver converged in {} iterations to ({:.6}, {:.6})",
1105 iters, x[0], x[1]
1106 );
1107 println!("Final gradient norm: {:.2e}", grad_norm);
1108 }
1109}