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
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:
@@ -20,6 +20,9 @@
|
||||
#include "layout.hpp"
|
||||
#include "period_matrix.hpp"
|
||||
#include "fundamental_domain.hpp"
|
||||
#include "mesh_io.hpp"
|
||||
#include "cut_graph.hpp"
|
||||
#include "gauss_bonnet.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <complex>
|
||||
@@ -343,6 +346,122 @@ TEST(PeriodMatrix, ComputePeriodMatrix_ReducedTau_InFD)
|
||||
EXPECT_TRUE(is_in_fundamental_domain(pd.tau, 1e-9));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// End-to-end holonomy → τ on real genus-1 torus meshes
|
||||
//
|
||||
// Regression test for the holonomy-extraction bug: euclidean_holonomy() developed
|
||||
// the cut surface along a BFS dual tree that crossed the primal-tree edges freely.
|
||||
// Relative to that tree the cut graph's 2g generator edges were NOT generators —
|
||||
// some were null-homotopic — so the two developed copies of a "cut" edge landed
|
||||
// on top of each other and compute_period_matrix() got ω ≈ 0 (→ τ = 0 / NaN /
|
||||
// huge). The fix develops across the cut graph's OWN dual spanning tree T* only
|
||||
// (CutGraph::is_dual_tree), unfolding the surface onto a true disk so the cut
|
||||
// edges become the boundary identifications that carry the lattice generators.
|
||||
//
|
||||
// Analytic target. The bundled meshes are tori of REVOLUTION (major radius R,
|
||||
// minor radius r, R > r), not abstract square/hexagonal flat tori. Their
|
||||
// conformal modulus is purely imaginary,
|
||||
//
|
||||
// τ = i · √(R² − r²) / r (reduced so |τ| ≥ 1)
|
||||
//
|
||||
// derived from the flat-conformal change of variable dψ = r/(R + r cos φ) dφ on
|
||||
// the induced metric ds² = (R + r cos φ)² dθ² + r² dφ²; the ψ-period is
|
||||
// 2πr/√(R²−r²), giving the rectangular lattice ratio above. Re(τ) = 0 follows
|
||||
// from the meridian ⟂ longitude reflection symmetry. The coarse polygonal cross
|
||||
// sections (square/hex/octagon) approximate the circular value from above; the
|
||||
// gap shrinks as the cross section gains sides.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
namespace {
|
||||
|
||||
// Run the full pipeline solve → cut → layout → period matrix on a torus mesh and
|
||||
// return the reduced τ together with the two raw holonomy generators.
|
||||
struct TorusTau {
|
||||
std::complex<double> tau;
|
||||
std::vector<Eigen::Vector2d> omega;
|
||||
bool converged = false;
|
||||
};
|
||||
|
||||
TorusTau run_torus_pipeline(const std::string& file)
|
||||
{
|
||||
const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/off/" + file;
|
||||
ConformalMesh mesh = load_mesh(path);
|
||||
|
||||
EuclideanMaps maps = setup_euclidean_maps(mesh); // Θ_v = 2π (flat target)
|
||||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
int idx = 0;
|
||||
bool pinned = false;
|
||||
for (auto v : mesh.vertices()) {
|
||||
if (!pinned) { maps.v_idx[v] = -1; pinned = true; }
|
||||
else maps.v_idx[v] = idx++;
|
||||
}
|
||||
enforce_gauss_bonnet(mesh, maps);
|
||||
|
||||
std::vector<double> x0(static_cast<std::size_t>(idx), 0.0);
|
||||
auto res = newton_euclidean(mesh, x0, maps);
|
||||
|
||||
CutGraph cg = compute_cut_graph(mesh);
|
||||
HolonomyData hol;
|
||||
euclidean_layout(mesh, res.x, maps, &cg, &hol, /*normalise=*/false);
|
||||
|
||||
PeriodData pd = compute_period_matrix(hol, /*reduce=*/true);
|
||||
return TorusTau{pd.tau, hol.translations, res.converged};
|
||||
}
|
||||
|
||||
// Reduced conformal modulus of a torus of revolution (major R, minor r).
|
||||
double revolution_tau_imag(double R, double r)
|
||||
{
|
||||
return std::sqrt(R * R - r * r) / r; // ≥ 1 form (|τ| ≥ 1)
|
||||
}
|
||||
|
||||
void check_torus(const std::string& file, double R, double r, double rel_tol)
|
||||
{
|
||||
TorusTau t = run_torus_pipeline(file);
|
||||
ASSERT_TRUE(t.converged) << file << ": Newton did not converge";
|
||||
|
||||
// Generators must be non-degenerate (the bug collapsed them to ~0).
|
||||
ASSERT_EQ(t.omega.size(), 2u);
|
||||
EXPECT_GT(t.omega[0].norm(), 1e-3) << file << ": ω₁ degenerate";
|
||||
EXPECT_GT(t.omega[1].norm(), 1e-3) << file << ": ω₂ degenerate";
|
||||
|
||||
EXPECT_TRUE(std::isfinite(t.tau.real()) && std::isfinite(t.tau.imag()))
|
||||
<< file << ": τ is not finite (" << t.tau.real() << "+" << t.tau.imag() << "i)";
|
||||
EXPECT_GT(t.tau.imag(), 0.0) << file << ": τ must lie in the upper half-plane";
|
||||
EXPECT_TRUE(is_in_fundamental_domain(t.tau, 1e-6))
|
||||
<< file << ": τ = " << t.tau.real() << "+" << t.tau.imag() << "i not in F";
|
||||
|
||||
// Re(τ) = 0 by the meridian ⟂ longitude reflection symmetry.
|
||||
EXPECT_NEAR(t.tau.real(), 0.0, 0.05)
|
||||
<< file << ": Re(τ) should vanish for a torus of revolution";
|
||||
|
||||
const double expected = revolution_tau_imag(R, r);
|
||||
EXPECT_NEAR(t.tau.imag(), expected, rel_tol * expected)
|
||||
<< file << ": Im(τ) = " << t.tau.imag()
|
||||
<< " vs analytic i·√(R²−r²)/r = " << expected;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// 4×4 torus of revolution: R = 2, r = 1 → τ = i√3 ≈ 1.732i.
|
||||
// Square (4-gon) cross section → coarsest circle approximation, looser tolerance.
|
||||
TEST(HolonomyEndToEnd, Torus4x4_TauMatchesRevolutionModulus)
|
||||
{
|
||||
check_torus("torus_4x4.off", /*R=*/2.0, /*r=*/1.0, /*rel_tol=*/0.10);
|
||||
}
|
||||
|
||||
// Hexagonal 6×6 torus of revolution: R = 3, r = 1 → τ = i√8 ≈ 2.828i.
|
||||
TEST(HolonomyEndToEnd, TorusHex6x6_TauMatchesRevolutionModulus)
|
||||
{
|
||||
check_torus("torus_hex_6x6.off", /*R=*/3.0, /*r=*/1.0, /*rel_tol=*/0.05);
|
||||
}
|
||||
|
||||
// Octagonal 8×8 torus of revolution: R = 3, r = 1 → τ = i√8 ≈ 2.828i.
|
||||
TEST(HolonomyEndToEnd, Torus8x8_TauMatchesRevolutionModulus)
|
||||
{
|
||||
check_torus("torus_8x8.off", /*R=*/3.0, /*r=*/1.0, /*rel_tol=*/0.05);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// FundamentalDomain — genus-1 parallelogram
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -486,3 +605,4 @@ TEST(TilingNeighbourhood, EmptyHolonomy_ReturnsSingleTile)
|
||||
auto tiles = tiling_neighbourhood(lay, hol);
|
||||
EXPECT_EQ(tiles.size(), 1u);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user