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>
81 lines
4.7 KiB
Markdown
81 lines
4.7 KiB
Markdown
# 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
|
||
|
||
```cpp
|
||
// 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.
|
||
|
||
```cpp
|
||
// 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).
|