1use crate::error::{IntegrateError, IntegrateResult};
25use scirs2_core::ndarray::{Array1, Array2};
26
27#[inline(always)]
32fn f64_to_f(v: f64) -> f64 {
33 v
34}
35
36fn gauss_solve(a: &mut Array2<f64>, b: &mut Array1<f64>) -> IntegrateResult<Array1<f64>> {
39 let n = b.len();
40 for col in 0..n {
41 let mut max_row = col;
43 let mut max_val = a[[col, col]].abs();
44 for row in (col + 1)..n {
45 let v = a[[row, col]].abs();
46 if v > max_val {
47 max_val = v;
48 max_row = row;
49 }
50 }
51 if max_val < 1e-300 {
52 return Err(IntegrateError::LinearSolveError(
53 "Singular matrix in continuation solve".to_string(),
54 ));
55 }
56 if max_row != col {
57 for j in col..n {
58 let tmp = a[[col, j]];
59 a[[col, j]] = a[[max_row, j]];
60 a[[max_row, j]] = tmp;
61 }
62 b.swap(col, max_row);
63 }
64 let pivot = a[[col, col]];
65 for row in (col + 1)..n {
66 let factor = a[[row, col]] / pivot;
67 for j in col..n {
68 let update = factor * a[[col, j]];
69 a[[row, j]] -= update;
70 }
71 let bup = factor * b[col];
72 b[row] -= bup;
73 }
74 }
75 let mut x = Array1::<f64>::zeros(n);
76 for i in (0..n).rev() {
77 let mut sum = b[i];
78 for j in (i + 1)..n {
79 sum -= a[[i, j]] * x[j];
80 }
81 x[i] = sum / a[[i, i]];
82 }
83 Ok(x)
84}
85
86fn numerical_jacobian_x<F>(f: &F, x: &Array1<f64>, lambda: f64, eps: f64) -> Array2<f64>
88where
89 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
90{
91 let n = x.len();
92 let mut jac = Array2::<f64>::zeros((n, n));
93 for j in 0..n {
94 let mut xp = x.clone();
95 let mut xm = x.clone();
96 xp[j] += eps;
97 xm[j] -= eps;
98 let fp = f(&xp, lambda);
99 let fm = f(&xm, lambda);
100 for i in 0..n {
101 jac[[i, j]] = (fp[i] - fm[i]) / (2.0 * eps);
102 }
103 }
104 jac
105}
106
107fn numerical_df_dlambda<F>(f: &F, x: &Array1<f64>, lambda: f64, eps: f64) -> Array1<f64>
109where
110 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
111{
112 let fp = f(x, lambda + eps);
113 let fm = f(x, lambda - eps);
114 let n = fp.len();
115 let mut df = Array1::<f64>::zeros(n);
116 for i in 0..n {
117 df[i] = (fp[i] - fm[i]) / (2.0 * eps);
118 }
119 df
120}
121
122#[derive(Debug, Clone)]
128pub struct BranchPoint {
129 pub x: Array1<f64>,
131 pub lambda: f64,
133 pub null_vector: Option<Array1<f64>>,
135}
136
137#[derive(Debug, Clone)]
139pub struct LimitPoint {
140 pub x: Array1<f64>,
142 pub lambda: f64,
144 pub branch_index: usize,
146 pub stability_change: bool,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum SolutionStability {
153 Stable,
155 Unstable,
157 Unknown,
159}
160
161#[derive(Debug, Clone)]
167pub struct ContinuationBranchResult {
168 pub x: Vec<Array1<f64>>,
170 pub lambda: Vec<f64>,
172 pub stability: Vec<SolutionStability>,
174 pub limit_points: Vec<LimitPoint>,
176 pub branch_points: Vec<BranchPoint>,
178 pub newton_iters: Vec<usize>,
180 pub success: bool,
182 pub message: String,
184}
185
186#[derive(Debug, Clone)]
192pub struct ContinuationConfig {
193 pub max_steps: usize,
195 pub ds: f64,
197 pub ds_min: f64,
199 pub ds_max: f64,
201 pub step_adapt_factor: f64,
203 pub newton_tol: f64,
205 pub max_newton_iter: usize,
207 pub fd_eps: f64,
209 pub compute_stability: bool,
211 pub limit_point_tol: f64,
213 pub desired_newton_iter: usize,
215}
216
217impl Default for ContinuationConfig {
218 fn default() -> Self {
219 Self {
220 max_steps: 500,
221 ds: 0.01,
222 ds_min: 1e-8,
223 ds_max: 1.0,
224 step_adapt_factor: 0.8,
225 newton_tol: 1e-10,
226 max_newton_iter: 20,
227 fd_eps: 1e-7,
228 compute_stability: true,
229 limit_point_tol: 1e-4,
230 desired_newton_iter: 5,
231 }
232 }
233}
234
235pub struct NaturalParameterContinuation;
260
261impl NaturalParameterContinuation {
262 pub fn run<F>(
264 f: &F,
265 x0: &Array1<f64>,
266 lambda0: f64,
267 lambda_end: f64,
268 cfg: &ContinuationConfig,
269 ) -> IntegrateResult<ContinuationBranchResult>
270 where
271 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
272 {
273 let n = x0.len();
274 let direction = if lambda_end >= lambda0 { 1.0 } else { -1.0 };
275 let mut ds = cfg.ds.abs() * direction;
276
277 let mut x = x0.clone();
278 let mut lambda = lambda0;
279
280 let mut xs = vec![x.clone()];
281 let mut lambdas = vec![lambda];
282 let mut stabilities = Vec::new();
283 let mut limit_points = Vec::new();
284 let mut branch_points_list = Vec::new();
285 let mut newton_iters = vec![0usize];
286
287 if cfg.compute_stability {
288 let stab = compute_stability_from_jacobian(f, &x, lambda, cfg.fd_eps);
289 stabilities.push(stab);
290 }
291
292 let mut prev_det = jacobian_determinant(f, &x, lambda, cfg.fd_eps);
293
294 for _step in 0..cfg.max_steps {
295 if (lambda + ds - lambda_end) * direction > 0.0 {
297 ds = (lambda_end - lambda).abs() * direction;
298 if ds.abs() < cfg.ds_min {
299 break;
300 }
301 }
302
303 let lambda_new = lambda + ds;
304
305 match newton_solve_continuation(f, &x, lambda_new, cfg) {
307 Ok((x_new, n_iters)) => {
308 let curr_det = jacobian_determinant(f, &x_new, lambda_new, cfg.fd_eps);
309
310 if prev_det * curr_det < 0.0 {
312 limit_points.push(LimitPoint {
313 x: x_new.clone(),
314 lambda: lambda_new,
315 branch_index: xs.len(),
316 stability_change: true,
317 });
318 }
319
320 if curr_det.abs() < cfg.limit_point_tol && (prev_det * curr_det >= 0.0) {
322 let null_vec =
323 approximate_null_vector(f, &x_new, lambda_new, cfg.fd_eps, n);
324 branch_points_list.push(BranchPoint {
325 x: x_new.clone(),
326 lambda: lambda_new,
327 null_vector: Some(null_vec),
328 });
329 }
330
331 let adapt_ratio = cfg.desired_newton_iter as f64 / n_iters.max(1) as f64;
333 let new_ds = (ds * adapt_ratio.sqrt())
334 .abs()
335 .clamp(cfg.ds_min, cfg.ds_max);
336 ds = new_ds * direction;
337
338 prev_det = curr_det;
339 x = x_new.clone();
340 lambda = lambda_new;
341
342 xs.push(x_new);
343 lambdas.push(lambda);
344 newton_iters.push(n_iters);
345
346 if cfg.compute_stability {
347 let stab = compute_stability_from_jacobian(f, &x, lambda, cfg.fd_eps);
348 stabilities.push(stab);
349 }
350
351 if (lambda - lambda_end).abs() < cfg.ds_min.abs() {
353 break;
354 }
355 }
356 Err(_) => {
357 ds *= cfg.step_adapt_factor;
359 if ds.abs() < cfg.ds_min {
360 return Ok(ContinuationBranchResult {
361 x: xs,
362 lambda: lambdas,
363 stability: stabilities,
364 limit_points,
365 branch_points: branch_points_list,
366 newton_iters,
367 success: false,
368 message: "Step size below minimum: Newton convergence failure"
369 .to_string(),
370 });
371 }
372 }
373 }
374 }
375
376 Ok(ContinuationBranchResult {
377 x: xs,
378 lambda: lambdas,
379 stability: stabilities,
380 limit_points,
381 branch_points: branch_points_list,
382 newton_iters,
383 success: true,
384 message: "Natural parameter continuation completed".to_string(),
385 })
386 }
387}
388
389pub struct PseudoArcLengthContinuation;
408
409impl PseudoArcLengthContinuation {
410 pub fn run<F>(
421 f: &F,
422 x0: &Array1<f64>,
423 lambda0: f64,
424 lambda_range: (f64, f64),
425 cfg: &ContinuationConfig,
426 direction: f64,
427 ) -> IntegrateResult<ContinuationBranchResult>
428 where
429 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
430 {
431 let n = x0.len();
432 let (lambda_min, lambda_max) = lambda_range;
433
434 let mut x = x0.clone();
435 let mut lambda = lambda0;
436
437 let (mut tx, mut tl) = compute_tangent(f, &x, lambda, direction, cfg.fd_eps, n)?;
439
440 let mut xs = vec![x.clone()];
441 let mut lambdas = vec![lambda];
442 let mut stabilities = Vec::new();
443 let mut limit_points = Vec::new();
444 let mut branch_points_list = Vec::new();
445 let mut newton_iters = vec![0usize];
446
447 if cfg.compute_stability {
448 let stab = compute_stability_from_jacobian(f, &x, lambda, cfg.fd_eps);
449 stabilities.push(stab);
450 }
451
452 let mut prev_det = jacobian_determinant(f, &x, lambda, cfg.fd_eps);
453 let mut ds = cfg.ds;
454
455 for _step in 0..cfg.max_steps {
456 let x_pred = {
458 let mut xp = Array1::<f64>::zeros(n);
459 for i in 0..n {
460 xp[i] = x[i] + ds * tx[i];
461 }
462 xp
463 };
464 let lambda_pred = lambda + ds * tl;
465
466 if lambda_pred < lambda_min || lambda_pred > lambda_max {
468 let lambda_target = if lambda_pred < lambda_min {
470 lambda_min
471 } else {
472 lambda_max
473 };
474 let ds_boundary = (lambda_target - lambda) / tl.max(1e-300);
476 if ds_boundary.abs() < cfg.ds_min {
477 break;
478 }
479 let x_boundary = {
480 let mut xb = Array1::<f64>::zeros(n);
481 for i in 0..n {
482 xb[i] = x[i] + ds_boundary * tx[i];
483 }
484 xb
485 };
486 if let Ok((x_b, ni)) = newton_solve_continuation(f, &x_boundary, lambda_target, cfg)
488 {
489 xs.push(x_b);
490 lambdas.push(lambda_target);
491 newton_iters.push(ni);
492 }
493 break;
494 }
495
496 match newton_extended(f, &x_pred, lambda_pred, &x, lambda, &tx, tl, ds, cfg, n) {
498 Ok((x_new, lambda_new, n_iters)) => {
499 let curr_det = jacobian_determinant(f, &x_new, lambda_new, cfg.fd_eps);
500
501 if prev_det * curr_det < 0.0 {
503 limit_points.push(LimitPoint {
504 x: x_new.clone(),
505 lambda: lambda_new,
506 branch_index: xs.len(),
507 stability_change: true,
508 });
509 }
510
511 if curr_det.abs() < cfg.limit_point_tol && (prev_det * curr_det >= 0.0) {
513 let null_vec =
514 approximate_null_vector(f, &x_new, lambda_new, cfg.fd_eps, n);
515 branch_points_list.push(BranchPoint {
516 x: x_new.clone(),
517 lambda: lambda_new,
518 null_vector: Some(null_vec),
519 });
520 }
521
522 if let Ok((new_tx, new_tl)) =
524 compute_tangent(f, &x_new, lambda_new, tl.signum(), cfg.fd_eps, n)
525 {
526 let dot = new_tx
528 .iter()
529 .zip(tx.iter())
530 .fold(0.0, |s, (&a, &b)| s + a * b)
531 + new_tl * tl;
532 if dot < 0.0 {
533 tx = new_tx.mapv(|v| -v);
534 tl = -new_tl;
535 } else {
536 tx = new_tx;
537 tl = new_tl;
538 }
539 }
540
541 let adapt = cfg.desired_newton_iter as f64 / n_iters.max(1) as f64;
543 ds = (ds * adapt.sqrt()).clamp(cfg.ds_min, cfg.ds_max);
544
545 prev_det = curr_det;
546 x = x_new.clone();
547 lambda = lambda_new;
548
549 xs.push(x_new);
550 lambdas.push(lambda);
551 newton_iters.push(n_iters);
552
553 if cfg.compute_stability {
554 let stab = compute_stability_from_jacobian(f, &x, lambda, cfg.fd_eps);
555 stabilities.push(stab);
556 }
557 }
558 Err(_) => {
559 ds *= cfg.step_adapt_factor;
560 if ds.abs() < cfg.ds_min {
561 return Ok(ContinuationBranchResult {
562 x: xs,
563 lambda: lambdas,
564 stability: stabilities,
565 limit_points,
566 branch_points: branch_points_list,
567 newton_iters,
568 success: false,
569 message: "Step size below minimum".to_string(),
570 });
571 }
572 }
573 }
574 }
575
576 Ok(ContinuationBranchResult {
577 x: xs,
578 lambda: lambdas,
579 stability: stabilities,
580 limit_points,
581 branch_points: branch_points_list,
582 newton_iters,
583 success: true,
584 message: "Pseudo-arclength continuation completed".to_string(),
585 })
586 }
587}
588
589fn newton_solve_continuation<F>(
595 f: &F,
596 x_guess: &Array1<f64>,
597 lambda: f64,
598 cfg: &ContinuationConfig,
599) -> IntegrateResult<(Array1<f64>, usize)>
600where
601 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
602{
603 let n = x_guess.len();
604 let mut x = x_guess.clone();
605
606 for iter in 0..cfg.max_newton_iter {
607 let fx = f(&x, lambda);
608 let res_norm: f64 = fx.iter().map(|&v| v * v).sum::<f64>().sqrt();
609 if res_norm < cfg.newton_tol {
610 return Ok((x, iter + 1));
611 }
612 let mut jac = numerical_jacobian_x(f, &x, lambda, cfg.fd_eps);
613 let mut neg_fx = fx.mapv(|v| -v);
614 let delta = gauss_solve(&mut jac, &mut neg_fx)?;
615 for i in 0..n {
616 x[i] += delta[i];
617 }
618 }
619
620 let fx_final = f(&x, lambda);
621 let res_norm: f64 = fx_final.iter().map(|&v| v * v).sum::<f64>().sqrt();
622 if res_norm < cfg.newton_tol * 100.0 {
623 return Ok((x, cfg.max_newton_iter));
624 }
625
626 Err(IntegrateError::ConvergenceError(format!(
627 "Newton did not converge in {} iterations (res={:.3e})",
628 cfg.max_newton_iter, res_norm
629 )))
630}
631
632fn compute_tangent<F>(
635 f: &F,
636 x: &Array1<f64>,
637 lambda: f64,
638 direction_sign: f64,
639 eps: f64,
640 n: usize,
641) -> IntegrateResult<(Array1<f64>, f64)>
642where
643 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
644{
645 let jx = numerical_jacobian_x(f, x, lambda, eps);
646 let jl = numerical_df_dlambda(f, x, lambda, eps);
647
648 let mut jx_copy = jx.clone();
651 let mut neg_jl = jl.mapv(|v| -v);
652
653 match gauss_solve(&mut jx_copy, &mut neg_jl) {
654 Ok(v) => {
655 let v_norm_sq: f64 = v.iter().map(|&vi| vi * vi).sum();
657 let tl_abs = 1.0 / (1.0 + v_norm_sq).sqrt();
658 let mut tx = v.mapv(|vi| vi * tl_abs);
659 let mut tl = tl_abs;
660
661 if tl * direction_sign < 0.0 {
663 tx = tx.mapv(|vi| -vi);
664 tl = -tl;
665 }
666 Ok((tx, tl))
667 }
668 Err(_) => {
669 let tx = Array1::<f64>::zeros(n);
671 let tl = direction_sign;
672 Ok((tx, tl))
673 }
674 }
675}
676
677fn newton_extended<F>(
681 f: &F,
682 x_pred: &Array1<f64>,
683 lambda_pred: f64,
684 x0: &Array1<f64>,
685 lambda0: f64,
686 tx: &Array1<f64>,
687 tl: f64,
688 ds: f64,
689 cfg: &ContinuationConfig,
690 n: usize,
691) -> IntegrateResult<(Array1<f64>, f64, usize)>
692where
693 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
694{
695 let mut x = x_pred.clone();
696 let mut lam = lambda_pred;
697 let size = n + 1;
698
699 for iter in 0..cfg.max_newton_iter {
700 let fx = f(&x, lam);
701
702 let arc_res: f64 = x
704 .iter()
705 .zip(x0.iter())
706 .zip(tx.iter())
707 .fold(0.0, |s, ((&xi, &x0i), &txi)| s + (xi - x0i) * txi)
708 + (lam - lambda0) * tl
709 - ds;
710
711 let res_norm: f64 = fx.iter().map(|&v| v * v).sum::<f64>() + arc_res * arc_res;
713 let res_norm = res_norm.sqrt();
714
715 if res_norm < cfg.newton_tol {
716 return Ok((x, lam, iter + 1));
717 }
718
719 let jx = numerical_jacobian_x(f, &x, lam, cfg.fd_eps);
723 let jl = numerical_df_dlambda(f, &x, lam, cfg.fd_eps);
724
725 let mut big_a = Array2::<f64>::zeros((size, size));
726 let mut big_b = Array1::<f64>::zeros(size);
727
728 for i in 0..n {
729 for j in 0..n {
730 big_a[[i, j]] = jx[[i, j]];
731 }
732 big_a[[i, n]] = jl[i];
733 big_b[i] = -fx[i];
734 }
735 for j in 0..n {
736 big_a[[n, j]] = tx[j];
737 }
738 big_a[[n, n]] = tl;
739 big_b[n] = -arc_res;
740
741 let delta = gauss_solve(&mut big_a, &mut big_b)?;
742
743 for i in 0..n {
744 x[i] += delta[i];
745 }
746 lam += delta[n];
747 }
748
749 Err(IntegrateError::ConvergenceError(format!(
750 "Pseudo-arclength Newton did not converge in {} iterations",
751 cfg.max_newton_iter
752 )))
753}
754
755fn jacobian_determinant<F>(f: &F, x: &Array1<f64>, lambda: f64, eps: f64) -> f64
757where
758 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
759{
760 let n = x.len();
761 let mut jac = numerical_jacobian_x(f, x, lambda, eps);
762
763 let mut sign = 1.0_f64;
765 let mut det_approx = 1.0_f64;
766
767 for col in 0..n {
768 let mut max_row = col;
769 let mut max_val = jac[[col, col]].abs();
770 for row in (col + 1)..n {
771 let v = jac[[row, col]].abs();
772 if v > max_val {
773 max_val = v;
774 max_row = row;
775 }
776 }
777 if max_val < 1e-300 {
778 return 0.0;
779 }
780 if max_row != col {
781 for j in col..n {
782 let tmp = jac[[col, j]];
783 jac[[col, j]] = jac[[max_row, j]];
784 jac[[max_row, j]] = tmp;
785 }
786 sign = -sign;
787 }
788 det_approx *= jac[[col, col]];
789 let pivot = jac[[col, col]];
790 for row in (col + 1)..n {
791 let factor = jac[[row, col]] / pivot;
792 for j in col..n {
793 let u = factor * jac[[col, j]];
794 jac[[row, j]] -= u;
795 }
796 }
797 }
798
799 sign * det_approx
800}
801
802fn compute_stability_from_jacobian<F>(
805 f: &F,
806 x: &Array1<f64>,
807 lambda: f64,
808 eps: f64,
809) -> SolutionStability
810where
811 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
812{
813 let n = x.len();
814 let jac = numerical_jacobian_x(f, x, lambda, eps);
815
816 let mut all_stable = true;
818 let mut any_unstable = false;
819
820 for i in 0..n {
821 let center = jac[[i, i]];
822 let radius: f64 = (0..n).filter(|&j| j != i).map(|j| jac[[i, j]].abs()).sum();
823 let rightmost = center + radius;
824 let leftmost = center - radius;
825
826 if leftmost > 0.0 {
827 any_unstable = true;
828 all_stable = false;
829 } else if rightmost > 0.0 {
830 all_stable = false;
831 }
832 }
833
834 if any_unstable {
835 SolutionStability::Unstable
836 } else if all_stable {
837 SolutionStability::Stable
838 } else {
839 SolutionStability::Unknown
840 }
841}
842
843fn approximate_null_vector<F>(
845 f: &F,
846 x: &Array1<f64>,
847 lambda: f64,
848 eps: f64,
849 n: usize,
850) -> Array1<f64>
851where
852 F: Fn(&Array1<f64>, f64) -> Array1<f64>,
853{
854 let jac = numerical_jacobian_x(f, x, lambda, eps);
855
856 let mut min_col = 0;
858 let mut min_val = jac[[0, 0]].abs();
859 for i in 1..n {
860 let v = jac[[i, i]].abs();
861 if v < min_val {
862 min_val = v;
863 min_col = i;
864 }
865 }
866
867 let mut null = Array1::<f64>::zeros(n);
869 null[min_col] = 1.0;
870 null
871}
872
873#[allow(dead_code)]
875fn _use_f64_to_f() {
876 let _ = f64_to_f(0.0);
877}
878
879#[cfg(test)]
884mod tests {
885 use super::*;
886 use scirs2_core::ndarray::array;
887
888 fn fold_residual(x: &Array1<f64>, lambda: f64) -> Array1<f64> {
891 array![x[0] * x[0] * x[0] - x[0] - lambda]
892 }
893
894 #[test]
895 fn test_natural_continuation_simple() {
896 let x0 = array![1.0];
899 let cfg = ContinuationConfig {
900 max_steps: 100,
901 ds: 0.05,
902 ..Default::default()
903 };
904 let result = NaturalParameterContinuation::run(&fold_residual, &x0, 0.0, 0.3, &cfg)
905 .expect("Natural continuation failed");
906
907 assert!(result.lambda.len() > 2, "Should have more than 2 points");
908 let last_lambda = *result.lambda.last().expect("no lambda");
909 assert!(
910 (last_lambda - 0.3).abs() < 0.1,
911 "Last lambda={} should be near 0.3",
912 last_lambda
913 );
914 }
915
916 #[test]
917 fn test_pseudo_arclength_continuation() {
918 let x0 = array![1.0];
920 let cfg = ContinuationConfig {
921 max_steps: 200,
922 ds: 0.05,
923 ds_max: 0.2,
924 compute_stability: false,
925 ..Default::default()
926 };
927 let result =
928 PseudoArcLengthContinuation::run(&fold_residual, &x0, 0.0, (-2.0, 2.0), &cfg, 1.0)
929 .expect("Pseudo-arclength continuation failed");
930
931 assert!(result.x.len() > 2, "Branch should have points");
932 }
933
934 #[test]
935 fn test_linear_problem_continuation() {
936 let linear_f = |x: &Array1<f64>, lambda: f64| array![x[0] - lambda];
938 let x0 = array![0.0];
939 let cfg = ContinuationConfig {
940 max_steps: 50,
941 ds: 0.1,
942 compute_stability: false,
943 ..Default::default()
944 };
945 let result = NaturalParameterContinuation::run(&linear_f, &x0, 0.0, 1.0, &cfg)
946 .expect("Linear continuation failed");
947
948 for (xi, &li) in result.x.iter().zip(result.lambda.iter()) {
950 assert!(
951 (xi[0] - li).abs() < 1e-6,
952 "x={} should equal lambda={}",
953 xi[0],
954 li
955 );
956 }
957 }
958}