feat(euclidean): block-FD edge-DOF Hessian → cyclic Newton + Java convergence oracle
Implements the edge-DOF (cyclic) Euclidean Hessian, unblocking the full cyclic
Newton solve, and enables the Java EuclideanCyclicConvergenceTest cross-validation.
- euclidean_hessian.hpp: `euclidean_hessian_block_fd` / `_sym` — per-face 6×6
block FD over (u1,u2,u3,λ12,λ23,λ31), mirroring hyper_ideal_hessian_block_fd.
Per-face outputs carry the gradient signs (−α vertex, +α_opp edge), so the
result equals ∂G/∂x by construction (locality lemma). Analytic vertex-only
cotangent Hessian unchanged (still used for vertex-only layouts).
- newton_solver.hpp: newton_euclidean routes cyclic layouts (edge DOFs present)
through the block-FD Hessian; vertex-only path unchanged.
- tests:
* CyclicCircularEdge_CatHead_JavaXVal (now GREEN) — prescribe φ=π−0.1 on one
interior edge, solve, assert realised α_opp+α_opp = π−0.1 @1e-9.
* CyclicCircularEdge_PhiEntersGradient_CatHead — solver-free φ-wiring check.
* CyclicHessian_BlockFD_MatchesGradientFD_Tetrahedron — Hessian correctness.
243/243 cgal tests pass; vertex-only Euclidean Newton unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
Tarik Moussa
parent
5d74a94b78
commit
ea5f01d73d
@@ -43,6 +43,7 @@
|
||||
#include "euclidean_functional.hpp"
|
||||
#include <Eigen/Sparse>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -187,6 +188,116 @@ inline Eigen::SparseMatrix<double> euclidean_hessian(
|
||||
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;
|
||||
}
|
||||
|
||||
// ── 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
|
||||
|
||||
@@ -258,7 +258,14 @@ inline NewtonResult newton_euclidean(
|
||||
}
|
||||
|
||||
// ── Hessian + solve H·Δx = −G (SparseQR fallback for singular H) ──
|
||||
auto H = euclidean_hessian(mesh, x, m);
|
||||
// Cyclic layout (edge DOFs present) → block-FD Hessian, which covers the
|
||||
// vertex-edge / edge-edge blocks the analytic cotangent Laplacian omits.
|
||||
// Vertex-only layout → analytic cotangent Laplacian (cheaper, exact).
|
||||
bool has_edge_dof = false;
|
||||
for (auto e : mesh.edges())
|
||||
if (m.e_idx[e] >= 0) { has_edge_dof = true; break; }
|
||||
auto H = has_edge_dof ? euclidean_hessian_block_fd_sym(mesh, x, m)
|
||||
: euclidean_hessian(mesh, x, m);
|
||||
bool ok = false;
|
||||
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
|
||||
if (!ok) break;
|
||||
|
||||
Reference in New Issue
Block a user