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>
212 lines
9.3 KiB
C++
212 lines
9.3 KiB
C++
#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 = l12−l23+l31, t31 = l12+l23−l31 │
|
||
// │ 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 Pinkall–Polthier. │
|
||
// │ │
|
||
// │ 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 (Pinkall–Polthier 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
|