// 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 #include 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 x0(static_cast(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(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 x0_zero(static_cast(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(i)]; } // Start from a perturbation. std::vector 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 x0(static_cast(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(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 x0(static_cast(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(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 x0(static_cast(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(i)]; } // Perturb away from the equilibrium and watch it return. std::vector x_pert(static_cast(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 x0(static_cast(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(i)]; } std::vector x_pert(static_cast(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 x0(static_cast(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(i)]; } // Strong perturbation — quadratic Newton with analytic Hessian // should still converge in a handful of iterations. std::vector 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"; }