README.md: reduced from 703 to ~75 lines — what/why, status, quick start, minimal usage example, navigation table to doc/ files. doc/architecture/overall_pipeline.md: trimmed — roadmap, extension points, declarative pipeline YAML, and references sections removed (each now has its own dedicated file). Replaced with a link table. New files: doc/getting-started.md — build modes, single-test invocation, CLI doc/api/pipeline.md — full pipeline API with code for all 3 geometries doc/api/extending.md — new functionals, geometry modes, Java porting guide doc/api/contracts.md — processing unit preconditions/provides table doc/api/cgal-package.md — Phase 8 CGAL package design + YAML pipeline (TODO) doc/math/geometry-modes.md — Euclidean/Spherical/HyperIdeal comparison doc/math/references.md — all papers by module doc/roadmap/phases.md — Phases 1–10 with porting/research boundary doc/roadmap/java-parity.md — Java vs C++ feature parity table doc/contributing.md — language policy, test standards, release flow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
142 lines
4.6 KiB
Markdown
142 lines
4.6 KiB
Markdown
# Extending conformallab++
|
||
|
||
## Adding a new functional
|
||
|
||
All three existing functionals (`euclidean_functional.hpp`, `spherical_functional.hpp`,
|
||
`hyper_ideal_functional.hpp`) follow the same pattern. Copy the structure of the
|
||
simplest one (`euclidean_functional.hpp`) as a template.
|
||
|
||
### Step 1 — Maps struct
|
||
|
||
```cpp
|
||
// my_functional.hpp
|
||
struct MyMaps {
|
||
ConformalMesh::Property_map<Vertex_index, double> lambda0; // initial log-lengths
|
||
ConformalMesh::Property_map<Vertex_index, double> theta_v; // target angles
|
||
ConformalMesh::Property_map<Vertex_index, int> v_idx; // DOF index, -1 = pinned
|
||
// add edge DOFs if needed:
|
||
ConformalMesh::Property_map<Edge_index, int> e_idx;
|
||
};
|
||
|
||
MyMaps setup_my_maps(ConformalMesh& mesh);
|
||
void compute_my_lambda0_from_mesh(ConformalMesh& mesh, MyMaps& maps);
|
||
```
|
||
|
||
### Step 2 — Gradient
|
||
|
||
The gradient must satisfy: `G_v = Σ(angle contributions at v) − theta_v[v]`.
|
||
|
||
```cpp
|
||
std::vector<double> my_gradient(
|
||
const ConformalMesh& mesh,
|
||
const std::vector<double>& x,
|
||
const MyMaps& maps)
|
||
{
|
||
std::vector<double> G(n_dofs, 0.0);
|
||
for (auto f : mesh.faces()) {
|
||
// compute angles in face f given current x
|
||
// accumulate into G[maps.v_idx[v]] for each vertex v of f
|
||
}
|
||
// subtract target angles
|
||
for (auto v : mesh.vertices())
|
||
if (maps.v_idx[v] >= 0)
|
||
G[maps.v_idx[v]] -= maps.theta_v[v];
|
||
return G;
|
||
}
|
||
```
|
||
|
||
### Step 3 — Gradient check test
|
||
|
||
Before claiming correctness, verify with finite differences. Copy any `GradientCheck_*`
|
||
test suite from `tests/cgal/test_*_functional.cpp`:
|
||
|
||
```cpp
|
||
TEST(MyFunctional, GradientCheck_Triangle) {
|
||
ConformalMesh mesh = make_single_triangle();
|
||
MyMaps maps = setup_my_maps(mesh);
|
||
// ... assign DOFs, set natural theta ...
|
||
|
||
const double eps = 1e-5;
|
||
auto G = my_gradient(mesh, x0, maps);
|
||
for (int i = 0; i < n; ++i) {
|
||
std::vector<double> xp = x0, xm = x0;
|
||
xp[i] += eps; xm[i] -= eps;
|
||
double fd = (my_energy(mesh, xp, maps) - my_energy(mesh, xm, maps)) / (2*eps);
|
||
EXPECT_NEAR(G[i], fd, 1e-7) << "DOF " << i;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Step 4 — Hessian and Newton wrapper
|
||
|
||
```cpp
|
||
// Reuse solve_linear_system from newton_solver.hpp
|
||
Eigen::SparseMatrix<double> H = my_hessian(mesh, x, maps);
|
||
bool used_fallback;
|
||
auto dx = conformallab::solve_linear_system(H, -G, &used_fallback);
|
||
```
|
||
|
||
Or write a thin `newton_my()` wrapper following the structure of `newton_euclidean()`.
|
||
|
||
---
|
||
|
||
## Adding a new geometry mode
|
||
|
||
To add a new space (e.g. de Sitter, flat 3-torus):
|
||
|
||
1. **Trilateration function** — implement `trilaterate_my(p1, p2, l12, l13, l23)` that
|
||
places a third point given two placed points and three edge lengths.
|
||
This is the only geometry-specific part of the layout.
|
||
|
||
2. **BFS structure** — reuse the priority-BFS loop from `euclidean_layout()` in `layout.hpp`.
|
||
The loop itself is geometry-agnostic; swap in your trilateration function.
|
||
|
||
3. **Holonomy** — if the space has a holonomy group, record the transition maps at seam edges
|
||
the same way `HolonomyData` does for translations (Euclidean) or Möbius maps (hyperbolic).
|
||
|
||
---
|
||
|
||
## Adding a new processing unit
|
||
|
||
1. Declare preconditions and outputs explicitly (see [contracts.md](contracts.md)).
|
||
|
||
2. Write a header-only implementation in `code/include/my_unit.hpp`.
|
||
|
||
3. Add tests in `code/tests/cgal/test_my_unit.cpp` and register in
|
||
`code/tests/cgal/CMakeLists.txt`.
|
||
|
||
4. Pipeline validation is currently manual (documented contracts). A compile-time or
|
||
runtime check via the declarative YAML pipeline (Phase 8e) is the planned upgrade —
|
||
see [cgal-package.md](cgal-package.md).
|
||
|
||
---
|
||
|
||
## Porting from Java
|
||
|
||
When porting a class from the Java library:
|
||
|
||
1. Locate the original at [github.com/varylab/conformallab](https://github.com/varylab/conformallab)
|
||
under `src/main/java/de/varylab/discreteconformal/`.
|
||
|
||
2. The Java library uses `CoHDS` (half-edge data structure) with intrusive vertex/edge data.
|
||
Map these to CGAL property maps:
|
||
```
|
||
Java: vertex.getLambda() → C++: maps.lambda0[v]
|
||
Java: edge.getAlpha() → C++: maps.e_alpha[e]
|
||
Java: vertex.getTheta() → C++: maps.theta_v[v]
|
||
Java: vertex.getSolverIndex() → C++: maps.v_idx[v]
|
||
```
|
||
|
||
3. Java uses `HalfedgeInterface` adapters with named accessors like `getOppositeVertex()`.
|
||
C++ equivalent:
|
||
```cpp
|
||
// Java: h.getOppositeVertex()
|
||
Vertex_index v_opp = mesh.target(mesh.next(h));
|
||
|
||
// Java: h.getNextHalfedge().getOppositeVertex()
|
||
Vertex_index v_opp2 = mesh.target(mesh.next(mesh.next(h)));
|
||
```
|
||
|
||
4. Port the test cases from the Java `@Test` methods. The "natural theta" trick
|
||
(`theta_v[v] = actual_angle_sum`) works the same way in both languages.
|