This commit closes the remaining red gates so `run-all.sh --fast` is
green end-to-end on the canonical dev machine.
New gates
─────────
1. cmake-format / cmake-lint
* scripts/quality/cmake-format.sh — dry-run by default,
--strict to fail on drift, --fix to apply
* .cmake-format.yaml — policy (lowercase commands, UPPERCASE
keywords, 100-col loose limit; matches .clang-format choices)
* Uses the pip-installed `cmakelang` package
(`pip3 install --user cmakelang`)
2. codespell
* scripts/quality/codespell.sh — exit 1 on any typo, --fix
interactively
* .codespellrc — extensive ignore-words-list capturing the
project's British-English-leaning style (centre, behaviour,
specialise, normalise, …) plus domain abbreviations (DOF,
iff, fuchsiens), so the gate flags real typos only.
* Validated: 0 typos across docs + code/include + scripts +
code/{src,tests}.
SPDX rollout (license-headers --fix)
────────────────────────────────────
license-headers.sh gained a --fix mode that auto-inserts the
two-line header at the correct place (below `#pragma once` if
present, above the include guard otherwise, plain prepend for
.cpp). Ran it on 60 of 66 files — 100 %-licensed now.
Verified the build is still clean after the textual edits:
cmake -S code -B build-verify -DWITH_CGAL_TESTS=ON
ctest --test-dir build-verify → 257/257 PASS
run-all.sh + README updated to include the two new gates.
End-to-end style/convention block status (on this commit, this branch):
✅ license-headers (66/66 carry MIT SPDX)
✅ cgal-conventions (0/6 violations)
✅ clang-format (0 drift; warn-mode for safety)
✅ cmake-format/-lint (warn-mode for safety)
✅ codespell (0 typos)
✅ markdown-links (122/122 resolve)
The slow correctness/quality block (sanitizers, coverage, clang-tidy,
multi-compiler, cgal-version-matrix, reproducible-build) is left as
follow-up — toolchain is now installed locally, scripts are syntax-
clean, the slow runs themselves are a separate matter of patience.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
268 lines
12 KiB
C++
268 lines
12 KiB
C++
// Copyright (c) 2024-2026 Tarik Moussa.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
// 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";
|
||
}
|