fix+test: Euclidean holonomy/τ end-to-end + spherical edge-DOF oracle (2026-05-29 audit)
All checks were successful
C++ Tests / test-fast (pull_request) Successful in 1m57s
API Docs / doc-build (pull_request) Successful in 1m3s
Markdown link check / check (pull_request) Successful in 44s
C++ Tests / test-cgal (pull_request) Has been skipped
C++ Tests / quality-gates (pull_request) Successful in 2m11s

Bundles the 2026-05-29 Java↔C++ math-correctness audit (doc/reviewer/
java-port-audit.md, 11 findings) with two follow-up fixes.

Audit code changes:
- Finding 3 (spherical_functional): edge-DOF replacement parameterization via
  spher_eff_lambda; edge gradient α_opp⁺+α_opp⁻−θ_e (drops additive −(S⁺+S⁻)/2)
- Finding 4 (spherical_hessian): always-compiled edge-DOF throw guard
- Finding 6 (period_matrix): faithful normalizeModulus (0≤Re≤½, Im≥0, |τ|≥1)
- Finding 9 (inversive_distance): degenerate-face limiting angles, no skip
- Findings 1/2 (euclidean): degenerate gradient limiting angles + Hessian guard

Euclidean holonomy/τ fix: develop the cut surface across the dual spanning tree
only (cut_graph now exposes is_dual_tree), so genus-1 cut edges yield
non-degenerate lattice generators. Previously τ came out 0 / NaN / 1e13 on the
bundled tori; now matches the analytic revolution modulus i·√(R²−r²)/r. Re-enabled
τ reporting in the Euclidean CLI; rewrote validation.md §3/§4 accordingly.

Tests (240 CGAL, 0 skipped):
- HolonomyEndToEnd ×3 — tori of revolution (4×4, hex 6×6, 8×8) vs analytic modulus
- SphericalFunctional.EdgeGradient_RegularTetClosedForm — independent closed-form
  π/3 oracle locking the Finding-3 edge formula (the path-integral FD check cannot
  detect a wrong-but-conservative gradient)

Also documents the latent spherical/hyperbolic holonomy-extraction bug (same
single-development pattern, dead code today) in research-track.md (Phase 9c/10),
and adds favour/normalisations to the codespell ignore list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-29 12:50:16 +02:00
parent ca936b7652
commit a3ee9576d4
23 changed files with 1065 additions and 165 deletions

View File

