Für einen Mathematiker der unabhängig validieren und eigene Forschung
einbringen möchte.
Doxygen-Kommentare (code/include/):
newton_solver.hpp — newton_euclidean(), newton_spherical(), newton_hyper_ideal()
je mit \param, \return, \note, \see inkl. mathematischer Begründung
(Konvexität, Vorzeichenkonvention, SparseQR-Fallback-Erklärung)
layout.hpp — euclidean_layout(), spherical_layout(), hyper_ideal_layout()
mit vollständiger Parameter-Doku, halfedge_uv-Semantik, Poincaré-Disk-Note
Neues Dokument:
doc/math/validation-protocol.md
7 reproduzierbare Checks mit konkreten Befehlen und erwartetem Output:
0. 170 Tests, 1 Skip
1. Gauss–Bonnet exakt (1e-10)
2. FD-Gradientencheck < 1e-6 für alle 3 Geometrien
3. Newton-Konvergenz < 50 Iterationen
4. τ ∈ SL(2,ℤ)-Fundamentaldomäne (3 Invarianten)
5. Möbius-Arithmetik (Inverse, Compose, from_three)
6. End-to-End-Pipeline
7. Manueller τ-Check für torus_4x4.off (Codebeispiel)
Neues Tutorial:
doc/tutorials/add-inversive-distance.md
Vollständiger Step-by-Step-Port von Phase 9a (Luo 2004):
Header anlegen, Energie/Gradient implementieren, FD-Check,
Newton-Wrapper, CMakeLists, Java-Referenzvergleich, Checkliste.
doc/getting-started.md:
Abschnitt "Known issues": macOS-Finder-Duplikate (rm-Befehl),
Warnung "First build 30–90s" (Tarball-Extraktion)
README.md:
Zwei neue Links in der Dokumentationstabelle (validation-protocol,
tutorial)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
379 lines
17 KiB
C++
379 lines
17 KiB
C++
#pragma once
|
||
// newton_solver.hpp
|
||
//
|
||
// Phase 4a — Newton solver for all three discrete conformal functionals.
|
||
//
|
||
// Solves G(x) = 0 where G is the gradient of the discrete conformal energy.
|
||
//
|
||
// ┌──────────────────────────────────────────────────────────────────────────┐
|
||
// │ Gradient sign conventions │
|
||
// │ Euclidean / Spherical: G_v = Θ_v − Σ α_v (target − actual) │
|
||
// │ HyperIdeal: G_v = Σ β_v − Θ_v (actual − target) │
|
||
// │ │
|
||
// │ All solvers use the same Newton step Δx = −H⁻¹·G │
|
||
// │ │
|
||
// │ Hessian sign at equilibrium │
|
||
// │ Euclidean: H PSD → SimplicialLDLT on H │
|
||
// │ Spherical: H NSD → SimplicialLDLT on −H (solve (−H)Δx = G) │
|
||
// │ HyperIdeal: H PSD → SimplicialLDLT on H (analytical H: future) │
|
||
// └──────────────────────────────────────────────────────────────────────────┘
|
||
//
|
||
// SparseQR fallback:
|
||
// When SimplicialLDLT reports a failure (e.g. singular H on a closed mesh
|
||
// without a pinned vertex), the solver automatically retries with
|
||
// Eigen::SparseQR, which finds the minimum-norm Newton step orthogonal to
|
||
// the null space. This handles the gauge mode on closed surfaces without
|
||
// requiring the caller to pin a vertex explicitly.
|
||
//
|
||
// Requires:
|
||
// Eigen::SimplicialLDLT, Eigen::SparseQR (Eigen sparse module)
|
||
|
||
#include "euclidean_hessian.hpp"
|
||
#include "spherical_hessian.hpp"
|
||
#include "hyper_ideal_hessian.hpp"
|
||
#include <Eigen/SparseCholesky>
|
||
#include <Eigen/SparseQR>
|
||
#include <Eigen/OrderingMethods>
|
||
#include <Eigen/Dense>
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
namespace conformallab {
|
||
|
||
// ── Result ────────────────────────────────────────────────────────────────────
|
||
|
||
struct NewtonResult {
|
||
std::vector<double> x; ///< DOF vector at termination
|
||
int iterations; ///< Newton steps taken
|
||
double grad_inf_norm;///< max |G_i| at termination
|
||
bool converged; ///< true iff grad_inf_norm < tol
|
||
};
|
||
|
||
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||
|
||
namespace detail {
|
||
|
||
// Solve A·Δx = rhs with SimplicialLDLT; on failure fall back to SparseQR.
|
||
// Returns Δx. ok is set to false only if both solvers fail.
|
||
// If fallback_used is non-null, it is set to true iff SparseQR was needed.
|
||
inline Eigen::VectorXd solve_with_fallback(
|
||
const Eigen::SparseMatrix<double>& A,
|
||
const Eigen::VectorXd& rhs,
|
||
bool& ok,
|
||
bool* fallback_used = nullptr)
|
||
{
|
||
if (fallback_used) *fallback_used = false;
|
||
|
||
Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>> ldlt(A);
|
||
if (ldlt.info() == Eigen::Success) {
|
||
Eigen::VectorXd dx = ldlt.solve(rhs);
|
||
if (ldlt.info() == Eigen::Success) { ok = true; return dx; }
|
||
}
|
||
// Fallback: SparseQR — handles singular/rank-deficient H (gauge modes).
|
||
if (fallback_used) *fallback_used = true;
|
||
Eigen::SparseQR<Eigen::SparseMatrix<double>, Eigen::COLAMDOrdering<int>> qr(A);
|
||
if (qr.info() == Eigen::Success) {
|
||
Eigen::VectorXd dx = qr.solve(rhs);
|
||
if (qr.info() == Eigen::Success) { ok = true; return dx; }
|
||
}
|
||
ok = false;
|
||
return Eigen::VectorXd::Zero(rhs.size());
|
||
}
|
||
|
||
} // namespace detail
|
||
|
||
// ── Public linear-system solver (SparseQR fallback) ──────────────────────────
|
||
//
|
||
// Solve A·x = rhs with Eigen::SimplicialLDLT; if that fails (singular or
|
||
// rank-deficient A), retry with Eigen::SparseQR which finds the minimum-norm
|
||
// solution orthogonal to the null space.
|
||
//
|
||
// This is the same primitive used internally by all three Newton solvers.
|
||
// Exposing it publicly lets callers (tests, downstream code) reuse the logic
|
||
// and — via the optional fallback_used pointer — verify which code path ran.
|
||
//
|
||
// fallback_used – if non-null, set to true iff SparseQR was invoked
|
||
// Returns Eigen::VectorXd::Zero(rhs.size()) if both solvers fail.
|
||
inline Eigen::VectorXd solve_linear_system(
|
||
const Eigen::SparseMatrix<double>& A,
|
||
const Eigen::VectorXd& rhs,
|
||
bool* fallback_used = nullptr)
|
||
{
|
||
bool ok = false;
|
||
return detail::solve_with_fallback(A, rhs, ok, fallback_used);
|
||
}
|
||
|
||
namespace detail { // re-open for the remaining helpers
|
||
|
||
// Backtracking line search: find the largest α in {1, 0.5, 0.25, …} such that
|
||
// ||G(x + α·Δx)||₂ < ||G(x)||₂. Returns the accepted step (α may stay 1).
|
||
template <typename GradFn>
|
||
inline std::vector<double> line_search(
|
||
const std::vector<double>& x,
|
||
const Eigen::VectorXd& dx,
|
||
double norm0,
|
||
GradFn&& grad_fn,
|
||
int max_halvings = 20)
|
||
{
|
||
const int n = static_cast<int>(x.size());
|
||
double alpha = 1.0;
|
||
std::vector<double> xnew(static_cast<std::size_t>(n));
|
||
|
||
for (int ls = 0; ls < max_halvings; ++ls) {
|
||
for (int i = 0; i < n; ++i)
|
||
xnew[static_cast<std::size_t>(i)] = x[static_cast<std::size_t>(i)]
|
||
+ alpha * dx[i];
|
||
auto Gnew = grad_fn(xnew);
|
||
double norm_new = 0.0;
|
||
for (double v : Gnew) norm_new += v * v;
|
||
norm_new = std::sqrt(norm_new);
|
||
if (norm_new < norm0) return xnew;
|
||
alpha *= 0.5;
|
||
}
|
||
// No improvement found — return best attempt (full step)
|
||
for (int i = 0; i < n; ++i)
|
||
xnew[static_cast<std::size_t>(i)] = x[static_cast<std::size_t>(i)] + dx[i];
|
||
return xnew;
|
||
}
|
||
|
||
} // namespace detail
|
||
|
||
// ── Euclidean Newton solver ────────────────────────────────────────────────────
|
||
|
||
/// Solve the Euclidean discrete conformal problem: find u ∈ ℝ^V such that
|
||
/// Σ_{faces adj v} α_v(u) = Θ_v for all vertices v.
|
||
///
|
||
/// Starting from x0, Newton's method minimises E(u) (the Euclidean DCE energy,
|
||
/// which is convex) by iterating u ← u − H⁻¹·G with backtracking line search.
|
||
/// The Hessian H is the cotangent Laplacian — PSD with one zero eigenvalue on
|
||
/// closed surfaces (gauge mode). A SparseQR fallback handles this automatically.
|
||
///
|
||
/// \param mesh Input triangulated surface (edges must carry lambda0 + theta_v).
|
||
/// \param x0 Initial DOF vector (length = number of free vertices).
|
||
/// Pass all-zeros for a flat start (typical).
|
||
/// \param m EuclideanMaps: lambda0[e], theta_v[v], v_idx[v] must be set.
|
||
/// Call setup_euclidean_maps() + compute_euclidean_lambda0_from_mesh()
|
||
/// + enforce_gauss_bonnet() before passing here.
|
||
/// \param tol Convergence threshold on max |G_i|. Default: 1e-8.
|
||
/// \param max_iter Maximum Newton iterations. Default: 200.
|
||
/// \return NewtonResult{x*, iterations, grad_inf_norm, converged}.
|
||
///
|
||
/// \note On closed meshes without a pinned vertex, SimplicialLDLT detects the
|
||
/// gauge singularity and falls back to SparseQR automatically.
|
||
///
|
||
/// \see doc/math/discrete-conformal-theory.md §3 for the mathematical background.
|
||
inline NewtonResult newton_euclidean(
|
||
ConformalMesh& mesh,
|
||
std::vector<double> x0,
|
||
const EuclideanMaps& 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;
|
||
|
||
Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>> solver;
|
||
|
||
for (int iter = 0; iter < max_iter; ++iter) {
|
||
// ── Gradient ──────────────────────────────────────────────────────────
|
||
auto G_std = 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;
|
||
}
|
||
|
||
// ── Hessian + solve H·Δx = −G (SparseQR fallback for singular H) ──
|
||
auto H = euclidean_hessian(mesh, x, m);
|
||
bool ok = false;
|
||
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
|
||
if (!ok) break;
|
||
|
||
// ── Backtracking line search ──────────────────────────────────────────
|
||
double norm0 = G.norm();
|
||
x = detail::line_search(x, dx, norm0,
|
||
[&](const std::vector<double>& xnew) {
|
||
return euclidean_gradient(mesh, xnew, m);
|
||
});
|
||
|
||
res.iterations = iter + 1;
|
||
}
|
||
|
||
// Report final gradient norm
|
||
auto G_final = 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;
|
||
}
|
||
|
||
// ── Spherical Newton solver ───────────────────────────────────────────────────
|
||
|
||
/// Solve the spherical discrete conformal problem: find u ∈ ℝ^V such that
|
||
/// Σ_{faces adj v} α_v(u) = Θ_v for all vertices v (genus-0 / sphere-like surfaces).
|
||
///
|
||
/// The spherical DCE energy is *concave*, so the Hessian H is NSD at the solution.
|
||
/// The solver factorises −H (which is PSD) and solves (−H)·Δx = G.
|
||
/// A gauge vertex must be pinned (set v_idx = -1) to remove the rotational mode.
|
||
///
|
||
/// \param mesh Input triangulated surface, genus 0.
|
||
/// \param x0 Initial DOF vector (length = free vertices, excluding gauge_vertex).
|
||
/// All-zeros is a good start.
|
||
/// \param m SphericalMaps: lambda0[e], theta_v[v], v_idx[v], gauge_vertex set.
|
||
/// Call setup_spherical_maps() + compute_spherical_lambda0_from_mesh()
|
||
/// + enforce_gauss_bonnet() (checks Σ(2π-Θ) > 0) before passing here.
|
||
/// \param tol Convergence threshold on max |G_i|. Default: 1e-8.
|
||
/// \param max_iter Maximum Newton iterations. Default: 200.
|
||
/// \return NewtonResult{x*, iterations, grad_inf_norm, converged}.
|
||
///
|
||
/// \note Unlike the Euclidean solver, the spherical solver does NOT need a SparseQR
|
||
/// fallback — the gauge vertex pins the null mode directly.
|
||
///
|
||
/// \see doc/math/geometry-modes.md §Spherical for sign-convention details.
|
||
inline NewtonResult newton_spherical(
|
||
ConformalMesh& mesh,
|
||
std::vector<double> x0,
|
||
const SphericalMaps& 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) {
|
||
// ── Gradient ──────────────────────────────────────────────────────────
|
||
auto G_std = spherical_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;
|
||
}
|
||
|
||
// ── Hessian: negate to get PSD; solve (−H)·Δx = G ──────────────────
|
||
auto H = spherical_hessian(mesh, x, m);
|
||
auto negH = Eigen::SparseMatrix<double>(-H);
|
||
bool ok = false;
|
||
Eigen::VectorXd dx = detail::solve_with_fallback(negH, G, ok);
|
||
if (!ok) break;
|
||
|
||
// ── Backtracking line search ──────────────────────────────────────────
|
||
double norm0 = G.norm();
|
||
x = detail::line_search(x, dx, norm0,
|
||
[&](const std::vector<double>& xnew) {
|
||
return spherical_gradient(mesh, xnew, m);
|
||
});
|
||
|
||
res.iterations = iter + 1;
|
||
}
|
||
|
||
auto G_final = spherical_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;
|
||
}
|
||
|
||
// ── HyperIdeal Newton solver ──────────────────────────────────────────────────
|
||
|
||
/// Solve the hyper-ideal discrete conformal problem: find (b, a) ∈ ℝ^{V+E} such that
|
||
/// Σ β_v(b,a) = Θ_v and Σ α_e(b,a) = θ_e for all vertices v and edges e.
|
||
///
|
||
/// Used for genus-g surfaces (g ≥ 1) under hyperbolic cone metrics.
|
||
/// The energy is *strictly convex* (Springborn 2020, Theorem 1.3), so Newton
|
||
/// converges globally from any starting point.
|
||
///
|
||
/// DOF layout: first V_free entries are vertex variables b_v (hyper-ideal radii),
|
||
/// followed by E entries for edge variables a_e (intersection angles).
|
||
/// Use assign_all_dof_indices(mesh, maps) to set v_idx and e_idx automatically —
|
||
/// no vertex needs to be pinned.
|
||
///
|
||
/// \param mesh Input triangulated surface, genus g ≥ 1.
|
||
/// \param x0 Initial DOF vector (length = V + E). All-zeros typical.
|
||
/// \param m HyperIdealMaps: lambda0[e], theta_v[v], v_idx[v], e_idx[e] set.
|
||
/// Call setup_hyper_ideal_maps() + compute_hyper_ideal_lambda0_from_mesh().
|
||
/// \param tol Convergence threshold on max |G_i|. Default: 1e-8.
|
||
/// \param max_iter Maximum Newton iterations. Default: 200.
|
||
/// \param hess_eps Finite-difference step for Hessian approximation. Default: 1e-5.
|
||
/// (Phase 9b will replace this with an analytic Hessian.)
|
||
/// \return NewtonResult{x*, iterations, grad_inf_norm, converged}.
|
||
///
|
||
/// \see Springborn (2020), Theorem 1.3 for the strict convexity proof.
|
||
/// \see doc/math/geometry-modes.md §Hyper-ideal for DOF layout details.
|
||
inline NewtonResult newton_hyper_ideal(
|
||
ConformalMesh& mesh,
|
||
std::vector<double> x0,
|
||
const HyperIdealMaps& 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;
|
||
|
||
for (int iter = 0; iter < max_iter; ++iter) {
|
||
// ── Gradient ──────────────────────────────────────────────────────────
|
||
auto G_std = evaluate_hyper_ideal(mesh, x, m, /*energy=*/false).gradient;
|
||
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;
|
||
}
|
||
|
||
// ── Hessian (numerical FD) + solve H·Δx = −G ─────────────────────────
|
||
auto H = hyper_ideal_hessian_sym(mesh, x, m, hess_eps);
|
||
bool ok = false;
|
||
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
|
||
if (!ok) break;
|
||
|
||
// ── Backtracking line search ──────────────────────────────────────────
|
||
double norm0 = G.norm();
|
||
x = detail::line_search(x, dx, norm0,
|
||
[&](const std::vector<double>& xnew) {
|
||
return evaluate_hyper_ideal(mesh, xnew, m, false).gradient;
|
||
});
|
||
|
||
res.iterations = iter + 1;
|
||
}
|
||
|
||
auto G_final = evaluate_hyper_ideal(mesh, x, m, false).gradient;
|
||
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
|