Files
ConformalLabpp/doc/math/validation.md
Tarik Moussa a3ee9576d4
All checks were successful
C++ Tests / test-fast (pull_request) Successful in 1m57s
API Docs / doc-build (pull_request) Successful in 1m3s
Markdown link check / check (pull_request) Successful in 44s
C++ Tests / test-cgal (pull_request) Has been skipped
C++ Tests / quality-gates (pull_request) Successful in 2m11s
fix+test: Euclidean holonomy/τ end-to-end + spherical edge-DOF oracle (2026-05-29 audit)
Bundles the 2026-05-29 Java↔C++ math-correctness audit (doc/reviewer/
java-port-audit.md, 11 findings) with two follow-up fixes.

Audit code changes:
- Finding 3 (spherical_functional): edge-DOF replacement parameterization via
  spher_eff_lambda; edge gradient α_opp⁺+α_opp⁻−θ_e (drops additive −(S⁺+S⁻)/2)
- Finding 4 (spherical_hessian): always-compiled edge-DOF throw guard
- Finding 6 (period_matrix): faithful normalizeModulus (0≤Re≤½, Im≥0, |τ|≥1)
- Finding 9 (inversive_distance): degenerate-face limiting angles, no skip
- Findings 1/2 (euclidean): degenerate gradient limiting angles + Hessian guard

Euclidean holonomy/τ fix: develop the cut surface across the dual spanning tree
only (cut_graph now exposes is_dual_tree), so genus-1 cut edges yield
non-degenerate lattice generators. Previously τ came out 0 / NaN / 1e13 on the
bundled tori; now matches the analytic revolution modulus i·√(R²−r²)/r. Re-enabled
τ reporting in the Euclidean CLI; rewrote validation.md §3/§4 accordingly.

Tests (240 CGAL, 0 skipped):
- HolonomyEndToEnd ×3 — tori of revolution (4×4, hex 6×6, 8×8) vs analytic modulus
- SphericalFunctional.EdgeGradient_RegularTetClosedForm — independent closed-form
  π/3 oracle locking the Finding-3 edge formula (the path-integral FD check cannot
  detect a wrong-but-conservative gradient)

Also documents the latent spherical/hyperbolic holonomy-extraction bug (same
single-development pattern, dead code today) in research-track.md (Phase 9c/10),
and adds favour/normalisations to the codespell ignore list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 12:50:16 +02:00

