Files
ConformalLabpp/doc/concepts/declarative-pipeline.md
Tarik Moussa 52604d8544 docs: Konzeptdokument Declarative YAML Pipeline (Phase 8e)
doc/concepts/declarative-pipeline.md — vollständige Design-Spezifikation:

  1. Kernidee: Processing Units mit expliziten require/provide-Contracts
  2. Token-Vokabular: 30 Tokens in 7 Kategorien
       input, setup, Gauss-Bonnet, solver, topology, layout, period/domain/output
  3. YAML-Schema: Vollständige Syntax inkl. Parameterdefaults aller Units
  4. Validierungsalgorithmus: monoton wachsendes provided-Set, Pre-Execution-Check
  5. 5 vollständige Beispiele:
       A — Euklidische Uniformisierung Torus (τ-Ausgabe)
       B — Sphärische Uniformisierung (cathead.obj)
       C — Hyperbolische Uniformisierung Torus (Poincaré-Disk)
       D — Volle Pipeline mit Periodenmatrix + 5×5-Kachelung
       E — Absichtlich fehlerhaftes Beispiel mit Validator-Fehlermeldungen
  6. C++-Mapping: alle YAML-Unit-Namen → C++-Funktionen + Header
  7. Implementierungsplan (Phase 8e): pipeline.hpp + CLI-App + YAML-Abhängigkeit
  8. Design-Entscheidungen: YAML vs. JSON/TOML, explizit vs. auto-inference,
     linear vs. DAG, eine Geometrie pro Datei

doc/api/cgal-package.md: Link zum Konzeptdokument ergänzt.
README.md: Link in Dokumentationstabelle ergänzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 01:39:14 +02:00

