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

18 KiB
Raw Blame History

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:

  1. Lists the units to execute and their parameters.
  2. Annotates each step with the tokens it requires and provides.
  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

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.