Files
ConformalLabpp/code/include/cut_graph.hpp
Tarik Moussa 4fc48b39f0 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>
2026-05-13 01:15:41 +02:00

153 lines
6.4 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
// cut_graph.hpp
//
// Phase 6 — Tree-cotree algorithm for computing a cut graph of a triangulated
// surface.
//
// For a closed, orientable, genus-g surface:
// #vertices (V), #edges (E), #faces (F)
// Euler: V E + F = 2 2g
// Primal spanning tree: V 1 edges
// Dual spanning tree: F 1 edges (avoiding duals of tree edges)
// Remaining: E (V1) (F1) = 2g cut edges
//
// These 2g cut edges generate H₁(M, ) ≅ ^{2g}.
// Cutting along them turns M into a topological disk.
//
// For open meshes (boundary present) the algorithm still works: the dual BFS
// starts from a boundary-adjacent face, and boundary half-edges are skipped.
// The number of cut edges will be E (V1) (F1) B where B counts
// boundary edges treated as dual tree edges.
//
// Usage:
// CutGraph cg = compute_cut_graph(mesh);
// // cg.cut_edge_flags[e.idx()] == true → treat edge as seam in BFS
// euclidean_layout(mesh, x, maps, &cg); // layout with holonomy tracking
#include "conformal_mesh.hpp"
#include "gauss_bonnet.hpp" // for euler_characteristic / genus
#include <vector>
#include <queue>
#include <cstddef>
namespace conformallab {
// ─────────────────────────────────────────────────────────────────────────────
// CutGraph
// ─────────────────────────────────────────────────────────────────────────────
struct CutGraph {
/// cut_edge_flags[e.idx()] = true ↔ this edge is a cut edge.
/// Size = mesh.number_of_edges().
std::vector<bool> cut_edge_flags;
/// Indices of the 2g cut edges in order (size = 2g).
std::vector<std::size_t> cut_edge_indices;
/// Genus of the surface (0 for topological spheres and open patches).
int genus = 0;
bool is_cut(Edge_index e) const
{
return static_cast<std::size_t>(e.idx()) < cut_edge_flags.size()
&& cut_edge_flags[static_cast<std::size_t>(e.idx())];
}
};
// ─────────────────────────────────────────────────────────────────────────────
// compute_cut_graph
// ─────────────────────────────────────────────────────────────────────────────
//
// Implements the standard tree-cotree algorithm (EricksonWhittlesey 2005):
//
// Step 1: BFS primal spanning tree T (V1 primal tree edges).
// Step 2: BFS dual spanning tree T* (F1 dual/primal edges, avoiding
// edges whose primal crosses T).
// Step 3: cut edges = primal edges neither in T nor "used" by T*.
inline CutGraph compute_cut_graph(const ConformalMesh& mesh)
{
const std::size_t nv = mesh.number_of_vertices();
const std::size_t ne = mesh.number_of_edges();
const std::size_t nf = mesh.number_of_faces();
CutGraph cg;
cg.cut_edge_flags.assign(ne, false);
cg.genus = conformallab::genus(mesh);
if (nv == 0 || nf == 0) return cg;
// ── Step 1: primal spanning tree via BFS from vertex 0 ───────────────────
std::vector<bool> tree_edge(ne, false);
std::vector<bool> v_visited(nv, false);
{
std::queue<Vertex_index> q;
auto v0 = *mesh.vertices().begin();
v_visited[v0.idx()] = true;
q.push(v0);
while (!q.empty()) {
Vertex_index v = q.front(); q.pop();
for (Halfedge_index h : CGAL::halfedges_around_target(v, mesh)) {
Vertex_index u = mesh.source(h);
if (!v_visited[static_cast<std::size_t>(u.idx())]) {
v_visited[static_cast<std::size_t>(u.idx())] = true;
tree_edge[static_cast<std::size_t>(mesh.edge(h).idx())] = true;
q.push(u);
}
}
}
}
// ── Step 2: dual spanning tree via BFS from face 0 ───────────────────────
// Dual edge between face f and face f_adj crosses primal edge e.
// Include dual edge only if:
// (a) e is not a primal tree edge (tree_edge[e] == false)
// (b) h_adj is not a border halfedge
std::vector<bool> dual_tree_edge(ne, false);
std::vector<bool> f_visited(nf, false);
{
std::queue<Face_index> q;
auto f0 = *mesh.faces().begin();
f_visited[static_cast<std::size_t>(f0.idx())] = true;
q.push(f0);
while (!q.empty()) {
Face_index f = q.front(); q.pop();
for (Halfedge_index h :
CGAL::halfedges_around_face(mesh.halfedge(f), mesh))
{
Halfedge_index h_opp = mesh.opposite(h);
if (mesh.is_border(h_opp)) continue; // boundary edge
Face_index f_adj = mesh.face(h_opp);
if (f_visited[static_cast<std::size_t>(f_adj.idx())]) continue;
std::size_t eidx = static_cast<std::size_t>(mesh.edge(h).idx());
if (!tree_edge[eidx]) {
// Use this dual edge in T*
f_visited[static_cast<std::size_t>(f_adj.idx())] = true;
dual_tree_edge[eidx] = true;
q.push(f_adj);
}
}
}
}
// ── Step 3: cut edges = neither in T nor in T* nor on boundary ───────────
// Boundary edges are adjacent to the "outer face" and need no cutting —
// they are implicitly handled by the boundary itself.
for (Edge_index e : mesh.edges()) {
std::size_t idx = static_cast<std::size_t>(e.idx());
if (tree_edge[idx] || dual_tree_edge[idx]) continue;
// Skip boundary edges — they are not interior homological cycles.
Halfedge_index h = mesh.halfedge(e);
if (mesh.is_border(h) || mesh.is_border(mesh.opposite(h))) continue;
cg.cut_edge_flags[idx] = true;
cg.cut_edge_indices.push_back(idx);
}
return cg;
}
} // namespace conformallab