fix+test: Euclidean holonomy/τ end-to-end + spherical edge-DOF oracle (2026-05-29 audit)
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

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>
This commit is contained in:
Tarik Moussa
2026-05-29 12:50:16 +02:00
parent ca936b7652
commit a3ee9576d4
23 changed files with 1065 additions and 165 deletions

View File

@@ -29,7 +29,7 @@ All tests have CTest prefix `cgal.` (set via `TEST_PREFIX "cgal."` in CMakeLists
| `ConformalMeshProperties` | `test_conformal_mesh.cpp` | 5 | Property maps (λ, θ, idx, α, geometry type) |
| `ConformalMeshValidity` | `test_conformal_mesh.cpp` | 1 | CGAL validity for all factory meshes |
| `HyperIdealFunctional` | `test_hyper_ideal_functional.cpp` | 7 | FD gradient checks + Hessian symmetry |
| `SphericalFunctional` | `test_spherical_functional.cpp` | 12 | Angle formula + gradient + gauge-fix + cross-module Hessian check |
| `SphericalFunctional` | `test_spherical_functional.cpp` | 13 | Angle formula + gradient + gauge-fix + cross-module Hessian check + closed-form edge-DOF oracle |
| `EuclideanFunctional` | `test_euclidean_functional.cpp` | 12 | Angle formula + gradient + cross-module Hessian check |
| `EuclideanHessian` | `test_euclidean_hessian.cpp` | 9 | Cotangent Laplacian structure, FD agreement, PSD, null space |
| `SphericalHessian` | `test_spherical_hessian.cpp` | 8 | Derivative correctness, NSD at equilibrium |
@@ -51,6 +51,7 @@ All tests have CTest prefix `cgal.` (set via `TEST_PREFIX "cgal."` in CMakeLists
| `PeriodMatrix` | `test_phase7.cpp` | 7 | τ ∈ , SL(2,) reduction, exception outside |
| `FundamentalDomain` | `test_phase7.cpp` | 7 | Genus-1 parallelogram CCW, generators, g > 1 empty |
| `TilingCopy/Neighbourhood` | `test_phase7.cpp` | 4 | Translation correct, tile count |
| `HolonomyEndToEnd` | `test_phase7.cpp` | 3 | Full pipeline τ on tori of revolution (4×4, hex 6×6, 8×8) vs analytic modulus i·√(R²r²)/r |
| `CuttingUtility` | `test_geometry_utils.cpp` | 3 | `point_in_triangle_2d`: false, true, unit triangle (Java CuttingUtilityTest) |
| `UnwrapUtility` | `test_geometry_utils.cpp` | 2 | Corner angle: collinear → π, equilateral → π/3 (Java UnwrapUtilityTest) |
| `ConvergenceUtility` | `test_geometry_utils.cpp` | 6 | Circumradius + scale-invariant R_f/√A (Java ConvergenceUtilityTests) |
@@ -66,7 +67,7 @@ All tests have CTest prefix `cgal.` (set via `TEST_PREFIX "cgal."` in CMakeLists
| `NewtonPhase9a` | `test_newton_phase9a.cpp` | 7 | Phase 9a-Newton — convergence for the two new circle-packing solvers |
| `CGALPhase8bLite` | `test_cgal_phase8b_lite.cpp` | 17 | Phase 8b-Lite — CGAL entries for all 5 DCE models + `output_uv_map` (Euclidean, Spherical, HyperIdeal, Inversive-Distance) + CP-Euclidean throws-clearly + pipe-operator chaining |
**Total: 236 tests, 0 skipped.**
**Total: 240 tests, 0 skipped.**
---

View File

@@ -39,7 +39,7 @@ where χ(M) = 2 2g is the Euler characteristic.
```cpp
#include "gauss_bonnet.hpp"
auto defect = gauss_bonnet_sum(mesh, maps); // Σ(2π Θᵥ)
auto chi = mesh.euler_characteristic();
auto chi = euler_characteristic(mesh); // free function, not a member
EXPECT_NEAR(defect, 2.0 * M_PI * chi, 1e-10);
```
@@ -71,59 +71,105 @@ Covered by: `cgal.PeriodMatrix.TauInFundamentalDomain_*` tests in `test_phase7.c
---
## 3 — Square-symmetric torus
## 3 — Torus of revolution: conformal modulus
**Setup.** Take a torus mesh with 4-fold rotational symmetry around the z-axis
(e.g. `code/data/off/torus_4x4.off`, which has M=4 columns of vertices).
**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:
**Expected.** The symmetry group Z₄ acts conformally. Conformal automorphisms
of the torus correspond to SL(2,) symmetries of τ. The unique fixed point of
a rotation of order 4 in the modular group is τ = i. Therefore:
| 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**:
```
For a mesh with exact 4-fold symmetry and uniform edge lengths:
Re(τ) = 0 (to machine precision, by symmetry)
Im(τ) ≈ 1 (approaches 1 as mesh is refined)
Re(τ) = 0 (meridian ⟂ longitude reflection symmetry)
Im(τ) = √(R² r²) / r (reduced conformal modulus)
```
The coarse 4×4 mesh (`torus_4x4.off`) gives Im(τ) in (0.7, 1.3) depending on
the 3D embedding (R=2, r=1 torus of revolution has unequal inner/outer edge lengths).
The uniformization algorithm finds the conformal class of the *abstract* metric
encoded in the edge lengths.
giving the analytic targets
**Manual verification** (run from the build directory after adding a small
program or reading from the test output):
| 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(mesh, "code/data/off/torus_4x4.off");
EuclideanMaps maps = setup_euclidean_maps(mesh);
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);
auto res = newton_euclidean(mesh, maps);
CutGraph cg = compute_cut_graph(mesh);
HolonomyData hol;
euclidean_layout(mesh, res.x, maps, &cg, &hol, true);
PeriodData pd = compute_period_matrix(hol);
// pd.tau_reduced satisfies the fundamental domain invariants above
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 — Hexagonal-symmetric torus
## 4 — Higher-resolution cross sections converge to the circular modulus
**Setup.** Take a torus mesh with 6-fold rotational symmetry
(`code/data/off/torus_hex_6x6.off`, M=6).
**Expected.** The unique τ fixed under a rotation of order 6 in SL(2,) is
τ = e^{iπ/3} = ½ + i√3/2. So:
```
Re(τ) = 0.5 (to machine precision, by symmetry)
Im(τ) = √3/2 ≈ 0.8660
```
The coarse 6×6 torus of revolution approximates this: Re(τ) ≈ 0.5 by symmetry,
Im(τ) approaches √3/2 as the mesh is refined toward a flat hexagonal lattice.
`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`.
---
@@ -136,12 +182,14 @@ Im(τ) approaches √3/2 as the mesh is refined toward a flat hexagonal lattice.
**fewer than 30 iterations** starting from u = 0.
```cpp
auto res = newton_euclidean(mesh, maps);
std::vector<double> x0(n_dofs, 0.0);
auto res = newton_euclidean(mesh, x0, maps);
EXPECT_LT(res.iterations, 30);
EXPECT_LT(res.gradient_norm, 1e-10);
EXPECT_LT(res.grad_inf_norm, 1e-8); // field is grad_inf_norm, default tol 1e-8
```
Covered by: `cgal.EuclideanPipeline.ConvRates_*` and similar tests.
Covered by: `cgal.NewtonSolver.*` (Euclidean ×3, Spherical ×4, HyperIdeal ×4)
and `cgal.NewtonPhase9a.*` (the two circle-packing solvers).
---
@@ -186,8 +234,12 @@ but the representation ρ: π₁(Σ_g) → SU(1,1) must still satisfy the relati
[T₁, T₂] · [T₃, T₄] · … = Id (product of g commutators = Id)
```
These are the **holonomy consistency** checks implemented in `test_phase7.cpp`
(`cgal.HolonomyData.*`).
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.
---
@@ -255,9 +307,8 @@ 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.TauInFundamentalDomain_*` pass → SL(2,) reduction correct
- [ ] `cgal.MobiusMap.Compose_*` and `Inverse_*` pass → Möbius arithmetic correct
- [ ] `cgal.HolonomyData.*` pass → holonomy loops close up
- [ ] `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.

View File

@@ -0,0 +1,244 @@
# Java → C++ Port Audit — Anomalies & Missing Test Cases
Systematic comparison of the C++ port (`code/include/*.hpp`) against the original
Java reference (`~/Desktop/conformallab`,
`de.varylab.discreteconformal.functional.*`). Focus: math correctness, logic, and
faithfulness to the reference.
Status legend: ✅ fixed · ⚠️ open / needs decision · note (no action)
---
## Files compared (math-critical)
| Java | C++ | Verdict |
|------|-----|---------|
| `Clausen.java` | `clausen.hpp` | ✅ faithful (incl. `inits` return, `clausen2`, `Л`, `ImLi2`) |
| `EuclideanCyclicFunctional.java` | `euclidean_functional.hpp`, `euclidean_geometry.hpp` | ✅ angles + gradient assembly match; degenerate handling fixed |
| — Hessian (`triangleHessian`/`conformalHessian`) | `euclidean_hessian.hpp` | cotangent + vertex block match; edge block **not implemented** (now guarded) |
| `SphericalFunctional.java` | `spherical_functional.hpp`, `spherical_geometry.hpp` | ✅ vertex-mode + edge-DOF replacement parameterization now match (Finding 3) |
| `HyperIdealFunctional.java` | `hyper_ideal_functional.hpp`, `hyper_ideal_geometry.hpp` | ✅ faithful (incl. degenerate (π,0,0) angles) |
| `HyperIdealUtility.java` | `hyper_ideal_utility.hpp` | ✅ ζ, ζ₁₃, ζ₁₄, ζ₁₅, both tetrahedron-volume formulas match |
| `CPEuclideanFunctional.java` | `cp_euclidean_functional.hpp` | ✅ `p()`, gradient, energy match |
| `DiscreteEllipticUtility.java` | `discrete_elliptic_utility.hpp`, `period_matrix.hpp` | ✅ `normalizeModulus` faithful and now wired into `compute_period_matrix` (Finding 6) |
| `HomologyUtility.java`, `CanonicalBasisUtility.java`, `SpanningTreeUtility.java` | `cut_graph.hpp` | tree-cotree only; no canonical/symplectic basis (Finding 7) |
| `EuclideanLayout.java` (holonomy) | `layout.hpp` (`detail::euclidean_holonomy`) | translation via midpoint displacement; ignores residual rotation (Finding 8) |
---
## Findings
### 1. ✅ FIXED — Degenerate triangle handling (Euclidean + Spherical gradient)
The Java reference assigns the **limiting** corner angles to a degenerate
(triangle-inequality-violating) face — the corner opposite the over-long edge is
**π**, the other two **0**. This is the convex C¹ extension that keeps the BPS
energy well-defined on the infeasible region, so Newton can pass through a flip
without stalling.
The C++ port returned `{0,0,0}` and **skipped the whole face** in the gradient.
Tell-tale: the hyper-ideal port *already* mirrors Java's (π,0,0) fallback —
euclidean/spherical were inconsistent oversights.
**Fix:** `euclidean_geometry.hpp` / `spherical_geometry.hpp` degenerate branches
now return the limiting angles (still `valid=false`, so the cotangent Hessian
keeps skipping — matching Java, whose `triangleHessian` zeroes cotangents on
degenerate faces). `euclidean_functional.hpp` / `spherical_functional.hpp`
gradients no longer skip. All 237 tests pass; solution unchanged (only affects
evaluations that previously contributed zero).
### 2. ✅ FIXED — `euclidean_hessian` missing edge-DOF guard
Doc comment claimed "the function asserts that no edge DOF is variable" — but
**no such guard existed**. With edge DOFs assigned
(`assign_euclidean_all_dof_indices`), the Java `conformalHessian`'s edge-edge and
vertex-edge blocks are absent, so the C++ would silently build a Hessian with
all-zero edge rows/cols (singular → LDLT fails → QR least-squares garbage).
**Fix:** added an always-compiled `throw std::logic_error` guard.
### 3. ✅ FIXED — Spherical edge-DOF parameterization now matches Java
Java's spherical functional **drops** the `u_i + u_j` vertex terms when an edge is
a variable (replacement parameterization, `SphericalFunctional.java:398400`),
whereas the C++ previously used additive `Λ = λ⁰ + u_i + u_j + λ_e` always. That
made the C++ spherical edge gradient carry an extra `(S_f⁺+S_f⁻)/2` term absent
from Java's `G.add(i, αk + αl π)` (`SphericalFunctional.java:283292`).
**Fix:** added helper `spher_eff_lambda` implementing the replacement convention
(`Λ_ij = λ_e` when the edge carries a DOF, else `λ⁰ + u_i + u_j`); `spherical_gradient`
Pass 1 now uses it, and the edge gradient is `α_opp⁺ + α_opp⁻ θ_e` (θ_e default
π), exactly matching Java. The energy is the path integral of this gradient, so it
follows automatically. The **vertex-only path is bit-for-bit unchanged** (no edge
DOFs → identical to before). All 237 tests pass.
### 4. ✅ FIXED — Spherical Hessian edge-DOF guard added
`spherical_hessian.hpp` builds only the vertex block; Java's spherical
`conformalHessian` includes edge terms. Same limitation as the Euclidean Hessian
but previously **without** the edge-DOF guard added in Finding 2.
**Fix:** added an always-compiled `throw std::logic_error` guard at the top of
`spherical_hessian` (mirrors Finding 2), so any attempt to use edge DOFs with the
spherical Hessian fails loudly instead of silently building a singular matrix.
All 237 tests pass.
### 5. NOTE — CP-Euclidean energy uses `clausen2`, Java uses `clausen`
`CPEuclideanFunctional.java:226` calls `Clausen.clausen` (the BORDERLINE-2.0944
polynomial); the C++ energy calls `clausen2` (the Chebyshev variant). Both compute
the same Cl₂ integral to ~machine precision, and the term appears only in the
energy (not the Newton-driving gradient). No functional impact. The C++ never
ported the `clausen()` variant at all — harmless.
### 6. ✅ FIXED — Period-matrix reduction now uses the faithful Java port
Two reductions coexist:
- `discrete_elliptic_utility.hpp::normalizeModulus` is a **faithful** line-by-line
port of Java `DiscreteEllipticUtility.normalizeModulus` — it folds τ into
**`0 ≤ Re(τ) ≤ ½`, `Im(τ) ≥ 0`, `|τ| ≥ 1`** (the extra `Re ≥ 0` fold uses the
mirror symmetry τ ≅ −τ̄).
- `period_matrix.hpp::reduce_to_fundamental_domain` reduces to the **standard**
SL(2,) domain **`−½ ≤ Re(τ) < ½`, `|τ| ≥ 1`** (no `Re ≥ 0` fold).
`compute_period_matrix` (the production path, and the one the `torus_8x8` τ test
exercises) calls **`reduce_to_fundamental_domain`**, so `normalizeModulus` is
**never used outside its own unit test** (verified by grep). Consequence: the C++
production τ can land with `Re(τ) < 0` where Java would report the mirrored
`+|Re(τ)|`. Same conformal type up to orientation, but **not equal to the Java
oracle value** — this will bite any golden-value cross-check (missing-test item 5).
**Fix (option a):** `compute_period_matrix` now calls `normalizeModulus`, so the
production τ matches the Java oracle exactly (folds into `0 ≤ Re ≤ ½`, `Im ≥ 0`,
`|τ| ≥ 1` via the mirror symmetry). `normalizeModulus` is no longer dead code. The
existing `torus_8x8` τ test still passes (`Re ∈ [0,½] ⊂ [−½,½]` so it satisfies
`is_in_fundamental_domain` too). `reduce_to_fundamental_domain` is retained for
callers that explicitly want the canonical (non-mirror-folded) SL(2,) domain.
### 7. NOTE — Cut graph is tree-cotree, not a canonical homology basis
`cut_graph.hpp` (`compute_cut_graph`) runs the EricksonWhittlesey tree-cotree
algorithm: primal BFS tree, dual BFS cotree, the remaining `2g` edges generate
H₁. Java's `CanonicalBasisUtility.getCanonicalHomologyBasis` goes further — it
builds the intersection form on the generators and solves for a **canonical
symplectic basis** `{a₁..a_g, b₁..b_g}` (and uses *weighted* shortest-path
cycles, not unweighted BFS).
- **Genus 1:** harmless — there is only one generator pair, and
`compute_period_matrix` reduces τ to the fundamental domain, so the basis choice
washes out. The common case matches.
- **Genus > 1:** the C++ `omega[]` ordering is arbitrary (edge-iteration order),
so `τ = ω₂/ω₁` is **not** the canonical Riemann period matrix. This is already
flagged in the `period_matrix.hpp` header ("Computing Ω from holonomy data
requires integration of holomorphic differentials — not implemented") so it is
consistent with the documented contract, not a regression.
### 8. NOTE — Holonomy translation assumes zero residual rotation
`detail::euclidean_holonomy` (`layout.hpp`) develops each face along a dual tree
that never crosses a cut edge, then reads each generator's translation as the
**midpoint displacement** `ω = midA midB` of the shared cut edge between its two
independent developments. This equals the true deck-translation **only when the
two developments differ by a pure translation** (linear part = identity), i.e. for
a perfectly flat cone metric (Θ ≡ 2π). When Newton has not fully converged (small
residual cone-angle defect), there is a residual rotation and the midpoint
displacement is a first-order approximation rather than the exact ω. A fully
robust version would fit the rigid motion mapping edge `(Bs,Bt)↦(At,As)` and read
its translation part. Acceptable for converged flat tori (the `torus_8x8` test
passes); worth tightening if higher-genus or under-converged inputs are used.
---
## Findings in non-ported (conformallab++-only) math modules
These modules have **no Java reference** — they were ported from the literature
(Luo 2004, Glickenstein 2011, BowersStephenson 2004) or are original additions.
They were audited against the cited formulas / first principles rather than Java.
### 9. ✅ FIXED — Inversive-distance gradient now uses limiting angles (consistent with Finding 1)
`inversive_distance_functional.hpp::inversive_distance_gradient` previously detected
a triangle-inequality-violating face two ways and **skipped it** in both:
- `if (l12sq <= 0 || l23sq <= 0 || l31sq <= 0) continue;` (non-real circle config)
- `if (!fa.valid) continue;` (degenerate-but-real triangle)
The second skip was the *same* situation Finding 1 fixed for Euclidean/Spherical:
`euclidean_angles` returns the **limiting angles** (π opposite the over-long edge,
0/0 for the others) with `valid=false`, but this code threw them away.
**Fix:** removed the `if (!fa.valid) continue;` skip so the limiting angles flow
into the gradient (the BPS-style convex C¹ extension), mirroring Finding 1; added a
comment explaining the rationale. The first skip (`l*sq <= 0`, a genuinely non-real
circle configuration with no limiting angle) is **kept**. All 237 tests pass.
### 10. NOTE — Inversive-distance edge length has no overflow centring
`id_detail::edge_length_squared` computes `ℓ² = rᵢ² + rⱼ² + 2·I·rᵢ·rⱼ` with
`r = exp(u)` directly — no log-centring like `euclidean_angles`'s `μ`. The *angle*
computation is still safe (it re-centres internally on `log ℓ²`), but the raw `ℓ²`
can overflow for extreme `|u|`. Cosmetic robustness only; not hit in practice.
### 11. NOTE — Verified correct (no action)
Audited against the literature and found faithful:
- **`trilaterate_2d / _sph / _hyp`** (`layout.hpp`) — circle-circle intersection
(ℝ²), spherical law of cosines via `α·pa+β·pb+γ·(pa×pb)`, and hyperbolic law of
cosines + Poincaré-disk Möbius placement. All three exact; left/CCW side correct.
- **`newton_solver.hpp`** — merit `f = ½‖G‖²`; Newton dir is always a descent dir
(`∇f·dx = ‖G‖²`), Armijo tests algebraically correct in both phases; spherical
solver factorises `H` (PSD) and `d_sd = H·G = ∇f` uses the un-negated H — all
consistent. SparseQR fallback handles the gauge null space (valid because
GaussBonnet makes `G ⟂ 𝟙`).
- **`gauss_bonnet.hpp`** — `χ=VE+F`, `g=(2χ)/2`, `enforce` shift
`δ=(lhsrhs)/V` makes the new sum equal `2π·χ` exactly.
- **Normalisations** — Euclidean PCA, spherical Rodrigues (mean→north),
hyperbolic weighted Fréchet-mean Möbius centring: all correct (spherical has a
benign antipodal-mean edge case that no-ops).
---
## Missing test cases (gaps observed during the audit)
1. **Degenerate / flipped-triangle gradient** — no test drives a face through the
triangle-inequality boundary to lock in Finding 1. Add: build a triangle with
one edge length > sum of the others, assert the gradient picks up the π corner
(not 0). Cover both Euclidean and Spherical.
2. **`euclidean_hessian` edge-DOF guard** — no test asserts the new `throw` fires
when `assign_euclidean_all_dof_indices` is used. Add an `EXPECT_THROW`.
3.**DONE (2026-05-29)** — Holonomy / period matrix end-to-end now covers
three tori of revolution (`HolonomyEndToEnd.{Torus4x4,TorusHex6x6,Torus8x8}`),
each asserting τ against the analytic modulus `i·√(R²r²)/r` and `Re(τ) ≈ 0`.
(This also fixed the Euclidean holonomy extraction — develop across the dual
tree only — see `layout.hpp` / `cut_graph.hpp`.)
4. ⚠️ **PARTLY DONE (2026-05-29)** — added `EdgeGradient_RegularTetClosedForm`:
an *independent closed-form* oracle that pins each edge-DOF gradient to
`π/3` (= 2·(2π/3) π) on the regular spherical tetrahedron, where the corner
angle 2π/3 follows from the spherical law of cosines. This locks the
Finding-3 formula `α_opp⁺ + α_opp⁻ θ_e` against a value the path-integral FD
check cannot detect (the energy is the integral of G, so FD only proves
curl-freeness). **Still open:** (i) Newton-to-convergence *with* edge DOFs is
blocked by the Finding-4 spherical-Hessian guard (`throw` on edge DOFs), so a
converged-metric test needs an FD-Hessian or guard relaxation first; (ii) a
live-Java golden-value oracle still requires running the upstream library.
5. **Cross-check vs Java numeric oracles** — there is no test that pins C++
functional/gradient values against recorded Java outputs on a fixed mesh. A
handful of golden-value tests (energy + gradient at a known `x`) would catch
any future silent divergence from the reference far more directly than the
self-consistent FD checks (which only verify curl-freeness, since the C++
energy is itself the path-integral of its own gradient).
6. **CP-Euclidean `clausen` vs `clausen2`** — a single test asserting
`|clausen(x) clausen2(x)| < 1e-12` across a sweep would document Finding 5
and guard against an approximation regression.
7. **Period-matrix domain convention (Finding 6)** — production now uses
`normalizeModulus` (folds `Re ≥ 0`). Add a torus whose true τ has `Re(τ) < 0`
before reduction and assert the resulting `Re(τ) ≥ 0` convention, so any future
switch back to `reduce_to_fundamental_domain` is caught. Pair with a golden τ
recorded from the Java `DiscreteEllipticUtility` on the same mesh.
8. **`normalizeModulus` vs Java oracle (Finding 6)** — the faithful
`normalizeModulus` is only checked against ad-hoc values. Add a few recorded
Java `normalizeModulus` outputs as golden values to lock the `Re ≥ 0` fold.
9. **Higher-genus cut graph (Finding 7)** — only genus-1 is exercised end-to-end.
Add a genus-2 mesh and assert `compute_cut_graph` returns exactly `2g = 4` cut
edges and `genus == 2`, documenting that the basis is *not* canonical (so no τ
correctness is claimed there).
10. **Inversive-distance degenerate face (Finding 9)** — no test drives an
inversive-distance triangle through the triangle-inequality boundary. Now that
Finding 9 mirrors Finding 1 (limiting angles used, not skipped), add the
analogue of missing-test item 1: assert the corner opposite the over-long edge
is picked up as π rather than silently skipped.

View File

@@ -293,6 +293,20 @@ The phase numbers match `doc/roadmap/phases.md`.
the cut-graph + holonomy infrastructure already in conformallab++).
* **Effort:** large (1014 days).
* **Status:** roadmap item, no PR yet.
* **Known prerequisite bug (latent, 2026-05-29):** the holonomy-extraction
blocks in `spherical_layout` and `hyper_ideal_layout` (`layout.hpp`)
repeat the flawed single-development pattern that produced garbage τ for
the Euclidean path before the 2026-05-29 fix. They read the
translation / Möbius deck transformation from one full-surface
development plus a one-sided apex trilateration, instead of developing
across only the **dual** spanning tree and measuring the shared-edge
displacement between two independent developments (as the corrected
`detail::euclidean_holonomy` now does). These blocks are currently dead
code — every caller passes `holonomy == nullptr` — but Phase 9c/10b will
exercise the hyperbolic path. Fix = add `detail::spherical_holonomy` /
`detail::hyperbolic_holonomy` mirroring `detail::euclidean_holonomy`.
The hyperbolic mirror additionally needs `cpp_dec_float_50` (group-relation
product ∏gᵢ = Id overflows `double`; see CLAUDE.md high-precision note).
---