use numeris::{FloatScalar, Vector};
use numeris::ode::OdeError;
const B_COEFFS: [[f64; 9]; 10] = [
[-2.869_754_464_285_714e-1,
-5.890_197_861_552_028e-01,
9.640_148_258_377_425e-01,
-1.240_890_376_984_127e+00,
1.140_564_373_897_707e+00,
-7.209_438_381_834_215e-01,
2.978_546_626_984_127e-01,
-7.249_696_869_488_536e-02,
7.892_554_012_345_68e-03], [7.892_554_012_345_68e-03,
-3.580_084_325_396_825_6e-01,
-3.048_878_417_107_584e-01,
3.010_402_888_007_055e-01,
-2.464_285_714_285_714_4e-01,
1.461_025_683_421_517e-01,
-5.796_930_114_638_448e-02,
1.372_271_825_396_825_4e-02,
-1.463_982_583_774_250_5e-03], [-1.463_982_583_774_250_5e-03,
2.106_839_726_631_393_2e-02,
-4.107_118_055_555_555_3e-01,
-1.819_133_046_737_213_3e-01,
1.165_784_832_451_499_1e-01,
-6.196_676_587_301_587_5e-02,
2.312_803_130_511_464e-2,
-5.265_928_130_511_464e-03,
5.468_75e-4], [5.468_75e-4,
-6.385_857_583_774_25e-3,
4.075_589_726_631_393_5e-02,
-4.566_493_055_555_555_5e-01,
-1.130_070_546_737_213_4e-01,
4.767_223_324_514_991e-02,
-1.602_926_587_301_587_2e-02,
3.440_531_305_114_638_6e-03,
-3.440_531_305_114_638_7e-04], [-3.440_531_305_114_638_7e-04,
3.643_353_174_603_174_6e-03,
-1.877_177_028_218_694_8e-02,
6.965_636_022_927_689e-02,
-5e-1,
-6.965_636_022_927_689e-02,
1.877_177_028_218_694_8e-02,
-3.643_353_174_603_174_6e-03,
3.440_531_305_114_638_7e-04], [3.440_531_305_114_638_7e-04,
-3.440_531_305_114_638_6e-03,
1.602_926_587_301_587_2e-02,
-4.767_223_324_514_991e-02,
1.130_070_546_737_213_4e-01,
-5.433_506_944_444_444e-1,
-4.075_589_726_631_393_5e-02,
6.385_857_583_774_25e-3,
-5.468_75e-4], [-5.468_75e-4,
5.265_928_130_511_464e-03,
-2.312_803_130_511_464e-2,
6.196_676_587_301_587_5e-02,
-1.165_784_832_451_499_1e-01,
1.819_133_046_737_213_3e-01,
-5.892_881_944_444_445e-1,
-2.106_839_726_631_393_2e-02,
1.463_982_583_774_250_5e-03], [1.463_982_583_774_250_5e-03,
-1.372_271_825_396_825_4e-02,
5.796_930_114_638_448e-02,
-1.461_025_683_421_517e-01,
2.464_285_714_285_714_4e-01,
-3.010_402_888_007_055e-01,
3.048_878_417_107_584e-01,
-6.419_915_674_603_175e-01,
-7.892_554_012_345_68e-03], [-7.892_554_012_345_68e-03,
7.249_696_869_488_536e-02,
-2.978_546_626_984_127e-01,
7.209_438_381_834_215e-01,
-1.140_564_373_897_707e+00,
1.240_890_376_984_127e+00,
-9.640_148_258_377_425e-01,
5.890_197_861_552_028e-01,
-7.130_245_535_714_286e-01], [2.869_754_464_285_714e-1,
-2.590_671_571_869_488_6e+00,
1.040_361_304_012_345_6e+01,
-2.440_379_216_269_841_2e+01,
3.687_985_008_818_342_4e+01,
-3.729_947_062_389_770_5e+01,
2.534_682_787_698_412_5e+01,
-1.129_513_089_726_631_3e+01,
3.171_798_804_012_345_5e+00], ];
const A_COEFFS: [[f64; 9]; 10] = [
[6.107_264_986_171_236e-02,
1.004_385_872_615_039_2e-01,
-2.179_954_555_475_388_8e-01,
3.026_027_386_964_887e-1,
-2.871_720_052_709_636e-01,
1.846_507_986_612_153_4e-01,
-7.709_378_006_253_007e-2,
1.889_758_197_049_863_6e-02,
-2.067_782_237_053_070_5e-03], [-2.067_782_237_053_070_5e-03,
7.968_268_999_519_e-02,
2.599_842_672_759_339_3e-02,
-4.430_174_763_508_097e-02,
4.206_217_682_780_183e-02,
-2.663_144_340_227_673_4e-02,
1.095_709_074_875_741_5e-02,
-2.653_619_528_619_528_7e-03,
2.875_418_370_210_037e-04], [2.875_418_370_210_037e-04,
-4.655_658_770_242_104e-03,
9.003_419_612_794_612e-02,
1.844_912_417_829_084_5e-03,
-8.071_476_170_434_504e-03,
5.831_905_363_155_364e-03,
-2.477_929_092_512_426e-03,
6.055_846_160_012_827e-04,
-6.574_299_543_049_544e-05], [-6.574_299_543_049_544e-05,
8.792_287_958_954_625e-04,
-7.022_406_605_739_94e-03,
9.555_660_774_410_775e-02,
-6.438_705_006_413_34e-03,
2.121_412_538_079_205e-04,
3.094_937_469_937_47e-04,
-1.111_812_570_145_903_5e-04,
1.389_765_712_682_379_3e-05], [1.389_765_712_682_379_3e-05,
-1.908_219_095_719_095_8e-04,
1.379_544_452_461_119e-03,
-8.189_809_804_393_138e-03,
9.730_771_254_208_755e-02,
-8.189_809_804_393_138e-03,
1.379_544_452_461_119e-03,
-1.908_219_095_719_095_8e-04,
1.389_765_712_682_379_3e-05], [1.389_765_712_682_379_3e-05,
-1.111_812_570_145_903_5e-04,
3.094_937_469_937_47e-04,
2.121_412_538_079_205e-04,
-6.438_705_006_413_34e-03,
9.555_660_774_410_775e-02,
-7.022_406_605_739_94e-03,
8.792_287_958_954_625e-04,
-6.574_299_543_049_544e-05], [-6.574_299_543_049_544e-05,
6.055_846_160_012_827e-04,
-2.477_929_092_512_426e-03,
5.831_905_363_155_364e-03,
-8.071_476_170_434_504e-03,
1.844_912_417_829_084_5e-03,
9.003_419_612_794_612e-02,
-4.655_658_770_242_104e-03,
2.875_418_370_210_037e-04], [2.875_418_370_210_037e-04,
-2.653_619_528_619_528_7e-03,
1.095_709_074_875_741_5e-02,
-2.663_144_340_227_673_4e-02,
4.206_217_682_780_183e-02,
-4.430_174_763_508_097e-02,
2.599_842_672_759_339_3e-02,
7.968_268_999_519_e-02,
-2.067_782_237_053_070_5e-03], [-2.067_782_237_053_070_5e-03,
1.889_758_197_049_863_6e-02,
-7.709_378_006_253_007e-2,
1.846_507_986_612_153_4e-01,
-2.871_720_052_709_636e-01,
3.026_027_386_964_887e-1,
-2.179_954_555_475_388_8e-01,
1.004_385_872_615_039_2e-01,
6.107_264_986_171_236e-02], [6.107_264_986_171_236e-02,
-5.517_216_309_924_643e-01,
2.217_512_976_992_144,
-5.207_196_368_446_368e+00,
7.879_804_681_236_973e+00,
-7.982_325_887_846_721e+00,
5.432_705_327_080_327e+00,
-2.416_610_850_569_184e+00,
6.500_924_360_169_151e-01], ];
#[derive(Debug, Clone, Copy)]
pub struct GJSettings<T> {
pub h: T,
pub max_corrector_iters: usize,
pub corrector_tol: T,
pub max_startup_iters: usize,
pub startup_tol: T,
pub max_steps: usize,
pub dense_output: bool,
}
impl Default for GJSettings<f64> {
fn default() -> Self {
Self {
h: 60.0,
max_corrector_iters: 2,
corrector_tol: 1.0e-13,
max_startup_iters: 12,
startup_tol: 1.0e-13,
max_steps: 1_000_000,
dense_output: false,
}
}
}
impl Default for GJSettings<f32> {
fn default() -> Self {
Self {
h: 60.0,
max_corrector_iters: 2,
corrector_tol: 1.0e-5,
max_startup_iters: 12,
startup_tol: 1.0e-5,
max_steps: 1_000_000,
dense_output: false,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GJDenseOutput<T: FloatScalar, const D: usize> {
pub t: Vec<T>,
pub r: Vec<Vector<T, D>>,
pub v: Vec<Vector<T, D>>,
pub a: Vec<Vector<T, D>>,
}
#[derive(Debug, Clone)]
pub struct GJSolution<T: FloatScalar, const D: usize> {
pub t: T,
pub r: Vector<T, D>,
pub v: Vector<T, D>,
pub evals: usize,
pub steps: usize,
pub startup_iters: usize,
pub dense: Option<GJDenseOutput<T, D>>,
}
pub struct GaussJackson8;
impl GaussJackson8 {
pub fn integrate<T: FloatScalar, const D: usize>(
t0: T,
tf: T,
r0: &Vector<T, D>,
v0: &Vector<T, D>,
mut f: impl FnMut(T, &Vector<T, D>, &Vector<T, D>) -> Vector<T, D>,
settings: &GJSettings<T>,
) -> Result<GJSolution<T, D>, OdeError> {
let zero = T::zero();
let one = T::one();
let dir = if tf >= t0 { one } else { -one };
let h = settings.h.abs() * dir;
if h == zero {
return Err(OdeError::StepNotFinite);
}
let total_span = tf - t0;
if (total_span / h).to_f64().unwrap_or(0.0).abs() < 8.0 {
return Err(OdeError::StepNotFinite); }
let mut nevals: usize = 0;
let mut r: [Vector<T, D>; 9] = [Vector::<T, D>::zeros(); 9];
let mut v: [Vector<T, D>; 9] = [Vector::<T, D>::zeros(); 9];
let mut a: [Vector<T, D>; 9] = [Vector::<T, D>::zeros(); 9];
r[4] = *r0;
v[4] = *v0;
a[4] = f(t0, &r[4], &v[4]);
nevals += 1;
for i in 5..9 {
let offset_prev = (i as f64) - 1.0 - 4.0;
let tn = t0 + T::from(offset_prev).unwrap() * h;
let (rn1, vn1, ne) = rk4_step(tn, &r[i - 1], &v[i - 1], h, &mut f);
r[i] = rn1;
v[i] = vn1;
nevals += ne;
}
for i in (0..4).rev() {
let offset_prev = (i as f64) + 1.0 - 4.0;
let tn = t0 + T::from(offset_prev).unwrap() * h;
let (rn1, vn1, ne) = rk4_step(tn, &r[i + 1], &v[i + 1], -h, &mut f);
r[i] = rn1;
v[i] = vn1;
nevals += ne;
}
for i in 0..9 {
if i == 4 {
continue;
}
let offset = (i as f64) - 4.0;
let ti = t0 + T::from(offset).unwrap() * h;
a[i] = f(ti, &r[i], &v[i]);
nevals += 1;
}
let mut startup_iters = 0usize;
for _iter in 0..settings.max_startup_iters {
startup_iters += 1;
let a_prev = a;
let s4 = {
let mut acc = Vector::<T, D>::zeros();
for k in 0..9 {
let bk = T::from(B_COEFFS[4][k]).unwrap();
if bk != zero {
acc += a[k] * bk;
}
}
v[4] / h - acc
};
let big_s4 = {
let mut acc = Vector::<T, D>::zeros();
for k in 0..9 {
let ak = T::from(A_COEFFS[4][k]).unwrap();
if ak != zero {
acc += a[k] * ak;
}
}
r[4] / (h * h) - acc
};
let mut s: [Vector<T, D>; 9] = [Vector::<T, D>::zeros(); 9];
let mut big_s: [Vector<T, D>; 9] = [Vector::<T, D>::zeros(); 9];
s[4] = s4;
big_s[4] = big_s4;
for i in 5..9 {
big_s[i] = big_s[i - 1] + s[i - 1];
s[i] = s[i - 1] + a[i];
}
for i in (0..4).rev() {
s[i] = s[i + 1] - a[i + 1];
big_s[i] = big_s[i + 1] - s[i + 1] + a[i + 1];
}
for i in 0..9 {
if i == 4 {
continue;
}
let row_b = &B_COEFFS[i];
let row_a = &A_COEFFS[i];
let mut sum_b = Vector::<T, D>::zeros();
let mut sum_a = Vector::<T, D>::zeros();
for k in 0..9 {
let bk = T::from(row_b[k]).unwrap();
let ak = T::from(row_a[k]).unwrap();
sum_b += a[k] * bk;
sum_a += a[k] * ak;
}
v[i] = (s[i] + sum_b) * h;
r[i] = (big_s[i] + sum_a) * (h * h);
}
for i in 0..9 {
if i == 4 {
continue;
}
let offset = (i as f64) - 4.0;
let ti = t0 + T::from(offset).unwrap() * h;
a[i] = f(ti, &r[i], &v[i]);
nevals += 1;
}
let mut max_delta = zero;
let mut max_mag = T::from(1.0e-30).unwrap();
for i in 0..9 {
let delta = (a[i] - a_prev[i]).abs().scaled_norm();
let mag = a[i].abs().scaled_norm();
if delta > max_delta {
max_delta = delta;
}
if mag > max_mag {
max_mag = mag;
}
}
if max_delta / max_mag < settings.startup_tol {
break;
}
}
let mut t_current = t0 + T::from(4.0).unwrap() * h;
let mut s_cur: Vector<T, D> = {
let mut acc = Vector::<T, D>::zeros();
for k in 0..9 {
let bk = T::from(B_COEFFS[4][k]).unwrap();
if bk != zero {
acc += a[k] * bk;
}
}
v[4] / h - acc
};
let mut big_s_cur: Vector<T, D> = {
let mut acc = Vector::<T, D>::zeros();
for k in 0..9 {
let ak = T::from(A_COEFFS[4][k]).unwrap();
if ak != zero {
acc += a[k] * ak;
}
}
r[4] / (h * h) - acc
};
for i in 4..8 {
big_s_cur += s_cur;
s_cur += a[i + 1];
}
let mut dense_store: Option<GJDenseOutput<T, D>> = if settings.dense_output {
let mut d = GJDenseOutput {
t: Vec::new(),
r: Vec::new(),
v: Vec::new(),
a: Vec::new(),
};
for i in 4..9 {
let offset = (i as f64) - 4.0;
d.t.push(t0 + T::from(offset).unwrap() * h);
d.r.push(r[i]);
d.v.push(v[i]);
d.a.push(a[i]);
}
Some(d)
} else {
None
};
let mut nsteps: usize = 4;
while (tf - t_current) * dir > zero {
if nsteps >= settings.max_steps {
return Err(OdeError::MaxStepsExceeded);
}
let remaining = tf - t_current;
if (remaining.abs() - h.abs()).to_f64().unwrap_or(0.0) < -1.0e-12 * h.abs().to_f64().unwrap_or(1.0) {
let (rn, vn, ne) = rk4_step(t_current, &r[8], &v[8], remaining, &mut f);
if let Some(ref mut d) = dense_store {
let a_final = f(tf, &rn, &vn);
d.t.push(tf);
d.r.push(rn);
d.v.push(vn);
d.a.push(a_final);
return Ok(GJSolution {
t: tf,
r: rn,
v: vn,
evals: nevals + ne + 1,
steps: nsteps + 1,
startup_iters,
dense: dense_store,
});
}
return Ok(GJSolution {
t: tf,
r: rn,
v: vn,
evals: nevals + ne,
steps: nsteps + 1,
startup_iters,
dense: None,
});
}
let mut sum_b_pred = Vector::<T, D>::zeros();
let mut sum_a_pred = Vector::<T, D>::zeros();
for k in 0..9 {
let bk = T::from(B_COEFFS[9][k]).unwrap();
let ak_coef = T::from(A_COEFFS[9][k]).unwrap();
sum_b_pred += a[k] * bk;
sum_a_pred += a[k] * ak_coef;
}
let big_s_new = big_s_cur + s_cur;
let mut r_new = (big_s_new + sum_a_pred) * (h * h);
let mut v_new = (s_cur + sum_b_pred) * h;
let t_new = t_current + h;
let mut a_new = f(t_new, &r_new, &v_new);
nevals += 1;
let mut sum_b_corr_fixed = Vector::<T, D>::zeros();
let mut sum_a_corr_fixed = Vector::<T, D>::zeros();
for k in 0..8 {
let bk = T::from(B_COEFFS[8][k]).unwrap();
let ak_coef = T::from(A_COEFFS[8][k]).unwrap();
sum_b_corr_fixed += a[k + 1] * bk;
sum_a_corr_fixed += a[k + 1] * ak_coef;
}
let b8 = T::from(B_COEFFS[8][8]).unwrap();
let a8 = T::from(A_COEFFS[8][8]).unwrap();
for _corr_iter in 0..settings.max_corrector_iters {
let s_new_post = s_cur + a_new;
let r_cand = (big_s_new + sum_a_corr_fixed + a_new * a8) * (h * h);
let v_cand = (s_new_post + sum_b_corr_fixed + a_new * b8) * h;
let dr = (r_cand - r_new).abs().scaled_norm();
let dv = (v_cand - v_new).abs().scaled_norm();
let rmag = r_cand.abs().scaled_norm() + T::from(1.0e-30).unwrap();
let vmag = v_cand.abs().scaled_norm() + T::from(1.0e-30).unwrap();
r_new = r_cand;
v_new = v_cand;
if dr / rmag < settings.corrector_tol
&& dv / vmag < settings.corrector_tol
{
break;
}
a_new = f(t_new, &r_new, &v_new);
nevals += 1;
}
for i in 0..8 {
r[i] = r[i + 1];
v[i] = v[i + 1];
a[i] = a[i + 1];
}
r[8] = r_new;
v[8] = v_new;
a[8] = a_new;
s_cur += a[8];
big_s_cur = big_s_new;
t_current = t_new;
nsteps += 1;
if let Some(ref mut d) = dense_store {
d.t.push(t_current);
d.r.push(r[8]);
d.v.push(v[8]);
d.a.push(a[8]);
}
}
Ok(GJSolution {
t: t_current,
r: r[8],
v: v[8],
evals: nevals,
steps: nsteps,
startup_iters,
dense: dense_store,
})
}
pub fn interpolate<T: FloatScalar, const D: usize>(
t_interp: T,
sol: &GJSolution<T, D>,
) -> Result<(Vector<T, D>, Vector<T, D>), OdeError> {
let dense = sol.dense.as_ref().ok_or(OdeError::NoDenseOutput)?;
if dense.t.len() < 2 {
return Err(OdeError::NoDenseOutput);
}
let forward = dense.t[dense.t.len() - 1] > dense.t[0];
let t_first = dense.t[0];
let t_last = dense.t[dense.t.len() - 1];
let (lo, hi) = if forward {
(t_first, t_last)
} else {
(t_last, t_first)
};
if t_interp < lo || t_interp > hi {
return Err(OdeError::InterpOutOfBounds);
}
let idx = if forward {
let i = dense.t.partition_point(|&x| x <= t_interp);
i.saturating_sub(1)
} else {
let i = dense.t.partition_point(|&x| x >= t_interp);
i.saturating_sub(1)
};
let idx = idx.min(dense.t.len() - 2);
Ok(quintic_hermite(
dense.t[idx], &dense.r[idx], &dense.v[idx], &dense.a[idx],
dense.t[idx + 1], &dense.r[idx + 1], &dense.v[idx + 1], &dense.a[idx + 1],
t_interp,
))
}
#[allow(clippy::type_complexity)] pub fn interpolate_batch<T: FloatScalar, const D: usize>(
t_interps: &[T],
sol: &GJSolution<T, D>,
) -> Result<Vec<(Vector<T, D>, Vector<T, D>)>, OdeError> {
let dense = sol.dense.as_ref().ok_or(OdeError::NoDenseOutput)?;
if dense.t.len() < 2 {
return Err(OdeError::NoDenseOutput);
}
let forward = dense.t[dense.t.len() - 1] > dense.t[0];
let t_first = dense.t[0];
let t_last = dense.t[dense.t.len() - 1];
let (lo, hi) = if forward {
(t_first, t_last)
} else {
(t_last, t_first)
};
let mut results = Vec::with_capacity(t_interps.len());
let mut idx = 0usize;
let last_idx = dense.t.len() - 2;
for &t_interp in t_interps {
if t_interp < lo || t_interp > hi {
return Err(OdeError::InterpOutOfBounds);
}
if forward {
while idx < last_idx && dense.t[idx + 1] < t_interp {
idx += 1;
}
} else {
while idx < last_idx && dense.t[idx + 1] > t_interp {
idx += 1;
}
}
results.push(quintic_hermite(
dense.t[idx], &dense.r[idx], &dense.v[idx], &dense.a[idx],
dense.t[idx + 1], &dense.r[idx + 1], &dense.v[idx + 1], &dense.a[idx + 1],
t_interp,
));
}
Ok(results)
}
}
#[allow(clippy::too_many_arguments)] fn quintic_hermite<T, const D: usize>(
t_a: T, r_a: &Vector<T, D>, v_a: &Vector<T, D>, acc_a: &Vector<T, D>,
t_b: T, r_b: &Vector<T, D>, v_b: &Vector<T, D>, acc_b: &Vector<T, D>,
t: T,
) -> (Vector<T, D>, Vector<T, D>)
where
T: FloatScalar,
{
let h = t_b - t_a;
let s = (t - t_a) / h;
let half = T::from(0.5).unwrap();
let three = T::from(3.0).unwrap();
let four = T::from(4.0).unwrap();
let five = T::from(5.0).unwrap();
let six = T::from(6.0).unwrap();
let seven = T::from(7.0).unwrap();
let ten = T::from(10.0).unwrap();
let fifteen = T::from(15.0).unwrap();
let hh = h * h;
let d1 = *r_b - *r_a - *v_a * h - *acc_a * (hh * half);
let d2 = (*v_b - *v_a) * h - *acc_a * hh;
let d3 = (*acc_b - *acc_a) * hh;
let c3 = d1 * ten - d2 * four + d3 * half;
let c4 = d1 * (-fifteen) + d2 * seven - d3;
let c5 = d1 * six - d2 * three + d3 * half;
let s2 = s * s;
let s3 = s2 * s;
let s4 = s3 * s;
let s5 = s4 * s;
let r = *r_a + *v_a * (h * s) + *acc_a * (hh * half * s2) + c3 * s3 + c4 * s4 + c5 * s5;
let v = *v_a
+ *acc_a * (h * s)
+ c3 * (three * s2 / h)
+ c4 * (four * s3 / h)
+ c5 * (five * s4 / h);
(r, v)
}
fn rk4_step<T, const D: usize, F>(
t: T,
r: &Vector<T, D>,
v: &Vector<T, D>,
h: T,
f: &mut F,
) -> (Vector<T, D>, Vector<T, D>, usize)
where
T: FloatScalar,
F: FnMut(T, &Vector<T, D>, &Vector<T, D>) -> Vector<T, D>,
{
let half = T::from(0.5).unwrap();
let sixth = T::from(1.0 / 6.0).unwrap();
let two = T::from(2.0).unwrap();
let k1 = *v;
let l1 = f(t, r, v);
let k2 = *v + l1 * (h * half);
let l2 = f(t + h * half, &(*r + k1 * (h * half)), &k2);
let k3 = *v + l2 * (h * half);
let l3 = f(t + h * half, &(*r + k2 * (h * half)), &k3);
let k4 = *v + l3 * h;
let l4 = f(t + h, &(*r + k3 * h), &k4);
let r_new = *r + (k1 + k2 * two + k3 * two + k4) * (h * sixth);
let v_new = *v + (l1 + l2 * two + l3 * two + l4) * (h * sixth);
(r_new, v_new, 4)
}
#[cfg(test)]
mod tests {
use super::*;
const TAU: f64 = 2.0 * std::f64::consts::PI;
#[test]
fn gj8_harmonic_oscillator() {
let r0 = Vector::<f64, 1>::from_array([1.0]);
let v0 = Vector::<f64, 1>::from_array([0.0]);
let settings = GJSettings {
h: TAU / 500.0,
..GJSettings::default()
};
let sol = GaussJackson8::integrate(
0.0, TAU, &r0, &v0,
|_t, r, _v| -*r,
&settings,
).unwrap();
assert!(
(sol.r[0] - 1.0).abs() < 1e-10,
"harmonic r[0] = {:.3e}, err = {:.3e}", sol.r[0], (sol.r[0] - 1.0).abs()
);
assert!(sol.v[0].abs() < 1e-10, "harmonic v[0] = {:.3e}", sol.v[0]);
}
#[test]
fn gj8_damped_oscillator() {
let r0 = Vector::<f64, 1>::from_array([1.0]);
let v0 = Vector::<f64, 1>::from_array([0.0]);
let gamma: f64 = 0.01;
let omega: f64 = 1.0;
let t_final = 10.0_f64;
let settings = GJSettings { h: 1.0e-2, ..GJSettings::default() };
let sol = GaussJackson8::integrate(
0.0, t_final, &r0, &v0,
|_t, r, v| -*r * (omega * omega) - *v * gamma,
&settings,
).unwrap();
let wd = (omega * omega - 0.25 * gamma * gamma).sqrt();
let env = (-0.5 * gamma * t_final).exp();
let expected = env * (f64::cos(wd * t_final)
+ 0.5 * gamma / wd * f64::sin(wd * t_final));
assert!(
(sol.r[0] - expected).abs() < 1e-8,
"damped r = {:.6e}, expected {:.6e}, err = {:.3e}",
sol.r[0], expected, (sol.r[0] - expected).abs()
);
}
#[test]
fn gj8_kepler_circular_10_orbits() {
let r0 = Vector::<f64, 3>::from_array([1.0, 0.0, 0.0]);
let v0 = Vector::<f64, 3>::from_array([0.0, 1.0, 0.0]);
let energy = |r: &Vector<f64, 3>, v: &Vector<f64, 3>| {
let rmag = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt();
0.5 * (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) - 1.0 / rmag
};
let lz = |r: &Vector<f64, 3>, v: &Vector<f64, 3>| r[0] * v[1] - r[1] * v[0];
let e0 = energy(&r0, &v0);
let l0 = lz(&r0, &v0);
let tf = 10.0 * TAU;
let settings = GJSettings { h: TAU / 200.0, ..GJSettings::default() };
let sol = GaussJackson8::integrate(
0.0, tf, &r0, &v0,
|_t, r, _v| {
let r2 = r[0] * r[0] + r[1] * r[1] + r[2] * r[2];
*r * (-1.0 / (r2 * r2.sqrt()))
},
&settings,
).unwrap();
let e_drift = ((energy(&sol.r, &sol.v) - e0) / e0.abs()).abs();
let l_drift = (lz(&sol.r, &sol.v) - l0).abs() / l0.abs();
let pos_err = ((sol.r[0] - 1.0).powi(2) + sol.r[1].powi(2) + sol.r[2].powi(2)).sqrt();
assert!(e_drift < 1e-10, "energy drift = {:.3e}", e_drift);
assert!(l_drift < 1e-10, "Lz drift = {:.3e}", l_drift);
assert!(pos_err < 1e-6, "position drift after 10 orbits = {:.3e}", pos_err);
}
#[test]
fn gj8_dense_interpolation_harmonic() {
let r0 = Vector::<f64, 1>::from_array([1.0]);
let v0 = Vector::<f64, 1>::from_array([0.0]);
let tf = TAU;
let settings = GJSettings {
h: TAU / 100.0,
dense_output: true,
..GJSettings::default()
};
let sol = GaussJackson8::integrate(
0.0, tf, &r0, &v0,
|_t, r, _v| -*r,
&settings,
).unwrap();
let dense = sol.dense.as_ref().expect("dense output should be present");
assert!(dense.t.len() >= 100, "expected at least 100 stored samples");
let test_points = [0.1, 1.0, 2.5, 4.0, 5.5];
for &t in &test_points {
let (r, v) = GaussJackson8::interpolate(t, &sol).unwrap();
let expected_r = t.cos();
let expected_v = -t.sin();
assert!(
(r[0] - expected_r).abs() < 1e-6,
"interp r at t={}: got {}, expected {}, err = {:.3e}",
t, r[0], expected_r, (r[0] - expected_r).abs()
);
assert!(
(v[0] - expected_v).abs() < 1e-6,
"interp v at t={}: got {}, expected {}, err = {:.3e}",
t, v[0], expected_v, (v[0] - expected_v).abs()
);
}
for (i, &ti) in dense.t.iter().enumerate() {
let (r, _) = GaussJackson8::interpolate(ti, &sol).unwrap();
assert!(
(r[0] - dense.r[i][0]).abs() < 1e-12,
"at stored sample index {}, interp disagreed with sample", i
);
}
}
#[test]
fn gj8_dense_interpolation_batch() {
let r0 = Vector::<f64, 1>::from_array([1.0]);
let v0 = Vector::<f64, 1>::from_array([0.0]);
let settings = GJSettings {
h: TAU / 100.0,
dense_output: true,
..GJSettings::default()
};
let sol = GaussJackson8::integrate(
0.0, TAU, &r0, &v0,
|_t, r, _v| -*r,
&settings,
).unwrap();
let times = [0.5, 1.3, 2.7, 4.2, 5.9];
let batch = GaussJackson8::interpolate_batch(×, &sol).unwrap();
for (i, &t) in times.iter().enumerate() {
let single = GaussJackson8::interpolate(t, &sol).unwrap();
assert!((batch[i].0[0] - single.0[0]).abs() < 1e-15);
assert!((batch[i].1[0] - single.1[0]).abs() < 1e-15);
}
}
#[test]
fn gj8_fewer_evals_than_rkv98() {
use numeris::ode::{AdaptiveSettings, RKAdaptive, RKV98};
let tf = 5.0 * TAU;
let r0 = Vector::<f64, 3>::from_array([1.0, 0.0, 0.0]);
let v0 = Vector::<f64, 3>::from_array([0.0, 1.0, 0.0]);
let gj_sol = GaussJackson8::integrate(
0.0, tf, &r0, &v0,
|_t, r, _v| {
let r2 = r[0] * r[0] + r[1] * r[1] + r[2] * r[2];
*r * (-1.0 / (r2 * r2.sqrt()))
},
&GJSettings { h: TAU / 100.0, ..GJSettings::default() },
).unwrap();
let y0_6 = Vector::<f64, 6>::from_array([1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
let rk_sol = RKV98::integrate(
0.0, tf, &y0_6,
|_t, y| {
let r2 = y[0] * y[0] + y[1] * y[1] + y[2] * y[2];
let s = -1.0 / (r2 * r2.sqrt());
Vector::from_array([y[3], y[4], y[5], y[0] * s, y[1] * s, y[2] * s])
},
&AdaptiveSettings::<f64> {
abs_tol: 1e-11,
rel_tol: 1e-11,
..Default::default()
},
).unwrap();
assert!(
gj_sol.evals < rk_sol.evals,
"GJ8 used {} evals, RKV98 used {} — expected GJ8 to be more efficient",
gj_sol.evals, rk_sol.evals
);
}
}