feat(phase3d+3e): port EuclideanCyclicFunctional + add SphericalFunctional gauge-fix

Phase 3d — EuclideanCyclicFunctional:
  • euclidean_geometry.hpp: t-value / atan2 corner-angle formula with
    centering trick (μ = (Λ̃₁₂+Λ̃₂₃+Λ̃₃₁)/6) for numerical stability
  • euclidean_functional.hpp: EuclideanMaps bundle, gradient (G_v = Θ_v − Σα_v,
    G_e = α_opp⁺ + α_opp⁻ − φ_e), 10-point GL path-integral energy,
    gradient_check_euclidean — identical halfedge convention to SphericalFunctional
  • test_euclidean_functional.cpp: 11 tests (1 skip) covering angle formula,
    right-isosceles triangle, angle sum = π, degenerate detection, gradient
    checks on triangle/quad-strip/tetrahedron/fan-5/mixed-pinned, NaN check

Phase 3e — Spherical gauge-fix:
  • spherical_gauge_shift(): Newton + backtracking line search to find t*
    where ΣG_v(x + t·1) = 0 (maximises E along the global scale direction);
    bisection used when sign change is detectable, Newton+backtrack otherwise
  • apply_spherical_gauge(): in-place wrapper
  • 3 new tests: GaugeFix_SpherTetVertexZerosSumGv, GaugeFix_ApplyInPlace,
    GaugeFix_AlreadyAtGaugeReturnsTNearZero

Total: 45 cgal tests pass, 3 skipped (@Ignore Hessian stubs, one per functional)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-12 07:39:14 +02:00
parent dc0d3ca005
commit 8c353bb884
7 changed files with 924 additions and 21 deletions

View File

@@ -354,4 +354,122 @@ inline bool gradient_check_spherical(
return ok;
}
// ── Gauge-fix for closed spherical surfaces ───────────────────────────────────
//
// On a closed (boundaryless) spherical surface the energy E(u + t·1) has a
// unique maximum w.r.t. t ∈ (the "global scale" gauge mode). Without
// fixing this gauge the functional is unbounded below and no solver converges.
//
// This function returns the scalar shift t* such that
// Σ_v G_v(x + t*·1_v) = 0
// i.e., the derivative of E(u+t·1) w.r.t. t is zero at t = t*.
//
// Apply the shift by adding t* to every vertex DOF in x.
//
// Implementation: bisection on f(t) = Σ_v G_v(x + t·1_v).
// f is strictly monotone decreasing (second derivative < 0) for a convex
// functional, so bisection converges in O(log₂(2·bracket/tol)) iterations.
//
// Parameters:
// bracket initial search interval [bracket, +bracket] (default 50)
// tol absolute tolerance on t* (default 1e-8)
//
// Returns 0.0 if the zero cannot be bracketed (already at gauge maximum,
// or open surface — no shift needed).
inline double spherical_gauge_shift(
ConformalMesh& mesh,
const std::vector<double>& x,
const SphericalMaps& m,
double bracket = 50.0,
double tol = 1e-8)
{
// Helper: sum of all vertex gradient components at x + t·1_v.
auto sum_Gv = [&](double t) -> double {
// Build a shifted copy of x (only vertex DOFs shifted).
std::vector<double> xt = x;
for (auto v : mesh.vertices()) {
int iv = m.v_idx[v];
if (iv >= 0)
xt[static_cast<std::size_t>(iv)] += t;
}
auto G = spherical_gradient(mesh, xt, m);
double sum = 0.0;
for (auto v : mesh.vertices()) {
int iv = m.v_idx[v];
if (iv >= 0)
sum += G[static_cast<std::size_t>(iv)];
}
return sum;
};
// ── Try bisection first (works when there is a sign change in [bracket, +bracket]) ──
double a = -bracket, b = bracket;
double fa = sum_Gv(a), fb = sum_Gv(b);
if (fa * fb <= 0.0) {
for (int iter = 0; iter < 120; ++iter) {
double c = 0.5 * (a + b);
double fc = sum_Gv(c);
if (std::abs(fc) < tol || (b - a) < tol) return c;
if (fa * fc < 0.0) { b = c; fb = fc; }
else { a = c; fa = fc; }
}
return 0.5 * (a + b);
}
// ── No sign change (zero may lie at a domain boundary). ───────────────────
// Use damped Newton's method with backtracking line search.
// f'(t) estimated by forward finite difference.
// When the Newton step overshoots the valid domain (ΣG_v jumps back up
// because faces become degenerate), backtracking halves the step until
// |f| strictly decreases.
const double fd_eps = 1e-5;
double t = 0.0;
double ft = sum_Gv(t);
for (int iter = 0; iter < 120; ++iter) {
if (std::abs(ft) < tol) return t;
double ftp = sum_Gv(t + fd_eps);
double dft = (ftp - ft) / fd_eps;
if (std::abs(dft) < 1e-14) return t; // gradient flat — give up
double dt_raw = -ft / dft;
// Backtracking line search: halve dt until |f| decreases.
double alpha = 1.0;
bool improved = false;
for (int back = 0; back < 40; ++back) {
double t_try = t + alpha * dt_raw;
t_try = std::max(-bracket, std::min(bracket, t_try));
double ft_try = sum_Gv(t_try);
if (std::abs(ft_try) < std::abs(ft)) {
t = t_try;
ft = ft_try;
improved = true;
break;
}
alpha *= 0.5;
}
if (!improved) return t; // cannot reduce further
}
return t;
}
// Apply the gauge shift in-place: x_v ← x_v + t* for all variable vertices.
inline void apply_spherical_gauge(
ConformalMesh& mesh,
std::vector<double>& x,
const SphericalMaps& m,
double bracket = 50.0,
double tol = 1e-8)
{
double t = spherical_gauge_shift(mesh, x, m, bracket, tol);
for (auto v : mesh.vertices()) {
int iv = m.v_idx[v];
if (iv >= 0)
x[static_cast<std::size_t>(iv)] += t;
}
}
} // namespace conformallab