#pragma once // 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). struct SpherCotWeights { double w12, w23, w31; bool valid; }; 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 Hessian ──────────────────────────────────────────────────────── // // Returns the n×n sparse Hessian matrix H where n = spherical_dimension(mesh, m). // x – current DOF vector. // // Derivation: G_v = θ_v − Σ_f α_v^f → H[i,j] = −Σ_f ∂α_i^f/∂u_j // // For a face (v1,v2,v3) with arc-lengths l12,l23,l31 and angles α1,α2,α3, // differentiating the spherical law of cosines // cos(l_opp) = cos(l_a)cos(l_b) + sin(l_a)sin(l_b)cos(α) // gives: // ∂α1/∂l12 = [cot(l12)cos(α1) − cot(l31)] / sin(α1) (adjacent side) // ∂α1/∂l31 = [cot(l31)cos(α1) − cot(l12)] / sin(α1) (adjacent side) // ∂α1/∂l23 = sin(l23) / [sin(l12)sin(l31)sin(α1)] (opposite side) // // Chain rule with ∂l_ij/∂u_k = tan(l_ij/2) (from l = 2·asin(exp(λ/2))): // ∂α1/∂u1 = ∂α1/∂l12·t12 + ∂α1/∂l31·t31 // ∂α1/∂u2 = ∂α1/∂l12·t12 + ∂α1/∂l23·t23 // ∂α1/∂u3 = ∂α1/∂l23·t23 + ∂α1/∂l31·t31 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; } // ── Finite-difference Hessian check ────────────────────────────────────────── // // Compares the analytical Hessian column-by-column against // H_fd[:, j] = (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