572 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Declarative YAML Pipeline — Concept & Design
> **Status: Phase 8e — designed, not yet implemented.**
> This document is the authoritative design specification.
> Implementation target: `code/include/pipeline.hpp` + CLI flag `--pipeline`.
---
## 1 — Core idea
Every algorithm in conformallab++ is a **Processing Unit**: a function with
explicit preconditions (*require*) and guarantees (*provide*).
The contract table in [doc/api/contracts.md](../api/contracts.md) lists them all.
A **declarative pipeline** is a YAML file that:
1. Lists the units to execute and their parameters.
2. Annotates each step with the tokens it `require`s and `provide`s.
3. Is validated **before any code runs** — the validator walks the dependency
graph and rejects the file if any `require` token is not yet provided by
a preceding step.
The result is a **self-documenting, reproducible experiment** that can be
version-controlled, shared, and re-run identically.
---
## 2 — Token vocabulary
Tokens are short strings. Each Processing Unit consumes and produces a fixed
set of tokens. The validator treats them as a monotonically growing *provided set*.
### Input tokens (provided by the `input:` block)
| Token | Meaning |
|---|---|
| `mesh` | A loaded, triangulated, oriented `ConformalMesh` |
| `mesh_closed` | Mesh has no boundary (required for cut graph) |
| `mesh_open` | Mesh has at least one boundary component |
### Setup tokens
| Token | Produced by | Required by |
|---|---|---|
| `maps_euclidean` | `setup_euclidean_maps` | `lambda0_euclidean`, `gauss_bonnet`, `newton_euclidean` |
| `maps_spherical` | `setup_spherical_maps` | `lambda0_spherical`, `gauss_bonnet`, `newton_spherical` |
| `maps_hyper_ideal` | `setup_hyper_ideal_maps` | `lambda0_hyper_ideal`, `gauss_bonnet`, `newton_hyper_ideal` |
| `lambda0_euclidean` | `compute_euclidean_lambda0_from_mesh` | `gauss_bonnet_euclidean`, `newton_euclidean` |
| `lambda0_spherical` | `compute_spherical_lambda0_from_mesh` | `gauss_bonnet_spherical`, `newton_spherical` |
| `lambda0_hyper_ideal` | `compute_hyper_ideal_lambda0_from_mesh` | `gauss_bonnet_hyper_ideal`, `newton_hyper_ideal` |
| `dof_indices` | DOF assignment step | `newton_*` |
### GaussBonnet tokens
| Token | Produced by |
|---|---|
| `gauss_bonnet_euclidean` | `check_gauss_bonnet` or `enforce_gauss_bonnet` (Euclidean maps) |
| `gauss_bonnet_spherical` | same, Spherical maps |
| `gauss_bonnet_hyper_ideal` | same, HyperIdeal maps |
### Solver tokens
| Token | Produced by |
|---|---|
| `x_euclidean` | `newton_euclidean` (converged) |
| `x_spherical` | `newton_spherical` (converged) |
| `x_hyper_ideal` | `newton_hyper_ideal` (converged) |
### Topology tokens
| Token | Produced by | Required by |
|---|---|---|
| `cut_graph` | `compute_cut_graph` | `euclidean_layout` (closed mesh), `hyper_ideal_layout` |
### Layout tokens
| Token | Produced by |
|---|---|
| `layout_euclidean` | `euclidean_layout` |
| `layout_spherical` | `spherical_layout` |
| `layout_hyper_ideal` | `hyper_ideal_layout` |
| `holonomy_euclidean` | `euclidean_layout` (with cut graph) |
| `holonomy_hyper_ideal` | `hyper_ideal_layout` (with cut graph) |
### Period / domain tokens
| Token | Produced by | Required by |
|---|---|---|
| `period_matrix` | `compute_period_matrix` | `fundamental_domain` |
| `fundamental_domain` | `compute_fundamental_domain` | `tiling` |
| `tiling` | `tiling_neighbourhood` | output steps |
### Output tokens
| Token | Produced by |
|---|---|
| `saved_layout` | `save_layout_off` |
| `saved_result` | `save_result_json` / `save_result_xml` |
---
## 3 — YAML schema
```yaml
pipeline:
name: <string> # human-readable experiment name
geometry: euclidean # euclidean | spherical | hyper_ideal
description: | # optional multi-line description
...
input:
source: <path> # mesh file (.off / .obj / .ply)
# Optional overrides:
theta_v: flat # flat (2π everywhere) | cone:<file> | custom:<file>
steps:
- id: <string> # unique step identifier
unit: <function> # C++ function name (see token table)
require: [<token>, ...] # tokens that must be in the provided set
provide: [<token>, ...] # tokens added to provided set after this step
params: # optional parameter overrides
<key>: <value>
output:
layout: <path> # optional: save Layout2D as .off with UVs
json: <path> # optional: save NewtonResult + Layout as JSON
xml: <path> # optional: save NewtonResult + Layout as XML
tau: <path> # optional: write τ (period matrix) as text
report: <path> # optional: write human-readable summary
```
### Parameter defaults by unit
| Unit | Parameter | Default |
|---|---|---|
| `newton_euclidean` | `tol` | `1e-8` |
| `newton_euclidean` | `max_iter` | `200` |
| `newton_spherical` | `tol` | `1e-8` |
| `newton_spherical` | `max_iter` | `200` |
| `newton_hyper_ideal` | `tol` | `1e-8` |
| `newton_hyper_ideal` | `max_iter` | `200` |
| `newton_hyper_ideal` | `hess_eps` | `1e-5` |
| `euclidean_layout` | `normalise` | `false` |
| `hyper_ideal_layout` | `normalise` | `false` |
| `spherical_layout` | `normalise` | `false` |
| `compute_period_matrix` | `reduce` | `true` (SL(2,)) |
| `tiling_neighbourhood` | `m` | `1` |
| `tiling_neighbourhood` | `n` | `1` |
---
## 4 — Validation algorithm
```
provided = { "mesh" } ← always available after input is loaded
if input.source has no boundary:
provided ← provided { "mesh_closed" }
else:
provided ← provided { "mesh_open" }
for each step in pipeline.steps:
for token in step.require:
if token ∉ provided:
ERROR: "Step '<id>' requires '<token>' which is not yet provided.
Add a step that provides it before step '<id>'."
provided ← provided step.provide
for each output key in pipeline.output:
check that its required token is in provided
(e.g. 'tau' requires 'period_matrix')
```
The validator runs **before any C++ code executes**. If validation passes,
the steps are executed in order. There is no parallelism — steps are sequential.
---
## 5 — Complete examples
### Example A — Euclidean uniformization of a flat torus (genus 1)
```yaml
pipeline:
name: flat_torus_euclidean
geometry: euclidean
description: |
Euclidean uniformization of the 4×4 torus of revolution.
Computes the period matrix τ and the fundamental domain parallelogram.
input:
source: code/data/off/torus_4x4.off
steps:
- id: setup
unit: setup_euclidean_maps
require: [mesh]
provide: [maps_euclidean]
- id: lambda0
unit: compute_euclidean_lambda0_from_mesh
require: [mesh, maps_euclidean]
provide: [lambda0_euclidean]
- id: dofs
unit: assign_dof_indices_euclidean
require: [maps_euclidean]
provide: [dof_indices]
params:
pin_strategy: first_vertex # pin v₀, assign sequential to rest
- id: gauss_bonnet
unit: enforce_gauss_bonnet
require: [maps_euclidean, lambda0_euclidean]
provide: [gauss_bonnet_euclidean]
- id: solve
unit: newton_euclidean
require: [gauss_bonnet_euclidean, dof_indices]
provide: [x_euclidean]
params:
tol: 1.0e-10
max_iter: 200
- id: cut
unit: compute_cut_graph
require: [mesh_closed]
provide: [cut_graph]
- id: layout
unit: euclidean_layout
require: [x_euclidean, cut_graph]
provide: [layout_euclidean, holonomy_euclidean]
params:
normalise: true
- id: period
unit: compute_period_matrix
require: [holonomy_euclidean]
provide: [period_matrix]
params:
reduce: true
- id: domain
unit: compute_fundamental_domain
require: [holonomy_euclidean]
provide: [fundamental_domain]
output:
layout: out/torus_layout.off
json: out/torus_result.json
tau: out/torus_tau.txt
```
**Expected output (`torus_tau.txt`):**
```
tau = 0.000... + 0.9...i # Re(τ) ≈ 0 (4-fold symmetry), Im(τ) ≈ 1
|tau| = 0.9... # ≥ 1 after SL(2,) reduction
```
---
### Example B — Spherical uniformization (genus 0)
```yaml
pipeline:
name: cathead_spherical
geometry: spherical
description: |
Map the open cathead mesh to the sphere.
input:
source: code/data/obj/cathead.obj
steps:
- id: setup
unit: setup_spherical_maps
require: [mesh]
provide: [maps_spherical]
- id: lambda0
unit: compute_spherical_lambda0_from_mesh
require: [mesh, maps_spherical]
provide: [lambda0_spherical]
- id: dofs
unit: assign_dof_indices_spherical
require: [maps_spherical]
provide: [dof_indices]
- id: gauss_bonnet
unit: enforce_gauss_bonnet
require: [maps_spherical, lambda0_spherical]
provide: [gauss_bonnet_spherical]
- id: solve
unit: newton_spherical
require: [gauss_bonnet_spherical, dof_indices]
provide: [x_spherical]
- id: layout
unit: spherical_layout
require: [x_spherical]
provide: [layout_spherical]
params:
normalise: true # rotate centroid to north pole
output:
json: out/cathead_spherical.json
```
---
### Example C — Hyperbolic uniformization (genus 1, Poincaré disk)
```yaml
pipeline:
name: torus_hyperbolic
geometry: hyper_ideal
description: |
Hyperbolic (hyper-ideal) uniformization of the 8×8 torus.
Lays out the mesh in the Poincaré disk with correct Möbius holonomy.
input:
source: code/data/off/torus_8x8.off
steps:
- id: setup
unit: setup_hyper_ideal_maps
require: [mesh]
provide: [maps_hyper_ideal]
- id: lambda0
unit: compute_hyper_ideal_lambda0_from_mesh
require: [mesh, maps_hyper_ideal]
provide: [lambda0_hyper_ideal]
- id: dofs
unit: assign_all_dof_indices
require: [maps_hyper_ideal]
provide: [dof_indices]
# HyperIdeal: all vertices AND edges are free DOFs — no vertex pinned.
- id: gauss_bonnet
unit: enforce_gauss_bonnet
require: [maps_hyper_ideal, lambda0_hyper_ideal]
provide: [gauss_bonnet_hyper_ideal]
- id: solve
unit: newton_hyper_ideal
require: [gauss_bonnet_hyper_ideal, dof_indices]
provide: [x_hyper_ideal]
params:
tol: 1.0e-10
- id: cut
unit: compute_cut_graph
require: [mesh_closed]
provide: [cut_graph]
- id: layout
unit: hyper_ideal_layout
require: [x_hyper_ideal, cut_graph]
provide: [layout_hyper_ideal, holonomy_hyper_ideal]
params:
normalise: true # Möbius-centre to disk origin
output:
layout: out/torus_disk.off
json: out/torus_disk.json
```
---
### Example D — Full pipeline with period matrix and tiling
```yaml
pipeline:
name: torus_full
geometry: euclidean
input:
source: code/data/off/torus_hex_6x6.off
steps:
- id: setup
unit: setup_euclidean_maps
require: [mesh]
provide: [maps_euclidean]
- id: lambda0
unit: compute_euclidean_lambda0_from_mesh
require: [mesh, maps_euclidean]
provide: [lambda0_euclidean]
- id: dofs
unit: assign_dof_indices_euclidean
require: [maps_euclidean]
provide: [dof_indices]
- id: gauss_bonnet
unit: enforce_gauss_bonnet
require: [maps_euclidean, lambda0_euclidean]
provide: [gauss_bonnet_euclidean]
- id: solve
unit: newton_euclidean
require: [gauss_bonnet_euclidean, dof_indices]
provide: [x_euclidean]
- id: cut
unit: compute_cut_graph
require: [mesh_closed]
provide: [cut_graph]
- id: layout
unit: euclidean_layout
require: [x_euclidean, cut_graph]
provide: [layout_euclidean, holonomy_euclidean]
params:
normalise: true
- id: period
unit: compute_period_matrix
require: [holonomy_euclidean]
provide: [period_matrix]
- id: domain
unit: compute_fundamental_domain
require: [holonomy_euclidean]
provide: [fundamental_domain]
- id: tiling
unit: tiling_neighbourhood
require: [layout_euclidean, holonomy_euclidean]
provide: [tiling]
params:
m: 2 # 5×5 tile grid around origin
n: 2
output:
layout: out/hex_torus_layout.off
tau: out/hex_torus_tau.txt
json: out/hex_torus_full.json
report: out/hex_torus_summary.txt
# Expected tau for 6-fold symmetric torus:
# Re(τ) ≈ 0.5, Im(τ) ≈ 0.866 (approaches e^{iπ/3} as mesh is refined)
```
---
### Example E — Validation error (intentional mistake)
```yaml
pipeline:
name: broken_example
geometry: euclidean
input:
source: code/data/off/torus_4x4.off
steps:
- id: solve # ← WRONG: skipped setup and lambda0
unit: newton_euclidean
require: [gauss_bonnet_euclidean, dof_indices]
provide: [x_euclidean]
- id: layout
unit: euclidean_layout
require: [x_euclidean, cut_graph] # ← WRONG: cut_graph never provided
provide: [layout_euclidean]
```
**Validator output:**
```
ERROR [step 'solve']: requires 'gauss_bonnet_euclidean' which is not yet provided.
Hint: add setup_euclidean_maps → compute_euclidean_lambda0_from_mesh
→ enforce_gauss_bonnet before 'solve'.
ERROR [step 'layout']: requires 'cut_graph' which is not yet provided.
Hint: add compute_cut_graph (requires: mesh_closed) before 'layout'.
Pipeline rejected. 2 contract violations.
```
---
## 6 — Mapping to C++ code
Each `unit:` name in the YAML maps directly to a C++ function:
| YAML unit | C++ function | Header |
|---|---|---|
| `setup_euclidean_maps` | `conformallab::setup_euclidean_maps()` | `euclidean_functional.hpp` |
| `compute_euclidean_lambda0_from_mesh` | `conformallab::compute_euclidean_lambda0_from_mesh()` | `euclidean_functional.hpp` |
| `enforce_gauss_bonnet` | `conformallab::enforce_gauss_bonnet()` | `gauss_bonnet.hpp` |
| `assign_dof_indices_euclidean` | manual pin + sequential loop | `euclidean_functional.hpp` |
| `assign_all_dof_indices` | `conformallab::assign_all_dof_indices()` | `hyper_ideal_functional.hpp` |
| `newton_euclidean` | `conformallab::newton_euclidean()` | `newton_solver.hpp` |
| `newton_spherical` | `conformallab::newton_spherical()` | `newton_solver.hpp` |
| `newton_hyper_ideal` | `conformallab::newton_hyper_ideal()` | `newton_solver.hpp` |
| `compute_cut_graph` | `conformallab::compute_cut_graph()` | `cut_graph.hpp` |
| `euclidean_layout` | `conformallab::euclidean_layout()` | `layout.hpp` |
| `spherical_layout` | `conformallab::spherical_layout()` | `layout.hpp` |
| `hyper_ideal_layout` | `conformallab::hyper_ideal_layout()` | `layout.hpp` |
| `compute_period_matrix` | `conformallab::compute_period_matrix()` | `period_matrix.hpp` |
| `compute_fundamental_domain` | `conformallab::compute_fundamental_domain()` | `fundamental_domain.hpp` |
| `tiling_neighbourhood` | `conformallab::tiling_neighbourhood()` | `fundamental_domain.hpp` |
| `save_layout_off` | `conformallab::save_layout_off()` | `mesh_io.hpp` |
| `save_result_json` | `conformallab::save_result_json()` | `serialization.hpp` |
---
## 7 — Implementation plan (Phase 8e)
```
code/include/pipeline.hpp ← YAML parser + validator + executor
code/apps/conformallab_pipeline.cpp ← CLI: conformallab_pipeline --pipeline foo.yml
External YAML dependency (header-only, already bundled):
code/deps/single_includes/yaml-cpp/yaml.h ← or single-include yaml.hpp
Alternative: use the bundled single_includes/nlohmann/json.hpp for a
JSON-based pipeline format (simpler, no new dependency).
```
### Validator pseudocode
```cpp
struct PipelineValidator {
std::set<std::string> provided;
void load_input(const YAML::Node& input) {
provided.insert("mesh");
auto mesh = load_mesh(input["source"].as<std::string>());
if (is_closed(mesh)) provided.insert("mesh_closed");
else provided.insert("mesh_open");
}
void validate_step(const YAML::Node& step) {
for (auto& tok : step["require"])
if (!provided.count(tok.as<std::string>()))
throw PipelineError("Step '" + step["id"].as<std::string>()
+ "' requires '" + tok.as<std::string>() + "' not yet provided.");
for (auto& tok : step["provide"])
provided.insert(tok.as<std::string>());
}
void validate(const YAML::Node& pipeline) {
load_input(pipeline["input"]);
for (auto& step : pipeline["steps"])
validate_step(step);
}
};
```
---
## 8 — Design decisions
**Why YAML and not JSON or TOML?**
YAML supports multi-line strings (for `description:`), comments (`#`), and
anchors/aliases — useful for parametric experiments. JSON lacks comments.
TOML lacks the list syntax needed for `require:` / `provide:`.
**Why explicit require/provide instead of auto-inference?**
Auto-inference would require the validator to know all function signatures
at parse time — this couples the validator tightly to the C++ code.
Explicit tokens make the contract visible in the YAML, making it
self-documenting and readable without the source code.
**Why sequential steps and not a DAG?**
The pipeline is a linear sequence for now (Phase 8e target). A DAG-based
executor (parallel steps where contracts allow) is a natural Phase 10+ extension
but adds significant complexity. Linear execution is correct and debuggable.
**Why one YAML per geometry mode?**
A single YAML could support multiple geometries with conditional blocks, but
this adds syntactic complexity. The `geometry:` field at the top locks the mode
and keeps the YAML readable. Cross-geometry experiments can chain two pipelines.