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
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>
265 lines
12 KiB
C++
265 lines
12 KiB
C++
// 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";
|
||
}
|