315 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Mathematical Validation
This document lists analytically known results and explains how to verify
them against conformallab++ output. It is the primary tool for an independent
mathematician to check the correctness of the implementation.
---
## How to run the examples
```bash
cmake -S code -B build -DWITH_CGAL=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build --target conformallab_cgal_tests
ctest --test-dir build -R cgal --output-on-failure
```
All tests pass, 0 skipped (per-suite breakdown: `doc/api/tests.md`).
---
## 1 — GaussBonnet (topology)
**Theorem.** For any closed triangulated surface M,
```
Σᵥ (2π Θᵥ) = 2π · χ(M)
```
where χ(M) = 2 2g is the Euler characteristic.
| Surface | g | χ | Σ(2π Θᵥ) |
|---|---|---|---|
| Sphere (tetrahedron, cube, …) | 0 | 2 | 4π |
| Torus | 1 | 0 | 0 |
| Double torus | 2 | 2 | 4π |
**How to check:**
```cpp
#include "gauss_bonnet.hpp"
auto defect = gauss_bonnet_sum(mesh, maps); // Σ(2π Θᵥ)
auto chi = euler_characteristic(mesh); // free function, not a member
EXPECT_NEAR(defect, 2.0 * M_PI * chi, 1e-10);
```
Covered by: `cgal.GaussBonnet.*` tests in `test_phase6.cpp`.
---
## 2 — Period matrix: fundamental domain invariants
**Theorem (SL(2,)-reduction).** Every lattice τ ∈ has a unique representative
in the standard fundamental domain
```
F = { τ ∈ : |τ| ≥ 1, |Re(τ)| ≤ 1/2, Im(τ) > 0 }
```
**After calling `compute_period_matrix(hol)`, the returned τ must satisfy:**
| Condition | Invariant |
|---|---|
| `pd.tau_reduced.imag() > 0` | τ lies in the upper half-plane |
| `std::abs(pd.tau_reduced) >= 1.0 - 1e-10` | τ outside unit disk |
| `std::abs(pd.tau_reduced.real()) <= 0.5 + 1e-10` | τ in vertical strip |
These three conditions hold for **any** closed genus-1 triangulated surface
processed through Euclidean uniformization — they are topology, not geometry.
Covered by: `cgal.PeriodMatrix.TauInFundamentalDomain_*` tests in `test_phase7.cpp`.
---
## 3 — Torus of revolution: conformal modulus
**Setup.** The bundled torus meshes are surfaces of **revolution** (a tube of
minor radius `r` swept around a major circle of radius `R > r`), *not* abstract
square/hexagonal flat tori:
| mesh | major R | minor r | cross section |
|---|---|---|---|
| `torus_4x4.off` | 2 | 1 | square (4-gon) |
| `torus_hex_6x6.off` | 3 | 1 | hexagon (6-gon) |
| `torus_8x8.off` | 3 | 1 | octagon (8-gon) |
**Expected.** The induced metric `ds² = (R + r cos φ)² dθ² + r² dφ²` is made
flat by the conformal change of variable `dψ = r/(R + r cos φ) dφ`. The
ψ-period is `∮ r/(R + r cos φ) dφ = 2πr/√(R²r²)`, so the flat torus is the
rectangular lattice `2π· × (2πr/√(R²r²))·ℤ`. Its period ratio, reduced so
that `|τ| ≥ 1`, is therefore **purely imaginary**:
```
Re(τ) = 0 (meridian ⟂ longitude reflection symmetry)
Im(τ) = √(R² r²) / r (reduced conformal modulus)
```
giving the analytic targets
| mesh | analytic τ = i·√(R²r²)/r |
|---|---|
| `torus_4x4.off` | i·√3 ≈ **1.732 i** |
| `torus_hex_6x6.off` | i·√8 ≈ **2.828 i** |
| `torus_8x8.off` | i·√8 ≈ **2.828 i** |
> A common pitfall is to expect τ = i (or the order-6 fixed point e^{iπ/3})
> from the 4-fold (6-fold) symmetry. That reasoning is **wrong** here: a torus
> of revolution's rotational symmetry is a rotation about the axis, which acts
> on the surface as a *fixed-point-free* translation along the longitude — it is
> not an order-4 conformal automorphism with a fixed point, so it does not pin τ
> to a modular fixed point. The correct invariant is the rectangular modulus
> above.
**Reproduced end-to-end** (solve → `compute_cut_graph``euclidean_layout(…,
&cg, &hol)``compute_period_matrix`). The coarse polygonal cross sections
approximate the circular modulus from above; the gap shrinks as the cross
section gains sides:
| mesh | analytic | computed τ | rel. error |
|---|---|---|---|
| `torus_4x4.off` | 1.732 i | ≈ 1.79 i | ~3 % (4-gon) |
| `torus_hex_6x6.off` | 2.828 i | ≈ 2.85 i | ~0.8 % (6-gon) |
| `torus_8x8.off` | 2.828 i | ≈ 2.84 i | ~0.4 % (8-gon) |
Covered by: `cgal.HolonomyEndToEnd.Torus*_TauMatchesRevolutionModulus` in
`test_phase7.cpp`.
**Conformal flattening** of the torus is wired end-to-end and converges in a
handful of Newton steps (34 on the bundled meshes):
```bash
./bin/conformallab_core -i code/data/off/torus_4x4.off -g euclidean -v -o lay.off
# → topology: closed, free DOFs=15, genus=1
# → Euclidean: converged=yes iter=3 |grad|_inf≈1e-12
```
Equivalent C++ (current API — note `newton_euclidean` takes an `x0` vector and
the DOF indices must be assigned first):
```cpp
ConformalMesh mesh = load_mesh("code/data/off/torus_4x4.off");
EuclideanMaps maps = setup_euclidean_maps(mesh); // Θ_v = 2π (flat target)
compute_euclidean_lambda0_from_mesh(mesh, maps);
// Pin one vertex (scale gauge), free the rest; make the flat target
// Gauss-Bonnet-consistent.
int idx = 0; bool pinned = false;
for (auto v : mesh.vertices())
maps.v_idx[v] = (!pinned ? (pinned = true, -1) : idx++);
enforce_gauss_bonnet(mesh, maps);
std::vector<double> x0(idx, 0.0);
auto res = newton_euclidean(mesh, x0, maps); // converged after ~3 iters
```
The CLI reports the period ratio for genus-1 inputs, e.g.
```bash
./bin/conformallab_core -i code/data/off/torus_4x4.off -g euclidean -v
# → period ratio τ = 0.000000 + 1.793... i (genus 1, reduced to fundamental domain)
```
---
## 4 — Higher-resolution cross sections converge to the circular modulus
`torus_hex_6x6.off` (R=3, r=1, hexagon) and `torus_8x8.off` (R=3, r=1, octagon)
share the same analytic modulus `i·√8 ≈ 2.828 i` (see §3). Because both
approximate the *circular* tube cross section, the computed τ approaches the
analytic value as the polygon gains sides: the 8-gon (≈ 2.84 i, ~0.4 %) is
closer than the 6-gon (≈ 2.85 i, ~0.8 %), which is closer than the 4-gon of
`torus_4x4.off` (~3 %). All three are asserted in
`cgal.HolonomyEndToEnd.Torus*_TauMatchesRevolutionModulus`.
---
## 5 — Newton convergence rate
**Theorem.** Because the Euclidean and hyper-ideal energies are strictly convex
(after gauge-fixing), Newton's method converges quadratically near the optimum.
**Expected:** for any mesh with up to a few hundred faces, Newton converges in
**fewer than 30 iterations** starting from u = 0.
```cpp
std::vector<double> x0(n_dofs, 0.0);
auto res = newton_euclidean(mesh, x0, maps);
EXPECT_LT(res.iterations, 30);
EXPECT_LT(res.grad_inf_norm, 1e-8); // field is grad_inf_norm, default tol 1e-8
```
Covered by: `cgal.NewtonSolver.*` (Euclidean ×3, Spherical ×4, HyperIdeal ×4)
and `cgal.NewtonPhase9a.*` (the two circle-packing solvers).
---
## 6 — Gradient check (finite differences)
For each functional F(u), the gradient G = ∂F/∂u is verified by:
```
|G(u)ᵢ (F(u + εeᵢ) F(u εeᵢ)) / (2ε)| < 1e-6
```
with ε = 1e-5. This check is run **inside the test suite** for all three
geometries (Euclidean, Spherical, HyperIdeal) at u = 0 and at random u.
Relevant test suites:
```
cgal.EuclideanFunctional.GradientCheck_*
cgal.SphericalFunctional.GradientCheck_*
cgal.HyperIdealFunctional.GradientCheck_*
```
A failing gradient check means the energy and its derivative are inconsistent —
the Newton solver will converge to the wrong point.
---
## 7 — Holonomy composition (Möbius maps)
For a closed surface, the composition of holonomies around any contractible cycle
must be the identity. In genus 1 with a single handle:
```
T₁ · T₂ · T₁⁻¹ · T₂⁻¹ = Id (commutator = Id for a torus)
```
because π₁(T²) = × is abelian.
For genus g ≥ 2, the fundamental group is non-abelian and this check does not hold,
but the representation ρ: π₁(Σ_g) → SU(1,1) must still satisfy the relation
```
[T₁, T₂] · [T₃, T₄] · … = Id (product of g commutators = Id)
```
The Möbius arithmetic these checks rely on (identity, inverse, composition,
`from_three`) is verified in `test_phase7.cpp` under `cgal.MobiusMap.*`. The
Euclidean end-to-end holonomy extraction is now validated against the analytic
torus-of-revolution modulus (§3, `cgal.HolonomyEndToEnd.*`). A standalone
`cgal.HolonomyData.*` commutator-closes-up suite for the *hyperbolic* (Möbius)
holonomy is still future work.
---
## 9 — Cross-validation with geometry-central *(optional / hypothetical)*
> **Note:** This section describes a possible external cross-validation that is not
> a prerequisite for the correctness of the implementation.
> It is of interest because geometry-central implements the same mathematical core
> (Gillespie, Springborn, Crane — SIGGRAPH 2021, building on
> Springborn 2020), but with a different algorithmic strategy
> (Ptolemaic flips + intrinsic triangulations instead of Newton on the
> original triangulation).
### Which outputs are comparable?
| Output | conformallab++ | geometry-central | Comparable? |
|---|---|---|---|
| u-vector (scale parameters) | `res.x` | `u` after Yamabe flow | ✓ after normalisation |
| UV coordinates | `layout.uv[v]` | conformal parameterisation | ✓ up to Möbius transformation |
| Gauss-Bonnet deficit | `gauss_bonnet_sum()` | implicit via curvature flow | ✓ (analytically identical) |
| Number of Newton iterations | `res.iterations` | Yamabe steps | ~ (different algorithm) |
| Period matrix τ | `pd.tau_reduced` | **not available** | ✗ |
| Möbius holonomy | `hol.T_a, T_b` | **not available** | ✗ |
### Normalisation alignment
The u-vector in conformallab++ has one degree of freedom (global additive constant —
gauge freedom after pin-fixing). geometry-central may use a different convention.
Normalise before comparing:
```cpp
// conformallab++: centre u
double mean_u = std::accumulate(x.begin(), x.end(), 0.0) / x.size();
std::vector<double> x_norm(x.size());
for (int i = 0; i < x.size(); ++i) x_norm[i] = x[i] - mean_u;
// Then compare with the geometry-central u-vector (also centred):
// max|x_norm[i] - gc_u[i]| < 1e-8 → identical convergence point
```
### When is the comparison useful?
| Point in time | What is possible |
|---|---|
| **Now (Phase 7)** | Manual comparison using the same `.off`/`.obj` test meshes |
| **After Phase 8** | Automated comparison script (Python or separate C++ binary) |
| **Phase 10 (research)** | Algorithm comparison: Newton vs. Ptolemaic flips on difficult meshes |
### Connection to the literature
The Springborn 2020 paper ("Ideal Hyperbolic Polyhedra and Discrete Uniformization")
is **already implemented in conformallab++** — it is the mathematical foundation
for the HyperIdeal geometry mode (Phase 2/3). The geometry-central implementation
is based on the extension by Gillespie, Springborn & Crane (2021), which uses the
same variational principle of BobenkoSpringborn 2004 but additionally applies
Ptolemaic flips to improve the triangulation during optimisation — an idea not yet
implemented in conformallab++ (→ GC-2 in the phase roadmap).
---
## 8 — Checklist for an independent reviewer
Run these in order to validate the implementation:
- [ ] `ctest --test-dir build -R cgal --output-on-failure` → all pass, 0 skipped (count: `doc/api/tests.md`)
- [ ] `cgal.GaussBonnet.*` all pass → topology is correctly read from mesh
- [ ] `cgal.EuclideanFunctional.GradientCheck_*` pass → energy = integral of gradient
- [ ] `cgal.PeriodMatrix.*` pass → SL(2,) reduction correct (on prescribed holonomy)
- [ ] `cgal.MobiusMap.*` pass → Möbius arithmetic (identity, inverse, compose) correct
All of the above are **deterministic, analytic tests** — no mesh loading, no
file I/O, no floating-point non-determinism beyond standard IEEE-754.