Files
ConformalLabpp/doc/api/contracts.md
Tarik Moussa b8d6e23adf docs(contracts): fix three errors in processing-unit contract table (U3)
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>
2026-05-31 01:44:36 +02:00

81 lines
4.7 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.

# 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.
---
## GaussBonnet — the most common source of failure for Euclidean and Spherical
Prescribing angles that violate GaussBonnet 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 GaussBonnet 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 GaussBonnet check (the identity does not hold for meshes with boundary).