Files
ConformalLabpp/code/include/spherical_hessian.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

250 lines
11 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
// spherical_hessian.hpp
//
// Analytical Hessian of the spherical discrete conformal energy —
// the spherical cotangent-Laplace operator.
//
// Ported from de.varylab.discreteconformal.functional.SphericalFunctional
// (the hessian() method, vertex DOFs).
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │ Hessian formula (vertex DOFs only) │
// │ │
// │ For a spherical face (v1, v2, v3) with vertex angles α1, α2, α3: │
// │ │
// │ Spherical cotangent weight for edge (vi, vj) with opposite vk: │
// │ β_k = (π αi αj + αk) / 2 │
// │ w_k = cot(β_k) = 1/tan(β_k) │
// │ │
// │ Euclidean limit: α1+α2+α3 → π, β_k → αk, w_k → cot(αk). ✓ │
// │ │
// │ Hessian contributions per face: │
// │ H[vi, vi] += w_ij + w_ik (diagonal: weights of incident edges) │
// │ H[vi, vj] -= w_ij (off-diagonal: weight of edge ij) │
// │ │
// │ where w_ij is the weight of the edge between vi and vj (opposite vk): │
// │ w_ij = cot(β_k) with β_k = (π αi αj + αk) / 2. │
// └──────────────────────────────────────────────────────────────────────────┘
//
// Requires Eigen (header-only). Returns Eigen::SparseMatrix<double>.
#include "spherical_functional.hpp"
#include <Eigen/Sparse>
#include <vector>
#include <cmath>
namespace conformallab {
// ── Spherical cotangent weight helper ────────────────────────────────────────
//
// Given the three face angles α1, α2, α3 of a spherical triangle, return the
// three edge cotangent weights w1 (edge opp v1), w2 (edge opp v2), w3 (edge opp v3).
//
// w_k = cot(β_k) where β_k = (π α_adj1 α_adj2 + α_opp) / 2
// = (π αi αj + αk) / 2 for edge (vi,vj), opposite vk
//
// Mapping in our CGAL halfedge convention:
// h0 = halfedge(f): edge v1-v2 → opposite v3 → w = cot(β3), β3=(π-α1-α2+α3)/2
// h1: edge v2-v3 → opposite v1 → w = cot(β1), β1=(π-α2-α3+α1)/2
// h2: edge v3-v1 → opposite v2 → w = cot(β2), β2=(π-α3-α1+α2)/2
//
// Returns valid=false if any β_k is out of range (degenerate face).
/// Three spherical "cotangent" weights for the three edges of a face,
/// derived from the per-vertex interior angles `α₁, α₂, α₃` via
/// `w_ij = cot(β_k)` with `β_k = (π α_i α_j + α_k) / 2`.
struct SpherCotWeights {
double w12; ///< Weight for edge v₁-v₂ (opposite vertex v₃).
double w23; ///< Weight for edge v₂-v₃ (opposite vertex v₁).
double w31; ///< Weight for edge v₃-v₁ (opposite vertex v₂).
bool valid; ///< `false` when any β_k is out of `(0, π/2]` (degenerate face).
};
/// Compute the three spherical cot weights from the three interior
/// angles `(α₁, α₂, α₃)` of a spherical triangle. See `SpherCotWeights`.
inline SpherCotWeights spherical_cot_weights(double alpha1, double alpha2, double alpha3)
{
// β for each edge:
// β3 = (π - α1 - α2 + α3)/2 — weight for edge v1-v2 (opposite v3)
// β1 = (π - α2 - α3 + α1)/2 — weight for edge v2-v3 (opposite v1)
// β2 = (π - α3 - α1 + α2)/2 — weight for edge v3-v1 (opposite v2)
const double beta3 = (PI - alpha1 - alpha2 + alpha3) * 0.5;
const double beta1 = (PI - alpha2 - alpha3 + alpha1) * 0.5;
const double beta2 = (PI - alpha3 - alpha1 + alpha2) * 0.5;
// Each β_k must be in (0, π/2] for the weight to be positive and well-defined.
// For degenerate or very flat triangles some β may be ≤ 0 or ≥ π/2.
if (beta1 <= 0.0 || beta2 <= 0.0 || beta3 <= 0.0) return {0.0, 0.0, 0.0, false};
const double tb1 = std::tan(beta1);
const double tb2 = std::tan(beta2);
const double tb3 = std::tan(beta3);
if (std::abs(tb1) < 1e-15 || std::abs(tb2) < 1e-15 || std::abs(tb3) < 1e-15)
return {0.0, 0.0, 0.0, false};
// w_ij = cot(β_k) where β_k is for the edge opposite vk.
// w12 is for edge v1-v2 (opposite v3): cot(β3)
// w23 is for edge v2-v3 (opposite v1): cot(β1)
// w31 is for edge v3-v1 (opposite v2): cot(β2)
return {1.0 / tb3, 1.0 / tb1, 1.0 / tb2, true};
}
/// Analytical Spherical Hessian via `∂α/∂u` from the spherical law of
/// cosines + chain rule `∂l/∂u = tan(l/2)`; returns an n×n sparse
/// matrix with `n = spherical_dimension(mesh, m)`. See block comment
/// inside the body for the per-face derivation.
inline Eigen::SparseMatrix<double> spherical_hessian(
ConformalMesh& mesh,
const std::vector<double>& x,
const SphericalMaps& m)
{
const int n = spherical_dimension(mesh, m);
std::vector<Eigen::Triplet<double>> trips;
trips.reserve(static_cast<std::size_t>(n) * 9);
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.
double u1 = spher_dof_val(m.v_idx[v1], x);
double u2 = spher_dof_val(m.v_idx[v2], x);
double u3 = spher_dof_val(m.v_idx[v3], x);
double lam12 = m.lambda0[e12] + u1 + u2 + spher_dof_val(m.e_idx[e12], x);
double lam23 = m.lambda0[e23] + u2 + u3 + spher_dof_val(m.e_idx[e23], x);
double lam31 = m.lambda0[e31] + u3 + u1 + spher_dof_val(m.e_idx[e31], x);
const double l12 = spherical_l(lam12);
const double l23 = spherical_l(lam23);
const double l31 = spherical_l(lam31);
SphericalFaceAngles fa = spherical_angles(l12, l23, l31);
if (!fa.valid) continue;
const double sinl12 = std::sin(l12), cosl12 = std::cos(l12);
const double sinl23 = std::sin(l23), cosl23 = std::cos(l23);
const double sinl31 = std::sin(l31), cosl31 = std::cos(l31);
if (sinl12 < 1e-15 || sinl23 < 1e-15 || sinl31 < 1e-15) continue;
const double cot12 = cosl12 / sinl12;
const double cot23 = cosl23 / sinl23;
const double cot31 = cosl31 / sinl31;
// ∂l_ij/∂λ_ij = tan(l_ij/2)
const double t12 = std::tan(l12 * 0.5);
const double t23 = std::tan(l23 * 0.5);
const double t31 = std::tan(l31 * 0.5);
const double sinA1 = std::sin(fa.alpha1), cosA1 = std::cos(fa.alpha1);
const double sinA2 = std::sin(fa.alpha2), cosA2 = std::cos(fa.alpha2);
const double sinA3 = std::sin(fa.alpha3), cosA3 = std::cos(fa.alpha3);
if (sinA1 < 1e-15 || sinA2 < 1e-15 || sinA3 < 1e-15) continue;
// ∂α1/∂l_jk (α1 at v1; opposite l23, adjacent l12,l31)
const double dA1_dl12 = (cot12 * cosA1 - cot31) / sinA1;
const double dA1_dl31 = (cot31 * cosA1 - cot12) / sinA1;
const double dA1_dl23 = sinl23 / (sinl12 * sinl31 * sinA1);
// ∂α2/∂l_jk (α2 at v2; opposite l31, adjacent l12,l23)
const double dA2_dl12 = (cot12 * cosA2 - cot23) / sinA2;
const double dA2_dl23 = (cot23 * cosA2 - cot12) / sinA2;
const double dA2_dl31 = sinl31 / (sinl12 * sinl23 * sinA2);
// ∂α3/∂l_jk (α3 at v3; opposite l12, adjacent l23,l31)
const double dA3_dl23 = (cot23 * cosA3 - cot31) / sinA3;
const double dA3_dl31 = (cot31 * cosA3 - cot23) / sinA3;
const double dA3_dl12 = sinl12 / (sinl23 * sinl31 * sinA3);
// Chain rule: ∂α_i/∂u_j (u1 affects l12,l31; u2 affects l12,l23; u3 affects l23,l31)
const double dA1_du1 = dA1_dl12 * t12 + dA1_dl31 * t31;
const double dA1_du2 = dA1_dl12 * t12 + dA1_dl23 * t23;
const double dA1_du3 = dA1_dl23 * t23 + dA1_dl31 * t31;
const double dA2_du1 = dA2_dl12 * t12 + dA2_dl31 * t31;
const double dA2_du2 = dA2_dl12 * t12 + dA2_dl23 * t23;
const double dA2_du3 = dA2_dl23 * t23 + dA2_dl31 * t31;
const double dA3_du1 = dA3_dl12 * t12 + dA3_dl31 * t31;
const double dA3_du2 = dA3_dl12 * t12 + dA3_dl23 * t23;
const double dA3_du3 = dA3_dl23 * t23 + dA3_dl31 * t31;
const int i1 = m.v_idx[v1];
const int i2 = m.v_idx[v2];
const int i3 = m.v_idx[v3];
// H[vi, vj] -= ∂α_i/∂u_j (G_v = θ_v Σ α_v, so ∂G_i/∂u_j = α_i/∂u_j)
if (i1 >= 0) trips.emplace_back(i1, i1, -dA1_du1);
if (i2 >= 0) trips.emplace_back(i2, i2, -dA2_du2);
if (i3 >= 0) trips.emplace_back(i3, i3, -dA3_du3);
if (i1 >= 0 && i2 >= 0) {
trips.emplace_back(i1, i2, -dA1_du2);
trips.emplace_back(i2, i1, -dA2_du1);
}
if (i2 >= 0 && i3 >= 0) {
trips.emplace_back(i2, i3, -dA2_du3);
trips.emplace_back(i3, i2, -dA3_du2);
}
if (i3 >= 0 && i1 >= 0) {
trips.emplace_back(i3, i1, -dA3_du1);
trips.emplace_back(i1, i3, -dA1_du3);
}
}
Eigen::SparseMatrix<double> H(n, n);
H.setFromTriplets(trips.begin(), trips.end());
return H;
}
/// FD Hessian check for the Spherical functional. Compares analytic
/// `H` column-by-column to `(G(x+εeⱼ) G(xεeⱼ)) / (2ε)`.
inline bool hessian_check_spherical(
ConformalMesh& mesh,
const std::vector<double>& x0,
const SphericalMaps& m,
double eps = 1e-5,
double tol = 1e-4)
{
const int n = static_cast<int>(x0.size());
auto H = spherical_hessian(mesh, x0, m);
std::vector<double> xp = x0, xm = x0;
bool ok = true;
for (int j = 0; j < n; ++j) {
const std::size_t sj = static_cast<std::size_t>(j);
xp[sj] = x0[sj] + eps;
xm[sj] = x0[sj] - eps;
auto Gp = spherical_gradient(mesh, xp, m);
auto Gm = spherical_gradient(mesh, xm, m);
xp[sj] = xm[sj] = x0[sj];
for (int i = 0; i < n; ++i) {
double fd_ij = (Gp[static_cast<std::size_t>(i)]
- Gm[static_cast<std::size_t>(i)]) / (2.0 * eps);
double H_ij = H.coeff(i, j);
double err = std::abs(H_ij - fd_ij);
double scale = std::max(1.0, std::abs(H_ij));
if (err / scale > tol) ok = false;
}
}
return ok;
}
} // namespace conformallab