use crate::common::IntegrateFloat;
use crate::error::IntegrateResult;
use crate::symplectic::{HamiltonianFn, SymplecticIntegrator};
use scirs2_core::ndarray::Array1;
#[derive(Debug, Clone)]
pub struct CompositionMethod<F: IntegrateFloat, S: SymplecticIntegrator<F>> {
base_method: S,
coefficients: Vec<F>,
}
impl<F: IntegrateFloat, S: SymplecticIntegrator<F>> CompositionMethod<F, S> {
pub fn new(_basemethod: S, coefficients: Vec<F>) -> Self {
CompositionMethod {
base_method: _basemethod,
coefficients,
}
}
pub fn fourth_order(_basemethod: S) -> Self {
let two = F::one() + F::one();
let two_to_third = two.powf(F::from_f64(1.0 / 3.0).expect("Operation failed"));
let w1 = F::one() / (two - two_to_third);
let w0 = -two_to_third / (two - two_to_third);
let coefficients = vec![w1, w0, w1];
CompositionMethod {
base_method: _basemethod,
coefficients,
}
}
pub fn sixth_order(_basemethod: S) -> Self {
let w1 = F::from_f64(0.784513610477560).expect("Operation failed");
let w2 = F::from_f64(0.235573213359357).expect("Operation failed");
let w3 = F::from_f64(-1.17767998417887).expect("Operation failed");
let w4 = F::from_f64(1.31518632068391).expect("Operation failed");
let coefficients = vec![w1, w2, w3, w4, w3, w2, w1];
CompositionMethod {
base_method: _basemethod,
coefficients,
}
}
pub fn eighth_order(_basemethod: S) -> Self {
let w = [
F::from_f64(0.74167036435061).expect("Operation failed"),
F::from_f64(-0.40910082580003).expect("Operation failed"),
F::from_f64(0.19075471029623).expect("Operation failed"),
F::from_f64(-0.57386247111608).expect("Operation failed"),
F::from_f64(0.29906418130365).expect("Operation failed"),
F::from_f64(0.33462491824529).expect("Operation failed"),
F::from_f64(0.31529309239676).expect("Operation failed"),
F::from_f64(-0.79688793935291).expect("Operation failed"),
];
let mut coefficients = Vec::with_capacity(15);
for &coef in &w {
coefficients.push(coef);
}
coefficients.push(w[7]);
for i in (0..7).rev() {
coefficients.push(w[i]);
}
CompositionMethod {
base_method: _basemethod,
coefficients,
}
}
}
impl<F: IntegrateFloat, S: SymplecticIntegrator<F>> SymplecticIntegrator<F>
for CompositionMethod<F, S>
{
fn step(
&self,
system: &dyn HamiltonianFn<F>,
t: F,
q: &Array1<F>,
p: &Array1<F>,
dt: F,
) -> IntegrateResult<(Array1<F>, Array1<F>)> {
let mut t_current = t;
let mut q_current = q.clone();
let mut p_current = p.clone();
for &coef in &self.coefficients {
let dt_sub = dt * coef;
let (q_next, p_next) = self
.base_method
.step(system, t_current, &q_current, &p_current, dt_sub)?;
q_current = q_next;
p_current = p_next;
t_current += dt_sub;
}
Ok((q_current, p_current))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symplectic::leapfrog::StormerVerlet;
use crate::symplectic::potential::SeparableHamiltonian;
use scirs2_core::ndarray::array;
#[test]
fn test_composition_convergence() {
let system = SeparableHamiltonian::new(
|_t, p| -> f64 { 0.5 * p.dot(p) },
|_t, q| -> f64 { 0.5 * q.dot(q) },
);
let q0 = array![1.0];
let p0 = array![0.0];
let q_exact = array![1.0_f64.cos()];
let p_exact = array![-1.0_f64.sin()];
let base_method = StormerVerlet::new();
let fourth_order = CompositionMethod::fourth_order(base_method.clone());
let dt_values = [0.1, 0.05, 0.025];
let mut base_errors = Vec::new();
let mut fourth_errors = Vec::new();
for &dt in &dt_values {
let base_result = base_method
.integrate(&system, 0.0, 1.0, dt, q0.clone(), p0.clone())
.expect("Test: integration failed");
let idx = base_result.q.len() - 1;
let q_base = &base_result.q[idx];
let p_base = &base_result.p[idx];
let base_error =
((q_base[0] - q_exact[0]).powi(2) + (p_base[0] - p_exact[0]).powi(2)).sqrt();
base_errors.push(base_error);
let fourth_result = fourth_order
.integrate(&system, 0.0, 1.0, dt, q0.clone(), p0.clone())
.expect("Test: integration failed");
let idx = fourth_result.q.len() - 1;
let q_fourth = &fourth_result.q[idx];
let p_fourth = &fourth_result.p[idx];
let fourth_error =
((q_fourth[0] - q_exact[0]).powi(2) + (p_fourth[0] - p_exact[0]).powi(2)).sqrt();
fourth_errors.push(fourth_error);
}
for i in 0..dt_values.len() - 1 {
let base_ratio = base_errors[i] / base_errors[i + 1];
let fourth_ratio = fourth_errors[i] / fourth_errors[i + 1];
assert!(
base_ratio > 3.5 && base_ratio < 4.5,
"Base method convergence rate incorrect: {base_ratio}"
);
assert!(
fourth_ratio > 12.0 && fourth_ratio < 20.0,
"4th-order method convergence rate incorrect: {fourth_ratio}"
);
}
for i in 0..dt_values.len() {
assert!(
fourth_errors[i] < base_errors[i],
"4th-order method should be more accurate than base method"
);
}
}
#[test]
fn test_long_time_energy_conservation() {
let kepler = SeparableHamiltonian::new(
|_t, p| -> f64 { 0.5 * p.dot(p) },
|_t, q| -> f64 {
let r = (q[0] * q[0] + q[1] * q[1]).sqrt();
if r < 1e-10 {
0.0
} else {
-1.0 / r
}
},
);
let q0 = array![1.0, 0.0];
let p0 = array![0.0, 1.2];
let t0 = 0.0;
let tf = 100.0; let dt = 0.1;
let base_method = StormerVerlet::new();
let fourth_order = CompositionMethod::fourth_order(base_method.clone());
let base_result = base_method
.integrate(&kepler, t0, tf, dt, q0.clone(), p0.clone())
.expect("Test: integration failed");
let fourth_result = fourth_order
.integrate(&kepler, t0, tf, dt, q0.clone(), p0.clone())
.expect("Test: integration failed");
if let (Some(base_error), Some(fourth_error)) = (
base_result.energy_relative_error,
fourth_result.energy_relative_error,
) {
assert!(
fourth_error < base_error,
"4th-order method should have better energy conservation. Base: {base_error}, 4th-order: {fourth_error}"
);
}
}
}