Declare node tunable parameters in the schema (typed, ranged) #30

Closed
opened 2026-06-05 23:40:02 +02:00 by Brummel · 1 comment
Owner

Part of milestone "The World — parameter-space & sweep" (cycle A — the root of the sequence).

Node parameters are currently hard-coded at build sites in the Rust builder — e.g. SMA(2), Exposure(0.5) — so there is no declared, enumerable parameter surface for an orchestration axis to vary. Contract C8 (docs/design/INDEX.md) already reserves this: a node's schema() declares "the node's own tunable parameters — typed, with ranges, which aggregate into the blueprint's param-space the optimizer sweeps". C23 names the gap explicitly: the sweep-level pass "presupposes per-node param declarations (C8 — deliberately not in the schema yet)".

This cycle adds the declaration: each node type exposes its tunable parameters as typed, ranged values that aggregate into the blueprint's param-space. No sweeping yet — only the declarable surface that param-set injection (next cycle) and the sweep (C12.1) build on.

Design is not yet settled (param type mechanism, how ranges are expressed, how per-node params aggregate to the blueprint) — entry via brainstorm → specify → planner.

Ledger refs: C8, C12, C19, C23 (docs/design/INDEX.md).

Part of milestone "The World — parameter-space & sweep" (cycle A — the root of the sequence). Node parameters are currently hard-coded at build sites in the Rust builder — e.g. `SMA(2)`, `Exposure(0.5)` — so there is no declared, enumerable parameter surface for an orchestration axis to vary. Contract C8 (docs/design/INDEX.md) already reserves this: a node's `schema()` declares "the node's own tunable parameters — typed, with ranges, which aggregate into the blueprint's param-space the optimizer sweeps". C23 names the gap explicitly: the sweep-level pass "presupposes per-node param declarations (C8 — deliberately not in the schema yet)". This cycle adds the declaration: each node type exposes its tunable parameters as typed, ranged values that aggregate into the blueprint's param-space. No sweeping yet — only the declarable surface that param-set injection (next cycle) and the sweep (C12.1) build on. Design is not yet settled (param type mechanism, how ranges are expressed, how per-node params aggregate to the blueprint) — entry via brainstorm → specify → planner. Ledger refs: C8, C12, C19, C23 (docs/design/INDEX.md).
Brummel added this to the The World — parameter-space & sweep milestone 2026-06-05 23:40:02 +02:00
Brummel added the feature label 2026-06-05 23:40:02 +02:00
Author
Owner

Refinement of the "design not yet settled" note — two precisions from a design discussion

Both sharpen what this cycle's param declaration must carry, and shift the #30 / #32 boundary. Neither is decided — they are arguments for the brainstorm entry, not a ratified design.

Precision 1 — the range form is not uniform; it is per-scalar-kind

"Range" as a single notion is misleading. The four scalar kinds (C7) are structurally different as a parameter domain, because "how is it enumerated" differs per type:

  • i64 — countable; the grid is lo..=hi.
  • f64 — continuous; an interval [lo, hi] holds uncountably many values. How finely it is sampled (5 points? 50? random-100?) is a property of the run, not the node — so an f64 declaration carries lo, hi but no step.
  • bool — no range concept; the domain is {false, true}. A uniform lo..hi would have to write false..true, which is nonsense — this alone refutes a single range form.
  • timestamp — likely not a tuning param at all; a timestamp-valued knob (e.g. session_start) is almost always a structural axis (which data window, C20), not a numerically swept param.

Proposed shape: an enum ParamDomain with one variant per admissible kind, parallel to the existing Scalar / ScalarKind line — not a generic Range<T>, because the domains differ structurally, not just in element type:

enum ParamDomain {
    I64  { lo: i64, hi: i64 },   // countable grid
    F64  { lo: f64, hi: f64 },   // continuous; no step in the declaration
    Bool,                         // the domain is {false, true}
    // Timestamp: probably not a tuning param
}

Open question for the brainstorm: may all four kinds be params, or only i64/f64/bool?

Precision 2 — the search-range belongs to the run, not the node; the validity constraint drives no mechanism

The range that an optimization run sweeps (length in 5..50 one run, 2..200 the next, with a chosen discretization) is itself a parameter of that run. So there are two ranges at two locations, and they do not collapse:

  • Definition domain (node, static): what the node can process at all — e.g. length >= 1. Permanent type property; known only to the node author.
  • Search axis (run, C20): a subset the experiment actually sweeps. Authored in Rust in the harness / experiment-builder API, varying per run. Relation: search-axis is contained in the definition domain.

