Finding-U3 from doc/reviewer/usability-audit-2026-05-31.md.
Three concrete errors corrected:
1. check_gauss_bonnet() row (was: implies all map types work)
→ now: 'EuclideanMaps/SphericalMaps only' — HyperIdealMaps overload
is = delete (compile error since external-audit Finding-B fix)
2. enforce_gauss_bonnet() row (same issue)
→ same restriction note added
3. newton_hyper_ideal() preconditions (was: 'lambda0 initialised')
→ WRONG: HyperIdeal computes lengths from b_v, a_e via zeta functions
internally; lambda0 plays no role. Correct precondition:
'no lambda0 needed · no GB pre-check'
→ Added explanation: strictly convex energy (Springborn 2020 Thm 1.3)
→ Newton converges for any targets without a pre-check
Gauss-Bonnet prose section also updated:
- Examples now show EuclideanMaps/SphericalMaps explicitly
- HyperIdeal compile-error note added with the correct hyperbolic
identity Σ(2π-Θ_v) - Area = 2π·χ and why no pre-check is needed
- Open-mesh note added (GB identity does not hold for boundary meshes)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.7 KiB
4.7 KiB
Processing Unit Contracts
Each pipeline unit has explicit preconditions and guarantees. A pipeline is valid if every unit's preconditions are satisfied by the outputs of all preceding units.
Contract table
| Unit | Preconditions | Provides |
|---|---|---|
load_mesh() |
Valid file path, supported format (OFF/OBJ/PLY) | Manifold, oriented, triangulated ConformalMesh |
setup_*_maps() |
Triangulated mesh | Initialised property maps; lambda0 zeroed |
compute_*_lambda0_from_mesh() |
setup_*_maps() called |
lambda0[e] set from 3-D edge lengths |
check_gauss_bonnet() |
theta_v[v] set · EuclideanMaps or SphericalMaps only |
Throws std::runtime_error if Σ(2π−Θᵥ) ≠ 2π·χ(M). Deleted overload for HyperIdealMaps — see note below. |
enforce_gauss_bonnet() |
theta_v[v] set · EuclideanMaps or SphericalMaps only |
Σ(2π−Θᵥ) = 2π·χ(M) guaranteed; theta_v modified. Deleted overload for HyperIdealMaps — see note below. |
newton_euclidean() |
GB satisfied · DOFs assigned · lambda0 initialised |
NewtonResult.x — converged scale factors; .converged, .iterations, .grad_inf_norm |
newton_spherical() |
GB satisfied · DOFs assigned · gauge vertex pinned | Same as above |
newton_hyper_ideal() |
assign_all_dof_indices() called · no lambda0 needed · no GB pre-check |
Same as above. HyperIdeal computes lengths internally from b_v, a_e (via ζ₁₃/ζ₁₄/ζ₁₅); lambda0 is irrelevant. Energy is strictly convex → Newton converges for any targets without a pre-check. |
compute_cut_graph() |
Closed, orientable, triangulated mesh | CutGraph.cut_edge_flags — 2g seam edges; .genus |
euclidean_layout() |
newton_euclidean() converged |
Layout2D.uv[v] · .halfedge_uv[h] · HolonomyData.translations |
spherical_layout() |
newton_spherical() converged |
Layout3D.xyz[v] |
hyper_ideal_layout() |
newton_hyper_ideal() converged |
Layout2D.uv[v] · .halfedge_uv[h] · HolonomyData.mobius_maps |
normalise_euclidean() |
layout.success == true |
Centroid at origin, major axis = x-axis |
normalise_hyperbolic() |
layout.success == true |
Weighted centroid at Poincaré disk origin |
normalise_spherical() |
layout.success == true |
Area centroid at north pole |
compute_period_matrix() |
hol.translations.size() >= 2 |
PeriodData.tau ∈ ℍ; optionally SL(2,ℤ)-reduced |
compute_fundamental_domain() |
HolonomyData (genus 1) |
CCW parallelogram {0, ω₁, ω₁+ω₂, ω₂} + edge identifications |
tiling_neighbourhood() |
Layout2D + HolonomyData (genus 1) |
(2m+1)·(2n+1) translated layout copies |
save_layout_off() |
Layout2D + mesh |
OFF file with UV coordinates |
save_result_json/xml() |
NewtonResult + optional Layout2D |
JSON/XML serialised result |
DOF assignment conventions
// Euclidean / Spherical: pin one vertex, assign sequential indices
maps.v_idx[first_vertex] = -1; // pinned: u_v = 0
int idx = 0;
for (auto v : remaining_vertices)
maps.v_idx[v] = idx++;
// HyperIdeal: all vertices and edges are free DOFs
int n_dofs = assign_all_dof_indices(mesh, maps);
// maps.v_idx[v] ∈ [0, n_v)
// maps.e_idx[e] ∈ [n_v, n_v + n_e)
-1 in v_idx or e_idx means the DOF is pinned (fixed at its lambda0 value).
The solver never writes to pinned DOFs. All indexing is 0-based and contiguous.
Gauss–Bonnet — the most common source of failure for Euclidean and Spherical
Prescribing angles that violate Gauss–Bonnet means no conformal factor can realise the target metric — the Newton solver will iterate indefinitely without converging.
// Option A: verify before solving — throws std::runtime_error on violation.
// Works with EuclideanMaps and SphericalMaps only.
check_gauss_bonnet(mesh, euclidean_maps); // or spherical_maps
check_gauss_bonnet(mesh, spherical_maps);
// Option B: auto-correct — redistributes the defect uniformly across all vertices.
enforce_gauss_bonnet(mesh, euclidean_maps);
enforce_gauss_bonnet(mesh, spherical_maps);
// ⚠ HyperIdealMaps: both functions have deleted overloads — calling them is a
// compile error. Reason: the correct hyperbolic Gauss–Bonnet identity includes
// a surface area term: Σ(2π−Θᵥ) − Area(M) = 2π·χ(M), not Σ(2π−Θᵥ) = 2π·χ(M).
// No pre-check is needed for HyperIdeal: the energy is strictly convex
// (Springborn 2020 Theorem 1.3), so newton_hyper_ideal converges for any targets.
The target violation is |Σ(2π−Θᵥ) − 2π·χ(M)| > tol. Default tolerance: 1e-10.
Applicable to closed meshes only — for open meshes, pin the boundary directly
and skip the Gauss–Bonnet check (the identity does not hold for meshes with boundary).