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
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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user