Error Handling
cartan uses a single CartanError enum for all fallible operations. Most
methods succeed for any valid input; only a handful of geometric degeneracies
can cause failure.
CartanError Variants
#[derive(Debug, thiserror::Error)]
pub enum CartanError {
#[error("point does not lie on the manifold (residual = {residual:.2e})")]
PointNotOnManifold { residual: f64 },
#[error("vector is not tangent at the given point (residual = {residual:.2e})")]
NotATangentVector { residual: f64 },
#[error("points are antipodal; log map is undefined")]
AntipodalPoints,
#[error("dimension mismatch: expected {expected}, got {got}")]
DimensionMismatch { expected: usize, got: usize },
#[error("numerical failure: {msg}")]
NumericalFailure { msg: String },
}Which Operations Can Fail
| Method | Return type | Failure condition |
|---|---|---|
log(p, q) | Option<Tangent> | q on cut locus of p |
geodesic(p, q, t) | Option<Point> | q on cut locus of p |
parallel_transport(p, q, v) | Option<Tangent> | q antipodal to p |
vector_transport(p, q, v) | Option<Tangent> | same as above |
inverse_retract(p, q) | Option<Tangent> | same as log |
check_point(p) | Result<(), CartanError> | p not on M |
check_tangent(p, v) | Result<(), CartanError> | v not in T_pM |
exp(p, v) | Point | never fails |
retract(p, v) | Point | never fails |
dist(p, q) | f64 | never fails |
inner(p, u, v) | f64 | never fails |
random_point | Point | never fails |
The Option-returning methods use None (not Err) because the failure
condition is a predictable geometric degeneracy, not a programming error.
Handling log and Transport
match manifold.log(&p, &q) {
Some(v) => {
// Normal path: use v for gradient descent, geodesic, etc.
}
None => {
// p and q are antipodal. Options:
// 1. Perturb q slightly off the cut locus.
// 2. Use a retraction-based fallback.
// 3. Skip this pair (e.g. in a Fréchet mean iteration).
eprintln!("antipodal pair encountered; skipping");
}
}Handling Validation Errors
use cartan::CartanError;
fn process(manifold: &Sphere<3>, p: Point, v: Tangent) -> Result<(), CartanError> {
manifold.check_point(&p)?;
manifold.check_tangent(&p, &v)?;
let q = manifold.exp(&p, &v); // safe after validation
Ok(())
}check_point and check_tangent are cheap: they compute a residual norm and
compare against a tolerance (default 1e-8). Use them at system boundaries
(deserialising stored points, user-provided inputs), not inside hot loops.
Antipodal Points in Practice
The cut locus of $p$ on is the single point . In random sampling or optimisation this is a measure-zero event. If it occurs repeatedly, it signals that your algorithm is taking steps of size $\approx \pi$: too large.
// Safe Fréchet mean iteration; skip antipodal pairs.
let mean_update: Tangent = points.iter()
.filter_map(|q| manifold.log(¤t, q))
.fold(zero_tangent(), |acc, v| acc + v);NumericalFailure
Reserved for operations that require a matrix decomposition that fails due to
near-singularity (e.g. SPD matrices near the boundary of the cone, or
Grassmann points with nearly equal singular values). These are returned as
Err(CartanError::NumericalFailure { msg }) from methods that return Result.
// Example: SPD geodesic near a rank-deficient matrix.
match spd.log(&p_near_boundary, &q) {
Some(v) => { /* proceed */ }
None => { /* Cholesky failed internally, treat as cut locus */ }
}Summary
Option<T>: geometric degeneracy (antipodal points, cut locus). Expected, recoverable.Result<T, CartanError>: validation failure or numerical breakdown. Use?to propagate.- Infallible:
exp,retract,dist,innernever fail on valid inputs.