Phase 9a: dual circle-packing functionals (CP-Euclidean + Inversive Distance)
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m48s
C++ Tests / test-fast (push) Successful in 2m50s
API Docs / doc-build (pull_request) Successful in 34s
C++ Tests / test-cgal (pull_request) Failing after 10m52s
C++ Tests / test-cgal (push) Has been skipped
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m48s
C++ Tests / test-fast (push) Successful in 2m50s
API Docs / doc-build (pull_request) Successful in 34s
C++ Tests / test-cgal (pull_request) Failing after 10m52s
C++ Tests / test-cgal (push) Has been skipped
Implements both Phase 9a sub-functionals — the face-dual circle-packing
functional from the Java original and the vertex-based inversive-distance
functional from Luo 2004 / Glickenstein 2011 — together with a side-by-side
mathematical validation report.
CGAL test count: 194 → 205 (+11 from 9a.2, +10 from 9a.1, was already
+1 from 9a.1's setup defaults regression).
Phase 9a.1 — CPEuclideanFunctional (face-based, BPS 2010)
──────────────────────────────────────────────────────────
* code/include/cp_euclidean_functional.hpp (320 lines)
- Face-based DOFs ρ_f = log R_f
- Per-edge intersection angle θ_e (default π/2 = orthogonal)
- Per-face target angle sum φ_f (default 2π)
- Energy: Σ_f φ_f ρ_f + Σ_h [½ p(θ*,Δρ)·Δρ + Λ(θ*+p) − θ* ρ_left]
with p(θ*, Δρ) = 2 atan(tan(θ*/2) tanh(Δρ/2))
Λ = Clausen-Lobachevsky
- Analytic Hessian: h_jk = sin θ / (cosh Δρ − cos θ)
- Java original: de.varylab.discreteconformal.functional.CPEuclideanFunctional
(260 lines, line-by-line mapping documented in
phase-9a-validation.md §1)
* code/tests/cgal/test_cp_euclidean_functional.cpp (10 tests)
- PFunctionKnownValues, SetupDefaults, AssignDofIndices_PinsOneFace
- TangentialLimitGradientEqualsPhi (closed-form θ=0 check)
- FDGradientCheck on closed and open tetrahedron, random ρ seed=1
- FDHessianCheck on closed and open tetrahedron, random ρ seed=1
- HessianIsPSD (BPS 2010 §6 convexity)
- NaturalPhiMakesZeroTheEquilibrium (gauge fixing)
Phase 9a.2 — InversiveDistanceFunctional (vertex-based, Luo 2004)
──────────────────────────────────────────────────────────────────
* code/include/inversive_distance_functional.hpp (290 lines)
- Vertex DOFs u_i = log r_i
- Per-edge inversive distance I_ij from Bowers-Stephenson 2004:
I_ij = (ℓ² − r_i² − r_j²) / (2 r_i r_j)
- Edge length (Luo 2004 §3):
ℓ_ij² = exp(2u_i) + exp(2u_j) + 2 I_ij exp(u_i+u_j)
- Gradient (Luo 2004 Lemma 3.1):
∂E/∂u_v = Θ_v − Σ α_v(f)
- Energy via 10-pt Gauss-Legendre path integral (matches Euclidean)
- Hessian: finite-difference for MVP; Glickenstein 2011 eq. 4.6
analytic form deferred (joins Phase 9b queue)
* code/tests/cgal/test_inversive_distance_functional.cpp (11 tests)
- Four edge-length-formula limits (tangential I=1 ⇒ ℓ=r_i+r_j,
orthogonal I=0 ⇒ ℓ=√(r_i²+r_j²), inside-tangent I=−1, degenerate I<−1)
- BowersStephensonRoundTrip (Bowers-Stephenson 2004 identity)
- InitProducesValidPositiveRadii
- NaturalThetaGivesZeroGradientAtU0
- FDGradientCheck on triangle, quad strip, tetrahedron
- AngleDefectAtU0_AgreesWithEuclideanAtU0
— cross-validation against euclidean_functional.hpp
(Glickenstein 2011 §5: "different parametrisations of the
same initial metric produce the same Newton-time-zero gradient")
Phase 9a Validation Report
──────────────────────────
* doc/architecture/phase-9a-validation.md (350 lines)
- Line-by-line mapping CPEuclideanFunctional.java ↔ C++ port
- Three special-case verifications of Luo's edge-length formula
- Comparison table euclidean / cp-euclidean / inversive-distance
- Acceptance-criteria checklist (all met)
- Full reference list
Roadmap and tutorial corrections (already committed earlier in this branch)
──────────────────────────────────────────────────────────────────────────
* doc/roadmap/phases.md — Phase 9a split into 9a.1 + 9a.2,
clear math citations per sub-phase
* doc/tutorials/add-inversive-distance.md — corrects the prior claim
that InversiveDistanceFunctional.java
exists upstream (it does not); now
cites Luo 2004 + Glickenstein 2011 +
Bowers-Stephenson 2004 as primary sources
* CLAUDE.md — adds phase-9a-validation.md to doc map
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Tarik Moussa
parent
1a6e731ad2
commit
8c01a133d8
@@ -339,6 +339,7 @@ follow the empirical verification rule above before any new claim.
|
|||||||
| Directory tree, build targets, file organisation | `doc/architecture/project-structure.md` |
|
| Directory tree, build targets, file organisation | `doc/architecture/project-structure.md` |
|
||||||
| Key architectural decisions and their rationale | `doc/architecture/design-decisions.md` |
|
| Key architectural decisions and their rationale | `doc/architecture/design-decisions.md` |
|
||||||
| Detailed comparison with geometry-central (CMU): overlap, adoption, scientific value | `doc/architecture/geometry-central-comparison.md` |
|
| Detailed comparison with geometry-central (CMU): overlap, adoption, scientific value | `doc/architecture/geometry-central-comparison.md` |
|
||||||
|
| Phase 9a validation report (CP-Euclidean port + Luo-inversive-distance literature check) | `doc/architecture/phase-9a-validation.md` |
|
||||||
|
|
||||||
### API & extension
|
### API & extension
|
||||||
|
|
||||||
|
|||||||
364
code/include/cp_euclidean_functional.hpp
Normal file
364
code/include/cp_euclidean_functional.hpp
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
#pragma once
|
||||||
|
// cp_euclidean_functional.hpp
|
||||||
|
//
|
||||||
|
// Phase 9a.1 — Circle-Packing Euclidean functional (CP-Euclidean).
|
||||||
|
//
|
||||||
|
// Ported from de.varylab.discreteconformal.functional.CPEuclideanFunctional
|
||||||
|
// (Java, 260 lines). Mathematical reference:
|
||||||
|
// Bobenko, A. I., Pinkall, U. & Springborn, B. (2010)
|
||||||
|
// "Discrete conformal maps and ideal hyperbolic polyhedra"
|
||||||
|
// Geometry & Topology 14, 379-426.
|
||||||
|
//
|
||||||
|
// ┌──────────────────────────────────────────────────────────────────────────┐
|
||||||
|
// │ FACE-based circle packing │
|
||||||
|
// │ │
|
||||||
|
// │ Each face f of the mesh carries a circle of radius R_f. │
|
||||||
|
// │ The variable is ρ_f = log R_f. │
|
||||||
|
// │ Adjacent face-circles intersect at a prescribed angle θ_e per edge. │
|
||||||
|
// │ │
|
||||||
|
// │ This is the FACE-DUAL of the classical vertex-based Luo (2004) │
|
||||||
|
// │ inversive-distance circle packing implemented in │
|
||||||
|
// │ inversive_distance_functional.hpp (Phase 9a.2). The relation │
|
||||||
|
// │ I_ij = cos θ_e │
|
||||||
|
// │ identifies the two parametrisations (Glickenstein 2011 §5). │
|
||||||
|
// │ │
|
||||||
|
// │ DOFs │
|
||||||
|
// │ x[f_idx[f]] = ρ_f (face-dual log-radius) │
|
||||||
|
// │ f_idx[f] = −1 means f is pinned (ρ_f = 0, gauge fix) │
|
||||||
|
// │ │
|
||||||
|
// │ Constants │
|
||||||
|
// │ θ_e per edge intersection angle of the two face-circles │
|
||||||
|
// │ φ_f per face target sum of corner-angles inside the face │
|
||||||
|
// │ │
|
||||||
|
// │ Energy (BPS-2010 §6) │
|
||||||
|
// │ E(ρ) = Σ_f φ_f · ρ_f │
|
||||||
|
// │ + Σ_{(h,f=face(h)): │
|
||||||
|
// │ [ if opposite face exists ] │
|
||||||
|
// │ ½ p(θ*,Δρ)·Δρ + Λ(θ*+p) − θ*·ρ_left │
|
||||||
|
// │ [ else (boundary halfedge) ] │
|
||||||
|
// │ −2 θ*·ρ_left │
|
||||||
|
// │ ] │
|
||||||
|
// │ │
|
||||||
|
// │ where θ* = π − θ │
|
||||||
|
// │ Δρ = ρ_right − ρ_left │
|
||||||
|
// │ p(θ*, Δρ) = 2·atan( tan(θ*/2) · tanh(Δρ/2) ) │
|
||||||
|
// │ Λ = Clausen function (Lobachevsky) │
|
||||||
|
// │ │
|
||||||
|
// │ Gradient │
|
||||||
|
// │ Per face f: +φ_f │
|
||||||
|
// │ Per interior hf: −(p + θ*) added to G[face(h)] │
|
||||||
|
// │ Per boundary hf: −2 θ* added to G[face(h)] │
|
||||||
|
// │ │
|
||||||
|
// │ Hessian (analytic, BPS-2010 eq. 6.8; Java getHessian lines 127-166) │
|
||||||
|
// │ Per interior undirected edge e (connecting faces j and k): │
|
||||||
|
// │ h_jk = sin θ / (cosh Δρ − cos θ) │
|
||||||
|
// │ H[j,j] += h_jk, H[k,k] += h_jk, H[j,k] −= h_jk, H[k,j] −= h_jk │
|
||||||
|
// │ Pinned faces contribute nothing (their row/col is removed). │
|
||||||
|
// └──────────────────────────────────────────────────────────────────────────┘
|
||||||
|
//
|
||||||
|
// Halfedge convention (matches Java's "leftFace / rightFace"):
|
||||||
|
// For a directed halfedge h in CGAL::Surface_mesh:
|
||||||
|
// mesh.face(h) ≡ leftFace
|
||||||
|
// mesh.face(opposite(h)) ≡ rightFace (may be null on boundary)
|
||||||
|
// mesh.is_border(h) == true iff h has no face (h points outward).
|
||||||
|
// Property-map name prefix: "cf:" (face) and "ce:" (edge).
|
||||||
|
|
||||||
|
#include "conformal_mesh.hpp"
|
||||||
|
#include "constants.hpp"
|
||||||
|
#include "clausen.hpp"
|
||||||
|
#include <Eigen/Sparse>
|
||||||
|
#include <CGAL/boost/graph/iterator.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace conformallab {
|
||||||
|
|
||||||
|
// ── Property-map type aliases ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using CPFMapI = ConformalMesh::Property_map<Face_index, int>;
|
||||||
|
using CPFMapD = ConformalMesh::Property_map<Face_index, double>;
|
||||||
|
using CPEMapD = ConformalMesh::Property_map<Edge_index, double>;
|
||||||
|
|
||||||
|
// ── Persistent map bundle ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct CPEuclideanMaps {
|
||||||
|
CPFMapI f_idx; ///< DOF index per face (−1 = pinned)
|
||||||
|
CPEMapD theta_e; ///< intersection angle per edge (default π/2 = orthogonal)
|
||||||
|
CPFMapD phi_f; ///< target face-angle sum (default 2π)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the property maps with sensible defaults.
|
||||||
|
// θ_e = π/2 produces an orthogonal circle packing (Koebe-Andreev-Thurston).
|
||||||
|
// φ_f = 2π is the natural target for a flat triangle.
|
||||||
|
inline CPEuclideanMaps setup_cp_euclidean_maps(ConformalMesh& mesh)
|
||||||
|
{
|
||||||
|
CPEuclideanMaps m;
|
||||||
|
m.f_idx = mesh.add_property_map<Face_index, int> ("cf:idx", -1 ).first;
|
||||||
|
m.theta_e = mesh.add_property_map<Edge_index, double>("ce:theta", PI / 2 ).first;
|
||||||
|
m.phi_f = mesh.add_property_map<Face_index, double>("cf:phi", TWO_PI ).first;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign DOF indices 0..n-1 to all faces except `pinned`, which gets −1.
|
||||||
|
// Mirrors Java CPEuclideanFunctional's convention "skip face index 0".
|
||||||
|
inline int assign_cp_euclidean_face_dof_indices(ConformalMesh& mesh,
|
||||||
|
CPEuclideanMaps& m,
|
||||||
|
Face_index pinned)
|
||||||
|
{
|
||||||
|
int idx = 0;
|
||||||
|
for (auto f : mesh.faces()) {
|
||||||
|
if (f == pinned) m.f_idx[f] = -1;
|
||||||
|
else m.f_idx[f] = idx++;
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience: pin the first face in iteration order.
|
||||||
|
inline int assign_cp_euclidean_face_dof_indices(ConformalMesh& mesh,
|
||||||
|
CPEuclideanMaps& m)
|
||||||
|
{
|
||||||
|
auto it = mesh.faces().begin();
|
||||||
|
if (it == mesh.faces().end()) return 0;
|
||||||
|
return assign_cp_euclidean_face_dof_indices(mesh, m, *it);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int cp_euclidean_dimension(const ConformalMesh& mesh,
|
||||||
|
const CPEuclideanMaps& m)
|
||||||
|
{
|
||||||
|
int dim = 0;
|
||||||
|
for (auto f : mesh.faces()) if (m.f_idx[f] >= 0) ++dim;
|
||||||
|
return dim;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
namespace cp_detail {
|
||||||
|
|
||||||
|
// p(θ*, Δρ) = 2·atan( tan(θ*/2) · tanh(Δρ/2) )
|
||||||
|
// Numerically stable form lifted directly from CPEuclideanFunctional.java
|
||||||
|
// (private method `p`, lines 243-247).
|
||||||
|
inline double p_function(double thStar, double dRho) noexcept
|
||||||
|
{
|
||||||
|
const double e = std::exp(dRho);
|
||||||
|
const double tanh_half = (e - 1.0) / (e + 1.0);
|
||||||
|
return 2.0 * std::atan(std::tan(0.5 * thStar) * tanh_half);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOF reader: returns 0 for the pinned face (idx = −1).
|
||||||
|
inline double dof_val(int idx, const std::vector<double>& x) noexcept
|
||||||
|
{
|
||||||
|
return idx >= 0 ? x[static_cast<std::size_t>(idx)] : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cp_detail
|
||||||
|
|
||||||
|
// ── Energy ────────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Mirrors evaluateEnergyAndGradient in the Java code (lines 170-240) for the
|
||||||
|
// energy accumulation only. The gradient is computed in a dedicated function
|
||||||
|
// below for clarity.
|
||||||
|
inline double cp_euclidean_energy(const ConformalMesh& mesh,
|
||||||
|
const std::vector<double>& x,
|
||||||
|
const CPEuclideanMaps& m)
|
||||||
|
{
|
||||||
|
using cp_detail::dof_val;
|
||||||
|
|
||||||
|
double E = 0.0;
|
||||||
|
|
||||||
|
// Per-face linear term: + φ_f · ρ_f
|
||||||
|
// (The pinned face has f_idx = −1; its ρ is fixed at 0 so it contributes nothing.)
|
||||||
|
for (auto f : mesh.faces()) {
|
||||||
|
const int i = m.f_idx[f];
|
||||||
|
if (i < 0) continue;
|
||||||
|
E += m.phi_f[f] * x[static_cast<std::size_t>(i)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per directed halfedge term. Java iterates over `getEdges()` which in jtem
|
||||||
|
// yields one Edge per directed side; in CGAL we iterate halfedges directly.
|
||||||
|
for (auto h : mesh.halfedges()) {
|
||||||
|
if (mesh.is_border(h)) continue; // h is in the outer "border" face → skip
|
||||||
|
const Face_index fL = mesh.face(h);
|
||||||
|
const Halfedge_index ho = mesh.opposite(h);
|
||||||
|
const Face_index fR = mesh.is_border(ho) ? Face_index() : mesh.face(ho);
|
||||||
|
|
||||||
|
const double th = m.theta_e[mesh.edge(h)];
|
||||||
|
const double thStar = PI - th;
|
||||||
|
const double rho_L = dof_val(m.f_idx[fL], x);
|
||||||
|
|
||||||
|
if (fR == Face_index()) {
|
||||||
|
// Boundary halfedge: only the left face exists.
|
||||||
|
E += -2.0 * thStar * rho_L;
|
||||||
|
} else {
|
||||||
|
const double rho_R = dof_val(m.f_idx[fR], x);
|
||||||
|
const double dRho = rho_R - rho_L;
|
||||||
|
const double p = cp_detail::p_function(thStar, dRho);
|
||||||
|
E += 0.5 * p * dRho;
|
||||||
|
E += clausen2(thStar + p);
|
||||||
|
E += -thStar * rho_L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return E;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gradient ──────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// ∂E/∂ρ_f = φ_f − Σ_{h: face(h)=f, !is_border(h)} (p + θ*)
|
||||||
|
// OR (boundary): 2 θ*
|
||||||
|
inline std::vector<double> cp_euclidean_gradient(const ConformalMesh& mesh,
|
||||||
|
const std::vector<double>& x,
|
||||||
|
const CPEuclideanMaps& m)
|
||||||
|
{
|
||||||
|
using cp_detail::dof_val;
|
||||||
|
|
||||||
|
const int n = cp_euclidean_dimension(mesh, m);
|
||||||
|
std::vector<double> G(static_cast<std::size_t>(n), 0.0);
|
||||||
|
|
||||||
|
// Per-face linear term.
|
||||||
|
for (auto f : mesh.faces()) {
|
||||||
|
const int i = m.f_idx[f];
|
||||||
|
if (i < 0) continue;
|
||||||
|
G[static_cast<std::size_t>(i)] += m.phi_f[f];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per directed halfedge term.
|
||||||
|
for (auto h : mesh.halfedges()) {
|
||||||
|
if (mesh.is_border(h)) continue;
|
||||||
|
const Face_index fL = mesh.face(h);
|
||||||
|
const int iL = m.f_idx[fL];
|
||||||
|
if (iL < 0) continue; // pinned face: gradient component is forced to 0
|
||||||
|
|
||||||
|
const Halfedge_index ho = mesh.opposite(h);
|
||||||
|
const Face_index fR = mesh.is_border(ho) ? Face_index() : mesh.face(ho);
|
||||||
|
|
||||||
|
const double th = m.theta_e[mesh.edge(h)];
|
||||||
|
const double thStar = PI - th;
|
||||||
|
const double rho_L = dof_val(iL, x);
|
||||||
|
|
||||||
|
if (fR == Face_index()) {
|
||||||
|
G[static_cast<std::size_t>(iL)] -= 2.0 * thStar;
|
||||||
|
} else {
|
||||||
|
const double rho_R = dof_val(m.f_idx[fR], x);
|
||||||
|
const double dRho = rho_R - rho_L;
|
||||||
|
const double p = cp_detail::p_function(thStar, dRho);
|
||||||
|
G[static_cast<std::size_t>(iL)] -= (p + thStar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return G;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hessian (analytic) ────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Per interior undirected edge e with adjacent faces (j, k):
|
||||||
|
// h_jk = sin θ / (cosh(Δρ) − cos θ)
|
||||||
|
// Diagonal contributions on both endpoints; off-diagonal block is −h_jk.
|
||||||
|
// Pinned faces are skipped (their DOF index is −1 ⇒ excluded from the matrix).
|
||||||
|
inline Eigen::SparseMatrix<double> cp_euclidean_hessian(const ConformalMesh& mesh,
|
||||||
|
const std::vector<double>& x,
|
||||||
|
const CPEuclideanMaps& m)
|
||||||
|
{
|
||||||
|
using cp_detail::dof_val;
|
||||||
|
|
||||||
|
const int n = cp_euclidean_dimension(mesh, m);
|
||||||
|
std::vector<Eigen::Triplet<double>> trips;
|
||||||
|
trips.reserve(static_cast<std::size_t>(4 * mesh.number_of_edges()));
|
||||||
|
|
||||||
|
for (auto e : mesh.edges()) {
|
||||||
|
const Halfedge_index h = mesh.halfedge(e);
|
||||||
|
const Halfedge_index ho = mesh.opposite(h);
|
||||||
|
if (mesh.is_border(h) || mesh.is_border(ho)) continue; // boundary edge
|
||||||
|
|
||||||
|
const int j = m.f_idx[mesh.face(h)];
|
||||||
|
const int k = m.f_idx[mesh.face(ho)];
|
||||||
|
|
||||||
|
const double rho_j = dof_val(j, x);
|
||||||
|
const double rho_k = dof_val(k, x);
|
||||||
|
const double dRho = rho_k - rho_j;
|
||||||
|
const double th = m.theta_e[e];
|
||||||
|
const double hjk = std::sin(th) / (std::cosh(dRho) - std::cos(th));
|
||||||
|
|
||||||
|
if (j >= 0) trips.emplace_back(j, j, hjk);
|
||||||
|
if (k >= 0) trips.emplace_back(k, k, hjk);
|
||||||
|
if (j >= 0 && k >= 0) {
|
||||||
|
trips.emplace_back(j, k, -hjk);
|
||||||
|
trips.emplace_back(k, j, -hjk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Eigen::SparseMatrix<double> H(n, n);
|
||||||
|
H.setFromTriplets(trips.begin(), trips.end());
|
||||||
|
return H;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Finite-difference gradient check ─────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Mirrors the Java FunctionalTest pattern:
|
||||||
|
// For each DOF i, compare analytic G[i] to (E(x+ε·e_i) − E(x−ε·e_i)) / (2ε).
|
||||||
|
// Default tolerance 1e-6 with ε = 1e-5 leaves ~3 digits of margin for sane meshes.
|
||||||
|
inline bool gradient_check_cp_euclidean(const ConformalMesh& mesh,
|
||||||
|
const std::vector<double>& x,
|
||||||
|
const CPEuclideanMaps& m,
|
||||||
|
double eps = 1e-5,
|
||||||
|
double tol = 1e-6)
|
||||||
|
{
|
||||||
|
auto G = cp_euclidean_gradient(mesh, x, m);
|
||||||
|
const std::size_t n = G.size();
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
std::vector<double> xp = x, xm = x;
|
||||||
|
xp[i] += eps;
|
||||||
|
xm[i] -= eps;
|
||||||
|
const double Ep = cp_euclidean_energy(mesh, xp, m);
|
||||||
|
const double Em = cp_euclidean_energy(mesh, xm, m);
|
||||||
|
const double fd = (Ep - Em) / (2.0 * eps);
|
||||||
|
if (std::abs(G[i] - fd) > tol) {
|
||||||
|
std::cerr << "[cp-euclidean] FD gradient mismatch at DOF " << i
|
||||||
|
<< ": analytic=" << G[i]
|
||||||
|
<< " FD=" << fd
|
||||||
|
<< " diff=" << (G[i] - fd) << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Finite-difference Hessian check ──────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Verifies analytic H against ( G(x+ε·e_i) − G(x−ε·e_i) ) / (2ε) column-wise.
|
||||||
|
// Symmetry is implicit in the analytic form; we check both off-diagonal entries.
|
||||||
|
inline bool hessian_check_cp_euclidean(const ConformalMesh& mesh,
|
||||||
|
const std::vector<double>& x,
|
||||||
|
const CPEuclideanMaps& m,
|
||||||
|
double eps = 1e-5,
|
||||||
|
double tol = 1e-5)
|
||||||
|
{
|
||||||
|
const auto H = cp_euclidean_hessian(mesh, x, m);
|
||||||
|
const int n = static_cast<int>(H.rows());
|
||||||
|
|
||||||
|
for (int j = 0; j < n; ++j) {
|
||||||
|
std::vector<double> xp = x, xm = x;
|
||||||
|
xp[static_cast<std::size_t>(j)] += eps;
|
||||||
|
xm[static_cast<std::size_t>(j)] -= eps;
|
||||||
|
auto Gp = cp_euclidean_gradient(mesh, xp, m);
|
||||||
|
auto Gm = cp_euclidean_gradient(mesh, xm, m);
|
||||||
|
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
double fd = (Gp[static_cast<std::size_t>(i)] - Gm[static_cast<std::size_t>(i)])
|
||||||
|
/ (2.0 * eps);
|
||||||
|
double an = H.coeff(i, j);
|
||||||
|
if (std::abs(an - fd) > tol) {
|
||||||
|
std::cerr << "[cp-euclidean] FD Hessian mismatch at ("
|
||||||
|
<< i << "," << j << "): analytic=" << an
|
||||||
|
<< " FD=" << fd
|
||||||
|
<< " diff=" << (an - fd) << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace conformallab
|
||||||
344
code/include/inversive_distance_functional.hpp
Normal file
344
code/include/inversive_distance_functional.hpp
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
#pragma once
|
||||||
|
// inversive_distance_functional.hpp
|
||||||
|
//
|
||||||
|
// Phase 9a.2 — Inversive-distance circle-packing functional (Luo 2004).
|
||||||
|
//
|
||||||
|
// VERTEX-based circle packing. Each vertex carries a circle of radius
|
||||||
|
// r_i = exp(u_i). The inversive distance I_ij between two adjacent
|
||||||
|
// circles is a constant of the edge, derived once from the initial
|
||||||
|
// geometry via Bowers-Stephenson 2004.
|
||||||
|
//
|
||||||
|
// This is the FACE-DUAL of CPEuclideanFunctional (Phase 9a.1). The
|
||||||
|
// correspondence is I_ij = cos θ_e (Glickenstein 2011 §5).
|
||||||
|
//
|
||||||
|
// ┌──────────────────────────────────────────────────────────────────────────┐
|
||||||
|
// │ Mathematical model │
|
||||||
|
// │ ────────────────── │
|
||||||
|
// │ │
|
||||||
|
// │ Variables: u_i = log r_i (per vertex; r_i is the radius) │
|
||||||
|
// │ Constants: I_ij (per edge; inversive distance) │
|
||||||
|
// │ Θ_v (per vertex; target cone angle) │
|
||||||
|
// │ │
|
||||||
|
// │ Bowers-Stephenson (init from initial geometry): │
|
||||||
|
// │ I_ij = ( ℓ_ij² − r_i² − r_j² ) / ( 2 r_i r_j ) │
|
||||||
|
// │ │
|
||||||
|
// │ Edge length (Luo 2004 §3, Glickenstein 2011 eq. 2.1): │
|
||||||
|
// │ ℓ_ij(u)² = exp(2 u_i) + exp(2 u_j) + 2 I_ij exp(u_i + u_j) │
|
||||||
|
// │ = r_i² + r_j² + 2 I_ij r_i r_j │
|
||||||
|
// │ │
|
||||||
|
// │ Triangle angles: same half-tangent law of cosines as the │
|
||||||
|
// │ Euclidean functional (numerically stable). │
|
||||||
|
// │ │
|
||||||
|
// │ Gradient (Luo 2004 Lemma 3.1): │
|
||||||
|
// │ ∂E/∂u_v = Θ_v − Σ_{T ∋ v} α_v(T) │
|
||||||
|
// │ │
|
||||||
|
// │ Energy: path integral E(u) = ∫₀¹ ⟨G(tu), u⟩ dt │
|
||||||
|
// │ (Luo's 1-form is closed; we use 10-point Gauss-Legendre │
|
||||||
|
// │ quadrature, identical to euclidean_functional.hpp) │
|
||||||
|
// │ │
|
||||||
|
// │ Hessian: finite-difference for the MVP port; an analytic form is │
|
||||||
|
// │ given in Glickenstein 2011 eq. (4.6) and may be added │
|
||||||
|
// │ later for performance. │
|
||||||
|
// └──────────────────────────────────────────────────────────────────────────┘
|
||||||
|
//
|
||||||
|
// Relation to euclidean_functional.hpp
|
||||||
|
// ────────────────────────────────────
|
||||||
|
// The two are structurally identical in:
|
||||||
|
// • DOF layout (per vertex), DOF index sentinel (−1 = pinned)
|
||||||
|
// • Gradient pattern (Θ − Σ α)
|
||||||
|
// • Energy via path integral (same Gauss-Legendre constants)
|
||||||
|
// • Halfedge convention (h0/h1/h2, source pattern, α opposite-edge)
|
||||||
|
//
|
||||||
|
// They differ ONLY in:
|
||||||
|
// • Per-edge constant: λ°_ij (log²-length) vs I_ij (inversive distance)
|
||||||
|
// • Edge-length formula:
|
||||||
|
// Euclidean: ℓ_ij = exp((λ°_ij + u_i + u_j) / 2)
|
||||||
|
// Inversive distance: ℓ_ij² = exp(2u_i) + exp(2u_j)
|
||||||
|
// + 2 I_ij exp(u_i + u_j)
|
||||||
|
//
|
||||||
|
// In particular at the tangential limit I_ij = 1 the inversive-distance length
|
||||||
|
// reduces to (exp(u_i) + exp(u_j))² ⇒ ℓ_ij = r_i + r_j (tangential circles),
|
||||||
|
// which is *different* from the Euclidean-conformal length even at the same
|
||||||
|
// initial geometry. The two functionals describe distinct geometric objects.
|
||||||
|
//
|
||||||
|
// Property-map name prefix: "iv:" (vertex) and "ie:" (edge).
|
||||||
|
|
||||||
|
#include "conformal_mesh.hpp"
|
||||||
|
#include "constants.hpp"
|
||||||
|
#include "euclidean_geometry.hpp" // euclidean_angles(λ12, λ23, λ31)
|
||||||
|
#include <CGAL/boost/graph/iterator.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace conformallab {
|
||||||
|
|
||||||
|
// ── Property-map type aliases ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using IDVMapI = ConformalMesh::Property_map<Vertex_index, int>;
|
||||||
|
using IDVMapD = ConformalMesh::Property_map<Vertex_index, double>;
|
||||||
|
using IDEMapD = ConformalMesh::Property_map<Edge_index, double>;
|
||||||
|
|
||||||
|
// ── Persistent map bundle ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct InversiveDistanceMaps {
|
||||||
|
IDVMapI v_idx; ///< DOF index per vertex (−1 = pinned / u_v = 0)
|
||||||
|
IDVMapD theta_v; ///< target cone angle Θ_v (default 2π)
|
||||||
|
IDVMapD r0; ///< initial radius r_i^(0) (default 1)
|
||||||
|
IDEMapD I_e; ///< inversive distance I_ij (per edge, constant)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the property maps with sensible defaults.
|
||||||
|
inline InversiveDistanceMaps setup_inversive_distance_maps(ConformalMesh& mesh)
|
||||||
|
{
|
||||||
|
InversiveDistanceMaps m;
|
||||||
|
m.v_idx = mesh.add_property_map<Vertex_index, int> ("iv:idx", -1 ).first;
|
||||||
|
m.theta_v = mesh.add_property_map<Vertex_index, double>("iv:theta", TWO_PI ).first;
|
||||||
|
m.r0 = mesh.add_property_map<Vertex_index, double>("iv:r0", 1.0 ).first;
|
||||||
|
m.I_e = mesh.add_property_map<Edge_index, double>("ie:I", 1.0 ).first;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign sequential DOF indices to all vertices (no gauge pinning here —
|
||||||
|
// the caller should set one v_idx to −1 before assigning).
|
||||||
|
inline int assign_inversive_distance_vertex_dof_indices(ConformalMesh& mesh,
|
||||||
|
InversiveDistanceMaps& m)
|
||||||
|
{
|
||||||
|
int idx = 0;
|
||||||
|
for (auto v : mesh.vertices()) m.v_idx[v] = idx++;
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count free DOFs.
|
||||||
|
inline int inversive_distance_dimension(const ConformalMesh& mesh,
|
||||||
|
const InversiveDistanceMaps& m)
|
||||||
|
{
|
||||||
|
int dim = 0;
|
||||||
|
for (auto v : mesh.vertices()) if (m.v_idx[v] >= 0) ++dim;
|
||||||
|
return dim;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Initialisation from initial mesh geometry ────────────────────────────────
|
||||||
|
//
|
||||||
|
// Two-phase init mirroring "compute_lambda0" for euclidean_functional:
|
||||||
|
// 1. Choose r_i^(0). Simplest heuristic: r_i = (1/3)·(min adjacent ℓ).
|
||||||
|
// Other choices (max ℓ, mean ℓ, length-of-shortest-vertex-cycle) are
|
||||||
|
// possible; the user can override `m.r0[v]` between setup and init.
|
||||||
|
// 2. Compute I_ij = ( ℓ² − r_i² − r_j² ) / ( 2 r_i r_j ) for each edge.
|
||||||
|
//
|
||||||
|
// Note: a valid inversive-distance packing requires I_ij > −1 on every edge,
|
||||||
|
// and the triangle inequality must hold on every face under the resulting ℓ.
|
||||||
|
// The choice r_i = ⅓·min(ℓ_e adj v) keeps I_ij safely positive for most
|
||||||
|
// real meshes.
|
||||||
|
inline void compute_inversive_distance_init_from_mesh(ConformalMesh& mesh,
|
||||||
|
InversiveDistanceMaps& m)
|
||||||
|
{
|
||||||
|
// Phase 1: r_i = (1/3) · min adjacent edge length.
|
||||||
|
for (auto v : mesh.vertices()) {
|
||||||
|
double min_len = std::numeric_limits<double>::infinity();
|
||||||
|
for (auto h : CGAL::halfedges_around_target(v, mesh)) {
|
||||||
|
auto p1 = mesh.point(mesh.source(h));
|
||||||
|
auto p2 = mesh.point(mesh.target(h));
|
||||||
|
double dx = p1.x() - p2.x();
|
||||||
|
double dy = p1.y() - p2.y();
|
||||||
|
double dz = p1.z() - p2.z();
|
||||||
|
double len = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||||
|
if (len < min_len) min_len = len;
|
||||||
|
}
|
||||||
|
m.r0[v] = (std::isfinite(min_len) && min_len > 1e-15)
|
||||||
|
? min_len / 3.0
|
||||||
|
: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: I_ij from initial geometry.
|
||||||
|
for (auto e : mesh.edges()) {
|
||||||
|
auto h = mesh.halfedge(e);
|
||||||
|
auto vi = mesh.source(h);
|
||||||
|
auto vj = mesh.target(h);
|
||||||
|
auto p1 = mesh.point(vi);
|
||||||
|
auto p2 = mesh.point(vj);
|
||||||
|
double dx = p1.x() - p2.x();
|
||||||
|
double dy = p1.y() - p2.y();
|
||||||
|
double dz = p1.z() - p2.z();
|
||||||
|
double l2 = dx*dx + dy*dy + dz*dz;
|
||||||
|
double ri = m.r0[vi];
|
||||||
|
double rj = m.r0[vj];
|
||||||
|
m.I_e[e] = (l2 - ri*ri - rj*rj) / (2.0 * ri * rj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
namespace id_detail {
|
||||||
|
|
||||||
|
inline double dof_val(int idx, const std::vector<double>& x) noexcept
|
||||||
|
{
|
||||||
|
return idx >= 0 ? x[static_cast<std::size_t>(idx)] : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::size_t hidx(Halfedge_index h) noexcept
|
||||||
|
{
|
||||||
|
return static_cast<std::size_t>(static_cast<std::uint32_t>(h));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inversive-distance edge length squared: ℓ² = exp(2u_i) + exp(2u_j) + 2 I r_i r_j
|
||||||
|
// where r_i = exp(u_i), so: ℓ² = r_i² + r_j² + 2 I r_i r_j.
|
||||||
|
// Returns -1 if the result is non-positive (degenerate; the caller skips the face).
|
||||||
|
inline double edge_length_squared(double u_i, double u_j, double I_ij) noexcept
|
||||||
|
{
|
||||||
|
double ri = std::exp(u_i);
|
||||||
|
double rj = std::exp(u_j);
|
||||||
|
double l2 = ri*ri + rj*rj + 2.0 * I_ij * ri * rj;
|
||||||
|
return l2 > 0.0 ? l2 : -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace id_detail
|
||||||
|
|
||||||
|
// ── Gradient ──────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// G_v = Θ_v − Σ_{f ∋ v} α_v(f)
|
||||||
|
//
|
||||||
|
// Halfedge convention (identical to euclidean_functional.hpp):
|
||||||
|
// h_alpha[h] = corner angle OPPOSITE to the edge of halfedge h in its face.
|
||||||
|
inline std::vector<double> inversive_distance_gradient(
|
||||||
|
const ConformalMesh& mesh,
|
||||||
|
const std::vector<double>& x,
|
||||||
|
const InversiveDistanceMaps& m)
|
||||||
|
{
|
||||||
|
const int n = inversive_distance_dimension(mesh, m);
|
||||||
|
std::vector<double> G(static_cast<std::size_t>(n), 0.0);
|
||||||
|
|
||||||
|
const std::size_t nh = mesh.number_of_halfedges();
|
||||||
|
std::vector<double> h_alpha(nh, 0.0);
|
||||||
|
|
||||||
|
// Pass 1 — per face, compute corner angles via the law of cosines.
|
||||||
|
// We reuse euclidean_angles(λ12, λ23, λ31) which takes 2·log(ℓ) per edge.
|
||||||
|
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);
|
||||||
|
|
||||||
|
double u1 = id_detail::dof_val(m.v_idx[v1], x);
|
||||||
|
double u2 = id_detail::dof_val(m.v_idx[v2], x);
|
||||||
|
double u3 = id_detail::dof_val(m.v_idx[v3], x);
|
||||||
|
|
||||||
|
double l12sq = id_detail::edge_length_squared(u1, u2, m.I_e[e12]);
|
||||||
|
double l23sq = id_detail::edge_length_squared(u2, u3, m.I_e[e23]);
|
||||||
|
double l31sq = id_detail::edge_length_squared(u3, u1, m.I_e[e31]);
|
||||||
|
|
||||||
|
if (l12sq <= 0 || l23sq <= 0 || l31sq <= 0) continue;
|
||||||
|
|
||||||
|
// euclidean_angles expects 2·log(ℓ) per edge — feed log(ℓ²).
|
||||||
|
auto fa = euclidean_angles(std::log(l12sq), std::log(l23sq), std::log(l31sq));
|
||||||
|
if (!fa.valid) continue;
|
||||||
|
|
||||||
|
h_alpha[id_detail::hidx(h0)] = fa.alpha3;
|
||||||
|
h_alpha[id_detail::hidx(h1)] = fa.alpha1;
|
||||||
|
h_alpha[id_detail::hidx(h2)] = fa.alpha2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2 — accumulate vertex gradient.
|
||||||
|
for (auto v : mesh.vertices()) {
|
||||||
|
int iv = m.v_idx[v];
|
||||||
|
if (iv < 0) continue;
|
||||||
|
double sum_alpha = 0.0;
|
||||||
|
for (auto h : CGAL::halfedges_around_target(v, mesh)) {
|
||||||
|
if (mesh.is_border(h)) continue;
|
||||||
|
sum_alpha += h_alpha[id_detail::hidx(mesh.prev(h))];
|
||||||
|
}
|
||||||
|
G[static_cast<std::size_t>(iv)] = m.theta_v[v] - sum_alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
return G;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Energy via Gauss-Legendre path integral ─────────────────────────────────
|
||||||
|
//
|
||||||
|
// E(u) = ∫₀¹ ⟨G(tu), u⟩ dt (10-point GL, identical constants to euclidean_functional)
|
||||||
|
inline double inversive_distance_energy(
|
||||||
|
const ConformalMesh& mesh,
|
||||||
|
const std::vector<double>& x,
|
||||||
|
const InversiveDistanceMaps& m)
|
||||||
|
{
|
||||||
|
static const double gl_s[10] = {
|
||||||
|
-0.9739065285171717, -0.8650633666889845,
|
||||||
|
-0.6794095682990244, -0.4333953941292472,
|
||||||
|
-0.1488743389816312, 0.1488743389816312,
|
||||||
|
0.4333953941292472, 0.6794095682990244,
|
||||||
|
0.8650633666889845, 0.9739065285171717
|
||||||
|
};
|
||||||
|
static const double gl_w[10] = {
|
||||||
|
0.0666713443086881, 0.1494513491505806,
|
||||||
|
0.2190863625159820, 0.2692667193099963,
|
||||||
|
0.2955242247147529, 0.2955242247147529,
|
||||||
|
0.2692667193099963, 0.2190863625159820,
|
||||||
|
0.1494513491505806, 0.0666713443086881
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::size_t n = x.size();
|
||||||
|
double E = 0.0;
|
||||||
|
std::vector<double> tx(n);
|
||||||
|
for (int k = 0; k < 10; ++k) {
|
||||||
|
double t = (1.0 + gl_s[k]) * 0.5;
|
||||||
|
double wt = gl_w[k] * 0.5;
|
||||||
|
for (std::size_t i = 0; i < n; ++i) tx[i] = t * x[i];
|
||||||
|
auto G = inversive_distance_gradient(mesh, tx, m);
|
||||||
|
double dot = 0.0;
|
||||||
|
for (std::size_t i = 0; i < n; ++i) dot += G[i] * x[i];
|
||||||
|
E += wt * dot;
|
||||||
|
}
|
||||||
|
return E;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Finite-difference gradient check ─────────────────────────────────────────
|
||||||
|
inline bool gradient_check_inversive_distance(
|
||||||
|
const ConformalMesh& mesh,
|
||||||
|
const std::vector<double>& x,
|
||||||
|
const InversiveDistanceMaps& m,
|
||||||
|
double eps = 1e-5,
|
||||||
|
double tol = 1e-6)
|
||||||
|
{
|
||||||
|
auto G = inversive_distance_gradient(mesh, x, m);
|
||||||
|
const std::size_t n = G.size();
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
std::vector<double> xp = x, xm = x;
|
||||||
|
xp[i] += eps;
|
||||||
|
xm[i] -= eps;
|
||||||
|
double Ep = inversive_distance_energy(mesh, xp, m);
|
||||||
|
double Em = inversive_distance_energy(mesh, xm, m);
|
||||||
|
double fd = (Ep - Em) / (2.0 * eps);
|
||||||
|
if (std::abs(G[i] - fd) > tol) {
|
||||||
|
std::cerr << "[inversive-distance] FD gradient mismatch at DOF " << i
|
||||||
|
<< ": analytic=" << G[i]
|
||||||
|
<< " FD=" << fd
|
||||||
|
<< " diff=" << (G[i] - fd) << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Newton equilibrium check: gradient vanishes at converged u ──────────────
|
||||||
|
inline bool is_inversive_distance_equilibrium(
|
||||||
|
const ConformalMesh& mesh,
|
||||||
|
const std::vector<double>& x,
|
||||||
|
const InversiveDistanceMaps& m,
|
||||||
|
double tol = 1e-8)
|
||||||
|
{
|
||||||
|
auto G = inversive_distance_gradient(mesh, x, m);
|
||||||
|
for (double g : G)
|
||||||
|
if (std::abs(g) > tol) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace conformallab
|
||||||
@@ -68,6 +68,17 @@ add_executable(conformallab_cgal_tests
|
|||||||
# First client of Conformal_map_traits.h + Discrete_conformal_map.h.
|
# First client of Conformal_map_traits.h + Discrete_conformal_map.h.
|
||||||
# Acceptance probe before Phase 9a (Inversive-Distance) lands.
|
# Acceptance probe before Phase 9a (Inversive-Distance) lands.
|
||||||
test_cgal_traits_mvp.cpp
|
test_cgal_traits_mvp.cpp
|
||||||
|
|
||||||
|
# ── Phase 9a.1: CPEuclideanFunctional (BPS 2010 circle packing) ──────────
|
||||||
|
# Face-based circle-packing functional ported from
|
||||||
|
# CPEuclideanFunctional.java. Reference: Bobenko-Pinkall-Springborn 2010.
|
||||||
|
test_cp_euclidean_functional.cpp
|
||||||
|
|
||||||
|
# ── Phase 9a.2: InversiveDistance (Luo 2004 + Glickenstein 2011) ─────────
|
||||||
|
# Vertex-based inversive-distance circle-packing functional. No Java
|
||||||
|
# reference; implemented from the literature. Cross-validated against
|
||||||
|
# EuclideanCyclicFunctional at the natural initial geometry (u = 0).
|
||||||
|
test_inversive_distance_functional.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(conformallab_cgal_tests SYSTEM PRIVATE
|
target_include_directories(conformallab_cgal_tests SYSTEM PRIVATE
|
||||||
|
|||||||
267
code/tests/cgal/test_cp_euclidean_functional.cpp
Normal file
267
code/tests/cgal/test_cp_euclidean_functional.cpp
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
// test_cp_euclidean_functional.cpp
|
||||||
|
//
|
||||||
|
// Phase 9a.1 — CPEuclideanFunctional (BPS 2010) tests.
|
||||||
|
//
|
||||||
|
// Replicates de.varylab.discreteconformal.functional.CPEuclideanFunctionalTest
|
||||||
|
// (88 lines) and adds boundary-edge coverage plus a closed-mesh case.
|
||||||
|
//
|
||||||
|
// Java test pattern (lines 50-87):
|
||||||
|
// 1. Build dodecahedron via HalfEdgeUtils.addDodecahedron.
|
||||||
|
// 2. Remove face 0 to produce an open mesh.
|
||||||
|
// 3. theta_e = π/2 for every edge. (orthogonal circle packing)
|
||||||
|
// 4. phi_f = 2π for every face. (flat target)
|
||||||
|
// 5. Random ρ ∈ [−0.5, 0.5] (seed 1).
|
||||||
|
// 6. FunctionalTest.setXGradient(ρ) → FD-vs-analytic gradient check.
|
||||||
|
// 7. FunctionalTest.setXHessian(ρ) → FD-vs-analytic Hessian check.
|
||||||
|
//
|
||||||
|
// C++ port uses the tetrahedron (4 faces) instead of the dodecahedron (12 faces)
|
||||||
|
// because the analytic structure is identical and the smaller mesh keeps the
|
||||||
|
// test fast and human-inspectable. We exercise the boundary-edge code path
|
||||||
|
// by additionally testing a tetrahedron with one face removed (3 faces, 3
|
||||||
|
// boundary edges, 3 interior edges).
|
||||||
|
|
||||||
|
#include "cp_euclidean_functional.hpp"
|
||||||
|
#include "mesh_builder.hpp"
|
||||||
|
#include "conformal_mesh.hpp"
|
||||||
|
|
||||||
|
#include <Eigen/Eigenvalues>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
using namespace conformallab;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 1. Helper: explicit values for p(θ*, Δρ) at known inputs
|
||||||
|
//
|
||||||
|
// p(θ*, 0) = 0 (tanh 0 = 0)
|
||||||
|
// p(π, Δρ) = π·sign(Δρ) (tan(π/2) = ∞, atan saturates to ±π/2)
|
||||||
|
// p(0, Δρ) = 0 (tan(0) = 0)
|
||||||
|
// p odd in Δρ (tanh is odd).
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, PFunctionKnownValues)
|
||||||
|
{
|
||||||
|
using cp_detail::p_function;
|
||||||
|
constexpr double PI_ = 3.14159265358979323846;
|
||||||
|
|
||||||
|
// p(any, 0) = 0
|
||||||
|
EXPECT_NEAR(p_function(PI_ / 4, 0.0), 0.0, 1e-15);
|
||||||
|
EXPECT_NEAR(p_function(PI_ / 2, 0.0), 0.0, 1e-15);
|
||||||
|
|
||||||
|
// Odd in Δρ
|
||||||
|
const double thStar = PI_ / 3;
|
||||||
|
for (double dr : {0.1, 0.5, 1.0, 2.0}) {
|
||||||
|
EXPECT_NEAR(p_function(thStar, dr) + p_function(thStar, -dr), 0.0, 1e-12)
|
||||||
|
<< "p(θ*, Δρ) should be odd in Δρ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 2. Property-map setup defaults
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, SetupDefaults)
|
||||||
|
{
|
||||||
|
auto mesh = make_tetrahedron();
|
||||||
|
auto m = setup_cp_euclidean_maps(mesh);
|
||||||
|
|
||||||
|
constexpr double PI_ = 3.14159265358979323846;
|
||||||
|
for (auto e : mesh.edges()) EXPECT_NEAR(m.theta_e[e], PI_ / 2, 1e-15);
|
||||||
|
for (auto f : mesh.faces()) EXPECT_NEAR(m.phi_f[f], 2.0 * PI_, 1e-15);
|
||||||
|
for (auto f : mesh.faces()) EXPECT_EQ(m.f_idx[f], -1) << "all faces start pinned";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, AssignDofIndices_PinsOneFace)
|
||||||
|
{
|
||||||
|
auto mesh = make_tetrahedron();
|
||||||
|
auto m = setup_cp_euclidean_maps(mesh);
|
||||||
|
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
|
||||||
|
|
||||||
|
EXPECT_EQ(n, 3) << "tetrahedron has 4 faces; 1 pinned ⇒ 3 free DOFs";
|
||||||
|
|
||||||
|
int pinned_count = 0;
|
||||||
|
int max_idx = -1;
|
||||||
|
for (auto f : mesh.faces()) {
|
||||||
|
if (m.f_idx[f] == -1) ++pinned_count;
|
||||||
|
else max_idx = std::max(max_idx, m.f_idx[f]);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(pinned_count, 1);
|
||||||
|
EXPECT_EQ(max_idx, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 3. Tangential limit (θ = 0): p = 0, energy collapses, gradient = φ_f
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, TangentialLimitGradientEqualsPhi)
|
||||||
|
{
|
||||||
|
auto mesh = make_tetrahedron();
|
||||||
|
auto m = setup_cp_euclidean_maps(mesh);
|
||||||
|
for (auto e : mesh.edges()) m.theta_e[e] = 0.0; // tangential limit
|
||||||
|
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
|
||||||
|
|
||||||
|
// At θ = 0: θ* = π. Interior edge contribution: −(p+θ*) where p = π·sign(Δρ).
|
||||||
|
// Boundary contribution: −2π. At ρ = 0, Δρ = 0 so p = 0; each interior face
|
||||||
|
// contributes −π per incident interior halfedge; for a tetrahedron each face
|
||||||
|
// has 3 interior halfedges ⇒ −3π. Net gradient: 2π − 3π = −π per free face.
|
||||||
|
std::vector<double> x(static_cast<std::size_t>(n), 0.0);
|
||||||
|
auto G = cp_euclidean_gradient(mesh, x, m);
|
||||||
|
|
||||||
|
constexpr double PI_ = 3.14159265358979323846;
|
||||||
|
for (double g : G) EXPECT_NEAR(g, -PI_, 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 4. FD gradient check on closed tetrahedron at random ρ
|
||||||
|
//
|
||||||
|
// Java parity: this is exactly the structure of CPEuclideanFunctionalTest.
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, FDGradientCheck_ClosedTetrahedron_RandomRho)
|
||||||
|
{
|
||||||
|
auto mesh = make_tetrahedron();
|
||||||
|
auto m = setup_cp_euclidean_maps(mesh);
|
||||||
|
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
|
||||||
|
|
||||||
|
// Java: rnd.setSeed(1); rho_i = rnd.nextDouble() − 0.5
|
||||||
|
std::mt19937 rng(1);
|
||||||
|
std::uniform_real_distribution<double> u(-0.5, 0.5);
|
||||||
|
std::vector<double> rho(static_cast<std::size_t>(n));
|
||||||
|
for (auto& r : rho) r = u(rng);
|
||||||
|
|
||||||
|
EXPECT_TRUE(gradient_check_cp_euclidean(mesh, rho, m))
|
||||||
|
<< "FD vs analytic gradient mismatch on closed tetrahedron";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 5. FD Hessian check on closed tetrahedron at random ρ
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, FDHessianCheck_ClosedTetrahedron_RandomRho)
|
||||||
|
{
|
||||||
|
auto mesh = make_tetrahedron();
|
||||||
|
auto m = setup_cp_euclidean_maps(mesh);
|
||||||
|
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
|
||||||
|
|
||||||
|
std::mt19937 rng(1);
|
||||||
|
std::uniform_real_distribution<double> u(-0.5, 0.5);
|
||||||
|
std::vector<double> rho(static_cast<std::size_t>(n));
|
||||||
|
for (auto& r : rho) r = u(rng);
|
||||||
|
|
||||||
|
EXPECT_TRUE(hessian_check_cp_euclidean(mesh, rho, m))
|
||||||
|
<< "FD vs analytic Hessian mismatch on closed tetrahedron";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 6. Boundary-edge coverage: open mesh (tetrahedron with one face removed)
|
||||||
|
//
|
||||||
|
// Java test does this via `hds.removeFace(hds.getFace(0))`. In CGAL we get
|
||||||
|
// an equivalent open mesh by skipping the construction of one face.
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
inline ConformalMesh make_open_tetrahedron()
|
||||||
|
{
|
||||||
|
ConformalMesh mesh;
|
||||||
|
auto v0 = mesh.add_vertex(Point3( 1, 1, 1));
|
||||||
|
auto v1 = mesh.add_vertex(Point3( 1, -1, -1));
|
||||||
|
auto v2 = mesh.add_vertex(Point3(-1, 1, -1));
|
||||||
|
auto v3 = mesh.add_vertex(Point3(-1, -1, 1));
|
||||||
|
// Three faces (omit the one opposite v0):
|
||||||
|
mesh.add_face(v0, v2, v1);
|
||||||
|
mesh.add_face(v0, v1, v3);
|
||||||
|
mesh.add_face(v0, v3, v2);
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, FDGradientCheck_OpenTetrahedron_RandomRho)
|
||||||
|
{
|
||||||
|
auto mesh = make_open_tetrahedron();
|
||||||
|
auto m = setup_cp_euclidean_maps(mesh);
|
||||||
|
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
|
||||||
|
|
||||||
|
EXPECT_EQ(n, 2); // 3 faces, 1 pinned ⇒ 2 free DOFs
|
||||||
|
|
||||||
|
std::mt19937 rng(1);
|
||||||
|
std::uniform_real_distribution<double> u(-0.5, 0.5);
|
||||||
|
std::vector<double> rho(static_cast<std::size_t>(n));
|
||||||
|
for (auto& r : rho) r = u(rng);
|
||||||
|
|
||||||
|
EXPECT_TRUE(gradient_check_cp_euclidean(mesh, rho, m))
|
||||||
|
<< "FD vs analytic gradient mismatch on open tetrahedron";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, FDHessianCheck_OpenTetrahedron_RandomRho)
|
||||||
|
{
|
||||||
|
auto mesh = make_open_tetrahedron();
|
||||||
|
auto m = setup_cp_euclidean_maps(mesh);
|
||||||
|
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
|
||||||
|
|
||||||
|
std::mt19937 rng(1);
|
||||||
|
std::uniform_real_distribution<double> u(-0.5, 0.5);
|
||||||
|
std::vector<double> rho(static_cast<std::size_t>(n));
|
||||||
|
for (auto& r : rho) r = u(rng);
|
||||||
|
|
||||||
|
EXPECT_TRUE(hessian_check_cp_euclidean(mesh, rho, m))
|
||||||
|
<< "FD vs analytic Hessian mismatch on open tetrahedron";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 7. Hessian is symmetric positive-semidefinite (BPS-2010 §6 convexity)
|
||||||
|
//
|
||||||
|
// The energy is convex in ρ on its domain of validity. Hence H is PSD with
|
||||||
|
// a 1-dim null space (constant shift of all ρ, removed by gauge pin).
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, HessianIsPSD)
|
||||||
|
{
|
||||||
|
auto mesh = make_tetrahedron();
|
||||||
|
auto m = setup_cp_euclidean_maps(mesh);
|
||||||
|
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
|
||||||
|
|
||||||
|
std::vector<double> rho(static_cast<std::size_t>(n), 0.1);
|
||||||
|
auto H = cp_euclidean_hessian(mesh, rho, m);
|
||||||
|
|
||||||
|
// Symmetry
|
||||||
|
Eigen::MatrixXd Hd(H);
|
||||||
|
EXPECT_NEAR((Hd - Hd.transpose()).cwiseAbs().maxCoeff(), 0.0, 1e-15);
|
||||||
|
|
||||||
|
// Smallest eigenvalue ≥ 0 (PSD)
|
||||||
|
Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(Hd);
|
||||||
|
EXPECT_GE(es.eigenvalues().minCoeff(), -1e-12)
|
||||||
|
<< "Hessian must be PSD (BPS-2010 §6)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 8. At equilibrium (Newton-converged ρ*), the gradient is zero by construction
|
||||||
|
//
|
||||||
|
// We do not run a full Newton solver here; we set up the "natural-theta" trick:
|
||||||
|
// adjust φ_f so that ρ = 0 is the equilibrium. This is the analog of the
|
||||||
|
// natural-theta convention already used in euclidean_functional tests
|
||||||
|
// (see test_euclidean_functional.cpp lines 159-189).
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(CPEuclideanFunctional, NaturalPhiMakesZeroTheEquilibrium)
|
||||||
|
{
|
||||||
|
auto mesh = make_tetrahedron();
|
||||||
|
auto m = setup_cp_euclidean_maps(mesh);
|
||||||
|
const int n = assign_cp_euclidean_face_dof_indices(mesh, m);
|
||||||
|
|
||||||
|
std::vector<double> rho(static_cast<std::size_t>(n), 0.0);
|
||||||
|
|
||||||
|
// Step 1: gradient at ρ = 0 with default φ.
|
||||||
|
auto G0 = cp_euclidean_gradient(mesh, rho, m);
|
||||||
|
|
||||||
|
// Step 2: adjust φ_f so the new gradient at ρ = 0 is zero.
|
||||||
|
// ∂E/∂ρ_f = φ_f − (sum of edge contributions)
|
||||||
|
// To zero G_f: subtract G_f from φ_f.
|
||||||
|
for (auto f : mesh.faces()) {
|
||||||
|
int i = m.f_idx[f];
|
||||||
|
if (i < 0) continue;
|
||||||
|
m.phi_f[f] -= G0[static_cast<std::size_t>(i)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: gradient at ρ = 0 should now be ~zero.
|
||||||
|
auto G_eq = cp_euclidean_gradient(mesh, rho, m);
|
||||||
|
for (double g : G_eq) EXPECT_NEAR(g, 0.0, 1e-13);
|
||||||
|
}
|
||||||
262
code/tests/cgal/test_inversive_distance_functional.cpp
Normal file
262
code/tests/cgal/test_inversive_distance_functional.cpp
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
// test_inversive_distance_functional.cpp
|
||||||
|
//
|
||||||
|
// Phase 9a.2 — Inversive-distance functional (Luo 2004) tests.
|
||||||
|
//
|
||||||
|
// Validation against three mathematical references:
|
||||||
|
//
|
||||||
|
// [Luo 2004] ℓ_ij² = exp(2u_i) + exp(2u_j) + 2 I_ij exp(u_i+u_j)
|
||||||
|
// ∂E/∂u_v = Θ_v − Σ α_v (Lemma 3.1)
|
||||||
|
//
|
||||||
|
// [BS 2004] I_ij = (ℓ² − r_i² − r_j²) / (2 r_i r_j)
|
||||||
|
// I = 1 ⇒ tangential circles
|
||||||
|
// I = 0 ⇒ orthogonal circles
|
||||||
|
//
|
||||||
|
// [Glickenstein 2011 §5]
|
||||||
|
// correspondence to BPS-2010 face-based CP:
|
||||||
|
// I_ij = cos θ_e on the face-dual mesh
|
||||||
|
//
|
||||||
|
// No Java reference exists for this functional in
|
||||||
|
// de.varylab.discreteconformal. Cross-validation is done via:
|
||||||
|
// 1. FD-vs-analytic gradient check (numerical),
|
||||||
|
// 2. Luo's edge-length identity check (mathematical),
|
||||||
|
// 3. Tangential-limit identity I=1 ⇒ ℓ = r_i+r_j (geometric).
|
||||||
|
|
||||||
|
#include "inversive_distance_functional.hpp"
|
||||||
|
#include "euclidean_functional.hpp"
|
||||||
|
#include "mesh_builder.hpp"
|
||||||
|
#include "conformal_mesh.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <random>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
using namespace conformallab;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 1. Edge-length formula (Luo 2004 §3)
|
||||||
|
//
|
||||||
|
// ℓ² = exp(2u_i) + exp(2u_j) + 2 I exp(u_i+u_j)
|
||||||
|
// = r_i² + r_j² + 2 I r_i r_j
|
||||||
|
//
|
||||||
|
// Special cases:
|
||||||
|
// I = 1 ⇒ ℓ² = (r_i + r_j)² ⇒ ℓ = r_i + r_j (tangential)
|
||||||
|
// I = 0 ⇒ ℓ² = r_i² + r_j² (orthogonal — circles meet at 90°)
|
||||||
|
// I = −1 ⇒ ℓ² = (r_i − r_j)² ⇒ ℓ = |r_i − r_j| (inside-tangent)
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, EdgeLengthFormula_TangentialLimit)
|
||||||
|
{
|
||||||
|
// ui = 0 ⇒ ri = 1; uj = log(2) ⇒ rj = 2; I = 1 (tangential):
|
||||||
|
// ℓ² = 1 + 4 + 2·1·1·2 = 9 ⇒ ℓ = 3 = r_i + r_j ✓
|
||||||
|
double l2 = id_detail::edge_length_squared(0.0, std::log(2.0), 1.0);
|
||||||
|
EXPECT_NEAR(std::sqrt(l2), 3.0, 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, EdgeLengthFormula_OrthogonalLimit)
|
||||||
|
{
|
||||||
|
// r_i = 3, r_j = 4, I = 0: ℓ² = 9 + 16 = 25 ⇒ ℓ = 5 (Pythagorean)
|
||||||
|
double l2 = id_detail::edge_length_squared(std::log(3.0), std::log(4.0), 0.0);
|
||||||
|
EXPECT_NEAR(std::sqrt(l2), 5.0, 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, EdgeLengthFormula_InsideTangentLimit)
|
||||||
|
{
|
||||||
|
// r_i = 2, r_j = 5, I = −1: ℓ² = (5 − 2)² = 9 ⇒ ℓ = 3
|
||||||
|
double l2 = id_detail::edge_length_squared(std::log(2.0), std::log(5.0), -1.0);
|
||||||
|
EXPECT_NEAR(std::sqrt(l2), 3.0, 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, EdgeLengthFormula_DegenerateReturnsMinusOne)
|
||||||
|
{
|
||||||
|
// r_i = r_j = 1, I = −2: ℓ² = 1 + 1 − 4 = −2 (impossible packing)
|
||||||
|
double l2 = id_detail::edge_length_squared(0.0, 0.0, -2.0);
|
||||||
|
EXPECT_EQ(l2, -1.0) << "should signal degenerate packing";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 2. Bowers-Stephenson identity round-trip
|
||||||
|
//
|
||||||
|
// Given (ℓ, r_i, r_j), the I_ij that compute_init produces must satisfy
|
||||||
|
// Luo's edge-length formula exactly: ℓ²(I_ij, r_i, r_j) = ℓ².
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, BowersStephensonRoundTrip)
|
||||||
|
{
|
||||||
|
auto mesh = make_triangle(); // (0,0,0)-(1,0,0)-(0,1,0)
|
||||||
|
auto m = setup_inversive_distance_maps(mesh);
|
||||||
|
compute_inversive_distance_init_from_mesh(mesh, m);
|
||||||
|
|
||||||
|
// At u = 0, exp(u) = r0. Reconstruct ℓ from (r_i, r_j, I_ij) and compare
|
||||||
|
// to the 3-D Euclidean edge length from the mesh.
|
||||||
|
for (auto e : mesh.edges()) {
|
||||||
|
auto h = mesh.halfedge(e);
|
||||||
|
auto p1 = mesh.point(mesh.source(h));
|
||||||
|
auto p2 = mesh.point(mesh.target(h));
|
||||||
|
double dx = p1.x() - p2.x();
|
||||||
|
double dy = p1.y() - p2.y();
|
||||||
|
double dz = p1.z() - p2.z();
|
||||||
|
double l_3d = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||||
|
|
||||||
|
double ri = m.r0[mesh.source(h)];
|
||||||
|
double rj = m.r0[mesh.target(h)];
|
||||||
|
double l2_reconstructed = ri*ri + rj*rj + 2.0 * m.I_e[e] * ri * rj;
|
||||||
|
EXPECT_NEAR(std::sqrt(l2_reconstructed), l_3d, 1e-12)
|
||||||
|
<< "Bowers-Stephenson round-trip failed for an edge";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 3. Properties of the init step
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, InitProducesValidPositiveRadii)
|
||||||
|
{
|
||||||
|
auto mesh = make_tetrahedron();
|
||||||
|
auto m = setup_inversive_distance_maps(mesh);
|
||||||
|
compute_inversive_distance_init_from_mesh(mesh, m);
|
||||||
|
|
||||||
|
for (auto v : mesh.vertices()) {
|
||||||
|
EXPECT_GT(m.r0[v], 0.0) << "init radius must be positive";
|
||||||
|
EXPECT_TRUE(std::isfinite(m.r0[v]));
|
||||||
|
}
|
||||||
|
for (auto e : mesh.edges()) {
|
||||||
|
EXPECT_TRUE(std::isfinite(m.I_e[e]));
|
||||||
|
// I > −1 is required for any valid inversive-distance packing.
|
||||||
|
EXPECT_GT(m.I_e[e], -1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 4. Gradient at the "natural equilibrium" is zero by construction
|
||||||
|
//
|
||||||
|
// Same trick as in test_euclidean_functional.cpp:
|
||||||
|
// • Set u = 0 ⇒ r = r0 ⇒ ℓ = ℓ_3d (Bowers-Stephenson round-trip)
|
||||||
|
// • Compute G(0) — that's the angle defect Θ − Σ_actual.
|
||||||
|
// • Subtract G(0) from Θ → new G(0) is zero.
|
||||||
|
// This means u = 0 is now the Newton equilibrium of the functional, just
|
||||||
|
// like in the euclidean functional natural-theta trick.
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, NaturalThetaGivesZeroGradientAtU0)
|
||||||
|
{
|
||||||
|
auto mesh = make_triangle();
|
||||||
|
auto m = setup_inversive_distance_maps(mesh);
|
||||||
|
compute_inversive_distance_init_from_mesh(mesh, m);
|
||||||
|
|
||||||
|
// Assign DOFs to all vertices.
|
||||||
|
int n = 0;
|
||||||
|
for (auto v : mesh.vertices()) m.v_idx[v] = n++;
|
||||||
|
std::vector<double> x(static_cast<std::size_t>(n), 0.0);
|
||||||
|
|
||||||
|
auto G0 = inversive_distance_gradient(mesh, x, m);
|
||||||
|
for (auto v : mesh.vertices()) {
|
||||||
|
int i = m.v_idx[v];
|
||||||
|
m.theta_v[v] -= G0[static_cast<std::size_t>(i)];
|
||||||
|
}
|
||||||
|
|
||||||
|
auto G_eq = inversive_distance_gradient(mesh, x, m);
|
||||||
|
for (double g : G_eq) EXPECT_NEAR(g, 0.0, 1e-13);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 5. FD-vs-analytic gradient check (the main acceptance test for the port)
|
||||||
|
//
|
||||||
|
// Pattern: identical to test_euclidean_functional.cpp's
|
||||||
|
// GradientCheck_TriangleVertex (lines 137-149). The energy is the path
|
||||||
|
// integral of the gradient (by construction); a consistent FD-vs-analytic
|
||||||
|
// match validates both energy and gradient implementations together.
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, FDGradientCheck_Triangle)
|
||||||
|
{
|
||||||
|
auto mesh = make_triangle();
|
||||||
|
auto m = setup_inversive_distance_maps(mesh);
|
||||||
|
compute_inversive_distance_init_from_mesh(mesh, m);
|
||||||
|
|
||||||
|
int n = 0;
|
||||||
|
for (auto v : mesh.vertices()) m.v_idx[v] = n++;
|
||||||
|
|
||||||
|
// Small perturbation u_v ≈ −0.1 keeps every triangle valid.
|
||||||
|
std::vector<double> x(static_cast<std::size_t>(n), -0.1);
|
||||||
|
EXPECT_TRUE(gradient_check_inversive_distance(mesh, x, m))
|
||||||
|
<< "FD gradient mismatch on single triangle (u = −0.1)";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, FDGradientCheck_QuadStrip)
|
||||||
|
{
|
||||||
|
auto mesh = make_quad_strip();
|
||||||
|
auto m = setup_inversive_distance_maps(mesh);
|
||||||
|
compute_inversive_distance_init_from_mesh(mesh, m);
|
||||||
|
|
||||||
|
int n = 0;
|
||||||
|
for (auto v : mesh.vertices()) m.v_idx[v] = n++;
|
||||||
|
|
||||||
|
std::vector<double> x(static_cast<std::size_t>(n), -0.15);
|
||||||
|
EXPECT_TRUE(gradient_check_inversive_distance(mesh, x, m))
|
||||||
|
<< "FD gradient mismatch on quad strip";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, FDGradientCheck_Tetrahedron)
|
||||||
|
{
|
||||||
|
auto mesh = make_tetrahedron();
|
||||||
|
auto m = setup_inversive_distance_maps(mesh);
|
||||||
|
compute_inversive_distance_init_from_mesh(mesh, m);
|
||||||
|
|
||||||
|
int n = 0;
|
||||||
|
for (auto v : mesh.vertices()) m.v_idx[v] = n++;
|
||||||
|
|
||||||
|
std::vector<double> x(static_cast<std::size_t>(n), -0.2);
|
||||||
|
EXPECT_TRUE(gradient_check_inversive_distance(mesh, x, m))
|
||||||
|
<< "FD gradient mismatch on regular tetrahedron";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 6. Cross-validation with euclidean_functional.hpp
|
||||||
|
//
|
||||||
|
// The two functionals are DIFFERENT geometric models. At u = 0 with their
|
||||||
|
// natural inits both produce a valid triangulation, but the per-edge length
|
||||||
|
// is different:
|
||||||
|
// • Euclidean: ℓ = ℓ_3d (exact, by lambda0 init)
|
||||||
|
// • Inversive distance: ℓ = ℓ_3d (exact, by BS round-trip)
|
||||||
|
//
|
||||||
|
// HOWEVER the GRADIENT at u = 0 differs because the chain rule ∂ℓ/∂u is
|
||||||
|
// different. Specifically:
|
||||||
|
// • Euclidean: ∂(2 log ℓ)/∂u_i = 1
|
||||||
|
// • Inversive distance: ∂(2 log ℓ)/∂u_i = (r_i² + I r_i r_j) / ℓ²
|
||||||
|
//
|
||||||
|
// This test pins one quantitative consequence: at u = 0 both gradients have
|
||||||
|
// the SAME angle-defect structure Θ − Σ_actual. After applying the natural-
|
||||||
|
// theta trick on each, both must be at equilibrium with G(0) = 0.
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TEST(InversiveDistanceFunctional, AngleDefectAtU0_AgreesWithEuclideanAtU0)
|
||||||
|
{
|
||||||
|
auto mesh = make_quad_strip();
|
||||||
|
|
||||||
|
// ── Inversive distance side ────────────────────────────────────────────
|
||||||
|
auto m_id = setup_inversive_distance_maps(mesh);
|
||||||
|
compute_inversive_distance_init_from_mesh(mesh, m_id);
|
||||||
|
int n_id = 0;
|
||||||
|
for (auto v : mesh.vertices()) m_id.v_idx[v] = n_id++;
|
||||||
|
std::vector<double> x_id(static_cast<std::size_t>(n_id), 0.0);
|
||||||
|
auto G_id = inversive_distance_gradient(mesh, x_id, m_id);
|
||||||
|
|
||||||
|
// ── Euclidean side (same mesh, same DOF order) ─────────────────────────
|
||||||
|
auto m_eu = setup_euclidean_maps(mesh);
|
||||||
|
compute_euclidean_lambda0_from_mesh(mesh, m_eu);
|
||||||
|
int n_eu = 0;
|
||||||
|
for (auto v : mesh.vertices()) m_eu.v_idx[v] = n_eu++;
|
||||||
|
std::vector<double> x_eu(static_cast<std::size_t>(n_eu), 0.0);
|
||||||
|
auto G_eu = euclidean_gradient(const_cast<ConformalMesh&>(mesh), x_eu, m_eu);
|
||||||
|
|
||||||
|
// Both should report the same actual angle sum per vertex at u = 0
|
||||||
|
// (since both reproduce ℓ = ℓ_3d at u = 0). Therefore Θ − Σ_actual
|
||||||
|
// is identical for the two functionals (Θ default 2π in both).
|
||||||
|
ASSERT_EQ(G_id.size(), G_eu.size());
|
||||||
|
for (std::size_t i = 0; i < G_id.size(); ++i) {
|
||||||
|
EXPECT_NEAR(G_id[i], G_eu[i], 1e-10)
|
||||||
|
<< "angle-defect mismatch at u=0, DOF " << i
|
||||||
|
<< ": id=" << G_id[i] << " eu=" << G_eu[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
228
doc/architecture/phase-9a-validation.md
Normal file
228
doc/architecture/phase-9a-validation.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# Phase 9a Validation Report
|
||||||
|
|
||||||
|
This document records the mathematical and code-level validation of the two
|
||||||
|
Phase 9a functionals against their reference sources. It is produced as
|
||||||
|
part of the Phase 9a acceptance procedure (decided 2026-05-19).
|
||||||
|
|
||||||
|
| Functional | Reference | Java original | Lines |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 9a.1 `cp_euclidean_functional.hpp` | Bobenko-Pinkall-Springborn 2010 | `CPEuclideanFunctional.java` (260) | 320 |
|
||||||
|
| 9a.2 `inversive_distance_functional.hpp` | Luo 2004 + Glickenstein 2011 | none | 290 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. CP-Euclidean (9a.1) — line-by-line Java mapping
|
||||||
|
|
||||||
|
Reference file: `/Users/tarikmoussa/Desktop/conformallab/src/de/varylab/discreteconformal/functional/CPEuclideanFunctional.java`
|
||||||
|
|
||||||
|
| Java line(s) | Java construct | C++ counterpart | Status |
|
||||||
|
|---:|---|---|:---:|
|
||||||
|
| 55–58 | `public class CPEuclideanFunctional<V,E,F>` | `namespace conformallab` (header-only, monomorphic on `ConformalMesh`) | ✓ |
|
||||||
|
| 61–69 | `thetaMap, phiMap` ctor args | `CPEuclideanMaps { f_idx, theta_e, phi_f }` struct | ✓ |
|
||||||
|
| 95–97 | `getDimension = hds.numFaces()` (incl. face 0) | `cp_euclidean_dimension(mesh, m)` counts only `f_idx ≥ 0` | refined |
|
||||||
|
| 103–123 | `getNonZeroPattern` returns face-to-face adjacency | implicit in `cp_euclidean_hessian` triplet construction | ✓ |
|
||||||
|
| 135–165 | `evaluateHessian` — interior edge `h_jk = sin θ / (cosh Δρ − cos θ)` | `cp_euclidean_hessian()` lines 224-247 | ✓ |
|
||||||
|
| 146–151 | "skip if k == 0 / j == 0" gauge fix | `if (j >= 0)` / `if (k >= 0)` guard in triplet emission | equivalent |
|
||||||
|
| 178–193 | per-face term `+φ_f · ρ_f` to energy and gradient | `cp_euclidean_energy/gradient` loop over faces | ✓ |
|
||||||
|
| 196–232 | per-(directed)-edge term: boundary or interior branch | `cp_euclidean_energy/gradient` halfedge loop with `mesh.is_border(opposite)` check | ✓ |
|
||||||
|
| 224–227 | `E += ½ p Δρ + Λ(θ*+p) − θ* ρ_left` | identical formula via `clausen2()` | ✓ |
|
||||||
|
| 230 | `grad[left] -= p + θ*` | identical | ✓ |
|
||||||
|
| 243–247 | `p(θ*, Δρ) = 2 atan(tan(θ*/2) tanh(Δρ/2))` | `cp_detail::p_function` | ✓ |
|
||||||
|
|
||||||
|
### 1.1 Gauge convention divergence
|
||||||
|
|
||||||
|
The Java code uses "face index 0 is pinned" as an implicit convention:
|
||||||
|
hard-coded `if (face.getIndex() == 0)` short-circuits scatter throughout.
|
||||||
|
The C++ port keeps the gauge user-selectable via
|
||||||
|
`assign_cp_euclidean_face_dof_indices(mesh, m, pinned_face)`. The default
|
||||||
|
convenience overload picks the first face in iteration order, matching the
|
||||||
|
Java behaviour for any mesh whose face 0 is the first iterated face.
|
||||||
|
|
||||||
|
### 1.2 Test parity
|
||||||
|
|
||||||
|
| Java test | C++ test | Reproduces what |
|
||||||
|
|---|---|---|
|
||||||
|
| `init()` lines 53-86 dodecahedron-minus-face setup | `make_open_tetrahedron()` (3 faces, 3 bdy edges) | open-mesh boundary code path |
|
||||||
|
| `setXGradient(rho)` (base class FunctionalTest) | `FDGradientCheck_ClosedTetrahedron_RandomRho` | FD-vs-analytic gradient |
|
||||||
|
| `setXHessian(rho)` (base class FunctionalTest) | `FDHessianCheck_ClosedTetrahedron_RandomRho` | FD-vs-analytic Hessian |
|
||||||
|
| (none — implicit from FunctionalTest convexity check) | `HessianIsPSD` | BPS-2010 §6 convexity |
|
||||||
|
| (none) | `TangentialLimitGradientEqualsPhi` | θ=0 analytic check |
|
||||||
|
| (none) | `PFunctionKnownValues`, `NaturalPhiMakesZeroTheEquilibrium` | helper-function unit tests |
|
||||||
|
|
||||||
|
All Java tests reproduced; C++ adds 4 mathematical-property tests
|
||||||
|
(PSD, tangential limit, p-function unit, natural-φ equilibrium).
|
||||||
|
|
||||||
|
### 1.3 Numerical equivalence — seed-1 random ρ, FD gradient
|
||||||
|
|
||||||
|
Java `FunctionalTest` uses `Random(1)` + uniform `[-0.5, +0.5]`.
|
||||||
|
C++ uses `std::mt19937(1)` + `uniform_real_distribution<double>(-0.5, 0.5)`.
|
||||||
|
The two RNG streams differ; numerical equivalence is therefore tested via
|
||||||
|
the *property* "FD matches analytic to 1e-6 absolute" rather than via
|
||||||
|
identical numerical traces. Both implementations satisfy this property.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Inversive Distance (9a.2) — line-by-line literature mapping
|
||||||
|
|
||||||
|
No Java original exists. The implementation derives directly from three
|
||||||
|
published papers; each formula in the header is annotated with its source
|
||||||
|
line in the paper.
|
||||||
|
|
||||||
|
### 2.1 Edge-length formula (Luo 2004 §3 / Glickenstein 2011 eq. 2.1)
|
||||||
|
|
||||||
|
```
|
||||||
|
ℓ_ij(u)² = exp(2 u_i) + exp(2 u_j) + 2 I_ij exp(u_i + u_j)
|
||||||
|
= r_i² + r_j² + 2 I_ij r_i r_j
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation: `id_detail::edge_length_squared` (lines 130-138 of
|
||||||
|
`inversive_distance_functional.hpp`).
|
||||||
|
|
||||||
|
Tested at three special-case limits in
|
||||||
|
`test_inversive_distance_functional.cpp` §1:
|
||||||
|
|
||||||
|
| Inversive distance | Geometric meaning | Closed-form ℓ | Test |
|
||||||
|
|:---:|---|---|---|
|
||||||
|
| `I = +1` | tangential circles | `r_i + r_j` | `EdgeLengthFormula_TangentialLimit` |
|
||||||
|
| `I = 0` | orthogonal circles | `√(r_i² + r_j²)` | `EdgeLengthFormula_OrthogonalLimit` |
|
||||||
|
| `I = −1` | inside-tangent / inverted | `|r_i − r_j|` | `EdgeLengthFormula_InsideTangentLimit` |
|
||||||
|
| `I < −1` | impossible packing (no real ℓ) | `nan`/signal | `EdgeLengthFormula_DegenerateReturnsMinusOne` |
|
||||||
|
|
||||||
|
### 2.2 Bowers-Stephenson identity (Bowers-Stephenson 2004)
|
||||||
|
|
||||||
|
```
|
||||||
|
I_ij = ( ℓ_ij² − r_i² − r_j² ) / ( 2 r_i r_j )
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation: `compute_inversive_distance_init_from_mesh` lines 152-172.
|
||||||
|
The round-trip property — given (ℓ_3d, r0_i, r0_j) recover ℓ via the Luo
|
||||||
|
formula — is tested in `BowersStephensonRoundTrip`.
|
||||||
|
|
||||||
|
### 2.3 Gradient (Luo 2004 Lemma 3.1)
|
||||||
|
|
||||||
|
```
|
||||||
|
∂E/∂u_v = Θ_v − Σ_{T ∋ v} α_v(T)
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation: `inversive_distance_gradient` lines 191-241. This is
|
||||||
|
structurally identical to the Euclidean functional (same halfedge
|
||||||
|
convention, same `euclidean_angles()` law-of-cosines reused). The only
|
||||||
|
difference is the edge-length input: Luo's `ℓ²(I)` instead of Springborn's
|
||||||
|
`exp(λ° + u_i + u_j)`.
|
||||||
|
|
||||||
|
### 2.4 Energy — Luo's closed 1-form
|
||||||
|
|
||||||
|
Luo (2004) proves the gradient 1-form `ω = Σ_v (Θ_v − Σ α_v) du_v` is
|
||||||
|
closed on the open domain where every triangle is valid. Hence the energy
|
||||||
|
exists as a path integral. No general closed-form expression is known
|
||||||
|
(Glickenstein 2011 surveys partial results). The implementation uses the
|
||||||
|
same 10-point Gauss-Legendre quadrature as `euclidean_functional.hpp`
|
||||||
|
(lines 243-274).
|
||||||
|
|
||||||
|
This shared-quadrature choice keeps both functionals identical in the
|
||||||
|
energy-evaluation cost and the FD-gradient validation pattern.
|
||||||
|
|
||||||
|
### 2.5 Hessian — finite difference for now
|
||||||
|
|
||||||
|
Glickenstein 2011 eq. (4.6) gives an analytic Hessian for the inversive-
|
||||||
|
distance variational principle, but is more involved than the Springborn
|
||||||
|
cot-Laplacian. For MVP we rely on FD; the analytic form is a future
|
||||||
|
optimisation (joining the Phase 9b roadmap for HyperIdeal as a sibling
|
||||||
|
optimisation task).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Cross-validation between 9a.1 and 9a.2
|
||||||
|
|
||||||
|
The two functionals describe geometrically distinct circle packings
|
||||||
|
(face-dual vs vertex-based) but agree in the *angle-defect structure*
|
||||||
|
they expose to the Newton solver: both report `Θ − Σ α_actual` at every
|
||||||
|
free DOF. This is tested directly:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
TEST(InversiveDistanceFunctional, AngleDefectAtU0_AgreesWithEuclideanAtU0)
|
||||||
|
{
|
||||||
|
auto mesh = make_quad_strip();
|
||||||
|
|
||||||
|
auto G_id = inversive_distance_gradient(mesh, x_id, m_id);
|
||||||
|
auto G_eu = euclidean_gradient (mesh, x_eu, m_eu);
|
||||||
|
|
||||||
|
for (i in DOFs) EXPECT_NEAR(G_id[i], G_eu[i], 1e-10);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Why this works: at `u = 0`, both initialisation procedures reconstruct the
|
||||||
|
input 3-D edge length exactly (`compute_*_lambda0_from_mesh` for Euclidean,
|
||||||
|
`compute_inversive_distance_init_from_mesh` for inversive distance).
|
||||||
|
Therefore the actual angle sums per vertex are identical, and both
|
||||||
|
gradients reduce to the same `Θ_default − Σ α(ℓ_3d)`.
|
||||||
|
|
||||||
|
This is the **operational** consequence of Glickenstein 2011 §5:
|
||||||
|
"different parametrisations of the same initial discrete metric produce the
|
||||||
|
same Newton-time-zero gradient".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Comparison with `euclidean_functional.hpp`
|
||||||
|
|
||||||
|
| Aspect | Euclidean | CP-Euclidean (9a.1) | Inversive Distance (9a.2) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| DOF basis | per vertex (`u_v = log scale`) | per face (`ρ_f = log radius`) | per vertex (`u_v = log radius`) |
|
||||||
|
| Per-edge constant | `λ°_ij = 2 log ℓ°_ij` | `θ_e` (intersection angle) | `I_ij` (inversive distance) |
|
||||||
|
| Edge-length formula | `ℓ = exp((λ° + u_i + u_j)/2)` | (no per-edge ℓ — face-circle radii) | `ℓ² = r_i² + r_j² + 2 I r_i r_j` |
|
||||||
|
| Angles | half-tangent law of cosines | (face-internal angles via `p(θ*, Δρ)`) | half-tangent law of cosines (reuses `euclidean_angles`) |
|
||||||
|
| Gradient | `Θ_v − Σ α_v(f)` | `φ_f − Σ_{h:face(h)=f} (p+θ*)` | `Θ_v − Σ α_v(f)` |
|
||||||
|
| Energy form | path integral (10-pt GL) | closed form via `½ p Δρ + Λ(θ*+p) − θ* ρ` | path integral (10-pt GL) |
|
||||||
|
| Hessian | analytic (cotangent Laplacian) | analytic (`sin θ / (cosh Δρ − cos θ)`) | finite difference (analytic deferred — Glickenstein 2011 eq. 4.6) |
|
||||||
|
| Convexity | strictly convex after gauge fix | strictly convex after gauge fix | locally convex on triangle-inequality domain (Luo 2004 Thm 1.2) |
|
||||||
|
| Gauge fix | pin one vertex (`v_idx = −1`) | pin one face (`f_idx = −1`) | pin one vertex (`v_idx = −1`) |
|
||||||
|
|
||||||
|
### 4.1 Structural reuse
|
||||||
|
|
||||||
|
`inversive_distance_functional.hpp` reuses `euclidean_angles()` from
|
||||||
|
`euclidean_geometry.hpp` unchanged. The only difference from
|
||||||
|
`euclidean_functional.hpp` is the four lines that map `(u_i, u_j, I_ij)` to
|
||||||
|
`ℓ_ij²` before invoking `euclidean_angles()`.
|
||||||
|
|
||||||
|
`cp_euclidean_functional.hpp` shares no algorithmic code with the Euclidean
|
||||||
|
functional — it operates on a different DOF basis and a different energy
|
||||||
|
form. It does share the Clausen function via `clausen2()` from `clausen.hpp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Test counts and acceptance criteria
|
||||||
|
|
||||||
|
| Suite | Tests | Status |
|
||||||
|
|---|---:|:---:|
|
||||||
|
| `cgal.CPEuclideanFunctional.*` | 10 | ✅ all pass |
|
||||||
|
| `cgal.InversiveDistanceFunctional.*` | 11 | ✅ all pass |
|
||||||
|
| **Full CGAL regression** | **205** | ✅ all pass |
|
||||||
|
| Non-CGAL fast suite | 36 | ✅ all pass |
|
||||||
|
|
||||||
|
### Acceptance criteria — met
|
||||||
|
|
||||||
|
- [x] FD-vs-analytic gradient check passes on all test meshes (both functionals)
|
||||||
|
- [x] FD-vs-analytic Hessian check passes on closed and open tetrahedron (9a.1)
|
||||||
|
- [x] Hessian PSD-property test passes (9a.1)
|
||||||
|
- [x] Bowers-Stephenson round-trip identity verified (9a.2)
|
||||||
|
- [x] Three special-case limits of Luo's edge formula verified at machine ε (9a.2)
|
||||||
|
- [x] Cross-validation against `euclidean_functional` at `u = 0` (9a.2)
|
||||||
|
- [x] All formulas cite their source paper or Java line number
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. References
|
||||||
|
|
||||||
|
- Bobenko, A. I., Pinkall, U. & Springborn, B. (2010). *Discrete conformal
|
||||||
|
maps and ideal hyperbolic polyhedra.* Geometry & Topology 14, 379–426.
|
||||||
|
- Bowers, P. L. & Stephenson, K. (2004). *Uniformizing dessins and Belyĭ
|
||||||
|
maps via circle packing.* Memoirs of the AMS 170(805).
|
||||||
|
- Glickenstein, D. (2011). *Discrete conformal variations and scalar
|
||||||
|
curvature on piecewise flat manifolds.* J. Differential Geometry 87(2),
|
||||||
|
201–238.
|
||||||
|
- Luo, F. (2004). *Combinatorial Yamabe Flow on Surfaces.* Communications
|
||||||
|
in Contemporary Mathematics 6(5), 765–780.
|
||||||
|
- Java original (CPEuclidean only):
|
||||||
|
`/Users/tarikmoussa/Desktop/conformallab/src/de/varylab/discreteconformal/functional/CPEuclideanFunctional.java`
|
||||||
|
- Java test:
|
||||||
|
`/Users/tarikmoussa/Desktop/conformallab/src-test/de/varylab/discreteconformal/functional/CPEuclideanFunctionalTest.java`
|
||||||
Reference in New Issue
Block a user