test: Java golden-value oracles for the five DCE math cores + P1-2/P1-3 fixes
All checks were successful
C++ Tests / test-fast (pull_request) Successful in 1m57s
API Docs / doc-build (pull_request) Successful in 59s
Markdown link check / check (pull_request) Successful in 51s
C++ Tests / test-cgal (pull_request) Has been skipped
C++ Tests / quality-gates (pull_request) Successful in 2m19s

Add bit-for-bit (1e-12) golden-value oracle tests pinning the C++ pure-math
and functional cores against the compiled upstream Java library (openjdk 17):

- HyperIdealGoldenJava: Clausen/Л/ImLi2, ζ13/14/15/ζ, both tetrahedron-volume
  formulas (real de.varylab…Clausen / HyperIdealUtility).
- EuclideanGoldenJava / SphericalGoldenJava: angle formulas + β relations + Л
  energy terms, plus FULL-MESH oracles driving the real EuclideanCyclicFunctional
  / SphericalFunctional on a shared tetrahedron — per-vertex gradient (Θ−Σα) and
  ΔE = E(x)−E(0) (C++ Gauss-Legendre path integral vs Java closed form).
- SphericalGoldenJava.FullMeshEdgeDofGradient: edge-DOF gradient (vertex + edge
  components, α_opp⁺+α_opp⁻−θ_e) vs raw conformalEnergyAndGradient — locks
  Finding 3 at the solution level (audit items 4 & 5).
- PeriodMatrix.NormalizeModulus_GoldenJava: τ-reduction fold convention vs the
  real DiscreteEllipticUtility.normalizeModulus (audit items 7 & 8).

