newton_faer/
lib.rs

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    /// Solves in-place.
71    /// - LU: overwrites `rhs` with the solution.
72    /// - QR least-squares: writes the solution into the top ncols(A) rows of `rhs`.
73    fn solve_in_place(&mut self, rhs: MatMut<T>) -> SolverResult<()>;
74}
75
76pub trait JacobianCache<T /* Real */> {
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        // A system with 2 variables and 3 residuals (overdetermined).
244        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                // Jacobian pattern: 3 residuals x 2 variables
266                let pairs = vec![
267                    Pair { row: 0, col: 0 },
268                    Pair { row: 0, col: 1 }, // First residual depends on both vars.
269                    Pair { row: 1, col: 0 },
270                    Pair { row: 1, col: 1 }, // Second residual depends on both vars.
271                    Pair { row: 2, col: 0 },
272                    Pair { row: 2, col: 1 }, // Third residual depends on both vars.
273                ];
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                // Overdetermined system.
304                // x + y = 3
305                // x - y = 1
306                // 2x + y = 5
307                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                // Jacobian entries in column-major order.
314                // d(r0)/dx = 1
315                // d(r1)/dx = 1
316                // d(r2)/dx = 2
317                // d(r0)/dy = 1
318                // d(r1)/dy = -1
319                // d(r2)/dy = 1
320
321                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]; // Initial guess
332        let cfg = NewtonCfg::<f64>::sparse().with_threads(1);
333
334        let result = crate::solve(&mut model, &mut x, cfg);
335
336        // The solver should now work with QR.
337        assert!(result.is_ok());
338        let iters = result.unwrap();
339        assert!(iters > 0 && iters <= 25);
340
341        // Check that we found a least-squares solution
342        // The exact solution would be x=2, y=1 (satisfies first two equations exactly).
343        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        // Fit data to Gaussian: y = a * exp(-((x-mu)/sigma)^2)
351        // 3 parameters for amplitude, mean and std dev (a, mu, sigma) with 5 data points (overdetermined).
352        // https://en.wikipedia.org/wiki/Gaussian_function
353        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                // Jacobian: 5 residuals x 3 variables, all related.
376                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                // Data generated from y = 2.0 * exp(-((x-1.0)/0.8)^2).
398                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                    // dr/da = exp(-z^2)
457                    v[i] = exp_term;
458
459                    // dr/dmu = a * exp(-z^2) * 2z/sigma = gaussian * 2(xi-mu)/sigma^2
460                    v[i + n_eqn] = gaussian * 2.0 * (xi - mu) / (sigma * sigma);
461
462                    // dr/dsigma = a * exp(-z^2) * 2z^2/sigma = gaussian * 2(xi-mu)^2/sigma^3
463                    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        // Initial guess: amplitude, mean, std_dev.
472        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        // Use callback version for reporting.
486        let result = crate::solve_cb(&mut model, &mut x, cfg, callback);
487
488        // The solver should converge to the true parameters.
489        assert!(result.is_ok());
490        let iters = result.unwrap();
491        assert!(iters > 0 && iters <= 50);
492
493        // Check that we recovered the original Gaussian parameters:
494        // True values: a=2.0, mu=1.0, sigma=0.8.
495        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        // Equivalent test exists in geometric constraint playground.
518        // System: x^2 + y^2 = 1 (circle), x - y = 0 (line)
519        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                // d(r0)/dx = 2x
578                // d(r1)/dx = 1
579                // d(r0)/dy = 2y
580                // d(r1)/dy = -1
581                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        // The solution should be close to [0.7071, 0.7071].
610        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        // Print final result and residuals.
626        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        // Inconsistent system:
636        //   r0 = x^2 + y^2 - 1          (unit circle)
637        //   r1 = x - y                  (x and y equal)
638        //   r2 = x + y - 2              (sum equals 2)
639        //
640        // No exact solution exists. The least-squares minimiser satisfies.
641        //   J^T r = 0  =>  x = y = (1/2)^(1/3) ≈ 0.793700526.
642        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                // Full 3x2 Jacobian sparsity, column-major (rows 0..2 for each col)
664                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                // r0 = x^2 + y^2 - 1
700                // r1 = x - y
701                // r2 = x + y - 2
702                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                // Column 0 (x): [dr0/dx, dr1/dx, dr2/dx] = [2x, 1, 1]
709                // Column 1 (y): [dr0/dy, dr1/dy, dr2/dy] = [2y, -1, 1]
710
711                let v = self.jac.values_mut();
712                v[0] = 2.0 * x[0];
713                v[1] = 1.0; // (row 1, col 0)
714                v[2] = 1.0; // (row 2, col 0)
715                v[3] = 2.0 * x[1]; // (row 0, col 1)
716                v[4] = -1.0; // (row 1, col 1)
717                v[5] = 1.0; // (row 2, col 1)
718            }
719        }
720
721        let mut system = InconsistentSystem::new();
722        let mut x = [0.5_f64, 0.5_f64]; // Initial guess
723
724        let cfg = NewtonCfg::<f64>::sparse()
725            .with_adaptive(true)
726            .with_tol_grad(1e-8)
727            .with_tol_step(0.0) // Disable step tolerance to focus on gradient tolerance.
728            .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        // Expected least-squares solution: x = y = (1/2)^(1/3)
749        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        // Verify we reached a (near) least-squares stationary point: J^T r ~= 0.
770        let mut r = [0.0; 3];
771        system.residual(&x, &mut r);
772        // J^T r components for this problem:
773        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        // Print final result and residuals / SSE.
783        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; // Very tight residual tolerance
793        let gtol = 1e-6; // Slack gradient tolerance should solve first.
794        let xtol = 0.0; // Disable step tolerance to focus on gradient convergence.
795
796        // Test that sparse matrices support gradient norm convergence checking.
797        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                // f1 depends only on x, f2 depends only on y.
819                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                // Sparse system: f1 = (x-1)^2, f2 = (y-2)^2
849                // Each residual depends on only one variable.
850                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                // Jacobian is diagonal:
856                // df1/dx = 2(x-1)
857                // df1/dy = 0 (not stored)
858                // df2/dx = 0 (not stored)
859                // df2/dy = 2(y-2)
860
861                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        // Start far from the solution.
870        let mut x = [0.0_f64, 0.0_f64];
871
872        // Configure to use sparse format with gradient tolerance.
873        let cfg = NewtonCfg::<f64>::sparse()
874            .with_tol(ftol)
875            .with_tol_grad(gtol) // Should converge via gradient tolerance first.
876            .with_tol_step(xtol) // Disable step tolerance.
877            .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        // Should converge close to the minimum at (1, 2).
902        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        // Verify gradient computation: J^T * r for sparse diagonal matrix.
917        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        // For diagonal sparse matrix: grad = [J(0,0)*r[0], J(1,1)*r[1]].
923        let grad_x = jac_vals[0] * residual[0]; // Only df1/dx contributes.
924        let grad_y = jac_vals[1] * residual[1]; // Only df2/dy contributes.
925        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; // Very tight residual tolerance
947        let gtol = 1e-6; // Slack gradient tolerance should solve first.
948        let xtol = 0.0; // Disable step tolerance to focus on gradient convergence.
949
950        // Test that dense matrices support gradient norm convergence checking.
951        // We'll use a simple system that should converge via gradient tolerance.
952        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                // Simple quadratic system: f1 = (x-1)^2, f2 = (y-2)^2
1008                // Minimum at x=1, y=2 with residual = 0
1009                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                // Jacobian:
1015                // df1/dx = 2(x-1), df1/dy = 0
1016                // df2/dx = 0,      df2/dy = 2(y-2)
1017
1018                // df1/dx
1019                // df2/dx
1020                // df1/dy
1021                // df2/dy
1022
1023                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        // Start far from the solution.
1034        let mut x = [0.0_f64, 0.0_f64];
1035
1036        // Configure to use dense format with gradient tolerance.
1037        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            // Check if we have a small gradient but large residual.
1046            if stats.residual > 1e-8 && stats.iter > 1 {
1047                // This suggests we're converging via gradient, not residual.
1048                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        // Should converge close to the minimum at (1, 2).
1072        let tol = 1e-2; // More lenient tolerance since we're using gradient convergence.
1073        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        // Verify the dense gradient computation worked by manually computing gradient norm.
1087        let mut residual = [0.0; 2];
1088        model.residual(&x, &mut residual);
1089
1090        // Compute gradient manually: J^T * r.
1091        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}