The search axis therefore belongs in the experiment-builder surface (#32 / C20), not in the node.

Functional analysis of what a param declaration must carry — what the param-set bind step (cycle B, #31) actually reads:

let set = [ length = 5, scale = 0.5 ];            // flat vector of typed values
ParamSpec { name:"length", kind:I64, slot:0 }     // kind+slot: READ, to route 5 -> Scalar::I64(5)
ParamSpec { ..              valid:>=1 }            // valid: read ONLY if validation runs

Identity + kind + slot is the load-bearing part: the bootstrap must know "knob 0 of this node is an i64 named length" to bind the vector, and the sweep (cycle C, #32) must know which knobs exist. That drives mechanism.

The validity constraint drives nothing. It is touched only to check an injected value, and only catches what the run misconfigured (the sweep enumerates the run-supplied search-axis; if that lies inside the valid set — which the run author ensures — the constraint never fires). And the guard already exists, at the right place:

// crates/aura-std/src/sma.rs:16
pub fn new(length: usize) -> Self {
    assert!(length >= 1, "SMA length must be >= 1");
    ..
}

A schema-level constraint would only duplicate this assert! and fire slightly earlier (at bind, not at construction).

One condition makes the definition domain functional after all: an autonomous optimizer whose search-range is not run-supplied but derived from the node domain ("sweep the whole valid range"). That, however, forces the node to invent an upper bound (length up to 200) that is not a truth about the node — the tension Precision 1's f64 point and this precision both flag. The two readings are mutually exclusive: either the run supplies the search-range (node constraint is a pure guard) or the optimizer derives it from the node domain (node must invent a bound it does not own).

Scope consequence for #30 vs #32

This thins the param declaration to its functionally-forced core:

ParamSpec { name, kind, slot }   // identity + type — drives bind (#31) and sweep enumeration (#32)
  • The run-specific search axis moves cleanly to #32 / the experiment-builder API (C20).
  • The validity constraint (defense-in-depth guard, mirroring the constructor assert!) and an optional recommended default range (UX hint for a C22 playground slider / aura new scaffolding) are optional add-ons with a named, smaller purpose — each decided separately in the brainstorm, neither the substance of #30.

The substance of #30 is that a param exists at all as a typed, addressable knob.

## Refinement of the "design not yet settled" note — two precisions from a design discussion Both sharpen what this cycle's param declaration must carry, and shift the #30 / #32 boundary. Neither is decided — they are arguments for the brainstorm entry, not a ratified design. ### Precision 1 — the range form is not uniform; it is per-scalar-kind "Range" as a single notion is misleading. The four scalar kinds (C7) are structurally different as a parameter domain, because "how is it enumerated" differs per type: - `i64` — countable; the grid *is* `lo..=hi`. - `f64` — continuous; an interval `[lo, hi]` holds uncountably many values. How finely it is sampled (5 points? 50? random-100?) is a property of the *run*, not the node — so an f64 declaration carries `lo, hi` but no `step`. - `bool` — no range concept; the domain *is* `{false, true}`. A uniform `lo..hi` would have to write `false..true`, which is nonsense — this alone refutes a single range form. - `timestamp` — likely not a tuning param at all; a timestamp-valued knob (e.g. `session_start`) is almost always a structural axis (which data window, C20), not a numerically swept param. Proposed shape: an `enum ParamDomain` with one variant per admissible kind, parallel to the existing `Scalar` / `ScalarKind` line — not a generic `Range<T>`, because the domains differ structurally, not just in element type: ```rust enum ParamDomain { I64 { lo: i64, hi: i64 }, // countable grid F64 { lo: f64, hi: f64 }, // continuous; no step in the declaration Bool, // the domain is {false, true} // Timestamp: probably not a tuning param } ``` Open question for the brainstorm: may all four kinds be params, or only i64/f64/bool? ### Precision 2 — the search-range belongs to the run, not the node; the validity constraint drives no mechanism The range that an optimization run sweeps (`length in 5..50` one run, `2..200` the next, with a chosen discretization) is itself a parameter of that run. So there are **two** ranges at two locations, and they do not collapse: - **Definition domain** (node, static): what the node can process at all — e.g. `length >= 1`. Permanent type property; known only to the node author. - **Search axis** (run, C20): a subset the experiment actually sweeps. Authored in Rust in the harness / experiment-builder API, varying per run. Relation: search-axis is contained in the definition domain. The search axis therefore belongs in the experiment-builder surface (#32 / C20), not in the node. Functional analysis of what a param declaration must carry — what the param-set bind step (cycle B, #31) actually reads: ```rust let set = [ length = 5, scale = 0.5 ]; // flat vector of typed values ParamSpec { name:"length", kind:I64, slot:0 } // kind+slot: READ, to route 5 -> Scalar::I64(5) ParamSpec { .. valid:>=1 } // valid: read ONLY if validation runs ``` Identity + kind + slot is the load-bearing part: the bootstrap must know "knob 0 of this node is an i64 named length" to bind the vector, and the sweep (cycle C, #32) must know *which knobs exist*. That drives mechanism. The validity constraint drives nothing. It is touched only to check an injected value, and only catches what the *run* misconfigured (the sweep enumerates the run-supplied search-axis; if that lies inside the valid set — which the run author ensures — the constraint never fires). And the guard already exists, at the right place: ```rust // crates/aura-std/src/sma.rs:16 pub fn new(length: usize) -> Self { assert!(length >= 1, "SMA length must be >= 1"); .. } ``` A schema-level constraint would only duplicate this `assert!` and fire slightly earlier (at bind, not at construction). One condition makes the definition domain functional after all: an autonomous optimizer whose search-range is *not* run-supplied but derived from the node domain ("sweep the whole valid range"). That, however, forces the node to invent an upper bound (`length up to 200`) that is not a truth about the node — the tension Precision 1's f64 point and this precision both flag. The two readings are mutually exclusive: either the run supplies the search-range (node constraint is a pure guard) or the optimizer derives it from the node domain (node must invent a bound it does not own). ### Scope consequence for #30 vs #32 This thins the param declaration to its functionally-forced core: ```rust ParamSpec { name, kind, slot } // identity + type — drives bind (#31) and sweep enumeration (#32) ``` - The run-specific **search axis** moves cleanly to #32 / the experiment-builder API (C20). - The validity **constraint** (defense-in-depth guard, mirroring the constructor `assert!`) and an optional recommended **default range** (UX hint for a C22 playground slider / `aura new` scaffolding) are optional add-ons with a named, smaller purpose — each decided separately in the brainstorm, neither the substance of #30. The substance of #30 is that a param exists at all as a typed, addressable knob.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#30