use crate::math::{chebyshev_gauss_points, chebyshev_lobatto_points, neg_one_pow_n};
use crate::StrError;
use crate::{Matrix, Vector};
const DX_EPSILON: f64 = 10.0 * f64::EPSILON;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InterpGrid {
Uniform,
ChebyshevGauss,
ChebyshevGaussLobatto,
}
#[derive(Clone, Copy, Debug)]
pub struct InterpParams {
pub grid_type: InterpGrid,
pub no_eta_normalization: bool,
pub eta_cutoff: usize,
pub lebesgue_estimate_nstation: usize,
pub error_estimate_nstation: usize,
}
impl InterpParams {
pub fn new() -> Self {
InterpParams {
grid_type: InterpGrid::ChebyshevGaussLobatto,
no_eta_normalization: false,
eta_cutoff: 700,
lebesgue_estimate_nstation: 10_000,
error_estimate_nstation: 1_000,
}
}
fn validate(&self) -> Result<(), StrError> {
if self.lebesgue_estimate_nstation < 2 {
return Err("lebesgue_estimate_nstation must be ≥ 2");
}
if self.error_estimate_nstation < 2 {
return Err("error_estimate_nstation must be ≥ 2");
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct InterpLagrange {
nn: usize,
params: InterpParams,
npoint: usize,
xx: Vector,
eta: Vector,
lambda: Vector,
dd1: Matrix,
dd2: Matrix,
}
impl InterpLagrange {
pub fn new(nn: usize, params: Option<InterpParams>) -> Result<Self, StrError> {
if nn < 1 || nn > 2048 {
return Err("the polynomial degree must be in [1, 2048]");
}
let par = match params {
Some(p) => p,
None => InterpParams::new(),
};
par.validate()?;
let npoint = nn + 1;
let mut interp = InterpLagrange {
nn,
params: par,
npoint,
xx: match par.grid_type {
InterpGrid::Uniform => Vector::linspace(-1.0, 1.0, npoint).unwrap(),
InterpGrid::ChebyshevGauss => chebyshev_gauss_points(nn),
InterpGrid::ChebyshevGaussLobatto => chebyshev_lobatto_points(nn),
},
eta: Vector::new(if par.no_eta_normalization { 0 } else { npoint }),
lambda: Vector::new(npoint),
dd1: Matrix::new(0, 0), dd2: Matrix::new(0, 0), };
if par.no_eta_normalization {
for j in 0..npoint {
let mut prod = 1.0;
for k in 0..npoint {
if k != j {
prod *= interp.xx[j] - interp.xx[k];
}
}
interp.lambda[j] = 1.0 / prod;
}
} else {
for j in 0..npoint {
for k in 0..npoint {
if k != j {
interp.eta[j] += f64::ln(f64::abs(interp.xx[j] - interp.xx[k]));
}
}
}
let nnf = nn as f64;
let (c0, c1, c2) = if nn > par.eta_cutoff {
(
f64::powf(2.0, nnf / 3.0),
f64::powf(2.0, nnf / 3.0),
f64::powf(2.0, nnf / 3.0 - 1.0) / nnf,
)
} else {
(f64::powf(2.0, nnf - 1.0) / nnf, 0.0, 0.0)
};
for j in 0..npoint {
let aj = neg_one_pow_n((j + nn) as i32);
let mj = -interp.eta[j];
if nn > par.eta_cutoff {
let bj = f64::exp(mj / 3.0);
interp.lambda[j] = aj * bj / c0;
interp.lambda[j] *= bj / c1;
interp.lambda[j] *= bj / c2;
} else {
let bj = f64::exp(mj);
interp.lambda[j] = aj * bj / c0;
}
assert!(interp.lambda[j].is_finite());
}
}
Ok(interp)
}
pub fn psi(&self, j: usize, x: f64) -> Result<f64, StrError> {
if j > self.nn {
return Err("j must be in 0 ≤ j ≤ N");
}
if x < -1.0 || x > 1.0 {
return Err("x must be in -1 ≤ x ≤ 1");
}
if f64::abs(x - self.xx[j]) < DX_EPSILON {
return Ok(1.0);
}
let mut sum = 0.0;
for k in 0..self.npoint {
sum += self.lambda[k] / (x - self.xx[k]);
}
Ok(self.lambda[j] / (x - self.xx[j]) / sum)
}
pub fn eval(&self, x: f64, uu: &Vector) -> Result<f64, StrError> {
if x < -1.0 || x > 1.0 {
return Err("x must be in -1 ≤ x ≤ 1");
}
if uu.dim() != self.npoint {
return Err("the dimension of the data vector U must be equal to N + 1");
}
let mut res = 0.0;
for j in 0..self.npoint {
res += uu[j] * self.psi(j, x).unwrap();
}
Ok(res)
}
pub fn eval_deriv1(&self, x: f64, uu: &Vector) -> Result<f64, StrError> {
if x < -1.0 || x > 1.0 {
return Err("x must be in -1 ≤ x ≤ 1");
}
if uu.dim() != self.npoint {
return Err("the dimension of the data vector U must be equal to N + 1");
}
let mut at_node = false;
let mut at_node_index = 0;
if x == -1.0 {
at_node = true;
at_node_index = 0;
} else if x == 1.0 {
at_node = true;
at_node_index = self.nn;
} else {
for j in 0..self.npoint {
let dx = x - self.xx[j];
if f64::abs(dx) < DX_EPSILON {
at_node = true;
at_node_index = j;
break;
}
}
}
if at_node {
let k = at_node_index;
let mut sum = 0.0;
for j in 0..self.npoint {
if j != k {
sum += self.lambda[j] * (uu[k] - uu[j]) / (self.xx[k] - self.xx[j]);
}
}
Ok(-sum / self.lambda[k])
} else {
let pnu_x = self.eval(x, uu).unwrap();
let mut num = 0.0;
let mut den = 0.0;
for j in 0..self.npoint {
let dx = x - self.xx[j];
let a = self.lambda[j] / dx;
num += a * (pnu_x - uu[j]) / dx;
den += a;
}
Ok(num / den)
}
}
pub fn eval_deriv2(&self, x: f64, uu: &Vector) -> Result<f64, StrError> {
if x < -1.0 || x > 1.0 {
return Err("x must be in -1 ≤ x ≤ 1");
}
if uu.dim() != self.npoint {
return Err("the dimension of the data vector U must be equal to N + 1");
}
let mut at_node = false;
let mut at_node_index = 0;
if x == -1.0 {
at_node = true;
at_node_index = 0;
} else if x == 1.0 {
at_node = true;
at_node_index = self.nn;
} else {
for j in 0..self.npoint {
let dx = x - self.xx[j];
if f64::abs(dx) < DX_EPSILON {
at_node = true;
at_node_index = j;
break;
}
}
}
if at_node {
let k = at_node_index;
let mut sum = 0.0;
for j in 0..self.npoint {
if j != k {
sum += self.lambda[j] / (self.xx[k] - self.xx[j]);
}
}
let d1kk = -sum / self.lambda[k];
sum = 0.0;
for j in 0..self.npoint {
if j != k {
let dx = self.xx[k] - self.xx[j];
let d1kj = (self.lambda[j] / self.lambda[k]) / dx;
sum += d1kj * (d1kk - 1.0 / dx) * (uu[k] - uu[j]);
}
}
Ok(-2.0 * sum)
} else {
let pnu_x = self.eval(x, uu).unwrap();
let d_pnu_x = self.eval_deriv1(x, uu).unwrap();
let mut num = 0.0;
let mut den = 0.0;
for j in 0..self.npoint {
let dx = x - self.xx[j];
let a = self.lambda[j] / dx;
let b = 2.0 / dx;
let c = d_pnu_x + (uu[j] - pnu_x) / dx;
num += a * b * c;
den += a;
}
Ok(num / den)
}
}
pub fn calc_dd1_matrix(&mut self) {
if self.dd1.dims().0 == self.npoint {
return; }
self.dd1 = Matrix::new(self.npoint, self.npoint);
if self.params.no_eta_normalization {
for k in 0..self.npoint {
let mut row_sum = 0.0;
for j in 0..self.npoint {
if k != j {
let v = (self.lambda[j] / self.lambda[k]) / (self.xx[k] - self.xx[j]);
self.dd1.set(k, j, v);
row_sum += v;
}
}
self.dd1.set(k, k, -row_sum); }
} else {
for k in 0..self.npoint {
let mut row_sum = 0.0;
for j in 0..self.npoint {
if k != j {
let r = neg_one_pow_n((k + j) as i32) * f64::exp(self.eta[k] - self.eta[j]);
let v = r / (self.xx[k] - self.xx[j]);
self.dd1.set(k, j, v);
row_sum += v;
}
}
self.dd1.set(k, k, -row_sum); }
}
}
pub fn calc_dd2_matrix(&mut self) {
self.calc_dd1_matrix();
if self.dd2.dims().0 == self.npoint {
return; }
self.dd2 = Matrix::new(self.npoint, self.npoint);
for k in 0..self.npoint {
let mut row_sum = 0.0;
for j in 0..self.npoint {
if k != j {
let v = 2.0 * self.dd1.get(k, j) * (self.dd1.get(k, k) - 1.0 / (self.xx[k] - self.xx[j]));
self.dd2.set(k, j, v);
row_sum += v;
}
}
self.dd2.set(k, k, -row_sum); }
}
pub fn estimate_lebesgue_constant(&self) -> f64 {
let n_station = self.params.lebesgue_estimate_nstation;
let mut lambda_times_nn = 0.0;
for j in 0..n_station {
let x = -1.0 + 2.0 * (j as f64) / ((n_station - 1) as f64);
let mut sum = f64::abs(self.psi(0, x).unwrap());
for i in 1..self.npoint {
sum += f64::abs(self.psi(i, x).unwrap());
}
if sum > lambda_times_nn {
lambda_times_nn = sum;
}
}
lambda_times_nn
}
pub fn estimate_max_error<F, A>(&self, args: &mut A, mut f: F) -> Result<f64, StrError>
where
F: FnMut(f64, &mut A) -> Result<f64, StrError>,
{
let mut uu = Vector::new(self.npoint);
for j in 0..self.npoint {
uu[j] = f(self.xx[j], args)?;
}
let mut err_f = 0.0;
let nstation = self.params.error_estimate_nstation;
let stations = Vector::linspace(-1.0, 1.0, nstation).unwrap();
for p in 0..nstation {
let fi = self.eval(stations[p], &uu).unwrap();
err_f = f64::max(err_f, f64::abs(fi - f(stations[p], args)?));
}
Ok(err_f)
}
pub fn estimate_max_error_all<F, G, H, A>(
&self,
exclude_boundaries: bool,
args: &mut A,
mut f: F,
mut g: G,
mut h: H,
) -> Result<(f64, f64, f64), StrError>
where
F: FnMut(f64, &mut A) -> Result<f64, StrError>,
G: FnMut(f64, &mut A) -> Result<f64, StrError>,
H: FnMut(f64, &mut A) -> Result<f64, StrError>,
{
let mut uu = Vector::new(self.npoint);
for j in 0..self.npoint {
uu[j] = f(self.xx[j], args)?;
}
let (mut err_f, mut err_g, mut err_h) = (0.0, 0.0, 0.0);
let nstation = self.params.error_estimate_nstation;
let stations = Vector::linspace(-1.0, 1.0, nstation).unwrap();
for p in 0..nstation {
let fi = self.eval(stations[p], &uu).unwrap();
let gi = self.eval_deriv1(stations[p], &uu).unwrap();
let hi = self.eval_deriv2(stations[p], &uu).unwrap();
err_f = f64::max(err_f, f64::abs(fi - f(stations[p], args)?));
if exclude_boundaries {
if p == 0 || p == nstation - 1 {
continue;
}
}
err_g = f64::max(err_g, f64::abs(gi - g(stations[p], args)?));
err_h = f64::max(err_h, f64::abs(hi - h(stations[p], args)?));
}
Ok((err_f, err_g, err_h))
}
pub fn get_degree(&self) -> usize {
self.nn
}
pub fn get_grid_type(&self) -> InterpGrid {
self.params.grid_type
}
pub fn get_points(&self) -> &Vector {
&self.xx
}
pub fn get_xrange(&self) -> (f64, f64) {
(-1.0, 1.0)
}
pub fn get_lambda(&self) -> &Vector {
&self.lambda
}
pub fn get_dd1(&self) -> Result<&Matrix, StrError> {
if self.dd1.dims().0 == 0 {
return Err("calc_dd1_matrix must be called first");
}
Ok(&self.dd1)
}
pub fn get_dd2(&self) -> Result<&Matrix, StrError> {
if self.dd2.dims().0 == 0 {
return Err("calc_dd2_matrix must be called first");
}
Ok(&self.dd2)
}
}
#[cfg(test)]
mod tests {
use super::{InterpGrid, InterpLagrange, InterpParams};
use crate::algo::NoArgs;
use crate::math::SQRT_3;
use crate::{
approx_eq, deriv1_approx_eq, deriv1_approx_eq_bw, deriv1_approx_eq_fw, deriv2_approx_eq, deriv2_approx_eq_bw,
deriv2_approx_eq_fw,
};
use crate::{mat_vec_mul, Vector};
#[test]
fn new_works() {
let interp = InterpLagrange::new(2, None).unwrap();
assert_eq!(interp.npoint, 3);
assert_eq!(interp.xx.as_data(), &[-1.0, 0.0, 1.0]);
assert_eq!(interp.eta.dim(), 3);
assert_eq!(interp.lambda.dim(), 3);
assert_eq!(interp.dd1.dims(), (0, 0));
assert_eq!(interp.dd2.dims(), (0, 0));
}
fn check_lambda(nn: usize, params: InterpParams, tol: f64) {
let nnf = nn as f64;
let npoint = nn + 1;
let interp = InterpLagrange::new(nn, Some(params)).unwrap();
let m = f64::powf(2.0, nnf - 1.0) / nnf;
for i in 0..npoint {
let mut d = 1.0;
for j in 0..npoint {
if i != j {
d *= interp.xx[i] - interp.xx[j]
}
}
approx_eq(interp.lambda[i], 1.0 / d / m, tol);
}
}
#[test]
fn lambda_is_correct() {
let mut params = InterpParams::new();
for nn in 1..20 {
for (tol, grid_type) in [
(1e-12, InterpGrid::Uniform),
(1e-14, InterpGrid::ChebyshevGauss),
(1e-14, InterpGrid::ChebyshevGaussLobatto),
] {
params.grid_type = grid_type;
check_lambda(nn, params, tol);
}
}
}
fn check_psi(nn: usize, params: InterpParams, tol_comparison: f64) {
let npoint = nn + 1;
let interp = InterpLagrange::new(nn, Some(params)).unwrap();
for i in 0..npoint {
let mut sum = 0.0;
for j in 0..npoint {
let psi = interp.psi(i, interp.xx[j]).unwrap();
let mut ana = 1.0;
if i != j {
ana = 0.0;
}
assert_eq!(psi, ana);
sum += psi;
}
assert_eq!(sum, 1.0);
}
let xx = Vector::linspace(-1.0, 1.0, 20).unwrap();
for x in xx {
for j in 0..npoint {
let psi_j = interp.psi(j, x).unwrap();
let mut ell_j = 1.0;
for k in 0..npoint {
if j != k {
ell_j *= (x - interp.xx[k]) / (interp.xx[j] - interp.xx[k]);
}
}
approx_eq(psi_j, ell_j, tol_comparison);
}
}
}
#[test]
fn psi_is_correct() {
let mut params = InterpParams::new();
for nn in 1..20 {
for (tol, grid_type) in [
(1e-11, InterpGrid::Uniform),
(1e-14, InterpGrid::ChebyshevGauss),
(1e-14, InterpGrid::ChebyshevGaussLobatto),
] {
params.grid_type = grid_type;
check_psi(nn, params, tol);
}
}
}
fn check_eval<F>(nn: usize, params: InterpParams, tol: f64, _name: &str, mut f: F)
where
F: Copy + FnMut(f64) -> f64,
{
let npoint = nn + 1;
let interp = InterpLagrange::new(nn, Some(params)).unwrap();
let mut uu = Vector::new(npoint);
for (i, x) in interp.get_points().into_iter().enumerate() {
uu[i] = f(*x);
}
for x in interp.get_points() {
assert_eq!(interp.eval(*x, &uu).unwrap(), f(*x));
}
let nstation = 20;
let station = Vector::linspace(-1.0, 1.0, nstation).unwrap();
for i in 0..nstation {
approx_eq(interp.eval(station[i], &uu).unwrap(), f(station[i]), tol);
}
}
#[test]
fn eval_works_1() {
let f = |x| f64::cos(f64::exp(2.0 * x));
let mut params = InterpParams::new();
for (nn, tol, grid_type) in [
(4, 1.379, InterpGrid::Uniform), (6, 1.054, InterpGrid::Uniform), (4, 1.14, InterpGrid::ChebyshevGauss),
(4, 1.08, InterpGrid::ChebyshevGaussLobatto), ] {
params.grid_type = grid_type;
check_eval(nn, params, tol, "cos-exp", f);
}
}
#[test]
fn eval_works_2() {
let f = |x| 1.0 / (1.0 + 16.0 * x * x);
let mut params = InterpParams::new();
for (nn, tol, grid_type) in [
(4, 0.385, InterpGrid::Uniform), (6, 0.486, InterpGrid::Uniform), (8, 0.690, InterpGrid::Uniform), (4, 0.308, InterpGrid::ChebyshevGauss), (6, 0.175, InterpGrid::ChebyshevGauss), (4, 0.69, InterpGrid::ChebyshevGaussLobatto),
] {
params.grid_type = grid_type;
check_eval(nn, params, tol, "runge", f);
}
}
fn check_dd1_matrix(nn: usize, params: InterpParams, tol: f64) {
let npoint = nn + 1;
let mut interp = InterpLagrange::new(nn, Some(params)).unwrap();
interp.calc_dd1_matrix();
struct Args {}
let args = &mut Args {};
for i in 0..npoint {
let xi = interp.xx[i];
for j in 0..npoint {
if i == 0 {
deriv1_approx_eq_fw(interp.dd1.get(i, j), xi, args, tol, |x, _| {
Ok(interp.psi(j, x).unwrap())
});
} else if i == nn {
deriv1_approx_eq_bw(interp.dd1.get(i, j), xi, args, tol, |x, _| {
Ok(interp.psi(j, x).unwrap())
});
} else {
deriv1_approx_eq(interp.dd1.get(i, j), xi, args, tol, |x, _| {
Ok(interp.psi(j, x).unwrap())
});
}
}
}
}
#[test]
fn dd1_matrix_works() {
let nn_and_tols = [(2, 1e-11), (5, 1e-9), (10, 1e-8)];
let mut params = InterpParams::new();
for (nn, tol) in nn_and_tols {
check_dd1_matrix(nn, params, tol);
}
let nn_and_tols = [(2, 1e-11), (5, 1e-9), (10, 1e-8)];
params.eta_cutoff = 0;
for (nn, tol) in nn_and_tols {
check_dd1_matrix(nn, params, tol);
}
let nn_and_tols = [(2, 1e-11), (5, 1e-9), (10, 1e-8)];
params.no_eta_normalization = true;
for (nn, tol) in nn_and_tols {
check_dd1_matrix(nn, params, tol);
}
}
fn check_dd2_matrix(nn: usize, params: InterpParams, tol: f64) {
let npoint = nn + 1;
let mut interp = InterpLagrange::new(nn, Some(params)).unwrap();
interp.calc_dd2_matrix();
struct Args {}
let args = &mut Args {};
for i in 0..npoint {
let xi = interp.xx[i];
for j in 0..npoint {
if i == 0 {
deriv2_approx_eq_fw(interp.dd2.get(i, j), xi, args, tol, |x, _| {
Ok(interp.psi(j, x).unwrap())
});
} else if i == nn {
deriv2_approx_eq_bw(interp.dd2.get(i, j), xi, args, tol, |x, _| {
Ok(interp.psi(j, x).unwrap())
});
} else {
deriv2_approx_eq(interp.dd2.get(i, j), xi, args, tol, |x, _| {
Ok(interp.psi(j, x).unwrap())
});
}
}
}
}
#[test]
fn dd2_matrix_works() {
#[rustfmt::skip]
let nn_and_tols = [
(2, 1e-8),
(5, 1e-7),
(10, 1e-8),
];
let params = InterpParams::new();
for (nn, tol) in nn_and_tols {
println!("nn = {:?}", nn);
check_dd2_matrix(nn, params, tol);
}
}
fn check_dd1_error<F, G>(nn: usize, params: InterpParams, tol: f64, mut f: F, mut g: G)
where
F: FnMut(f64) -> f64,
G: FnMut(f64) -> f64,
{
let npoint = nn + 1;
let mut interp = InterpLagrange::new(nn, Some(params)).unwrap();
let mut uu = Vector::new(npoint);
for (i, x) in interp.get_points().into_iter().enumerate() {
uu[i] = f(*x);
}
interp.calc_dd1_matrix();
let mut num = Vector::new(npoint);
mat_vec_mul(&mut num, 1.0, &interp.dd1, &uu).unwrap();
let mut max_err = 0.0;
for i in 0..npoint {
let ana = g(interp.xx[i]);
let diff = f64::abs(num[i] - ana);
if diff > max_err {
max_err = diff;
}
}
approx_eq(max_err, 0.0, tol);
}
#[test]
fn dd1_times_uu_works() {
let mut params = InterpParams::new();
let f = |x| f64::powf(x, 8.0);
let g = |x| 8.0 * f64::powf(x, 7.0);
for (nn, grid_type, tol) in [
(8, InterpGrid::Uniform, 1e-13),
(8, InterpGrid::ChebyshevGauss, 1e-13),
(8, InterpGrid::ChebyshevGaussLobatto, 1e-13),
] {
params.grid_type = grid_type;
check_dd1_error(nn, params, tol, f, g);
}
}
fn check_dd2_error<F, H>(nn: usize, params: InterpParams, tol: f64, mut f: F, mut h: H)
where
F: FnMut(f64) -> f64,
H: FnMut(f64) -> f64,
{
let npoint = nn + 1;
let mut interp = InterpLagrange::new(nn, Some(params)).unwrap();
let mut uu = Vector::new(npoint);
for (i, x) in interp.get_points().into_iter().enumerate() {
uu[i] = f(*x);
}
interp.calc_dd2_matrix();
let mut num = Vector::new(npoint);
mat_vec_mul(&mut num, 1.0, &interp.dd2, &uu).unwrap();
let mut max_err = 0.0;
for i in 0..npoint {
let ana = h(interp.xx[i]);
let diff = f64::abs(num[i] - ana);
if diff > max_err {
max_err = diff;
}
}
approx_eq(max_err, 0.0, tol);
}
#[test]
fn dd2_times_uu_works() {
let mut params = InterpParams::new();
let f = |x| f64::powf(x, 8.0);
let h = |x| 56.0 * f64::powf(x, 6.0);
for (nn, grid_type, tol) in [
(8, InterpGrid::Uniform, 1e-11),
(8, InterpGrid::ChebyshevGauss, 1e-12),
(8, InterpGrid::ChebyshevGaussLobatto, 1e-12),
] {
params.grid_type = grid_type;
check_dd2_error(nn, params, tol, f, h);
}
}
#[test]
fn dd_matrices_are_computed_just_once() {
let mut interp = InterpLagrange::new(2, None).unwrap();
interp.calc_dd1_matrix();
interp.calc_dd2_matrix();
interp.calc_dd1_matrix();
interp.calc_dd2_matrix();
}
fn check_eval_deriv1<F, G>(nn: usize, params: InterpParams, tol: f64, mut f: F, mut g: G)
where
F: Copy + FnMut(f64) -> f64,
G: Copy + FnMut(f64) -> f64,
{
let npoint = nn + 1;
let mut interp = InterpLagrange::new(nn, Some(params)).unwrap();
let mut uu = Vector::new(npoint);
for (i, x) in interp.get_points().into_iter().enumerate() {
uu[i] = f(*x);
}
interp.calc_dd1_matrix();
let mut d1_at_nodes = Vector::new(npoint);
mat_vec_mul(&mut d1_at_nodes, 1.0, &interp.dd1, &uu).unwrap();
let nstation = 20;
let stations = Vector::linspace(-1.0, 1.0, nstation).unwrap();
for i in 0..nstation {
let d1 = interp.eval_deriv1(stations[i], &uu).unwrap();
approx_eq(d1, g(stations[i]), tol);
if i == 0 || i == (nstation - 1) {
let j = if i == 0 { 0 } else { npoint - 1 };
assert!(f64::abs(stations[i] - interp.xx[j]) < 10.0 * f64::EPSILON);
approx_eq(d1, d1_at_nodes[j], 1e-14);
}
}
let x_mid = interp.xx[nn / 2];
let d1 = interp.eval_deriv1(x_mid, &uu).unwrap();
approx_eq(d1, g(x_mid), tol);
}
#[test]
fn eval_deriv1_works() {
let f = |x| f64::powf(x, 8.0);
let g = |x| 8.0 * f64::powf(x, 7.0);
let params = InterpParams::new();
check_eval_deriv1(8, params, 1e-13, f, g)
}
fn check_eval_deriv2<F, H>(nn: usize, params: InterpParams, tol: f64, mut f: F, mut h: H)
where
F: Copy + FnMut(f64) -> f64,
H: Copy + FnMut(f64) -> f64,
{
let npoint = nn + 1;
let mut interp = InterpLagrange::new(nn, Some(params)).unwrap();
let mut uu = Vector::new(npoint);
for (i, x) in interp.get_points().into_iter().enumerate() {
uu[i] = f(*x);
}
interp.calc_dd2_matrix();
let mut d2_at_nodes = Vector::new(npoint);
mat_vec_mul(&mut d2_at_nodes, 1.0, &interp.dd2, &uu).unwrap();
let nstation = 20;
let stations = Vector::linspace(-1.0, 1.0, nstation).unwrap();
for i in 0..nstation {
let d2 = interp.eval_deriv2(stations[i], &uu).unwrap();
approx_eq(d2, h(stations[i]), tol);
if i == 0 || i == (nstation - 1) {
let j = if i == 0 { 0 } else { npoint - 1 };
assert!(f64::abs(stations[i] - interp.xx[j]) < 10.0 * f64::EPSILON);
approx_eq(d2, d2_at_nodes[j], 1e-12);
}
}
let x_mid = interp.xx[nn / 2];
let d2 = interp.eval_deriv2(x_mid, &uu).unwrap();
approx_eq(d2, h(x_mid), tol);
}
#[test]
fn eval_deriv2_works() {
let f = |x| f64::powf(x, 8.0);
let h = |x| 56.0 * f64::powf(x, 6.0);
let params = InterpParams::new();
check_eval_deriv2(8, params, 1e-12, f, h)
}
#[test]
fn lebesgue_works_uniform() {
let mut params = InterpParams::new();
params.grid_type = InterpGrid::Uniform;
params.lebesgue_estimate_nstation = 10; let tol = 0.25;
let interp = InterpLagrange::new(5, Some(params)).unwrap();
approx_eq(interp.estimate_lebesgue_constant(), 3.106301040275436e+00, tol);
}
#[test]
fn lebesgue_works_chebyshev_gauss() {
let mut params = InterpParams::new();
let data = [
(4, 1e-14, 1.988854381999833e+00),
(8, 1e-15, 2.361856787767076e+00),
(24, 1e-14, 3.011792612349363e+00),
];
for (nn, tol, reference) in data {
params.grid_type = InterpGrid::ChebyshevGauss;
params.lebesgue_estimate_nstation = 10;
let interp = InterpLagrange::new(nn, Some(params)).unwrap();
approx_eq(interp.estimate_lebesgue_constant(), reference, tol);
}
}
#[test]
fn lebesgue_works_chebyshev_gauss_lobatto() {
let mut params = InterpParams::new();
let data = [
(4, 1e-15, 1.798761778849085e+00),
(8, 1e-15, 2.274730699116020e+00),
(24, 1e-14, 2.984443326362511e+00),
];
for (nn, _, reference) in data {
params.grid_type = InterpGrid::ChebyshevGaussLobatto;
params.lebesgue_estimate_nstation = 10;
let tol = 0.12; let interp = InterpLagrange::new(nn, Some(params)).unwrap();
approx_eq(interp.estimate_lebesgue_constant(), reference, tol);
}
}
#[test]
fn estimate_max_error_all_works() {
let f = |x, _: &mut NoArgs| Ok(x * x * x);
let g = |x, _: &mut NoArgs| Ok(3.0 * x * x);
let h = |x, _: &mut NoArgs| Ok(6.0 * x);
let mut params = InterpParams::new();
params.error_estimate_nstation = 21;
let interp = InterpLagrange::new(2, Some(params)).unwrap();
let args = &mut 0;
let just_err_f = interp.estimate_max_error(args, f).unwrap();
let (err_f, err_g, err_h) = interp.estimate_max_error_all(false, args, f, g, h).unwrap();
assert_eq!(just_err_f, err_f);
approx_eq(err_f, 2.0 * SQRT_3 / 9.0, 9.1e-4); assert_eq!(err_g, 2.0); assert_eq!(err_h, 6.0);
params.error_estimate_nstation = 3;
let interp = InterpLagrange::new(2, Some(params)).unwrap();
let (err_f, err_g, err_h) = interp.estimate_max_error_all(true, args, f, g, h).unwrap();
assert_eq!(err_f, 0.0);
assert_eq!(err_g, 1.0);
assert_eq!(err_h, 0.0);
}
#[test]
fn estimate_functions_capture_errors() {
struct Args {
counter: usize,
target: usize,
}
let args = &mut Args { counter: 0, target: 0 };
let f = |_, _: &mut Args| Ok(0.0);
let g = |_, _: &mut Args| Ok(0.0);
let h = |_, _: &mut Args| Ok(0.0);
assert!(h(0.0, args).is_ok());
let f_err = |_, a: &mut Args| {
let res = if a.counter == a.target { Err("f: stop") } else { Ok(0.0) };
a.counter += 1;
res
};
let g_err = |_, _: &mut Args| Err("g: stop");
let h_err = |_, _: &mut Args| Err("h: stop");
let nn = 1;
let npoint = nn + 1;
let interp = InterpLagrange::new(nn, None).unwrap();
assert_eq!(interp.estimate_max_error(args, f_err).err(), Some("f: stop"));
args.counter = 0;
args.target = npoint;
assert_eq!(interp.estimate_max_error(args, f_err).err(), Some("f: stop"));
args.counter = 0;
args.target = 0;
assert_eq!(
interp.estimate_max_error_all(true, args, f_err, g, h).err(),
Some("f: stop")
);
args.counter = 0;
args.target = npoint;
assert_eq!(
interp.estimate_max_error_all(true, args, f_err, g, h).err(),
Some("f: stop")
);
assert_eq!(
interp.estimate_max_error_all(true, args, f, g_err, h).err(),
Some("g: stop")
);
assert_eq!(
interp.estimate_max_error_all(true, args, f, g, h_err).err(),
Some("h: stop")
);
}
#[test]
fn getters_work() {
let mut interp = InterpLagrange::new(2, None).unwrap();
assert_eq!(interp.get_degree(), 2);
assert_eq!(interp.get_grid_type(), InterpGrid::ChebyshevGaussLobatto);
assert_eq!(interp.get_points().as_data(), &[-1.0, 0.0, 1.0]);
assert_eq!(interp.get_xrange(), (-1.0, 1.0));
assert_eq!(interp.get_lambda().dim(), 3);
interp.calc_dd1_matrix();
interp.calc_dd2_matrix();
assert_eq!(interp.get_dd1().unwrap().dims(), (3, 3));
assert_eq!(interp.get_dd2().unwrap().dims(), (3, 3));
}
#[test]
fn params_validate_captures_errors() {
let mut params = InterpParams::new();
params.lebesgue_estimate_nstation = 0;
assert_eq!(params.validate().err(), Some("lebesgue_estimate_nstation must be ≥ 2"));
params.lebesgue_estimate_nstation = 2;
params.error_estimate_nstation = 0;
assert_eq!(params.validate().err(), Some("error_estimate_nstation must be ≥ 2"));
}
#[test]
fn new_captures_errors() {
assert_eq!(
InterpLagrange::new(0, None).err(),
Some("the polynomial degree must be in [1, 2048]")
);
assert_eq!(
InterpLagrange::new(0, None).err(),
Some("the polynomial degree must be in [1, 2048]")
);
let mut params = InterpParams::new();
params.lebesgue_estimate_nstation = 0;
assert_eq!(
InterpLagrange::new(2, Some(params)).err(),
Some("lebesgue_estimate_nstation must be ≥ 2")
);
}
#[test]
fn functions_check_ranges() {
let interp = InterpLagrange::new(2, None).unwrap();
let uu = Vector::new(0);
assert_eq!(interp.psi(100, -1.0).err(), Some("j must be in 0 ≤ j ≤ N"));
assert_eq!(interp.psi(0, -2.0).err(), Some("x must be in -1 ≤ x ≤ 1"));
assert_eq!(interp.eval(-2.0, &uu).err(), Some("x must be in -1 ≤ x ≤ 1"));
assert_eq!(
interp.eval(-1.0, &uu).err(),
Some("the dimension of the data vector U must be equal to N + 1")
);
assert_eq!(interp.eval_deriv1(-2.0, &uu).err(), Some("x must be in -1 ≤ x ≤ 1"));
assert_eq!(
interp.eval_deriv1(-1.0, &uu).err(),
Some("the dimension of the data vector U must be equal to N + 1")
);
assert_eq!(interp.eval_deriv2(-2.0, &uu).err(), Some("x must be in -1 ≤ x ≤ 1"));
assert_eq!(
interp.eval_deriv2(-1.0, &uu).err(),
Some("the dimension of the data vector U must be equal to N + 1")
);
assert_eq!(interp.get_dd1().err(), Some("calc_dd1_matrix must be called first"));
assert_eq!(interp.get_dd2().err(), Some("calc_dd2_matrix must be called first"));
}
}