use ndarray::Array1;
use rebasis::{
CoordinateMap, Normalization, Parameter, Profile, Space, Weight, inner_products, measure,
measure_derivative,
};
const U: Weight = Weight::Uniform;
const NM: CoordinateMap = CoordinateMap::NM_CMINV;
fn linspace(a: f64, b: f64, n: usize) -> Vec<f64> {
(0..n)
.map(|i| a + (b - a) * i as f64 / (n - 1) as f64)
.collect()
}
fn max_abs(a: &ndarray::Array2<f64>) -> f64 {
a.iter().fold(0.0f64, |m, v| m.max(v.abs()))
}
fn pairs() -> Vec<(&'static str, Space, Space)> {
let centers = [499.3712, 500.0217, 500.9031];
let hats = Space::hats(&linspace(497.0, 503.0, 2401)).unwrap();
let boxes = Space::boxes(&linspace(497.0, 503.0, 241)).unwrap();
let splines = Space::bsplines_clamped(&linspace(497.0, 503.0, 61), 3).unwrap();
vec![
(
"gaussian/hats",
Space::profiles(¢ers, &[Profile::gaussian(0.2).unwrap()], 6.0).unwrap(),
hats.clone(),
),
(
"voigt/hats",
Space::profiles(¢ers, &[Profile::voigt(0.1, 0.05).unwrap()], 10.0).unwrap(),
hats.clone(),
),
(
"lorentzian/boxes",
Space::profiles(¢ers, &[Profile::lorentzian(0.05).unwrap()], 20.0).unwrap(),
boxes.clone(),
),
(
"super_gaussian/splines",
Space::profiles(
¢ers,
&[Profile::super_gaussian(0.2, 0.3, 3.0).unwrap()],
5.0,
)
.unwrap(),
splines.clone(),
),
(
"rectangles/hats",
Space::rectangles(¢ers, &[0.4]).unwrap(),
hats,
),
("splines/boxes", splines, boxes),
]
}
#[test]
fn inner_products_are_invariant_under_a_change_of_coordinate() {
for (name, a, b) in pairs() {
let direct = inner_products(&a, &b, U).to_dense();
let mapped = inner_products(
&a.mapped(NM, true).unwrap(),
&b.mapped(NM, false).unwrap(),
U,
)
.to_dense();
let err = max_abs(&(&direct - &mapped));
assert!(
err < 2e-10 * max_abs(&direct),
"{name}: {err:e} of {:e}",
max_abs(&direct)
);
}
}
#[test]
fn measuring_a_wavenumber_spectrum_with_a_wavelength_line_shape() {
let nu = linspace(19_880.0, 20_120.0, 24001);
let hats_nu = Space::hats(&nu).unwrap();
let f: Array1<f64> = nu.iter().map(|v| 1.0 + 0.3 * (v / 7.0).sin()).collect();
let ils_nm =
Space::profiles(&[499.12, 500.0, 500.7], &[Profile::gaussian(0.3).unwrap()], 6.0).unwrap();
let g_nu = measure(
&ils_nm.mapped(NM, true).unwrap(),
&hats_nu,
Normalization::Integral,
U,
)
.apply(f.view());
let g_nm = measure(
&ils_nm,
&hats_nu.mapped(NM, false).unwrap(),
Normalization::Integral,
U,
)
.apply(f.view());
for (a, b) in g_nu.iter().zip(g_nm.iter()) {
assert!((a - b).abs() < 1e-10, "{a} vs {b}");
}
let ones = Array1::<f64>::ones(nu.len());
let g1 = measure(
&ils_nm.mapped(NM, true).unwrap(),
&hats_nu,
Normalization::Integral,
U,
)
.apply(ones.view());
let worst = g1.iter().fold(0.0f64, |m, v| m.max((v - 1.0).abs()));
assert!(worst < 1e-12, "constant: {worst:e}");
}
#[test]
fn integrals_with_the_jacobian_are_preserved() {
for (name, a, _) in pairs() {
let i = a.integrals(U);
let j = a.mapped(NM, true).unwrap().integrals(U);
for (x, y) in i.iter().zip(&j) {
assert!((x - y).abs() < 1e-11 * x.abs(), "{name}: {x} vs {y}");
}
}
}
#[test]
fn affine_maps_stay_exact_polynomials() {
let hats = Space::hats(&linspace(400.0, 401.0, 11)).unwrap();
let um = CoordinateMap::Affine {
offset: 0.0,
scale: 1e-3,
};
let m = hats.mapped(um, false).unwrap();
let a = hats.integrals(U);
let b = m.integrals(U);
for (x, y) in a.iter().zip(&b) {
assert!((x * 1e-3 - y).abs() < 1e-18);
}
let g = inner_products(&m, &m, U).to_dense();
let h = inner_products(&hats, &hats, U).to_dense() * 1e-3;
let err = max_abs(&(&g - &h));
assert!(
err < 1e-11 * max_abs(&h),
"affine: {err:e} of {:e}",
max_abs(&h)
);
}
#[test]
fn derivatives_with_respect_to_wavelength_parameters_of_a_mapped_kernel() {
let nu = linspace(19_800.0, 20_200.0, 8001);
let hats_nu = Space::hats(&nu).unwrap();
let build = |c: f64, s: f64| {
Space::profiles(&[499.7123 + c], &[Profile::gaussian(0.2 * s).unwrap()], 6.0)
.unwrap()
.mapped(NM, true)
.unwrap()
};
for (p, plus, minus) in [
(Parameter::Center, build(1e-5, 1.0), build(-1e-5, 1.0)),
(
Parameter::Scale,
build(0.0, 1e-5f64.exp()),
build(0.0, (-1e-5f64).exp()),
),
] {
let (_, dw) =
measure_derivative(&build(0.0, 1.0), &hats_nu, Normalization::Integral, U, p).unwrap();
let fd = (measure(&plus, &hats_nu, Normalization::Integral, U).to_dense()
- measure(&minus, &hats_nu, Normalization::Integral, U).to_dense())
/ 2e-5;
let err = max_abs(&(&dw.to_dense() - &fd));
assert!(
err < 1e-5 * max_abs(&fd),
"{p:?}: {err:e} of {:e}",
max_abs(&fd)
);
}
}
#[test]
fn reciprocal_maps_reject_supports_through_zero() {
let s = Space::boxes(&[-1.0, 1.0]).unwrap();
assert!(s.mapped(NM, false).is_err());
}