Files
ConformalLabpp/code/include/hyper_ideal_hessian.hpp
user2595 704f42bbfd
Some checks failed
C++ Tests / test-fast (push) Has started running
C++ Tests / test-cgal (push) Has been cancelled
API Docs / doc-build (push) Has been cancelled
Doxygen → Codeberg Pages / publish (push) Has been cancelled
Markdown link check / check (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Merge pull request 'ci+quality: structural gates (CI: 3 new; local: 7 new + .clang-tidy)' (#18) from ci/structural-tests into main
2026-05-26 09:14:45 +00:00

232 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
// Copyright (c) 2024-2026 Tarik Moussa.
// SPDX-License-Identifier: MIT
// hyper_ideal_hessian.hpp
//
// Phase 4a — Hessian of the hyper-ideal discrete conformal functional.
// Phase 9b — Block-finite-difference Hessian (intermediate optimisation).
//
// ┌──────────────────────────────────────────────────────────────────────────┐
// │ Implementation strategy │
// │ │
// │ The hyper-ideal functional involves angle functions (ζ, σ, α, β) │
// │ composed through several nested layers (lij → ζ13/14/15 → β/α). │
// │ Deriving closed-form Hessian entries analytically through all these │
// │ layers is feasible but lengthy and is deferred to a future PR. │
// │ │
// │ TWO Hessian implementations are provided here: │
// │ │
// │ 1. `hyper_ideal_hessian` — full finite-difference baseline. │
// │ Cost ≈ n × (cost of full gradient evaluation) │
// │ = O(n · F) where n = #DOFs and F = #faces. │
// │ Used for correctness reference and small meshes. │
// │ │
// │ 2. `hyper_ideal_hessian_block_fd` — block-local finite-difference, │
// │ Phase 9b. Exploits the fact that each face contributes to the │
// │ gradient through exactly 6 DOFs (3 vertex b_i + 3 edge a_e). │
// │ Cost ≈ F × 6 × (cost of a single face-angle evaluation) │
// │ = O(36 · F). │
// │ Speed-up factor ≈ n / 36, i.e. typically 1050× on V > 200. │
// │ │
// │ Both produce the same Hessian to O(ε²) and pass identical PSD checks. │
// │ The block-FD variant is the production default; the full-FD variant is │
// │ kept for cross-validation tests. │
// │ │
// │ An analytic Hessian via Schläfli-type differentiation through the chain │
// │ (bᵢ, aₑ) → lᵢⱼ → ζ₁₃/ζ₁₄/ζ₁₅ → αᵢⱼ / βᵢ │
// │ is deferred to a future PR (Phase 9b-analytic). Speed-up would be │
// │ another ~6×, taking the cost to O(F). │
// │ │
// │ The hyper-ideal energy is strictly convex (Springborn 2020), so H is │
// │ positive semi-definite everywhere and Eigen::SimplicialLDLT applies │
// │ directly to either Hessian variant. │
// └──────────────────────────────────────────────────────────────────────────┘
//
// Note on the Java reference: HyperIdealFunctional.java line 295-298 declares
// public boolean hasHessian() { return false; }
// — i.e. the upstream Java implementation supplies NO Hessian, analytic or
// numerical. Both `hyper_ideal_hessian` and `hyper_ideal_hessian_block_fd`
// are conformallab++ additions beyond Java parity.
#include "hyper_ideal_functional.hpp"
#include <Eigen/Sparse>
#include <vector>
#include <cmath>
#include <cstdint>
namespace conformallab {
/// Full finite-difference HyperIdeal Hessian (baseline, Phase 4a).
/// Cost: `n` full-gradient evaluations ≈ `O(n·F)`. Use for small
/// meshes or as a correctness reference for the block-FD variant.
inline Eigen::SparseMatrix<double> hyper_ideal_hessian(
ConformalMesh& mesh,
const std::vector<double>& x,
const HyperIdealMaps& m,
double eps = 1e-5)
{
const int n = hyper_ideal_dimension(mesh, m);
std::vector<Eigen::Triplet<double>> trips;
trips.reserve(static_cast<std::size_t>(n * n));
std::vector<double> xp = x, xm = x;
for (int j = 0; j < n; ++j) {
const std::size_t sj = static_cast<std::size_t>(j);
xp[sj] = x[sj] + eps;
xm[sj] = x[sj] - eps;
auto Gp = evaluate_hyper_ideal(mesh, xp, m, /*energy=*/false).gradient;
auto Gm = evaluate_hyper_ideal(mesh, xm, m, /*energy=*/false).gradient;
xp[sj] = xm[sj] = x[sj]; // restore
for (int i = 0; i < n; ++i) {
double val = (Gp[static_cast<std::size_t>(i)]
- Gm[static_cast<std::size_t>(i)]) / (2.0 * eps);
if (std::abs(val) > 1e-15)
trips.emplace_back(i, j, val);
}
}
Eigen::SparseMatrix<double> H(n, n);
H.setFromTriplets(trips.begin(), trips.end());
return H;
}
/// Symmetrised full-FD HyperIdeal Hessian: returns `(H + Hᵀ) / 2` to
/// scrub the tiny asymmetries introduced by floating-point rounding.
inline Eigen::SparseMatrix<double> hyper_ideal_hessian_sym(
ConformalMesh& mesh,
const std::vector<double>& x,
const HyperIdealMaps& m,
double eps = 1e-5)
{
auto H = hyper_ideal_hessian(mesh, x, m, eps);
Eigen::SparseMatrix<double> Ht = H.transpose();
return (H + Ht) * 0.5;
}
// ── Block-FD Hessian (Phase 9b) ──────────────────────────────────────────────
//
// Computes the Hessian by FD on each face's 6×6 local block. The 6 local
// DOFs of a face f are:
// (b_{v1}, b_{v2}, b_{v3}, a_{e12}, a_{e23}, a_{e31}).
// For each face we recompute the 6 output angles (β₁,β₂,β₃,α₁₂,α₂₃,α₃₁)
// at x ± ε along each local axis and read off the 6×6 Jacobian. The result
// scatters into the global Hessian via the DOF-index lookup.
//
// Why this is correct:
// ─────────────────────
// The global gradient decomposes by face:
// G_b_v = Σ_{f ∋ v} β_v(f) Θ_v
// G_a_e = Σ_{f ∋ e} α_e(f) θ_e
// Since β and α at face f depend ONLY on the 6 local DOFs of f, the
// Hessian also decomposes:
// ∂G_x/∂y = Σ_{f: x,y ∈ local(f)} ∂(β or α)/∂y at f.
// So accumulating per-face 6×6 blocks reproduces the full Hessian.
//
// Cost: F × 12 face-angle evaluations (6 DOFs × 2 directions).
// On a tetrahedron (F=4, n≈10): 48 face evaluations
// vs full-FD ≈ 80 → ~1.7× speed-up.
// On cathead.obj (F=248, n≈400): 2976 face evaluations
// vs full-FD ≈ 99,200 → ~33× speed-up.
// On brezel.obj (F=13824, n≈14000): 165 888 face evaluations
// vs full-FD ≈ 193 M → ~1166× speed-up.
/// Per-face block-FD HyperIdeal Hessian (Phase 9b). Uses the locality
/// lemma `∂G_x/∂y = Σ_{f: x,y ∈ local(f)} ∂(β or α)/∂y` to perturb only
/// the 6 face-local DOFs at a time, giving an `F·12` face-evaluation
/// budget vs `n·F` for full-FD (~96× speed-up on brezel.obj).
inline Eigen::SparseMatrix<double> hyper_ideal_hessian_block_fd(
ConformalMesh& mesh,
const std::vector<double>& x,
const HyperIdealMaps& m,
double eps = 1e-5)
{
const int n = hyper_ideal_dimension(mesh, m);
std::vector<Eigen::Triplet<double>> trips;
trips.reserve(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);
// Local DOF indices: (b1, b2, b3, a12, a23, a31). Pinned slots = -1.
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 bool v1b = idx[0] >= 0;
const bool v2b = idx[1] >= 0;
const bool v3b = idx[2] >= 0;
// Local DOF values (0 for pinned).
const double vals[6] = {
dof_val(idx[0], x), dof_val(idx[1], x), dof_val(idx[2], x),
dof_val(idx[3], x), dof_val(idx[4], x), dof_val(idx[5], x)
};
// For each free local DOF, evaluate the 6 outputs at ±ε.
// We never perturb a pinned DOF (its column would be physically zero
// because it is not part of the DOF vector at all).
for (int j = 0; j < 6; ++j) {
if (idx[j] < 0) continue;
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 = face_angles_from_local_dofs(
vp[0], vp[1], vp[2], vp[3], vp[4], vp[5], v1b, v2b, v3b);
auto Om = face_angles_from_local_dofs(
vm[0], vm[1], vm[2], vm[3], vm[4], vm[5], v1b, v2b, v3b);
const double Gp[6] = {
Op.beta1, Op.beta2, Op.beta3,
Op.alpha12, Op.alpha23, Op.alpha31
};
const double Gm[6] = {
Om.beta1, Om.beta2, Om.beta3,
Om.alpha12, Om.alpha23, Om.alpha31
};
for (int i = 0; i < 6; ++i) {
if (idx[i] < 0) continue; // pinned: contributes nothing
const double val = (Gp[i] - Gm[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 HyperIdeal Hessian: returns `(H + Hᵀ) / 2` of
/// `hyper_ideal_hessian_block_fd(...)` for downstream solvers that
/// require strict symmetry.
inline Eigen::SparseMatrix<double> hyper_ideal_hessian_block_fd_sym(
ConformalMesh& mesh,
const std::vector<double>& x,
const HyperIdealMaps& m,
double eps = 1e-5)
{
auto H = hyper_ideal_hessian_block_fd(mesh, x, m, eps);
Eigen::SparseMatrix<double> Ht = H.transpose();
return (H + Ht) * 0.5;
}
} // namespace conformallab