feat(phase3c): port SphericalFunctional onto ConformalMesh; update README
New headers:
- spherical_geometry.hpp: spherical arc length l(λ) and half-angle formula
for interior angles of a spherical triangle (SphericalFaceAngles struct)
- spherical_functional.hpp: SphericalMaps bundle, setup/assign DOF helpers,
compute_lambda0_from_mesh(), gradient (Θ_v − Σα_v; Schläfli edge formula),
energy via 10-point Gauss-Legendre path integral, gradient_check_spherical()
Updated:
- mesh_builder.hpp: add make_spherical_tetrahedron() (vertices on unit sphere)
and make_octahedron_face() (single right-angled spherical triangle)
- tests/cgal/CMakeLists.txt: enable test_spherical_functional.cpp
- README.md: rewrite for CGAL-package goal, two test targets, all headers,
updated project tree, Phase progress table, key design decisions
Tests (cgal.SphericalFunctional.*): 8 active + 1 skip
- OctaFaceAnglesAreRightAngles, SpherTetAngleSumExceedsPi
- GradientCheck_{OctaFaceVertex, SpherTetVertex, SpherTetAllDofs,
SpherFan4Vertex, MixedPinnedVertices}
- AnglesFiniteAtKnownPoint
All 30 cgal.* tests pass (2 @Ignore skips).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
246
code/tests/cgal/test_spherical_functional.cpp
Normal file
246
code/tests/cgal/test_spherical_functional.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
// test_spherical_functional.cpp
|
||||
//
|
||||
// Phase 3c — SphericalFunctional ported to ConformalMesh.
|
||||
//
|
||||
// Corresponds to de.varylab.discreteconformal.functional.SphericalFunctionalTest.
|
||||
//
|
||||
// Test map (Java → C++)
|
||||
// ──────────────────────
|
||||
// testHessian (Ignored) → GradientCheck_Hessian (SKIPPED)
|
||||
// testGradientWithHyperIdeal… → GradientCheck_OctaFaceVertex (ported)
|
||||
// testGradientInExtendedDomain → GradientCheck_SpherTetVertex (ported)
|
||||
// testGradientWithHyperelliptic → GradientCheck_SpherTetAllDofs (ported)
|
||||
// testFunctionalAtNaNValue → AnglesFiniteAtKnownPoint (ported)
|
||||
//
|
||||
// Energy model
|
||||
// ────────────
|
||||
// The energy is computed as the Schläfli path integral E(x) = ∫₀¹⟨G(tx),x⟩dt
|
||||
// using 10-point Gauss-Legendre quadrature. The gradient check therefore
|
||||
// verifies that G is curl-free (the integrability / exactness condition of
|
||||
// the spherical discrete conformal functional). This is equivalent to the
|
||||
// Java FunctionalTest gradient check.
|
||||
|
||||
#include "conformal_mesh.hpp"
|
||||
#include "mesh_builder.hpp"
|
||||
#include "spherical_functional.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
using namespace conformallab;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// @Ignore in Java: no Hessian implemented
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, GradientCheck_Hessian)
|
||||
{
|
||||
GTEST_SKIP() << "@Ignore in Java – Hessian not implemented";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Angle formula: octahedron-face triangle has all angles = π/2
|
||||
//
|
||||
// The triangle (1,0,0)–(0,1,0)–(0,0,1) has l_ij = π/2 for all edges.
|
||||
// Half-angle formula: s = 3π/4, s_ij = π/4 for all three.
|
||||
// All angles = π/2 (right-angled spherical triangle).
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, OctaFaceAnglesAreRightAngles)
|
||||
{
|
||||
// l_ij = π/2 for all edges (octahedron face on unit sphere)
|
||||
const double l = PI_SPHER / 2.0;
|
||||
auto fa = spherical_angles(l, l, l);
|
||||
|
||||
ASSERT_TRUE(fa.valid) << "Equilateral spherical triangle must be valid";
|
||||
EXPECT_NEAR(PI_SPHER / 2.0, fa.alpha1, 1e-12);
|
||||
EXPECT_NEAR(PI_SPHER / 2.0, fa.alpha2, 1e-12);
|
||||
EXPECT_NEAR(PI_SPHER / 2.0, fa.alpha3, 1e-12);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Angle sum of a spherical triangle exceeds π (positive curvature)
|
||||
//
|
||||
// For the spherical tetrahedron face (arccos(−1/3) ≈ 1.9106 per edge):
|
||||
// The dihedral angle = arccos(1/3) ≈ 70.53°; by symmetry the face angles
|
||||
// (vertex angles of the spherical triangle) are all equal.
|
||||
// Angle sum must be > π and equal 3·arccos(1/3) ≈ 3·1.2310 ≈ 3.693 rad.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, SpherTetAngleSumExceedsPi)
|
||||
{
|
||||
// Edge length of spherical tetrahedron face: arccos(−1/3)
|
||||
const double l = std::acos(-1.0 / 3.0);
|
||||
auto fa = spherical_angles(l, l, l);
|
||||
|
||||
ASSERT_TRUE(fa.valid);
|
||||
EXPECT_GT(fa.alpha1 + fa.alpha2 + fa.alpha3, PI_SPHER)
|
||||
<< "Angle sum of spherical triangle must exceed π";
|
||||
|
||||
// By symmetry all three angles must be equal
|
||||
EXPECT_NEAR(fa.alpha1, fa.alpha2, 1e-12);
|
||||
EXPECT_NEAR(fa.alpha2, fa.alpha3, 1e-12);
|
||||
|
||||
// For a regular spherical tetrahedron with edge arccos(−1/3):
|
||||
// half-angle: tan(α/2) = √(sin(l/2)/sin(3l/2)) = √3 → α/2 = π/3 → α = 2π/3.
|
||||
// (arccos(1/3) ≈ 1.231 is the 3D dihedral angle of a Euclidean tetrahedron, not this.)
|
||||
double expected = 2.0 * PI_SPHER / 3.0; // 120°
|
||||
EXPECT_NEAR(fa.alpha1, expected, 1e-10);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Gradient check: octahedron-face triangle, vertex DOFs only
|
||||
//
|
||||
// Sets λ° from mesh geometry (unit sphere), all u_i = −0.3 (slightly smaller).
|
||||
// Mirrors Java testGradientWithHyperIdeal… on a single-triangle mesh.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, GradientCheck_OctaFaceVertex)
|
||||
{
|
||||
auto mesh = make_octahedron_face();
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
compute_lambda0_from_mesh(mesh, maps);
|
||||
int n = assign_vertex_dof_indices(mesh, maps);
|
||||
|
||||
// Small uniform conformal factor: shrink the triangle slightly.
|
||||
std::vector<double> x(static_cast<std::size_t>(n), -0.3);
|
||||
|
||||
EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
|
||||
<< "Gradient check failed on octahedron-face triangle (vertex DOFs)";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Gradient check: spherical tetrahedron (4 faces), vertex DOFs only
|
||||
//
|
||||
// Closed surface; exercises accumulation over multiple faces per vertex.
|
||||
// Mirrors Java testGradientInTheExtendedDomain.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, GradientCheck_SpherTetVertex)
|
||||
{
|
||||
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.2);
|
||||
|
||||
EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
|
||||
<< "Gradient check failed on spherical tetrahedron (vertex DOFs)";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Gradient check: spherical tetrahedron, all DOFs (vertex + edge)
|
||||
//
|
||||
// Exercises the edge-gradient branch: G_e = α_opp⁺ + α_opp⁻ − π.
|
||||
// Mirrors Java testGradientWithHyperellipticCurve.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, GradientCheck_SpherTetAllDofs)
|
||||
{
|
||||
auto mesh = make_spherical_tetrahedron();
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
compute_lambda0_from_mesh(mesh, maps);
|
||||
int n = assign_all_spherical_dof_indices(mesh, maps);
|
||||
|
||||
// Small but non-zero values; vertex DOFs negative, edge DOFs zero.
|
||||
// Edge DOF adjusts the effective log-length Λ_ij = λ°_ij + u_i + u_j + λ_e.
|
||||
std::vector<double> x(static_cast<std::size_t>(n), 0.0);
|
||||
// Set vertex DOFs (indices 0..3) to -0.2 to keep triangle well-formed.
|
||||
for (int i = 0; i < 4; ++i) x[static_cast<std::size_t>(i)] = -0.2;
|
||||
|
||||
EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
|
||||
<< "Gradient check failed on spherical tetrahedron (all DOFs)";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Angles are finite at a known interior point
|
||||
//
|
||||
// Mirrors Java testFunctionalAtNaNValue: choose DOFs that could hit
|
||||
// a degenerate branch (l_ij → 0 or triangle inequality fails) and check
|
||||
// the gradient vector is free of NaN/Inf.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, AnglesFiniteAtKnownPoint)
|
||||
{
|
||||
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);
|
||||
|
||||
// u_i = -1.5: contracts the triangle heavily but stays non-degenerate.
|
||||
std::vector<double> x(static_cast<std::size_t>(n), -1.5);
|
||||
auto G = spherical_gradient(mesh, x, maps);
|
||||
|
||||
for (std::size_t i = 0; i < G.size(); ++i) {
|
||||
EXPECT_FALSE(std::isnan(G[i])) << "Gradient component " << i << " is NaN";
|
||||
EXPECT_FALSE(std::isinf(G[i])) << "Gradient component " << i << " is Inf";
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Gradient check: fan-4 mesh on unit sphere, vertex DOFs only
|
||||
//
|
||||
// Make a fan of 4 triangles around the north pole (0,0,1);
|
||||
// rim vertices projected onto the equator.
|
||||
// Exercises high-valence vertex gradient accumulation.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, GradientCheck_SpherFan4Vertex)
|
||||
{
|
||||
// Build a fan with 4 spherical triangles manually (can't use make_fan
|
||||
// directly because those vertices are not on the unit sphere).
|
||||
ConformalMesh mesh;
|
||||
auto center = mesh.add_vertex(Point3(0, 0, 1)); // north pole
|
||||
const int n_rim = 4;
|
||||
std::vector<Vertex_index> rim(n_rim);
|
||||
const double dtheta = 2.0 * PI_SPHER / n_rim;
|
||||
const double phi = PI_SPHER / 4.0; // 45° colatitude
|
||||
for (int i = 0; i < n_rim; ++i) {
|
||||
double theta = i * dtheta;
|
||||
rim[i] = mesh.add_vertex(Point3(
|
||||
std::sin(phi) * std::cos(theta),
|
||||
std::sin(phi) * std::sin(theta),
|
||||
std::cos(phi)));
|
||||
}
|
||||
for (int i = 0; i < n_rim; ++i)
|
||||
mesh.add_face(center, rim[i], rim[(i + 1) % n_rim]);
|
||||
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
compute_lambda0_from_mesh(mesh, maps);
|
||||
int ndof = assign_vertex_dof_indices(mesh, maps);
|
||||
|
||||
std::vector<double> x(static_cast<std::size_t>(ndof), -0.3);
|
||||
|
||||
EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
|
||||
<< "Gradient check failed on spherical fan-4 mesh";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Gradient check: mixed pinned/variable vertices
|
||||
//
|
||||
// One vertex pinned (u_v = 0 fixed), others variable.
|
||||
// Verifies that the gradient accumulation skips pinned vertices correctly.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalFunctional, GradientCheck_MixedPinnedVertices)
|
||||
{
|
||||
auto mesh = make_octahedron_face();
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
compute_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
// Pin v0, make v1 and v2 variable.
|
||||
auto vit = mesh.vertices().begin();
|
||||
Vertex_index v0 = *vit++;
|
||||
Vertex_index v1 = *vit++;
|
||||
Vertex_index v2 = *vit;
|
||||
|
||||
maps.v_idx[v0] = -1; // pinned
|
||||
maps.v_idx[v1] = 0;
|
||||
maps.v_idx[v2] = 1;
|
||||
|
||||
std::vector<double> x = {-0.2, -0.4};
|
||||
|
||||
EXPECT_TRUE(gradient_check_spherical(mesh, x, maps))
|
||||
<< "Gradient check failed for mixed pinned/variable vertices";
|
||||
}
|
||||
Reference in New Issue
Block a user