# 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_*` | ### 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 ```yaml pipeline: name: # human-readable experiment name geometry: euclidean # euclidean | spherical | hyper_ideal description: | # optional multi-line description ... input: source: # mesh file (.off / .obj / .ply) # Optional overrides: theta_v: flat # flat (2π everywhere) | cone: | custom: steps: - id: # unique step identifier unit: # C++ function name (see token table) require: [, ...] # tokens that must be in the provided set provide: [, ...] # tokens added to provided set after this step params: # optional parameter overrides : output: layout: # optional: save Layout2D as .off with UVs json: # optional: save NewtonResult + Layout as JSON xml: # optional: save NewtonResult + Layout as XML tau: # optional: write τ (period matrix) as text report: # 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 '' requires '' which is not yet provided. Add a step that provides it before step ''." 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 provided; void load_input(const YAML::Node& input) { provided.insert("mesh"); auto mesh = load_mesh(input["source"].as()); 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())) throw PipelineError("Step '" + step["id"].as() + "' requires '" + tok.as() + "' not yet provided."); for (auto& tok : step["provide"]) provided.insert(tok.as()); } 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.