feat(phase6): exact hyperbolic layout, Gauss–Bonnet, cut graph, normalisation — 121 tests
New files: - gauss_bonnet.hpp: euler_characteristic, genus, Σ(2π-Θ_v) sum/rhs/deficit, check_gauss_bonnet (throws), enforce_gauss_bonnet (correct sign: Δ=(lhs-rhs)/V) - cut_graph.hpp: CutGraph struct + compute_cut_graph (tree-cotree, Erickson–Whittlesey 2005); boundary edges correctly excluded from cut set - test_phase6.cpp: 26 new tests (GaussBonnet ×8, CutGraph ×6, HyperbolicTrilateration ×4, Normalisation ×4 — all pass) layout.hpp (Phase 6 rewrite): - detail::trilaterate_hyp: exact Möbius + hyperbolic law of cosines replacing old tanh(d/2) - detail::center_poincare_disk: Möbius centering for hyperbolic normalisation - normalise_euclidean: centroid → origin + PCA major-axis rotation - normalise_hyperbolic: Möbius centering in the Poincaré disk - normalise_spherical: Rodrigues rotation → north pole - euclidean_layout / hyper_ideal_layout: optional CutGraph* + HolonomyData* + normalise Bug fixes caught by new tests: - gauss_bonnet.hpp: enforce_gauss_bonnet had wrong sign for delta - cut_graph.hpp: boundary edges were incorrectly marked as cut edges 121 tests pass, 2 skipped (Hessian stubs). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
145
code/include/gauss_bonnet.hpp
Normal file
145
code/include/gauss_bonnet.hpp
Normal file
@@ -0,0 +1,145 @@
|
||||
#pragma once
|
||||
// gauss_bonnet.hpp
|
||||
//
|
||||
// Phase 6 — Gauss–Bonnet consistency check for prescribed target angles.
|
||||
//
|
||||
// Before calling newton_*() with custom target angles, verify that
|
||||
// the angle defect sum matches the topology:
|
||||
//
|
||||
// Σ_v (2π − Θ_v) = 2π · χ(M) (Euclidean / flat)
|
||||
// Σ_v (2π − Θ_v) > 0 (spherical, χ > 0)
|
||||
// Σ_v (2π − Θ_v) < 0 (hyperbolic, χ < 0)
|
||||
//
|
||||
// If this fails, no conformal factor can realise the target angles and
|
||||
// Newton will silently fail to converge.
|
||||
//
|
||||
// API:
|
||||
// int euler_characteristic(mesh)
|
||||
// int genus(mesh)
|
||||
// double gauss_bonnet_sum(mesh, maps) — Σ(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
|
||||
// void enforce_gauss_bonnet(mesh, maps) — shifts θ_v by uniform Δ
|
||||
|
||||
#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) ─────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
inline double gauss_bonnet_sum(const ConformalMesh& m, const EuclideanMaps& mp)
|
||||
{ return gauss_bonnet_sum(m, mp.theta_v); }
|
||||
inline double gauss_bonnet_sum(const ConformalMesh& m, const SphericalMaps& mp)
|
||||
{ return gauss_bonnet_sum(m, mp.theta_v); }
|
||||
inline double gauss_bonnet_sum(const ConformalMesh& m, const HyperIdealMaps& mp)
|
||||
{ return gauss_bonnet_sum(m, mp.theta_v); }
|
||||
|
||||
// ── Right-hand side 2π · χ(M) ───────────────────────────────────────────────
|
||||
|
||||
inline double gauss_bonnet_rhs(const ConformalMesh& mesh)
|
||||
{
|
||||
return TWO_PI * static_cast<double>(euler_characteristic(mesh));
|
||||
}
|
||||
|
||||
// ── Deficit: lhs − rhs (0 = Gauss–Bonnet 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);
|
||||
}
|
||||
|
||||
// ── check_gauss_bonnet — throws std::runtime_error if |deficit| > tol ─────────
|
||||
|
||||
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 << "Gauss–Bonnet violated:\n"
|
||||
<< " Σ(2π−Θ_v) = " << lhs
|
||||
<< " expected 2π·χ = " << rhs
|
||||
<< " (χ = " << euler_characteristic(mesh)
|
||||
<< ", genus = " << genus(mesh) << ")\n"
|
||||
<< " deficit = " << def;
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
}
|
||||
|
||||
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 δ = (rhs − lhs) / V to every θ_v so that Gauss–Bonnet holds exactly.
|
||||
// After this call, check_gauss_bonnet() will not throw (up to floating-point).
|
||||
// Only modifies free vertices (v_idx[v] >= 0 for EuclideanMaps / SphericalMaps;
|
||||
// always all vertices for the raw property-map overload).
|
||||
|
||||
inline void 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;
|
||||
}
|
||||
|
||||
template <typename Maps>
|
||||
inline void enforce_gauss_bonnet(ConformalMesh& mesh, Maps& maps)
|
||||
{
|
||||
enforce_gauss_bonnet(mesh, maps.theta_v);
|
||||
}
|
||||
|
||||
} // namespace conformallab
|
||||
Reference in New Issue
Block a user