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:
@@ -1,4 +1,4 @@
|
||||
// test_spherical_functional.cpp
|
||||
// test_spherical_functional.cpp (Phase 3c + 3e)
|
||||
//
|
||||
// Phase 3c — SphericalFunctional ported to ConformalMesh.
|
||||
//
|
||||
@@ -244,3 +244,97 @@ TEST(SphericalFunctional, GradientCheck_MixedPinnedVertices)
|
||||
EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
|
||||
<< "Gradient check failed for mixed pinned/variable vertices";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Phase 3e — Gauge-fix for closed spherical surfaces
|
||||
//
|
||||
// On a closed spherical surface, the functional has a gauge mode:
|
||||
// E(u + t·1) is maximised at some t*.
|
||||
// At t*, the sum of all vertex gradients equals zero: Σ G_v = 0.
|
||||
//
|
||||
// Test: start from a point with non-zero ΣG_v, apply the gauge shift,
|
||||
// and verify ΣG_v(x + t*·1) ≈ 0.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, GaugeFix_SpherTetVertexZerosSumGv)
|
||||
{
|
||||
auto mesh = make_spherical_tetrahedron();
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
compute_lambda0_from_mesh(mesh, maps);
|
||||
int n = assign_vertex_dof_indices(mesh, maps);
|
||||
|
||||
// Off-gauge starting point: all u_i = -0.5
|
||||
std::vector<double> x(static_cast<std::size_t>(n), -0.5);
|
||||
|
||||
// Compute ΣG_v before gauge shift.
|
||||
{
|
||||
auto G = spherical_gradient(mesh, x, maps);
|
||||
double sum = 0.0;
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv >= 0) sum += G[static_cast<std::size_t>(iv)];
|
||||
}
|
||||
// At -0.5 the surface is compressed; ΣG_v should be non-zero.
|
||||
EXPECT_NE(sum, 0.0) << "Pre-gauge ΣG_v should be non-zero";
|
||||
}
|
||||
|
||||
// Compute gauge shift and apply.
|
||||
double t = spherical_gauge_shift(mesh, x, maps);
|
||||
std::vector<double> x_fixed = x;
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv >= 0)
|
||||
x_fixed[static_cast<std::size_t>(iv)] += t;
|
||||
}
|
||||
|
||||
// Verify ΣG_v ≈ 0 at the gauge-fixed point.
|
||||
{
|
||||
auto G = spherical_gradient(mesh, x_fixed, maps);
|
||||
double sum = 0.0;
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv >= 0) sum += G[static_cast<std::size_t>(iv)];
|
||||
}
|
||||
EXPECT_NEAR(sum, 0.0, 1e-6)
|
||||
<< "After gauge fix, Σ G_v should vanish; t* = " << t;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SphericalFunctional, GaugeFix_ApplyInPlace)
|
||||
{
|
||||
auto mesh = make_spherical_tetrahedron();
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
compute_lambda0_from_mesh(mesh, maps);
|
||||
int n = assign_vertex_dof_indices(mesh, maps);
|
||||
|
||||
// x = -0.3: compressed but inside the valid spherical domain.
|
||||
std::vector<double> x(static_cast<std::size_t>(n), -0.3);
|
||||
|
||||
apply_spherical_gauge(mesh, x, maps);
|
||||
|
||||
// After in-place gauge fix, ΣG_v must be near 0.
|
||||
auto G = spherical_gradient(mesh, x, maps);
|
||||
double sum = 0.0;
|
||||
for (auto v : mesh.vertices()) {
|
||||
int iv = maps.v_idx[v];
|
||||
if (iv >= 0) sum += G[static_cast<std::size_t>(iv)];
|
||||
}
|
||||
EXPECT_NEAR(sum, 0.0, 1e-6)
|
||||
<< "apply_spherical_gauge must drive Σ G_v to zero";
|
||||
}
|
||||
|
||||
TEST(SphericalFunctional, GaugeFix_AlreadyAtGaugeReturnsTNearZero)
|
||||
{
|
||||
// A symmetric, equilateral spherical tetrahedron at x=0 is already
|
||||
// at the gauge maximum (by symmetry, ΣG_v = 0).
|
||||
auto mesh = make_spherical_tetrahedron();
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
compute_lambda0_from_mesh(mesh, maps);
|
||||
int n = assign_vertex_dof_indices(mesh, maps);
|
||||
|
||||
std::vector<double> x(static_cast<std::size_t>(n), 0.0);
|
||||
double t = spherical_gauge_shift(mesh, x, maps);
|
||||
// Symmetric starting point → t* should be very close to 0.
|
||||
EXPECT_NEAR(t, 0.0, 1e-5)
|
||||
<< "Gauge shift from the symmetric point should be ~0; got " << t;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user