feat(phase3b): port HyperIdealFunctional energy + gradient onto ConformalMesh
Implements the hyper-ideal discrete conformal map functional on
CGAL::Surface_mesh. The energy and analytic gradient are ported directly
from HyperIdealFunctional.java; correctness is verified via a
finite-difference gradient check (same eps=1E-5 / tol=1E-4 as Java).
New files:
include/hyper_ideal_geometry.hpp — ζ, ζ₁₃, ζ₁₄, ζ₁₅, lij, αij, σi, σij
include/hyper_ideal_functional.hpp — HyperIdealMaps, evaluate_hyper_ideal,
gradient_check
tests/cgal/test_hyper_ideal_functional.cpp — 6 tests (1 skipped @Ignore)
Test results (local, -DWITH_CGAL=ON):
conformallab_cgal_tests: 21 registered | 20 passed | 1 skipped | 0 failed
- GradientCheck_AllHyperIdealTriangle ✓
- GradientCheck_ExtendedDomain ✓
- GradientCheck_TetrahedronAllVariable ✓
- EnergyFiniteAtTestPoint ✓
- GradientCheck_MixedIdealHyperIdeal ✓
- GradientCheck_Fan6AllVariable ✓
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
138
code/include/hyper_ideal_geometry.hpp
Normal file
138
code/include/hyper_ideal_geometry.hpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
// hyper_ideal_geometry.hpp
|
||||
//
|
||||
// Pure-math building blocks for the hyper-ideal discrete conformal map.
|
||||
// Ported from de.varylab.discreteconformal.functional.HyperIdealUtility
|
||||
// and the private helpers of HyperIdealFunctional (lij, αij, σi, σij).
|
||||
//
|
||||
// All functions are independent of the mesh type.
|
||||
//
|
||||
// Notation follows the original Java / paper:
|
||||
// b_i, b_j – vertex variables (log scale factors, hyper-ideal vertices)
|
||||
// a_ij – edge variable (intersection angle between horocycles)
|
||||
// l_ij – effective hyperbolic edge length in the auxiliary triangle
|
||||
// β_i – interior angle of the hyperbolic triangle at vertex i
|
||||
// α_ij – dihedral angle of the tetrahedron at edge ij
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace conformallab {
|
||||
|
||||
constexpr double PI = 3.14159265358979323846264338328;
|
||||
|
||||
// ── Length functions ─────────────────────────────────────────────────────────
|
||||
|
||||
// ζ(x,y,z) — interior angle in a hyperbolic triangle with edge lengths
|
||||
// x, y, z, opposite to the side of length z.
|
||||
// Ports HyperIdealUtility.ζ(x, y, z).
|
||||
inline double zeta(double x, double y, double z)
|
||||
{
|
||||
double cx = std::cosh(x), cy = std::cosh(y), cz = std::cosh(z);
|
||||
double sx = std::sinh(x), sy = std::sinh(y);
|
||||
double nbd = (cx*cy - cz) / (sx*sy);
|
||||
nbd = std::clamp(nbd, -1.0, 1.0); // guard floating-point rounding
|
||||
return std::acos(nbd);
|
||||
}
|
||||
|
||||
// ζ₁₃(x,y,z) — third edge length in a right-angled hyperbolic hexagon.
|
||||
// Ports HyperIdealUtility.ζ_13(x, y, z).
|
||||
inline double zeta13(double x, double y, double z)
|
||||
{
|
||||
double cx = std::cosh(x), cy = std::cosh(y), cz = std::cosh(z);
|
||||
double sx = std::sinh(x), sy = std::sinh(y);
|
||||
return std::acosh((cx*cy + cz) / (sx*sy));
|
||||
}
|
||||
|
||||
// ζ₁₄(x,y) — edge length in a hyperbolic pentagon with one ideal vertex.
|
||||
// Ports HyperIdealUtility.ζ_14(x, y).
|
||||
inline double zeta14(double x, double y)
|
||||
{
|
||||
double cy = std::cosh(y), sy = std::sinh(y);
|
||||
return std::acosh((std::exp(x) + cy) / sy);
|
||||
}
|
||||
|
||||
// ζ₁₅(x) — length in a hyperbolic quadrilateral with two ideal vertices.
|
||||
// Ports HyperIdealUtility.ζ_15(x).
|
||||
inline double zeta15(double x)
|
||||
{
|
||||
return 2.0 * std::asinh(std::exp(x / 2.0));
|
||||
}
|
||||
|
||||
// ── Effective edge length ─────────────────────────────────────────────────────
|
||||
|
||||
// l_ij: effective hyperbolic length of edge ij.
|
||||
// b_i, b_j – vertex log scale factors (used only if vertex is hyper-ideal)
|
||||
// a_ij – edge intersection-angle variable
|
||||
// vi_var – true if vertex i is hyper-ideal (has a DOF b_i)
|
||||
// vj_var – true if vertex j is hyper-ideal
|
||||
// Ports HyperIdealFunctional.lij().
|
||||
inline double lij(double bi, double bj, double aij, bool vi_var, bool vj_var)
|
||||
{
|
||||
if (vi_var && vj_var) return zeta13(bi, bj, aij);
|
||||
if (vi_var) return zeta14(aij, bi);
|
||||
if (vj_var) return zeta14(aij, bj);
|
||||
return zeta15(aij);
|
||||
}
|
||||
|
||||
// ── Auxiliary angle functions ─────────────────────────────────────────────────
|
||||
|
||||
// σᵢ(aᵢⱼ, aₖᵢ, aⱼₖ, vj_var, vk_var) — intermediate half-length at vertex i.
|
||||
// Ports HyperIdealFunctional.σi().
|
||||
inline double sigma_i(double aij, double aki, double ajk, bool vj_var, bool vk_var)
|
||||
{
|
||||
if (vj_var && vk_var) return zeta13(aij, aki, ajk);
|
||||
if (vj_var) return zeta14(ajk - aki, aij);
|
||||
if (vk_var) return zeta14(ajk - aij, aki);
|
||||
return zeta15(ajk - aij - aki);
|
||||
}
|
||||
|
||||
// σᵢⱼ(aᵢⱼ, bᵢ, bⱼ, vj_var) — intermediate half-length for edge ij from vertex i.
|
||||
// Ports HyperIdealFunctional.σij().
|
||||
inline double sigma_ij(double aij, double bi, double bj, bool vj_var)
|
||||
{
|
||||
if (vj_var) return zeta13(aij, bi, bj);
|
||||
return zeta14(-aij, bi);
|
||||
}
|
||||
|
||||
// α_ij: computed dihedral angle at edge ij in the face with vertices i, j, k.
|
||||
//
|
||||
// Arguments (cyclic role assignment):
|
||||
// aij, ajk, aki – edge variables
|
||||
// bi, bj, bk – vertex variables
|
||||
// βi, βj, βk – interior angles of the auxiliary hyperbolic triangle
|
||||
// vi_var, vj_var, vk_var – which vertices are hyper-ideal
|
||||
//
|
||||
// Ports HyperIdealFunctional.αij() (the private helper).
|
||||
// Note: the vk_var case recurses once (never more than one level deep).
|
||||
inline double alpha_ij(
|
||||
double aij, double ajk, double aki,
|
||||
double bi, double bj, double bk,
|
||||
double beta_i, double beta_j, double beta_k,
|
||||
bool vi_var, bool vj_var, bool vk_var)
|
||||
{
|
||||
if (vi_var) {
|
||||
double si = sigma_i (aij, aki, ajk, vj_var, vk_var);
|
||||
double sij = sigma_ij(aij, bi, bj, vj_var);
|
||||
double sik = sigma_ij(aki, bi, bk, vk_var);
|
||||
return zeta(si, sij, sik);
|
||||
}
|
||||
if (vj_var) {
|
||||
double sj = sigma_i (ajk, aij, aki, vk_var, vi_var);
|
||||
double sjk = sigma_ij(ajk, bj, bk, vk_var);
|
||||
double sji = sigma_ij(aij, bj, bi, vi_var);
|
||||
return zeta(sj, sji, sjk);
|
||||
}
|
||||
if (vk_var) {
|
||||
// Derive α_ij from α_jk (one level of recursion).
|
||||
double a_jk = alpha_ij(ajk, aki, aij,
|
||||
bj, bk, bi,
|
||||
beta_j, beta_k, beta_i,
|
||||
vj_var, vk_var, vi_var);
|
||||
return PI - a_jk - beta_j;
|
||||
}
|
||||
// All ideal: closed-form formula.
|
||||
return 0.5 * (PI + beta_k - beta_i - beta_j);
|
||||
}
|
||||
|
||||
} // namespace conformallab
|
||||
Reference in New Issue
Block a user