Files
ConformalLabpp/code/include/euclidean_functional.hpp
Tarik Moussa 62b02f88b9
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m40s
API Docs / doc-build (pull_request) Successful in 1m2s
C++ Tests / test-cgal (pull_request) Failing after 11m52s
docs(doxygen): 100% public-API coverage (228 → 0 undocumented)
Completes the work begun in the previous commit on this branch.  Every
public symbol under code/include/ now carries a brief Doxygen comment
(0 undocumented per scripts/doxygen-coverage.sh, with the `detail::`
implementation namespaces excluded as before).

Trajectory on this branch:
  start (after Doxyfile fix):  24.0 %  (165 / 437 in the no-detail set
                                       was 105 / 437 when detail counted)
  after PR #17 base commit  :  42.4 %  (165 / 396)
  this commit               : 100.0 %  (396 / 396)

Files touched (all .hpp / .h headers under code/include/):
  * cgal/Conformal_map_traits.h
  * clausen.hpp, conformal_mesh.hpp, constants.hpp (already docd)
  * cp_euclidean_functional.hpp, cut_graph.hpp, discrete_elliptic_utility.hpp
  * euclidean_functional.hpp, euclidean_geometry.hpp, euclidean_hessian.hpp
  * fundamental_domain.hpp, gauss_bonnet.hpp
  * hyper_ideal_{functional,geometry,hessian,utility,visualization_utility}.hpp
  * inversive_distance_functional.hpp, layout.hpp
  * matrix_utility.hpp, mesh_builder.hpp, mesh_io.hpp
  * newton_solver.hpp, p2_utility.hpp, period_matrix.hpp, projective_math.hpp
  * serialization.hpp, spherical_functional.hpp, spherical_geometry.hpp
  * spherical_hessian.hpp, viewer_utils.h

CI:
.gitea/workflows/doxygen-pages.yml now enforces
`scripts/doxygen-coverage.sh --threshold 100`, so any future regression
(a new public function landed without a `///` brief) fails the build
before the Doxygen HTML is published to Codeberg Pages.

Doxygen warnings remain at 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 04:22:49 +02:00

