#pragma once // Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // spherical_hessian.hpp // // Analytical Hessian of the spherical discrete conformal energy — // the spherical cotangent-Laplace operator. // // Ported from de.varylab.discreteconformal.functional.SphericalFunctional // (the hessian() method, vertex DOFs). // // ┌──────────────────────────────────────────────────────────────────────────┐ // │ Hessian formula (vertex DOFs only) │ // │ │ // │ For a spherical face (v1, v2, v3) with vertex angles α1, α2, α3: │ // │ │ // │ Spherical cotangent weight for edge (vi, vj) with opposite vk: │ // │ β_k = (π − αi − αj + αk) / 2 │ // │ w_k = cot(β_k) = 1/tan(β_k) │ // │ │ // │ Euclidean limit: α1+α2+α3 → π, β_k → αk, w_k → cot(αk). ✓ │ // │ │ // │ Hessian contributions per face: │ // │ H[vi, vi] += w_ij + w_ik (diagonal: weights of incident edges) │ // │ H[vi, vj] -= w_ij (off-diagonal: weight of edge ij) │ // │ │ // │ where w_ij is the weight of the edge between vi and vj (opposite vk): │ // │ w_ij = cot(β_k) with β_k = (π − αi − αj + αk) / 2. │ // └──────────────────────────────────────────────────────────────────────────┘ // // Requires Eigen (header-only). Returns Eigen::SparseMatrix. #include "spherical_functional.hpp" #include #include #include namespace conformallab { // ── Spherical cotangent weight helper ──────────────────────────────────────── // // Given the three face angles α1, α2, α3 of a spherical triangle, return the // three edge cotangent weights w1 (edge opp v1), w2 (edge opp v2), w3 (edge opp v3). // // w_k = cot(β_k) where β_k = (π − α_adj1 − α_adj2 + α_opp) / 2 // = (π − αi − αj + αk) / 2 for edge (vi,vj), opposite vk // // Mapping in our CGAL halfedge convention: // h0 = halfedge(f): edge v1-v2 → opposite v3 → w = cot(β3), β3=(π-α1-α2+α3)/2 // h1: edge v2-v3 → opposite v1 → w = cot(β1), β1=(π-α2-α3+α1)/2 // h2: edge v3-v1 → opposite v2 → w = cot(β2), β2=(π-α3-α1+α2)/2 // // Returns valid=false if any β_k is out of range (degenerate face). /// Three spherical "cotangent" weights for the three edges of a face, /// derived from the per-vertex interior angles `α₁, α₂, α₃` via /// `w_ij = cot(β_k)` with `β_k = (π − α_i − α_j + α_k) / 2`. struct SpherCotWeights { double w12; ///< Weight for edge v₁-v₂ (opposite vertex v₃). double w23; ///< Weight for edge v₂-v₃ (opposite vertex v₁). double w31; ///< Weight for edge v₃-v₁ (opposite vertex v₂). bool valid; ///< `false` when any β_k is out of `(0, π/2]` (degenerate face). }; /// Compute the three spherical cot weights from the three interior /// angles `(α₁, α₂, α₃)` of a spherical triangle. See `SpherCotWeights`. inline SpherCotWeights spherical_cot_weights(double alpha1, double alpha2, double alpha3) { // β for each edge: // β3 = (π - α1 - α2 + α3)/2 — weight for edge v1-v2 (opposite v3) // β1 = (π - α2 - α3 + α1)/2 — weight for edge v2-v3 (opposite v1) // β2 = (π - α3 - α1 + α2)/2 — weight for edge v3-v1 (opposite v2) const double beta3 = (PI - alpha1 - alpha2 + alpha3) * 0.5; const double beta1 = (PI - alpha2 - alpha3 + alpha1) * 0.5; const double beta2 = (PI - alpha3 - alpha1 + alpha2) * 0.5; // Each β_k must be in (0, π/2] for the weight to be positive and well-defined. // For degenerate or very flat triangles some β may be ≤ 0 or ≥ π/2. if (beta1 <= 0.0 || beta2 <= 0.0 || beta3 <= 0.0) return {0.0, 0.0, 0.0, false}; const double tb1 = std::tan(beta1); const double tb2 = std::tan(beta2); const double tb3 = std::tan(beta3); if (std::abs(tb1) < 1e-15 || std::abs(tb2) < 1e-15 || std::abs(tb3) < 1e-15) return {0.0, 0.0, 0.0, false}; // w_ij = cot(β_k) where β_k is for the edge opposite vk. // w12 is for edge v1-v2 (opposite v3): cot(β3) // w23 is for edge v2-v3 (opposite v1): cot(β1) // w31 is for edge v3-v1 (opposite v2): cot(β2) return {1.0 / tb3, 1.0 / tb1, 1.0 / tb2, true}; } /// Analytical Spherical Hessian via `∂α/∂u` from the spherical law of /// cosines + chain rule `∂l/∂u = tan(l/2)`; returns an n×n sparse /// matrix with `n = spherical_dimension(mesh, m)`. See block comment /// inside the body for the per-face derivation. inline Eigen::SparseMatrix spherical_hessian( ConformalMesh& mesh, const std::vector& x, const SphericalMaps& m) { const int n = spherical_dimension(mesh, m); std::vector> trips; trips.reserve(static_cast(n) * 9); 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 = spher_dof_val(m.v_idx[v1], x); double u2 = spher_dof_val(m.v_idx[v2], x); double u3 = spher_dof_val(m.v_idx[v3], x); double lam12 = m.lambda0[e12] + u1 + u2 + spher_dof_val(m.e_idx[e12], x); double lam23 = m.lambda0[e23] + u2 + u3 + spher_dof_val(m.e_idx[e23], x); double lam31 = m.lambda0[e31] + u3 + u1 + spher_dof_val(m.e_idx[e31], x); const double l12 = spherical_l(lam12); const double l23 = spherical_l(lam23); const double l31 = spherical_l(lam31); SphericalFaceAngles fa = spherical_angles(l12, l23, l31); if (!fa.valid) continue; const double sinl12 = std::sin(l12), cosl12 = std::cos(l12); const double sinl23 = std::sin(l23), cosl23 = std::cos(l23); const double sinl31 = std::sin(l31), cosl31 = std::cos(l31); if (sinl12 < 1e-15 || sinl23 < 1e-15 || sinl31 < 1e-15) continue; const double cot12 = cosl12 / sinl12; const double cot23 = cosl23 / sinl23; const double cot31 = cosl31 / sinl31; // ∂l_ij/∂λ_ij = tan(l_ij/2) const double t12 = std::tan(l12 * 0.5); const double t23 = std::tan(l23 * 0.5); const double t31 = std::tan(l31 * 0.5); const double sinA1 = std::sin(fa.alpha1), cosA1 = std::cos(fa.alpha1); const double sinA2 = std::sin(fa.alpha2), cosA2 = std::cos(fa.alpha2); const double sinA3 = std::sin(fa.alpha3), cosA3 = std::cos(fa.alpha3); if (sinA1 < 1e-15 || sinA2 < 1e-15 || sinA3 < 1e-15) continue; // ∂α1/∂l_jk (α1 at v1; opposite l23, adjacent l12,l31) const double dA1_dl12 = (cot12 * cosA1 - cot31) / sinA1; const double dA1_dl31 = (cot31 * cosA1 - cot12) / sinA1; const double dA1_dl23 = sinl23 / (sinl12 * sinl31 * sinA1); // ∂α2/∂l_jk (α2 at v2; opposite l31, adjacent l12,l23) const double dA2_dl12 = (cot12 * cosA2 - cot23) / sinA2; const double dA2_dl23 = (cot23 * cosA2 - cot12) / sinA2; const double dA2_dl31 = sinl31 / (sinl12 * sinl23 * sinA2); // ∂α3/∂l_jk (α3 at v3; opposite l12, adjacent l23,l31) const double dA3_dl23 = (cot23 * cosA3 - cot31) / sinA3; const double dA3_dl31 = (cot31 * cosA3 - cot23) / sinA3; const double dA3_dl12 = sinl12 / (sinl23 * sinl31 * sinA3); // Chain rule: ∂α_i/∂u_j (u1 affects l12,l31; u2 affects l12,l23; u3 affects l23,l31) const double dA1_du1 = dA1_dl12 * t12 + dA1_dl31 * t31; const double dA1_du2 = dA1_dl12 * t12 + dA1_dl23 * t23; const double dA1_du3 = dA1_dl23 * t23 + dA1_dl31 * t31; const double dA2_du1 = dA2_dl12 * t12 + dA2_dl31 * t31; const double dA2_du2 = dA2_dl12 * t12 + dA2_dl23 * t23; const double dA2_du3 = dA2_dl23 * t23 + dA2_dl31 * t31; const double dA3_du1 = dA3_dl12 * t12 + dA3_dl31 * t31; const double dA3_du2 = dA3_dl12 * t12 + dA3_dl23 * t23; const double dA3_du3 = dA3_dl23 * t23 + dA3_dl31 * t31; const int i1 = m.v_idx[v1]; const int i2 = m.v_idx[v2]; const int i3 = m.v_idx[v3]; // H[vi, vj] -= ∂α_i/∂u_j (G_v = θ_v − Σ α_v, so ∂G_i/∂u_j = −∂α_i/∂u_j) if (i1 >= 0) trips.emplace_back(i1, i1, -dA1_du1); if (i2 >= 0) trips.emplace_back(i2, i2, -dA2_du2); if (i3 >= 0) trips.emplace_back(i3, i3, -dA3_du3); if (i1 >= 0 && i2 >= 0) { trips.emplace_back(i1, i2, -dA1_du2); trips.emplace_back(i2, i1, -dA2_du1); } if (i2 >= 0 && i3 >= 0) { trips.emplace_back(i2, i3, -dA2_du3); trips.emplace_back(i3, i2, -dA3_du2); } if (i3 >= 0 && i1 >= 0) { trips.emplace_back(i3, i1, -dA3_du1); trips.emplace_back(i1, i3, -dA1_du3); } } Eigen::SparseMatrix H(n, n); H.setFromTriplets(trips.begin(), trips.end()); return H; } /// FD Hessian check for the Spherical functional. Compares analytic /// `H` column-by-column to `(G(x+εeⱼ) − G(x−εeⱼ)) / (2ε)`. inline bool hessian_check_spherical( ConformalMesh& mesh, const std::vector& x0, const SphericalMaps& m, double eps = 1e-5, double tol = 1e-4) { const int n = static_cast(x0.size()); auto H = spherical_hessian(mesh, x0, m); std::vector xp = x0, xm = x0; bool ok = true; for (int j = 0; j < n; ++j) { const std::size_t sj = static_cast(j); xp[sj] = x0[sj] + eps; xm[sj] = x0[sj] - eps; auto Gp = spherical_gradient(mesh, xp, m); auto Gm = spherical_gradient(mesh, xm, m); xp[sj] = xm[sj] = x0[sj]; for (int i = 0; i < n; ++i) { double fd_ij = (Gp[static_cast(i)] - Gm[static_cast(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