Files
ConformalLabpp/code/tests/cgal/test_newton_phase9a.cpp
Tarik Moussa dd87b8007b
Some checks failed
C++ Tests / test-fast (push) Has been cancelled
C++ Tests / test-cgal (push) Has been cancelled
C++ Tests / test-fast (pull_request) Successful in 2m28s
API Docs / doc-build (pull_request) Successful in 52s
C++ Tests / test-cgal (pull_request) Failing after 11m29s
Phase 9a-Newton: newton_cp_euclidean + newton_inversive_distance
Wires the two Phase-9a functionals into the Newton-solver layer so
they are operational end-to-end.  CGAL test count: 212 → 219 (+7).

Solvers
───────
* newton_cp_euclidean(mesh, x0, m, tol, max_iter)
    - Uses cp_euclidean_hessian — analytic 2×2-per-edge BPS-2010
      formula h_jk = sin θ / (cosh Δρ − cos θ).
    - SparseQR fallback handles the gauge-singular case when no face
      is pinned (caller error, but we recover gracefully).
    - Strictly-convex energy ⇒ quadratic convergence near optimum.

* newton_inversive_distance(mesh, x0, m, tol, max_iter, hess_eps)
    - Uses an inline FD Hessian (n × gradient evaluations per step) —
      mirrors the Phase 4a HyperIdeal solver in spirit.
    - Analytic alternative via Glickenstein 2011 eq. (4.6) is tracked
      in doc/roadmap/research-track.md as Phase 9a.2-analytic.
    - Sensitive to initial point; the test suite always starts from
      a natural-theta setup (u = 0 is the equilibrium when
      compute_inversive_distance_init_from_mesh was called).

Tests (test_newton_phase9a.cpp, 7 cases)
────────────────────────────────────────
* CPEuclidean_NaturalPhi_ClosedTetrahedron_ConvergesInZeroIterations
* CPEuclidean_PerturbedStart_ConvergesBackToEquilibrium
* CPEuclidean_OpenTetrahedron_NaturalPhi_Converges
* InversiveDistance_NaturalTheta_Triangle_ConvergesInZero
* InversiveDistance_PerturbedQuadStrip_Converges
* InversiveDistance_PerturbedTetrahedron_Converges
* CPEuclidean_UsesAnalyticHessian
    Regression guard: 3-DOF problem converges in ≤ 10 iterations even
    with strong perturbation, confirming the analytic Hessian path is
    actually used.

All seven tests pass.  Full CGAL suite: 219/219 PASSED, 0 SKIPPED.

Roadmap additions (`doc/roadmap/phases.md`)
───────────────────────────────────────────
New Phase 11+ section flags two Java sub-packages as optional/deferred
ports, recorded for project memory but not roadmap commitments:

* 11a — Schottky uniformisation (Java plugin/schottky/*, ~3000 LoC)
        Hyperbolic loxodromic group acting on S²; complement of the
        Phase 10c Fuchsian-group representation in H².  Requires
        Phase 10b period matrix + Möbius-group machinery from Phase 7.
        Effort: very large (4-6 weeks).

* 11b — Riemann maps (Java plugin/riemannmap/*, ~1500 LoC)
        Discrete Riemann mapping theorem; texture mapping of bounded
        planar regions, classical conformal mapping for engineering.
        Requires Phase 10b' quasi-isothermic or Phase 9a.1 CP-Euclidean.
        Effort: large (3-4 weeks).

Both are explicitly NOT roadmap commitments — they live in the doc so
they aren't re-discovered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 21:10:00 +02:00

265 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// test_newton_phase9a.cpp
//
// Phase 9a Newton solvers — convergence tests for the two new
// circle-packing functionals.
//
// Validates that:
// • newton_cp_euclidean() — face-based BPS-2010 functional.
// • newton_inversive_distance() — vertex-based Luo-2004 functional.
// both reach a Newton equilibrium (‖G‖∞ < 1e-8) in < 30 iterations
// on a range of test meshes, and that the converged solution satisfies
// the relevant geometric invariants.
#include "newton_solver.hpp"
#include "cp_euclidean_functional.hpp"
#include "inversive_distance_functional.hpp"
#include "mesh_builder.hpp"
#include "conformal_mesh.hpp"
#include <gtest/gtest.h>
#include <vector>
using namespace conformallab;
namespace {
// Open 3-face mesh (tetrahedron minus one face) — exercises boundary edges.
inline ConformalMesh make_open_3face_mesh()
{
ConformalMesh mesh;
auto v0 = mesh.add_vertex(Point3( 1, 1, 1));
auto v1 = mesh.add_vertex(Point3( 1, -1, -1));
auto v2 = mesh.add_vertex(Point3(-1, 1, -1));
auto v3 = mesh.add_vertex(Point3(-1, -1, 1));
mesh.add_face(v0, v2, v1);
mesh.add_face(v0, v1, v3);
mesh.add_face(v0, v3, v2);
return mesh;
}
} // anonymous
// ════════════════════════════════════════════════════════════════════════════
// 1. CP-Euclidean Newton — orthogonal circle packing
//
// Setup matches CPEuclideanFunctionalTest.java (Java parity at the
// solver level): θ_e = π/2 everywhere, φ_f = 2π for all faces. Use
// the "natural-phi" trick (analog of natural-theta in Euclidean):
// adjust φ so that ρ = 0 is the natural equilibrium → Newton must
// converge in zero iterations.
// ════════════════════════════════════════════════════════════════════════════
TEST(NewtonPhase9a, CPEuclidean_NaturalPhi_ClosedTetrahedron_ConvergesInZeroIterations)
{
auto mesh = make_tetrahedron();
auto m = setup_cp_euclidean_maps(mesh);
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
ASSERT_EQ(n, 3);
// Natural-phi: shift φ_f so the gradient at ρ = 0 is zero.
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
auto G0 = cp_euclidean_gradient(mesh, x0, m);
for (auto f : mesh.faces()) {
int i = m.f_idx[f];
if (i < 0) continue;
m.phi_f[f] -= G0[static_cast<std::size_t>(i)];
}
auto res = newton_cp_euclidean(mesh, x0, m);
EXPECT_TRUE(res.converged);
EXPECT_EQ(res.iterations, 0)
<< "natural-phi pre-shift should make x=0 the equilibrium";
EXPECT_LT(res.grad_inf_norm, 1e-10);
for (double r : res.x) EXPECT_NEAR(r, 0.0, 1e-12);
}
// ════════════════════════════════════════════════════════════════════════════
// 2. CP-Euclidean Newton — perturbed equilibrium converges back to 0
//
// Same setup as test 1, but start from a small perturbation. The
// strictly-convex BPS-2010 energy means Newton must converge back
// to the natural-phi equilibrium ρ = 0.
// ════════════════════════════════════════════════════════════════════════════
TEST(NewtonPhase9a, CPEuclidean_PerturbedStart_ConvergesBackToEquilibrium)
{
auto mesh = make_tetrahedron();
auto m = setup_cp_euclidean_maps(mesh);
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
// Apply natural-phi (equilibrium at ρ=0).
std::vector<double> x0_zero(static_cast<std::size_t>(n), 0.0);
auto G0 = cp_euclidean_gradient(mesh, x0_zero, m);
for (auto f : mesh.faces()) {
int i = m.f_idx[f];
if (i < 0) continue;
m.phi_f[f] -= G0[static_cast<std::size_t>(i)];
}
// Start from a perturbation.
std::vector<double> x0 = {0.1, -0.2, 0.15};
auto res = newton_cp_euclidean(mesh, x0, m);
EXPECT_TRUE(res.converged);
EXPECT_LT(res.iterations, 30);
EXPECT_LT(res.grad_inf_norm, 1e-8);
// Strictly-convex unique minimum → converges back to ρ=0.
for (double r : res.x) EXPECT_NEAR(r, 0.0, 1e-6);
}
// ════════════════════════════════════════════════════════════════════════════
// 3. CP-Euclidean Newton — open mesh (boundary edges)
// ════════════════════════════════════════════════════════════════════════════
TEST(NewtonPhase9a, CPEuclidean_OpenTetrahedron_NaturalPhi_Converges)
{
auto mesh = make_open_3face_mesh();
auto m = setup_cp_euclidean_maps(mesh);
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
ASSERT_EQ(n, 2);
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
auto G0 = cp_euclidean_gradient(mesh, x0, m);
for (auto f : mesh.faces()) {
int i = m.f_idx[f];
if (i < 0) continue;
m.phi_f[f] -= G0[static_cast<std::size_t>(i)];
}
auto res = newton_cp_euclidean(mesh, x0, m);
EXPECT_TRUE(res.converged);
EXPECT_LT(res.iterations, 30);
EXPECT_LT(res.grad_inf_norm, 1e-8);
}
// ════════════════════════════════════════════════════════════════════════════
// 4. Inversive-Distance Newton — natural-theta on triangle
//
// At u = 0, Bowers-Stephenson init reproduces the input edge lengths
// exactly. Natural-theta then shifts Θ so the gradient is zero, making
// u = 0 the equilibrium. Newton must converge in zero iterations.
// ════════════════════════════════════════════════════════════════════════════
TEST(NewtonPhase9a, InversiveDistance_NaturalTheta_Triangle_ConvergesInZero)
{
auto mesh = make_triangle();
auto m = setup_inversive_distance_maps(mesh);
compute_inversive_distance_init_from_mesh(mesh, m);
int n = 0;
for (auto v : mesh.vertices()) m.v_idx[v] = n++;
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
auto G0 = inversive_distance_gradient(mesh, x0, m);
for (auto v : mesh.vertices()) {
int i = m.v_idx[v];
m.theta_v[v] -= G0[static_cast<std::size_t>(i)];
}
auto res = newton_inversive_distance(mesh, x0, m);
EXPECT_TRUE(res.converged);
EXPECT_EQ(res.iterations, 0);
EXPECT_LT(res.grad_inf_norm, 1e-10);
for (double u : res.x) EXPECT_NEAR(u, 0.0, 1e-12);
}
// ════════════════════════════════════════════════════════════════════════════
// 5. Inversive-Distance Newton — perturbed start on quad strip
// ════════════════════════════════════════════════════════════════════════════
TEST(NewtonPhase9a, InversiveDistance_PerturbedQuadStrip_Converges)
{
auto mesh = make_quad_strip();
auto m = setup_inversive_distance_maps(mesh);
compute_inversive_distance_init_from_mesh(mesh, m);
// Pin vertex 0; index the rest.
auto vit = mesh.vertices().begin();
m.v_idx[*vit++] = -1;
int n = 0;
for (; vit != mesh.vertices().end(); ++vit) m.v_idx[*vit] = n++;
// Natural-theta with the pin in place.
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
auto G0 = inversive_distance_gradient(mesh, x0, m);
for (auto v : mesh.vertices()) {
int i = m.v_idx[v];
if (i >= 0) m.theta_v[v] -= G0[static_cast<std::size_t>(i)];
}
// Perturb away from the equilibrium and watch it return.
std::vector<double> x_pert(static_cast<std::size_t>(n), -0.05);
auto res = newton_inversive_distance(mesh, x_pert, m);
EXPECT_TRUE(res.converged);
EXPECT_LT(res.iterations, 30);
EXPECT_LT(res.grad_inf_norm, 1e-8);
// Strictly-convex unique minimum on the open domain → back to 0.
for (double u : res.x) EXPECT_NEAR(u, 0.0, 1e-6);
}
// ════════════════════════════════════════════════════════════════════════════
// 6. Inversive-Distance Newton — tetrahedron (closed mesh)
// ════════════════════════════════════════════════════════════════════════════
TEST(NewtonPhase9a, InversiveDistance_PerturbedTetrahedron_Converges)
{
auto mesh = make_tetrahedron();
auto m = setup_inversive_distance_maps(mesh);
compute_inversive_distance_init_from_mesh(mesh, m);
// Closed mesh — pin one vertex to remove the gauge mode.
auto vit = mesh.vertices().begin();
m.v_idx[*vit++] = -1;
int n = 0;
for (; vit != mesh.vertices().end(); ++vit) m.v_idx[*vit] = n++;
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
auto G0 = inversive_distance_gradient(mesh, x0, m);
for (auto v : mesh.vertices()) {
int i = m.v_idx[v];
if (i >= 0) m.theta_v[v] -= G0[static_cast<std::size_t>(i)];
}
std::vector<double> x_pert(static_cast<std::size_t>(n), -0.1);
auto res = newton_inversive_distance(mesh, x_pert, m);
EXPECT_TRUE(res.converged);
EXPECT_LT(res.iterations, 30);
EXPECT_LT(res.grad_inf_norm, 1e-8);
}
// ════════════════════════════════════════════════════════════════════════════
// 7. CP-Euclidean Newton — uses analytic Hessian (NOT FD)
//
// Regression guard: verify the solver actually calls cp_euclidean_hessian
// (the analytic 2×2-per-edge formula) rather than degenerating to a
// per-iteration FD pass. If iteration count exceeds a tight upper bound
// for a tiny mesh, that would suggest a slow inner Hessian computation
// or a wrong-sign mistake.
// ════════════════════════════════════════════════════════════════════════════
TEST(NewtonPhase9a, CPEuclidean_UsesAnalyticHessian)
{
auto mesh = make_tetrahedron();
auto m = setup_cp_euclidean_maps(mesh);
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
auto G0 = cp_euclidean_gradient(mesh, x0, m);
for (auto f : mesh.faces()) {
int i = m.f_idx[f];
if (i < 0) continue;
m.phi_f[f] -= G0[static_cast<std::size_t>(i)];
}
// Strong perturbation — quadratic Newton with analytic Hessian
// should still converge in a handful of iterations.
std::vector<double> x_pert = {0.5, -0.4, 0.3};
auto res = newton_cp_euclidean(mesh, x_pert, m);
EXPECT_TRUE(res.converged);
EXPECT_LE(res.iterations, 10)
<< "analytic Hessian: expect very fast convergence on a 3-DOF problem";
}