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:
@@ -27,6 +27,8 @@
|
||||
#include "mesh_builder.hpp"
|
||||
#include "spherical_functional.hpp"
|
||||
#include "spherical_hessian.hpp"
|
||||
#include "spherical_geometry.hpp"
|
||||
#include "clausen.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
@@ -416,3 +418,237 @@ TEST(SphericalFunctional, GaugeFix_AlreadyAtGaugeReturnsTNearZero)
|
||||
EXPECT_NEAR(t, 0.0, 1e-5)
|
||||
<< "Gauge shift from the symmetric point should be ~0; got " << t;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Golden-value oracle — pin the spherical law-of-cosines angle formula, the β
|
||||
// half-angle relations, and the Lobachevsky energy term bit-for-bit against the
|
||||
// upstream Java reference (SphericalFunctional.triangleEnergyAndAlphas, lines
|
||||
// 401-453), captured by running the compiled Java library (openjdk 17) with the
|
||||
// real de.varylab…Clausen.Л on these exact arc lengths. Unlike the Schläfli
|
||||
// path-integral gradient check (which only verifies curl-freeness), this locks
|
||||
// the absolute angle/β/energy values against an independent implementation,
|
||||
// catching any silent index/sign/convention drift in the spherical port.
|
||||
//
|
||||
// To regenerate: /tmp/oracle/SphereOracle.java (recipe in doc/reviewer/
|
||||
// java-port-audit.md). Values are Java printf %.17g. Tolerance 1e-12.
|
||||
//
|
||||
// Index map (C++ spherical_angles ↔ Java): with l12=lij, l23=ljk, l31=lki the
|
||||
// returned alpha1/alpha2/alpha3 are Java αi/αj/αk (angle opposite ljk/lki/lij).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
TEST(SphericalGoldenJava, AngleBetaEnergyFromLengths)
|
||||
{
|
||||
auto check = [](double lij, double ljk, double lki,
|
||||
double ai_g, double aj_g, double ak_g,
|
||||
double bi_g, double bj_g, double bk_g, double L_g) {
|
||||
auto fa = spherical_angles(lij, ljk, lki);
|
||||
EXPECT_TRUE(fa.valid);
|
||||
EXPECT_NEAR(fa.alpha1, ai_g, 1e-12);
|
||||
EXPECT_NEAR(fa.alpha2, aj_g, 1e-12);
|
||||
EXPECT_NEAR(fa.alpha3, ak_g, 1e-12);
|
||||
|
||||
const double ai = fa.alpha1, aj = fa.alpha2, ak = fa.alpha3;
|
||||
const double bi = 0.5 * (PI + ai - aj - ak);
|
||||
const double bj = 0.5 * (PI - ai + aj - ak);
|
||||
const double bk = 0.5 * (PI - ai - aj + ak);
|
||||
EXPECT_NEAR(bi, bi_g, 1e-12);
|
||||
EXPECT_NEAR(bj, bj_g, 1e-12);
|
||||
EXPECT_NEAR(bk, bk_g, 1e-12);
|
||||
|
||||
const double Lterm =
|
||||
Lobachevsky(ai) + Lobachevsky(aj) + Lobachevsky(ak) +
|
||||
Lobachevsky(bi) + Lobachevsky(bj) + Lobachevsky(bk) +
|
||||
Lobachevsky(0.5 * (PI - ai - aj - ak));
|
||||
EXPECT_NEAR(Lterm, L_g, 1e-12);
|
||||
};
|
||||
|
||||
// Scalene spherical triangle (all Δ > 0, Δ_ijk < 2π).
|
||||
check(1.0, 1.2, 0.9,
|
||||
1.5305813141072122, 0.99684981272794600, 1.1246069519101605,
|
||||
1.2753586015294494, 0.74162710015018330, 0.86938423933239760,
|
||||
1.3576100185550408);
|
||||
// Equilateral spherical triangle.
|
||||
check(0.7, 0.7, 0.7,
|
||||
1.1225596283199812, 1.1225596283199812, 1.1225596283199812,
|
||||
1.0095165126349062, 1.0095165126349060, 1.0095165126349060,
|
||||
1.6806828584976297);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// FULL-MESH golden oracle (spherical) — drives the REAL upstream SphericalFunctional
|
||||
// on the shared tetrahedron (scaled so every arc length stays < π) and pins BOTH
|
||||
// the per-vertex gradient G_v = Θ_v − Σα AND ΔE = E(x) − E(0) bit-for-bit.
|
||||
//
|
||||
// IMPORTANT — the oracle calls Java's `conformalEnergyAndGradient` (the RAW
|
||||
// θ−Σα gradient), NOT `evaluate()`: `evaluate()` first runs a 1-D Brent
|
||||
// maximization over the global-scale gauge direction (`maximizeInNegativeDirection`),
|
||||
// which the C++ `spherical_gradient` deliberately does NOT — C++ factors that
|
||||
// gauge into the Newton solver's `spherical_gauge_shift` instead. Comparing
|
||||
// against `evaluate()` would (wrongly) drive every component to ~0. The raw
|
||||
// gradient is the piece that corresponds 1:1 to the C++ functional.
|
||||
//
|
||||
// Setup parity (UnwrapUtility.prepareInvariantDataHyperbolicAndSpherical, scale):
|
||||
// closed mesh, ALL 4 vertices variable, Θ_v = 2π, no edge DOFs,
|
||||
// λ°_e = 2·log(SCALE·|p_i − p_j|) (Java uses chord length × scale, whereas the
|
||||
// C++ compute_lambda0_from_mesh helper assumes unit-sphere vertices and uses
|
||||
// ARC length — so we set λ° directly here to match Java exactly),
|
||||
// per-vertex u(P) = 0.10·X − 0.07·Y + 0.13·Z, SCALE = 0.2.
|
||||
//
|
||||
// To regenerate: /tmp/oracle/{tet.obj,SphereMeshOracle.java}. Values are Java %.17g.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
TEST(SphericalGoldenJava, FullMeshGradientAndEnergy_Tetrahedron)
|
||||
{
|
||||
constexpr double TWO_PI = 2.0 * 3.14159265358979323846264338328;
|
||||
constexpr double SCALE = 0.2;
|
||||
|
||||
auto mesh = make_tetrahedron(); // same 4 vertices as /tmp/oracle/tet.obj
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
|
||||
// λ°_e = 2·log(SCALE · chord), matching Java's prepareInvariantData(scale).
|
||||
for (auto e : mesh.edges()) {
|
||||
auto h = mesh.halfedge(e);
|
||||
const auto& p1 = mesh.point(mesh.source(h));
|
||||
const auto& p2 = mesh.point(mesh.target(h));
|
||||
const double dx = p1.x() - p2.x(), dy = p1.y() - p2.y(), dz = p1.z() - p2.z();
|
||||
const double chord = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
maps.lambda0[e] = 2.0 * std::log(SCALE * chord);
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
for (auto v : mesh.vertices()) {
|
||||
maps.v_idx[v] = idx++;
|
||||
maps.theta_v[v] = TWO_PI;
|
||||
}
|
||||
|
||||
auto u_of = [](const Point3& p) {
|
||||
return 0.10 * p.x() - 0.07 * p.y() + 0.13 * p.z();
|
||||
};
|
||||
|
||||
std::vector<double> x(static_cast<std::size_t>(idx), 0.0);
|
||||
for (auto v : mesh.vertices())
|
||||
x[static_cast<std::size_t>(maps.v_idx[v])] = u_of(mesh.point(v));
|
||||
|
||||
auto G = spherical_gradient(mesh, x, maps);
|
||||
|
||||
struct GoldRow { double X, Y, Z, G; };
|
||||
const GoldRow gold[4] = {
|
||||
{ 1, 1, 1, 2.7671034786104927},
|
||||
{ 1, -1, -1, 2.5216834054857546},
|
||||
{-1, 1, -1, 1.5401761310866633},
|
||||
{-1, -1, 1, 2.6518569531861100},
|
||||
};
|
||||
for (auto v : mesh.vertices()) {
|
||||
const auto& p = mesh.point(v);
|
||||
const double g = G[static_cast<std::size_t>(maps.v_idx[v])];
|
||||
bool matched = false;
|
||||
for (const auto& row : gold) {
|
||||
if (std::abs(p.x() - row.X) < 1e-9 &&
|
||||
std::abs(p.y() - row.Y) < 1e-9 &&
|
||||
std::abs(p.z() - row.Z) < 1e-9) {
|
||||
EXPECT_NEAR(g, row.G, 1e-12)
|
||||
<< "gradient mismatch at (" << p.x() << "," << p.y()
|
||||
<< "," << p.z() << ")";
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(matched) << "unexpected vertex position";
|
||||
}
|
||||
|
||||
std::vector<double> x0(static_cast<std::size_t>(idx), 0.0);
|
||||
const double dE = spherical_energy(mesh, x, maps)
|
||||
- spherical_energy(mesh, x0, maps);
|
||||
EXPECT_NEAR(dE, 0.16409141487397116, 1e-12);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// FULL-MESH EDGE-DOF golden oracle (spherical, Finding 3) — the missing solution-
|
||||
// level Java oracle for the edge-DOF gradient path. Makes two opposite edges of
|
||||
// the shared tetrahedron variable (replacement parameterization: λ_e = x[e_idx],
|
||||
// no u-shift) and pins BOTH the vertex gradient (Θ−Σα) AND the edge gradient
|
||||
// (α_opp⁺ + α_opp⁻ − θ_e, θ_e = π) bit-for-bit against the REAL upstream
|
||||
// SphericalFunctional.conformalEnergyAndGradient (raw gradient, not evaluate()).
|
||||
//
|
||||
// This is a pure GRADIENT oracle (no ΔE): with an edge DOF, x = 0 means λ_e = 0
|
||||
// ⇒ spherical arc length = π (degenerate), so the path-integral-from-origin
|
||||
// energy reference is ill-defined — the gradient at a fixed non-degenerate x is
|
||||
// the unambiguous Finding-3 quantity. It does NOT touch the spherical Hessian,
|
||||
// so it is unaffected by the Finding-4 edge-DOF Hessian guard.
|
||||
//
|
||||
// To regenerate: /tmp/oracle/SphereEdgeOracle.java. Values are Java %.17g.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
TEST(SphericalGoldenJava, FullMeshEdgeDofGradient_Tetrahedron)
|
||||
{
|
||||
constexpr double TWO_PI = 2.0 * 3.14159265358979323846264338328;
|
||||
constexpr double PI_C = 3.14159265358979323846264338328;
|
||||
constexpr double SCALE = 0.2;
|
||||
|
||||
auto mesh = make_tetrahedron();
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
|
||||
for (auto e : mesh.edges()) {
|
||||
auto h = mesh.halfedge(e);
|
||||
const auto& p1 = mesh.point(mesh.source(h));
|
||||
const auto& p2 = mesh.point(mesh.target(h));
|
||||
const double dx = p1.x()-p2.x(), dy = p1.y()-p2.y(), dz = p1.z()-p2.z();
|
||||
maps.lambda0[e] = 2.0 * std::log(SCALE * std::sqrt(dx*dx + dy*dy + dz*dz));
|
||||
maps.theta_e[e] = PI_C;
|
||||
maps.e_idx[e] = -1;
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
for (auto v : mesh.vertices()) { maps.v_idx[v] = idx++; maps.theta_v[v] = TWO_PI; }
|
||||
|
||||
auto pos = [&](Vertex_index v){ return mesh.point(v); };
|
||||
auto same = [](const Point3& p, double X, double Y, double Z) {
|
||||
return std::abs(p.x()-X)<1e-9 && std::abs(p.y()-Y)<1e-9 && std::abs(p.z()-Z)<1e-9;
|
||||
};
|
||||
// Edge connects positions (aX,aY,aZ)–(bX,bY,bZ) in either order?
|
||||
auto connects = [&](Edge_index e, const double a[3], const double b[3]) {
|
||||
auto h = mesh.halfedge(e);
|
||||
const Point3& s = pos(mesh.source(h));
|
||||
const Point3& t = pos(mesh.target(h));
|
||||
return (same(s,a[0],a[1],a[2]) && same(t,b[0],b[1],b[2])) ||
|
||||
(same(s,b[0],b[1],b[2]) && same(t,a[0],a[1],a[2]));
|
||||
};
|
||||
const double A[3]={1,1,1}, B[3]={1,-1,-1}, C[3]={-1,1,-1}, D[3]={-1,-1,1};
|
||||
|
||||
// Make edges A–B and C–D variable (replacement parameterization).
|
||||
int edof = idx; // edge DOFs start after the 4 vertex DOFs
|
||||
Edge_index eAB, eCD;
|
||||
for (auto e : mesh.edges()) {
|
||||
if (connects(e, A, B)) { maps.e_idx[e] = edof++; eAB = e; }
|
||||
else if (connects(e, C, D)) { maps.e_idx[e] = edof++; eCD = e; }
|
||||
}
|
||||
ASSERT_EQ(edof, 6); // 4 vertex + 2 edge DOFs
|
||||
|
||||
auto u_of = [](const Point3& p){ return 0.10*p.x() - 0.07*p.y() + 0.13*p.z(); };
|
||||
|
||||
std::vector<double> x(6, 0.0);
|
||||
for (auto v : mesh.vertices())
|
||||
x[static_cast<std::size_t>(maps.v_idx[v])] = u_of(mesh.point(v));
|
||||
// Edge DOF value = λ⁰_e + 0.1 (same perturbation as the Java oracle).
|
||||
x[static_cast<std::size_t>(maps.e_idx[eAB])] = maps.lambda0[eAB] + 0.1;
|
||||
x[static_cast<std::size_t>(maps.e_idx[eCD])] = maps.lambda0[eCD] + 0.1;
|
||||
|
||||
auto G = spherical_gradient(mesh, x, maps);
|
||||
|
||||
// Vertex gradients keyed by position.
|
||||
struct VRow { double X, Y, Z, G; };
|
||||
const VRow vg[4] = {
|
||||
{ 1, 1, 1, 2.5036751008175546},
|
||||
{ 1, -1, -1, 2.2303362601050205},
|
||||
{-1, 1, -1, 1.8463710314817632},
|
||||
{-1, -1, 1, 2.7499719487341570},
|
||||
};
|
||||
for (auto v : mesh.vertices()) {
|
||||
const auto& p = mesh.point(v);
|
||||
const double g = G[static_cast<std::size_t>(maps.v_idx[v])];
|
||||
bool m = false;
|
||||
for (auto& r : vg)
|
||||
if (same(p, r.X, r.Y, r.Z)) { EXPECT_NEAR(g, r.G, 1e-12); m = true; break; }
|
||||
EXPECT_TRUE(m);
|
||||
}
|
||||
// Edge gradients: A–B and C–D (Java golden values).
|
||||
EXPECT_NEAR(G[static_cast<std::size_t>(maps.e_idx[eAB])], -0.35189517043413690, 1e-12);
|
||||
EXPECT_NEAR(G[static_cast<std::size_t>(maps.e_idx[eCD])], -0.44101986058895950, 1e-12);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user