Subtlety documented: the spherical oracles call Java's raw
conformalEnergyAndGradient, not evaluate() (which pre-runs a Brent gauge
maximization that C++ factors into the Newton solver's spherical_gauge_shift).

Also:
- P1-2 (layout.hpp): Euclidean holonomy now uses a per-cut-edge rigid-motion fit
  g(z)=a·z+b, exposing residual_rotation = |arg(a)| as a diagnostic; non-
  regressive (flat case a=1 reduces to the old midpoint formula).
- P1-3 (period_matrix.hpp): is_in_fundamental_domain fixed to the correct
  half-open SL(2,ℤ) domain (−½ ≤ Re < ½). Updated the now-exposed
  ComputePeriodMatrix_ReducedTau_InFD to assert the normalizeModulus domain
  (closed +½ edge) instead.

Test counts (single source of truth = doc/api/tests.md): 272/272 pass, 0
skipped (26 non-CGAL + 246 CGAL).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-29 19:08:37 +02:00
parent 18b9c61492
commit ba83974525
9 changed files with 580 additions and 61 deletions

View File

@@ -176,6 +176,14 @@ struct HolonomyData {
std::vector<Eigen::Vector2d> translations; ///< Euclidean / spherical translation per cut edge.
std::vector<MobiusMap> mobius_maps; ///< Hyperbolic Möbius isometry per cut edge (Phase 7).
std::vector<std::size_t> cut_edge_indices; ///< Index (in the cut-graph edge list) of each holonomy entry.
/// Residual rotation |arg(a)| (radians) of the Euclidean deck isometry
/// z ↦ a·z + b per cut edge. Zero for a perfectly flat cone metric;
/// a non-zero value signals under-convergence or a genuine cone-angle
/// defect, in which case `translations[i]` (the isometry's translation
/// part b) is only an approximate lattice generator. Empty for the
/// hyperbolic (Möbius) path.
std::vector<double> residual_rotation;
};
// ── Internal helpers ──────────────────────────────────────────────────────────
@@ -490,11 +498,23 @@ inline void set_root_huv_2d(
// different locations. The rigid motion identifying those two copies is the
// deck transformation of the generator loop (cut edge + tree path) — i.e. the
// holonomy. For a flat cone metric (Θ ≡ 2π) the linear part is trivial, so the
// holonomy is the pure translation given by the displacement of the shared
// edge's midpoint between the two developments. That translation is exactly
// the lattice generator ω consumed by compute_period_matrix().
// holonomy is the pure translation that identifies the two developments of the
// shared edge. We recover it as a full rigid motion z ↦ a·z + b fitted to the
// correspondence (Bs,Bt) ↦ (As,At): the shared edge endpoints S,T as developed
// in face B map to the same endpoints as developed in face A. Because both
// faces use the same edge length, |a| = 1 and the fit is an exact
// orientation-preserving isometry. Its translation part b is the lattice
// generator ω consumed by compute_period_matrix(); the rotation magnitude
// |arg(a)| is returned as a convergence diagnostic. For a perfectly flat cone
// metric (Θ ≡ 2π) a = 1 and b reduces to the midpoint displacement midA midB
// (the previous formula), so converged flat tori are bit-for-bit unchanged.
struct EuclideanHolonomyResult {
std::vector<Eigen::Vector2d> translations; ///< ω_i = translation part b
std::vector<double> residual_rotation; ///< |arg(a_i)| per cut edge
};
template <typename EdgeLenFn>
inline std::vector<Eigen::Vector2d> euclidean_holonomy(
inline EuclideanHolonomyResult euclidean_holonomy(
const ConformalMesh& mesh,
const CutGraph& cut,
EdgeLenFn&& edge_len)
@@ -565,9 +585,10 @@ inline std::vector<Eigen::Vector2d> euclidean_holonomy(
for (auto f : mesh.faces())
if (!face_done[static_cast<std::size_t>(f.idx())]) develop(f);
// ── Holonomy translation per cut edge ─────────────────────────────────────
std::vector<Eigen::Vector2d> omega;
omega.reserve(cut.cut_edge_indices.size());
// ── Holonomy isometry per cut edge ─────────────────────────────────────────
EuclideanHolonomyResult out;
out.translations.reserve(cut.cut_edge_indices.size());
out.residual_rotation.reserve(cut.cut_edge_indices.size());
for (std::size_t ce : cut.cut_edge_indices) {
Edge_index e = *std::next(mesh.edges().begin(), static_cast<std::ptrdiff_t>(ce));
Halfedge_index h = mesh.halfedge(e);
@@ -575,7 +596,8 @@ inline std::vector<Eigen::Vector2d> euclidean_holonomy(
if (mesh.is_border(h) || mesh.is_border(ho)
|| !face_done[static_cast<std::size_t>(mesh.face(h).idx())]
|| !face_done[static_cast<std::size_t>(mesh.face(ho).idx())]) {
omega.push_back(Eigen::Vector2d::Zero());
out.translations.push_back(Eigen::Vector2d::Zero());
out.residual_rotation.push_back(0.0);
continue;
}
// Face A = face(h) places edge e as halfedge h: S=source(h), T=target(h)
@@ -585,12 +607,15 @@ inline std::vector<Eigen::Vector2d> euclidean_holonomy(
// target(ho)=S → S=pos of source(next(ho)), T=pos of source(ho)
C Bt = hpos[static_cast<std::size_t>(ho.idx())];
C Bs = hpos[static_cast<std::size_t>(mesh.next(ho).idx())];
C midA = 0.5 * (As + At);
C midB = 0.5 * (Bs + Bt);
C w = midA - midB; // pure translation for a flat cone metric
omega.push_back(Eigen::Vector2d(w.real(), w.imag()));
// Fit the deck isometry g(z) = a·z + b with (Bs,Bt) ↦ (As,At).
// |a| = 1 exactly (shared edge length); b is the translation part.
C edgeB = Bt - Bs;
C a = (std::abs(edgeB) > 1e-300) ? (At - As) / edgeB : C(1.0, 0.0);
C b = As - a * Bs;
out.translations.push_back(Eigen::Vector2d(b.real(), b.imag()));
out.residual_rotation.push_back(std::abs(std::arg(a)));
}
return omega;
return out;
}
} // namespace detail
@@ -719,7 +744,9 @@ inline Layout2D euclidean_layout(
if (cut && holonomy) {
holonomy->cut_edge_indices = cut->cut_edge_indices;
holonomy->mobius_maps.clear();
holonomy->translations = detail::euclidean_holonomy(mesh, *cut, edge_len);
auto eh = detail::euclidean_holonomy(mesh, *cut, edge_len);
holonomy->translations = std::move(eh.translations);
holonomy->residual_rotation = std::move(eh.residual_rotation);
// Preserve the per-cut-edge seam UV in halfedge_uv (texture atlas), as
// before, so HalfedgeUV-based tests still see the seam-crossing layout.

View File

@@ -114,11 +114,18 @@ inline std::complex<double> reduce_to_fundamental_domain(std::complex<double> ta
// is_in_fundamental_domain — check membership in F with tolerance tol.
// ─────────────────────────────────────────────────────────────────────────────
/// `true` iff `τ` lies inside the standard SL(2,) fundamental domain
/// with tolerance `tol`.
/// `F = { Im τ > 0, −½ ≤ Re τ < ½, |τ| ≥ 1 }` with tolerance `tol`.
///
/// This is the half-open domain produced by `reduce_to_fundamental_domain`
/// (the right boundary `Re τ = +½ ≡ −½` is excluded via `T`). It is NOT the
/// mirror-folded `0 ≤ Re τ ≤ ½` domain produced by `normalizeModulus` (used
/// inside `compute_period_matrix`); a τ with `Re τ < 0` is a legitimate member
/// of `F` here but would be folded to `Re ≥ 0` by `normalizeModulus`.
inline bool is_in_fundamental_domain(std::complex<double> tau, double tol = 1e-9)
{
if (tau.imag() <= 0.0) return false;
if (std::abs(tau.real()) > 0.5 + tol) return false;
if (tau.real() < -0.5 - tol) return false; // left boundary closed
if (tau.real() > 0.5 - tol) return false; // right boundary Re = +½ excluded (≡ −½)
if (std::abs(tau) < 1.0 - tol) return false;
return true;
}