The hyper-ideal vertex scale b is floored to keep the geometry valid. The original clamp `b<0 → 0.01` mirrors the Java oracle but is only C⁰ (in fact value-discontinuous at b=0): a Newton step crossing the feasibility boundary hits a kink that can stall convergence (numerical-stability audit N3). Rather than replace the Java-faithful behaviour (which would break the golden parity tests), make the floor a selectable mode so BOTH the Java standpoint and the clean mathematics are available: - HyperIdealScaleClamp::HardJava (DEFAULT) — the original snap, bit-for-bit faithful to HyperIdealFunctional.java → all parity tests unchanged. - HyperIdealScaleClamp::SmoothBarrier — C¹ softplus floor b ↦ floor + softplus_β(b−floor), β = HYPER_IDEAL_SCALE_SHARPNESS (=100); ≈ identity away from the floor, smooth across b=0. Opt-in. clamp_hyper_ideal_scale centralises the logic (also folds in the N4 nachzügler: compute_face_angles used a bare 0.01). The mode threads with a defaulted trailing parameter through compute_face_angles, face_angles_from_local_dofs, evaluate_hyper_ideal, the four hyper_ideal_hessian* variants and newton_hyper_ideal — so every existing call site keeps HardJava behaviour. Tests (+4): clamp-function C¹/floor/identity contract, mode-equivalence away from the boundary, and end-to-end SmoothBarrier convergence to the same Java golden vector (LawsonHyperIdeal). 296/296 CGAL tests pass. Documented in doc/math/geometry-modes.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.6 KiB
The Three Geometry Modes
conformallab++ implements discrete conformal geometry in three model spaces. All three share the same algorithmic structure — only the angle formula, the trilateration geometry, and the Hessian sign differ.
Comparison
Euclidean Spherical Hyper-ideal
─────────────────────────────────────────────────────
Space ℝ² S² H² (Poincaré disk)
Curvature K = 0 K = +1 K = −1
Typical genus any (cone metrics) 0 ≥ 1
Angle sum Σαᵥ = Θᵥ Σαᵥ = Θᵥ Σβᵥ = Θᵥ
Gradient sign G_v = actual − Θᵥ G_v = Θᵥ − actual G_v = actual − Θᵥ
Hessian PSD (cotangent-Lap.) NSD (sign-flip) PSD (FD, strict conv.)
Newton solve SimplicialLDLT(H) SimplicialLDLT(−H) SimplicialLDLT(H)
Holonomy translations ωᵢ rotations (2-D) Möbius maps Tᵢ ∈ SU(1,1)
Period τ = ω₂/ω₁ ∈ ℍ — axis of Tᵢ
Normalise PCA centring Rodrigues to N pole weighted Möbius centring
Layout Layout2D (ℝ²) Layout3D (unit sphere) Layout2D (Poincaré disk)
Euclidean (ℝ²)
Energy: the cotangent-weighted angle defect energy (Pinkall & Polthier 1993).
Effective log-length:
Λ̃ᵢⱼ = λ°ᵢⱼ + uᵢ + uⱼ
Gradient: G_v = Σ_{faces adj. v} α_v(face) − Θᵥ — the actual angle sum minus the target.
Hessian: cotangent Laplacian, assembled in euclidean_hessian.hpp. PSD with one zero
eigenvalue (constant function) for closed meshes — handled by pinning one vertex or SparseQR.
Normalisation: centroid → origin, major axis → x-axis via PCA (normalise_euclidean).
Holonomy: lattice translations ω₁, ω₂ ∈ ℂ for closed surfaces. Period ratio τ = ω₂/ω₁ ∈ ℍ is the conformal modulus of the torus.
Spherical (S²)
Typical use: genus-0 (sphere-like) surfaces.
Gradient: G_v = Θᵥ − Σ α_v(face) — note the sign reversal vs. Euclidean.
Hessian: NSD (the spherical energy is concave). Newton solves (−H)·Δx = G, i.e.
SimplicialLDLT is called on −H. This is handled transparently in newton_spherical().
Gauge fix: for a closed spherical surface, one additional DOF must be pinned to remove
the rotational gauge mode. SphericalMaps has a dedicated gauge_vertex field.
Normalisation: normalise_spherical() rotates the layout so the area centroid
maps to the north pole (Rodrigues rotation formula).
Hyper-ideal (H²)
Typical use: genus-g surfaces (g ≥ 1), hyperbolic cone metrics.
DOFs: both vertex variables bᵢ (hyper-ideal radius) and edge variables aₑ
(intersection angle). assign_all_dof_indices(mesh, maps) assigns both automatically.
No vertex needs to be pinned — the functional is strictly convex.
Geometry functions (in hyper_ideal_geometry.hpp):
ζ₁₃(b, a) ζ₁₄(b, a) ζ₁₅(b, a) — the three fundamental Springborn functions
lᵢⱼ(ζ) — edge length from ζ value
αᵢⱼ(l₁, l₂, l₃) — interior angle
βᵢ(l₁, l₂, l₃) — vertex angle sum contribution
Hessian: a symmetric finite-difference approximation (see hyper_ideal_hessian.hpp).
The solver uses the per-face block-FD variant by default (O(F) face evaluations,
~33–1166× faster than full-FD); the full-FD baseline is retained for cross-validation.
The analytic Hessian through the (bᵢ, aₑ) → lᵢⱼ → ζ → αᵢⱼ/βᵢ chain is Phase 9b-analytic.
Scale-floor clamp mode (HyperIdealScaleClamp). The vertex scale bᵢ must stay
positive for the geometry to be valid. When a Newton iterate drives bᵢ toward or below
zero, one of two floors is applied — selectable per solver call (last argument of
newton_hyper_ideal, default HardJava):
| Mode | Behaviour | When to use |
|---|---|---|
HardJava (default) |
bᵢ < 0 → HYPER_IDEAL_SCALE_FLOOR (= 0.01). Bit-for-bit faithful to HyperIdealFunctional.java, so the Java golden-oracle parity tests hold exactly. It is only C⁰ (in fact value-discontinuous at bᵢ = 0), so a step crossing the feasibility boundary sees a kink that can stall Newton. |
Default — preserves parity; correct whenever scales stay positive (the normal case). |
SmoothBarrier |
bᵢ ↦ floor + softplusβ(bᵢ − floor) with β = HYPER_IDEAL_SCALE_SHARPNESS (= 100). C¹ everywhere, ≈ bᵢ to machine precision once β·(bᵢ−floor) ≳ 35, and decays smoothly to floor as bᵢ → −∞. Removes the N3 kink, but perturbs values near the boundary and therefore deviates from the Java oracle there. |
Opt in when a solve is expected to cross the feasibility boundary and convergence robustness matters more than strict Java parity. |
Both modes share HYPER_IDEAL_SCALE_FLOOR; the edge clamp (aₑ < 0 → 0) is unchanged.
Away from the floor (the normal bᵢ ≫ 0.01 regime) the two modes are numerically
identical — the LawsonHyperIdeal.SmoothBarrierConvergesToGoldenVector test confirms the
smooth mode reaches the same golden solution as HardJava. Implemented in
clamp_hyper_ideal_scale (hyper_ideal_functional.hpp); origin: numerical-stability
audit finding N3.
Holonomy: Möbius isometries Tᵢ ∈ SU(1,1) (orientation-preserving isometries of the Poincaré disk).
MobiusMap in layout.hpp: T(z) = (az+b)/(cz+d).
Normalisation: normalise_hyperbolic() performs iterative face-area-weighted Möbius centring
(Fréchet mean, 30 iterations) to map the weighted centroid to the disk origin.
Gauss–Bonnet constraint
Before calling any Newton solver, the target angles must satisfy the Gauss–Bonnet equation:
Σᵥ (2π − Θᵥ) = 2π · χ(M) (Euclidean / flat)
Σᵥ (2π − Θᵥ) > 0 (spherical, χ > 0)
Σᵥ (2π − Θᵥ) < 0 (hyperbolic, χ < 0)
If this is violated, no conformal factor can realise the target angles and Newton will fail to converge without a visible error.
check_gauss_bonnet(mesh, maps); // throws if violated
enforce_gauss_bonnet(mesh, maps); // redistributes residual uniformly across all vertices
This is the most common source of silent non-convergence. Always call one of these before solving.