docs: 5-document meeting prep — tutorials + research note + status + architecture

External-reviewer-visit prep package (Springborn-Bobenko PhD alumnus,
2026-05-26).  All five documents target the same audience: a
mathematician who wants to evaluate, extend, or contribute to
conformallab++.  Goal: make the project maximally hackable BEFORE the
meeting.  Code unchanged in this commit — pure documentation.

Files added
───────────

1. **doc/tutorials/block-fd-hessian.md** (460 lines)
   Step-by-step tutorial on the per-face block-FD Hessian pattern
   shipped in Phase 9b (96× speed-up).  Matches the style of
   add-inversive-distance.md.  Covers:
   * The per-face locality lemma (mathematical justification).
   * Cost analysis (full-FD vs block-FD vs analytic).
   * Implementation walkthrough through face_angles_from_local_dofs +
     hyper_ideal_hessian_block_fd.
   * Porting checklist for applying the same pattern to a new
     functional.
   * The four cross-validation criteria.
   * When NOT to use block-FD + upgrade path to Phase 9b-analytic.

2. **doc/tutorials/add-output-uv-map.md** (477 lines)
   Tutorial for the `output_uv_map` named-parameter pattern shipped in
   PR #14.  Covers:
   * The UX problem (two-step pipeline → one-call wrapper).
   * The CGAL named-parameter mechanism + how the entry functions
     wire it (get_parameter + constexpr if).
   * Step-by-step recipe for adding a new named parameter (worked
     example: hypothetical `output_holonomy_map`).
   * The five test patterns for verification.
   * Why CP-Euclidean (face-DOF) and Inversive-Distance (Luo-edge-length)
     do not yet support output_uv_map — what is needed to add them.

3. **doc/math/hyperideal-hessian-derivation.md** (805 lines)
   Research-quality LaTeX-formatted derivation of the analytic
   HyperIdeal Hessian via the Schläfli identity (Phase 9b-analytic
   preparation).  Covers:
   * Schläfli identity (1858/60) — gradient and second-order form.
   * Derivatives of ζ, ζ₁₃, ζ₁₄, ζ₁₅ (all hyper-ideal-to-fully-ideal cases).
   * Chain rule for ∂β_i/∂(b,a) and ∂α_ij/∂(b,a) — case-split on the
     four α_ij branches.
   * Per-face 6×6 block formulas.
   * Acceptance criteria for the future implementation.
   * Implementation outline (Conformal_map header sketch).
   * Appendix A: sign / argument-order pitfalls reading the code.
   * References: Schläfli 1858, Milnor 1982, Vinberg 1993, Cho-Kim 1999,
     Rivin, Glickenstein 2011, Springborn 2020, BPS 2015.

4. **doc/roadmap/porting-status.md** (~250 lines)
   Operational snapshot of "where is each piece of Java math today"
   at v0.9.0.  Sections:
   * 25 000 lines of Java in one table (ported / worth porting /
     intentionally skipped breakdown).
   * Five DCE models — full status matrix with Java port status,
     Hessian type, Newton support, CGAL entry, UV-output capability.
   * Topology + solver infrastructure status.
   * CGAL public API map + known limitations (no chaining, Surface_mesh
     only, submission-readiness gaps).
   * Reverse cross-reference: Java class → C++ port location (or
     "skipped: replaced by …" / "in roadmap: phase X").
   * Things in C++ that the Java original does NOT have (research
     extensions track).
   * "How to use the library today" quickstart.

5. **doc/architecture/locked-vs-flexible.md** (~270 lines)
   12-item architecture-decision review with tier classification
   (🔴 load-bearing / 🟡 semi-fixed / 🟢 opportunistic).  Each item
   includes: locked-since date, cost to change, when to revisit,
   recommended posture for new contributors.  Key insight stated up
   front: "the load-bearing decisions are all good in 2026".  Closes
   with five open questions for the external reviewer — items where
   a second opinion would genuinely help (Phase 9c algorithm choice,
   Phase 10a priorities, analytic-Hessian payoff justification,
   CGAL upstream vs independent distribution, geometry-central
   cross-validation).

Total: ~2 250 lines across five new docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-22 13:38:53 +02:00
parent ff9c9ec11b
commit eb393537f3
5 changed files with 2270 additions and 0 deletions

View File

