1use pounce_common::types::Number;
16
17#[derive(Debug, Clone)]
19pub struct BlockSolveOptions {
20 pub max_iter: usize,
22 pub tol: Number,
24 pub min_step: Number,
26 pub max_dim: usize,
29 pub bounds: Option<(Vec<Number>, Vec<Number>)>,
35}
36
37impl Default for BlockSolveOptions {
38 fn default() -> Self {
39 Self {
40 max_iter: 30,
41 tol: 1e-8,
42 min_step: 1e-6,
43 max_dim: 8,
44 bounds: None,
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum BlockSolveError {
52 Diverged,
55 Singular,
57 TooLarge,
59 MaxIterReached,
61 EvalFailed,
63 OutOfBounds,
68}
69
70#[derive(Debug, Clone)]
72pub struct BlockSolveOutcome {
73 pub x: Vec<Number>,
75 pub residual_norm: Number,
77 pub iterations: usize,
79}
80
81pub trait BlockEquations {
84 fn dim(&self) -> usize;
85 fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool;
86 fn jacobian(&mut self, x: &[Number], jac_row_major: &mut [Number]) -> bool;
87}
88
89pub trait BlockSolver {
93 fn solve(
94 &mut self,
95 x0: &[Number],
96 eqs: &mut dyn BlockEquations,
97 options: &BlockSolveOptions,
98 ) -> Result<BlockSolveOutcome, BlockSolveError>;
99}
100
101#[derive(Debug, Default, Clone, Copy)]
103pub struct DampedNewtonSolver;
104
105pub trait LargeBlockSolver {
115 fn solve_large(
116 &mut self,
117 x0: &[Number],
118 eqs: &mut dyn BlockEquations,
119 options: &BlockSolveOptions,
120 ) -> Result<BlockSolveOutcome, BlockSolveError>;
121}
122
123#[derive(Debug, Default, Clone, Copy)]
129pub struct RelaxedNewtonSolver;
130
131impl LargeBlockSolver for RelaxedNewtonSolver {
132 fn solve_large(
133 &mut self,
134 x0: &[Number],
135 eqs: &mut dyn BlockEquations,
136 options: &BlockSolveOptions,
137 ) -> Result<BlockSolveOutcome, BlockSolveError> {
138 let relaxed = BlockSolveOptions {
143 tol: options.tol,
144 max_iter: options.max_iter.max(200),
145 min_step: options.min_step.min(1e-10),
146 max_dim: options.max_dim.max(64),
147 bounds: options.bounds.clone(),
148 };
149 DampedNewtonSolver.solve(x0, eqs, &relaxed)
150 }
151}
152
153impl BlockSolver for DampedNewtonSolver {
154 fn solve(
186 &mut self,
187 x0: &[Number],
188 eqs: &mut dyn BlockEquations,
189 options: &BlockSolveOptions,
190 ) -> Result<BlockSolveOutcome, BlockSolveError> {
191 let n = eqs.dim();
192 if n > options.max_dim {
193 return Err(BlockSolveError::TooLarge);
194 }
195 if x0.len() != n {
196 return Err(BlockSolveError::EvalFailed);
197 }
198
199 let mut x: Vec<Number> = x0.to_vec();
200 let mut f = vec![0.0; n];
201 let mut f_new = vec![0.0; n];
202 let mut jac = vec![0.0; n * n];
203 let mut rhs = vec![0.0; n];
204 let mut x_new = vec![0.0; n];
205
206 if !eqs.eval(&x, &mut f) {
207 return Err(BlockSolveError::EvalFailed);
208 }
209 let mut res = inf_norm(&f);
210
211 for iter in 0..options.max_iter {
212 if res < options.tol {
213 check_bounds(&x, options)?;
214 return Ok(BlockSolveOutcome {
215 x,
216 residual_norm: res,
217 iterations: iter,
218 });
219 }
220 if !eqs.jacobian(&x, &mut jac) {
221 return Err(BlockSolveError::EvalFailed);
222 }
223 for i in 0..n {
224 rhs[i] = -f[i];
225 }
226 let piv =
227 lu_factor_partial_pivot(&mut jac, n).map_err(|_| BlockSolveError::Singular)?;
228 lu_solve(&jac, &piv, &mut rhs, n);
229 let mut alpha: Number = 1.0;
233 let mut accepted = false;
234 while alpha >= options.min_step {
235 for i in 0..n {
236 x_new[i] = x[i] + alpha * rhs[i];
237 }
238 if !eqs.eval(&x_new, &mut f_new) {
239 return Err(BlockSolveError::EvalFailed);
240 }
241 let res_new = inf_norm(&f_new);
242 if res_new < res {
243 x.copy_from_slice(&x_new);
244 f.copy_from_slice(&f_new);
245 res = res_new;
246 accepted = true;
247 break;
248 }
249 alpha *= 0.5;
250 }
251 if !accepted {
252 return Err(BlockSolveError::Diverged);
253 }
254 }
255 if res < options.tol {
257 check_bounds(&x, options)?;
258 Ok(BlockSolveOutcome {
259 x,
260 residual_norm: res,
261 iterations: options.max_iter,
262 })
263 } else {
264 Err(BlockSolveError::MaxIterReached)
265 }
266 }
267}
268
269fn check_bounds(x: &[Number], options: &BlockSolveOptions) -> Result<(), BlockSolveError> {
274 if let Some((lo, hi)) = &options.bounds {
275 let slop = options.tol.max(1e-12);
277 for (i, &xi) in x.iter().enumerate() {
278 if xi < lo[i] - slop || xi > hi[i] + slop {
279 return Err(BlockSolveError::OutOfBounds);
280 }
281 }
282 }
283 Ok(())
284}
285
286fn inf_norm(v: &[Number]) -> Number {
287 v.iter().map(|x| x.abs()).fold(0.0, Number::max)
288}
289
290pub(crate) fn lu_factor_partial_pivot(a: &mut [Number], n: usize) -> Result<Vec<usize>, ()> {
298 let mut piv: Vec<usize> = (0..n).collect();
299 for k in 0..n {
300 let mut max_abs: Number = 0.0;
302 let mut pivot_row = k;
303 for i in k..n {
304 let v = a[i * n + k].abs();
305 if v > max_abs {
306 max_abs = v;
307 pivot_row = i;
308 }
309 }
310 if max_abs < 1e-14 {
311 return Err(());
312 }
313 if pivot_row != k {
314 for j in 0..n {
315 a.swap(k * n + j, pivot_row * n + j);
316 }
317 piv.swap(k, pivot_row);
318 }
319 let pivot = a[k * n + k];
321 for i in (k + 1)..n {
322 let factor = a[i * n + k] / pivot;
323 a[i * n + k] = factor;
324 for j in (k + 1)..n {
325 let upper = a[k * n + j];
326 a[i * n + j] -= factor * upper;
327 }
328 }
329 }
330 Ok(piv)
331}
332
333pub(crate) fn lu_solve(a: &[Number], piv: &[usize], b: &mut [Number], n: usize) {
337 let mut pb = vec![0.0; n];
339 for i in 0..n {
340 pb[i] = b[piv[i]];
341 }
342 for i in 0..n {
344 let mut sum = pb[i];
345 for j in 0..i {
346 sum -= a[i * n + j] * pb[j];
347 }
348 pb[i] = sum;
349 }
350 for i in (0..n).rev() {
352 let mut sum = pb[i];
353 for j in (i + 1)..n {
354 sum -= a[i * n + j] * pb[j];
355 }
356 pb[i] = sum / a[i * n + i];
357 }
358 b.copy_from_slice(&pb);
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
368 fn lu_solves_identity() {
369 let mut a = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
370 let mut b = vec![1.0, 2.0, 3.0];
371 let piv = lu_factor_partial_pivot(&mut a, 3).expect("non-singular");
372 lu_solve(&a, &piv, &mut b, 3);
373 assert!((b[0] - 1.0).abs() < 1e-12);
374 assert!((b[1] - 2.0).abs() < 1e-12);
375 assert!((b[2] - 3.0).abs() < 1e-12);
376 }
377
378 #[test]
379 fn lu_solves_pivoted_2x2() {
380 let mut a = vec![0.0, 1.0, 1.0, 0.0];
382 let mut b = vec![3.0, 4.0];
383 let piv = lu_factor_partial_pivot(&mut a, 2).expect("non-singular");
384 lu_solve(&a, &piv, &mut b, 2);
385 assert!((b[0] - 4.0).abs() < 1e-12);
386 assert!((b[1] - 3.0).abs() < 1e-12);
387 }
388
389 #[test]
390 fn lu_detects_singular() {
391 let mut a = vec![1.0, 2.0, 2.0, 4.0];
393 let result = lu_factor_partial_pivot(&mut a, 2);
394 assert!(result.is_err());
395 }
396
397 #[test]
400 fn relaxed_newton_solves_10_var_linear() {
401 let n = 10;
405 let mut a = vec![0.0; n * n];
406 let mut b = vec![0.0; n];
407 for i in 0..n {
408 a[i * n + i] = 2.0;
409 b[i] = (i + 1) as Number;
410 }
411 struct Lin {
412 a: Vec<Number>,
413 b: Vec<Number>,
414 n: usize,
415 }
416 impl BlockEquations for Lin {
417 fn dim(&self) -> usize {
418 self.n
419 }
420 fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool {
421 for i in 0..self.n {
422 let mut s = -self.b[i];
423 for j in 0..self.n {
424 s += self.a[i * self.n + j] * x[j];
425 }
426 f[i] = s;
427 }
428 true
429 }
430 fn jacobian(&mut self, _x: &[Number], j: &mut [Number]) -> bool {
431 j.copy_from_slice(&self.a);
432 true
433 }
434 }
435 let mut eqs = Lin { a, b, n };
436 let opts = BlockSolveOptions::default();
439
440 let lightweight_err = DampedNewtonSolver
442 .solve(&vec![0.0; n], &mut eqs, &opts)
443 .expect_err("default should reject 10-dim block");
444 assert_eq!(lightweight_err, BlockSolveError::TooLarge);
445
446 let out = RelaxedNewtonSolver
448 .solve_large(&vec![0.0; n], &mut eqs, &opts)
449 .expect("relaxed solver handles 10-dim");
450 for i in 0..n {
451 assert!((out.x[i] - (i + 1) as Number / 2.0).abs() < 1e-10);
452 }
453 }
454
455 struct LinearF {
458 a: Vec<Number>,
459 b: Vec<Number>,
460 n: usize,
461 }
462 impl BlockEquations for LinearF {
463 fn dim(&self) -> usize {
464 self.n
465 }
466 fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool {
467 for i in 0..self.n {
468 let mut s = -self.b[i];
469 for j in 0..self.n {
470 s += self.a[i * self.n + j] * x[j];
471 }
472 f[i] = s;
473 }
474 true
475 }
476 fn jacobian(&mut self, _x: &[Number], j: &mut [Number]) -> bool {
477 j.copy_from_slice(&self.a);
478 true
479 }
480 }
481
482 #[test]
483 fn newton_linear_1d() {
484 let mut eqs = LinearF {
486 a: vec![1.0],
487 b: vec![3.0],
488 n: 1,
489 };
490 let opt = BlockSolveOptions::default();
491 let out = DampedNewtonSolver
492 .solve(&[0.0], &mut eqs, &opt)
493 .expect("converges");
494 assert!((out.x[0] - 3.0).abs() < 1e-10);
495 assert!(out.residual_norm < 1e-10);
496 }
497
498 #[test]
499 fn newton_linear_2d() {
500 let mut eqs = LinearF {
502 a: vec![1.0, 1.0, 1.0, -1.0],
503 b: vec![1.0, 0.0],
504 n: 2,
505 };
506 let opt = BlockSolveOptions::default();
507 let out = DampedNewtonSolver
508 .solve(&[0.0, 0.0], &mut eqs, &opt)
509 .expect("converges");
510 assert!((out.x[0] - 0.5).abs() < 1e-10);
511 assert!((out.x[1] - 0.5).abs() < 1e-10);
512 }
513
514 struct Quadratic;
515 impl BlockEquations for Quadratic {
516 fn dim(&self) -> usize {
517 1
518 }
519 fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool {
520 f[0] = x[0] * x[0] - 4.0;
521 true
522 }
523 fn jacobian(&mut self, x: &[Number], j: &mut [Number]) -> bool {
524 j[0] = 2.0 * x[0];
525 true
526 }
527 }
528
529 #[test]
530 fn newton_quadratic() {
531 let mut eqs = Quadratic;
533 let opt = BlockSolveOptions::default();
534 let out = DampedNewtonSolver
535 .solve(&[1.0], &mut eqs, &opt)
536 .expect("converges");
537 assert!((out.x[0] - 2.0).abs() < 1e-8);
538 }
539
540 struct ThreeVar;
543 impl BlockEquations for ThreeVar {
544 fn dim(&self) -> usize {
545 3
546 }
547 fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool {
548 f[0] = x[0] * x[0] + x[1] * x[1] + x[2] * x[2] - 14.0;
549 f[1] = x[0] + x[1] + x[2] - 6.0;
550 f[2] = x[0] * x[1] * x[2] - 6.0;
551 true
552 }
553 fn jacobian(&mut self, x: &[Number], j: &mut [Number]) -> bool {
554 j[0] = 2.0 * x[0];
555 j[1] = 2.0 * x[1];
556 j[2] = 2.0 * x[2];
557 j[3] = 1.0;
558 j[4] = 1.0;
559 j[5] = 1.0;
560 j[6] = x[1] * x[2];
561 j[7] = x[0] * x[2];
562 j[8] = x[0] * x[1];
563 true
564 }
565 }
566
567 #[test]
568 fn newton_3var_nonlinear() {
569 let mut eqs = ThreeVar;
571 let opt = BlockSolveOptions::default();
572 let out = DampedNewtonSolver
573 .solve(&[1.1, 1.9, 3.05], &mut eqs, &opt)
574 .expect("converges");
575 let mut sorted = out.x.clone();
578 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
579 assert!((sorted[0] - 1.0).abs() < 1e-6);
580 assert!((sorted[1] - 2.0).abs() < 1e-6);
581 assert!((sorted[2] - 3.0).abs() < 1e-6);
582 assert!(out.residual_norm < 1e-8);
583 }
584
585 struct Tan;
590 impl BlockEquations for Tan {
591 fn dim(&self) -> usize {
592 1
593 }
594 fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool {
595 f[0] = x[0].tan();
596 true
597 }
598 fn jacobian(&mut self, x: &[Number], j: &mut [Number]) -> bool {
599 let c = x[0].cos();
600 j[0] = 1.0 / (c * c);
601 true
602 }
603 }
604
605 #[test]
606 fn newton_diverges_when_starting_point_bad() {
607 let mut eqs = Tan;
608 let opt = BlockSolveOptions {
609 max_iter: 30,
610 tol: 1e-10,
611 ..Default::default()
619 };
620 let result = DampedNewtonSolver.solve(&[1.5], &mut eqs, &opt);
621 match result {
622 Err(BlockSolveError::Diverged)
623 | Err(BlockSolveError::MaxIterReached)
624 | Err(BlockSolveError::Singular) => {}
625 Ok(out) => {
626 assert!(out.residual_norm < opt.tol);
629 }
630 Err(e) => panic!("unexpected error variant: {e:?}"),
631 }
632 }
633
634 #[test]
635 fn newton_rejects_too_large() {
636 struct Big;
637 impl BlockEquations for Big {
638 fn dim(&self) -> usize {
639 9
640 }
641 fn eval(&mut self, _x: &[Number], f: &mut [Number]) -> bool {
642 for v in f.iter_mut() {
643 *v = 0.0;
644 }
645 true
646 }
647 fn jacobian(&mut self, _x: &[Number], j: &mut [Number]) -> bool {
648 for v in j.iter_mut() {
649 *v = 0.0;
650 }
651 true
652 }
653 }
654 let mut eqs = Big;
655 let opt = BlockSolveOptions::default();
656 let err = DampedNewtonSolver
657 .solve(&[0.0; 9], &mut eqs, &opt)
658 .expect_err("should reject");
659 assert_eq!(err, BlockSolveError::TooLarge);
660 }
661
662 struct FuzzRng(u64);
667 impl FuzzRng {
668 fn new(seed: u64) -> Self {
669 Self(seed)
670 }
671 fn next_u64(&mut self) -> u64 {
672 self.0 = self
673 .0
674 .wrapping_mul(6364136223846793005)
675 .wrapping_add(1442695040888963407);
676 self.0 >> 32
677 }
678 fn unit(&mut self) -> Number {
679 let raw = (self.next_u64() & 0x3fff_ffff) as Number;
680 raw / (1u64 << 29) as Number - 1.0
681 }
682 }
683
684 #[test]
692 fn newton_fuzz_random_linear_vs_direct_lu() {
693 let mut rng = FuzzRng::new(0xfeed_face_dead_b33f);
694
695 let mut tested = 0usize;
696 for _ in 0..30 {
697 let n = 1 + (rng.next_u64() % 6) as usize; let mut a = vec![0.0; n * n];
701 for i in 0..n {
702 for j in 0..n {
703 a[i * n + j] = if i == j {
704 2.0 + rng.unit().abs()
705 } else {
706 0.3 * rng.unit()
707 };
708 }
709 }
710 let x_star: Vec<Number> = (0..n).map(|_| rng.unit()).collect();
712 let mut b = vec![0.0; n];
713 for i in 0..n {
714 let mut s = 0.0;
715 for j in 0..n {
716 s += a[i * n + j] * x_star[j];
717 }
718 b[i] = s;
719 }
720
721 let mut a_ref = a.clone();
723 let piv = lu_factor_partial_pivot(&mut a_ref, n).expect("well-conditioned");
724 let mut x_lu = b.clone();
725 lu_solve(&a_ref, &piv, &mut x_lu, n);
726
727 struct LinSys {
729 a: Vec<Number>,
730 b: Vec<Number>,
731 n: usize,
732 }
733 impl BlockEquations for LinSys {
734 fn dim(&self) -> usize {
735 self.n
736 }
737 fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool {
738 for i in 0..self.n {
739 let mut s = -self.b[i];
740 for j in 0..self.n {
741 s += self.a[i * self.n + j] * x[j];
742 }
743 f[i] = s;
744 }
745 true
746 }
747 fn jacobian(&mut self, _x: &[Number], j: &mut [Number]) -> bool {
748 j.copy_from_slice(&self.a);
749 true
750 }
751 }
752 let mut eqs = LinSys {
753 a: a.clone(),
754 b: b.clone(),
755 n,
756 };
757 let x0 = vec![0.0; n];
759 let opt = BlockSolveOptions::default();
760 let out = DampedNewtonSolver
761 .solve(&x0, &mut eqs, &opt)
762 .expect("Newton converges on a well-conditioned linear system");
763
764 let mut max_diff: Number = 0.0;
767 for i in 0..n {
768 max_diff = max_diff.max((out.x[i] - x_lu[i]).abs());
769 max_diff = max_diff.max((out.x[i] - x_star[i]).abs());
770 }
771 assert!(
772 max_diff < 1e-10,
773 "Newton vs LU disagreement of {max_diff:.3e} on n={n}"
774 );
775 tested += 1;
776 }
777 assert_eq!(tested, 30);
778 }
779
780 #[test]
784 fn newton_fuzz_nonlinear_quadratic_root() {
785 let mut rng = FuzzRng::new(0xcafe_b00b_1337_5eed);
786
787 for trial in 0..20 {
788 let n = 1 + (rng.next_u64() % 4) as usize; let mut a = vec![0.0; n * n];
791 for i in 0..n {
792 for j in 0..n {
793 a[i * n + j] = if i == j {
794 3.0 + rng.unit().abs()
795 } else {
796 0.2 * rng.unit()
797 };
798 }
799 }
800 let x_star: Vec<Number> = (0..n).map(|_| rng.unit()).collect();
801 let eps = 0.1;
803
804 struct Nl {
805 a: Vec<Number>,
806 x_star: Vec<Number>,
807 eps: Number,
808 n: usize,
809 }
810 impl BlockEquations for Nl {
811 fn dim(&self) -> usize {
812 self.n
813 }
814 fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool {
815 for i in 0..self.n {
816 let mut s = 0.0;
817 for j in 0..self.n {
818 s += self.a[i * self.n + j] * (x[j] - self.x_star[j]);
819 }
820 let dxi = x[i] - self.x_star[i];
821 s += self.eps * dxi * dxi;
822 f[i] = s;
823 }
824 true
825 }
826 fn jacobian(&mut self, x: &[Number], j: &mut [Number]) -> bool {
827 for i in 0..self.n {
828 for k in 0..self.n {
829 let mut v = self.a[i * self.n + k];
830 if i == k {
831 v += 2.0 * self.eps * (x[i] - self.x_star[i]);
832 }
833 j[i * self.n + k] = v;
834 }
835 }
836 true
837 }
838 }
839
840 let x0: Vec<Number> = x_star.iter().map(|&v| v + 0.1 * rng.unit()).collect();
842 let mut eqs = Nl {
843 a: a.clone(),
844 x_star: x_star.clone(),
845 eps,
846 n,
847 };
848 let opt = BlockSolveOptions::default();
849 let out = DampedNewtonSolver
850 .solve(&x0, &mut eqs, &opt)
851 .unwrap_or_else(|e| panic!("trial {trial} (n={n}): {e:?}"));
852 let mut max_err: Number = 0.0;
853 for i in 0..n {
854 max_err = max_err.max((out.x[i] - x_star[i]).abs());
855 }
856 assert!(
857 max_err < 1e-7,
858 "trial {trial}: Newton missed root by {max_err:.3e}"
859 );
860 }
861 }
862
863 #[test]
864 fn newton_eval_failure_propagates() {
865 struct Failing;
866 impl BlockEquations for Failing {
867 fn dim(&self) -> usize {
868 1
869 }
870 fn eval(&mut self, _x: &[Number], _f: &mut [Number]) -> bool {
871 false
872 }
873 fn jacobian(&mut self, _x: &[Number], _j: &mut [Number]) -> bool {
874 true
875 }
876 }
877 let mut eqs = Failing;
878 let opt = BlockSolveOptions::default();
879 let err = DampedNewtonSolver
880 .solve(&[0.0], &mut eqs, &opt)
881 .expect_err("eval fails");
882 assert_eq!(err, BlockSolveError::EvalFailed);
883 }
884
885 #[test]
890 fn newton_out_of_bounds_rejects() {
891 let mut eqs = LinearF {
892 a: vec![1.0],
893 b: vec![3.0],
894 n: 1,
895 };
896 let opt = BlockSolveOptions {
897 bounds: Some((vec![0.0], vec![2.0])),
898 ..Default::default()
899 };
900 let err = DampedNewtonSolver
901 .solve(&[0.0], &mut eqs, &opt)
902 .expect_err("root outside bounds");
903 assert_eq!(err, BlockSolveError::OutOfBounds);
904 }
905
906 #[test]
909 fn newton_in_bounds_accepts() {
910 let mut eqs = LinearF {
911 a: vec![1.0],
912 b: vec![3.0],
913 n: 1,
914 };
915 let opt = BlockSolveOptions {
916 bounds: Some((vec![0.0], vec![10.0])),
917 ..Default::default()
918 };
919 let out = DampedNewtonSolver
920 .solve(&[0.0], &mut eqs, &opt)
921 .expect("root inside bounds");
922 assert!((out.x[0] - 3.0).abs() < 1e-10);
923 }
924}