77a1d26017
Cycle 0002, the second walking-skeleton slice on top of the 0001 substrate:
the interface every node forever implements (C8), proven end-to-end by a real
node with no engine present.
- `Node` (aura-core) — `schema() -> NodeSchema` declares inputs (kind +
lookback) and the single output kind; `eval(&mut self, Ctx) -> Option<Scalar>`
computes one cycle's output (`None` = filter / not-yet-warmed-up). `&mut self`
so a node may keep its own derived state.
- `Ctx<'a>` (aura-core) — a `Copy` borrow-wrapper handing `eval` zero-copy,
financial-indexed `Window`s per input (`ctx.f64_in(i)[k]`, index 0 = newest).
A kind mismatch panics ("engine bug") — wiring guarantees the kind, so a
mismatch can only mean the wiring layer is broken; this keeps node-author code
clean (`w[k]`, not `w?[k]`).
- `AnyColumn::as_f64/as_i64/as_bool/as_ts` (aura-core) — the read-side mirror of
the existing `as_*_mut`, the mechanism `Ctx` uses to get a typed window from a
type-erased edge. Closes the read-side gap the cycle-0001 audit recorded.
- `Sma` (aura-std) — the skeleton's first real block: a producer node computing
the moving mean of one f64 input, emitting `None` until warmed up. Authored in
a downstream crate (proving aura-core's contract is usable across the crate
boundary) and driven by a hand-written test that mimics exactly what the sim
loop will later generalize: push fresh input, eval, collect.
Deliberately deferred (spec 0002 "Out of scope", recorded as decisions): the
firing policies (C6 — InputSpec carries no firing field yet), the sim loop (C4)
and freshness gating (C5), schema-level tunable params (C12/C19), and the
no-output sink refinement (C8 consumer side).
Gates green: cargo build/test (20: 18 aura-core + 2 aura-std)/clippy -D warnings
all clean; surface-purity grep (no dyn-Any / Rc / RefCell) clean.
refs walking-skeleton
80 lines
2.5 KiB
Rust
80 lines
2.5 KiB
Rust
//! `Sma` — simple moving average over the last `length` values of one f64
|
|
//! input. The walking skeleton's first worked node: it proves the `aura-core`
|
|
//! `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, InputSpec, Node, NodeSchema, Scalar, ScalarKind};
|
|
|
|
/// Simple moving average over the last `length` values of one f64 input.
|
|
pub struct Sma {
|
|
length: usize,
|
|
}
|
|
|
|
impl Sma {
|
|
/// Build an SMA of window `length` (must be >= 1).
|
|
pub fn new(length: usize) -> Self {
|
|
assert!(length >= 1, "SMA length must be >= 1");
|
|
Self { length }
|
|
}
|
|
}
|
|
|
|
impl Node for Sma {
|
|
fn schema(&self) -> NodeSchema {
|
|
NodeSchema {
|
|
inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: self.length }],
|
|
output: ScalarKind::F64,
|
|
}
|
|
}
|
|
|
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<Scalar> {
|
|
let w = ctx.f64_in(0);
|
|
if w.len() < self.length {
|
|
return None; // not yet warmed up
|
|
}
|
|
let mut sum = 0.0;
|
|
for k in 0..self.length {
|
|
sum += w[k]; // index 0 = newest (financial indexing)
|
|
}
|
|
Some(Scalar::F64(sum / self.length as f64))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use aura_core::AnyColumn;
|
|
|
|
#[test]
|
|
fn sma_warms_up_then_tracks_the_window_mean() {
|
|
let mut sma = Sma::new(3);
|
|
let schema = sma.schema();
|
|
|
|
// size the input column from the schema, as the engine will at wiring
|
|
let mut inputs = vec![AnyColumn::with_capacity(
|
|
schema.inputs[0].kind,
|
|
schema.inputs[0].lookback,
|
|
)];
|
|
|
|
let feed = [1.0_f64, 2.0, 3.0, 4.0, 5.0];
|
|
// means of [1,2,3], [2,3,4], [3,4,5] once warmed up
|
|
let expect = [None, None, Some(2.0), Some(3.0), Some(4.0)];
|
|
|
|
for (v, want) in feed.iter().zip(expect) {
|
|
inputs[0].push(Scalar::F64(*v)).unwrap();
|
|
assert_eq!(sma.eval(Ctx::new(&inputs)), want.map(Scalar::F64));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn sma_length_one_is_identity() {
|
|
let mut sma = Sma::new(1);
|
|
let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
|
|
|
inputs[0].push(Scalar::F64(7.0)).unwrap();
|
|
assert_eq!(sma.eval(Ctx::new(&inputs)), Some(Scalar::F64(7.0)));
|
|
|
|
inputs[0].push(Scalar::F64(9.0)).unwrap();
|
|
assert_eq!(sma.eval(Ctx::new(&inputs)), Some(Scalar::F64(9.0)));
|
|
}
|
|
}
|