scirs2_integrate/symplectic/
potential.rs1use 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
18type EnergyFunction<F> = Box<dyn Fn(F, &Array1<F>) -> F + Send + Sync>;
20
21type GradientFunction<F> = Box<dyn Fn(F, &Array1<F>) -> Array1<F> + Send + Sync>;
23
24type EquationOfMotion<F> = Box<dyn Fn(F, &Array1<F>, &Array1<F>) -> Array1<F> + Send + Sync>;
26
27type HamiltonianFunction<F> = Box<dyn Fn(F, &Array1<F>, &Array1<F>) -> F + Send + Sync>;
29
30pub struct SeparableHamiltonian<F: IntegrateFloat> {
41 kinetic_energy: EnergyFunction<F>,
43
44 potential_energy: EnergyFunction<F>,
46
47 potential_gradient: Option<GradientFunction<F>>,
49
50 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 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 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 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 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 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 if let Some(grad) = &self.kinetic_gradient {
198 Ok(grad(t, p))
199 } else {
200 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 if let Some(grad) = &self.potential_gradient {
224 let dp = -grad(t, q);
226 Ok(dp)
227 } else {
228 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 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
264pub struct HamiltonianSystem<F: IntegrateFloat> {
269 dq_dt_fn: EquationOfMotion<F>,
271
272 dp_dt_fn: EquationOfMotion<F>,
274
275 hamiltonian_fn: Option<HamiltonianFunction<F>>,
277}
278
279impl<F: IntegrateFloat> HamiltonianSystem<F> {
280 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 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]
350 fn test_harmonic_oscillator() {
351 let system = SeparableHamiltonian::harmonic_oscillator();
353
354 let q0 = array![1.0_f64];
356 let p0 = array![0.0_f64];
357 let t0 = 0.0_f64;
358
359 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 assert!((dq[0] - p0[0]).abs() < 1e-10_f64);
367 assert!((dp[0] + q0[0]).abs() < 1e-10_f64);
368
369 let h_fn = system.hamiltonian();
371 if let Some(hamiltonian) = h_fn {
372 let energy = hamiltonian(t0, &q0, &p0).expect("Operation failed");
373 assert!((energy - 0.5_f64).abs() < 1e-10);
375 } else {
376 panic!("Hamiltonian function not provided");
377 }
378 }
379
380 #[test]
382 fn test_integrate_pendulum() {
383 let system = SeparableHamiltonian::pendulum();
385
386 let q0 = array![0.1]; let p0 = array![0.0]; let t0 = 0.0;
390
391 let period = 2.0 * PI;
394 let tf = period;
395 let dt = 0.01;
396
397 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 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 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]
430 fn test_generic_hamiltonian() {
431 let system = HamiltonianSystem::new(
433 |_t, q, p| p.clone(),
435 |_t, q, _p| -q.clone(),
437 )
438 .with_hamiltonian(
439 |_t, q, p| 0.5 * (q[0] * q[0] + p[0] * p[0]),
441 );
442
443 let q0 = array![1.0_f64];
445 let p0 = array![0.0_f64];
446 let t0 = 0.0_f64;
447
448 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 let h_fn = system.hamiltonian();
457 if let Some(hamiltonian) = h_fn {
458 let energy = hamiltonian(t0, &q0, &p0).expect("Operation failed");
459 assert!((energy - 0.5_f64).abs() < 1e-10);
461 } else {
462 panic!("Hamiltonian function not provided");
463 }
464 }
465}