Files
ConformalLabpp/code/include/euclidean_hessian.hpp
Tarik Moussa 194effba97 feat(phase3f+3g): analytical Hessians + PI consolidation
Phase 3g — constants.hpp:
  - Introduce conformallab::PI and TWO_PI in a single constants.hpp
  - Remove scattered local PI/pi definitions from hyper_ideal_geometry.hpp,
    hyper_ideal_utility.hpp, euclidean_functional.hpp, mesh_builder.hpp,
    spherical_geometry.hpp (backward-compatible PI_SPHER alias kept)

Phase 3f — Euclidean Hessian (euclidean_hessian.hpp):
  - Cotangent-Laplace operator (Pinkall–Polthier 1993)
  - euclidean_cot_weights() helper + euclidean_hessian() + hessian_check_euclidean()
  - Correct Pinkall–Polthier 1/2 normalization factor
  - 8 tests: cot weights, symmetry, null-space (H·1=0), PSD, FD × 4 meshes

Phase 3f — Spherical Hessian (spherical_hessian.hpp):
  - Derives ∂α_i/∂u_j directly from the spherical law of cosines:
      ∂α1/∂l_opp  = sin(l_opp) / [sin(l_a)·sin(l_b)·sin(α1)]
      ∂α1/∂l_adj  = [cot(l_adj)·cos(α1) − cot(l_other)] / sin(α1)
    then chains with ∂l/∂λ = tan(l/2)
  - spherical_cot_weights() kept as a standalone helper (tested separately)
  - 8 tests: cot weights, symmetry, correct null-space & sign-convention
    (H·1 ≠ 0; H is NSD at equilibrium), FD × 3 meshes

All 62 cgal tests pass (3 skipped as before).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-12 17:22:28 +02:00

