// test_spherical_functional.cpp (Phase 3c + 3e) // // 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 #include #include 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 x(static_cast(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 x(static_cast(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 x(static_cast(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(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 x(static_cast(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 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 x(static_cast(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 x = {-0.2, -0.4}; 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 x(static_cast(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(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 x_fixed = x; for (auto v : mesh.vertices()) { int iv = maps.v_idx[v]; if (iv >= 0) x_fixed[static_cast(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(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 x(static_cast(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(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 x(static_cast(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; }