use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
#[inline(always)]
fn f64_to_f(v: f64) -> f64 {
v
}
fn gauss_solve(a: &mut Array2<f64>, b: &mut Array1<f64>) -> IntegrateResult<Array1<f64>> {
let n = b.len();
for col in 0..n {
let mut max_row = col;
let mut max_val = a[[col, col]].abs();
for row in (col + 1)..n {
let v = a[[row, col]].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-300 {
return Err(IntegrateError::LinearSolveError(
"Singular matrix in continuation solve".to_string(),
));
}
if max_row != col {
for j in col..n {
let tmp = a[[col, j]];
a[[col, j]] = a[[max_row, j]];
a[[max_row, j]] = tmp;
}
b.swap(col, max_row);
}
let pivot = a[[col, col]];
for row in (col + 1)..n {
let factor = a[[row, col]] / pivot;
for j in col..n {
let update = factor * a[[col, j]];
a[[row, j]] -= update;
}
let bup = factor * b[col];
b[row] -= bup;
}
}
let mut x = Array1::<f64>::zeros(n);
for i in (0..n).rev() {
let mut sum = b[i];
for j in (i + 1)..n {
sum -= a[[i, j]] * x[j];
}
x[i] = sum / a[[i, i]];
}
Ok(x)
}
fn numerical_jacobian_x<F>(
f: &F,
x: &Array1<f64>,
lambda: f64,
eps: f64,
) -> Array2<f64>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let n = x.len();
let mut jac = Array2::<f64>::zeros((n, n));
for j in 0..n {
let mut xp = x.clone();
let mut xm = x.clone();
xp[j] += eps;
xm[j] -= eps;
let fp = f(&xp, lambda);
let fm = f(&xm, lambda);
for i in 0..n {
jac[[i, j]] = (fp[i] - fm[i]) / (2.0 * eps);
}
}
jac
}
fn numerical_df_dlambda<F>(
f: &F,
x: &Array1<f64>,
lambda: f64,
eps: f64,
) -> Array1<f64>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let fp = f(x, lambda + eps);
let fm = f(x, lambda - eps);
let n = fp.len();
let mut df = Array1::<f64>::zeros(n);
for i in 0..n {
df[i] = (fp[i] - fm[i]) / (2.0 * eps);
}
df
}
#[derive(Debug, Clone)]
pub struct BranchPoint {
pub x: Array1<f64>,
pub lambda: f64,
pub null_vector: Option<Array1<f64>>,
}
#[derive(Debug, Clone)]
pub struct LimitPoint {
pub x: Array1<f64>,
pub lambda: f64,
pub branch_index: usize,
pub stability_change: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolutionStability {
Stable,
Unstable,
Unknown,
}
#[derive(Debug, Clone)]
pub struct ContinuationBranchResult {
pub x: Vec<Array1<f64>>,
pub lambda: Vec<f64>,
pub stability: Vec<SolutionStability>,
pub limit_points: Vec<LimitPoint>,
pub branch_points: Vec<BranchPoint>,
pub newton_iters: Vec<usize>,
pub success: bool,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct ContinuationConfig {
pub max_steps: usize,
pub ds: f64,
pub ds_min: f64,
pub ds_max: f64,
pub step_adapt_factor: f64,
pub newton_tol: f64,
pub max_newton_iter: usize,
pub fd_eps: f64,
pub compute_stability: bool,
pub limit_point_tol: f64,
pub desired_newton_iter: usize,
}
impl Default for ContinuationConfig {
fn default() -> Self {
Self {
max_steps: 500,
ds: 0.01,
ds_min: 1e-8,
ds_max: 1.0,
step_adapt_factor: 0.8,
newton_tol: 1e-10,
max_newton_iter: 20,
fd_eps: 1e-7,
compute_stability: true,
limit_point_tol: 1e-4,
desired_newton_iter: 5,
}
}
}
pub struct NaturalParameterContinuation;
impl NaturalParameterContinuation {
pub fn run<F>(
f: &F,
x0: &Array1<f64>,
lambda0: f64,
lambda_end: f64,
cfg: &ContinuationConfig,
) -> IntegrateResult<ContinuationBranchResult>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let n = x0.len();
let direction = if lambda_end >= lambda0 { 1.0 } else { -1.0 };
let mut ds = cfg.ds.abs() * direction;
let mut x = x0.clone();
let mut lambda = lambda0;
let mut xs = vec![x.clone()];
let mut lambdas = vec![lambda];
let mut stabilities = Vec::new();
let mut limit_points = Vec::new();
let mut branch_points_list = Vec::new();
let mut newton_iters = vec![0usize];
if cfg.compute_stability {
let stab = compute_stability_from_jacobian(f, &x, lambda, cfg.fd_eps);
stabilities.push(stab);
}
let mut prev_det = jacobian_determinant(f, &x, lambda, cfg.fd_eps);
for _step in 0..cfg.max_steps {
if (lambda + ds - lambda_end) * direction > 0.0 {
ds = (lambda_end - lambda).abs() * direction;
if ds.abs() < cfg.ds_min {
break;
}
}
let lambda_new = lambda + ds;
match newton_solve_continuation(f, &x, lambda_new, cfg) {
Ok((x_new, n_iters)) => {
let curr_det =
jacobian_determinant(f, &x_new, lambda_new, cfg.fd_eps);
if prev_det * curr_det < 0.0 {
limit_points.push(LimitPoint {
x: x_new.clone(),
lambda: lambda_new,
branch_index: xs.len(),
stability_change: true,
});
}
if curr_det.abs() < cfg.limit_point_tol
&& (prev_det * curr_det >= 0.0)
{
let null_vec =
approximate_null_vector(f, &x_new, lambda_new, cfg.fd_eps, n);
branch_points_list.push(BranchPoint {
x: x_new.clone(),
lambda: lambda_new,
null_vector: Some(null_vec),
});
}
let adapt_ratio = cfg.desired_newton_iter as f64 / n_iters.max(1) as f64;
let new_ds = (ds * adapt_ratio.sqrt()).abs().clamp(cfg.ds_min, cfg.ds_max);
ds = new_ds * direction;
prev_det = curr_det;
x = x_new.clone();
lambda = lambda_new;
xs.push(x_new);
lambdas.push(lambda);
newton_iters.push(n_iters);
if cfg.compute_stability {
let stab = compute_stability_from_jacobian(f, &x, lambda, cfg.fd_eps);
stabilities.push(stab);
}
if (lambda - lambda_end).abs() < cfg.ds_min.abs() {
break;
}
}
Err(_) => {
ds *= cfg.step_adapt_factor;
if ds.abs() < cfg.ds_min {
return Ok(ContinuationBranchResult {
x: xs,
lambda: lambdas,
stability: stabilities,
limit_points,
branch_points: branch_points_list,
newton_iters,
success: false,
message: "Step size below minimum: Newton convergence failure"
.to_string(),
});
}
}
}
}
Ok(ContinuationBranchResult {
x: xs,
lambda: lambdas,
stability: stabilities,
limit_points,
branch_points: branch_points_list,
newton_iters,
success: true,
message: "Natural parameter continuation completed".to_string(),
})
}
}
pub struct PseudoArcLengthContinuation;
impl PseudoArcLengthContinuation {
pub fn run<F>(
f: &F,
x0: &Array1<f64>,
lambda0: f64,
lambda_range: (f64, f64),
cfg: &ContinuationConfig,
direction: f64,
) -> IntegrateResult<ContinuationBranchResult>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let n = x0.len();
let (lambda_min, lambda_max) = lambda_range;
let mut x = x0.clone();
let mut lambda = lambda0;
let (mut tx, mut tl) = compute_tangent(f, &x, lambda, direction, cfg.fd_eps, n)?;
let mut xs = vec![x.clone()];
let mut lambdas = vec![lambda];
let mut stabilities = Vec::new();
let mut limit_points = Vec::new();
let mut branch_points_list = Vec::new();
let mut newton_iters = vec![0usize];
if cfg.compute_stability {
let stab = compute_stability_from_jacobian(f, &x, lambda, cfg.fd_eps);
stabilities.push(stab);
}
let mut prev_det = jacobian_determinant(f, &x, lambda, cfg.fd_eps);
let mut ds = cfg.ds;
for _step in 0..cfg.max_steps {
let x_pred = {
let mut xp = Array1::<f64>::zeros(n);
for i in 0..n {
xp[i] = x[i] + ds * tx[i];
}
xp
};
let lambda_pred = lambda + ds * tl;
if lambda_pred < lambda_min || lambda_pred > lambda_max {
let lambda_target = if lambda_pred < lambda_min {
lambda_min
} else {
lambda_max
};
let ds_boundary = (lambda_target - lambda) / tl.max(1e-300);
if ds_boundary.abs() < cfg.ds_min {
break;
}
let x_boundary = {
let mut xb = Array1::<f64>::zeros(n);
for i in 0..n {
xb[i] = x[i] + ds_boundary * tx[i];
}
xb
};
if let Ok((x_b, ni)) = newton_solve_continuation(f, &x_boundary, lambda_target, cfg) {
xs.push(x_b);
lambdas.push(lambda_target);
newton_iters.push(ni);
}
break;
}
match newton_extended(f, &x_pred, lambda_pred, &x, lambda, &tx, tl, ds, cfg, n) {
Ok((x_new, lambda_new, n_iters)) => {
let curr_det =
jacobian_determinant(f, &x_new, lambda_new, cfg.fd_eps);
if prev_det * curr_det < 0.0 {
limit_points.push(LimitPoint {
x: x_new.clone(),
lambda: lambda_new,
branch_index: xs.len(),
stability_change: true,
});
}
if curr_det.abs() < cfg.limit_point_tol
&& (prev_det * curr_det >= 0.0)
{
let null_vec =
approximate_null_vector(f, &x_new, lambda_new, cfg.fd_eps, n);
branch_points_list.push(BranchPoint {
x: x_new.clone(),
lambda: lambda_new,
null_vector: Some(null_vec),
});
}
if let Ok((new_tx, new_tl)) =
compute_tangent(f, &x_new, lambda_new, tl.signum(), cfg.fd_eps, n)
{
let dot = new_tx
.iter()
.zip(tx.iter())
.fold(0.0, |s, (&a, &b)| s + a * b)
+ new_tl * tl;
if dot < 0.0 {
tx = new_tx.mapv(|v| -v);
tl = -new_tl;
} else {
tx = new_tx;
tl = new_tl;
}
}
let adapt = cfg.desired_newton_iter as f64 / n_iters.max(1) as f64;
ds = (ds * adapt.sqrt()).clamp(cfg.ds_min, cfg.ds_max);
prev_det = curr_det;
x = x_new.clone();
lambda = lambda_new;
xs.push(x_new);
lambdas.push(lambda);
newton_iters.push(n_iters);
if cfg.compute_stability {
let stab = compute_stability_from_jacobian(f, &x, lambda, cfg.fd_eps);
stabilities.push(stab);
}
}
Err(_) => {
ds *= cfg.step_adapt_factor;
if ds.abs() < cfg.ds_min {
return Ok(ContinuationBranchResult {
x: xs,
lambda: lambdas,
stability: stabilities,
limit_points,
branch_points: branch_points_list,
newton_iters,
success: false,
message: "Step size below minimum".to_string(),
});
}
}
}
}
Ok(ContinuationBranchResult {
x: xs,
lambda: lambdas,
stability: stabilities,
limit_points,
branch_points: branch_points_list,
newton_iters,
success: true,
message: "Pseudo-arclength continuation completed".to_string(),
})
}
}
fn newton_solve_continuation<F>(
f: &F,
x_guess: &Array1<f64>,
lambda: f64,
cfg: &ContinuationConfig,
) -> IntegrateResult<(Array1<f64>, usize)>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let n = x_guess.len();
let mut x = x_guess.clone();
for iter in 0..cfg.max_newton_iter {
let fx = f(&x, lambda);
let res_norm: f64 = fx.iter().map(|&v| v * v).sum::<f64>().sqrt();
if res_norm < cfg.newton_tol {
return Ok((x, iter + 1));
}
let mut jac = numerical_jacobian_x(f, &x, lambda, cfg.fd_eps);
let mut neg_fx = fx.mapv(|v| -v);
let delta = gauss_solve(&mut jac, &mut neg_fx)?;
for i in 0..n {
x[i] += delta[i];
}
}
let fx_final = f(&x, lambda);
let res_norm: f64 = fx_final.iter().map(|&v| v * v).sum::<f64>().sqrt();
if res_norm < cfg.newton_tol * 100.0 {
return Ok((x, cfg.max_newton_iter));
}
Err(IntegrateError::ConvergenceError(format!(
"Newton did not converge in {} iterations (res={:.3e})",
cfg.max_newton_iter, res_norm
)))
}
fn compute_tangent<F>(
f: &F,
x: &Array1<f64>,
lambda: f64,
direction_sign: f64,
eps: f64,
n: usize,
) -> IntegrateResult<(Array1<f64>, f64)>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let jx = numerical_jacobian_x(f, x, lambda, eps);
let jl = numerical_df_dlambda(f, x, lambda, eps);
let mut jx_copy = jx.clone();
let mut neg_jl = jl.mapv(|v| -v);
match gauss_solve(&mut jx_copy, &mut neg_jl) {
Ok(v) => {
let v_norm_sq: f64 = v.iter().map(|&vi| vi * vi).sum();
let tl_abs = 1.0 / (1.0 + v_norm_sq).sqrt();
let mut tx = v.mapv(|vi| vi * tl_abs);
let mut tl = tl_abs;
if tl * direction_sign < 0.0 {
tx = tx.mapv(|vi| -vi);
tl = -tl;
}
Ok((tx, tl))
}
Err(_) => {
let tx = Array1::<f64>::zeros(n);
let tl = direction_sign;
Ok((tx, tl))
}
}
}
fn newton_extended<F>(
f: &F,
x_pred: &Array1<f64>,
lambda_pred: f64,
x0: &Array1<f64>,
lambda0: f64,
tx: &Array1<f64>,
tl: f64,
ds: f64,
cfg: &ContinuationConfig,
n: usize,
) -> IntegrateResult<(Array1<f64>, f64, usize)>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let mut x = x_pred.clone();
let mut lam = lambda_pred;
let size = n + 1;
for iter in 0..cfg.max_newton_iter {
let fx = f(&x, lam);
let arc_res: f64 = x
.iter()
.zip(x0.iter())
.zip(tx.iter())
.fold(0.0, |s, ((&xi, &x0i), &txi)| s + (xi - x0i) * txi)
+ (lam - lambda0) * tl
- ds;
let res_norm: f64 = fx.iter().map(|&v| v * v).sum::<f64>() + arc_res * arc_res;
let res_norm = res_norm.sqrt();
if res_norm < cfg.newton_tol {
return Ok((x, lam, iter + 1));
}
let jx = numerical_jacobian_x(f, &x, lam, cfg.fd_eps);
let jl = numerical_df_dlambda(f, &x, lam, cfg.fd_eps);
let mut big_a = Array2::<f64>::zeros((size, size));
let mut big_b = Array1::<f64>::zeros(size);
for i in 0..n {
for j in 0..n {
big_a[[i, j]] = jx[[i, j]];
}
big_a[[i, n]] = jl[i];
big_b[i] = -fx[i];
}
for j in 0..n {
big_a[[n, j]] = tx[j];
}
big_a[[n, n]] = tl;
big_b[n] = -arc_res;
let delta = gauss_solve(&mut big_a, &mut big_b)?;
for i in 0..n {
x[i] += delta[i];
}
lam += delta[n];
}
Err(IntegrateError::ConvergenceError(format!(
"Pseudo-arclength Newton did not converge in {} iterations",
cfg.max_newton_iter
)))
}
fn jacobian_determinant<F>(
f: &F,
x: &Array1<f64>,
lambda: f64,
eps: f64,
) -> f64
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let n = x.len();
let mut jac = numerical_jacobian_x(f, x, lambda, eps);
let mut sign = 1.0_f64;
let mut det_approx = 1.0_f64;
for col in 0..n {
let mut max_row = col;
let mut max_val = jac[[col, col]].abs();
for row in (col + 1)..n {
let v = jac[[row, col]].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-300 {
return 0.0;
}
if max_row != col {
for j in col..n {
let tmp = jac[[col, j]];
jac[[col, j]] = jac[[max_row, j]];
jac[[max_row, j]] = tmp;
}
sign = -sign;
}
det_approx *= jac[[col, col]];
let pivot = jac[[col, col]];
for row in (col + 1)..n {
let factor = jac[[row, col]] / pivot;
for j in col..n {
let u = factor * jac[[col, j]];
jac[[row, j]] -= u;
}
}
}
sign * det_approx
}
fn compute_stability_from_jacobian<F>(
f: &F,
x: &Array1<f64>,
lambda: f64,
eps: f64,
) -> SolutionStability
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let n = x.len();
let jac = numerical_jacobian_x(f, x, lambda, eps);
let mut all_stable = true;
let mut any_unstable = false;
for i in 0..n {
let center = jac[[i, i]];
let radius: f64 = (0..n).filter(|&j| j != i).map(|j| jac[[i, j]].abs()).sum();
let rightmost = center + radius;
let leftmost = center - radius;
if leftmost > 0.0 {
any_unstable = true;
all_stable = false;
} else if rightmost > 0.0 {
all_stable = false;
}
}
if any_unstable {
SolutionStability::Unstable
} else if all_stable {
SolutionStability::Stable
} else {
SolutionStability::Unknown
}
}
fn approximate_null_vector<F>(
f: &F,
x: &Array1<f64>,
lambda: f64,
eps: f64,
n: usize,
) -> Array1<f64>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let jac = numerical_jacobian_x(f, x, lambda, eps);
let mut min_col = 0;
let mut min_val = jac[[0, 0]].abs();
for i in 1..n {
let v = jac[[i, i]].abs();
if v < min_val {
min_val = v;
min_col = i;
}
}
let mut null = Array1::<f64>::zeros(n);
null[min_col] = 1.0;
null
}
#[allow(dead_code)]
fn _use_f64_to_f() {
let _ = f64_to_f(0.0);
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
fn fold_residual(x: &Array1<f64>, lambda: f64) -> Array1<f64> {
array![x[0] * x[0] * x[0] - x[0] - lambda]
}
#[test]
fn test_natural_continuation_simple() {
let x0 = array![1.0];
let cfg = ContinuationConfig {
max_steps: 100,
ds: 0.05,
..Default::default()
};
let result =
NaturalParameterContinuation::run(&fold_residual, &x0, 0.0, 0.3, &cfg)
.expect("Natural continuation failed");
assert!(result.lambda.len() > 2, "Should have more than 2 points");
let last_lambda = *result.lambda.last().expect("no lambda");
assert!(
(last_lambda - 0.3).abs() < 0.1,
"Last lambda={} should be near 0.3",
last_lambda
);
}
#[test]
fn test_pseudo_arclength_continuation() {
let x0 = array![1.0];
let cfg = ContinuationConfig {
max_steps: 200,
ds: 0.05,
ds_max: 0.2,
compute_stability: false,
..Default::default()
};
let result = PseudoArcLengthContinuation::run(
&fold_residual,
&x0,
0.0,
(-2.0, 2.0),
&cfg,
1.0,
)
.expect("Pseudo-arclength continuation failed");
assert!(result.x.len() > 2, "Branch should have points");
}
#[test]
fn test_linear_problem_continuation() {
let linear_f = |x: &Array1<f64>, lambda: f64| array![x[0] - lambda];
let x0 = array![0.0];
let cfg = ContinuationConfig {
max_steps: 50,
ds: 0.1,
compute_stability: false,
..Default::default()
};
let result =
NaturalParameterContinuation::run(&linear_f, &x0, 0.0, 1.0, &cfg)
.expect("Linear continuation failed");
for (xi, &li) in result.x.iter().zip(result.lambda.iter()) {
assert!(
(xi[0] - li).abs() < 1e-6,
"x={} should equal lambda={}",
xi[0],
li
);
}
}
}