All checks were successful
C++ Tests / test-fast (pull_request) Successful in 1m57s
API Docs / doc-build (pull_request) Successful in 1m3s
Markdown link check / check (pull_request) Successful in 44s
C++ Tests / test-cgal (pull_request) Has been skipped
C++ Tests / quality-gates (pull_request) Successful in 2m11s
Bundles the 2026-05-29 Java↔C++ math-correctness audit (doc/reviewer/ java-port-audit.md, 11 findings) with two follow-up fixes. Audit code changes: - Finding 3 (spherical_functional): edge-DOF replacement parameterization via spher_eff_lambda; edge gradient α_opp⁺+α_opp⁻−θ_e (drops additive −(S⁺+S⁻)/2) - Finding 4 (spherical_hessian): always-compiled edge-DOF throw guard - Finding 6 (period_matrix): faithful normalizeModulus (0≤Re≤½, Im≥0, |τ|≥1) - Finding 9 (inversive_distance): degenerate-face limiting angles, no skip - Findings 1/2 (euclidean): degenerate gradient limiting angles + Hessian guard Euclidean holonomy/τ fix: develop the cut surface across the dual spanning tree only (cut_graph now exposes is_dual_tree), so genus-1 cut edges yield non-degenerate lattice generators. Previously τ came out 0 / NaN / 1e13 on the bundled tori; now matches the analytic revolution modulus i·√(R²−r²)/r. Re-enabled τ reporting in the Euclidean CLI; rewrote validation.md §3/§4 accordingly. Tests (240 CGAL, 0 skipped): - HolonomyEndToEnd ×3 — tori of revolution (4×4, hex 6×6, 8×8) vs analytic modulus - SphericalFunctional.EdgeGradient_RegularTetClosedForm — independent closed-form π/3 oracle locking the Finding-3 edge formula (the path-integral FD check cannot detect a wrong-but-conservative gradient) Also documents the latent spherical/hyperbolic holonomy-extraction bug (same single-development pattern, dead code today) in research-track.md (Phase 9c/10), and adds favour/normalisations to the codespell ignore list. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
172 lines
7.0 KiB
C++
172 lines
7.0 KiB
C++
#pragma once
|
||
// Copyright (c) 2024-2026 Tarik Moussa.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
// 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 − (V−1) − (F−1) = 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 − (V−1) − (F−1) − 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
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// Cut-graph result of the tree-cotree algorithm: the set of `2g` edges
|
||
/// whose removal turns a closed genus-`g` surface into a topological disk.
|
||
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;
|
||
|
||
/// dual_tree_edge_flags[e.idx()] = true ↔ edge `e` is a dual-spanning-tree
|
||
/// edge (its dual is in T*). Size = mesh.number_of_edges().
|
||
/// Crossing only these edges develops the surface onto a topological disk
|
||
/// (the fundamental polygon), so that the `2g` cut edges become genuine
|
||
/// boundary identifications carrying the holonomy generators.
|
||
std::vector<bool> dual_tree_edge_flags;
|
||
|
||
/// Genus of the surface (0 for topological spheres and open patches).
|
||
int genus = 0;
|
||
|
||
/// `true` iff edge `e` is a cut edge of this graph.
|
||
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())];
|
||
}
|
||
|
||
/// `true` iff edge `e` is a dual-spanning-tree edge (crossable when
|
||
/// developing the surface onto a disk).
|
||
bool is_dual_tree(Edge_index e) const
|
||
{
|
||
return static_cast<std::size_t>(e.idx()) < dual_tree_edge_flags.size()
|
||
&& dual_tree_edge_flags[static_cast<std::size_t>(e.idx())];
|
||
}
|
||
};
|
||
|
||
/// Compute the cut graph of `mesh` via the standard tree-cotree
|
||
/// algorithm (Erickson–Whittlesey 2005): primal BFS spanning tree T,
|
||
/// dual BFS spanning tree T* avoiding T-primals, then the `2g` cut
|
||
/// edges are those in neither T nor 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);
|
||
}
|
||
|
||
// Expose the dual spanning tree T*: developing across only these edges
|
||
// unfolds the surface onto a disk, making the cut edges the boundary
|
||
// identifications that carry the holonomy generators.
|
||
cg.dual_tree_edge_flags = std::move(dual_tree_edge);
|
||
|
||
return cg;
|
||
}
|
||
|
||
} // namespace conformallab
|