Files
ConformalLabpp/code/include/gauss_bonnet.hpp
Tarik Moussa 1375878d9d
All checks were successful
C++ Tests / test-fast (pull_request) Successful in 2m31s
C++ Tests / quality-gates (pull_request) Has been skipped
C++ Tests / test-cgal (pull_request) Has been skipped
feat(p1): CLI extensions + quality measures + stereographic layout
Implement Phase-Session P1 quick wins (4 independent additions):

9h.1: Add --tol and --max-iter CLI options to conformallab_core
  - Newton solver tolerance [default 1e-8]
  - Newton iteration limit [default 200]
  - Thread both through run_euclidean / run_spherical / run_hyper_ideal
  - Update CLI parameter table in documentation

9h.2: Add -g cp_euclidean and -g inversive_distance geometry routes
  - run_cp_euclidean() & run_inversive_distance() pipelines (~60 lines each)
  - Face-based DOF assignment for CP-Euclidean
  - Vertex-based DOF assignment for Inversive-Distance
  - Both integrated into CLI geometry validator (IsMember)

9g.1: Create conformal_quality.hpp with validation measures
  - IsothermicityMeasure: metric anisotropy (conformality deviation)
  - DiscreteConformalEquivalenceMeasure: length-cross-ratio residuals
  - FlippedTriangles: detects inverted/degenerate triangles
  - LengthCrossRatio: discrete conformal invariant computation
  - ConvergenceUtility: aggregated convergence statistics (max/mean/sum)
  - Ported from Java: plugin/visualizer + convergence utilities
  - Includes sanity tests validating finite outputs on valid layouts

9d.3: Create stereographic_layout.hpp for S² → ℂ projection
  - Stereographic projection from north pole: S² → ℂ ∪ {∞}
  - Inverse projection: ℂ → S² for round-trip validation
  - Möbius centring: centres the 2-D point cloud at origin
  - stereographic_layout(Layout3D) -> Layout2D conversion
  - Round-trip tests: south pole, equator, random sphere points
  - Tests: projection/inverse consistency, north pole handling

Test results: 336/336 CGAL tests pass (272 pre-existing + 64 new from all phases)
- conformal_quality.cpp: 13 new tests (measures, isothermic, dce, convergence)
- stereographic_layout.cpp: 10 new tests (projection, inverse, round-trip, layout)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-01 01:25:43 +02:00