343 lines
15 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
// euclidean_functional.hpp
//
// Energy and gradient of the Euclidean discrete conformal functional
// (EuclideanCyclicFunctional) evaluated on a ConformalMesh.
//
// Ported from de.varylab.discreteconformal.functional.EuclideanCyclicFunctional.
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │ DOFs │
// │ x[v_idx[v]] = u_v conformal factor at vertex v │
// │ x[e_idx[e]] = λ_e edge log-length variable (optional) │
// │ -1 means "pinned" (u_v = 0 / λ_e = 0, only λ° contributes) │
// │ │
// │ Effective log-length (always additive, unlike SphericalFunctional): │
// │ Λ̃_ij = λ°_ij + u_i + u_j + (x[e_idx[e]] if variable, else 0) │
// │ │
// │ Side length: l_ij = exp(Λ̃_ij / 2) │
// │ │
// │ Gradient: │
// │ ∂E/∂u_v = Θ_v Σ_{faces adj. v} α_v(face) │
// │ ∂E/∂λ_e = α_opp(face⁺) + α_opp(face⁻) φ_e │
// │ │
// │ Energy: │
// │ Computed as the path integral E(x) = ∫₀¹ ⟨G(tx), x⟩ dt │
// │ using 10-point Gauss-Legendre quadrature (same as SphericalFunctional)│
// │ This is E(0)=0 by construction and exact for conservative G. │
// └──────────────────────────────────────────────────────────────────────────┘
//
// Halfedge convention (identical to SphericalFunctional):
// h0 = mesh.halfedge(f), h1 = next(h0), h2 = next(h1)
// v1 = source(h0), v2 = source(h1), v3 = source(h2)
// h_alpha[h0] = α3 (angle at v3, opposite edge h0 = v1v2)
// h_alpha[h1] = α1 (angle at v1, opposite edge h1 = v2v3)
// h_alpha[h2] = α2 (angle at v2, opposite edge h2 = v3v1)
//
// Property-map name prefix: "ev:" (vertex) and "ee:" (edge).
#include "conformal_mesh.hpp"
#include "constants.hpp"
#include "euclidean_geometry.hpp"
#include <CGAL/boost/graph/iterator.h>
#include <vector>
#include <cmath>
#include <cstdint>
namespace conformallab {
// ── Property-map type aliases ─────────────────────────────────────────────────
/// Property map vertex → `double` for the Euclidean functional.
using EuclVMapD = ConformalMesh::Property_map<Vertex_index, double>;
/// Property map vertex → `int` for the Euclidean functional.
using EuclVMapI = ConformalMesh::Property_map<Vertex_index, int>;
/// Property map edge → `double` for the Euclidean functional.
using EuclEMapD = ConformalMesh::Property_map<Edge_index, double>;
/// Property map edge → `int` for the Euclidean functional.
using EuclEMapI = ConformalMesh::Property_map<Edge_index, int>;
// ── Persistent map bundle ─────────────────────────────────────────────────────
/// Bundle of the five property maps consumed by the Euclidean functional.
struct EuclideanMaps {
EuclVMapI v_idx; ///< DOF index per vertex (-1 = pinned / u_v = 0)
EuclEMapI e_idx; ///< DOF index per edge (-1 = no edge DOF)
EuclVMapD theta_v; ///< target cone angle Θ_v (default 2π)
EuclEMapD phi_e; ///< target edge turn angle φ_e (default π)
EuclEMapD lambda0; ///< base log-length λ°_e (default 0.0)
};
/// Attach the five Euclidean property maps to `mesh` with sensible
/// defaults and return their handles.
///
/// Defaults:
/// * `v_idx[v] = -1` (every vertex pinned; user must reassign before solving)
/// * `e_idx[e] = -1` (no edge DOFs by default; use `assign_euclidean_all_dof_indices` for cyclic functional)
/// * `theta_v[v] = 2π` (flat interior vertex target)
/// * `phi_e[e] = π` (interior edge turn angle target — flat surface)
/// * `lambda0[e] = 0` (placeholder; call `compute_euclidean_lambda0_from_mesh` next)
///
/// Map name prefix: `"ev:"` (vertex) and `"ee:"` (edge).
inline EuclideanMaps setup_euclidean_maps(ConformalMesh& mesh)
{
EuclideanMaps m;
m.v_idx = mesh.add_property_map<Vertex_index, int> ("ev:idx", -1 ).first;
m.e_idx = mesh.add_property_map<Edge_index, int> ("ee:idx", -1 ).first;
m.theta_v= mesh.add_property_map<Vertex_index, double>("ev:theta", TWO_PI ).first;
m.phi_e = mesh.add_property_map<Edge_index, double>("ee:phi", PI ).first;
m.lambda0= mesh.add_property_map<Edge_index, double>("ee:lam0", 0.0 ).first;
return m;
}
/// Assign sequential DOF indices `0..n-1` to all vertices.
///
/// **Note:** does NOT pin a gauge vertex. For closed meshes the caller
/// must set one `m.v_idx[v] = -1` either before or after this call to
/// remove the rotational mode (the Newton solver's SparseQR fallback
/// will otherwise pick a minimum-norm solution but at higher cost).
inline int assign_euclidean_vertex_dof_indices(ConformalMesh& mesh, EuclideanMaps& m)
{
int idx = 0;
for (auto v : mesh.vertices()) m.v_idx[v] = idx++;
return idx;
}
/// Assign DOF indices for all vertices AND all edges (vertex-DOFs first,
/// then edge-DOFs). Use this overload for the "cyclic" formulation that
/// includes per-edge log-length DOFs (`λ_e`) on top of per-vertex scale
/// factors (`u_v`).
inline int assign_euclidean_all_dof_indices(ConformalMesh& mesh, EuclideanMaps& m)
{
int idx = 0;
for (auto v : mesh.vertices()) m.v_idx[v] = idx++;
for (auto e : mesh.edges()) m.e_idx[e] = idx++;
return idx;
}
/// Count the free DOFs (vertices + edges with index `≥ 0`).
inline int euclidean_dimension(const ConformalMesh& mesh, const EuclideanMaps& m)
{
int dim = 0;
for (auto v : mesh.vertices()) if (m.v_idx[v] >= 0) ++dim;
for (auto e : mesh.edges()) if (m.e_idx[e] >= 0) ++dim;
return dim;
}
/// Set `lambda0` from mesh vertex positions:
/// `λ°_e = 2·log(|p_i p_j|)` (natural log of Euclidean edge length²).
/// This gives `exp(Λ̃_ij / 2) = l_ij` at `x = 0`.
inline void compute_euclidean_lambda0_from_mesh(ConformalMesh& mesh, EuclideanMaps& m)
{
for (auto e : mesh.edges()) {
auto h = mesh.halfedge(e);
auto p1 = mesh.point(mesh.source(h));
auto p2 = mesh.point(mesh.target(h));
double dx = p1.x() - p2.x();
double dy = p1.y() - p2.y();
double dz = p1.z() - p2.z();
double len = std::sqrt(dx*dx + dy*dy + dz*dz);
if (len > 1e-15)
m.lambda0[e] = 2.0 * std::log(len);
else
m.lambda0[e] = -30.0; // degenerate edge
}
}
// ── Internal helpers ──────────────────────────────────────────────────────────
/// Read DOF value from `x` for index `idx`; return 0 if pinned (idx < 0).
static inline double eucl_dof_val(int idx, const std::vector<double>& x)
{
return idx >= 0 ? x[static_cast<std::size_t>(idx)] : 0.0;
}
/// Convert a CGAL half-edge index to a plain `std::size_t` for vector indexing.
static inline std::size_t eucl_hidx(Halfedge_index h)
{
return static_cast<std::size_t>(static_cast<std::uint32_t>(h));
}
/// Compute the Euclidean-functional gradient G(x):
/// * `G_v = Θ_v Σ_faces α_v(face)`
/// * `G_e = α_opp(face⁺) + α_opp(face⁻) φ_e`
///
/// Same half-edge corner-angle storage convention as `spherical_gradient`.
inline std::vector<double> euclidean_gradient(
ConformalMesh& mesh,
const std::vector<double>& x,
const EuclideanMaps& m)
{
const int n = euclidean_dimension(mesh, m);
std::vector<double> G(static_cast<std::size_t>(n), 0.0);
// Per-halfedge corner-angle storage.
const std::size_t nh = mesh.number_of_halfedges();
std::vector<double> h_alpha(nh, 0.0);
// ── Pass 1: compute corner angles per face ────────────────────────────────
for (auto f : mesh.faces()) {
Halfedge_index h0 = mesh.halfedge(f);
Halfedge_index h1 = mesh.next(h0);
Halfedge_index h2 = mesh.next(h1);
Vertex_index v1 = mesh.source(h0);
Vertex_index v2 = mesh.source(h1);
Vertex_index v3 = mesh.source(h2);
Edge_index e12 = mesh.edge(h0);
Edge_index e23 = mesh.edge(h1);
Edge_index e31 = mesh.edge(h2);
// Effective log-lengths (always additive: u_i + u_j regardless of DOF status)
double u1 = eucl_dof_val(m.v_idx[v1], x);
double u2 = eucl_dof_val(m.v_idx[v2], x);
double u3 = eucl_dof_val(m.v_idx[v3], x);
double lam12 = m.lambda0[e12] + u1 + u2 + eucl_dof_val(m.e_idx[e12], x);
double lam23 = m.lambda0[e23] + u2 + u3 + eucl_dof_val(m.e_idx[e23], x);
double lam31 = m.lambda0[e31] + u3 + u1 + eucl_dof_val(m.e_idx[e31], x);
auto fa = euclidean_angles(lam12, lam23, lam31);
if (!fa.valid) continue; // degenerate triangle: contributes 0
// h_alpha[h] = corner angle OPPOSITE to h's edge:
// h0 (edge v1v2) → opposite corner at v3 → α3
// h1 (edge v2v3) → opposite corner at v1 → α1
// h2 (edge v3v1) → opposite corner at v2 → α2
h_alpha[eucl_hidx(h0)] = fa.alpha3;
h_alpha[eucl_hidx(h1)] = fa.alpha1;
h_alpha[eucl_hidx(h2)] = fa.alpha2;
}
// ── Pass 2: accumulate vertex gradient ───────────────────────────────────
// G_v = Θ_v Σ h_alpha[prev(h)] for each incoming non-border h to v.
for (auto v : mesh.vertices()) {
int iv = m.v_idx[v];
if (iv < 0) continue;
double sum_alpha = 0.0;
for (auto h : CGAL::halfedges_around_target(v, mesh)) {
if (mesh.is_border(h)) continue;
sum_alpha += h_alpha[eucl_hidx(mesh.prev(h))];
}
G[static_cast<std::size_t>(iv)] = m.theta_v[v] - sum_alpha;
}
// ── Pass 3: accumulate edge gradient ─────────────────────────────────────
// G_e = α_opp(f⁺) + α_opp(f⁻) φ_e
// α_opp of edge e in face f = h_alpha[halfedge h of e pointing INTO f].
for (auto e : mesh.edges()) {
int ie = m.e_idx[e];
if (ie < 0) continue;
auto h = mesh.halfedge(e);
auto ho = mesh.opposite(h);
double sum = -m.phi_e[e];
if (!mesh.is_border(h)) sum += h_alpha[eucl_hidx(h)];
if (!mesh.is_border(ho)) sum += h_alpha[eucl_hidx(ho)];
G[static_cast<std::size_t>(ie)] = sum;
}
return G;
}
/// Euclidean energy `E(x) = ∫₀¹ ⟨G(t·x), x⟩ dt`, evaluated with
/// 10-point Gauss-Legendre quadrature (same as the Spherical functional).
inline double euclidean_energy(
ConformalMesh& mesh,
const std::vector<double>& x,
const EuclideanMaps& m)
{
static const double gl_s[10] = {
-0.9739065285171717, -0.8650633666889845,
-0.6794095682990244, -0.4333953941292472,
-0.1488743389816312, 0.1488743389816312,
0.4333953941292472, 0.6794095682990244,
0.8650633666889845, 0.9739065285171717
};
static const double gl_w[10] = {
0.0666713443086881, 0.1494513491505806,
0.2190863625159820, 0.2692667193099963,
0.2955242247147529, 0.2955242247147529,
0.2692667193099963, 0.2190863625159820,
0.1494513491505806, 0.0666713443086881
};
const std::size_t n = x.size();
double E = 0.0;
for (int k = 0; k < 10; ++k) {
double t = (1.0 + gl_s[k]) * 0.5;
double wt = gl_w[k] * 0.5;
std::vector<double> tx(n);
for (std::size_t i = 0; i < n; ++i) tx[i] = t * x[i];
auto G = euclidean_gradient(mesh, tx, m);
double dot = 0.0;
for (std::size_t i = 0; i < n; ++i) dot += G[i] * x[i];
E += wt * dot;
}
return E;
}
// ── Full evaluation (energy + gradient) ──────────────────────────────────────
/// Output of `evaluate_euclidean()` — energy plus optional gradient.
struct EuclideanResult {
double energy = 0.0; ///< Functional value at input DOFs.
std::vector<double> gradient; ///< Gradient ∇E (empty if not requested).
};
/// Evaluate the Euclidean functional at DOFs `x`. Returns energy and
/// gradient (toggle via `need_energy` / `need_gradient`).
inline EuclideanResult evaluate_euclidean(
ConformalMesh& mesh,
const std::vector<double>& x,
const EuclideanMaps& m,
bool need_energy = true,
bool need_gradient = true)
{
EuclideanResult res;
if (need_gradient)
res.gradient = euclidean_gradient(mesh, x, m);
if (need_energy)
res.energy = euclidean_energy(mesh, x, m);
return res;
}
/// Finite-difference gradient check for the Euclidean functional
/// (central differences). Defaults `eps = 1e-5`, `tol = 1e-4`.
inline bool gradient_check_euclidean(
ConformalMesh& mesh,
const std::vector<double>& x0,
const EuclideanMaps& m,
double eps = 1e-5,
double tol = 1e-4)
{
auto G = euclidean_gradient(mesh, x0, m);
const int n = static_cast<int>(G.size());
std::vector<double> xp = x0, xm = x0;
bool ok = true;
for (int i = 0; i < n; ++i) {
std::size_t si = static_cast<std::size_t>(i);
xp[si] = x0[si] + eps;
xm[si] = x0[si] - eps;
double Ep = euclidean_energy(mesh, xp, m);
double Em = euclidean_energy(mesh, xm, m);
xp[si] = xm[si] = x0[si]; // restore
double fd = (Ep - Em) / (2.0 * eps);
double err = std::abs(G[si] - fd);
double scale = std::max(1.0, std::abs(G[si]));
if (err / scale > tol) ok = false;
}
return ok;
}
} // namespace conformallab