Skip to main content

gam_geometry/
integrator.rs

1use ndarray::{Array1, Array2, ArrayView1};
2
3use crate::manifold::{GeometryResult, RiemannianManifold, check_len};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub struct GeodesicIntegrator {
7    pub steps: usize,
8    pub step_size: f64,
9}
10
11impl Default for GeodesicIntegrator {
12    fn default() -> Self {
13        Self {
14            steps: 32,
15            step_size: 1.0 / 32.0,
16        }
17    }
18}
19
20impl GeodesicIntegrator {
21    /// Integrate the geodesic ODE on the manifold by composing the closed-form
22    /// geodesic flow `exp_p(v · h)` with parallel transport of the velocity
23    /// along that segment. This is the mathematically correct flow on every
24    /// Riemannian manifold whose `RiemannianManifold` impl supplies an
25    /// `exp_map` and `parallel_transport` consistent with its metric — it
26    /// reduces to a Christoffel-driven leap-frog integrator on coordinates
27    /// where Γ ≡ 0 (e.g. Euclidean / flat tori) but stays on the manifold
28    /// and conserves the Riemannian kinetic energy on positively curved
29    /// spaces such as the sphere, which previously drifted off because the
30    /// extrinsic Christoffel symbols are not zero.
31    pub fn integrate(
32        &self,
33        manifold: &dyn RiemannianManifold,
34        point: ArrayView1<'_, f64>,
35        tangent: ArrayView1<'_, f64>,
36    ) -> GeometryResult<Array1<f64>> {
37        let d = manifold.ambient_dim();
38        check_len("geodesic integrator point", point.len(), d)?;
39        check_len("geodesic integrator tangent", tangent.len(), d)?;
40        // Integrate the geodesic from parameter t=0 to t=1 so the result is
41        // the canonical unit-time endpoint exp_p(v). The IVP energy ½|v(t)|²
42        // is invariant along any geodesic; the BVP quantity ½|log_p(γ(1))|²
43        // matches ½|v|² only when total integration time equals 1. We honour
44        // both `steps` (substep count) and `step_size` (substep duration) by
45        // taking the finer of the two — `n = max(steps, ⌈1/step_size⌉)` —
46        // each of duration `h = 1/n`. This gives strictly more accuracy when
47        // either bound is tightened and never overshoots the unit-time
48        // endpoint, so closed-form exp_map/parallel_transport composition is
49        // numerically exact for any caller-provided refinement.
50        let mut x = point.to_owned();
51        let mut v = manifold.project_tangent(point, tangent)?;
52        let steps_from_size = if self.step_size.is_finite() && self.step_size > 0.0 {
53            (1.0 / self.step_size).ceil() as usize
54        } else {
55            1
56        };
57        let n_substeps = self.steps.max(1).max(steps_from_size);
58        let h = 1.0 / n_substeps as f64;
59        for _ in 0..n_substeps {
60            let step_tangent = &v * h;
61            let x_next = manifold.exp_map(x.view(), step_tangent.view())?;
62            let mut path = Array2::<f64>::zeros((2, d));
63            path.row_mut(0).assign(&x);
64            path.row_mut(1).assign(&x_next);
65            let v_next = manifold.parallel_transport(path.view(), v.view())?;
66            x = x_next;
67            v = manifold.project_tangent(x.view(), v_next.view())?;
68        }
69        Ok(x)
70    }
71}