#![allow(clippy::cast_precision_loss)]
use crate::{
boundary::BoundaryPolicy,
error::SemiflowError,
grid::Grid1D,
grid_chebyshev_nodes::{chebyshev_nodes, chebyshev_weights, is_supported_m},
};
use crate::grid_chebyshev_septic::sample_septic_1d as sample_virtual_node;
const EPSILON_FACTOR: f64 = 8.0;
pub(crate) use chebyshev_generic::sample_chebyshev_spectral_1d_generic;
pub(crate) mod chebyshev_generic;
fn reflect_into_domain(x: f64, xmin: f64, xmax: f64) -> f64 {
let width = xmax - xmin;
debug_assert!(width > 0.0, "reflect_into_domain: xmin must be < xmax");
let period = 2.0 * width;
let t = (x - xmin).rem_euclid(period);
let folded = if t > width { period - t } else { t };
xmin + folded
}
fn wrap_periodic(x: f64, xmin: f64, xmax: f64) -> f64 {
let width = xmax - xmin;
debug_assert!(width > 0.0, "wrap_periodic: xmin must be < xmax");
let t = (x - xmin).rem_euclid(width);
xmin + t.min(width - f64::EPSILON * width.abs())
}
fn linear_extrapolate_chebyshev(values: &[f64], grid: &Grid1D, x: f64, m: usize) -> f64 {
let half = (grid.xmax - grid.xmin) * 0.5;
let mid = (grid.xmax + grid.xmin) * 0.5;
let x0 = mid + half; let x1 = mid + half * libm::cos(core::f64::consts::PI / m as f64); let f0 = sample_virtual_node(values, grid, x0);
let f1 = sample_virtual_node(values, grid, x1);
let slope = if (x0 - x1).abs() > f64::EPSILON {
(f0 - f1) / (x0 - x1)
} else {
0.0
};
if x > grid.xmax {
f0 + slope * (x - x0)
} else {
let xl = mid - half; let xl1 = mid + half * libm::cos((m as f64 - 1.0) * core::f64::consts::PI / m as f64);
let fl = sample_virtual_node(values, grid, xl);
let fl1 = sample_virtual_node(values, grid, xl1);
let slope_l = if (xl - xl1).abs() > f64::EPSILON {
(fl - fl1) / (xl - xl1)
} else {
0.0
};
fl + slope_l * (x - xl)
}
}
fn out_of_domain_sample(values: &[f64], grid: &Grid1D, x: f64, m: usize) -> f64 {
match grid.boundary {
BoundaryPolicy::Reflect | BoundaryPolicy::Robin { .. } => {
let reflected = reflect_into_domain(x, grid.xmin, grid.xmax);
debug_assert!(
reflected >= grid.xmin && reflected <= grid.xmax,
"reflect_into_domain produced out-of-domain result"
);
sample_chebyshev_1d(values, grid, reflected, m).unwrap_or(0.0)
}
BoundaryPolicy::Periodic => {
let wrapped = wrap_periodic(x, grid.xmin, grid.xmax);
sample_chebyshev_1d(values, grid, wrapped, m).unwrap_or(0.0)
}
BoundaryPolicy::ZeroExtend => 0.0,
BoundaryPolicy::LinearExtrapolate => linear_extrapolate_chebyshev(values, grid, x, m),
BoundaryPolicy::Dirichlet { value } => value,
BoundaryPolicy::Neumann => {
let nearest = if x > grid.xmax { grid.xmax } else { grid.xmin };
sample_virtual_node(values, grid, nearest)
}
BoundaryPolicy::OddReflect => {
let reflected = reflect_into_domain(x, grid.xmin, grid.xmax);
-sample_chebyshev_1d(values, grid, reflected, m).unwrap_or(0.0)
}
}
}
fn barycentric_lobatto_eval(
values: &[f64],
grid: &Grid1D,
x: f64,
m: usize,
nodes_ref: &[f64],
weights_ref: &[f64],
) -> f64 {
let mid = (grid.xmax + grid.xmin) * 0.5;
let half = (grid.xmax - grid.xmin) * 0.5;
let dx_abs = grid.dx().abs();
let guard = EPSILON_FACTOR * f64::EPSILON * dx_abs;
let mut num = 0.0_f64;
let mut den = 0.0_f64;
for k in 0..=m {
let x_k = mid + half * nodes_ref[k];
let w_k = weights_ref[k];
let diff = x - x_k;
if diff.abs() < guard {
return sample_virtual_node(values, grid, x_k);
}
let f_k = sample_virtual_node(values, grid, x_k);
let term = w_k / diff;
num += term * f_k;
den += term;
}
num / den
}
pub(crate) fn sample_chebyshev_1d(
values: &[f64],
grid: &Grid1D,
x: f64,
m: usize,
) -> Result<f64, SemiflowError> {
if !is_supported_m(m) {
return Err(SemiflowError::DomainViolation {
what: "Chebyshev M must be in {8,16,32,64,128,256,512}",
value: m as f64,
});
}
if x < grid.xmin || x > grid.xmax {
return Ok(out_of_domain_sample(values, grid, x, m));
}
let nodes_ref = chebyshev_nodes(m)?;
let weights_ref = chebyshev_weights(m)?;
Ok(barycentric_lobatto_eval(
values,
grid,
x,
m,
nodes_ref,
weights_ref,
))
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
use crate::{boundary::InterpKind, grid::OobPolicy, BoundaryPolicy, Grid1D};
fn gauss(x: f64) -> f64 {
libm::exp(-x * x)
}
#[test]
fn at_lobatto_node_returns_node_value() {
let grid = Grid1D::new(-5.0, 5.0, 128).unwrap();
let values: Vec<f64> = (0..grid.n).map(|i| gauss(grid.x_at(i))).collect();
let x_first = grid.xmax; let result = sample_chebyshev_1d(&values, &grid, x_first, 16).unwrap();
let expected = sample_virtual_node(&values, &grid, x_first);
let err = (result - expected).abs();
assert!(
err < 1e-14,
"at-node removable-singularity: err = {err:.3e}, expected < 1e-14"
);
}
#[test]
fn interior_sample_finite() {
let grid = Grid1D::new(-3.0, 3.0, 64).unwrap();
let values: Vec<f64> = (0..grid.n).map(|i| gauss(grid.x_at(i))).collect();
let probe = 0.731_f64;
let result = sample_chebyshev_1d(&values, &grid, probe, 32).unwrap();
assert!(
result.is_finite(),
"Chebyshev sample must be finite at interior point"
);
}
#[test]
fn unsupported_m_returns_error() {
let grid = Grid1D::new(-1.0, 1.0, 16).unwrap();
let values: Vec<f64> = vec![0.0; 16];
let err = sample_chebyshev_1d(&values, &grid, 0.0, 7);
assert!(err.is_err(), "M=7 must return DomainViolation error");
}
#[test]
fn chebyshev_improves_over_cubic_sanity() {
let grid = Grid1D::new(-5.0, 5.0, 64).unwrap();
let values: Vec<f64> = (0..grid.n).map(|i| gauss(grid.x_at(i))).collect();
let dx = grid.dx();
let probe = grid.x_at(32) + 0.5 * dx;
let analytic = gauss(probe);
let cubic_result = grid.interp(&values, probe).unwrap();
let cubic_err = (cubic_result - analytic).abs();
let cheb_result = sample_chebyshev_1d(&values, &grid, probe, 32).unwrap();
let cheb_err = (cheb_result - analytic).abs();
assert!(
cheb_err <= cubic_err * 1000.0,
"Chebyshev not catastrophically worse than cubic: cubic={cubic_err:.3e} cheb={cheb_err:.3e}"
);
}
#[test]
fn dispatch_via_interp_with_bc() {
let grid = Grid1D::new(-2.0, 2.0, 32).unwrap();
let cheb_grid = grid.with_interp(InterpKind::ChebyshevSpectralWithBC {
m: 16,
oob_policy: OobPolicy::Inherit,
});
let values: Vec<f64> = (0..grid.n).map(|i| gauss(grid.x_at(i))).collect();
let probe = 0.25_f64;
let result = cheb_grid.interp(&values, probe).unwrap();
assert!(
result.is_finite(),
"Dispatch via Grid1D::interp must return finite value"
);
}
#[test]
fn out_of_domain_reflect_returns_finite() {
let grid = Grid1D::new(-1.0, 1.0, 64)
.unwrap()
.with_boundary(BoundaryPolicy::Reflect);
let values: Vec<f64> = (0..grid.n).map(|i| gauss(grid.x_at(i))).collect();
let result = sample_chebyshev_1d(&values, &grid, 1.5, 16).unwrap();
assert!(result.is_finite(), "Reflect OOB must be finite: {result}");
}
#[test]
fn out_of_domain_zero_extend_returns_zero() {
let grid = Grid1D::new(-1.0, 1.0, 64)
.unwrap()
.with_boundary(BoundaryPolicy::ZeroExtend);
let values: Vec<f64> = (0..grid.n).map(|i| gauss(grid.x_at(i))).collect();
let result = sample_chebyshev_1d(&values, &grid, 2.0, 16).unwrap();
assert_eq!(result, 0.0, "ZeroExtend OOB must return 0.0");
}
}