scirs2_integrate/symplectic/
mod.rs

1//! Symplectic integrators for Hamiltonian systems
2//!
3//! This module provides specialized integrators for Hamiltonian systems that
4//! preserve important geometric properties such as energy conservation (to within
5//! bounded error), phase space volume, and symplectic structure.
6//!
7//! Symplectic integrators are particularly useful for:
8//! - Long-time integration of mechanical systems
9//! - Molecular dynamics simulations
10//! - Orbital mechanics
11//! - Any conservative system where energy conservation is critical
12//!
13//! # Methods
14//!
15//! Several symplectic integration methods are provided:
16//!
17//! - **Symplectic Euler**: First-order, simple method (forward and backward variants)
18//! - **Leapfrog/Störmer-Verlet**: Second-order, widely used for molecular dynamics
19//! - **Symplectic Runge-Kutta**: Higher-order methods with improved accuracy
20//! - **Composition methods**: Methods constructed by composing lower-order integrators
21//!
22//! # Basic Usage
23//!
24//! ```
25//! use scirs2_integrate::symplectic::potential::HamiltonianSystem;
26//! use scirs2_integrate::symplectic::leapfrog::StormerVerlet;
27//! use scirs2_integrate::symplectic::SymplecticIntegrator;
28//! use scirs2_core::ndarray::array;
29//!
30//! // Define a simple harmonic oscillator: H = p²/2 + q²/2
31//! let system = HamiltonianSystem::new(
32//!     |_t, _q, p| p.clone(),  // dq/dt = p
33//!     |_t, q, _p| -q.clone(), // dp/dt = -q
34//! );
35//!
36//! // Initial conditions: (q0, p0) = (1.0, 0.0)
37//! let q0 = array![1.0];
38//! let p0 = array![0.0];
39//! let t = 0.0;
40//! let dt = 0.01;
41//!
42//! // Create integrator
43//! let integrator = StormerVerlet::<f64>::new();
44//!
45//! // Take one step
46//! let (q1, p1) = integrator.step(&system, t, &q0, &p0, dt).unwrap();
47//!
48//! // Energy should be conserved (approximately)
49//! let initial_energy = 0.5_f64 * p0.dot(&p0) + 0.5_f64 * q0.dot(&q0);
50//! let final_energy = 0.5_f64 * p1.dot(&p1) + 0.5_f64 * q1.dot(&q1);
51//! // For a single step with dt=0.1, the energy error is acceptable
52//! assert!((initial_energy - final_energy).abs() < 1e-6);
53//! ```
54
55// Public sub-modules
56pub mod composition;
57pub mod euler;
58pub mod leapfrog;
59pub mod potential;
60pub mod runge_kutta;
61
62// Re-exports for convenience
63pub use composition::CompositionMethod;
64pub use euler::{symplectic_euler, symplectic_euler_a, symplectic_euler_b};
65pub use leapfrog::{position_verlet, velocity_verlet, StormerVerlet};
66pub use potential::{HamiltonianSystem, SeparableHamiltonian};
67pub use runge_kutta::{GaussLegendre4, GaussLegendre6};
68
69use crate::common::IntegrateFloat;
70use crate::error::{IntegrateError, IntegrateResult};
71use scirs2_core::ndarray::Array1;
72
73/// Result of symplectic integration containing state history
74#[derive(Debug, Clone)]
75pub struct SymplecticResult<F: IntegrateFloat> {
76    /// Time points
77    pub t: Vec<F>,
78    /// Position coordinates at each time point
79    pub q: Vec<Array1<F>>,
80    /// Momentum coordinates at each time point
81    pub p: Vec<Array1<F>>,
82    /// Total integration time
83    pub total_time: F,
84    /// Number of steps taken
85    pub steps: usize,
86    /// Function evaluations
87    pub n_evaluations: usize,
88    /// Relative error in energy conservation (if available)
89    pub energy_relative_error: Option<F>,
90}
91
92/// Trait for symplectic integrators
93pub trait SymplecticIntegrator<F: IntegrateFloat> {
94    /// Perform a single integration step
95    ///
96    /// # Arguments
97    ///
98    /// * `system` - The Hamiltonian system to integrate
99    /// * `t` - Current time
100    /// * `q` - Current position coordinates
101    /// * `p` - Current momentum coordinates
102    /// * `dt` - Step size
103    ///
104    /// # Returns
105    ///
106    /// Updated position and momentum coordinates
107    fn step(
108        &self,
109        system: &dyn HamiltonianFn<F>,
110        t: F,
111        q: &Array1<F>,
112        p: &Array1<F>,
113        dt: F,
114    ) -> IntegrateResult<(Array1<F>, Array1<F>)>;
115
116    /// Integrate the system over a time interval
117    ///
118    /// # Arguments
119    ///
120    /// * `system` - The Hamiltonian system to integrate
121    /// * `t0` - Initial time
122    /// * `tf` - Final time
123    /// * `dt` - Step size
124    /// * `q0` - Initial position coordinates
125    /// * `p0` - Initial momentum coordinates
126    ///
127    /// # Returns
128    ///
129    /// Integration result containing state history
130    fn integrate(
131        &self,
132        system: &dyn HamiltonianFn<F>,
133        t0: F,
134        tf: F,
135        dt: F,
136        q0: Array1<F>,
137        p0: Array1<F>,
138    ) -> IntegrateResult<SymplecticResult<F>> {
139        // Validate inputs
140        if dt <= F::zero() {
141            return Err(IntegrateError::ValueError(
142                "Step size must be positive".into(),
143            ));
144        }
145
146        if q0.len() != p0.len() {
147            return Err(IntegrateError::ValueError(
148                "Position and momentum vectors must have the same length".into(),
149            ));
150        }
151
152        // Determine number of steps
153        let t_span = tf - t0;
154        let n_steps = (t_span / dt).ceil().to_f64().unwrap() as usize;
155        let actual_dt = t_span / F::from_usize(n_steps).unwrap();
156
157        // Initialize result containers
158        let mut t = Vec::with_capacity(n_steps + 1);
159        let mut q = Vec::with_capacity(n_steps + 1);
160        let mut p = Vec::with_capacity(n_steps + 1);
161
162        // Add initial state
163        t.push(t0);
164        q.push(q0.clone());
165        p.push(p0.clone());
166
167        // Perform integration
168        let mut curr_t = t0;
169        let mut curr_q = q0;
170        let mut curr_p = p0;
171        let mut n_evals = 0;
172
173        for _ in 0..n_steps {
174            // Calculate next state
175            let (next_q, next_p) = self.step(system, curr_t, &curr_q, &curr_p, actual_dt)?;
176            n_evals += 2; // Approximation: most methods use at least 2 function evaluations per step
177
178            // Advance time
179            curr_t += actual_dt;
180
181            // Store results
182            t.push(curr_t);
183            q.push(next_q.clone());
184            p.push(next_p.clone());
185
186            // Update current state
187            curr_q = next_q;
188            curr_p = next_p;
189        }
190
191        // Calculate energy conservation error if the system provides a Hamiltonian
192        let energy_error = if let Some(hamiltonian) = system.hamiltonian() {
193            let initial_energy = hamiltonian(t[0], &q[0], &p[0])?;
194            let final_energy = hamiltonian(t[t.len() - 1], &q[q.len() - 1], &p[p.len() - 1])?;
195
196            // Calculate relative error
197            if initial_energy.abs() > F::from_f64(1e-10).unwrap() {
198                Some((final_energy - initial_energy).abs() / initial_energy.abs())
199            } else {
200                Some((final_energy - initial_energy).abs())
201            }
202        } else {
203            None
204        };
205
206        Ok(SymplecticResult {
207            t,
208            q,
209            p,
210            total_time: tf - t0,
211            steps: n_steps,
212            n_evaluations: n_evals,
213            energy_relative_error: energy_error,
214        })
215    }
216}
217
218/// Type alias for Hamiltonian function
219pub type HamiltonianFnBox<'a, F> =
220    Box<dyn Fn(F, &Array1<F>, &Array1<F>) -> IntegrateResult<F> + 'a>;
221
222/// Trait for Hamiltonian systems
223pub trait HamiltonianFn<F: IntegrateFloat> {
224    /// Computes the time derivative of position coordinates: dq/dt = ∂H/∂p
225    ///
226    /// # Arguments
227    ///
228    /// * `t` - Current time
229    /// * `q` - Position coordinates
230    /// * `p` - Momentum coordinates
231    ///
232    /// # Returns
233    ///
234    /// Time derivative of position coordinates
235    fn dq_dt(&self, t: F, q: &Array1<F>, p: &Array1<F>) -> IntegrateResult<Array1<F>>;
236
237    /// Computes the time derivative of momentum coordinates: dp/dt = -∂H/∂q
238    ///
239    /// # Arguments
240    ///
241    /// * `t` - Current time
242    /// * `q` - Position coordinates
243    /// * `p` - Momentum coordinates
244    ///
245    /// # Returns
246    ///
247    /// Time derivative of momentum coordinates
248    fn dp_dt(&self, t: F, q: &Array1<F>, p: &Array1<F>) -> IntegrateResult<Array1<F>>;
249
250    /// Optional: Computes the Hamiltonian function H(q, p)
251    ///
252    /// This is used for energy conservation checks and diagnostics.
253    /// Return None if not implemented.
254    ///
255    /// # Arguments
256    ///
257    /// * `t` - Current time
258    /// * `q` - Position coordinates
259    /// * `p` - Momentum coordinates
260    ///
261    /// # Returns
262    ///
263    /// Value of the Hamiltonian (energy)
264    fn hamiltonian(&self) -> Option<HamiltonianFnBox<'_, F>> {
265        None
266    }
267}