1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
//! # Getting started
//!
//! Every manifold implements [`Manifold`](crate::traits::Manifold), which
//! supplies `exp`, `log`, `dist`, `inner` and `project_tangent`. Manifold types
//! are zero-sized: the geometry lives in the trait impls, so `Sphere::<3>` costs
//! nothing to hold or pass around.
//!
//! ```
//! use cartan::prelude::*;
//! use cartan::manifolds::Sphere;
//!
//! let s2 = Sphere::<3>; // the 2-sphere embedded in R^3
//!
//! let mut rng = rand::rng();
//! let p = s2.random_point(&mut rng);
//!
//! // Scaled inside the injectivity radius, which is pi on the unit sphere.
//! // `random_tangent` draws a projected standard normal, so its norm is
//! // unbounded; past pi the geodesic runs beyond the antipode and `log`
//! // returns the shorter way back, so the two stop being inverse.
//! let v = s2.random_tangent(&p, &mut rng).normalize() * 1.2;
//!
//! // Walk along the geodesic leaving p with velocity v.
//! let q = s2.exp(&p, &v);
//!
//! // Recover the velocity that carries p to q.
//! let v_back = s2.log(&p, &q).unwrap();
//! assert!((v - v_back).norm() < 1e-10);
//!
//! // Geodesic distance is the length of that velocity.
//! let d = s2.dist(&p, &q).unwrap();
//! assert!((d - s2.norm(&p, &v)).abs() < 1e-10);
//!
//! // Sectional curvature needs two independent tangents to span a plane.
//! // Every such plane on the unit sphere has K = 1.
//! let w = s2.random_tangent(&p, &mut rng);
//! assert!((s2.sectional_curvature(&p, &v, &w) - 1.0).abs() < 1e-9);
//! ```
//!
//! ## exp and log are inverses, within the injectivity radius
//!
//! `log` returns a [`Result`] because it fails past the cut locus, where the
//! minimising geodesic stops being unique. On a sphere that is the antipode:
//! every great circle through `p` reaches `-p` in the same distance, so there is
//! no single answer to return.
//!
//! ```
//! use cartan::prelude::*;
//! use cartan::manifolds::Sphere;
//! use nalgebra::SVector;
//!
//! let s2 = Sphere::<3>;
//! let north = SVector::<f64, 3>::new(0.0, 0.0, 1.0);
//! let south = SVector::<f64, 3>::new(0.0, 0.0, -1.0);
//!
//! // Antipodal points sit exactly on the cut locus.
//! assert!(s2.log(&north, &south).is_err());
//!
//! // Anywhere nearer than that, log succeeds.
//! let equator = SVector::<f64, 3>::new(1.0, 0.0, 0.0);
//! assert!(s2.log(&north, &equator).is_ok());
//! ```
//!
//! ## Parallel transport
//!
//! Moving a tangent vector between points requires a connection, since tangent
//! spaces at different points are different vector spaces. Transport preserves
//! inner products, which is the defining property of the Levi-Civita connection.
//!
//! ```
//! use cartan::prelude::*;
//! use cartan::manifolds::Sphere;
//!
//! let s2 = Sphere::<3>;
//! let mut rng = rand::rng();
//!
//! let p = s2.random_point(&mut rng);
//! let q = s2.random_point(&mut rng);
//! let u = s2.random_tangent(&p, &mut rng);
//! let v = s2.random_tangent(&p, &mut rng);
//!
//! let u_q = s2.transport(&p, &q, &u).unwrap();
//! let v_q = s2.transport(&p, &q, &v).unwrap();
//!
//! // Transport is an isometry between tangent spaces.
//! let before = s2.inner(&p, &u, &v);
//! let after = s2.inner(&q, &u_q, &v_q);
//! assert!((before - after).abs() < 1e-9);
//! ```
//!
//! ## Where to go next
//!
//! - [`manifolds`](super::manifolds) for the catalogue and how to choose.
//! - [`optimisation`](super::optimisation) to minimise a function on a manifold.