Upgrades the cyclic Euclidean Hessian from block-FD to closed-form, satisfying
the novelty-statement §3.2 "analytic Hessians, not finite difference" claim for
the Euclidean path.
- euclidean_hessian.hpp: `euclidean_hessian_analytic` — closed-form cyclic
Hessian from the law-of-cosines angle derivatives
∂α_i/∂s_i = ℓ_i²/4A, ∂α_i/∂s_j = ½cot α_i − ℓ_j²/4A (Σ_j = 0),
chained to (u, λ_e) and sign-mapped to the gradient outputs (−α vertex,
+α_opp edge). Reuses euclidean_cot_weights. Block-FD kept as cross-check.
- newton_solver.hpp: newton_euclidean cyclic path now uses the analytic Hessian.
- tests: CyclicHessian_Analytic_MatchesBlockFD_Tetrahedron — analytic == block-FD
(1e-6), == gradient FD (1e-5), symmetric (1e-9). Existing cyclic convergence
oracle still GREEN with the analytic Hessian routed in.
Tier-2 (Wente) finding: wente_torus02.obj is a QUAD mesh (1240 quads) and the
Java golden comes from cyclic (quad-net) uniformization; the C++ period-matrix
pipeline is triangle-based, so a faithful bit-vs-Java τ comparison needs a
quad/cyclic pipeline (Phase 9f). Deferred and documented; golden τ = ½+i√3/2.
244/244 cgal tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
457 lines
20 KiB
C++
457 lines
20 KiB
C++
#pragma once
|
||
// Copyright (c) 2024-2026 Tarik Moussa.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
// 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 <array>
|
||
#include <cmath>
|
||
#include <stdexcept>
|
||
|
||
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).
|
||
/// Three Euclidean cotangent weights `(cot1, cot2, cot3)` for the
|
||
/// vertices opposite to edges (l₂₃, l₃₁, l₁₂) of a triangle, plus a
|
||
/// `valid` flag that is `false` when the triangle is degenerate.
|
||
struct EuclCotWeights {
|
||
double cot1; ///< Cotangent at vertex 1 (opposite to l₂₃).
|
||
double cot2; ///< Cotangent at vertex 2 (opposite to l₃₁).
|
||
double cot3; ///< Cotangent at vertex 3 (opposite to l₁₂).
|
||
bool valid;///< `false` when the triangle is degenerate (triangle inequality violated or area = 0).
|
||
};
|
||
|
||
/// Compute the three Euclidean cotangent weights from edge lengths.
|
||
/// Returns `{0,0,0,false}` for degenerate triangles.
|
||
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 Euclidean Hessian (cotangent Laplacian), sparse.
|
||
/// Only vertex DOFs are supported — the function asserts that no edge
|
||
/// DOF is variable. `x` is used to compute effective log-lengths Λ̃ᵢⱼ.
|
||
inline Eigen::SparseMatrix<double> euclidean_hessian(
|
||
ConformalMesh& mesh,
|
||
const std::vector<double>& x,
|
||
const EuclideanMaps& m)
|
||
{
|
||
const int n = euclidean_dimension(mesh, m);
|
||
|
||
// Only the vertex block of the cyclic Hessian is implemented here. If any
|
||
// edge DOF is variable (assign_euclidean_all_dof_indices), the edge-edge and
|
||
// vertex-edge blocks present in the Java reference (conformalHessian) are
|
||
// missing, which would leave singular zero rows/cols. Fail loudly instead
|
||
// of silently returning a rank-deficient matrix (matches the doc contract).
|
||
for (auto e : mesh.edges()) {
|
||
if (m.e_idx[e] >= 0)
|
||
throw std::logic_error(
|
||
"euclidean_hessian: edge DOFs are not supported "
|
||
"(only the vertex-block cotangent Laplacian is implemented)");
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// ── Block-FD Hessian (cyclic: vertex + edge DOFs) ────────────────────────────
|
||
//
|
||
// The analytic `euclidean_hessian` above covers only the vertex-block cotangent
|
||
// Laplacian. The *cyclic* functional adds edge DOFs λ_e, giving vertex-edge and
|
||
// edge-edge Hessian blocks. We obtain the full Hessian by per-face block FD,
|
||
// mirroring `hyper_ideal_hessian_block_fd`.
|
||
//
|
||
// Locality lemma: the gradient decomposes by face,
|
||
// G_v = Θ_v − Σ_{f∋v} α_v(f) (vertex output = −α_v)
|
||
// G_e = Σ_{f∋e} α_opp(f) − φ_e (edge output = +α_opp)
|
||
// and α at face f depends ONLY on f's 6 local DOFs (u₁,u₂,u₃,λ₁₂,λ₂₃,λ₃₁), so
|
||
// ∂G_x/∂y = Σ_{f: x,y ∈ local(f)} ∂(output)/∂y at f.
|
||
// Accumulating per-face 6×6 blocks therefore reproduces the full Hessian and is
|
||
// consistent with `euclidean_gradient` by construction (it is its FD).
|
||
|
||
/// Per-face contributions to the cyclic gradient, in slot order
|
||
/// (v₁, v₂, v₃, e₁₂, e₂₃, e₃₁). Vertex slots carry −α (since G_v = Θ−Σα),
|
||
/// edge slots carry +α_opp (since G_e = Σα_opp − φ).
|
||
inline std::array<double, 6> eucl_face_local_outputs(
|
||
double u1, double u2, double u3,
|
||
double d12, double d23, double d31,
|
||
double lam0_12, double lam0_23, double lam0_31)
|
||
{
|
||
const double lam12 = lam0_12 + u1 + u2 + d12;
|
||
const double lam23 = lam0_23 + u2 + u3 + d23;
|
||
const double lam31 = lam0_31 + u3 + u1 + d31;
|
||
const auto fa = euclidean_angles(lam12, lam23, lam31);
|
||
// edge slot e12 ↔ opposite corner α3, e23 ↔ α1, e31 ↔ α2 (see gradient Pass 3).
|
||
return { -fa.alpha1, -fa.alpha2, -fa.alpha3,
|
||
+fa.alpha3, +fa.alpha1, +fa.alpha2 };
|
||
}
|
||
|
||
/// Block-FD Euclidean cyclic Hessian (vertex + edge DOFs), sparse.
|
||
/// Supports both vertex-only and cyclic DOF layouts; cost `F·12` face-angle
|
||
/// evaluations. Consistent with `euclidean_gradient` to `O(ε²)`.
|
||
inline Eigen::SparseMatrix<double> euclidean_hessian_block_fd(
|
||
ConformalMesh& mesh,
|
||
const std::vector<double>& x,
|
||
const EuclideanMaps& m,
|
||
double eps = 1e-5)
|
||
{
|
||
const int n = euclidean_dimension(mesh, m);
|
||
std::vector<Eigen::Triplet<double>> trips;
|
||
trips.reserve(static_cast<std::size_t>(36) * mesh.number_of_faces());
|
||
|
||
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);
|
||
|
||
const int idx[6] = {
|
||
m.v_idx[v1], m.v_idx[v2], m.v_idx[v3],
|
||
m.e_idx[e12], m.e_idx[e23], m.e_idx[e31]
|
||
};
|
||
const double lam0_12 = m.lambda0[e12];
|
||
const double lam0_23 = m.lambda0[e23];
|
||
const double lam0_31 = m.lambda0[e31];
|
||
|
||
const double vals[6] = {
|
||
eucl_dof_val(idx[0], x), eucl_dof_val(idx[1], x), eucl_dof_val(idx[2], x),
|
||
eucl_dof_val(idx[3], x), eucl_dof_val(idx[4], x), eucl_dof_val(idx[5], x)
|
||
};
|
||
|
||
for (int j = 0; j < 6; ++j) {
|
||
if (idx[j] < 0) continue; // pinned: no column
|
||
|
||
double vp[6], vm[6];
|
||
for (int k = 0; k < 6; ++k) { vp[k] = vm[k] = vals[k]; }
|
||
vp[j] += eps;
|
||
vm[j] -= eps;
|
||
|
||
auto Op = eucl_face_local_outputs(
|
||
vp[0], vp[1], vp[2], vp[3], vp[4], vp[5], lam0_12, lam0_23, lam0_31);
|
||
auto Om = eucl_face_local_outputs(
|
||
vm[0], vm[1], vm[2], vm[3], vm[4], vm[5], lam0_12, lam0_23, lam0_31);
|
||
|
||
for (int i = 0; i < 6; ++i) {
|
||
if (idx[i] < 0) continue;
|
||
const double val = (Op[static_cast<std::size_t>(i)]
|
||
- Om[static_cast<std::size_t>(i)]) / (2.0 * eps);
|
||
if (std::abs(val) > 1e-15)
|
||
trips.emplace_back(idx[i], idx[j], val);
|
||
}
|
||
}
|
||
}
|
||
|
||
Eigen::SparseMatrix<double> H(n, n);
|
||
H.setFromTriplets(trips.begin(), trips.end());
|
||
return H;
|
||
}
|
||
|
||
/// Symmetrised block-FD Euclidean cyclic Hessian: `(H + Hᵀ)/2`.
|
||
inline Eigen::SparseMatrix<double> euclidean_hessian_block_fd_sym(
|
||
ConformalMesh& mesh,
|
||
const std::vector<double>& x,
|
||
const EuclideanMaps& m,
|
||
double eps = 1e-5)
|
||
{
|
||
auto H = euclidean_hessian_block_fd(mesh, x, m, eps);
|
||
Eigen::SparseMatrix<double> Ht = H.transpose();
|
||
return (H + Ht) * 0.5;
|
||
}
|
||
|
||
// ── Analytic cyclic Hessian (vertex + edge DOFs) ─────────────────────────────
|
||
//
|
||
// Closed-form Jacobian of the cyclic gradient. With s_i = effective log-length
|
||
// (Λ̃) of the edge OPPOSITE vertex i (s = 2·ln ℓ), the Euclidean corner-angle
|
||
// derivatives are (derived from the law of cosines; Σ_j ∂α_i/∂s_j = 0):
|
||
//
|
||
// ∂α_i/∂s_i = ℓ_i² / (4A)
|
||
// ∂α_i/∂s_j = ½ cot α_i − ℓ_j² / (4A) (j ≠ i)
|
||
//
|
||
// where A is the triangle area (4A = denom2/2, denom2 = 8A from the cot helper).
|
||
// Chaining through s₁ = Λ̃(e₂₃), s₂ = Λ̃(e₃₁), s₃ = Λ̃(e₁₂) and
|
||
// Λ̃_e = λ⁰_e + u_i + u_j + λ_e gives the per-face 6×6 block over the local
|
||
// DOFs (u₁,u₂,u₃,λ₁₂,λ₂₃,λ₃₁); per-face outputs carry the gradient signs
|
||
// (−α vertex, +α_opp edge), so the assembled matrix equals ∂G/∂x exactly.
|
||
// This is the analytic counterpart of `euclidean_hessian_block_fd` (no FD).
|
||
|
||
/// Analytic Euclidean cyclic Hessian (vertex + edge DOFs), sparse.
|
||
/// Closed-form (no finite differences); matches `euclidean_hessian_block_fd`
|
||
/// to round-off and the analytic vertex-only Laplacian on vertex-only layouts.
|
||
inline Eigen::SparseMatrix<double> euclidean_hessian_analytic(
|
||
ConformalMesh& mesh,
|
||
const std::vector<double>& x,
|
||
const EuclideanMaps& m)
|
||
{
|
||
const int n = euclidean_dimension(mesh, m);
|
||
std::vector<Eigen::Triplet<double>> trips;
|
||
trips.reserve(static_cast<std::size_t>(36) * mesh.number_of_faces());
|
||
|
||
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);
|
||
|
||
const double u1 = eucl_dof_val(m.v_idx[v1], x);
|
||
const double u2 = eucl_dof_val(m.v_idx[v2], x);
|
||
const double u3 = eucl_dof_val(m.v_idx[v3], x);
|
||
|
||
const double lam12 = m.lambda0[e12] + u1 + u2 + eucl_dof_val(m.e_idx[e12], x); // s3
|
||
const double lam23 = m.lambda0[e23] + u2 + u3 + eucl_dof_val(m.e_idx[e23], x); // s1
|
||
const double lam31 = m.lambda0[e31] + u3 + u1 + eucl_dof_val(m.e_idx[e31], x); // s2
|
||
|
||
// Centered side lengths (scale-invariant for ℓ²/4A and cot).
|
||
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);
|
||
|
||
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) continue;
|
||
const double l123 = l12 + l23 + l31;
|
||
const double denom2_sq = t12 * t23 * t31 * l123;
|
||
if (denom2_sq <= 0.0) continue;
|
||
const double denom2 = 2.0 * std::sqrt(denom2_sq); // = 8A
|
||
const double fourA = denom2 * 0.5; // = 4A
|
||
|
||
auto cw = euclidean_cot_weights(l12, l23, l31);
|
||
if (!cw.valid) continue;
|
||
const double cot[3] = { cw.cot1, cw.cot2, cw.cot3 }; // at v1, v2, v3
|
||
|
||
// ℓ_i² = squared length of edge opposite vertex i:
|
||
// opp(v1)=e23=l23, opp(v2)=e31=l31, opp(v3)=e12=l12
|
||
const double Lsq[3] = { l23 * l23, l31 * l31, l12 * l12 };
|
||
|
||
// dA[i][j] = ∂α_i/∂s_j (i,j ∈ {0,1,2}; s_j opposite v_{j+1})
|
||
double dA[3][3];
|
||
for (int i = 0; i < 3; ++i)
|
||
for (int j = 0; j < 3; ++j)
|
||
dA[i][j] = (i == j) ? (Lsq[i] / fourA)
|
||
: (0.5 * cot[i] - Lsq[j] / fourA);
|
||
|
||
// ∂α_i/∂(local dof), dof order (u1,u2,u3, λ12,λ23,λ31).
|
||
// u1 ↔ s2,s3 ; u2 ↔ s1,s3 ; u3 ↔ s1,s2 ; λ12 ↔ s3 ; λ23 ↔ s1 ; λ31 ↔ s2.
|
||
double g[3][6];
|
||
for (int i = 0; i < 3; ++i) {
|
||
g[i][0] = dA[i][1] + dA[i][2]; // u1
|
||
g[i][1] = dA[i][0] + dA[i][2]; // u2
|
||
g[i][2] = dA[i][0] + dA[i][1]; // u3
|
||
g[i][3] = dA[i][2]; // λ12 (s3)
|
||
g[i][4] = dA[i][0]; // λ23 (s1)
|
||
g[i][5] = dA[i][1]; // λ31 (s2)
|
||
}
|
||
|
||
// Output slot → (angle index, sign): (−α1,−α2,−α3,+α3,+α1,+α2).
|
||
const int out_a[6] = { 0, 1, 2, 2, 0, 1 };
|
||
const double out_sg[6] = { -1.0, -1.0, -1.0, +1.0, +1.0, +1.0 };
|
||
|
||
const int idx[6] = {
|
||
m.v_idx[v1], m.v_idx[v2], m.v_idx[v3],
|
||
m.e_idx[e12], m.e_idx[e23], m.e_idx[e31]
|
||
};
|
||
|
||
for (int oi = 0; oi < 6; ++oi) {
|
||
if (idx[oi] < 0) continue;
|
||
for (int dj = 0; dj < 6; ++dj) {
|
||
if (idx[dj] < 0) continue;
|
||
const double val = out_sg[oi] * g[out_a[oi]][dj];
|
||
if (std::abs(val) > 1e-15)
|
||
trips.emplace_back(idx[oi], idx[dj], val);
|
||
}
|
||
}
|
||
}
|
||
|
||
Eigen::SparseMatrix<double> H(n, n);
|
||
H.setFromTriplets(trips.begin(), trips.end());
|
||
return H;
|
||
}
|
||
|
||
// ── Finite-difference Hessian check ──────────────────────────────────────────
|
||
/// FD Hessian check for the Euclidean functional. Compares analytic
|
||
/// `H` column-by-column to `(G(x+εeⱼ) − G(x−εeⱼ)) / (2ε)`; returns
|
||
/// `true` iff max relative error is below `tol`.
|
||
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
|