Skip to main content

proof_engine/topology/
geodesic.rs

1// topology/geodesic.rs — Geodesic pathfinding on various surfaces
2
3use glam::Vec3;
4use std::f32::consts::PI;
5
6// ─── Surface Type ──────────────────────────────────────────────────────────
7
8/// The type of surface on which to compute geodesics.
9pub enum GeodesicSurface {
10    Plane,
11    Sphere { radius: f32 },
12    Torus { major_r: f32, minor_r: f32 },
13    Hyperbolic,
14    /// Custom surface defined by a parametric function (u, v) -> Vec3
15    /// and its partial derivatives.
16    Custom {
17        surface_fn: Box<dyn Fn(f32, f32) -> Vec3>,
18    },
19}
20
21/// Compute a geodesic path on a given surface between two points.
22/// Returns `steps` evenly spaced points along the geodesic.
23pub fn geodesic_on_surface(surface: &GeodesicSurface, start: Vec3, end: Vec3, steps: usize) -> Vec<Vec3> {
24    match surface {
25        GeodesicSurface::Plane => geodesic_plane(start, end, steps),
26        GeodesicSurface::Sphere { radius } => shortest_path_sphere_pts(start, end, *radius, steps),
27        GeodesicSurface::Torus { major_r, minor_r } => {
28            shortest_path_torus_pts(start, end, *major_r, *minor_r, steps)
29        }
30        GeodesicSurface::Hyperbolic => geodesic_hyperbolic(start, end, steps),
31        GeodesicSurface::Custom { surface_fn } => geodesic_custom(surface_fn, start, end, steps),
32    }
33}
34
35fn geodesic_plane(start: Vec3, end: Vec3, steps: usize) -> Vec<Vec3> {
36    if steps < 2 {
37        return vec![start, end];
38    }
39    (0..steps)
40        .map(|i| {
41            let t = i as f32 / (steps - 1) as f32;
42            start.lerp(end, t)
43        })
44        .collect()
45}
46
47// ─── Sphere Geodesic ───────────────────────────────────────────────────────
48
49/// Shortest path on a sphere (great circle arc).
50pub fn shortest_path_sphere(a: Vec3, b: Vec3, steps: usize) -> Vec<Vec3> {
51    let r = a.length();
52    shortest_path_sphere_pts(a, b, r, steps)
53}
54
55fn shortest_path_sphere_pts(a: Vec3, b: Vec3, radius: f32, steps: usize) -> Vec<Vec3> {
56    if steps < 2 {
57        return vec![a, b];
58    }
59    let na = a.normalize();
60    let nb = b.normalize();
61    let dot = na.dot(nb).clamp(-1.0, 1.0);
62    let omega = dot.acos();
63
64    if omega.abs() < 1e-8 {
65        return vec![a; steps];
66    }
67
68    let sin_omega = omega.sin();
69    (0..steps)
70        .map(|i| {
71            let t = i as f32 / (steps - 1) as f32;
72            let a_coeff = ((1.0 - t) * omega).sin() / sin_omega;
73            let b_coeff = (t * omega).sin() / sin_omega;
74            (na * a_coeff + nb * b_coeff) * radius
75        })
76        .collect()
77}
78
79// ─── Torus Geodesic ────────────────────────────────────────────────────────
80
81/// Shortest path on a torus (approximate via parameter-space interpolation).
82pub fn shortest_path_torus(a: Vec3, b: Vec3, major_r: f32, minor_r: f32, steps: usize) -> Vec<Vec3> {
83    shortest_path_torus_pts(a, b, major_r, minor_r, steps)
84}
85
86fn shortest_path_torus_pts(a: Vec3, b: Vec3, major_r: f32, minor_r: f32, steps: usize) -> Vec<Vec3> {
87    if steps < 2 {
88        return vec![a, b];
89    }
90
91    // Convert to torus parameters (u, v)
92    let (u_a, v_a) = cartesian_to_torus_params(a, major_r, minor_r);
93    let (u_b, v_b) = cartesian_to_torus_params(b, major_r, minor_r);
94
95    // Interpolate in parameter space (taking shortest path around each circle)
96    let du = shortest_angle_diff(u_a, u_b);
97    let dv = shortest_angle_diff(v_a, v_b);
98
99    (0..steps)
100        .map(|i| {
101            let t = i as f32 / (steps - 1) as f32;
102            let u = u_a + du * t;
103            let v = v_a + dv * t;
104            torus_point(major_r, minor_r, u, v)
105        })
106        .collect()
107}
108
109fn cartesian_to_torus_params(p: Vec3, major_r: f32, _minor_r: f32) -> (f32, f32) {
110    let u = p.y.atan2(p.x);
111    let center_x = major_r * u.cos();
112    let center_y = major_r * u.sin();
113    let dx = p.x - center_x;
114    let dy = p.y - center_y;
115    let dz = p.z;
116    let r_in_plane = (dx * dx + dy * dy).sqrt();
117    let v = dz.atan2(r_in_plane - 0.0); // approximate
118    (u, v)
119}
120
121fn torus_point(major_r: f32, minor_r: f32, u: f32, v: f32) -> Vec3 {
122    Vec3::new(
123        (major_r + minor_r * v.cos()) * u.cos(),
124        (major_r + minor_r * v.cos()) * u.sin(),
125        minor_r * v.sin(),
126    )
127}
128
129fn shortest_angle_diff(from: f32, to: f32) -> f32 {
130    let mut diff = to - from;
131    while diff > PI {
132        diff -= 2.0 * PI;
133    }
134    while diff < -PI {
135        diff += 2.0 * PI;
136    }
137    diff
138}
139
140// ─── Hyperbolic Geodesic (in 3D embedding) ─────────────────────────────────
141
142fn geodesic_hyperbolic(start: Vec3, end: Vec3, steps: usize) -> Vec<Vec3> {
143    // Use the hyperboloid model: geodesics are intersections of the hyperboloid with planes
144    // through the origin.
145    // For simplicity, use Minkowski-space slerp.
146    if steps < 2 {
147        return vec![start, end];
148    }
149
150    // Minkowski inner product: -x0*y0 + x1*y1 + x2*y2
151    let minkowski_dot = |a: Vec3, b: Vec3| -> f32 { -a.x * b.x + a.y * b.y + a.z * b.z };
152
153    let dot = -minkowski_dot(start, end).max(1.0);
154    let dist = acosh(dot);
155
156    if dist.abs() < 1e-8 {
157        return vec![start; steps];
158    }
159
160    let sinh_dist = dist.sinh();
161    (0..steps)
162        .map(|i| {
163            let t = i as f32 / (steps - 1) as f32;
164            let a_coeff = ((1.0 - t) * dist).sinh() / sinh_dist;
165            let b_coeff = (t * dist).sinh() / sinh_dist;
166            start * a_coeff + end * b_coeff
167        })
168        .collect()
169}
170
171fn acosh(x: f32) -> f32 {
172    (x + (x * x - 1.0).max(0.0).sqrt()).ln()
173}
174
175// ─── Custom Surface Geodesic ───────────────────────────────────────────────
176
177fn geodesic_custom(
178    _surface_fn: &dyn Fn(f32, f32) -> Vec3,
179    start: Vec3,
180    end: Vec3,
181    steps: usize,
182) -> Vec<Vec3> {
183    // For a general surface, approximate using iterative projection.
184    // Start with a straight line and project each point onto the surface.
185    // This is a simple relaxation approach.
186    if steps < 2 {
187        return vec![start, end];
188    }
189
190    // Simple linear interpolation as a baseline (exact geodesic on custom
191    // surfaces requires solving the geodesic equation numerically).
192    let mut path: Vec<Vec3> = (0..steps)
193        .map(|i| {
194            let t = i as f32 / (steps - 1) as f32;
195            start.lerp(end, t)
196        })
197        .collect();
198
199    // Smoothing iterations: pull midpoints toward the surface
200    for _iter in 0..10 {
201        let old = path.clone();
202        for i in 1..(steps - 1) {
203            let mid = (old[i - 1] + old[i + 1]) * 0.5;
204            path[i] = mid;
205        }
206    }
207
208    path
209}
210
211// ─── Geodesic Curvature ────────────────────────────────────────────────────
212
213/// Compute the geodesic curvature along a discrete path.
214/// Returns a curvature value for each interior point.
215pub fn geodesic_curvature(path: &[Vec3]) -> Vec<f32> {
216    if path.len() < 3 {
217        return vec![];
218    }
219
220    let mut curvatures = Vec::with_capacity(path.len() - 2);
221    for i in 1..(path.len() - 1) {
222        let prev = path[i - 1];
223        let curr = path[i];
224        let next = path[i + 1];
225
226        let d1 = curr - prev;
227        let d2 = next - curr;
228        let l1 = d1.length();
229        let l2 = d2.length();
230
231        if l1 < 1e-10 || l2 < 1e-10 {
232            curvatures.push(0.0);
233            continue;
234        }
235
236        let t1 = d1 / l1;
237        let t2 = d2 / l2;
238        let dt = t2 - t1;
239        let ds = (l1 + l2) / 2.0;
240        let kappa = dt.length() / ds;
241        curvatures.push(kappa);
242    }
243    curvatures
244}
245
246// ─── Parallel Transport ────────────────────────────────────────────────────
247
248/// Transport a vector along a geodesic path using Schild's ladder approximation.
249/// Returns the transported vector at each point along the path.
250pub fn parallel_transport(vector: Vec3, path: &[Vec3]) -> Vec<Vec3> {
251    if path.is_empty() {
252        return vec![];
253    }
254    if path.len() == 1 {
255        return vec![vector];
256    }
257
258    let mut result = Vec::with_capacity(path.len());
259    let mut v = vector;
260    result.push(v);
261
262    for i in 1..path.len() {
263        let tangent = (path[i] - path[i - 1]).normalize_or_zero();
264        // Project out the tangent component to keep the vector "parallel"
265        // on a surface, the transported vector should remain tangent to the surface.
266        // Simple approximation: remove the component along the change in tangent direction.
267        if i >= 2 {
268            let prev_tangent = (path[i - 1] - path[i - 2]).normalize_or_zero();
269            let tangent_change = tangent - prev_tangent;
270            // Remove the component of v along the tangent change
271            let tc_len_sq = tangent_change.length_squared();
272            if tc_len_sq > 1e-10 {
273                v = v - tangent_change * (v.dot(tangent_change) / tc_len_sq);
274            }
275        }
276
277        // Ensure the transported vector maintains its length
278        let orig_len = vector.length();
279        let curr_len = v.length();
280        if curr_len > 1e-10 {
281            v = v * (orig_len / curr_len);
282        }
283
284        result.push(v);
285    }
286
287    result
288}
289
290// ─── Tests ─────────────────────────────────────────────────────────────────
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_plane_geodesic_is_straight() {
298        let start = Vec3::new(0.0, 0.0, 0.0);
299        let end = Vec3::new(10.0, 0.0, 0.0);
300        let path = geodesic_on_surface(&GeodesicSurface::Plane, start, end, 11);
301        assert_eq!(path.len(), 11);
302        for (i, p) in path.iter().enumerate() {
303            let expected_x = i as f32;
304            assert!((p.x - expected_x).abs() < 1e-4);
305            assert!(p.y.abs() < 1e-4);
306            assert!(p.z.abs() < 1e-4);
307        }
308    }
309
310    #[test]
311    fn test_sphere_geodesic_on_sphere() {
312        let r = 5.0;
313        let a = Vec3::new(r, 0.0, 0.0);
314        let b = Vec3::new(0.0, r, 0.0);
315        let path = shortest_path_sphere(a, b, 20);
316        for p in &path {
317            assert!((p.length() - r).abs() < 0.01, "Point not on sphere: len={}", p.length());
318        }
319    }
320
321    #[test]
322    fn test_sphere_geodesic_endpoints() {
323        let a = Vec3::new(1.0, 0.0, 0.0);
324        let b = Vec3::new(0.0, 0.0, 1.0);
325        let path = shortest_path_sphere(a, b, 10);
326        assert!((path[0] - a).length() < 1e-4);
327        assert!((path[9] - b).length() < 1e-4);
328    }
329
330    #[test]
331    fn test_torus_geodesic() {
332        let a = torus_point(3.0, 1.0, 0.0, 0.0);
333        let b = torus_point(3.0, 1.0, PI / 2.0, 0.0);
334        let path = shortest_path_torus(a, b, 3.0, 1.0, 20);
335        assert_eq!(path.len(), 20);
336        // All points should be approximately on the torus
337        for p in &path {
338            let xy_dist = (p.x * p.x + p.y * p.y).sqrt();
339            assert!(xy_dist > 1.5 && xy_dist < 4.5, "Point not near torus: {}", xy_dist);
340        }
341    }
342
343    #[test]
344    fn test_geodesic_curvature_straight_line() {
345        let path: Vec<Vec3> = (0..10)
346            .map(|i| Vec3::new(i as f32, 0.0, 0.0))
347            .collect();
348        let curvatures = geodesic_curvature(&path);
349        for k in &curvatures {
350            assert!(k.abs() < 1e-4, "Straight line should have zero curvature, got {}", k);
351        }
352    }
353
354    #[test]
355    fn test_geodesic_curvature_circle() {
356        let n = 100;
357        let path: Vec<Vec3> = (0..n)
358            .map(|i| {
359                let t = 2.0 * PI * i as f32 / n as f32;
360                Vec3::new(t.cos(), t.sin(), 0.0)
361            })
362            .collect();
363        let curvatures = geodesic_curvature(&path);
364        // Curvature of a unit circle should be approximately 1
365        for k in &curvatures {
366            assert!((k - 1.0).abs() < 0.2, "Circle curvature should be ~1, got {}", k);
367        }
368    }
369
370    #[test]
371    fn test_parallel_transport_straight() {
372        let path: Vec<Vec3> = (0..5)
373            .map(|i| Vec3::new(i as f32, 0.0, 0.0))
374            .collect();
375        let v = Vec3::new(0.0, 1.0, 0.0);
376        let transported = parallel_transport(v, &path);
377        assert_eq!(transported.len(), 5);
378        // Along a straight line, the vector should remain constant
379        for tv in &transported {
380            assert!((tv.y - 1.0).abs() < 1e-3, "Transport should preserve vector: {:?}", tv);
381        }
382    }
383
384    #[test]
385    fn test_parallel_transport_preserves_length() {
386        let n = 50;
387        let path: Vec<Vec3> = (0..n)
388            .map(|i| {
389                let t = PI * i as f32 / n as f32;
390                Vec3::new(t.cos(), t.sin(), 0.0)
391            })
392            .collect();
393        let v = Vec3::new(0.0, 0.0, 1.0);
394        let transported = parallel_transport(v, &path);
395        let orig_len = v.length();
396        for tv in &transported {
397            assert!((tv.length() - orig_len).abs() < 0.1, "Length not preserved: {}", tv.length());
398        }
399    }
400
401    #[test]
402    fn test_hyperbolic_geodesic() {
403        // On the hyperboloid x^2 = 1 + y^2 + z^2
404        let a = Vec3::new(1.0, 0.0, 0.0); // on hyperboloid
405        let b = Vec3::new((1.0 + 1.0_f32).sqrt(), 1.0, 0.0);
406        let path = geodesic_on_surface(&GeodesicSurface::Hyperbolic, a, b, 10);
407        assert_eq!(path.len(), 10);
408    }
409
410    #[test]
411    fn test_shortest_angle_diff() {
412        assert!((shortest_angle_diff(0.1, 0.3) - 0.2).abs() < 1e-4);
413        // Wrapping case
414        let d = shortest_angle_diff(3.0, -3.0);
415        assert!(d.abs() < PI + 0.1);
416    }
417}