Files
ConformalLabpp/doc/architecture/design-decisions.md
Tarik Moussa ca936b7652
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m0s
API Docs / doc-build (pull_request) Successful in 53s
Markdown link check / check (pull_request) Successful in 54s
C++ Tests / test-cgal (pull_request) Has been skipped
C++ Tests / quality-gates (pull_request) Failing after 2m14s
docs: note high-precision requirement for Phase 9c/10 uniformization
The Java uniformization classes (FundamentalPolygonUtility, CanonicalFormUtility)
rely on the *Big arbitrary-precision geometry (MathContext(50)) because products
of hyperbolic isometry generators grow exponentially and double fails to verify
the group relation ∏gᵢ = Id. Record this planning-relevant prerequisite and
resolve the contradiction in java-parity.md, which previously listed *Big as
permanently out of scope.

- CLAUDE.md: † note on the Phase-9 not-yet-ported table
- java-parity.md: *Big exception (localized high-precision substrate for 9c/10)
- phases.md: precision prerequisite as 9c sub-task + effort estimate
- design-decisions.md: new "Scalar type: double, with one localized exception"

Core flattening (Newton/energy/Eigen solver) stays double; the substrate
(cpp_dec_float_50 / mpreal) is localized to the uniformization module only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 07:41:23 +02:00

5.7 KiB
Raw Blame History

Key Design Decisions

Rationale for the architectural choices that distinguish conformallab++ from the Java original and from generic geometry-processing frameworks.


CGAL Surface_mesh as the halfedge data structure

The Java library uses CoHDS — a custom intrusive halfedge data structure with CoVertex, CoEdge, CoFace types that carry domain-specific data directly as fields.

conformallab++ replaces this with CGAL::Surface_mesh<Point3> and attaches data via named property maps:

// Java: vertex.getLambda()           → C++: maps.lambda0[v]
// Java: edge.getAlpha()              → C++: maps.e_alpha[e]
// Java: vertex.getSolverIndex()      → C++: maps.v_idx[v]

auto [lambda0, ok] = mesh.add_property_map<Edge_index, double>("e:lambda0", 0.0);
lambda0[e] = 1.234;

This decouples the mesh topology from the algorithm data, makes it straightforward to attach multiple independent data sets to the same mesh, and will enable the Phase 8 traits-class design to work with any CGAL-conforming mesh type.


DOF vector convention

All three functionals use the same indexing scheme: a flat std::vector<double> x indexed by v_idx[v] (vertices) and e_idx[e] (edges, HyperIdeal only). Index value -1 means "pinned" — the DOF is fixed at zero and excluded from the Newton system.

x[maps.v_idx[v]]  = uᵥ   (conformal scale factor, Euclidean/Spherical)
x[maps.e_idx[e]]  = λₑ   (edge log-length variable, HyperIdeal only)
-1                        pinned — u_v = 0 / λ_e = 0

This is consistent across all three geometry modes, enabling the same Newton solver and linear system infrastructure to serve all three without branching.


Scalar type: double, with one localized exception

The kernel is CGAL::Simple_cartesian<double> and the whole numerical core (functionals, Hessians, Newton, Eigen sparse solvers) is double. Conformal flattening minimises a smooth energy over transcendental (floating-point) lengths and angles, so exact/extended arithmetic buys nothing there — the Java original is double here too. The CGAL wrapper (include/CGAL/*.h) is templated on FT for API genericity, but delegates to the double core.

The one exception (deferred, Phase 9c/10): hyperbolic uniformization (FundamentalPolygonUtility, CanonicalFormUtility) composes products of isometry generators whose entries grow exponentially; double then fails to verify the group relation ∏gᵢ = Id. The Java original handles this with RnBig/PnBig/P2Big at MathContext(50). When ported, this needs a localized high-precision substrate (boost::multiprecision::cpp_dec_float_50 or MPFR mpreal) inside the uniformization module only — never the core or the Eigen solver. See CLAUDE.md Phase-9 note and doc/roadmap/java-parity.md (*Big exception).


Priority-BFS layout

A naive BFS layout places faces in arbitrary order; trilateration errors accumulate along the BFS frontier. conformallab++ uses a priority min-heap on BFS depth:

depth(face) = max(depth[v_src], depth[v_tgt]) + 1   for each new face

Faces with smaller depth (closer to the root) are placed first. This means each face's trilateration uses the two most accurately-placed adjacent vertices, minimising error propagation across the mesh.

Root face selection: largest 3-D area face, with an additional 1.5× bonus for interior faces over boundary faces. This heuristic places the root where metric distortion is lowest.


halfedge_uv semantics

layout.uv[v.idx()] gives the primary UV coordinate of vertex v — the position from the shallowest BFS visit. At seam edges this is insufficient for GPU rendering: two faces sharing a seam vertex need different UV values for that vertex.

layout.halfedge_uv[h.idx()] stores the UV of source(h) as seen from face(h):

halfedge h  →  face(h)  →  source(h) has UV = halfedge_uv[h.idx()]
opposite(h) →  face(h') →  source(h) has UV = halfedge_uv[opposite(h).idx()]
                            (different value at a seam)

At seam halfedges the two opposite halfedges carry different UV values — each face gets its own copy of the seam vertex. This enables a proper GPU texture atlas without vertex duplication in the index buffer.


Spherical Hessian sign convention

The spherical energy functional is concave (negative semidefinite Hessian). Standard Newton would require solving H·Δx = G with NSD H, which Cholesky cannot handle.

newton_spherical() solves (H)·Δx = G instead — algebraically identical, but H is PSD and SimplicialLDLT works correctly. This sign flip is handled transparently inside newton_spherical(); callers need not be aware of it.

The gradient sign in spherical mode is also flipped vs. Euclidean:

  • Euclidean: G_v = actual_sum Θᵥ
  • Spherical: G_v = Θᵥ actual_sum

Both conventions drive the same equilibrium condition G = 0.


HyperIdeal Hessian via finite differences

The analytic HyperIdeal Hessian requires differentiating through the chain (bᵢ, aₑ) → lᵢⱼ → ζ₁₃/ζ₁₄/ζ₁₅ → αᵢⱼ/βᵢ with four vertex-type combinations per edge — substantial implementation complexity.

conformallab++ uses a symmetric finite-difference Hessian instead:

H[i,j] = (G(x + ε·eⱼ)[i]  G(x  ε·eⱼ)[i]) / (2ε),   ε = 1e-5

Properties:

  • O(ε²) accuracy — relative error ≈ 10⁻¹⁰ at ε = 10⁻⁵
  • PSD guaranteed by strict convexity of the HyperIdeal energy (Springborn 2020)
  • Symmetrised automatically: H = (H + Hᵀ) / 2
  • Cost: n extra gradient evaluations per Newton step (acceptable for < 500 DOFs)

The analytic Hessian is deferred to Phase 9b. See roadmap/java-parity.md.