212 lines
9.3 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_hessian.hpp
//
// Analytical Hessian of the Euclidean discrete conformal energy —
// the cotangent-Laplace operator.
//
// Ported from de.varylab.discreteconformal.functional.EuclideanCyclicFunctional
// (the hessian() method).
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │ Hessian formula (vertex DOFs only) │
// │ │
// │ For a face (v1, v2, v3) with effective log-lengths Λ̃ij and │
// │ side lengths lij = exp(Λ̃ij/2): │
// │ │
// │ t12 = l12+l23+l31, t23 = l12l23+l31, t31 = l12+l23l31 │
// │ denom2 = 2·sqrt(t12·t23·t31·l123) = 8·Area │
// │ │
// │ cot_k = (t_adj1·l123 t_adj2·t_opp) / denom2 │
// │ = cotangent of the angle αk at vertex k │
// │ │
// │ Hessian contributions per face: │
// │ H[vi, vi] += cot_vj + cot_vk (diagonal, both non-opp angles) │
// │ H[vi, vj] -= cot_vk (off-diagonal, for variable vi,vj│
// │ │
// │ This is exactly the cotangent-Laplace operator from PinkallPolthier. │
// │ │
// │ Pinned vertices (v_idx = 1) contribute to diagonal of neighbours but │
// │ do not create a column/row in H themselves. │
// └──────────────────────────────────────────────────────────────────────────┘
//
// Requires Eigen (header-only). The Hessian is returned as an
// Eigen::SparseMatrix<double> for direct use in the Phase-4 Newton solver
// (Eigen::SimplicialLDLT).
//
// The Hessian is symmetric positive semi-definite for any valid mesh with
// no degenerate faces. The null space is spanned by the uniform-shift
// vector 1 on closed surfaces (Euler characteristic = 0).
#include "euclidean_functional.hpp"
#include <Eigen/Sparse>
#include <vector>
#include <cmath>
namespace conformallab {
// ── Cotangent weight helper ───────────────────────────────────────────────────
//
// Given three Euclidean SIDE LENGTHS l12, l23, l31 (already exp(Λ̃/2)),
// return the three cotangent weights (cot1, cot2, cot3).
//
// cot_k = (t_adj·l123 t_opp·t_other) / (8·Area)
//
// Returns {0,0,0} for degenerate faces (triangle inequality violated or Area=0).
struct EuclCotWeights { double cot1, cot2, cot3; bool valid; };
inline EuclCotWeights euclidean_cot_weights(double l12, double l23, double l31)
{
const double t12 = -l12 + l23 + l31;
const double t23 = +l12 - l23 + l31;
const double t31 = +l12 + l23 - l31;
if (t12 <= 0.0 || t23 <= 0.0 || t31 <= 0.0)
return {0.0, 0.0, 0.0, false};
const double l123 = l12 + l23 + l31;
const double denom2_sq = t12 * t23 * t31 * l123;
if (denom2_sq <= 0.0) return {0.0, 0.0, 0.0, false};
// denom2 = 2·sqrt(t12·t23·t31·l123) = 8·Area
const double denom2 = 2.0 * std::sqrt(denom2_sq);
// cot at v1 (opposite l23): adjacent t-values are t12 and t31.
// cot at v2 (opposite l31): adjacent t-values are t12 and t23.
// cot at v3 (opposite l12): adjacent t-values are t23 and t31.
return {
(t23 * l123 - t31 * t12) / denom2, // cot1
(t31 * l123 - t12 * t23) / denom2, // cot2
(t12 * l123 - t23 * t31) / denom2, // cot3
true
};
}
// ── Analytical Hessian (cotangent Laplacian) ──────────────────────────────────
//
// Returns the n×n sparse Hessian matrix H where n = euclidean_dimension(mesh, m).
//
// Only vertex DOFs are supported. Edge DOFs (m.e_idx[e] >= 0) produce
// additional mixed-derivative entries that are not yet implemented; this
// function asserts they are absent.
//
// x current DOF vector (used to compute effective log-lengths Λ̃ij).
inline Eigen::SparseMatrix<double> euclidean_hessian(
ConformalMesh& mesh,
const std::vector<double>& x,
const EuclideanMaps& m)
{
const int n = euclidean_dimension(mesh, m);
// Collect triplets (row, col, value) — setFromTriplets sums duplicates.
std::vector<Eigen::Triplet<double>> trips;
trips.reserve(static_cast<std::size_t>(n) * 7); // rough estimate
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 = 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);
// Side lengths (centered to avoid overflow, same as euclidean_angles).
const double mu = (lam12 + lam23 + lam31) / 6.0;
const double l12 = std::exp((lam12 - 2.0 * mu) * 0.5);
const double l23 = std::exp((lam23 - 2.0 * mu) * 0.5);
const double l31 = std::exp((lam31 - 2.0 * mu) * 0.5);
auto [cot1, cot2, cot3, valid] = euclidean_cot_weights(l12, l23, l31);
if (!valid) continue;
const int i1 = m.v_idx[v1];
const int i2 = m.v_idx[v2];
const int i3 = m.v_idx[v3];
// ── Diagonal contributions ──────────────────────────────────────────
// H[v1,v1] += (cot2 + cot3)/2 (PinkallPolthier factor of 1/2)
// H[v2,v2] += (cot3 + cot1)/2
// H[v3,v3] += (cot1 + cot2)/2
if (i1 >= 0) trips.emplace_back(i1, i1, (cot2 + cot3) * 0.5);
if (i2 >= 0) trips.emplace_back(i2, i2, (cot3 + cot1) * 0.5);
if (i3 >= 0) trips.emplace_back(i3, i3, (cot1 + cot2) * 0.5);
// ── Off-diagonal contributions (only for variable pairs) ────────────
// Edge v1-v2 opposite α3: H[v1,v2] -= cot3/2
if (i1 >= 0 && i2 >= 0) {
trips.emplace_back(i1, i2, -cot3 * 0.5);
trips.emplace_back(i2, i1, -cot3 * 0.5);
}
// Edge v2-v3 opposite α1: H[v2,v3] -= cot1/2
if (i2 >= 0 && i3 >= 0) {
trips.emplace_back(i2, i3, -cot1 * 0.5);
trips.emplace_back(i3, i2, -cot1 * 0.5);
}
// Edge v3-v1 opposite α2: H[v3,v1] -= cot2/2
if (i3 >= 0 && i1 >= 0) {
trips.emplace_back(i3, i1, -cot2 * 0.5);
trips.emplace_back(i1, i3, -cot2 * 0.5);
}
}
Eigen::SparseMatrix<double> H(n, n);
H.setFromTriplets(trips.begin(), trips.end());
return H;
}
// ── Finite-difference Hessian check ──────────────────────────────────────────
//
// Compares the analytical Hessian column-by-column against
// H_fd[:, j] = (G(x + ε·eⱼ) G(x ε·eⱼ)) / (2ε).
//
// Returns true if max relative error < tol for every entry.
inline bool hessian_check_euclidean(
ConformalMesh& mesh,
const std::vector<double>& x0,
const EuclideanMaps& m,
double eps = 1e-5,
double tol = 1e-4)
{
const int n = static_cast<int>(x0.size());
auto H = euclidean_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 = euclidean_gradient(mesh, xp, m);
auto Gm = euclidean_gradient(mesh, xm, m);
xp[sj] = xm[sj] = x0[sj]; // restore
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