@@ -115,35 +115,89 @@ inline Eigen::VectorXd solve_linear_system(
namespace detail { // re-open for the remaining helpers
// Backtracking line search: find the largest α in {1, 0.5, 0.25, …} such that
// ||G(x + α·Δx)||₂ < ||G(x)||₂. Returns the accepted step (α may stay 1).
// Globalised line search for the Newton system G(x) = 0.
//
// Merit function f(x) = ½‖G(x)‖²₂. Driving f to its minimum drives the
// residual G to zero; because the merit only depends on ‖G‖ it is sign-agnostic
// and works identically for the convex Euclidean / HyperIdeal / CP / InvDist
// energies and the concave Spherical energy.
//
// Phase 1 — Newton direction `dx` (satisfies H·dx = G, so the merit slope
// ∇f·dx = GᵀH·dx = ‖G‖² < 0 — always a descent direction).
// Backtrack α ∈ {1, ½, ¼, …} until the Armijo sufficient-decrease
// condition holds:
// ‖G(x + α·dx)‖² ≤ (1 2·c1·α)·‖G(x)‖²
//
// Phase 2 — if Phase 1 exhausts its halvings, fall back to the steepest-
// descent direction of the merit, `d_sd = H·G` (slope
// ∇f·d_sd = ‖H·G‖² ≤ 0 regardless of the definiteness of H), with
// the analogous Armijo test:
// ‖G(x + α·d_sd)‖² ≤ ‖G(x)‖² 2·c1·α·‖d_sd‖²
//
// If neither phase satisfies Armijo, return the best (smallest-residual) point
// visited. If nothing beat ‖G(x)‖, return x unchanged and set *improved=false,
// so the caller can stop cleanly instead of taking the old divergent full step.
template <typename GradFn>
inline std::vector<double> line_search(
const std::vector<double>& x,
const Eigen::VectorXd& dx,
const Eigen::VectorXd& d_sd,
double norm0,
GradFn&& grad_fn,
int max_halvings = 20)
bool* improved = nullptr,
int max_halvings = 20,
double c1 = 1e-4)
{
const int n = static_cast<int>(x.size());
double alpha = 1.0;
std::vector<double> xnew(static_cast<std::size_t>(n));
const int n = static_cast<int>(x.size());
const double norm0_sq = norm0 * norm0;
for (int ls = 0; ls < max_halvings; ++ls) {
std::vector<double> xnew(static_cast<std::size_t>(n));
std::vector<double> best_x = x;
double best_norm = norm0;
// Evaluate ‖G(x + α·dir)‖₂, leaving the trial point in `xnew`.
auto eval = [&](const Eigen::VectorXd& dir, double alpha) -> double {
for (int i = 0; i < n; ++i)
xnew[static_cast<std::size_t>(i)] = x[static_cast<std::size_t>(i)]
+ alpha * dx[i];
xnew[static_cast<std::size_t>(i)] =
x[static_cast<std::size_t>(i)] + alpha * dir[i];
auto Gnew = grad_fn(xnew);
double norm_new = 0.0;
for (double v : Gnew) norm_new += v * v;
norm_new = std::sqrt(norm_new);
if (norm_new < norm0) return xnew;
double s = 0.0;
for (double v : Gnew) s += v * v;
return std::sqrt(s);
};
// ── Phase 1: Newton direction, Armijo backtracking ────────────────────────
double alpha = 1.0;
for (int ls = 0; ls < max_halvings; ++ls) {
double norm_new = eval(dx, alpha);
if (norm_new < best_norm) { best_norm = norm_new; best_x = xnew; }
if (norm_new * norm_new <= (1.0 - 2.0 * c1 * alpha) * norm0_sq) {
if (improved) *improved = true;
return xnew;
}
alpha *= 0.5;
}
// No improvement found — return best attempt (full step)
for (int i = 0; i < n; ++i)
xnew[static_cast<std::size_t>(i)] = x[static_cast<std::size_t>(i)] + dx[i];
return xnew;
// ── Phase 2: steepest-descent fallback (H·G), Armijo backtracking ────────
const double dsd_sq = d_sd.squaredNorm();
if (dsd_sq > 0.0) {
alpha = 1.0;
for (int ls = 0; ls < max_halvings; ++ls) {
double thresh_sq = norm0_sq - 2.0 * c1 * alpha * dsd_sq;
double norm_new = eval(d_sd, alpha);
if (norm_new < best_norm) { best_norm = norm_new; best_x = xnew; }
if (thresh_sq >= 0.0 && norm_new * norm_new <= thresh_sq) {
if (improved) *improved = true;
return xnew;
}
alpha *= 0.5;
}
}
// ── Both phases failed Armijo — never take the divergent full step. ───────
// Return the best point seen; if none improved, stay put and signal stall.
if (improved) *improved = (best_norm < norm0);
return best_x;
}
} // namespace detail
@@ -209,12 +263,15 @@ inline NewtonResult newton_euclidean(
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
if (!ok) break;
// ── Backtracking line search ──────────────────────────────────────────
// ── Globalised line search (Armijo + steepest-descent fallback) ───────
double norm0 = G.norm();
x = detail::line_search(x, dx, norm0,
Eigen::VectorXd d_sd = -(H * G); // merit-function steepest descent
bool improved = true;
x = detail::line_search(x, dx, d_sd, norm0,
[&](const std::vector<double>& xnew) {
return euclidean_gradient(mesh, xnew, m);
});
}, &improved);
if (!improved) break; // line search stalled — stop cleanly
res.iterations = iter + 1;
}
@@ -287,12 +344,16 @@ inline NewtonResult newton_spherical(
Eigen::VectorXd dx = detail::solve_with_fallback(negH, G, ok);
if (!ok) break;
// ── Backtracking line search ──────────────────────────────────────────
// ── Globalised line search (Armijo + steepest-descent fallback) ───────
// d_sd uses the actual (un-negated) Hessian: ∇f = H·G for f = ½‖G‖².
double norm0 = G.norm();
x = detail::line_search(x, dx, norm0,
Eigen::VectorXd d_sd = -(H * G);
bool improved = true;
x = detail::line_search(x, dx, d_sd, norm0,
[&](const std::vector<double>& xnew) {
return spherical_gradient(mesh, xnew, m);
});
}, &improved);
if (!improved) break;
res.iterations = iter + 1;
}
@@ -367,12 +428,15 @@ inline NewtonResult newton_hyper_ideal(
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
if (!ok) break;
// ── Backtracking line search ──────────────────────────────────────────
// ── Globalised line search (Armijo + steepest-descent fallback) ───────
double norm0 = G.norm();
x = detail::line_search(x, dx, norm0,
Eigen::VectorXd d_sd = -(H * G);
bool improved = true;
x = detail::line_search(x, dx, d_sd, norm0,
[&](const std::vector<double>& xnew) {
return evaluate_hyper_ideal(mesh, xnew, m, false).gradient;
});
}, &improved);
if (!improved) break;
res.iterations = iter + 1;
}
@@ -443,10 +507,13 @@ inline NewtonResult newton_cp_euclidean(
if (!ok) break;
double norm0 = G.norm();
x = detail::line_search(x, dx, norm0,
Eigen::VectorXd d_sd = -(H * G);
bool improved = true;
x = detail::line_search(x, dx, d_sd, norm0,
[&](const std::vector<double>& xnew) {
return cp_euclidean_gradient(mesh, xnew, m);
});
}, &improved);
if (!improved) break;
res.iterations = iter + 1;
}
@@ -552,10 +619,13 @@ inline NewtonResult newton_inversive_distance(
if (!ok) break;
double norm0 = G.norm();
x = detail::line_search(x, dx, norm0,
Eigen::VectorXd d_sd = -(H * G);
bool improved = true;
x = detail::line_search(x, dx, d_sd, norm0,
[&](const std::vector<double>& xnew) {
return inversive_distance_gradient(mesh, xnew, m);
});
}, &improved);
if (!improved) break;
res.iterations = iter + 1;
}