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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user