scirs2_integrate/symplectic/
composition.rs1use crate::common::IntegrateFloat;
15use crate::error::IntegrateResult;
16use crate::symplectic::{HamiltonianFn, SymplecticIntegrator};
17use scirs2_core::ndarray::Array1;
18
19#[derive(Debug, Clone)]
21pub struct CompositionMethod<F: IntegrateFloat, S: SymplecticIntegrator<F>> {
22 base_method: S,
24 coefficients: Vec<F>,
26}
27
28impl<F: IntegrateFloat, S: SymplecticIntegrator<F>> CompositionMethod<F, S> {
29 pub fn new(_basemethod: S, coefficients: Vec<F>) -> Self {
40 CompositionMethod {
41 base_method: _basemethod,
42 coefficients,
43 }
44 }
45
46 pub fn fourth_order(_basemethod: S) -> Self {
59 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 let coefficients = vec![w1, w0, w1];
67
68 CompositionMethod {
69 base_method: _basemethod,
70 coefficients,
71 }
72 }
73
74 pub fn sixth_order(_basemethod: S) -> Self {
86 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 let coefficients = vec![w1, w2, w3, w4, w3, w2, w1];
94
95 CompositionMethod {
96 base_method: _basemethod,
97 coefficients,
98 }
99 }
100
101 pub fn eighth_order(_basemethod: S) -> Self {
113 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 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 let mut t_current = t;
155 let mut q_current = q.clone();
156 let mut p_current = p.clone();
157
158 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]
183 fn test_composition_convergence() {
184 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 let q0 = array![1.0];
192 let p0 = array![0.0];
193
194 let q_exact = array![1.0_f64.cos()];
196 let p_exact = array![-1.0_f64.sin()];
197
198 let base_method = StormerVerlet::new();
200
201 let fourth_order = CompositionMethod::fourth_order(base_method.clone());
203
204 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 let base_result = base_method
213 .integrate(&system, 0.0, 1.0, dt, q0.clone(), p0.clone())
214 .unwrap();
215
216 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 let fourth_result = fourth_order
226 .integrate(&system, 0.0, 1.0, dt, q0.clone(), p0.clone())
227 .unwrap();
228
229 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 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 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 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]
267 fn test_long_time_energy_conservation() {
268 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 let q0 = array![1.0, 0.0];
283 let p0 = array![0.0, 1.2]; let t0 = 0.0;
287 let tf = 100.0; let dt = 0.1;
289
290 let base_method = StormerVerlet::new();
292
293 let fourth_order = CompositionMethod::fourth_order(base_method.clone());
295
296 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 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}