@@ -0,0 +1,477 @@
# Tutorial: The `output_uv_map` Named Parameter
This tutorial documents the `output_uv_map` named parameter for the
CGAL-style entry functions of conformallab++: how to use it (the "happy
path"), how the CGAL named-parameter machinery threads a user-supplied
property map through to the layout step, and how to extend the same
pattern to add new named parameters to existing or new entry functions.
**Target audience.** A working mathematician using the C++ API who is
comfortable with templates and CGAL property maps but wants a single
authoritative reference for the pattern.
**Prerequisite reading.**
- `code/include/CGAL/Conformal_map/internal/parameters.h` — tag definitions
- `code/include/CGAL/Discrete_conformal_map.h` — wiring in three entries
- `code/tests/cgal/test_cgal_phase8b_lite.cpp` — the OutputUvMap_* tests
- `doc/tutorials/add-inversive-distance.md` — sibling tutorial in this series
---
## 1. What this tutorial is and isn't
### The UX problem `output_uv_map` solves
Before Phase 8b-Lite, computing a flattening required **two separate
calls**: the Newton wrapper, then a manual rebuild of `*_maps` plus a
replay of the wrapper's gauge/DOF choices, then `*_layout()`. Any drift
between the wrapper's gauge vertex and the caller's silently produced
inconsistent UVs.
The `output_uv_map` named parameter collapses this into one call:
```cpp
auto uv = mesh.add_property_map<Vertex_index, K::Point_2>("uv").first;
CGAL::discrete_conformal_map_euclidean(
mesh, CGAL::parameters::output_uv_map(uv));
// uv[v] now holds the flattened UV coordinate of vertex v.
```
This tutorial is **not** a derivation of the Newton solver (see
`add-inversive-distance.md`), not a guide to writing a new layout
routine, and not an introduction to CGAL named parameters in general
(see `<CGAL/Named_function_parameters.h>`).
---
## 2. Mathematical background
The package implements **two mathematically distinct stages**:
### Stage A — Newton solver (variational optimisation)
For the Euclidean DCE functional (SpringbornSchmiesBobenko 2008), the
solver finds the conformal scale factor vector `u ∈ ℝⁿ` that satisfies
```
G(u)_v := Θ_v Σ_{T ∋ v} α_v(T; u) = 0 ∀ v ∈ V \ {pinned}
```
i.e. the actual cone angles match the target curvature `Θ_v` at every
non-pinned vertex. The gradient `G` is the variational derivative of
the convex energy
```
E(u) = ∫_0^1 ⟨ G(t u), u ⟩ dt (path integral on the closed Yamabe 1-form)
```
The output is **purely combinatorial-metric**: the converged `u_v`
determines an intrinsic flat metric `_ij(u) = exp(½(u_i + u_j)) · _ij^(0)`
on the abstract triangulation. **No embedding yet exists.**
### Stage B — Layout (geometric realisation)
The layout step is a separate **trilateration** turning intrinsic edge
lengths into a `Point_2` (or `Point_3`) per vertex:
1. Pick a seed triangle `T₀`; place its three vertices canonically.
2. BFS over the dual graph from `T₀`. Each newly visited triangle has
two vertices already placed; the third is the unique solution of
two distance constraints (intersection of two circles).
3. The BFS uses a **priority queue keyed on BFS depth**
(`euclidean_layout` in `code/include/layout.hpp`) to avoid the
numerical drift of pure DFS on long thin meshes.
Spherical / hyperbolic variants substitute spherical-cap or Poincaré-
disk trilateration respectively.
Stage A is convex optimisation in ℝⁿ; Stage B is deterministic
realisation with no degrees of freedom. The named parameter chains them.
---
## 3. The user-facing API
### Happy path
```cpp
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Discrete_conformal_map.h>
using K = CGAL::Simple_cartesian<double>;
using Mesh = CGAL::Surface_mesh<K::Point_3>;
using Vertex_i = Mesh::Vertex_index;
Mesh mesh = /* load triangle mesh */;
// 1. Allocate a writable property map on the mesh.
auto uv = mesh.add_property_map<Vertex_i, K::Point_2>(
"v:uv", K::Point_2(0, 0)).first;
// 2. Ask the wrapper to populate it.
auto res = CGAL::discrete_conformal_map_euclidean(
mesh,
CGAL::parameters::output_uv_map(uv));
// 3. Use the UVs.
if (res.converged) {
for (auto v : mesh.vertices())
std::cout << v.idx() << ": " << uv[v] << '\n';
}
```
### Supported entries
| Entry | Value type per vertex |
|--------------------------------------|--------------------------------------------------|
| `discrete_conformal_map_euclidean` | `Point_2` — UV in ℝ² |
| `discrete_conformal_map_spherical` | `Point_3` — point on S² ⊂ ℝ³ |
| `discrete_conformal_map_hyper_ideal` | `Point_2` — point in the Poincaré disk (|p|≤1) |
### NOT supported (yet)
| Entry | Reason |
|----------------------------------------|-------------------------------------------------------------|
| `discrete_circle_packing_euclidean` | Face-based parametrisation — UV is per-face, not per-vertex |
| `discrete_inversive_distance_map` | Requires `inversive_distance_layout()` using Luo's `ℓ²` formula — not yet written |
For the circle-packing entry a separate `output_circle_map` parameter
(face → (centre, radius)) is the natural extension; for the inversive-
distance entry the layout primitive must be written first. See §7.
### Optional companion: `normalise_layout`
```cpp
CGAL::parameters::normalise_layout(true)
```
When the layout writes into `output_uv_map`, this flag triggers a
canonical post-processing pass:
- Euclidean: PCA — translate centroid to origin, rotate so the principal
axis is along `x`.
- Spherical: Rodrigues rotation so that the centroid is at the north pole.
- Hyperbolic: Möbius centring of the Poincaré disk.
Default: `false`. Has no effect unless `output_uv_map` is also passed.
### Current limitation: no chaining
The CGAL machinery supports chaining named parameters via `.member()`
syntax for the standard CGAL tags (e.g. `geom_traits`, `vertex_point_map`).
**Package-local tags do not yet have member-function counterparts**, so
```cpp
// DOES NOT COMPILE:
CGAL::parameters::output_uv_map(uv).normalise_layout(true);
```
is currently rejected. Pass one parameter per call instead. This is
the same limitation noted in `test_cgal_phase8b_lite.cpp`:
```cpp
// Named-parameter chaining (`a.b().c()`) is not currently supported on
// the package-local tags; pass one parameter per call instead.
```
Adding member-function chaining is a separate Phase 8b extension —
requires writing a derived `Named_function_parameters` subclass that
exposes `.output_uv_map(...)`, `.normalise_layout(...)`, etc.
---
## 4. Named-parameter mechanism — how it works
CGAL's named parameters are a compile-time dispatch trick. Three pieces:
### 4.1 The tag (in `internal_np`)
```cpp
namespace CGAL::Conformal_map::internal_np {
enum output_uv_map_t { output_uv_map };
}
```
A single-value `enum` is the lightest possible type-level marker. The
value `output_uv_map` is what callers will pass as the *key*; the type
`output_uv_map_t` is what `get_parameter` will use for the lookup.
### 4.2 The user-facing helper (in `CGAL::parameters`)
```cpp
template <typename PropertyMap>
auto output_uv_map(const PropertyMap& pmap) {
return CGAL::Named_function_parameters<
PropertyMap,
Conformal_map::internal_np::output_uv_map_t,
CGAL::internal_np::No_property // no "next" parameter in chain
>(pmap);
}
```
This wraps the property map in a `Named_function_parameters` object
tagged with `output_uv_map_t`. The third template argument is the
"previous" link in a chain; `No_property` means this is a singleton
pack. (Chaining would substitute the previous pack's type here.)
### 4.3 The internal lookup (inside the entry function)
The full read-and-act pattern from `Discrete_conformal_map.h`:
```cpp
// Step 1 — try to extract the parameter.
auto uv_param = parameters::get_parameter(
np, Conformal_map::internal_np::output_uv_map);
// Step 2 — at compile time, did we find one?
constexpr bool has_uv = !std::is_same_v<
decltype(uv_param), internal_np::Param_not_found>;
// Step 3 — conditionally do the extra work.
if constexpr (has_uv) {
if (nr.converged) {
auto layout = ::conformallab::euclidean_layout(mesh, nr.x, maps);
const bool do_norm = parameters::choose_parameter(
parameters::get_parameter(np, Conformal_map::internal_np::normalise_layout),
false);
if (do_norm) ::conformallab::normalise_euclidean(layout);
for (auto v : mesh.vertices()) {
const auto& uv = layout.uv[v.idx()];
put(uv_param, v,
typename Traits::Kernel::Point_2(uv.x(), uv.y()));
}
}
}
```
Key points:
- `get_parameter` returns either the wrapped value or a sentinel
`Param_not_found`. The check is `std::is_same_v<…, Param_not_found>`.
- `choose_parameter(get_parameter(np, tag), default_value)` is the
one-liner form for scalar parameters with a default.
- `constexpr if` ensures that if the user did not pass `output_uv_map`,
the layout call is **not even instantiated** — no runtime cost, and
no need for the layout routine to be callable on the wrapper's
signature (e.g. it can require a Point_2 traits type that the user
hasn't supplied).
- The layout step is gated on `nr.converged` for a reason: trilateration
on a non-converged `u` produces garbage edge lengths and can throw
on degenerate triangles. The current contract is **layout iff Newton
converged**; on non-convergence the pmap is left at the value the
caller initialised it with.
The same three-step pattern appears in `discrete_conformal_map_spherical`
(writing `Point_3`) and `discrete_conformal_map_hyper_ideal` (writing
`Point_2` in the Poincaré disk).
---
## 5. How to extend the pattern
Recipe for adding a new package-local named parameter — running example
`output_holonomy_map` (a per-edge map carrying the Möbius/translation
holonomy data that some downstream algorithms consume).
### Step 1 — Add the tag in `internal/parameters.h`
```cpp
namespace CGAL::Conformal_map::internal_np {
/// Property-map: edge_descriptor → 2x2 matrix of holonomy data.
/// Default: not populated.
enum output_holonomy_map_t { output_holonomy_map };
}
```
Two-line block. Use a meaningful Doxygen `\internal` comment because
this is the canonical contract for the parameter's semantics.
### Step 2 — Add the user-facing helper in `parameters.h`
```cpp
namespace CGAL::parameters {
/// `output_holonomy_map(pmap)` — write per-edge holonomy matrices
/// into `pmap`. Only meaningful for closed surfaces with non-trivial
/// π₁; ignored on simply-connected meshes.
template <typename PropertyMap>
auto output_holonomy_map(const PropertyMap& pmap) {
return CGAL::Named_function_parameters<
PropertyMap,
Conformal_map::internal_np::output_holonomy_map_t,
CGAL::internal_np::No_property
>(pmap);
}
} // namespace CGAL::parameters
```
A Doxygen comment is **mandatory** here — this is the public surface.
### Step 3 — Use `get_parameter + constexpr if` in the entry function
```cpp
auto hol_param = parameters::get_parameter(
np, Conformal_map::internal_np::output_holonomy_map);
constexpr bool has_hol = !std::is_same_v<
decltype(hol_param), internal_np::Param_not_found>;
if constexpr (has_hol) {
auto H = ::conformallab::compute_holonomy(mesh, nr.x, maps);
for (auto e : mesh.edges())
put(hol_param, e, H[e.idx()]);
}
```
### Step 4 — Add tests
At minimum two:
1. **Takes-effect test.** Pass the parameter; verify the side effect
actually happens (pmap is populated, values are non-default).
2. **Sanity-when-absent test.** Call the wrapper without the parameter;
verify the call still succeeds and produces the expected base result.
Both patterns are exemplified in `test_cgal_phase8b_lite.cpp` (next
section).
### Optional Step 5 — Companion flag
If your new parameter has a Boolean tweak (analogous to
`normalise_layout`), follow exactly the same recipe — `enum X_t { X }`
+ helper + `choose_parameter(..., default)` at the read site.
---
## 6. Tests — walkthrough of `test_cgal_phase8b_lite.cpp`
The OutputUvMap_* tests live at the bottom of the file. Each codifies
one acceptance criterion.
### 6.1 `OutputUvMap_Euclidean_PopulatesPmap`
Two assertions: (a) every UV is finite, (b) at least one vertex has
moved off the origin. Test (b) rules out the false-positive where the
layout silently returned the pmap's default at every vertex.
### 6.2 `OutputUvMap_Spherical_PopulatesXyz`
Geometric round-trip: every output point must lie on the unit sphere.
```cpp
const double r = std::sqrt(p.x()*p.x() + p.y()*p.y() + p.z()*p.z());
EXPECT_NEAR(r, 1.0, 1e-6);
```
The spherical analogue of 6.1(b): the layout is correct iff the
realisation lands on S².
### 6.3 `OutputUvMap_HyperIdeal_PointsInPoincareDisk`
The natural Θ/θ targets do not always reach Newton equilibrium inside
the default `max_iterations(200)`. The test branches:
```cpp
if (res.converged) {
for (auto v : mesh.vertices()) {
const double r2 = p.x()*p.x() + p.y()*p.y();
EXPECT_LE(r2, 1.0 + 1e-6);
}
}
// else: layout was skipped by the wrapper's `if (nr.converged)` guard.
```
Documenting the wrapper contract ("layout iff converged") via the
test keeps it resilient to PRNG-sensitive Newton paths.
### 6.4 `OutputUvMap_Absent_DoesNotRunLayout`
Sanity test: omitting `output_uv_map` must not change the existing
behaviour. Catches the regression where adding the new `constexpr if`
perturbs the wrapper's result type or consumes extra work.
### 6.5 `OutputUvMap_NormaliseLayout_TakesEffect`
Because chaining is not yet implemented (§3), this exercises the toggle
indirectly: it runs the wrapper twice into two separate pmaps and
verifies both populate finite values. When chaining lands, upgrade this
to actually pass `normalise_layout(true)` on the second call and compare
UV bounding boxes (the normalised one must be axis-aligned and
centroid-zero).
---
## 7. Adding output to the two missing modes
### 7.1 CP-Euclidean (face-based) — proposed `output_circle_map`
The circle-packing entry parametrises **faces**: each face `f` has a
radius `ρ_f`. The natural per-face output is `(centre, radius)`:
```cpp
auto circles = mesh.add_property_map<Face_index,
std::pair<K::Point_2, double>>("f:circle", ...).first;
CGAL::discrete_circle_packing_euclidean(
mesh, CGAL::parameters::output_circle_map(circles));
```
Effort: **12 days**. The face-graph BFS-packing is Stephenson's
`circlepack` algorithm; radii come from Newton, new work is the
geometric loop plus the §5 plumbing.
### 7.2 Inversive-Distance — needs new layout routine first
The inversive-distance functional yields edge lengths
```
_ij(u)² = r_i² + r_j² + 2 I_ij r_i r_j (Luo 2004 §3)
```
with `r_i = exp(u_i)`. Required work: write
`inversive_distance_layout()` in `code/include/layout.hpp` mirroring
`euclidean_layout()` but consuming the above `_ij(u)` instead of
`exp(½(u_i + u_j)) · _ij^(0)`. Then wire the parameter through
`discrete_inversive_distance_map` exactly as §4.3 shows.
Effort: **~3 days**. Status: **TBD**.
---
## 8. Acceptance checklist for a new named parameter
Use this checklist when extending the package:
- [ ] **Tag declared** in `code/include/CGAL/Conformal_map/internal/parameters.h`
(`enum X_t { X };`) with a `\internal` Doxygen comment specifying
the property-map key/value types and default behaviour.
- [ ] **User-facing helper** in the same file's `CGAL::parameters`
namespace, with a public Doxygen comment.
- [ ] **`get_parameter + constexpr if` pattern** in every entry function
that supports the parameter (do not leak the tag into entries that
ignore it).
- [ ] **At least two tests** in `test_cgal_phase8b_lite.cpp` (or a new
test file if introducing a new functional): a
*parameter-takes-effect* test and a *sanity-when-absent* test.
- [ ] **Limitation documented.** If the parameter is mode-specific
(e.g. only meaningful for closed meshes) or cannot be chained
with existing helpers, say so in the user-facing Doxygen and in
a comment near the read site.
- [ ] **No silent regressions.** If the parameter changes the wrapper's
observable behaviour even when absent (e.g. via a new default),
add a regression test pinning the old default.
---
## 9. Further reading
- Springborn, Schmies, Bobenko (2008). *Conformal equivalence of
triangle meshes.* SIGGRAPH. — Euclidean DCE functional.
- Bobenko, Pinkall, Springborn (2015). *Discrete conformal maps and
ideal hyperbolic polyhedra.* Geom. Topol. — hyper-ideal variant.
- Glickenstein (2011). *Discrete conformal variations and scalar
curvature on piecewise flat manifolds.* JDG 87(2).
- `<CGAL/Named_function_parameters.h>` — upstream machinery.