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>
18 KiB
Java → C++ Port Audit — Anomalies & Missing Test Cases
Systematic comparison of the C++ port (code/include/*.hpp) against the original
Java reference (~/Desktop/conformallab,
de.varylab.discreteconformal.functional.*). Focus: math correctness, logic, and
faithfulness to the reference.
Status legend: ✅ fixed · ⚠️ open / needs decision · ℹ️ note (no action)
Files compared (math-critical)
| Java | C++ | Verdict |
|---|---|---|
Clausen.java |
clausen.hpp |
✅ faithful (incl. inits return, clausen2, Л, ImLi2) |
EuclideanCyclicFunctional.java |
euclidean_functional.hpp, euclidean_geometry.hpp |
✅ angles + gradient assembly match; degenerate handling fixed |
— Hessian (triangleHessian/conformalHessian) |
euclidean_hessian.hpp |
cotangent + vertex block match; edge block not implemented (now guarded) |
SphericalFunctional.java |
spherical_functional.hpp, spherical_geometry.hpp |
✅ vertex-mode + edge-DOF replacement parameterization now match (Finding 3) |
HyperIdealFunctional.java |
hyper_ideal_functional.hpp, hyper_ideal_geometry.hpp |
✅ faithful (incl. degenerate (π,0,0) angles) |
HyperIdealUtility.java |
hyper_ideal_utility.hpp |
✅ ζ, ζ₁₃, ζ₁₄, ζ₁₅, both tetrahedron-volume formulas match |
CPEuclideanFunctional.java |
cp_euclidean_functional.hpp |
✅ p(), gradient, energy match |
DiscreteEllipticUtility.java |
discrete_elliptic_utility.hpp, period_matrix.hpp |
✅ normalizeModulus faithful and now wired into compute_period_matrix (Finding 6) |
HomologyUtility.java, CanonicalBasisUtility.java, SpanningTreeUtility.java |
cut_graph.hpp |
tree-cotree only; no canonical/symplectic basis ℹ️ (Finding 7) |
EuclideanLayout.java (holonomy) |
layout.hpp (detail::euclidean_holonomy) |
translation via midpoint displacement; ignores residual rotation ℹ️ (Finding 8) |
Findings
1. ✅ FIXED — Degenerate triangle handling (Euclidean + Spherical gradient)
The Java reference assigns the limiting corner angles to a degenerate (triangle-inequality-violating) face — the corner opposite the over-long edge is π, the other two 0. This is the convex C¹ extension that keeps the BPS energy well-defined on the infeasible region, so Newton can pass through a flip without stalling.
The C++ port returned {0,0,0} and skipped the whole face in the gradient.
Tell-tale: the hyper-ideal port already mirrors Java's (π,0,0) fallback —
euclidean/spherical were inconsistent oversights.
Fix: euclidean_geometry.hpp / spherical_geometry.hpp degenerate branches
now return the limiting angles (still valid=false, so the cotangent Hessian
keeps skipping — matching Java, whose triangleHessian zeroes cotangents on
degenerate faces). euclidean_functional.hpp / spherical_functional.hpp
gradients no longer skip. All 237 tests pass; solution unchanged (only affects
evaluations that previously contributed zero).
2. ✅ FIXED — euclidean_hessian missing edge-DOF guard
Doc comment claimed "the function asserts that no edge DOF is variable" — but
no such guard existed. With edge DOFs assigned
(assign_euclidean_all_dof_indices), the Java conformalHessian's edge-edge and
vertex-edge blocks are absent, so the C++ would silently build a Hessian with
all-zero edge rows/cols (singular → LDLT fails → QR least-squares garbage).
Fix: added an always-compiled throw std::logic_error guard.
3. ✅ FIXED — Spherical edge-DOF parameterization now matches Java
Java's spherical functional drops the u_i + u_j vertex terms when an edge is
a variable (replacement parameterization, SphericalFunctional.java:398–400),
whereas the C++ previously used additive Λ = λ⁰ + u_i + u_j + λ_e always. That
made the C++ spherical edge gradient carry an extra −(S_f⁺+S_f⁻)/2 term absent
from Java's G.add(i, αk + αl − π) (SphericalFunctional.java:283–292).
Fix: added helper spher_eff_lambda implementing the replacement convention
(Λ_ij = λ_e when the edge carries a DOF, else λ⁰ + u_i + u_j); spherical_gradient
Pass 1 now uses it, and the edge gradient is α_opp⁺ + α_opp⁻ − θ_e (θ_e default
π), exactly matching Java. The energy is the path integral of this gradient, so it
follows automatically. The vertex-only path is bit-for-bit unchanged (no edge
DOFs → identical to before). All 237 tests pass.
4. ✅ FIXED — Spherical Hessian edge-DOF guard added
spherical_hessian.hpp builds only the vertex block; Java's spherical
conformalHessian includes edge terms. Same limitation as the Euclidean Hessian
but previously without the edge-DOF guard added in Finding 2.
Fix: added an always-compiled throw std::logic_error guard at the top of
spherical_hessian (mirrors Finding 2), so any attempt to use edge DOFs with the
spherical Hessian fails loudly instead of silently building a singular matrix.
All 237 tests pass.
5. ℹ️ NOTE — CP-Euclidean energy uses clausen2, Java uses clausen
CPEuclideanFunctional.java:226 calls Clausen.clausen (the BORDERLINE-2.0944
polynomial); the C++ energy calls clausen2 (the Chebyshev variant). Both compute
the same Cl₂ integral to ~machine precision, and the term appears only in the
energy (not the Newton-driving gradient). No functional impact. The C++ never
ported the clausen() variant at all — harmless.
6. ✅ FIXED — Period-matrix reduction now uses the faithful Java port
Two reductions coexist:
discrete_elliptic_utility.hpp::normalizeModulusis a faithful line-by-line port of JavaDiscreteEllipticUtility.normalizeModulus— it folds τ into0 ≤ Re(τ) ≤ ½,Im(τ) ≥ 0,|τ| ≥ 1(the extraRe ≥ 0fold uses the mirror symmetry τ ≅ −τ̄).period_matrix.hpp::reduce_to_fundamental_domainreduces to the standard SL(2,ℤ) domain−½ ≤ Re(τ) < ½,|τ| ≥ 1(noRe ≥ 0fold).
compute_period_matrix (the production path, and the one the torus_8x8 τ test
exercises) calls reduce_to_fundamental_domain, so normalizeModulus is
never used outside its own unit test (verified by grep). Consequence: the C++
production τ can land with Re(τ) < 0 where Java would report the mirrored
+|Re(τ)|. Same conformal type up to orientation, but not equal to the Java
oracle value — this will bite any golden-value cross-check (missing-test item 5).
Fix (option a): compute_period_matrix now calls normalizeModulus, so the
production τ matches the Java oracle exactly (folds into 0 ≤ Re ≤ ½, Im ≥ 0,
|τ| ≥ 1 via the mirror symmetry). normalizeModulus is no longer dead code. The
existing torus_8x8 τ test still passes (Re ∈ [0,½] ⊂ [−½,½] so it satisfies
is_in_fundamental_domain too). reduce_to_fundamental_domain is retained for
callers that explicitly want the canonical (non-mirror-folded) SL(2,ℤ) domain.
7. ℹ️ NOTE — Cut graph is tree-cotree, not a canonical homology basis
cut_graph.hpp (compute_cut_graph) runs the Erickson–Whittlesey tree-cotree
algorithm: primal BFS tree, dual BFS cotree, the remaining 2g edges generate
H₁. Java's CanonicalBasisUtility.getCanonicalHomologyBasis goes further — it
builds the intersection form on the generators and solves for a canonical
symplectic basis {a₁..a_g, b₁..b_g} (and uses weighted shortest-path
cycles, not unweighted BFS).
- Genus 1: harmless — there is only one generator pair, and
compute_period_matrixreduces τ to the fundamental domain, so the basis choice washes out. The common case matches. - Genus > 1: the C++
omega[]ordering is arbitrary (edge-iteration order), soτ = ω₂/ω₁is not the canonical Riemann period matrix. This is already flagged in theperiod_matrix.hppheader ("Computing Ω from holonomy data requires integration of holomorphic differentials — not implemented") so it is consistent with the documented contract, not a regression.
8. ✅ FIXED — Holonomy now fits the full deck isometry (was: midpoint only)
detail::euclidean_holonomy (layout.hpp) develops each face along a dual tree
that never crosses a cut edge. It previously read each generator's
translation as the midpoint displacement ω = midA − midB of the shared cut
edge between its two independent developments — exact only when the two
developments differ by a pure translation (linear part = identity), i.e. a
perfectly flat cone metric (Θ ≡ 2π). Under residual cone-angle defect that was
a first-order approximation.
Fix (2026-05-29): it now fits the full orientation-preserving deck isometry
g(z) = a·z + b to the correspondence (Bs,Bt) ↦ (As,At) (a = (At−As)/(Bt−Bs),
b = As − a·Bs). Because both faces use the same edge length, |a| = 1 exactly,
so the fit is an exact isometry; ω = b is its translation part and |arg(a)|
is exposed per cut edge as HolonomyData::residual_rotation (a convergence
diagnostic). For a flat cone metric a = 1 and b reduces to midA − midB, so
converged flat tori are bit-for-bit unchanged — all 240 CGAL tests still pass,
including HolonomyEndToEnd.{Torus4x4,TorusHex6x6,Torus8x8}. Note: when
|arg(a)| is non-negligible the holonomy is a genuine rotation and no lattice
translation is well-defined; the diagnostic now surfaces that instead of silently
averaging.
Findings in non-ported (conformallab++-only) math modules
These modules have no Java reference — they were ported from the literature (Luo 2004, Glickenstein 2011, Bowers–Stephenson 2004) or are original additions. They were audited against the cited formulas / first principles rather than Java.
9. ✅ FIXED — Inversive-distance gradient now uses limiting angles (consistent with Finding 1)
inversive_distance_functional.hpp::inversive_distance_gradient previously detected
a triangle-inequality-violating face two ways and skipped it in both:
if (l12sq <= 0 || l23sq <= 0 || l31sq <= 0) continue;(non-real circle config)if (!fa.valid) continue;(degenerate-but-real triangle)
The second skip was the same situation Finding 1 fixed for Euclidean/Spherical:
euclidean_angles returns the limiting angles (π opposite the over-long edge,
0/0 for the others) with valid=false, but this code threw them away.
Fix: removed the if (!fa.valid) continue; skip so the limiting angles flow
into the gradient (the BPS-style convex C¹ extension), mirroring Finding 1; added a
comment explaining the rationale. The first skip (l*sq <= 0, a genuinely non-real
circle configuration with no limiting angle) is kept. All 237 tests pass.
10. ℹ️ NOTE — Inversive-distance edge length has no overflow centring
id_detail::edge_length_squared computes ℓ² = rᵢ² + rⱼ² + 2·I·rᵢ·rⱼ with
r = exp(u) directly — no log-centring like euclidean_angles's μ. The angle
computation is still safe (it re-centres internally on log ℓ²), but the raw ℓ²
can overflow for extreme |u|. Cosmetic robustness only; not hit in practice.
11. ℹ️ NOTE — Verified correct (no action)
Audited against the literature and found faithful:
trilaterate_2d / _sph / _hyp(layout.hpp) — circle-circle intersection (ℝ²), spherical law of cosines viaα·pa+β·pb+γ·(pa×pb), and hyperbolic law of cosines + Poincaré-disk Möbius placement. All three exact; left/CCW side correct.newton_solver.hpp— meritf = ½‖G‖²; Newton dir is always a descent dir (∇f·dx = −‖G‖²), Armijo tests algebraically correct in both phases; spherical solver factorises−H(PSD) andd_sd = −H·G = −∇fuses the un-negated H — all consistent. SparseQR fallback handles the gauge null space (valid because Gauss–Bonnet makesG ⟂ 𝟙).gauss_bonnet.hpp—χ=V−E+F,g=(2−χ)/2,enforceshiftδ=(lhs−rhs)/Vmakes the new sum equal2π·χexactly.- Normalisations — Euclidean PCA, spherical Rodrigues (mean→north), hyperbolic weighted Fréchet-mean Möbius centring: all correct (spherical has a benign antipodal-mean edge case that no-ops).
Missing test cases (gaps observed during the audit)
-
Degenerate / flipped-triangle gradient — no test drives a face through the triangle-inequality boundary to lock in Finding 1. Add: build a triangle with one edge length > sum of the others, assert the gradient picks up the π corner (not 0). Cover both Euclidean and Spherical.
-
euclidean_hessianedge-DOF guard — no test asserts the newthrowfires whenassign_euclidean_all_dof_indicesis used. Add anEXPECT_THROW. -
✅ DONE (2026-05-29) — Holonomy / period matrix end-to-end now covers three tori of revolution (
HolonomyEndToEnd.{Torus4x4,TorusHex6x6,Torus8x8}), each asserting τ against the analytic modulusi·√(R²−r²)/randRe(τ) ≈ 0. (This also fixed the Euclidean holonomy extraction — develop across the dual tree only — seelayout.hpp/cut_graph.hpp.) -
✅ DONE (2026-05-29) — the Finding-3 spherical edge-DOF gradient is now locked at two levels: (a)
EdgeGradient_RegularTetClosedForm— an independent closed-form oracle pinning each edge-DOF gradient toπ/3(= 2·(2π/3) − π) on the regular spherical tetrahedron; and (b)SphericalGoldenJava.FullMeshEdgeDofGradient_Tetrahedron— a live-Java full-mesh oracle (item 5) on a generic asymmetric configuration that pins BOTH the vertex gradient AND the edge gradientα_opp⁺ + α_opp⁻ − θ_eto 1e-12 vs the real rawconformalEnergyAndGradient. The vertex-DOF spherical path is likewise locked bySphericalGoldenJava.FullMeshGradientAndEnergy_…. Still open (solver feature, not a correctness gap): Newton-to-convergence with edge DOFs is blocked by the Finding-4 spherical-Hessian guard (throwon edge DOFs), so a converged-metric test needs an FD-Hessian or guard relaxation first. -
✅ DONE (2026-05-29) — golden-value oracles now span both levels: • pure-math / per-triangle (1e-12 vs compiled upstream, openjdk 17):
HyperIdealGoldenJava(Clausen/Л/ImLi₂, ζ₁₃/₁₄/₁₅/ζ, both tetrahedron-volume formulas),EuclideanGoldenJava(angle formula + 2·Л energy),SphericalGoldenJava(law-of-cosines angles + β relations + Л energy),PeriodMatrix.NormalizeModulus_GoldenJava(item 8). • full-mesh energy + gradient at a knownx— the originally-fragile part, now solved via a shared OBJ tetrahedron loaded identically in both languages:EuclideanGoldenJava.FullMeshGradientAndEnergy_TetrahedronandSphericalGoldenJava.FullMeshGradientAndEnergy_Tetrahedrondrive the REALEuclideanCyclicFunctional/SphericalFunctionaland pin every per-vertex gradientΘ−Σα(matched by vertex position) ANDΔE = E(x)−E(0)to 1e-12. The ΔE check is doubly independent: C++ computes energy as a Gauss-Legendre PATH INTEGRAL of its gradient, Java as a CLOSED-FORM sum — the initialEnergy constant and the φ·λ⁰ term cancel in the difference (no edge DOFs here). • full-mesh EDGE-DOF gradient —SphericalGoldenJava.FullMeshEdgeDofGradient_Tetrahedronmakes two opposite tetrahedron edges variable (replacement parameterization λ_e = x[e_idx]) and pins BOTH the vertex gradient AND the edge gradientα_opp⁺+α_opp⁻−θ_eto 1e-12 vs the real rawconformalEnergyAndGradient, closing the Finding-3 solution-level gap (item 4). It is a pure gradient oracle (no ΔE): with an edge DOF, x=0 ⇒ λ_e=0 ⇒ arc length π is degenerate, so the path-integral-from- origin energy reference is ill-defined; the fixed-x gradient is the unambiguous quantity. It never touches the Hessian, so the Finding-4 edge-DOF Hessian guard does not block it. Harness:/tmp/oracle/SphereEdgeOracle.java. Subtlety surfaced: the spherical oracle must call Java's rawconformalEnergyAndGradient, notevaluate();evaluate()first runs a 1-D Brent maximization over the global-scale gauge (maximizeInNegativeDirection), which the C++spherical_gradientdeliberately omits (C++ factors that gauge into the Newton solver'sspherical_gauge_shift). Harnesses:/tmp/oracle/{tet.obj,EucMeshOracle.java,SphereMeshOracle.java,SphereEdgeOracle.java}. The edge-DOF full-mesh gradient oracle is now also done (item 4,SphereEdgeOracle.java). -
CP-Euclidean
clausenvsclausen2— a single test asserting|clausen(x) − clausen2(x)| < 1e-12across a sweep would document Finding 5 and guard against an approximation regression. -
⚠️ PARTLY DONE (2026-05-29) — the
Re ≥ 0fold convention is now locked at the function level by item 8 (NormalizeModulus_GoldenJava, includingRe < 0inputs). Still open: an end-to-end torus whose true τ hasRe(τ) < 0before reduction, asserting the production pipeline yieldsRe(τ) ≥ 0(so any future switch back toreduce_to_fundamental_domainis caught at the mesh level, not just the function level). -
✅ DONE (2026-05-29) —
PeriodMatrix.NormalizeModulus_GoldenJavapins the C++normalizeModulusagainst five recorded outputs of the real JavaDiscreteEllipticUtility.normalizeModulus(1e-12), includingRe < 0mirror folds, large-ReT-steps, and|τ| < 1S-inversions — locking the0 ≤ Re ≤ ½fold convention. (Generated via/tmp/oracle/TauOracle.java.) This also surfaced a real interaction:compute_period_matrixcan land exactly onRe = +½(the closed right edge ofnormalizeModulus's mirror- folded domain), which the half-open SL(2,ℤ)is_in_fundamental_domain(−½ ≤ Re < ½, P1-3) correctly excludes;ComputePeriodMatrix_ReducedTau_InFDwas updated to assert thenormalizeModulusdomain rather than the SL(2,ℤ) one. -
Higher-genus cut graph (Finding 7) — only genus-1 is exercised end-to-end. Add a genus-2 mesh and assert
compute_cut_graphreturns exactly2g = 4cut edges andgenus == 2, documenting that the basis is not canonical (so no τ correctness is claimed there). -
Inversive-distance degenerate face (Finding 9) — no test drives an inversive-distance triangle through the triangle-inequality boundary. Now that Finding 9 mirrors Finding 1 (limiting angles used, not skipped), add the analogue of missing-test item 1: assert the corner opposite the over-long edge is picked up as π rather than silently skipped.