scirs2_integrate/symplectic/
composition.rs

1//! Composition methods for symplectic integration
2//!
3//! This module provides tools for constructing higher-order symplectic
4//! integrators by composing lower-order methods with carefully chosen
5//! coefficients. This technique allows creating methods of arbitrary order.
6//!
7//! The composition follows the principle that if Φₕ is a symplectic integrator
8//! of order p, then the composition:
9//!
10//! Ψₕ = Φ_{c₁h} ∘ Φ_{c₂h} ∘ ... ∘ Φ_{cₘh}
11//!
12//! with appropriate coefficients c₁, c₂, ..., cₘ, can achieve order p+2.
13
14use crate::common::IntegrateFloat;
15use crate::error::IntegrateResult;
16use crate::symplectic::{HamiltonianFn, SymplecticIntegrator};
17use scirs2_core::ndarray::Array1;
18
19/// A symplectic integrator constructed by composition of a base method
20#[derive(Debug, Clone)]
21pub struct CompositionMethod<F: IntegrateFloat, S: SymplecticIntegrator<F>> {
22    /// Base integrator
23    base_method: S,
24    /// Coefficients for the composition
25    coefficients: Vec<F>,
26}
27
28impl<F: IntegrateFloat, S: SymplecticIntegrator<F>> CompositionMethod<F, S> {
29    /// Create a new composition method from a base integrator and coefficients
30    ///
31    /// # Arguments
32    ///
33    /// * `base_method` - The base symplectic integrator
34    /// * `coefficients` - Coefficients for the composition steps
35    ///
36    /// # Returns
37    ///
38    /// A new composition method
39    pub fn new(_basemethod: S, coefficients: Vec<F>) -> Self {
40        CompositionMethod {
41            base_method: _basemethod,
42            coefficients,
43        }
44    }
45
46    /// Create a 4th-order composition from a 2nd-order base method
47    ///
48    /// This uses the standard 3-stage composition with coefficients:
49    /// c₁ = c₃ = 1/(2-2^(1/3)), c₂ = -2^(1/3)/(2-2^(1/3))
50    ///
51    /// # Arguments
52    ///
53    /// * `base_method` - A second-order symplectic integrator
54    ///
55    /// # Returns
56    ///
57    /// A fourth-order symplectic integrator
58    pub fn fourth_order(_basemethod: S) -> Self {
59        // Constants for Yoshida 4th-order composition
60        let two = F::one() + F::one();
61        let two_to_third = two.powf(F::from_f64(1.0 / 3.0).unwrap());
62        let w1 = F::one() / (two - two_to_third);
63        let w0 = -two_to_third / (two - two_to_third);
64
65        // Coefficients: [w1, w0, w1]
66        let coefficients = vec![w1, w0, w1];
67
68        CompositionMethod {
69            base_method: _basemethod,
70            coefficients,
71        }
72    }
73
74    /// Create a 6th-order composition from a 2nd-order base method
75    ///
76    /// This uses the standard 7-stage composition with optimized coefficients
77    ///
78    /// # Arguments
79    ///
80    /// * `base_method` - A second-order symplectic integrator
81    ///
82    /// # Returns
83    ///
84    /// A sixth-order symplectic integrator
85    pub fn sixth_order(_basemethod: S) -> Self {
86        // Coefficients for 6th-order composition _method (Yoshida 1990)
87        let w1 = F::from_f64(0.784513610477560).unwrap();
88        let w2 = F::from_f64(0.235573213359357).unwrap();
89        let w3 = F::from_f64(-1.17767998417887).unwrap();
90        let w4 = F::from_f64(1.31518632068391).unwrap();
91
92        // Create symmetric coefficients
93        let coefficients = vec![w1, w2, w3, w4, w3, w2, w1];
94
95        CompositionMethod {
96            base_method: _basemethod,
97            coefficients,
98        }
99    }
100
101    /// Create an 8th-order composition method from a 2nd-order base method
102    ///
103    /// Uses a 15-stage composition with optimized coefficients
104    ///
105    /// # Arguments
106    ///
107    /// * `base_method` - A second-order symplectic integrator
108    ///
109    /// # Returns
110    ///
111    /// An eighth-order symplectic integrator
112    pub fn eighth_order(_basemethod: S) -> Self {
113        // Coefficients for 8th-order composition (Yoshida 1990)
114        let w = [
115            F::from_f64(0.74167036435061).unwrap(),
116            F::from_f64(-0.40910082580003).unwrap(),
117            F::from_f64(0.19075471029623).unwrap(),
118            F::from_f64(-0.57386247111608).unwrap(),
119            F::from_f64(0.29906418130365).unwrap(),
120            F::from_f64(0.33462491824529).unwrap(),
121            F::from_f64(0.31529309239676).unwrap(),
122            F::from_f64(-0.79688793935291).unwrap(),
123        ];
124
125        // Create symmetric coefficients [w0, w1, ..., w7, w7, ..., w1, w0]
126        let mut coefficients = Vec::with_capacity(15);
127        for &coef in &w {
128            coefficients.push(coef);
129        }
130        coefficients.push(w[7]);
131        for i in (0..7).rev() {
132            coefficients.push(w[i]);
133        }
134
135        CompositionMethod {
136            base_method: _basemethod,
137            coefficients,
138        }
139    }
140}
141
142impl<F: IntegrateFloat, S: SymplecticIntegrator<F>> SymplecticIntegrator<F>
143    for CompositionMethod<F, S>
144{
145    fn step(
146        &self,
147        system: &dyn HamiltonianFn<F>,
148        t: F,
149        q: &Array1<F>,
150        p: &Array1<F>,
151        dt: F,
152    ) -> IntegrateResult<(Array1<F>, Array1<F>)> {
153        // Start with the initial state
154        let mut t_current = t;
155        let mut q_current = q.clone();
156        let mut p_current = p.clone();
157
158        // Apply each sub-step with its coefficient
159        for &coef in &self.coefficients {
160            let dt_sub = dt * coef;
161            let (q_next, p_next) = self
162                .base_method
163                .step(system, t_current, &q_current, &p_current, dt_sub)?;
164
165            q_current = q_next;
166            p_current = p_next;
167            t_current += dt_sub;
168        }
169
170        Ok((q_current, p_current))
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::symplectic::leapfrog::StormerVerlet;
178    use crate::symplectic::potential::SeparableHamiltonian;
179    use scirs2_core::ndarray::array;
180
181    /// Test error convergence rates for different order methods
182    #[test]
183    fn test_composition_convergence() {
184        // Simple harmonic oscillator
185        let system = SeparableHamiltonian::new(
186            |_t, p| -> f64 { 0.5 * p.dot(p) },
187            |_t, q| -> f64 { 0.5 * q.dot(q) },
188        );
189
190        // Initial state
191        let q0 = array![1.0];
192        let p0 = array![0.0];
193
194        // Exact solution at t=1.0: q = cos(1.0), p = -sin(1.0)
195        let q_exact = array![1.0_f64.cos()];
196        let p_exact = array![-1.0_f64.sin()];
197
198        // Create base 2nd-order method
199        let base_method = StormerVerlet::new();
200
201        // Create 4th-order method
202        let fourth_order = CompositionMethod::fourth_order(base_method.clone());
203
204        // Compare error convergence for different step sizes
205        let dt_values = [0.1, 0.05, 0.025];
206
207        let mut base_errors = Vec::new();
208        let mut fourth_errors = Vec::new();
209
210        for &dt in &dt_values {
211            // Integrate to t=1.0 with base method
212            let base_result = base_method
213                .integrate(&system, 0.0, 1.0, dt, q0.clone(), p0.clone())
214                .unwrap();
215
216            // Compute error
217            let idx = base_result.q.len() - 1;
218            let q_base = &base_result.q[idx];
219            let p_base = &base_result.p[idx];
220            let base_error =
221                ((q_base[0] - q_exact[0]).powi(2) + (p_base[0] - p_exact[0]).powi(2)).sqrt();
222            base_errors.push(base_error);
223
224            // Integrate with 4th-order method
225            let fourth_result = fourth_order
226                .integrate(&system, 0.0, 1.0, dt, q0.clone(), p0.clone())
227                .unwrap();
228
229            // Compute error
230            let idx = fourth_result.q.len() - 1;
231            let q_fourth = &fourth_result.q[idx];
232            let p_fourth = &fourth_result.p[idx];
233            let fourth_error =
234                ((q_fourth[0] - q_exact[0]).powi(2) + (p_fourth[0] - p_exact[0]).powi(2)).sqrt();
235            fourth_errors.push(fourth_error);
236        }
237
238        // Check convergence rate: when dt is halved, error should decrease by 2^p for p-th order method
239        // For 2nd-order method, error ratio should be ~4
240        // For 4th-order method, error ratio should be ~16
241        for i in 0..dt_values.len() - 1 {
242            let base_ratio = base_errors[i] / base_errors[i + 1];
243            let fourth_ratio = fourth_errors[i] / fourth_errors[i + 1];
244
245            // Allow some tolerance in ratio checks
246            assert!(
247                base_ratio > 3.5 && base_ratio < 4.5,
248                "Base method convergence rate incorrect: {base_ratio}"
249            );
250            assert!(
251                fourth_ratio > 12.0 && fourth_ratio < 20.0,
252                "4th-order method convergence rate incorrect: {fourth_ratio}"
253            );
254        }
255
256        // Check that 4th-order is more accurate than base method for same step size
257        for i in 0..dt_values.len() {
258            assert!(
259                fourth_errors[i] < base_errors[i],
260                "4th-order method should be more accurate than base method"
261            );
262        }
263    }
264
265    /// Test energy conservation over long time integration
266    #[test]
267    fn test_long_time_energy_conservation() {
268        // Kepler problem (2D planetary orbit)
269        let kepler = SeparableHamiltonian::new(
270            |_t, p| -> f64 { 0.5 * p.dot(p) },
271            |_t, q| -> f64 {
272                let r = (q[0] * q[0] + q[1] * q[1]).sqrt();
273                if r < 1e-10 {
274                    0.0
275                } else {
276                    -1.0 / r
277                }
278            },
279        );
280
281        // Initial condition for elliptical orbit
282        let q0 = array![1.0, 0.0];
283        let p0 = array![0.0, 1.2]; // Slightly faster than circular orbit
284
285        // Integration parameters
286        let t0 = 0.0;
287        let tf = 100.0; // Long integration time
288        let dt = 0.1;
289
290        // Base 2nd-order method
291        let base_method = StormerVerlet::new();
292
293        // 4th-order composition method
294        let fourth_order = CompositionMethod::fourth_order(base_method.clone());
295
296        // Integrate with both methods
297        let base_result = base_method
298            .integrate(&kepler, t0, tf, dt, q0.clone(), p0.clone())
299            .unwrap();
300        let fourth_result = fourth_order
301            .integrate(&kepler, t0, tf, dt, q0.clone(), p0.clone())
302            .unwrap();
303
304        // Check energy conservation - 4th-order should be better
305        if let (Some(base_error), Some(fourth_error)) = (
306            base_result.energy_relative_error,
307            fourth_result.energy_relative_error,
308        ) {
309            assert!(
310                fourth_error < base_error,
311                "4th-order method should have better energy conservation. Base: {base_error}, 4th-order: {fourth_error}"
312            );
313        }
314    }
315}