feat(euclidean): full analytic edge-DOF (cyclic) Hessian; Tier-2 Wente finding
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>
This commit is contained in:
committed by
Tarik Moussa
parent
ea5f01d73d
commit
822a27da69
@@ -587,3 +587,61 @@ TEST(EuclideanFunctional, CyclicHessian_BlockFD_MatchesGradientFD_Tetrahedron)
|
||||
EXPECT_LT(max_err, 1e-5)
|
||||
<< "block-FD cyclic Hessian disagrees with the gradient finite difference";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Analytic cyclic Hessian == block-FD (and == gradient FD)
|
||||
//
|
||||
// `euclidean_hessian_analytic` is the closed-form counterpart of
|
||||
// `euclidean_hessian_block_fd`. On the full cyclic layout they must agree to
|
||||
// round-off, and both must match the gradient finite difference. This validates
|
||||
// the analytic derivation (∂α_i/∂s_i = ℓ_i²/4A, ∂α_i/∂s_j = ½cot α_i − ℓ_j²/4A).
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
TEST(EuclideanFunctional, CyclicHessian_Analytic_MatchesBlockFD_Tetrahedron)
|
||||
{
|
||||
auto mesh = make_tetrahedron();
|
||||
auto maps = setup_euclidean_maps(mesh);
|
||||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||||
const int n = assign_euclidean_all_dof_indices(mesh, maps); // 4 + 6 = 10
|
||||
|
||||
std::vector<double> x(static_cast<std::size_t>(n), 0.0);
|
||||
for (int i = 0; i < 4; ++i) x[static_cast<std::size_t>(i)] = -0.15;
|
||||
for (int i = 4; i < n; ++i) x[static_cast<std::size_t>(i)] = 0.05;
|
||||
|
||||
auto Ha = euclidean_hessian_analytic(mesh, x, maps);
|
||||
auto Hb = euclidean_hessian_block_fd(mesh, x, maps);
|
||||
|
||||
// Analytic vs block-FD.
|
||||
double max_ab = 0.0;
|
||||
for (int i = 0; i < n; ++i)
|
||||
for (int j = 0; j < n; ++j)
|
||||
max_ab = std::max(max_ab, std::abs(Ha.coeff(i, j) - Hb.coeff(i, j)));
|
||||
EXPECT_LT(max_ab, 1e-6)
|
||||
<< "analytic cyclic Hessian disagrees with block-FD";
|
||||
|
||||
// Analytic vs gradient FD (independent ground truth).
|
||||
const double eps = 1e-6;
|
||||
std::vector<double> xp = x, xm = x;
|
||||
double max_af = 0.0;
|
||||
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 = euclidean_gradient(mesh, xp, maps);
|
||||
auto Gm = euclidean_gradient(mesh, xm, maps);
|
||||
xp[sj] = xm[sj] = x[sj];
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const double fd = (Gp[static_cast<std::size_t>(i)]
|
||||
- Gm[static_cast<std::size_t>(i)]) / (2.0 * eps);
|
||||
max_af = std::max(max_af, std::abs(Ha.coeff(i, j) - fd));
|
||||
}
|
||||
}
|
||||
EXPECT_LT(max_af, 1e-5)
|
||||
<< "analytic cyclic Hessian disagrees with the gradient finite difference";
|
||||
|
||||
// Symmetry (Hessian of a scalar energy).
|
||||
double max_sym = 0.0;
|
||||
for (int i = 0; i < n; ++i)
|
||||
for (int j = 0; j < n; ++j)
|
||||
max_sym = std::max(max_sym, std::abs(Ha.coeff(i, j) - Ha.coeff(j, i)));
|
||||
EXPECT_LT(max_sym, 1e-9) << "analytic cyclic Hessian is not symmetric";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user