feat(aura-core,aura-std,aura-engine): declare node tunable params (#30)
Cycle A of milestone 'The World — parameter-space & sweep'. A node now declares its tunable parameters in its C8 schema, and a blueprint aggregates them into one flat, inspectable param-space — the root that unlocks the C12 orchestration axes (#31 bind, #32 sweep), filling the gap C8/C23 name 'deliberately not in the schema yet'. - aura-core: ParamSpec { name: String, kind: ScalarKind } as a third schema- declaration type; NodeSchema gains a third field 'params'. name is String (not &'static like FieldSpec) because a vector knob carries a runtime index and aggregation prefixes the composite path. - aura-std: Sma declares [length:I64], Exposure [scale:F64], LinComb expands to N flat [weights[i]:F64] (N = its input arity, topology-fixed per C19); Sub/Add/ SimBroker/Recorder declare none (pip_size is metadata, C10/C15; Recorder is wiring). - aura-engine: Blueprint::param_space() walks the graph-as-data depth-first in lower_items order, path-qualifying via the already-public Composite::name() — a read-only projection (C9). compile/inline_composite/lower_items are untouched, so the compilat stays bit-identical (composite_sma_cross_runs_bit_identical_to_hand_wired and the golden render tests stay green). Design: param identity is positional (slot after the deterministic inline order, C23 'by index not name'); the path-qualified name is a non-load-bearing debug symbol — same-type siblings in one composite share a name, uniqueness is at the slot. Flat over a structured arity-bearing ParamSpec because flattening is unavoidable (a sweep enumerates a flat point-space) and structure-in-the-runtime is the nested-composite reading C23 rejects (see spec 0015 for the full rationale). Scope is declaration + aggregation + inspection only; binding (#31), sweep enumeration (#32), search-range, validity-constraint, and default-range/slider are deferred. An E2E test pins the load-bearing C23/#31 invariant: param_space() slot order mirrors compile()'s flat-node param order, kind-by-slot, on the SMA-cross harness. fieldtests/ (excluded crates) left as frozen snapshots. Verified: cargo build/test --workspace green (64 tests), clippy --all-targets -D warnings clean; compile/inline diff empty. closes #30
This commit is contained in:
@@ -39,5 +39,5 @@ pub use any::AnyColumn;
|
||||
pub use column::{Column, Window};
|
||||
pub use ctx::Ctx;
|
||||
pub use error::KindMismatch;
|
||||
pub use node::{FieldSpec, Firing, InputSpec, Node, NodeSchema};
|
||||
pub use node::{FieldSpec, Firing, InputSpec, Node, NodeSchema, ParamSpec};
|
||||
pub use scalar::{Scalar, ScalarKind, Timestamp};
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
//! and its output record (0..K base columns, C7; an empty output declares a
|
||||
//! pure consumer / sink role, C8) via `schema`, and computes one cycle's row
|
||||
//! via `eval`.
|
||||
//! Tunable params (C12/C19) are deliberately not part of the schema yet — see
|
||||
//! spec 0002's "Out of scope".
|
||||
//! Tunable params (C12/C19) are declared via `params` (cycle 0015): each node's
|
||||
//! typed knobs, which `Blueprint::param_space` aggregates into the sweep's flat,
|
||||
//! path-qualified param-space (C8/C23). Identity is positional (slot); the name is
|
||||
//! a non-load-bearing debug symbol.
|
||||
|
||||
use crate::{Ctx, Scalar, ScalarKind};
|
||||
|
||||
@@ -41,6 +43,20 @@ pub struct FieldSpec {
|
||||
pub kind: ScalarKind,
|
||||
}
|
||||
|
||||
/// One declared tunable parameter of a node (C8/C12): its render name and scalar
|
||||
/// kind. The name is a **non-load-bearing** debug symbol (path-qualified at
|
||||
/// aggregation, as `FieldSpec.name` already is); a param's identity is its
|
||||
/// positional slot in the blueprint's aggregated param-space (C23 — by index, not
|
||||
/// by name). Unlike `FieldSpec.name` (`&'static str`), the name is a `String`: a
|
||||
/// vector knob carries a runtime index (`weights[0]`) and aggregation prefixes the
|
||||
/// composite path (`strategy.weights[0]`). Permitted kinds: `I64`/`F64`/`Bool`;
|
||||
/// `Timestamp` is a structural axis (C20), never a numeric knob.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ParamSpec {
|
||||
pub name: String,
|
||||
pub kind: ScalarKind,
|
||||
}
|
||||
|
||||
/// A node's declared interface: its inputs (in order) and its output record — an
|
||||
/// ordered list of named base columns; length 1 is a scalar (the degenerate
|
||||
/// case), and an **empty** `output` (`vec![]`) declares a **pure consumer**
|
||||
@@ -50,6 +66,7 @@ pub struct FieldSpec {
|
||||
pub struct NodeSchema {
|
||||
pub inputs: Vec<InputSpec>,
|
||||
pub output: Vec<FieldSpec>,
|
||||
pub params: Vec<ParamSpec>,
|
||||
}
|
||||
|
||||
/// The universal composable dataflow unit (C8): a **producer, consumer, or both**.
|
||||
@@ -97,7 +114,7 @@ mod tests {
|
||||
struct Bare;
|
||||
impl Node for Bare {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema { inputs: vec![], output: vec![] }
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![] }
|
||||
}
|
||||
fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
None
|
||||
@@ -105,4 +122,16 @@ mod tests {
|
||||
}
|
||||
assert_eq!(Bare.label(), "node");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_carries_declared_params() {
|
||||
let s = NodeSchema {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
};
|
||||
assert_eq!(s.params.len(), 1);
|
||||
assert_eq!(s.params[0].name, "length");
|
||||
assert_eq!(s.params[0].kind, ScalarKind::I64);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! adds no optimisation pass (CSE/DCE, sweep-invariant hoisting are deferred,
|
||||
//! C23) and no external dependency (C16).
|
||||
|
||||
use aura_core::{Node, NodeSchema, ScalarKind};
|
||||
use aura_core::{Node, NodeSchema, ParamSpec, ScalarKind};
|
||||
|
||||
use crate::harness::{BootstrapError, Edge, Harness, SourceSpec, Target};
|
||||
|
||||
@@ -110,7 +110,7 @@ impl Composite {
|
||||
})
|
||||
.collect();
|
||||
let out_field = self.nodes[self.output.node].schema().output[self.output.field];
|
||||
NodeSchema { inputs, output: vec![out_field] }
|
||||
NodeSchema { inputs, output: vec![out_field], params: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,18 @@ impl Blueprint {
|
||||
&self.edges
|
||||
}
|
||||
|
||||
/// The aggregated, flat, path-qualified param-space (C12): every node's declared
|
||||
/// params, concatenated in the deterministic depth-first item order `lower_items`
|
||||
/// uses, so a param's slot here matches the later flat-node order (#31 binds
|
||||
/// slot-by-slot). Read-only graph-as-data (C9); does not compile. Names are
|
||||
/// non-load-bearing: a composite's `name()` is prefixed at each level, but
|
||||
/// same-type siblings in one composite share a name — uniqueness is at the slot.
|
||||
pub fn param_space(&self) -> Vec<ParamSpec> {
|
||||
let mut out = Vec::new();
|
||||
collect_params(&self.nodes, "", &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Lower to the flat compilat: inline every composite (recursive), offset
|
||||
/// interior indices, rewrite edges, and fan input roles out. The run loop and
|
||||
/// `bootstrap`'s data model are unchanged; the lowered compilat is wired by raw
|
||||
@@ -198,6 +210,35 @@ impl Blueprint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursive read-only walk for `Blueprint::param_space`: a leaf contributes its
|
||||
/// declared params under the running path prefix; a composite pushes its `name()`
|
||||
/// onto the path and recurses. Order mirrors `lower_items` (items in declared order,
|
||||
/// composites depth-first) so a param's slot matches the later flat-node order.
|
||||
fn collect_params(items: &[BlueprintNode], prefix: &str, out: &mut Vec<ParamSpec>) {
|
||||
for item in items {
|
||||
match item {
|
||||
BlueprintNode::Leaf(node) => {
|
||||
for p in node.schema().params {
|
||||
let name = if prefix.is_empty() {
|
||||
p.name
|
||||
} else {
|
||||
format!("{prefix}.{}", p.name)
|
||||
};
|
||||
out.push(ParamSpec { name, kind: p.kind });
|
||||
}
|
||||
}
|
||||
BlueprintNode::Composite(c) => {
|
||||
let child = if prefix.is_empty() {
|
||||
c.name().to_string()
|
||||
} else {
|
||||
format!("{prefix}.{}", c.name())
|
||||
};
|
||||
collect_params(c.nodes(), &child, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How one blueprint item resolved into the flat compilat. Edges and source
|
||||
/// targets to/from an item are resolved through this.
|
||||
enum ItemLowering {
|
||||
@@ -376,6 +417,7 @@ mod tests {
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
],
|
||||
output: vec![FieldSpec { name: "v", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -419,6 +461,7 @@ mod tests {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }],
|
||||
output: vec![FieldSpec { name: "v", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -438,6 +481,7 @@ mod tests {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -453,6 +497,7 @@ mod tests {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec { kind: ScalarKind::I64, lookback: 1, firing: Firing::Any }],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -745,4 +790,115 @@ mod tests {
|
||||
assert!(!comp_eq_v.is_empty(), "equity trace must be populated");
|
||||
assert!(!comp_ex_v.is_empty(), "exposure trace must be populated");
|
||||
}
|
||||
|
||||
/// E2E (cycle 0015): the C23/#31 cross-cutting invariant — `param_space()` is a
|
||||
/// parallel projection of the *same* traversal `compile` inlines, so a param's
|
||||
/// slot in the aggregated space lines up, in order and kind, with the declared
|
||||
/// params of the compiled flat nodes (the premise #31's slot-by-slot binding
|
||||
/// rests on). Driven on the realistic SMA-cross harness, not a synthetic graph,
|
||||
/// and on the blueprint *as compiled* — so a future inliner reorder that
|
||||
/// silently desynced the two projections would fail here, not just the isolated
|
||||
/// `param_space` order tests.
|
||||
#[test]
|
||||
fn param_space_mirrors_compiled_flat_node_param_order() {
|
||||
let (bp, _rx_eq, _rx_ex) = composite_sma_cross_harness();
|
||||
|
||||
// the aggregated, path-qualified projection
|
||||
let space = bp.param_space();
|
||||
|
||||
// the same blueprint, actually compiled to its flat node array; each flat
|
||||
// node's own declared params, concatenated in flat-node order
|
||||
let (flat_nodes, _sources, _edges) = bp.compile().expect("harness compiles");
|
||||
let from_compilat: Vec<ParamSpec> =
|
||||
flat_nodes.iter().flat_map(|n| n.schema().params).collect();
|
||||
|
||||
// same count, same per-slot kind, same order — the projection mirrors the
|
||||
// compilation (names differ: param_space path-qualifies, the raw node does
|
||||
// not, so compare on the load-bearing axis, kind-by-slot)
|
||||
assert_eq!(space.len(), from_compilat.len(), "param count must match the compilat");
|
||||
assert_eq!(
|
||||
space.iter().map(|p| p.kind).collect::<Vec<_>>(),
|
||||
from_compilat.iter().map(|p| p.kind).collect::<Vec<_>>(),
|
||||
"per-slot param kinds must line up with the compiled flat-node order",
|
||||
);
|
||||
// the realistic harness's concrete space: two SMA lengths (I64) + Exposure
|
||||
// scale (F64); Sub/SimBroker/Recorder declare none
|
||||
assert_eq!(
|
||||
space.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
|
||||
["sma_cross.length", "sma_cross.length", "scale"],
|
||||
);
|
||||
assert_eq!(
|
||||
space.iter().map(|p| p.kind).collect::<Vec<_>>(),
|
||||
[ScalarKind::I64, ScalarKind::I64, ScalarKind::F64],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_space_is_flat_path_qualified_and_slot_disambiguated() {
|
||||
use aura_std::{LinComb, Sma, Sub};
|
||||
// inner composite "fast_slow": two SMAs (same type → same param name) + a Sub
|
||||
let fast_slow = Composite::new(
|
||||
"fast_slow",
|
||||
vec![Sma::new(2).into(), Sma::new(4).into(), Sub::new().into()],
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||
],
|
||||
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
|
||||
OutPort { node: 2, field: 0 },
|
||||
);
|
||||
// outer composite "strategy": the inner composite + a LinComb([1,-1])
|
||||
let strategy = Composite::new(
|
||||
"strategy",
|
||||
vec![BlueprintNode::Composite(fast_slow), LinComb::new(vec![1.0, -1.0]).into()],
|
||||
vec![],
|
||||
vec![vec![Target { node: 0, slot: 0 }]],
|
||||
OutPort { node: 0, field: 0 },
|
||||
);
|
||||
let bp = Blueprint::new(vec![BlueprintNode::Composite(strategy)], vec![], vec![]);
|
||||
|
||||
let space = bp.param_space();
|
||||
let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
[
|
||||
"strategy.fast_slow.length", // slot 0 — Sma(2)
|
||||
"strategy.fast_slow.length", // slot 1 — Sma(4): same name, distinct slot
|
||||
"strategy.weights[0]", // slot 2 — LinComb weight 0
|
||||
"strategy.weights[1]", // slot 3 — LinComb weight 1
|
||||
]
|
||||
);
|
||||
assert_eq!(space[0].kind, ScalarKind::I64);
|
||||
assert_eq!(space[2].kind, ScalarKind::F64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_leaf_params_are_unqualified() {
|
||||
use aura_std::Sma;
|
||||
let bp = Blueprint::new(vec![Sma::new(3).into()], vec![], vec![]);
|
||||
let space = bp.param_space();
|
||||
assert_eq!(space.len(), 1);
|
||||
assert_eq!(space[0].name, "length"); // no path prefix at the top level
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_space_is_deterministic() {
|
||||
use aura_std::{LinComb, Sma};
|
||||
let bp = Blueprint::new(
|
||||
vec![Sma::new(2).into(), LinComb::new(vec![1.0, -1.0]).into()],
|
||||
vec![],
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(bp.param_space(), bp.param_space()); // pure structural function (C1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_space_empty_for_paramless_and_empty_blueprints() {
|
||||
use aura_std::{Add, Sub};
|
||||
let only_paramless =
|
||||
Blueprint::new(vec![Sub::new().into(), Add::new().into()], vec![], vec![]);
|
||||
assert!(only_paramless.param_space().is_empty());
|
||||
let empty = Blueprint::new(vec![], vec![], vec![]);
|
||||
assert!(empty.param_space().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,7 @@ mod tests {
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -387,6 +388,7 @@ mod tests {
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Barrier(0) },
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -410,6 +412,7 @@ mod tests {
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -448,6 +451,7 @@ mod tests {
|
||||
FieldSpec { name: "close", kind: ScalarKind::F64 },
|
||||
FieldSpec { name: "volume", kind: ScalarKind::F64 },
|
||||
],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -477,6 +481,7 @@ mod tests {
|
||||
FieldSpec { name: "f", kind: ScalarKind::F64 },
|
||||
FieldSpec { name: "i", kind: ScalarKind::I64 },
|
||||
],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -498,6 +503,7 @@ mod tests {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
|
||||
@@ -41,7 +41,7 @@ pub use report::{f64_field, summarize, RunManifest, RunMetrics, RunReport};
|
||||
// #29: re-export the core scalar vocabulary a Blueprint builder needs
|
||||
// (SourceSpec.kind is a ScalarKind; sources/Recorder columns are Scalar /
|
||||
// Firing / Timestamp) so a graph builder has one import surface, not two.
|
||||
pub use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
||||
pub use aura_core::{Firing, ParamSpec, Scalar, ScalarKind, Timestamp};
|
||||
|
||||
#[cfg(test)]
|
||||
mod reexport_tests {
|
||||
|
||||
@@ -41,6 +41,7 @@ impl Node for Add {
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! exposure stream`: one f64 input, one f64 output `clamp(signal / scale, -1, +1)`.
|
||||
//! `scale` sets which signal magnitude maps to full exposure (sizing lives here).
|
||||
|
||||
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind};
|
||||
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Node, NodeSchema, ParamSpec, Scalar, ScalarKind};
|
||||
|
||||
/// Bounded exposure from a raw signal score: `clamp(signal / scale, -1.0, +1.0)`.
|
||||
/// Emits `None` until its input is present (warm-up filter, C8).
|
||||
@@ -25,6 +25,7 @@ impl Node for Exposure {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }],
|
||||
output: vec![FieldSpec { name: "exposure", kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
//! general combinator for the north-star "combine signals with weights" move
|
||||
//! (C10). `LinComb([1.0, 1.0])` is `Add`; `LinComb([1.0, -1.0])` is `Sub`. The
|
||||
//! weights are construction parameters that configure the node and fix its
|
||||
//! arity (`weights.len()` inputs) — the natural home for the combination tuning
|
||||
//! params, though the schema-level tunable-param surface (C12/C19) is not yet
|
||||
//! implemented: the node contract deliberately keeps params out of the schema
|
||||
//! for now (see aura_core's node contract and spec 0002's "Out of scope").
|
||||
//! arity (`weights.len()` inputs); they are also the combination's tunable
|
||||
//! params, declared in the schema (cycle 0015) as `weights[0..N]` — N flat
|
||||
//! indexed `F64` knobs that `Blueprint::param_space` aggregates (C8/C12/C19).
|
||||
|
||||
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind};
|
||||
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Node, NodeSchema, ParamSpec, Scalar, ScalarKind};
|
||||
|
||||
/// Weighted sum of `N` f64 inputs: `Σ weights[i] · input[i]`. The `weights` are
|
||||
/// construction parameters that configure the node and fix its arity
|
||||
@@ -48,6 +47,9 @@ impl Node for LinComb {
|
||||
.map(|_| InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any })
|
||||
.collect(),
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: (0..self.weights.len())
|
||||
.map(|i| ParamSpec { name: format!("weights[{i}]"), kind: ScalarKind::F64 })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ impl Node for Recorder {
|
||||
.map(|&kind| InputSpec { kind, lookback: 1, firing: self.firing })
|
||||
.collect(),
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ impl Node for SimBroker {
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }, // 1 price
|
||||
],
|
||||
output: vec![FieldSpec { name: "equity", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! `Node` contract is authorable from a downstream crate and evaluable with no
|
||||
//! engine present (the test drives it by hand, as the sim loop later will).
|
||||
|
||||
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind};
|
||||
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Node, NodeSchema, ParamSpec, Scalar, ScalarKind};
|
||||
|
||||
/// Simple moving average over the last `length` values of one f64 input.
|
||||
pub struct Sma {
|
||||
@@ -28,6 +28,7 @@ impl Node for Sma {
|
||||
firing: Firing::Any,
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,4 +110,31 @@ mod tests {
|
||||
let (tx, _rx) = std::sync::mpsc::channel();
|
||||
assert_eq!(Recorder::new(&[ScalarKind::F64], Firing::Any, tx).label(), "Recorder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nodes_declare_expected_params() {
|
||||
use crate::{Add, Exposure, LinComb, Recorder, SimBroker, Sub};
|
||||
use aura_core::{Firing, ParamSpec, ScalarKind};
|
||||
// single scalar knobs
|
||||
assert_eq!(
|
||||
Sma::new(3).schema().params,
|
||||
vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
);
|
||||
assert_eq!(
|
||||
Exposure::new(0.5).schema().params,
|
||||
vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
||||
);
|
||||
// vector knob expands flat to N indexed F64 entries
|
||||
let lc = LinComb::new(vec![1.0, -1.0]).schema().params;
|
||||
assert_eq!(lc.len(), 2);
|
||||
assert_eq!(lc[0].name, "weights[0]");
|
||||
assert_eq!(lc[1].name, "weights[1]");
|
||||
assert!(lc.iter().all(|p| p.kind == ScalarKind::F64));
|
||||
// param-less nodes declare empty
|
||||
assert!(Sub::new().schema().params.is_empty());
|
||||
assert!(Add::new().schema().params.is_empty());
|
||||
assert!(SimBroker::new(0.0001).schema().params.is_empty());
|
||||
let (tx, _rx) = std::sync::mpsc::channel();
|
||||
assert!(Recorder::new(&[ScalarKind::F64], Firing::Any, tx).schema().params.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ impl Node for Sub {
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user