205 lines
9.9 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
// Copyright (c) 2024-2026 Tarik Moussa.
// SPDX-License-Identifier: MIT
// gauss_bonnet.hpp
//
// Phase 6 — GaussBonnet consistency check for prescribed target angles.
//
// Before calling newton_*() with custom target angles, verify that
// the angle defect sum matches the topology.
//
// ┌─────────────────────────────────────────────────────────────────────────┐
// │ Geometry Identity to satisfy │
// │ ───────────────────────────────────────────────────────────────────── │
// │ Euclidean/flat Σ_v (2π Θ_v) = 2π · χ(M) (exact equality) │
// │ Spherical Σ_v (2π Θ_v) > 0 (sufficient, χ > 0) │
// │ │
// │ HyperIdeal — NOT SUPPORTED by this header. │
// │ The correct hyperbolic GaussBonnet identity is │
// │ Σ_v (2π Θ_v) Area(M) = 2π · χ(M) │
// │ which differs from the Euclidean identity by the Area(M) > 0 term. │
// │ Computing Area(M) from the HyperIdeal DOFs is non-trivial. │
// │ gauss_bonnet_sum(mesh, HyperIdealMaps) and │
// │ enforce_gauss_bonnet(mesh, HyperIdealMaps) are therefore DELETED. │
// │ Do NOT call check_gauss_bonnet before newton_hyper_ideal — │
// │ it is not needed; the HyperIdeal energy is strictly convex so Newton │
// │ converges without a pre-check. │
// └─────────────────────────────────────────────────────────────────────────┘
//
// If the Euclidean/Spherical check fails, no conformal factor can realise
// the target angles and Newton will silently fail to converge.
//
// PRECONDITION — closed meshes only. Every function here sums (2π Θ_v)
// over ALL vertices. On a mesh with boundary the boundary vertices carry a
// (π Θ_v) term instead, so the identity Σ(2πΘ_v) = 2π·χ does NOT hold and
// `check_gauss_bonnet` will (correctly) throw. For open meshes pin the
// boundary directly and skip the GaussBonnet check (see the CLI's
// flattening path).
//
// API:
// int euler_characteristic(mesh)
// int genus(mesh)
// double gauss_bonnet_sum(mesh, EuclideanMaps/SphericalMaps) — Σ(2π Θ_v)
// double gauss_bonnet_rhs(mesh) — 2π · χ(M)
// double gauss_bonnet_deficit(mesh, maps) — lhs rhs (0 = satisfied)
// void check_gauss_bonnet(mesh, maps [, tol]) — throws if violated
// double enforce_gauss_bonnet(mesh, maps) — shifts θ_v by uniform Δ; returns |deficit|
// (HyperIdealMaps overloads are deleted — see box above)
#include "conformal_mesh.hpp"
#include "euclidean_functional.hpp"
#include "spherical_functional.hpp"
#include "hyper_ideal_functional.hpp"
#include "constants.hpp"
#include <stdexcept>
#include <sstream>
#include <cmath>
#include <string>
namespace conformallab {
// ── Topology helpers ──────────────────────────────────────────────────────────
/// Euler characteristic χ = V E + F.
/// For closed orientable surfaces: χ = 2 2g.
inline int euler_characteristic(const ConformalMesh& mesh)
{
return static_cast<int>(mesh.number_of_vertices())
- static_cast<int>(mesh.number_of_edges())
+ static_cast<int>(mesh.number_of_faces());
}
/// Genus of a closed orientable surface: g = (2 χ) / 2.
/// Returns 0 for open meshes (boundary present) — callers should check.
inline int genus(const ConformalMesh& mesh)
{
int chi = euler_characteristic(mesh);
return (2 - chi) / 2;
}
// ── Left-hand side Σ(2π Θ_v) ─────────────────────────────────────────────
/// Sum `Σ_v (2π Θ_v)` for a raw vertex → angle property map.
inline double gauss_bonnet_sum(
const ConformalMesh& mesh,
const ConformalMesh::Property_map<Vertex_index, double>& theta)
{
double s = 0.0;
for (auto v : mesh.vertices())
s += TWO_PI - theta[v];
return s;
}
/// `gauss_bonnet_sum` for the Euclidean-functional property bundle.
inline double gauss_bonnet_sum(const ConformalMesh& m, const EuclideanMaps& mp)
{ return gauss_bonnet_sum(m, mp.theta_v); }
/// `gauss_bonnet_sum` for the Spherical-functional property bundle.
inline double gauss_bonnet_sum(const ConformalMesh& m, const SphericalMaps& mp)
{ return gauss_bonnet_sum(m, mp.theta_v); }
// gauss_bonnet_sum for HyperIdealMaps is intentionally DELETED.
// The correct hyperbolic GaussBonnet identity is
// Σ(2πΘ_v) Area(M) = 2π·χ(M)
// not the Euclidean form Σ(2πΘ_v) = 2π·χ(M). Providing this overload
// would silently skip the Area term, making check_gauss_bonnet always
// fail for valid hyperbolic targets (e.g. a genus-2 mesh with Θ_v=2π
// gives Σ(2πΘ_v)=0 but 2π·χ=4π → deficit=4π ≠ 0 every time).
// Use newton_hyper_ideal directly — no pre-check is needed because the
// HyperIdeal energy is strictly convex (Springborn 2020 Theorem 1.3).
inline double gauss_bonnet_sum(const ConformalMesh&, const HyperIdealMaps&) = delete;
// ── Right-hand side 2π · χ(M) ───────────────────────────────────────────────
/// Right-hand side of Gauss-Bonnet: `2π · χ(M)`.
inline double gauss_bonnet_rhs(const ConformalMesh& mesh)
{
return TWO_PI * static_cast<double>(euler_characteristic(mesh));
}
// ── Deficit: lhs rhs (0 = GaussBonnet satisfied) ─────────────────────────
/// Gauss-Bonnet deficit `lhs rhs`; zero iff the identity is satisfied.
template <typename Maps>
inline double gauss_bonnet_deficit(const ConformalMesh& mesh, const Maps& maps)
{
return gauss_bonnet_sum(mesh, maps) - gauss_bonnet_rhs(mesh);
}
/// Throws `std::runtime_error` if `|lhs 2π·χ| > tol`.
/// Overload accepting a precomputed `lhs`.
inline void check_gauss_bonnet(const ConformalMesh& mesh,
double lhs,
double tol = 1e-8)
{
double rhs = gauss_bonnet_rhs(mesh);
double def = lhs - rhs;
if (std::abs(def) > tol) {
std::ostringstream msg;
msg << "GaussBonnet violated:\n"
<< " Σ(2πΘ_v) = " << lhs
<< " expected 2π·χ = " << rhs
<< " (χ = " << euler_characteristic(mesh)
<< ", genus = " << genus(mesh) << ")\n"
<< " deficit = " << def;
throw std::runtime_error(msg.str());
}
}
/// Throws `std::runtime_error` if Gauss-Bonnet is violated by more than `tol`.
template <typename Maps>
inline void check_gauss_bonnet(const ConformalMesh& mesh,
const Maps& maps,
double tol = 1e-8)
{
check_gauss_bonnet(mesh, gauss_bonnet_sum(mesh, maps), tol);
}
// ── enforce_gauss_bonnet — adjust θ_v by uniform Δ ───────────────────────────
//
// Adds δ = (lhs rhs) / V to every θ_v so that GaussBonnet holds exactly.
// After this call, check_gauss_bonnet() will not throw (up to floating-point).
// Modifies ALL vertices' θ_v (no v_idx filtering) — the shift is a property
// of the target angles, independent of which vertices are free DOFs.
//
// H3 (test-coverage audit, 2026-06-01): both overloads now return the total
// absolute correction applied: |Σ(2πΘ_v) 2π·χ|. A large value signals
// that the input angles were far from satisfying GaussBonnet.
/// Distribute the Gauss-Bonnet deficit uniformly across all `Θ_v`:
/// add `δ = (lhs rhs) / V` to every entry so that the identity holds
/// exactly afterwards. Overload for a raw property map.
/// Returns `|lhs rhs|` (total absolute correction applied).
inline double enforce_gauss_bonnet(
ConformalMesh& mesh,
ConformalMesh::Property_map<Vertex_index, double>& theta)
{
double lhs = gauss_bonnet_sum(mesh, theta);
double rhs = gauss_bonnet_rhs(mesh);
// Adding δ to every θ_v decreases the sum Σ(2πθ_v) by V·δ.
// We need lhs V·δ = rhs, so δ = (lhs rhs) / V.
double delta = (lhs - rhs) / static_cast<double>(mesh.number_of_vertices());
for (auto v : mesh.vertices())
theta[v] += delta;
return std::abs(lhs - rhs);
}
/// Distribute the Gauss-Bonnet deficit uniformly across `maps.theta_v`.
/// Supported for EuclideanMaps and SphericalMaps only.
/// HyperIdealMaps overload is deleted — see header comment for why.
/// Returns `|lhs rhs|` (total absolute correction applied; see raw-map overload).
template <typename Maps>
inline double enforce_gauss_bonnet(ConformalMesh& mesh, Maps& maps)
{
return enforce_gauss_bonnet(mesh, maps.theta_v);
}
// enforce_gauss_bonnet for HyperIdealMaps is intentionally DELETED.
// The Euclidean identity Σ(2πΘ_v)=2π·χ is not the correct pre-condition
// for HyperIdeal. Calling this function would silently shift Θ_v to
// satisfy the wrong identity, producing incorrect target angles.
inline void enforce_gauss_bonnet(ConformalMesh&, HyperIdealMaps&) = delete;
} // namespace conformallab