use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
pub(crate) fn gauss_legendre_nodes(npts: usize) -> (Vec<f64>, Vec<f64>) {
match npts {
1 => (vec![0.0], vec![2.0]),
2 => (
vec![-0.577_350_269_189_625_8, 0.577_350_269_189_625_8],
vec![1.0, 1.0],
),
3 => (
vec![-0.774_596_669_241_483_4, 0.0, 0.774_596_669_241_483_4],
vec![
0.555_555_555_555_555_6,
0.888_888_888_888_888_9,
0.555_555_555_555_555_6,
],
),
4 => (
vec![
-0.861_136_311_594_052_6,
-0.339_981_043_584_856_3,
0.339_981_043_584_856_3,
0.861_136_311_594_052_6,
],
vec![
0.347_854_845_137_453_8,
0.652_145_154_862_546_1,
0.652_145_154_862_546_1,
0.347_854_845_137_453_8,
],
),
5 => (
vec![
-0.906_179_845_938_664_0,
-0.538_469_310_105_683_1,
0.0,
0.538_469_310_105_683_1,
0.906_179_845_938_664_0,
],
vec![
0.236_926_885_056_189_1,
0.478_628_670_499_366_5,
0.568_888_888_888_888_9,
0.478_628_670_499_366_5,
0.236_926_885_056_189_1,
],
),
8 => (
vec![
-0.960_289_856_497_536_3,
-0.796_666_477_413_626_7,
-0.525_532_409_916_329_0,
-0.183_434_642_495_649_8,
0.183_434_642_495_649_8,
0.525_532_409_916_329_0,
0.796_666_477_413_626_7,
0.960_289_856_497_536_3,
],
vec![
0.101_228_536_290_376_3,
0.222_381_034_453_374_5,
0.313_706_645_877_887_3,
0.362_683_783_378_362_0,
0.362_683_783_378_362_0,
0.313_706_645_877_887_3,
0.222_381_034_453_374_5,
0.101_228_536_290_376_3,
],
),
10 => (
vec![
-0.973_906_528_517_171_7,
-0.865_063_366_688_984_5,
-0.679_409_568_299_024_4,
-0.433_395_394_129_247_2,
-0.148_874_338_981_631_2,
0.148_874_338_981_631_2,
0.433_395_394_129_247_2,
0.679_409_568_299_024_4,
0.865_063_366_688_984_5,
0.973_906_528_517_171_7,
],
vec![
0.066_671_344_086_681_0,
0.149_451_349_150_580_6,
0.219_086_362_515_982_0,
0.269_266_719_309_996_4,
0.295_524_224_714_752_9,
0.295_524_224_714_752_9,
0.269_266_719_309_996_4,
0.219_086_362_515_982_0,
0.149_451_349_150_580_6,
0.066_671_344_086_681_0,
],
),
12 => (
vec![
-0.981_560_634_246_719_2,
-0.904_117_256_370_474_9,
-0.769_902_674_194_304_7,
-0.587_317_954_286_617_7,
-0.367_831_498_998_180_2,
-0.125_233_408_511_468_9,
0.125_233_408_511_468_9,
0.367_831_498_998_180_2,
0.587_317_954_286_617_7,
0.769_902_674_194_304_7,
0.904_117_256_370_474_9,
0.981_560_634_246_719_2,
],
vec![
0.047_175_336_386_511_8,
0.106_939_325_995_318_4,
0.160_078_328_543_346_2,
0.203_167_426_723_065_9,
0.233_492_536_538_354_8,
0.249_147_045_813_402_8,
0.249_147_045_813_402_8,
0.233_492_536_538_354_8,
0.203_167_426_723_065_9,
0.160_078_328_543_346_2,
0.106_939_325_995_318_4,
0.047_175_336_386_511_8,
],
),
_ => {
gauss_legendre_nodes(5)
}
}
}
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 or near-singular Nyström matrix".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 sub = factor * a[[col, j]];
a[[row, j]] -= sub;
}
let sub_b = factor * b[col];
b[row] -= sub_b;
}
}
let mut x = Array1::<f64>::zeros(n);
for i in (0..n).rev() {
let mut s = b[i];
for j in (i + 1)..n {
s -= a[[i, j]] * x[j];
}
x[i] = s / a[[i, i]];
}
Ok(x)
}
#[derive(Debug, Clone)]
pub struct FredholmSolverConfig {
pub a: f64,
pub b: f64,
pub n_quad: usize,
pub lambda: f64,
pub neumann_max_terms: usize,
pub neumann_tol: f64,
}
impl Default for FredholmSolverConfig {
fn default() -> Self {
Self {
a: 0.0,
b: 1.0,
n_quad: 8,
lambda: 1.0,
neumann_max_terms: 50,
neumann_tol: 1e-12,
}
}
}
#[derive(Debug, Clone)]
pub struct FredholmSolution {
pub nodes: Vec<f64>,
pub weights: Vec<f64>,
pub u: Vec<f64>,
pub condition_estimate: f64,
pub n_terms_used: usize,
}
pub struct FredholmSolver {
pub kernel: Box<dyn Fn(f64, f64) -> f64>,
pub config: FredholmSolverConfig,
}
impl FredholmSolver {
pub fn new(
kernel: impl Fn(f64, f64) -> f64 + 'static,
config: FredholmSolverConfig,
) -> Self {
Self {
kernel: Box::new(kernel),
config,
}
}
pub fn nystrom_method(
&self,
f: impl Fn(f64) -> f64,
) -> IntegrateResult<FredholmSolution> {
nystrom_method(&self.kernel, &f, &self.config)
}
pub fn degenerate_kernel(
&self,
f: impl Fn(f64) -> f64,
basis_a: &[Box<dyn Fn(f64) -> f64>],
basis_b: &[Box<dyn Fn(f64) -> f64>],
eval_points: &[f64],
) -> IntegrateResult<FredholmSolution> {
degenerate_kernel(&self.config, &f, basis_a, basis_b, eval_points)
}
pub fn neumann_series(
&self,
f: impl Fn(f64) -> f64,
) -> IntegrateResult<FredholmSolution> {
fredholm_neumann(&self.kernel, &f, &self.config)
}
}
pub fn nystrom_method(
kernel: &dyn Fn(f64, f64) -> f64,
f: &dyn Fn(f64) -> f64,
cfg: &FredholmSolverConfig,
) -> IntegrateResult<FredholmSolution> {
let n = cfg.n_quad;
if n == 0 {
return Err(IntegrateError::InvalidInput(
"n_quad must be at least 1".to_string(),
));
}
let a = cfg.a;
let b = cfg.b;
let lam = cfg.lambda;
let (xi_ref, wi_ref) = gauss_legendre_nodes(n);
let half_len = (b - a) * 0.5;
let mid = (a + b) * 0.5;
let nodes: Vec<f64> = xi_ref.iter().map(|&xi| mid + half_len * xi).collect();
let weights: Vec<f64> = wi_ref.iter().map(|&wi| wi * half_len).collect();
let mut a_mat = Array2::<f64>::zeros((n, n));
let mut rhs = Array1::<f64>::zeros(n);
for i in 0..n {
rhs[i] = f(nodes[i]);
for j in 0..n {
let k_ij = kernel(nodes[i], nodes[j]);
a_mat[[i, j]] = if i == j {
1.0 - lam * weights[j] * k_ij
} else {
-lam * weights[j] * k_ij
};
}
}
let diag_max = (0..n).fold(f64::NEG_INFINITY, |m, i| m.max(a_mat[[i, i]].abs()));
let diag_min = (0..n).fold(f64::INFINITY, |m, i| m.min(a_mat[[i, i]].abs()));
let condition_estimate = if diag_min > 1e-300 {
diag_max / diag_min
} else {
f64::INFINITY
};
let u_vec = gauss_solve(&mut a_mat, &mut rhs)?;
Ok(FredholmSolution {
nodes,
weights,
u: u_vec.to_vec(),
condition_estimate,
n_terms_used: 0,
})
}
pub fn nystrom_evaluate(
x: f64,
kernel: &dyn Fn(f64, f64) -> f64,
f: &dyn Fn(f64) -> f64,
sol: &FredholmSolution,
lambda: f64,
) -> f64 {
let sum: f64 = sol
.nodes
.iter()
.zip(sol.weights.iter())
.zip(sol.u.iter())
.map(|((&tj, &wj), &uj)| wj * kernel(x, tj) * uj)
.sum();
f(x) + lambda * sum
}
pub fn degenerate_kernel(
cfg: &FredholmSolverConfig,
f: &dyn Fn(f64) -> f64,
basis_a: &[Box<dyn Fn(f64) -> f64>],
basis_b: &[Box<dyn Fn(f64) -> f64>],
eval_points: &[f64],
) -> IntegrateResult<FredholmSolution> {
let rank = basis_a.len();
if rank == 0 {
return Err(IntegrateError::InvalidInput(
"basis_a must be non-empty".to_string(),
));
}
if basis_b.len() != rank {
return Err(IntegrateError::InvalidInput(format!(
"basis_a (len={}) and basis_b (len={}) must have the same length",
rank,
basis_b.len()
)));
}
if eval_points.is_empty() {
return Err(IntegrateError::InvalidInput(
"eval_points must be non-empty".to_string(),
));
}
let a = cfg.a;
let b = cfg.b;
let lam = cfg.lambda;
let n_quad = cfg.n_quad.max(rank * 4);
let (xi_ref, wi_ref) = gauss_legendre_nodes(n_quad.min(12));
let half_len = (b - a) * 0.5;
let mid = (a + b) * 0.5;
let nodes: Vec<f64> = xi_ref.iter().map(|&xi| mid + half_len * xi).collect();
let weights: Vec<f64> = wi_ref.iter().map(|&wi| wi * half_len).collect();
let nq = nodes.len();
let mut b_mat = Array2::<f64>::zeros((rank, rank));
for r in 0..rank {
for s in 0..rank {
let val: f64 = (0..nq)
.map(|k| weights[k] * basis_b[r](nodes[k]) * basis_a[s](nodes[k]))
.sum();
b_mat[[r, s]] = val;
}
}
let mut d_vec = Array1::<f64>::zeros(rank);
for r in 0..rank {
d_vec[r] = (0..nq)
.map(|k| weights[k] * basis_b[r](nodes[k]) * f(nodes[k]))
.sum();
}
let mut sys = Array2::<f64>::zeros((rank, rank));
for r in 0..rank {
for s in 0..rank {
sys[[r, s]] = if r == s {
1.0 - lam * b_mat[[r, s]]
} else {
-lam * b_mat[[r, s]]
};
}
}
let c_vec = gauss_solve(&mut sys, &mut d_vec)?;
let u_vals: Vec<f64> = eval_points
.iter()
.map(|&x| {
let correction: f64 = (0..rank).map(|r| c_vec[r] * basis_a[r](x)).sum();
f(x) + lam * correction
})
.collect();
let diag_max = (0..rank).fold(f64::NEG_INFINITY, |m, i| m.max(sys[[i, i]].abs()));
let diag_min = (0..rank).fold(f64::INFINITY, |m, i| m.min(sys[[i, i]].abs()));
let condition_estimate = if diag_min > 1e-300 {
diag_max / diag_min
} else {
f64::INFINITY
};
Ok(FredholmSolution {
nodes: eval_points.to_vec(),
weights: vec![1.0; eval_points.len()],
u: u_vals,
condition_estimate,
n_terms_used: 0,
})
}
pub fn fredholm_neumann(
kernel: &dyn Fn(f64, f64) -> f64,
f: &dyn Fn(f64) -> f64,
cfg: &FredholmSolverConfig,
) -> IntegrateResult<FredholmSolution> {
let a = cfg.a;
let b = cfg.b;
let lam = cfg.lambda;
let n = cfg.n_quad;
let (xi_ref, wi_ref) = gauss_legendre_nodes(n);
let half_len = (b - a) * 0.5;
let mid = (a + b) * 0.5;
let nodes: Vec<f64> = xi_ref.iter().map(|&xi| mid + half_len * xi).collect();
let weights: Vec<f64> = wi_ref.iter().map(|&wi| wi * half_len).collect();
let nq = nodes.len();
let f_vals: Vec<f64> = nodes.iter().map(|&x| f(x)).collect();
let mut k_mat = vec![vec![0.0_f64; nq]; nq];
for i in 0..nq {
for j in 0..nq {
k_mat[i][j] = kernel(nodes[i], nodes[j]);
}
}
let mut u_prev = f_vals.clone();
let mut n_terms = 0usize;
for _iter in 0..cfg.neumann_max_terms {
let mut u_next = vec![0.0_f64; nq];
for i in 0..nq {
let integral: f64 = (0..nq).map(|j| weights[j] * k_mat[i][j] * u_prev[j]).sum();
u_next[i] = f_vals[i] + lam * integral;
}
let max_diff = u_next
.iter()
.zip(u_prev.iter())
.map(|(&a, &b)| (a - b).abs())
.fold(0.0_f64, f64::max);
u_prev = u_next;
n_terms += 1;
if max_diff < cfg.neumann_tol {
return Ok(FredholmSolution {
nodes,
weights,
u: u_prev,
condition_estimate: 0.0,
n_terms_used: n_terms,
});
}
}
Err(IntegrateError::ConvergenceError(format!(
"Neumann series did not converge in {} iterations; \
check that |λ| * ‖K‖ < 1",
cfg.neumann_max_terms
)))
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_nystrom_zero_kernel() {
let cfg = FredholmSolverConfig {
a: 0.0,
b: 1.0,
n_quad: 8,
lambda: 1.0,
..Default::default()
};
let f = |x: f64| (PI * x).sin();
let kernel = |_x: f64, _t: f64| 0.0_f64;
let sol = nystrom_method(&kernel, &f, &cfg).expect("nystrom zero kernel failed");
for (&xi, &ui) in sol.nodes.iter().zip(sol.u.iter()) {
let exact = (PI * xi).sin();
assert!(
(ui - exact).abs() < 1e-12,
"u({:.3}) = {:.6} != f = {:.6}",
xi,
ui,
exact
);
}
}
#[test]
fn test_nystrom_separable_kernel() {
let lam = 0.5_f64;
let c_exact = 1.0 / (PI * (1.0 - lam / 3.0));
let cfg = FredholmSolverConfig {
a: 0.0,
b: 1.0,
n_quad: 8,
lambda: lam,
..Default::default()
};
let f = |x: f64| (PI * x).sin();
let kernel = |x: f64, t: f64| x * t;
let sol = nystrom_method(&kernel, &f, &cfg).expect("nystrom separable failed");
for (&xi, &ui) in sol.nodes.iter().zip(sol.u.iter()) {
let exact = (PI * xi).sin() + lam * c_exact * xi;
assert!(
(ui - exact).abs() < 1e-6,
"u({:.3}) = {:.8} != {:.8}",
xi,
ui,
exact
);
}
}
#[test]
fn test_solver_struct_nystrom() {
let cfg = FredholmSolverConfig {
a: 0.0,
b: 1.0,
n_quad: 8,
lambda: 0.3,
..Default::default()
};
let solver = FredholmSolver::new(|x: f64, t: f64| (-((x - t).powi(2))).exp(), cfg);
let f = |x: f64| 1.0_f64 + x;
let sol = solver.nystrom_method(f).expect("solver nystrom failed");
assert_eq!(sol.nodes.len(), 8);
assert_eq!(sol.u.len(), 8);
}
#[test]
fn test_degenerate_constant_kernel() {
let cfg = FredholmSolverConfig {
a: 0.0,
b: 1.0,
n_quad: 8,
lambda: 0.5,
..Default::default()
};
let f = |_x: f64| 1.0_f64;
let basis_a: Vec<Box<dyn Fn(f64) -> f64>> = vec![Box::new(|_x: f64| 1.0_f64)];
let basis_b: Vec<Box<dyn Fn(f64) -> f64>> = vec![Box::new(|_t: f64| 1.0_f64)];
let eval_pts: Vec<f64> = (0..=10).map(|i| i as f64 / 10.0).collect();
let sol = degenerate_kernel(&cfg, &f, &basis_a, &basis_b, &eval_pts)
.expect("degenerate constant kernel failed");
for &ui in &sol.u {
assert!(
(ui - 2.0).abs() < 1e-10,
"u = {} != 2.0",
ui
);
}
}
#[test]
fn test_degenerate_xt_kernel() {
let lam = 0.5_f64;
let c_exact = 1.0 / (PI * (1.0 - lam / 3.0));
let cfg = FredholmSolverConfig {
a: 0.0,
b: 1.0,
n_quad: 10,
lambda: lam,
..Default::default()
};
let f = |x: f64| (PI * x).sin();
let basis_a: Vec<Box<dyn Fn(f64) -> f64>> = vec![Box::new(|x: f64| x)];
let basis_b: Vec<Box<dyn Fn(f64) -> f64>> = vec![Box::new(|t: f64| t)];
let eval_pts: Vec<f64> = (1..=9).map(|i| i as f64 / 10.0).collect();
let sol = degenerate_kernel(&cfg, &f, &basis_a, &basis_b, &eval_pts)
.expect("degenerate xt kernel failed");
for (&xi, &ui) in sol.nodes.iter().zip(sol.u.iter()) {
let exact = (PI * xi).sin() + lam * c_exact * xi;
assert!(
(ui - exact).abs() < 1e-6,
"u({:.2}) = {:.8} != {:.8}",
xi,
ui,
exact
);
}
}
#[test]
fn test_neumann_zero_kernel() {
let cfg = FredholmSolverConfig {
a: 0.0,
b: 1.0,
n_quad: 8,
lambda: 1.0,
neumann_max_terms: 50,
neumann_tol: 1e-12,
};
let f = |x: f64| x * x;
let kernel = |_x: f64, _t: f64| 0.0_f64;
let sol = fredholm_neumann(&kernel, &f, &cfg).expect("neumann zero kernel failed");
for (&xi, &ui) in sol.nodes.iter().zip(sol.u.iter()) {
assert!(
(ui - xi * xi).abs() < 1e-12,
"neumann zero: u({}) = {} != {}",
xi,
ui,
xi * xi
);
}
assert_eq!(sol.n_terms_used, 1);
}
#[test]
fn test_neumann_constant_kernel() {
let cfg = FredholmSolverConfig {
a: 0.0,
b: 1.0,
n_quad: 8,
lambda: 0.5,
neumann_max_terms: 200,
neumann_tol: 1e-10,
};
let f = |_x: f64| 1.0_f64;
let kernel = |_x: f64, _t: f64| 1.0_f64;
let sol = fredholm_neumann(&kernel, &f, &cfg).expect("neumann constant kernel failed");
for &ui in &sol.u {
assert!(
(ui - 2.0).abs() < 1e-4,
"neumann constant: u = {} != 2",
ui
);
}
}
#[test]
fn test_neumann_vs_nystrom() {
let lam = 0.1_f64; let cfg = FredholmSolverConfig {
a: 0.0,
b: 1.0,
n_quad: 8,
lambda: lam,
neumann_max_terms: 100,
neumann_tol: 1e-10,
};
let f = |x: f64| 1.0_f64 + 0.5 * x;
let kernel = |x: f64, t: f64| (x * t).sin();
let sol_nm = fredholm_neumann(&kernel, &f, &cfg).expect("neumann sin kernel");
let sol_ny = nystrom_method(&kernel, &f, &cfg).expect("nystrom sin kernel");
for (un, uy) in sol_nm.u.iter().zip(sol_ny.u.iter()) {
assert!(
(un - uy).abs() < 1e-4,
"neumann={:.8} nystrom={:.8} differ by more than 1e-4",
un,
uy
);
}
}
}