#pragma once // Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // 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 #include #include #include 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(mesh.number_of_vertices()) - static_cast(mesh.number_of_edges()) + static_cast(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& 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 the HyperIdeal-functional property bundle. inline double gauss_bonnet_sum(const ConformalMesh& m, const HyperIdealMaps& mp) { return gauss_bonnet_sum(m, mp.theta_v); } // ── 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(euler_characteristic(mesh)); } // ── Deficit: lhs − rhs (0 = Gauss–Bonnet satisfied) ───────────────────────── /// Gauss-Bonnet deficit `lhs − rhs`; zero iff the identity is satisfied. template 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 << "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()); } } /// Throws `std::runtime_error` if Gauss-Bonnet is violated by more than `tol`. template 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). /// 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. inline void enforce_gauss_bonnet( ConformalMesh& mesh, ConformalMesh::Property_map& 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(mesh.number_of_vertices()); for (auto v : mesh.vertices()) theta[v] += delta; } /// Distribute the Gauss-Bonnet deficit uniformly across `maps.theta_v`. template inline void enforce_gauss_bonnet(ConformalMesh& mesh, Maps& maps) { enforce_gauss_bonnet(mesh, maps.theta_v); } } // namespace conformallab