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>
18 KiB
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 lists them all.
A declarative pipeline is a YAML file that:
- Lists the units to execute and their parameters.
- Annotates each step with the tokens it
requires andprovides. - Is validated before any code runs — the validator walks the dependency
graph and rejects the file if any
requiretoken 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_* |
Gauss–Bonnet 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
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)
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)
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)
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
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)
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
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.