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:
@@ -26,6 +26,7 @@
|
||||
#include "euclidean_geometry.hpp"
|
||||
#include "euclidean_functional.hpp"
|
||||
#include "euclidean_hessian.hpp"
|
||||
#include "clausen.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
@@ -290,3 +291,117 @@ TEST(EuclideanFunctional, GradientCheck_MixedPinnedVertices)
|
||||
EXPECT_TRUE(gradient_check_euclidean(mesh, x, maps))
|
||||
<< "Gradient check failed for mixed pinned/variable vertices";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Golden-value oracle — pin the Euclidean angle formula and the 2·Л(α) energy
|
||||
// term bit-for-bit against the upstream Java reference (EuclideanCyclicFunctional
|
||||
// .triangleEnergyAndAlphas, lines 341-361), captured by running the compiled Java
|
||||
// library (openjdk 17) with the real de.varylab…Clausen.Л on these exact edge
|
||||
// lengths. Companion to HyperIdealGoldenJava and SphericalGoldenJava: locks the
|
||||
// absolute angle/energy values against an independent implementation, catching
|
||||
// silent index/sign drift the curl-free path-integral gradient check cannot see.
|
||||
//
|
||||
// To regenerate: /tmp/oracle/EucOracle.java. Values are Java printf %.17g.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
TEST(EuclideanGoldenJava, AngleAndLobachevskyEnergyFromLengths)
|
||||
{
|
||||
auto check = [](double l12, double l23, double l31,
|
||||
double a1_g, double a2_g, double a3_g, double L_g) {
|
||||
auto fa = euclidean_angles_from_lengths(l12, l23, l31);
|
||||
EXPECT_TRUE(fa.valid);
|
||||
EXPECT_NEAR(fa.alpha1, a1_g, 1e-12);
|
||||
EXPECT_NEAR(fa.alpha2, a2_g, 1e-12);
|
||||
EXPECT_NEAR(fa.alpha3, a3_g, 1e-12);
|
||||
const double Lterm = 2.0 * Lobachevsky(fa.alpha1)
|
||||
+ 2.0 * Lobachevsky(fa.alpha2)
|
||||
+ 2.0 * Lobachevsky(fa.alpha3);
|
||||
EXPECT_NEAR(Lterm, L_g, 1e-12);
|
||||
};
|
||||
check(1.0, 1.2, 0.9,
|
||||
1.3637649752769678, 0.82416964552030680, 0.95365803279251860,
|
||||
1.9456273836230942);
|
||||
check(1.0, 1.0, 1.0,
|
||||
1.0471975511965979, 1.0471975511965979, 1.0471975511965979,
|
||||
2.0298832128193070);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// FULL-MESH golden oracle — the strongest cross-check: drives the REAL upstream
|
||||
// EuclideanCyclicFunctional (openjdk 17) on a tetrahedron loaded from a shared
|
||||
// OBJ (identical topology + geometry to make_tetrahedron()), and pins BOTH the
|
||||
// per-vertex gradient G_v = Θ_v − Σα AND the energy difference ΔE = E(x) − E(0)
|
||||
// bit-for-bit against it.
|
||||
//
|
||||
// This closes audit missing-test item 5 (full-mesh energy + gradient at a known
|
||||
// x). It is genuinely independent of the C++ implementation in two ways:
|
||||
// • the gradient is the upstream library's own analytic gradient (not an FD
|
||||
// check, which only proves curl-freeness of the C++ self-consistent energy);
|
||||
// • the energy is Java's CLOSED-FORM functional value, whereas C++ computes it
|
||||
// as a Gauss-Legendre PATH INTEGRAL of its gradient — two different methods
|
||||
// that must agree on ΔE (the initialEnergy constant and the φ·λ⁰ term cancel
|
||||
// in the difference, and there are no edge DOFs here).
|
||||
//
|
||||
// Setup parity (verified against UnwrapUtility.prepareInvariantDataEuclidean):
|
||||
// closed mesh, ALL 4 vertices variable (no pin), Θ_v = 2π, no edge DOFs,
|
||||
// λ°_e = 2·log(|p_i − p_j|), per-vertex u(P) = 0.10·X − 0.07·Y + 0.13·Z.
|
||||
//
|
||||
// To regenerate: /tmp/oracle/{tet.obj,EucMeshOracle.java}. Values are Java %.17g.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
TEST(EuclideanGoldenJava, FullMeshGradientAndEnergy_Tetrahedron)
|
||||
{
|
||||
constexpr double TWO_PI = 2.0 * 3.14159265358979323846264338328;
|
||||
|
||||
auto mesh = make_tetrahedron(); // same 4 vertices as /tmp/oracle/tet.obj
|
||||
auto maps = setup_euclidean_maps(mesh);
|
||||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
// All four vertices are DOFs (Java prepareInvariantData makes every interior
|
||||
// vertex variable on a closed mesh); Θ_v = 2π; no edge DOFs.
|
||||
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 = euclidean_gradient(mesh, x, maps);
|
||||
|
||||
// Java golden gradients keyed by vertex position (order-independent lookup).
|
||||
struct GoldRow { double X, Y, Z, G; };
|
||||
const GoldRow gold[4] = {
|
||||
{ 1, 1, 1, 3.5277511803984396},
|
||||
{ 1, -1, -1, 3.2837611905358440},
|
||||
{-1, 1, -1, 2.3437485291237100},
|
||||
{-1, -1, 1, 3.4111097143011780},
|
||||
};
|
||||
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";
|
||||
}
|
||||
|
||||
// ΔE = E(x) − E(0). C++ path integral vs Java closed-form functional value.
|
||||
std::vector<double> x0(static_cast<std::size_t>(idx), 0.0);
|
||||
const double dE = euclidean_energy(mesh, x, maps)
|
||||
- euclidean_energy(mesh, x0, maps);
|
||||
EXPECT_NEAR(dE, 0.15962619236187336, 1e-12);
|
||||
}
|
||||
|
||||
@@ -338,12 +338,52 @@ TEST(PeriodMatrix, ComputePeriodMatrix_UnitSquare)
|
||||
TEST(PeriodMatrix, ComputePeriodMatrix_ReducedTau_InFD)
|
||||
{
|
||||
// ω_1 = (1, 0), ω_2 = (0.5, 0.25) → τ = 0.5 + 0.25i
|
||||
// |τ| = sqrt(0.25 + 0.0625) ≈ 0.559 < 1 → needs S step
|
||||
// |τ| = sqrt(0.25 + 0.0625) ≈ 0.559 < 1 → needs S step.
|
||||
// compute_period_matrix reduces with normalizeModulus (Finding 6), whose
|
||||
// mirror-folded target domain is { 0 ≤ Re ≤ ½, Im > 0, |τ| ≥ 1 } — note the
|
||||
// RIGHT boundary Re = +½ is CLOSED here. This is NOT the half-open SL(2,ℤ)
|
||||
// domain of is_in_fundamental_domain (−½ ≤ Re < ½), which excludes Re = +½
|
||||
// because +½ ≡ −½ under T. For this input normalizeModulus lands exactly on
|
||||
// τ = ½ + i, so we must check the normalizeModulus domain, not the SL(2,ℤ)
|
||||
// one (asserting is_in_fundamental_domain here would wrongly fail on +½).
|
||||
HolonomyData hol;
|
||||
hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.5, 0.25) };
|
||||
PeriodData pd = compute_period_matrix(hol, /*reduce=*/true);
|
||||
EXPECT_TRUE(pd.in_fundamental_domain);
|
||||
EXPECT_TRUE(is_in_fundamental_domain(pd.tau, 1e-9));
|
||||
const double tol = 1e-9;
|
||||
EXPECT_GT(pd.tau.imag(), 0.0);
|
||||
EXPECT_GE(pd.tau.real(), 0.0 - tol); // 0 ≤ Re (mirror fold)
|
||||
EXPECT_LE(pd.tau.real(), 0.5 + tol); // Re ≤ ½ (closed right edge)
|
||||
EXPECT_GE(std::abs(pd.tau), 1.0 - tol); // |τ| ≥ 1
|
||||
// Concretely: τ = ½ + i.
|
||||
EXPECT_NEAR(pd.tau.real(), 0.5, 1e-12);
|
||||
EXPECT_NEAR(pd.tau.imag(), 1.0, 1e-12);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Golden-value oracle — pin normalizeModulus (the τ reduction used by
|
||||
// compute_period_matrix, Finding 6) bit-for-bit against the upstream Java
|
||||
// reference (de.varylab.discreteconformal.util.DiscreteEllipticUtility.
|
||||
// normalizeModulus), captured by calling the compiled Java method (openjdk 17)
|
||||
// on these exact τ. This locks the sign/fold conventions of the SL(2,ℤ)+mirror
|
||||
// reduction (0 ≤ Re ≤ ½, Im ≥ 0, |τ| ≥ 1) against an independent implementation,
|
||||
// catching drift the existing in-FD membership checks cannot (they only assert
|
||||
// the result lies in F, not that it is the SAME representative Java picks).
|
||||
//
|
||||
// To regenerate: /tmp/oracle/TauOracle.java. Values are Java printf %.17g.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
TEST(PeriodMatrix, NormalizeModulus_GoldenJava)
|
||||
{
|
||||
auto chk = [](double re, double im, double re_g, double im_g) {
|
||||
C n = conformallab::normalizeModulus(C(re, im));
|
||||
EXPECT_NEAR(n.real(), re_g, 1e-12);
|
||||
EXPECT_NEAR(n.imag(), im_g, 1e-12);
|
||||
};
|
||||
chk(0.3, 0.5, 0.11764705882352933, 1.4705882352941178); // |τ|<1 → S + folds
|
||||
chk(-0.4, 1.3, 0.40000000000000000, 1.3000000000000000); // Re<0 mirror fold
|
||||
chk(2.7, 0.8, 0.41095890410958880, 1.0958904109589043); // large Re → T
|
||||
chk(0.1, 2.0, 0.10000000000000000, 2.0000000000000000); // already in F
|
||||
chk(-1.6, 0.9, 0.41237113402061850, 0.92783505154639180); // T + S + mirror
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
// Port of de.varylab.discreteconformal.functional.HyperIdealUtilityTest (Java/JUnit).
|
||||
|
||||
#include "hyper_ideal_utility.hpp"
|
||||
#include "hyper_ideal_geometry.hpp"
|
||||
#include "clausen.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <complex>
|
||||
|
||||
using conformallab::calculateTetrahedronVolume;
|
||||
using conformallab::calculateTetrahedronVolumeWithIdealVertexAtGamma;
|
||||
@@ -93,3 +95,43 @@ TEST(HyperIdealUtilityTest, CompareGeneralAndIdealFormulaCase2) {
|
||||
double V = calculateTetrahedronVolume(bi, bj, bk, ai, aj, ak);
|
||||
EXPECT_NEAR(Ve, V, 1e-12);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Golden-value oracle tests — pin the C++ pure-math core bit-for-bit against the
|
||||
// upstream Java reference (de.varylab.discreteconformal.functional.{Clausen,
|
||||
// HyperIdealUtility}), captured by running the compiled Java library (openjdk 17)
|
||||
// on these exact inputs. Unlike the FD gradient checks (which only verify
|
||||
// curl-freeness, since the C++ energy is the path-integral of its own gradient),
|
||||
// these lock the absolute values of the math-critical helpers against an
|
||||
// independent implementation, catching any silent convention/formula drift.
|
||||
//
|
||||
// To regenerate: see doc/reviewer/java-port-audit.md (oracle harness recipe).
|
||||
// Values are Java's Double.toString output (shortest round-trip). Tolerance is
|
||||
// 1e-12 (well above the ~1e-15 inter-platform libm divergence for these ranges).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
TEST(HyperIdealGoldenJava, ClausenLobachevskyImLi2) {
|
||||
EXPECT_NEAR(conformallab::clausen2(0.7), 0.954448086482735, 1e-12);
|
||||
EXPECT_NEAR(conformallab::clausen2(2.5), 0.4335982032355327, 1e-12);
|
||||
EXPECT_NEAR(conformallab::clausen2(-1.3), -0.9897032532295984, 1e-12);
|
||||
EXPECT_NEAR(Lobachevsky(0.9), 0.4121734067662043, 1e-12);
|
||||
EXPECT_NEAR(conformallab::ImLi2(std::complex<double>(0.3, 0.4)),
|
||||
0.46136289181910894, 1e-12);
|
||||
}
|
||||
|
||||
TEST(HyperIdealGoldenJava, ZetaFamily) {
|
||||
EXPECT_NEAR(conformallab::zeta13(0.5, 0.7, 0.9), 2.663195966482385, 1e-12);
|
||||
EXPECT_NEAR(conformallab::zeta14(0.4, 0.8), 1.8262295633065202, 1e-12);
|
||||
EXPECT_NEAR(conformallab::zeta15(0.6), 2.216976794676588, 1e-12);
|
||||
EXPECT_NEAR(conformallab::zeta(0.5, 0.7, 0.9), 1.6156519307269948, 1e-12);
|
||||
}
|
||||
|
||||
TEST(HyperIdealGoldenJava, TetrahedronVolumes) {
|
||||
// Generic non-degenerate generalized hyperbolic tetrahedron.
|
||||
EXPECT_NEAR(calculateTetrahedronVolume(0.6, 0.7, 0.8, 0.5, 0.9, 0.4),
|
||||
2.9074633382516435, 1e-12);
|
||||
// One ideal vertex at gamma, non-symmetric angles (Java test2 inputs).
|
||||
EXPECT_NEAR(calculateTetrahedronVolumeWithIdealVertexAtGamma(
|
||||
0.6623267054958116, 1.437248992086214, 1.0420169560077686,
|
||||
0.6896178197389236, 0.5195634857410114, 0.6304500578493993),
|
||||
2.0459750326926214, 1e-12);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user