# 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:     <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.
