Skip to main content

scirs2_integrate/symplectic/
potential.rs

1//! Specialized symplectic integrators for separable Hamiltonian systems
2//!
3//! This module provides implementations for Hamiltonian systems with
4//! a separable structure H(q, p) = T(p) + V(q), which is common in
5//! many physical systems. The separation into kinetic energy T(p) and
6//! potential energy V(q) allows for specialized implementations.
7//!
8//! It also provides a generic Hamiltonian system implementation for
9//! non-separable Hamiltonians.
10
11use crate::common::IntegrateFloat;
12use crate::error::IntegrateResult;
13use crate::symplectic::HamiltonianFn;
14use scirs2_core::ndarray::Array1;
15use std::f64::consts::PI;
16use std::fmt::{Debug, Formatter};
17
18/// Type alias for energy function
19type EnergyFunction<F> = Box<dyn Fn(F, &Array1<F>) -> F + Send + Sync>;
20
21/// Type alias for gradient function  
22type GradientFunction<F> = Box<dyn Fn(F, &Array1<F>) -> Array1<F> + Send + Sync>;
23
24/// Type alias for Hamiltonian equations of motion
25type EquationOfMotion<F> = Box<dyn Fn(F, &Array1<F>, &Array1<F>) -> Array1<F> + Send + Sync>;
26
27/// Type alias for Hamiltonian function
28type HamiltonianFunction<F> = Box<dyn Fn(F, &Array1<F>, &Array1<F>) -> F + Send + Sync>;
29
30/// A separable Hamiltonian system with H(q, p) = T(p) + V(q)
31///
32/// This represents systems where the Hamiltonian can be split into
33/// kinetic energy T(p) depending only on momenta and potential
34/// energy V(q) depending only on positions.
35///
36/// Examples include:
37/// - Simple harmonic oscillator: H = p²/2 + q²/2
38/// - Pendulum: H = p²/2 - cos(q)
39/// - Kepler problem: H = |p|²/2 - 1/|q|
40pub struct SeparableHamiltonian<F: IntegrateFloat> {
41    /// Kinetic energy function T(p)
42    kinetic_energy: EnergyFunction<F>,
43
44    /// Potential energy function V(q)
45    potential_energy: EnergyFunction<F>,
46
47    /// Gradient of potential energy ∇V(q)
48    potential_gradient: Option<GradientFunction<F>>,
49
50    /// Gradient of kinetic energy ∇T(p)
51    kinetic_gradient: Option<GradientFunction<F>>,
52}
53
54impl<F: IntegrateFloat> Debug for SeparableHamiltonian<F> {
55    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct("SeparableHamiltonian")
57            .field("kinetic_energy", &"<dyn Fn>")
58            .field("potential_energy", &"<dyn Fn>")
59            .field("potential_gradient", &"<dyn Fn>")
60            .field("kinetic_gradient", &"<dyn Fn>")
61            .finish()
62    }
63}
64
65impl<F: IntegrateFloat> SeparableHamiltonian<F> {
66    /// Create a new separable Hamiltonian system
67    ///
68    /// # Arguments
69    ///
70    /// * `kinetic_energy` - Function computing T(p)
71    /// * `potential_energy` - Function computing V(q)
72    ///
73    /// # Returns
74    ///
75    /// A new separable Hamiltonian system
76    pub fn new<K, V>(_kinetic_energy: K, potentialenergy: V) -> Self
77    where
78        K: Fn(F, &Array1<F>) -> F + 'static + Send + Sync,
79        V: Fn(F, &Array1<F>) -> F + 'static + Send + Sync,
80    {
81        SeparableHamiltonian {
82            kinetic_energy: Box::new(_kinetic_energy),
83            potential_energy: Box::new(potentialenergy),
84            potential_gradient: None,
85            kinetic_gradient: None,
86        }
87    }
88
89    /// Add analytical gradient functions for improved performance
90    ///
91    /// # Arguments
92    ///
93    /// * `kinetic_gradient` - Function computing ∇T(p)
94    /// * `potential_gradient` - Function computing ∇V(q)
95    ///
96    /// # Returns
97    ///
98    /// Self with gradients configured
99    pub fn with_gradients<KG, VG>(mut self, kinetic_gradient: KG, potentialgradient: VG) -> Self
100    where
101        KG: Fn(F, &Array1<F>) -> Array1<F> + 'static + Send + Sync,
102        VG: Fn(F, &Array1<F>) -> Array1<F> + 'static + Send + Sync,
103    {
104        self.kinetic_gradient = Some(Box::new(kinetic_gradient));
105        self.potential_gradient = Some(Box::new(potentialgradient));
106        self
107    }
108
109    /// Create a simple harmonic oscillator system
110    ///
111    /// H(q, p) = p²/2 + q²/2
112    ///
113    /// # Returns
114    ///
115    /// A harmonic oscillator Hamiltonian
116    pub fn harmonic_oscillator() -> Self {
117        let kinetic = |_t: F, p: &Array1<F>| -> F {
118            p.iter().map(|&pi| pi * pi).sum::<F>() * F::from_f64(0.5).expect("Operation failed")
119        };
120
121        let potential = |_t: F, q: &Array1<F>| -> F {
122            q.iter().map(|&qi| qi * qi).sum::<F>() * F::from_f64(0.5).expect("Operation failed")
123        };
124
125        let kinetic_grad = |_t: F, p: &Array1<F>| -> Array1<F> { p.to_owned() };
126
127        let potential_grad = |_t: F, q: &Array1<F>| -> Array1<F> { q.to_owned() };
128
129        SeparableHamiltonian::new(kinetic, potential).with_gradients(kinetic_grad, potential_grad)
130    }
131
132    /// Create a pendulum system
133    ///
134    /// H(q, p) = p²/2 - cos(q)
135    ///
136    /// # Returns
137    ///
138    /// A pendulum Hamiltonian
139    pub fn pendulum() -> Self {
140        let kinetic = |_t: F, p: &Array1<F>| -> F {
141            F::from_f64(0.5).expect("Operation failed") * p[0] * p[0]
142        };
143
144        let potential = |_t: F, q: &Array1<F>| -> F { -q[0].cos() };
145
146        let kinetic_grad = |_t: F, p: &Array1<F>| -> Array1<F> { Array1::from_vec(vec![p[0]]) };
147
148        let potential_grad =
149            |_t: F, q: &Array1<F>| -> Array1<F> { Array1::from_vec(vec![q[0].sin()]) };
150
151        SeparableHamiltonian::new(kinetic, potential).with_gradients(kinetic_grad, potential_grad)
152    }
153
154    /// Create a 2D Kepler problem (planetary orbit)
155    ///
156    /// H(q, p) = |p|²/2 - 1/|q|
157    ///
158    /// # Returns
159    ///
160    /// A Kepler problem Hamiltonian
161    pub fn kepler_problem() -> Self {
162        let kinetic = |_t: F, p: &Array1<F>| -> F {
163            F::from_f64(0.5).expect("Operation failed") * (p[0] * p[0] + p[1] * p[1])
164        };
165
166        let potential = |_t: F, q: &Array1<F>| -> F {
167            let r = (q[0] * q[0] + q[1] * q[1]).sqrt();
168            if r < F::from_f64(1e-10).expect("Operation failed") {
169                F::zero()
170            } else {
171                -F::one() / r
172            }
173        };
174
175        let kinetic_grad = |_t: F, p: &Array1<F>| -> Array1<F> { p.to_owned() };
176
177        let potential_grad = |_t: F, q: &Array1<F>| -> Array1<F> {
178            let r2 = q[0] * q[0] + q[1] * q[1];
179            let r = r2.sqrt();
180
181            if r < F::from_f64(1e-10).expect("Operation failed") {
182                Array1::zeros(q.len())
183            } else {
184                let r3 = r * r2;
185                let factor = F::one() / r3;
186                Array1::from_vec(vec![q[0] * factor, q[1] * factor])
187            }
188        };
189
190        SeparableHamiltonian::new(kinetic, potential).with_gradients(kinetic_grad, potential_grad)
191    }
192}
193
194impl<F: IntegrateFloat> HamiltonianFn<F> for SeparableHamiltonian<F> {
195    fn dq_dt(&self, t: F, q: &Array1<F>, p: &Array1<F>) -> IntegrateResult<Array1<F>> {
196        // For separable Hamiltonian: dq/dt = ∂H/∂p = ∂T/∂p
197        if let Some(grad) = &self.kinetic_gradient {
198            Ok(grad(t, p))
199        } else {
200            // Numerical approximation using finite differences
201            let h = F::from_f64(1e-6).expect("Operation failed");
202            let mut dq = Array1::zeros(p.len());
203
204            for i in 0..p.len() {
205                let mut p_plus = p.to_owned();
206                p_plus[i] += h;
207
208                let mut p_minus = p.to_owned();
209                p_minus[i] -= h;
210
211                let t_plus = (self.kinetic_energy)(t, &p_plus);
212                let t_minus = (self.kinetic_energy)(t, &p_minus);
213
214                dq[i] = (t_plus - t_minus) / (F::from_f64(2.0).expect("Operation failed") * h);
215            }
216
217            Ok(dq)
218        }
219    }
220
221    fn dp_dt(&self, t: F, q: &Array1<F>, p: &Array1<F>) -> IntegrateResult<Array1<F>> {
222        // For separable Hamiltonian: dp/dt = -∂H/∂q = -∂V/∂q
223        if let Some(grad) = &self.potential_gradient {
224            // Negate the gradient since dp/dt = -∇V(q)
225            let dp = -grad(t, q);
226            Ok(dp)
227        } else {
228            // Numerical approximation using finite differences
229            let h = F::from_f64(1e-6).expect("Operation failed");
230            let mut dp = Array1::zeros(q.len());
231
232            for i in 0..q.len() {
233                let mut q_plus = q.to_owned();
234                q_plus[i] += h;
235
236                let mut q_minus = q.to_owned();
237                q_minus[i] -= h;
238
239                let v_plus = (self.potential_energy)(t, &q_plus);
240                let v_minus = (self.potential_energy)(t, &q_minus);
241
242                // Negative gradient
243                dp[i] = -(v_plus - v_minus) / (F::from_f64(2.0).expect("Operation failed") * h);
244            }
245
246            Ok(dp)
247        }
248    }
249
250    fn hamiltonian(
251        &self,
252    ) -> Option<Box<dyn Fn(F, &Array1<F>, &Array1<F>) -> IntegrateResult<F> + '_>> {
253        let kinetic = &self.kinetic_energy;
254        let potential = &self.potential_energy;
255
256        Some(Box::new(move |t, q, p| {
257            let t_val = kinetic(t, p);
258            let v_val = potential(t, q);
259            Ok(t_val + v_val)
260        }))
261    }
262}
263
264/// A general non-separable Hamiltonian system
265///
266/// This represents Hamiltonian systems where the equations of motion are
267/// specified directly without assuming a separable structure.
268pub struct HamiltonianSystem<F: IntegrateFloat> {
269    /// Function computing dq/dt = ∂H/∂p
270    dq_dt_fn: EquationOfMotion<F>,
271
272    /// Function computing dp/dt = -∂H/∂q
273    dp_dt_fn: EquationOfMotion<F>,
274
275    /// Optional function computing the Hamiltonian H(q, p)
276    hamiltonian_fn: Option<HamiltonianFunction<F>>,
277}
278
279impl<F: IntegrateFloat> HamiltonianSystem<F> {
280    /// Create a new general Hamiltonian system
281    ///
282    /// # Arguments
283    ///
284    /// * `dq_dt_fn` - Function computing dq/dt = ∂H/∂p
285    /// * `dp_dt_fn` - Function computing dp/dt = -∂H/∂q
286    ///
287    /// # Returns
288    ///
289    /// A new Hamiltonian system
290    pub fn new<DQ, DP>(_dq_dt_fn: DQ, dp_dtfn: DP) -> Self
291    where
292        DQ: Fn(F, &Array1<F>, &Array1<F>) -> Array1<F> + 'static + Send + Sync,
293        DP: Fn(F, &Array1<F>, &Array1<F>) -> Array1<F> + 'static + Send + Sync,
294    {
295        HamiltonianSystem {
296            dq_dt_fn: Box::new(_dq_dt_fn),
297            dp_dt_fn: Box::new(dp_dtfn),
298            hamiltonian_fn: None,
299        }
300    }
301
302    /// Add Hamiltonian function for energy tracking
303    ///
304    /// # Arguments
305    ///
306    /// * `hamiltonian_fn` - Function computing H(q, p)
307    ///
308    /// # Returns
309    ///
310    /// Self with Hamiltonian function configured
311    pub fn with_hamiltonian<H>(mut self, hamiltonianfn: H) -> Self
312    where
313        H: Fn(F, &Array1<F>, &Array1<F>) -> F + 'static + Send + Sync,
314    {
315        self.hamiltonian_fn = Some(Box::new(hamiltonianfn));
316        self
317    }
318}
319
320impl<F: IntegrateFloat> HamiltonianFn<F> for HamiltonianSystem<F> {
321    fn dq_dt(&self, t: F, q: &Array1<F>, p: &Array1<F>) -> IntegrateResult<Array1<F>> {
322        Ok((self.dq_dt_fn)(t, q, p))
323    }
324
325    fn dp_dt(&self, t: F, q: &Array1<F>, p: &Array1<F>) -> IntegrateResult<Array1<F>> {
326        Ok((self.dp_dt_fn)(t, q, p))
327    }
328
329    fn hamiltonian(
330        &self,
331    ) -> Option<Box<dyn Fn(F, &Array1<F>, &Array1<F>) -> IntegrateResult<F> + '_>> {
332        if let Some(h_fn) = &self.hamiltonian_fn {
333            let h = h_fn;
334            Some(Box::new(move |t, q, p| Ok(h(t, q, p))))
335        } else {
336            None
337        }
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use crate::symplectic::leapfrog::StormerVerlet;
345    use crate::symplectic::SymplecticIntegrator;
346    use scirs2_core::ndarray::array;
347
348    /// Test SeparableHamiltonian implementation with harmonic oscillator
349    #[test]
350    fn test_harmonic_oscillator() {
351        // Create a harmonic oscillator
352        let system = SeparableHamiltonian::harmonic_oscillator();
353
354        // Initial state
355        let q0 = array![1.0_f64];
356        let p0 = array![0.0_f64];
357        let t0 = 0.0_f64;
358
359        // Verify equations of motion
360        let dq = system.dq_dt(t0, &q0, &p0).expect("Operation failed");
361        let dp = system.dp_dt(t0, &q0, &p0).expect("Operation failed");
362
363        // For harmonic oscillator:
364        // dq/dt = p
365        // dp/dt = -q
366        assert!((dq[0] - p0[0]).abs() < 1e-10_f64);
367        assert!((dp[0] + q0[0]).abs() < 1e-10_f64);
368
369        // Verify Hamiltonian function
370        let h_fn = system.hamiltonian();
371        if let Some(hamiltonian) = h_fn {
372            let energy = hamiltonian(t0, &q0, &p0).expect("Operation failed");
373            // H = p²/2 + q²/2 = 0 + 0.5 = 0.5
374            assert!((energy - 0.5_f64).abs() < 1e-10);
375        } else {
376            panic!("Hamiltonian function not provided");
377        }
378    }
379
380    /// Test integration of separable system
381    #[test]
382    fn test_integrate_pendulum() {
383        // Create a pendulum
384        let system = SeparableHamiltonian::pendulum();
385
386        // Initial state (small angle)
387        let q0 = array![0.1]; // Small angle from vertical
388        let p0 = array![0.0]; // Starting from rest
389        let t0 = 0.0;
390
391        // For small angles, pendulum is approximately harmonic
392        // with period T = 2π
393        let period = 2.0 * PI;
394        let tf = period;
395        let dt = 0.01;
396
397        // Integrate
398        let integrator = StormerVerlet::new();
399        let result = integrator
400            .integrate(&system, t0, tf, dt, q0.clone(), p0.clone())
401            .expect("Failed to integrate");
402
403        // After one period, should be close to initial state
404        let q_final = &result.q[result.q.len() - 1];
405        let p_final = &result.p[result.p.len() - 1];
406
407        assert!(
408            (q_final[0] - q0[0]).abs() < 0.01_f64,
409            "q: {} vs {}",
410            q_final[0],
411            q0[0]
412        );
413        assert!(
414            (p_final[0] - p0[0]).abs() < 0.01_f64,
415            "p: {} vs {}",
416            p_final[0],
417            p0[0]
418        );
419
420        // Check energy conservation
421        if let Some(err) = result.energy_relative_error {
422            assert!(err < 1e-3, "Energy conservation error too large: {err}");
423        } else {
424            panic!("Energy conservation error not calculated");
425        }
426    }
427
428    /// Test generic Hamiltonian system
429    #[test]
430    fn test_generic_hamiltonian() {
431        // Create a generic Hamiltonian for a harmonic oscillator
432        let system = HamiltonianSystem::new(
433            // dq/dt = ∂H/∂p = p
434            |_t, q, p| p.clone(),
435            // dp/dt = -∂H/∂q = -q
436            |_t, q, _p| -q.clone(),
437        )
438        .with_hamiltonian(
439            // H(q, p) = p²/2 + q²/2
440            |_t, q, p| 0.5 * (q[0] * q[0] + p[0] * p[0]),
441        );
442
443        // Initial state
444        let q0 = array![1.0_f64];
445        let p0 = array![0.0_f64];
446        let t0 = 0.0_f64;
447
448        // Verify equations of motion
449        let dq = system.dq_dt(t0, &q0, &p0).expect("Operation failed");
450        let dp = system.dp_dt(t0, &q0, &p0).expect("Operation failed");
451
452        assert!((dq[0] - p0[0]).abs() < 1e-10_f64);
453        assert!((dp[0] + q0[0]).abs() < 1e-10_f64);
454
455        // Verify Hamiltonian function
456        let h_fn = system.hamiltonian();
457        if let Some(hamiltonian) = h_fn {
458            let energy = hamiltonian(t0, &q0, &p0).expect("Operation failed");
459            // H = p²/2 + q²/2 = 0 + 0.5 = 0.5
460            assert!((energy - 0.5_f64).abs() < 1e-10);
461        } else {
462            panic!("Hamiltonian function not provided");
463        }
464    }
465}