docs: Doxygen-API + Validierungsprotokoll + Porting-Tutorial
Für einen Mathematiker der unabhängig validieren und eigene Forschung
einbringen möchte.
Doxygen-Kommentare (code/include/):
newton_solver.hpp — newton_euclidean(), newton_spherical(), newton_hyper_ideal()
je mit \param, \return, \note, \see inkl. mathematischer Begründung
(Konvexität, Vorzeichenkonvention, SparseQR-Fallback-Erklärung)
layout.hpp — euclidean_layout(), spherical_layout(), hyper_ideal_layout()
mit vollständiger Parameter-Doku, halfedge_uv-Semantik, Poincaré-Disk-Note
Neues Dokument:
doc/math/validation-protocol.md
7 reproduzierbare Checks mit konkreten Befehlen und erwartetem Output:
0. 170 Tests, 1 Skip
1. Gauss–Bonnet exakt (1e-10)
2. FD-Gradientencheck < 1e-6 für alle 3 Geometrien
3. Newton-Konvergenz < 50 Iterationen
4. τ ∈ SL(2,ℤ)-Fundamentaldomäne (3 Invarianten)
5. Möbius-Arithmetik (Inverse, Compose, from_three)
6. End-to-End-Pipeline
7. Manueller τ-Check für torus_4x4.off (Codebeispiel)
Neues Tutorial:
doc/tutorials/add-inversive-distance.md
Vollständiger Step-by-Step-Port von Phase 9a (Luo 2004):
Header anlegen, Energie/Gradient implementieren, FD-Check,
Newton-Wrapper, CMakeLists, Java-Referenzvergleich, Checkliste.
doc/getting-started.md:
Abschnitt "Known issues": macOS-Finder-Duplikate (rm-Befehl),
Warnung "First build 30–90s" (Tarball-Extraktion)
README.md:
Zwei neue Links in der Dokumentationstabelle (validation-protocol,
tutorial)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,30 @@ After a full build (`-DWITH_CGAL=ON`):
|
||||
|
||||
---
|
||||
|
||||
## Known issues
|
||||
|
||||
### macOS Finder duplicates
|
||||
|
||||
macOS Finder sometimes creates duplicate files named `foo 2.hpp` when copying
|
||||
the repository. These cause confusing compile errors ("redefinition of …").
|
||||
|
||||
**Fix (run once from the repo root):**
|
||||
```bash
|
||||
find code/include -name "* 2.*" -delete
|
||||
find code/include -name "*\ 2.*" -delete
|
||||
```
|
||||
|
||||
Files without a ` 2` suffix are always canonical — the duplicates are safe to delete.
|
||||
|
||||
### First build is slow
|
||||
|
||||
The first CMake configure extracts four tarballs (Eigen 3.4, CGAL 6.1.1,
|
||||
libigl 2.6, GLFW 3.4) and downloads GTest via `FetchContent`.
|
||||
Allow **30–90 seconds** for the first configure. Subsequent builds are fast
|
||||
(< 10 s incremental).
|
||||
|
||||
---
|
||||
|
||||
## Rebuilding the CI Docker image
|
||||
|
||||
The CI runner is a self-hosted Raspberry Pi (ARM64). After changes to
|
||||
|
||||
213
doc/math/validation-protocol.md
Normal file
213
doc/math/validation-protocol.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Validation Protocol
|
||||
|
||||
Concrete, reproducible steps to verify the mathematical correctness of
|
||||
conformallab++. Every check below has a deterministic expected outcome.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
cmake -S code -B build -DWITH_CGAL=ON -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --target conformallab_cgal_tests -j$(nproc)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Check 0 — All tests pass
|
||||
|
||||
```bash
|
||||
ctest --test-dir build -R "^cgal\." --output-on-failure
|
||||
```
|
||||
|
||||
**Expected output (last lines):**
|
||||
```
|
||||
100% tests passed, 0 tests failed out of 170
|
||||
|
||||
The following tests did not run:
|
||||
206 - cgal.HomologyGenerators.Genus2_FourGeneratorPaths_BLOCKED (Skipped)
|
||||
```
|
||||
|
||||
If any test fails, stop — the implementation is broken.
|
||||
|
||||
---
|
||||
|
||||
## Check 1 — Gauss–Bonnet (topological identity, error < 1e-10)
|
||||
|
||||
```bash
|
||||
./build/conformallab_cgal_tests --gtest_filter="GaussBonnet.*" -v
|
||||
```
|
||||
|
||||
**Expected:** all 8 tests `[ PASSED ]`
|
||||
|
||||
What is verified: for each test mesh (tetrahedron χ=2, torus χ=0, open mesh χ=1):
|
||||
|
||||
```
|
||||
Σᵥ (2π − Θᵥ) = 2π · χ(M) ± 1e-10
|
||||
```
|
||||
|
||||
This is a **pure topology check** — it fails only if vertex/face counts or
|
||||
property-map assignments are wrong. It does not depend on the Newton solver.
|
||||
|
||||
---
|
||||
|
||||
## Check 2 — Euclidean gradient consistency (FD vs. analytic, error < 1e-6)
|
||||
|
||||
```bash
|
||||
./build/conformallab_cgal_tests --gtest_filter="EuclideanFunctional.GradientCheck*" -v
|
||||
```
|
||||
|
||||
**Expected:** all `GradientCheck_*` tests `[ PASSED ]`
|
||||
|
||||
What is verified: for ε = 1e-5,
|
||||
|
||||
```
|
||||
|G(u)ᵢ − (E(u + εeᵢ) − E(u − εeᵢ)) / (2ε)| < 1e-6
|
||||
```
|
||||
|
||||
This check **proves that the energy and its gradient are mathematically consistent**.
|
||||
A failing FD check means Newton will converge to the wrong point — it is the most
|
||||
important correctness check for any new functional.
|
||||
|
||||
Also run for Spherical and HyperIdeal:
|
||||
```bash
|
||||
./build/conformallab_cgal_tests --gtest_filter="SphericalFunctional.GradientCheck*" -v
|
||||
./build/conformallab_cgal_tests --gtest_filter="HyperIdealFunctional.GradientCheck*" -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Check 3 — Newton convergence on canonical test meshes
|
||||
|
||||
```bash
|
||||
./build/conformallab_cgal_tests --gtest_filter="NewtonSolver.*" -v
|
||||
```
|
||||
|
||||
**Expected:** all 11 tests `[ PASSED ]`
|
||||
|
||||
Each test verifies:
|
||||
- `res.converged == true`
|
||||
- `res.grad_inf_norm < 1e-8`
|
||||
- `res.iterations < 50` (typically 5–20 for the small test meshes)
|
||||
|
||||
---
|
||||
|
||||
## Check 4 — Period matrix: SL(2,ℤ)-reduction invariants
|
||||
|
||||
```bash
|
||||
./build/conformallab_cgal_tests --gtest_filter="PeriodMatrix.*" -v
|
||||
```
|
||||
|
||||
**Expected:** all 7 tests `[ PASSED ]`
|
||||
|
||||
The three mathematical invariants checked for **any** genus-1 output τ:
|
||||
|
||||
| Property | Condition | Why |
|
||||
|---|---|---|
|
||||
| Upper half-plane | `Im(τ) > 0` | τ encodes a positive-area lattice |
|
||||
| Outside unit disk | `|τ| ≥ 1 − 1e-10` | SL(2,ℤ) reduction step |
|
||||
| Vertical strip | `|Re(τ)| ≤ 0.5 + 1e-10` | SL(2,ℤ) reduction step |
|
||||
|
||||
These hold for **any** well-formed genus-1 mesh — they are topology, not geometry.
|
||||
|
||||
---
|
||||
|
||||
## Check 5 — Möbius arithmetic (complex analysis correctness)
|
||||
|
||||
```bash
|
||||
./build/conformallab_cgal_tests --gtest_filter="MobiusMap.*" -v
|
||||
```
|
||||
|
||||
**Expected:** all 8 tests `[ PASSED ]`
|
||||
|
||||
What is verified:
|
||||
- `T ∘ T⁻¹ = Id` (inverse is correct)
|
||||
- `(T₁ ∘ T₂)(z) = T₁(T₂(z))` (composition is associative)
|
||||
- `from_three(z₁, z₂, z₃)` maps z₁→0, z₂→1, z₃→∞ (unique Möbius transformation)
|
||||
|
||||
A bug here would corrupt **all** hyperbolic holonomy computation.
|
||||
|
||||
---
|
||||
|
||||
## Check 6 — End-to-end pipeline (build + solve + layout)
|
||||
|
||||
```bash
|
||||
./build/conformallab_cgal_tests --gtest_filter="Pipeline.*" -v
|
||||
```
|
||||
|
||||
**Expected:** all 5 tests `[ PASSED ]`
|
||||
|
||||
What is verified: starting from a mesh file, the full pipeline
|
||||
(setup → Gauss-Bonnet → Newton → layout → serialise → reload)
|
||||
produces a consistent result.
|
||||
|
||||
---
|
||||
|
||||
## Check 7 — Manual torus τ verification
|
||||
|
||||
This check requires adding a small program (or modifying an existing test).
|
||||
It validates that `torus_4x4.off` produces τ in the fundamental domain:
|
||||
|
||||
```cpp
|
||||
#include "conformal_mesh.hpp"
|
||||
#include "euclidean_functional.hpp"
|
||||
#include "newton_solver.hpp"
|
||||
#include "cut_graph.hpp"
|
||||
#include "layout.hpp"
|
||||
#include "period_matrix.hpp"
|
||||
#include "mesh_io.hpp"
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
conformallab::ConformalMesh mesh;
|
||||
conformallab::load_mesh(mesh, "code/data/off/torus_4x4.off");
|
||||
|
||||
auto maps = conformallab::setup_euclidean_maps(mesh);
|
||||
conformallab::compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||||
conformallab::enforce_gauss_bonnet(mesh, maps);
|
||||
|
||||
auto res = conformallab::newton_euclidean(mesh, std::vector<double>(maps.n_dof, 0.0), maps);
|
||||
std::cout << "Converged: " << res.converged
|
||||
<< " iterations: " << res.iterations
|
||||
<< " |G|∞: " << res.grad_inf_norm << "\n";
|
||||
|
||||
auto cg = conformallab::compute_cut_graph(mesh);
|
||||
conformallab::HolonomyData hol;
|
||||
conformallab::euclidean_layout(mesh, res.x, maps, &cg, &hol, true);
|
||||
auto pd = conformallab::compute_period_matrix(hol);
|
||||
|
||||
std::cout << "τ = " << pd.tau_reduced.real()
|
||||
<< " + " << pd.tau_reduced.imag() << "i\n";
|
||||
std::cout << "|τ| = " << std::abs(pd.tau_reduced) << "\n";
|
||||
std::cout << "|Re(τ)| = " << std::abs(pd.tau_reduced.real()) << "\n";
|
||||
}
|
||||
```
|
||||
|
||||
**Expected output (torus_4x4.off, R=2, r=1 torus of revolution):**
|
||||
```
|
||||
Converged: 1 iterations: <30 |G|∞: <1e-8
|
||||
τ = [small] + [positive]i (Re close to 0 by 4-fold symmetry)
|
||||
|τ| ≥ 1.0 (fundamental domain)
|
||||
|Re(τ)| ≤ 0.5 (fundamental domain)
|
||||
```
|
||||
|
||||
The exact value of Im(τ) depends on the 3D embedding (R=2, r=1 gives unequal
|
||||
inner/outer edge lengths). Use `torus_8x8.off` for a finer approximation.
|
||||
|
||||
---
|
||||
|
||||
## Summary checklist
|
||||
|
||||
```
|
||||
[ ] Check 0: 170 tests pass, 1 skip
|
||||
[ ] Check 1: Gauss–Bonnet exact (1e-10)
|
||||
[ ] Check 2: FD gradient < 1e-6 for all 3 geometries
|
||||
[ ] Check 3: Newton convergence < 50 iterations
|
||||
[ ] Check 4: τ in SL(2,ℤ) fundamental domain
|
||||
[ ] Check 5: Möbius arithmetic (inverse, compose, from_three)
|
||||
[ ] Check 6: End-to-end pipeline
|
||||
[ ] Check 7: Torus τ in upper half-plane with correct symmetry
|
||||
```
|
||||
|
||||
All checks are deterministic and do not depend on random initialization or
|
||||
floating-point non-determinism beyond standard IEEE-754.
|
||||
202
doc/tutorials/add-inversive-distance.md
Normal file
202
doc/tutorials/add-inversive-distance.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Tutorial: Porting the Inversive-Distance Functional (Phase 9a)
|
||||
|
||||
This is a complete, step-by-step example of how to add a new functional
|
||||
to conformallab++. It ports `InversiveDistanceFunctional.java` from the
|
||||
Java reference implementation (Luo 2004).
|
||||
|
||||
**Prerequisite:** Read [doc/api/extending.md](../api/extending.md) first
|
||||
for the general pattern. This tutorial fills in every detail for one
|
||||
specific case.
|
||||
|
||||
---
|
||||
|
||||
## Mathematical background
|
||||
|
||||
Inversive distance (Luo 2004) uses a different edge-length update formula:
|
||||
|
||||
```
|
||||
Given inversive distances I_{ij} ∈ ℝ for each edge {i,j}:
|
||||
|
||||
cosh(l̃_{ij}) = I_{ij} · cosh((u_i + u_j) / 2)
|
||||
+ (cosh²((u_i - u_j) / 2) - 1) · ...
|
||||
```
|
||||
|
||||
For the Euclidean version the angle formula is the same law of cosines,
|
||||
but the log-lengths Λ̃ are computed differently from the u-vector.
|
||||
|
||||
**Reference:** Luo, F. (2004). *Combinatorial Yamabe Flow on Surfaces*.
|
||||
Communications in Contemporary Mathematics, 6(5), 765–780.
|
||||
|
||||
**Java source:** `de.varylab.discreteconformal.functional.InversiveDistanceFunctional`
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Create the header
|
||||
|
||||
```bash
|
||||
cp code/include/euclidean_functional.hpp \
|
||||
code/include/inversive_distance_functional.hpp
|
||||
```
|
||||
|
||||
Edit the new file:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
// inversive_distance_functional.hpp
|
||||
//
|
||||
// Phase 9a — Inversive-distance discrete conformal functional (Luo 2004).
|
||||
// Ported from de.varylab.discreteconformal.functional.InversiveDistanceFunctional.
|
||||
//
|
||||
// Usage: identical to euclidean_functional.hpp — replace lambda0 with
|
||||
// inversive_distance0 (the initial inversive distances per edge).
|
||||
|
||||
#include "conformal_mesh.hpp"
|
||||
#include <Eigen/Dense>
|
||||
#include <cmath>
|
||||
|
||||
namespace conformallab {
|
||||
|
||||
struct InversiveDistanceMaps {
|
||||
CGAL::Surface_mesh<CGAL::Simple_cartesian<double>::Point_3>
|
||||
::Property_map<Edge_index, double> inv_dist0; ///< I_{ij} per edge
|
||||
CGAL::Surface_mesh<CGAL::Simple_cartesian<double>::Point_3>
|
||||
::Property_map<Vertex_index, double> theta_v; ///< target corner angles Θ_v
|
||||
CGAL::Surface_mesh<CGAL::Simple_cartesian<double>::Point_3>
|
||||
::Property_map<Vertex_index, int> v_idx; ///< DOF index (-1 = pinned)
|
||||
};
|
||||
|
||||
// ... (follow euclidean_functional.hpp exactly, replacing lambda0 with inv_dist0
|
||||
// and the angle formula with the Luo (2004) version)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Implement the energy, gradient, and angle formula
|
||||
|
||||
The Euclidean angle formula uses the law of cosines on edge lengths
|
||||
derived from log-lengths. For inversive distance, the edge lengths
|
||||
are derived from inversive distances I_{ij} and the u-vector:
|
||||
|
||||
```cpp
|
||||
// Euclidean (reference):
|
||||
// lambda_ij = lambda0_ij + u_i + u_j
|
||||
// l_ij = exp(lambda_ij / 2)
|
||||
|
||||
// Inversive distance (Luo 2004):
|
||||
// cosh(l̃_ij) = I_ij * cosh((u_i + u_j) / 2) [simplified form]
|
||||
// l̃_ij = acosh(I_ij * cosh((u_i + u_j) / 2))
|
||||
```
|
||||
|
||||
Then the **corner angle** at vertex k opposite edge {i,j} in triangle {i,j,k}
|
||||
is computed by the standard law of cosines from l̃_{ij}, l̃_{jk}, l̃_{ki}.
|
||||
|
||||
The **gradient** is the same as Euclidean:
|
||||
|
||||
```cpp
|
||||
G_v = Σ_{faces containing v} alpha_v(face, u) − Theta_v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Write the gradient-check test
|
||||
|
||||
Create `code/tests/cgal/test_inversive_distance.cpp`:
|
||||
|
||||
```cpp
|
||||
#include "inversive_distance_functional.hpp"
|
||||
#include "mesh_factory.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
// Finite-difference gradient check: copy the pattern from
|
||||
// test_euclidean_functional.cpp :: EuclideanFunctional.GradientCheck_Triangle
|
||||
TEST(InversiveDistance, GradientCheck_Triangle)
|
||||
{
|
||||
// Build a single equilateral triangle (open mesh, no boundary issues).
|
||||
ConformalMesh mesh = MeshFactory::make_open_mesh(MeshFactory::Kind::triangle);
|
||||
InversiveDistanceMaps maps = setup_inversive_distance_maps(mesh);
|
||||
compute_inversive_distance0_from_mesh(mesh, maps); // I_ij from 3D edge lengths
|
||||
|
||||
const int n = /* count free DOFs */;
|
||||
std::vector<double> x0(n, 0.0);
|
||||
constexpr double eps = 1e-5;
|
||||
|
||||
auto G = inversive_distance_gradient(mesh, x0, maps);
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
auto xp = x0; xp[i] += eps;
|
||||
auto xm = x0; xm[i] -= eps;
|
||||
double Ep = inversive_distance_energy(mesh, xp, maps);
|
||||
double Em = inversive_distance_energy(mesh, xm, maps);
|
||||
double fd = (Ep - Em) / (2.0 * eps);
|
||||
EXPECT_NEAR(G[i], fd, 1e-6) << "gradient mismatch at DOF " << i;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run with:
|
||||
|
||||
```bash
|
||||
./build/conformallab_cgal_tests --gtest_filter="InversiveDistance.*"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Register in CMakeLists
|
||||
|
||||
Add to `code/tests/cgal/CMakeLists.txt` inside the `add_executable` block:
|
||||
|
||||
```cmake
|
||||
# ── Phase 9a: Inversive-distance functional ────────────────────────────────
|
||||
test_inversive_distance.cpp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Add a Newton wrapper
|
||||
|
||||
In `code/include/newton_solver.hpp` add:
|
||||
|
||||
```cpp
|
||||
/// Solve the inversive-distance conformal problem.
|
||||
/// \see newton_euclidean() — identical structure.
|
||||
inline NewtonResult newton_inversive_distance(
|
||||
ConformalMesh& mesh,
|
||||
std::vector<double> x0,
|
||||
const InversiveDistanceMaps& m,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200)
|
||||
{
|
||||
// Copy newton_euclidean() exactly, replacing euclidean_* with
|
||||
// inversive_distance_*. The Hessian is PSD (Luo 2004, Thm. 1),
|
||||
// so SimplicialLDLT with SparseQR fallback applies directly.
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Verify against Java output
|
||||
|
||||
The Java library outputs text results for a triangulated torus. To compare:
|
||||
|
||||
1. Run Java ConformalLab on the same OFF mesh with Inversive-Distance mode.
|
||||
2. Record the final gradient norm and angle sums.
|
||||
3. Run the C++ equivalent and check:
|
||||
|
||||
```cpp
|
||||
EXPECT_LT(res.grad_inf_norm, 1e-8);
|
||||
EXPECT_LT(res.iterations, 50); // inversive-distance converges faster than hyper-ideal
|
||||
```
|
||||
|
||||
**Java reference:** `InversiveDistanceFunctionalTest.java` in `de.varylab.discreteconformal.test`
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] `code/include/inversive_distance_functional.hpp` compiles
|
||||
- [ ] `GradientCheck_Triangle` passes
|
||||
- [ ] `GradientCheck_OpenMesh` passes (copy from Euclidean tests)
|
||||
- [ ] Newton converges on `torus_4x4.off`
|
||||
- [ ] Result matches Java output (gradient norm < 1e-8 on same mesh)
|
||||
- [ ] Registered in `CMakeLists.txt`
|
||||
- [ ] `doc/roadmap/java-parity.md` updated: Phase 9a → ✅
|
||||
Reference in New Issue
Block a user