Phase 9a-Newton: newton_cp_euclidean + newton_inversive_distance
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
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>
This commit is contained in:
@@ -31,6 +31,8 @@
|
||||
#include "euclidean_hessian.hpp"
|
||||
#include "spherical_hessian.hpp"
|
||||
#include "hyper_ideal_hessian.hpp"
|
||||
#include "cp_euclidean_functional.hpp"
|
||||
#include "inversive_distance_functional.hpp"
|
||||
#include <Eigen/SparseCholesky>
|
||||
#include <Eigen/SparseQR>
|
||||
#include <Eigen/OrderingMethods>
|
||||
@@ -375,4 +377,187 @@ inline NewtonResult newton_hyper_ideal(
|
||||
return res;
|
||||
}
|
||||
|
||||
// ── CP-Euclidean Newton solver (Phase 9a.1) ───────────────────────────────────
|
||||
|
||||
/// Solve the CP-Euclidean circle-packing problem: find ρ ∈ ℝ^F such that the
|
||||
/// per-face angle sums match φ_f at every free face.
|
||||
///
|
||||
/// The CP-Euclidean energy (Bobenko-Pinkall-Springborn 2010 §6) is strictly
|
||||
/// convex on its open domain of validity, so the Hessian H is PSD and the
|
||||
/// solution is unique up to the gauge mode pinned by `f_idx == −1`.
|
||||
/// `cp_euclidean_hessian` provides the analytic 2×2-per-edge formula
|
||||
/// `h_jk = sin θ / (cosh Δρ − cos θ)`; no FD machinery is required.
|
||||
///
|
||||
/// \param mesh Input triangle mesh (closed or with boundary).
|
||||
/// \param x0 Initial DOF vector (length = number of free faces).
|
||||
/// All-zeros is a valid start.
|
||||
/// \param m CPEuclideanMaps: f_idx must have one pinned face
|
||||
/// (`f_idx[f0] == −1`); theta_e and phi_f set by the caller.
|
||||
/// \param tol Convergence threshold on `‖G‖∞`. Default: 1e-8.
|
||||
/// \param max_iter Newton iteration limit. Default: 200.
|
||||
/// \return NewtonResult{x*, iterations, grad_inf_norm, converged}.
|
||||
///
|
||||
/// \note Unlike the Euclidean solver, the CP-Euclidean Hessian is exact
|
||||
/// (analytic), so the SparseQR fallback only triggers in genuine
|
||||
/// gauge-singular situations (no pinned face).
|
||||
/// \see doc/architecture/phase-9a-validation.md §1 for the BPS-2010 mapping.
|
||||
inline NewtonResult newton_cp_euclidean(
|
||||
ConformalMesh& mesh,
|
||||
std::vector<double> x0,
|
||||
const CPEuclideanMaps& m,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200)
|
||||
{
|
||||
std::vector<double> x = x0;
|
||||
const int n = static_cast<int>(x.size());
|
||||
|
||||
NewtonResult res;
|
||||
res.converged = false;
|
||||
res.iterations = 0;
|
||||
res.grad_inf_norm = 0.0;
|
||||
|
||||
for (int iter = 0; iter < max_iter; ++iter) {
|
||||
auto G_std = cp_euclidean_gradient(mesh, x, m);
|
||||
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
||||
|
||||
double inf_norm = G.cwiseAbs().maxCoeff();
|
||||
if (inf_norm < tol) {
|
||||
res.converged = true;
|
||||
res.grad_inf_norm = inf_norm;
|
||||
res.iterations = iter;
|
||||
res.x = x;
|
||||
return res;
|
||||
}
|
||||
|
||||
auto H = cp_euclidean_hessian(mesh, x, m);
|
||||
bool ok = false;
|
||||
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
|
||||
if (!ok) break;
|
||||
|
||||
double norm0 = G.norm();
|
||||
x = detail::line_search(x, dx, norm0,
|
||||
[&](const std::vector<double>& xnew) {
|
||||
return cp_euclidean_gradient(mesh, xnew, m);
|
||||
});
|
||||
|
||||
res.iterations = iter + 1;
|
||||
}
|
||||
|
||||
auto G_final = cp_euclidean_gradient(mesh, x, m);
|
||||
double inf_final = 0.0;
|
||||
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
||||
res.grad_inf_norm = inf_final;
|
||||
res.x = x;
|
||||
return res;
|
||||
}
|
||||
|
||||
// ── Inversive-Distance Newton solver (Phase 9a.2) ─────────────────────────────
|
||||
|
||||
/// Solve the inversive-distance circle-packing problem: find u ∈ ℝ^V such that
|
||||
/// Σ_{faces adj v} α_v(u) = Θ_v at every free vertex (Luo 2004 Lemma 3.1).
|
||||
///
|
||||
/// The inversive-distance energy is (locally) strictly convex on the open
|
||||
/// domain where every triangle satisfies the inequalities. Luo's 1-form is
|
||||
/// closed there, so the path-integral energy is well-defined.
|
||||
///
|
||||
/// MVP implementation: the Hessian is computed by **finite differences** of
|
||||
/// the analytic gradient (same pattern as the Phase 4a HyperIdeal solver).
|
||||
/// An analytic Hessian via Glickenstein 2011 eq. (4.6) is tracked in
|
||||
/// `doc/roadmap/research-track.md` as Phase 9a.2-analytic.
|
||||
///
|
||||
/// \param mesh Input triangle mesh.
|
||||
/// \param x0 Initial DOF vector (length = number of free vertices).
|
||||
/// \param m InversiveDistanceMaps: v_idx has at least one pinned
|
||||
/// vertex; I_e and r0 set by compute_inversive_distance_init.
|
||||
/// \param tol Convergence threshold on `‖G‖∞`. Default: 1e-8.
|
||||
/// \param max_iter Newton iteration limit. Default: 200.
|
||||
/// \param hess_eps FD step size for the Hessian. Default: 1e-5.
|
||||
/// \return NewtonResult{x*, iterations, grad_inf_norm, converged}.
|
||||
///
|
||||
/// \note Convergence is sensitive to the initial point: u = 0 is the
|
||||
/// natural choice when `compute_inversive_distance_init_from_mesh`
|
||||
/// has been called, since the Bowers-Stephenson identity reconstructs
|
||||
/// the input edge lengths at u = 0.
|
||||
inline NewtonResult newton_inversive_distance(
|
||||
ConformalMesh& mesh,
|
||||
std::vector<double> x0,
|
||||
const InversiveDistanceMaps& m,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200,
|
||||
double hess_eps = 1e-5)
|
||||
{
|
||||
std::vector<double> x = x0;
|
||||
const int n = static_cast<int>(x.size());
|
||||
|
||||
NewtonResult res;
|
||||
res.converged = false;
|
||||
res.iterations = 0;
|
||||
res.grad_inf_norm = 0.0;
|
||||
|
||||
// Local FD Hessian builder — n × (cost of gradient eval).
|
||||
auto build_hessian = [&](const std::vector<double>& xc) -> Eigen::SparseMatrix<double> {
|
||||
std::vector<Eigen::Triplet<double>> trips;
|
||||
trips.reserve(static_cast<std::size_t>(n) * 16); // sparse heuristic
|
||||
|
||||
std::vector<double> xp = xc, xm = xc;
|
||||
for (int j = 0; j < n; ++j) {
|
||||
const std::size_t sj = static_cast<std::size_t>(j);
|
||||
xp[sj] = xc[sj] + hess_eps;
|
||||
xm[sj] = xc[sj] - hess_eps;
|
||||
|
||||
auto Gp = inversive_distance_gradient(mesh, xp, m);
|
||||
auto Gm = inversive_distance_gradient(mesh, xm, m);
|
||||
|
||||
xp[sj] = xm[sj] = xc[sj]; // restore
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double val = (Gp[static_cast<std::size_t>(i)]
|
||||
- Gm[static_cast<std::size_t>(i)])
|
||||
/ (2.0 * hess_eps);
|
||||
if (std::abs(val) > 1e-15)
|
||||
trips.emplace_back(i, j, val);
|
||||
}
|
||||
}
|
||||
Eigen::SparseMatrix<double> H(n, n);
|
||||
H.setFromTriplets(trips.begin(), trips.end());
|
||||
// Symmetrise — FD rounding may introduce tiny asymmetries.
|
||||
Eigen::SparseMatrix<double> Ht = H.transpose();
|
||||
return (H + Ht) * 0.5;
|
||||
};
|
||||
|
||||
for (int iter = 0; iter < max_iter; ++iter) {
|
||||
auto G_std = inversive_distance_gradient(mesh, x, m);
|
||||
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
||||
|
||||
double inf_norm = G.cwiseAbs().maxCoeff();
|
||||
if (inf_norm < tol) {
|
||||
res.converged = true;
|
||||
res.grad_inf_norm = inf_norm;
|
||||
res.iterations = iter;
|
||||
res.x = x;
|
||||
return res;
|
||||
}
|
||||
|
||||
auto H = build_hessian(x);
|
||||
bool ok = false;
|
||||
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
|
||||
if (!ok) break;
|
||||
|
||||
double norm0 = G.norm();
|
||||
x = detail::line_search(x, dx, norm0,
|
||||
[&](const std::vector<double>& xnew) {
|
||||
return inversive_distance_gradient(mesh, xnew, m);
|
||||
});
|
||||
|
||||
res.iterations = iter + 1;
|
||||
}
|
||||
|
||||
auto G_final = inversive_distance_gradient(mesh, x, m);
|
||||
double inf_final = 0.0;
|
||||
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
||||
res.grad_inf_norm = inf_final;
|
||||
res.x = x;
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace conformallab
|
||||
|
||||
@@ -79,6 +79,11 @@ add_executable(conformallab_cgal_tests
|
||||
# reference; implemented from the literature. Cross-validated against
|
||||
# EuclideanCyclicFunctional at the natural initial geometry (u = 0).
|
||||
test_inversive_distance_functional.cpp
|
||||
|
||||
# ── Phase 9a: Newton solvers for the two new circle-packing functionals ──
|
||||
# Convergence tests for newton_cp_euclidean (analytic Hessian) and
|
||||
# newton_inversive_distance (FD Hessian).
|
||||
test_newton_phase9a.cpp
|
||||
)
|
||||
|
||||
target_include_directories(conformallab_cgal_tests SYSTEM PRIVATE
|
||||
|
||||
264
code/tests/cgal/test_newton_phase9a.cpp
Normal file
264
code/tests/cgal/test_newton_phase9a.cpp
Normal file
@@ -0,0 +1,264 @@
|
||||
// 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";
|
||||
}
|
||||
@@ -245,3 +245,54 @@ Phase 10 Global uniformization for genus g ≥ 2
|
||||
None of these are required for the genus-g uniformization
|
||||
pipeline; they extend the breadth of methods.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ◼ Phase 11+ — Specialised applications (optional, deferred)
|
||||
|
||||
> **Status:** out-of-scope for v1.0 but recorded here so that future
|
||||
> contributors don't re-discover them. Both items live in the Java
|
||||
> repo as plugin sub-packages and would benefit from porting *only*
|
||||
> after Phase 10 is complete (they require the period-matrix and
|
||||
> fundamental-domain infrastructure to be in place first).
|
||||
|
||||
```
|
||||
11a Schottky uniformisation
|
||||
Java plugin: plugin/schottky/* (~12 Java files, ~3 000 LoC)
|
||||
Mathematical basis: Schottky group — discrete subgroup
|
||||
Γ ⊂ PSL(2,ℂ) generated by hyperbolic loxodromic
|
||||
elements, fundamental domain a sphere with
|
||||
2g disjoint discs removed.
|
||||
Use case: "handlebody" uniformisation, complement of
|
||||
Phase 10c's Fuchsian-group representation
|
||||
(Schottky represents Riemann surfaces as
|
||||
quotients of domains in S² rather than of H²).
|
||||
Requires: Phase 10b (period matrix) + working
|
||||
Möbius-group machinery from Phase 7.
|
||||
Effort: very large (4–6 weeks) — significant Java
|
||||
code, complex-analytic algorithms,
|
||||
substantial test design.
|
||||
|
||||
11b Riemann maps (planar conformal mapping)
|
||||
Java plugin: plugin/riemannmap/* (~6 Java files, ~1 500 LoC)
|
||||
Mathematical basis: Riemann mapping theorem — every simply
|
||||
connected proper subdomain of ℂ is conformally
|
||||
equivalent to the unit disc. Discrete version
|
||||
via circle packing or Schwarz-Christoffel-like
|
||||
formulae.
|
||||
Use case: Texture mapping of bounded planar regions;
|
||||
classical conformal mapping for engineering
|
||||
applications (electrostatics, fluid flow).
|
||||
Requires: Phase 10b' QuasiisothermicUtility or the
|
||||
CP-Euclidean machinery from Phase 9a.1
|
||||
(depending on the chosen discrete-Riemann
|
||||
algorithm).
|
||||
Effort: large (3–4 weeks) — smaller than Schottky
|
||||
but still substantial. Heavy on
|
||||
visualisation; consider porting only the
|
||||
algorithmic core.
|
||||
|
||||
Both items are tracked here so the project memory is preserved; they
|
||||
are NOT roadmap commitments. See `research-track.md` for the formal
|
||||
research-versus-port classification before starting either.
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user