From 5d74a94b789b96f523679c9bfecb8aad673954d6 Mon Sep 17 00:00:00 2001 From: Tarik Moussa Date: Fri, 29 May 2026 23:55:16 +0200 Subject: [PATCH] =?UTF-8?q?test(euclidean):=20Tier-1=20circular-edge=20?= =?UTF-8?q?=CF=86=20cross-validation=20+=20cyclic-solve=20blocker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build-verified attempt to port the Java EuclideanCyclicConvergenceTest (cathead, "circular hole edge" φ = π−0.1). - ✅ GREEN `CyclicCircularEdge_PhiEntersGradient_CatHead`: solver-free cross-validation that the circular-edge φ target enters the cyclic edge gradient exactly (ΔG_e = −Δφ_e at 1e-12; no other component moves). - ⏸️ DISABLED `CyclicCircularEdge_CatHead_JavaXVal`: the full Java convergence assertion (α_opp+α_opp = π−0.1), kept with golden semantics. Auto-activates once the edge-DOF Hessian lands. Build-verification finding: `newton_euclidean` -> `euclidean_hessian` throws "edge DOFs are not supported" — the cyclic full solve is blocked by a missing edge-DOF Euclidean Hessian (gradient supports edge DOFs, analytic Hessian does not). Spherical convergence (Tier-1 #2) is already covered by test_newton_solver. Documented in doc/reviewer/java-ignore-crossvalidation.md. All 13 EuclideanFunctional tests pass (1 disabled). Co-Authored-By: Claude Opus 4.8 --- code/tests/cgal/test_euclidean_functional.cpp | 148 ++++++++++++++++++ doc/reviewer/java-ignore-crossvalidation.md | 63 ++++++++ 2 files changed, 211 insertions(+) diff --git a/code/tests/cgal/test_euclidean_functional.cpp b/code/tests/cgal/test_euclidean_functional.cpp index 5f9e249..2f1fc87 100644 --- a/code/tests/cgal/test_euclidean_functional.cpp +++ b/code/tests/cgal/test_euclidean_functional.cpp @@ -27,9 +27,12 @@ #include "euclidean_functional.hpp" #include "euclidean_hessian.hpp" #include "clausen.hpp" +#include "mesh_io.hpp" +#include "newton_solver.hpp" #include #include #include +#include using namespace conformallab; @@ -405,3 +408,148 @@ TEST(EuclideanGoldenJava, FullMeshGradientAndEnergy_Tetrahedron) - euclidean_energy(mesh, x0, maps); EXPECT_NEAR(dE, 0.15962619236187336, 1e-12); } + +// ════════════════════════════════════════════════════════════════════════════ +// Java cross-validation (Tier 1, GREEN) — circular-edge φ wiring (no solver) +// +// The "circular hole edge" of EuclideanCyclicConvergenceTest works by setting a +// non-default edge turn angle φ_e. Since the cyclic edge gradient is exactly +// G_e = α_opp(f⁺) + α_opp(f⁻) − φ_e, +// lowering φ_e by 0.1 must raise G_e by exactly 0.1 — independent of geometry — +// and must leave every other gradient component untouched. This pins the φ +// wiring without needing the (not-yet-implemented) edge-DOF Hessian, so it runs +// today and is the evaluation-level prerequisite of the DISABLED convergence +// test below. +// ════════════════════════════════════════════════════════════════════════════ +TEST(EuclideanFunctional, CyclicCircularEdge_PhiEntersGradient_CatHead) +{ + const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/obj/cathead.obj"; + ConformalMesh mesh; + ASSERT_NO_THROW(mesh = load_mesh(path)) << "cathead.obj not found: " << path; + + auto maps = setup_euclidean_maps(mesh); + compute_euclidean_lambda0_from_mesh(mesh, maps); + + int idx = 0; + for (auto v : mesh.vertices()) + maps.v_idx[v] = mesh.is_border(v) ? -1 : idx++; + for (auto e : mesh.edges()) + maps.e_idx[e] = idx++; + const int n = idx; + ASSERT_GT(n, 0); + + Edge_index e_circ{}; + bool found = false; + for (auto e : mesh.edges()) { + auto h = mesh.halfedge(e); + auto ho = mesh.opposite(h); + if (mesh.is_border(h) || mesh.is_border(ho)) continue; + e_circ = e; found = true; break; + } + ASSERT_TRUE(found) << "no interior edge found on cathead"; + const std::size_t ie = static_cast(maps.e_idx[e_circ]); + + std::vector x(static_cast(n), 0.0); + + maps.phi_e[e_circ] = PI; + auto G1 = euclidean_gradient(mesh, x, maps); + maps.phi_e[e_circ] = PI - 0.1; + auto G2 = euclidean_gradient(mesh, x, maps); + + // Lowering φ_e by 0.1 raises exactly this edge's gradient component by 0.1. + EXPECT_NEAR(0.1, G2[ie] - G1[ie], 1e-12) + << "circular-edge φ target not wired into the cyclic gradient"; + + // No other gradient component changes. + double max_other = 0.0; + for (std::size_t k = 0; k < G1.size(); ++k) + if (k != ie) max_other = std::max(max_other, std::abs(G2[k] - G1[k])); + EXPECT_LT(max_other, 1e-12) + << "changing one φ_e perturbed unrelated gradient components"; +} + +// ════════════════════════════════════════════════════════════════════════════ +// Java cross-validation (Tier 1) — EuclideanCyclicConvergenceTest +// +// Ports de.varylab.discreteconformal.functional.EuclideanCyclicConvergenceTest: +// prescribe a non-default edge turn angle φ = π − 0.1 on one interior edge of +// cathead.obj ("circular hole edge"), solve the cyclic Euclidean functional +// (vertex + edge DOFs), then assert the realised opposite-corner-angle sum +// across that edge equals π − 0.1. +// +// The C++ edge gradient is G_e = α_opp(f⁺) + α_opp(f⁻) − φ_e, so at the +// solution (G_e = 0) the geometric angle sum equals φ_e — exactly the Java +// assertion `circularEdge.getAlpha() + opposite.getAlpha() == π − 0.1`. +// +// "Natural targets" first make x = 0 the equilibrium (so the *only* deviation +// is the prescribed φ); the test therefore FAILS if the solver ignores a +// non-default φ (the sum would stay at its natural value, not π − 0.1). +// +// ⚠️ DISABLED (build-verified 2026-05-29): blocked by a missing feature, not a +// bug. `newton_euclidean` uses `euclidean_hessian`, which throws +// "euclidean_hessian: edge DOFs are not supported +// (only the vertex-block cotangent Laplacian is implemented)". +// The cyclic functional needs vertex+edge DOFs, so the full Newton solve is not +// yet possible in C++ (the gradient supports edge DOFs; the analytic Hessian +// does not). PREREQUISITE: edge-DOF Euclidean Hessian — see +// `doc/roadmap/research-track.md` and `doc/reviewer/java-ignore-crossvalidation.md`. +// The golden semantics (φ = π−0.1 ⇒ realised α_opp+α_opp = π−0.1) are kept here +// so this test auto-activates once that Hessian lands; just drop the DISABLED_. +// ════════════════════════════════════════════════════════════════════════════ +TEST(EuclideanFunctional, DISABLED_CyclicCircularEdge_CatHead_JavaXVal) +{ + const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/obj/cathead.obj"; + ConformalMesh mesh; + ASSERT_NO_THROW(mesh = load_mesh(path)) << "cathead.obj not found: " << path; + + auto maps = setup_euclidean_maps(mesh); + compute_euclidean_lambda0_from_mesh(mesh, maps); + + // Cyclic DOFs: interior vertices (border pinned) + all edges. + int idx = 0; + for (auto v : mesh.vertices()) + maps.v_idx[v] = mesh.is_border(v) ? -1 : idx++; + for (auto e : mesh.edges()) + maps.e_idx[e] = idx++; + const int n = idx; + ASSERT_GT(n, 0); + + // Natural targets: set Θ_v / φ_e so that x = 0 is the equilibrium (G(0)=0). + std::vector x0(static_cast(n), 0.0); + auto G0 = euclidean_gradient(mesh, x0, maps); + for (auto v : mesh.vertices()) { + int iv = maps.v_idx[v]; + if (iv >= 0) maps.theta_v[v] -= G0[static_cast(iv)]; + } + for (auto e : mesh.edges()) { + int ie = maps.e_idx[e]; + if (ie >= 0) maps.phi_e[e] += G0[static_cast(ie)]; + } + + // Pick one interior edge: both incident faces present, both endpoints interior. + Edge_index circular{}; + bool found = false; + for (auto e : mesh.edges()) { + auto h = mesh.halfedge(e); + auto ho = mesh.opposite(h); + if (mesh.is_border(h) || mesh.is_border(ho)) continue; + if (mesh.is_border(mesh.source(h)) || mesh.is_border(mesh.target(h))) continue; + circular = e; found = true; break; + } + ASSERT_TRUE(found) << "no interior edge found on cathead"; + const std::size_t ie = static_cast(maps.e_idx[circular]); + + // Prescribe the circular edge turn angle φ = π − 0.1 (Java CustomEdgeInfo.phi). + const double phi_target = PI - 0.1; + maps.phi_e[circular] = phi_target; + + auto res = newton_euclidean(mesh, x0, maps, /*tol=*/1e-11, /*max_iter=*/200); + ASSERT_TRUE(res.converged) + << "Newton did not converge; ||G||=" << res.grad_inf_norm; + + // Realised geometric opposite-corner-angle sum = φ_e + G_e(x*) (= α_opp+α_opp). + auto Gf = euclidean_gradient(mesh, res.x, maps); + const double realised = maps.phi_e[circular] + Gf[ie]; + EXPECT_NEAR(phi_target, realised, 1e-9) + << "prescribed circular edge turn angle π−0.1 not realised at the solution"; +} diff --git a/doc/reviewer/java-ignore-crossvalidation.md b/doc/reviewer/java-ignore-crossvalidation.md index 3ace6fc..c70c0db 100644 --- a/doc/reviewer/java-ignore-crossvalidation.md +++ b/doc/reviewer/java-ignore-crossvalidation.md @@ -93,6 +93,69 @@ Assertion strategy: because the values are symmetric per DOF-class, assert --- +## 3b — Effort-adjusted tiering (2026-05-29 inventory) + +> A broader scan (not just `@Ignore`d tests) revealed that the §3 ranking was +> **effort-blind**: cheaper, equally-valuable oracles exist than the Lawson +> generator. Re-tiered by **value / cost**: + +| Tier | Oracle | Mesh / asset | Solver | Golden | C++ status | Cost | +|---|---|---|---|---|---|---| +| **1** | `EuclideanCyclicConvergenceTest` | `cathead.obj` *(already in C++)* | MTJ BiCGstab (no PETSc) | `α_opp+α_opp = π−0.1` @1e-12 (per-edge φ target) | `euclidean_functional` ✅, `phi_e` ✅, cathead ✅ | **near-zero** | +| **1** | `SphericalConvergenceTest` | octahedron | MTJ Newton (no PETSc) | self-consistency (const. curvature) | spherical ✅, `make_octahedron_face` ✅ | low | +| **2** | Wente genus-1 uniformization | `wente_torus02.obj` + `wente_uniformization.xml` | — (stored result) | exact `IsometryPSL2R` uniformizing-group generators | holonomy/`MobiusMap` ✅; **needs XML reader** | medium | +| **2** | `regular_uniformization.xml` | (stored) | — | uniformization data | needs XML reader | medium | +| **3** | HyperIdeal Lawson convergence (3 golden vectors) | combinatorial `createLawsonSquareTiled` | — (hard-coded u\*) | symmetric u\* | needs **low-level half-edge generator** (§5) | high | +| **4** | genus-2 (`genus2.xml`/`lawson2498.obj`), holomorphic forms (`HolomorphicEuclideanUniformization_*.xml`), Schottky (`genus2.xml`), hyperelliptic | assets present in Java | — | various | Phases 10a/10c/11/13 | future | + +**Key correction:** the Lawson generator (originally flagged HIGH) is actually +**Tier 3** — higher effort than Tier 1/2. Cheapest first wins: + +1. **Tier 1 — `EuclideanCyclicConvergenceTest` on cathead** (asset already + present; the C++ edge gradient `G_e = α_opp(f⁺)+α_opp(f⁻) − φ_e` makes the + Java assertion `α_opp+α_opp = π−0.1` exactly the converged `G_e = 0` + condition with `phi_e = π−0.1`). Fails if the solver ignores a non-default + `φ`, so it is a genuine check, not a tautology. +2. **Tier 1 — `SphericalConvergenceTest`** on the octahedron. +3. **Tier 2 — Wente genus-1 uniformization** (needs a small `UniformizationData` + XML reader; strongest deterministic pipeline oracle that maps to landed C++). +4. **Tier 3 — Lawson generator** (only after Tiers 1–2). + +### Build-verification finding (2026-05-29) — Tier 1 cyclic is blocked + +Attempting the Tier-1 `EuclideanCyclicConvergenceTest` port **build-verified** a +blocker: `newton_euclidean` calls `euclidean_hessian`, which throws +*"edge DOFs are not supported (only the vertex-block cotangent Laplacian is +implemented)"*. The cyclic functional needs **vertex + edge** DOFs, so the full +Newton solve is **not yet possible** in C++ — the *gradient* supports edge DOFs, +the *analytic Hessian* does not. (This is also why PR #27 cross-validated only +the cyclic *evaluation*, never a solve.) + +**Landed in this PR instead** (`test_euclidean_functional.cpp`): +- ✅ `CyclicCircularEdge_PhiEntersGradient_CatHead` (**GREEN**) — solver-free + cross-validation that the circular-edge φ target enters the cyclic gradient + exactly (`ΔG_e = −Δφ_e` at 1e-12, no other component moves). This is the + evaluation-level prerequisite of the convergence oracle. +- ⏸️ `DISABLED_CyclicCircularEdge_CatHead_JavaXVal` — the full Java convergence + assertion (`α_opp+α_opp = π−0.1`), kept in-code with the golden semantics; it + auto-activates (drop `DISABLED_`) once the edge-DOF Hessian lands. + +**New prerequisite for Tier-1 cyclic:** an **edge-DOF Euclidean Hessian** (the +cyclic Hessian over vertex + edge variables, cf. Java `getNonZeroPattern`). Spherical +convergence (Tier-1 #2) turned out to be **already covered** by +`test_newton_solver` (`Spherical_ConvergesFromPerturbation` et al. on the +spherical tetrahedron), and `make_octahedron_face()` is a single face, not a +closed octahedron — so Tier-1 #2 adds little. → the next genuinely-new +cross-validation target is **Tier 2 (Wente uniformization XML)** or implementing +the edge-DOF Hessian to unblock the cyclic convergence oracle. + +### Other generators in the Java tree +`HyperellipticCurveGenerator` (→ Phase 13), `SchottkyGenerator` (→ Phase 11a), +`FunctionalTest.createOctahedron/createTetrahedron` (trivial — C++ has +`make_octahedron_face` / `make_tetrahedron`). + +--- + ## 4 — Recommended order of work 1. **Port `make_lawson_square_tiled()`** (low-level half-edge; see §5) →