use dace_rs::{Da, NormType};
fn main() {
harmonic_oscillator();
let (y0, y_final) = kepler();
let mut rng = Lcg(0x853c49e6748fea9b);
validity_domain(&y0, &y_final, &mut rng);
}
fn rk4_step_da(f: &dyn Fn(&[Da]) -> Vec<Da>, y: &[Da], h: f64) -> Vec<Da> {
let axpy = |y: &[Da], k: &[Da], a: f64| -> Vec<Da> {
y.iter()
.zip(k)
.map(|(yi, ki)| yi.clone() + a * ki.clone())
.collect()
};
let k1 = f(y);
let k2 = f(&axpy(y, &k1, 0.5 * h));
let k3 = f(&axpy(y, &k2, 0.5 * h));
let k4 = f(&axpy(y, &k3, h));
y.iter()
.enumerate()
.map(|(i, yi)| {
yi.clone()
+ (h / 6.0)
* (k1[i].clone() + 2.0 * k2[i].clone() + 2.0 * k3[i].clone() + k4[i].clone())
})
.collect()
}
fn rk4_step_f64(f: &dyn Fn(&[f64; 4]) -> [f64; 4], y: &[f64; 4], h: f64) -> [f64; 4] {
let axpy = |y: &[f64; 4], k: [f64; 4], a: f64| -> [f64; 4] {
let mut out = [0.0; 4];
for i in 0..4 {
out[i] = y[i] + a * k[i];
}
out
};
let k1 = f(y);
let k2 = f(&axpy(y, k1, 0.5 * h));
let k3 = f(&axpy(y, k2, 0.5 * h));
let k4 = f(&axpy(y, k3, h));
let mut out = [0.0; 4];
for i in 0..4 {
out[i] = y[i] + (h / 6.0) * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
}
out
}
struct Lcg(u64);
impl Lcg {
fn next_unit(&mut self) -> f64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let u = (self.0 >> 11) as f64 / (1u64 << 53) as f64;
2.0 * u - 1.0
}
}
fn harmonic_oscillator() {
dace_rs::init(20, 2).expect("init(20, 2)");
println!("== harmonic oscillator: x'' = -x, order 20, 2 vars ==");
let rhs = |y: &[Da]| vec![y[1].clone(), -y[0].clone()];
let y0 = vec![1.0 + Da::variable(1), Da::variable(2)];
let t = 1.7_f64;
let n = 4096;
let h = t / n as f64;
let mut y = y0;
for _ in 0..n {
y = rk4_step_da(&rhs, &y, h);
}
let (x, v) = (&y[0], &y[1]);
let (c, s) = (t.cos(), t.sin());
let lin_x = x.linear();
let lin_v = v.linear();
let rows = [
("x(T) const", x.cons(), c),
("x(T) d/dx0", lin_x[0], c),
("x(T) d/dv0", lin_x[1], s),
("v(T) const", v.cons(), -s),
("v(T) d/dx0", lin_v[0], -s),
("v(T) d/dv0", lin_v[1], c),
];
println!("\nT = {t}, h = {h:.6e}, n = {n} RK4 steps");
println!("coefficient DA value analytic deviation");
let mut max_dev = 0.0_f64;
for (label, da_val, analytic) in rows {
let dev = (da_val - analytic).abs();
max_dev = max_dev.max(dev);
println!("{label:<12} {da_val:+19.15} {analytic:+19.15} {dev:.3e}");
assert!(dev < 1e-12, "{label}: deviation {dev:.3e} >= 1e-12");
}
println!("max deviation from the analytic flow: {max_dev:.3e}");
for (label, q) in [
("x(T) dx^2", x.get_coefficient(&[2, 0])),
("x(T) dx dv", x.get_coefficient(&[1, 1])),
("x(T) dv^2", x.get_coefficient(&[0, 2])),
("v(T) dx^2", v.get_coefficient(&[2, 0])),
("v(T) dx dv", v.get_coefficient(&[1, 1])),
("v(T) dv^2", v.get_coefficient(&[0, 2])),
] {
assert_eq!(q, 0.0, "{label} must be exactly 0.0, got {q}");
}
println!("all second-order coefficients are exactly 0.0 (linear flow)");
println!("\nx(T) polynomial:\n{x}");
}
fn energy_da(y: &[Da]) -> Da {
0.5 * (y[2].sqr() + y[3].sqr()) - (y[0].sqr() + y[1].sqr()).sqrt().minv()
}
fn kepler() -> (Vec<Da>, Vec<Da>) {
dace_rs::init(8, 4).expect("init(8, 4)");
println!("\n== planar two-body Kepler orbit: order 8, 4 vars ==");
let rhs = |y: &[Da]| -> Vec<Da> {
let r2 = y[0].sqr() + y[1].sqr();
let r3inv = (r2.clone() * r2.sqrt()).minv();
vec![
y[2].clone(),
y[3].clone(),
-(y[0].clone() * r3inv.clone()),
-(y[1].clone() * r3inv),
]
};
let base = [0.8, 0.0, 0.0, 1.5_f64.sqrt()];
let y0: Vec<Da> = (0..4)
.map(|i| Da::constant(base[i]) + Da::variable(i as u32 + 1))
.collect();
let e0 = energy_da(&y0).cons();
println!("E(y0) = {e0:+.15} (expected -0.5)");
let t = std::f64::consts::TAU;
let mut y_final = Vec::new();
let mut metrics = Vec::new();
let mut residuals = Vec::new();
for &n in &[128_usize, 256] {
let h = t / n as f64;
let mut y = y0.clone();
for _ in 0..n {
y = rk4_step_da(&rhs, &y, h);
}
let p = energy_da(&y) - energy_da(&y0);
let onorm = p.order_norm(0, NormType::Infinity);
let m = onorm[1..=4].iter().copied().fold(0.0_f64, f64::max);
println!("\nenergy residual P = E(y_final) - E(y0), n = {n} steps (h = {h:.6e}):");
println!(" P.const = {:+.3e}", p.cons());
println!(" order-norm rows (infinity norm per order):");
for (order, norm) in onorm.iter().enumerate().skip(1) {
println!(" order {order}: {norm:.3e}");
}
metrics.push((n, m));
residuals.push(p.cons());
y_final = y;
}
let (m1, m2) = (metrics[0].1, metrics[1].1);
let ratio = m1 / m2;
println!("\nmetric m = max order-norm over orders 1..=4 of P:");
println!(" m(h) = {:.3e}", m1);
println!(" m(h/2) = {:.3e}", m2);
println!(" m(h)/m(h/2) = {ratio:.3} (RK4 global error O(h^4) -> expected ~16)");
assert!(
(10.0..=26.0).contains(&ratio),
"h^4 scaling broken: m(h)/m(h/2) = {ratio}"
);
assert!(
residuals[1].abs() <= 1e-5,
"constant-part energy drift too large: {}",
residuals[1]
);
let ox = y_final[0].order_norm(0, NormType::Infinity);
let ovx = y_final[2].order_norm(0, NormType::Infinity);
println!("\norder norms of the 256-step terminal flow (they grow: small convergence radius):");
println!(" order x(T) vx(T)");
for order in 0..=8 {
println!(" {order:>5} {:>15.3e} {:>15.3e}", ox[order], ovx[order]);
}
let rho = y_final[0].conv_radius(1e-10, NormType::Infinity);
println!(
"\nconv_radius(x(T), eps = 1e-10) = {rho:.6} \
(estimated offset radius at which the next-order remainder of the\n\
\x20 flow polynomial drops below eps; informational only, the\n\
\x20 exponential fit behind it may warn)"
);
(y0, y_final)
}
fn validity_domain(y0: &[Da], y_final: &[Da], rng: &mut Lcg) {
println!("\n== validity domain of the polynomial flow ==");
let rhs = |y: &[f64; 4]| -> [f64; 4] {
let r2 = y[0] * y[0] + y[1] * y[1];
let r3 = r2 * r2.sqrt();
[y[2], y[3], -y[0] / r3, -y[1] / r3]
};
let base = [y0[0].cons(), y0[1].cons(), y0[2].cons(), y0[3].cons()];
let n = 256_usize;
let h = std::f64::consts::TAU / n as f64;
println!("evaluating the flow polynomial vs re-integration, 16 samples per radius:");
println!(" radius s max deviation");
let mut first = 0.0_f64;
let mut last = 0.0_f64;
for &s in &[0.01, 0.05, 0.1, 0.2, 0.4] {
let mut max_dev = 0.0_f64;
for _ in 0..16 {
let delta = [
rng.next_unit(),
rng.next_unit(),
rng.next_unit(),
rng.next_unit(),
];
let delta = delta.map(|u| s * u);
let poly = [
y_final[0].eval(&delta),
y_final[1].eval(&delta),
y_final[2].eval(&delta),
y_final[3].eval(&delta),
];
let mut y = base;
for i in 0..4 {
y[i] += delta[i];
}
for _ in 0..n {
y = rk4_step_f64(&rhs, &y, h);
}
let dev: f64 = poly
.iter()
.zip(&y)
.map(|(a, b)| (a - b) * (a - b))
.sum::<f64>()
.sqrt();
max_dev = max_dev.max(dev);
}
println!(" {s:>8.2} {max_dev:.3e}");
first = if s == 0.01 { max_dev } else { first };
last = if s == 0.4 { max_dev } else { last };
}
assert!(
last > first,
"deviation must grow with the box radius: dev(0.01) = {first:.3e}, dev(0.4) = {last:.3e}"
);
println!("the deviation climbs steeply with s: the polynomial flow is an");
println!("order-8 truncation of a Taylor series whose radius rho lies well");
println!("inside the real collision distance, so it is valid only for s <~ 1e-2.");
}