90e298d3b4
First fieldtest of the sum combinators. A standalone downstream-consumer crate
(fieldtests/cycle-0008-sum-combinators/) path-depends on the engine crates and
exercises the post-0008 surface from the public interface only (rustdoc + ledger
+ glossary + project-layout, never crates/*/src).
Primary axis empirically met: the north-star two-signal combine move now uses the
shipped Add (Add::new() dropped in exactly where the 0007 fixture hand-authored
an Add2) — the 0007 boilerplate is retired, end to end through SimBroker to a
deterministic recorded pip curve.
Findings: 0 bugs, 1 friction, 2 spec_gaps, 3 working.
- working ×3: Add2 retired; LinComb weighted/variadic sums exact (<1e-12);
warm-up barrier + empty-weights panic as documented.
- spec_gap: Add/LinComb per-input firing policy (Firing::Any / mode-A as-of
join) absent from the public surface — same class as the 0007 SimBroker gap.
- spec_gap: a fired combinator emits one row per *cycle*, so a heterogeneous
same-timestamp multi-source trace can carry >1 row per timestamp (correct per
C4/C5, undocumented at this surface).
- friction: nested consumer crate still needs the empty [workspace] table
(carried from 0006/0007; #9 closed the docs half; folds into aura new).
Spec feeds the next plan as reference.
103 lines
3.9 KiB
Rust
103 lines
3.9 KiB
Rust
//! Example 3 — the variadic-arity case: `LinComb` over N>2 signal legs.
|
|
//!
|
|
//! Three moving averages combined with weights [0.5, 0.3, 0.2]. Verifies the
|
|
//! arity is fixed by `weights.len()` (3 input slots), and that the recorded
|
|
//! output equals the 3-leg weighted sum on every fired cycle. Also confirms
|
|
//! the firing barrier: LinComb withholds output until ALL THREE legs warm up
|
|
//! (i.e. until the slowest, Sma(4), produces a value).
|
|
|
|
mod recorder;
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use aura_core::ScalarKind;
|
|
use aura_core::{Scalar, Timestamp};
|
|
use aura_engine::{Edge, Harness, SourceSpec, Target};
|
|
use aura_std::{LinComb, Sma};
|
|
|
|
use recorder::{Recorder, Tape};
|
|
|
|
fn main() {
|
|
const SMA2: usize = 0;
|
|
const SMA3: usize = 1;
|
|
const SMA4: usize = 2;
|
|
const LINCOMB: usize = 3;
|
|
const REC2: usize = 4;
|
|
const REC3: usize = 5;
|
|
const REC4: usize = 6;
|
|
const REC_OUT: usize = 7;
|
|
|
|
let t2: Tape = Rc::new(RefCell::new(Vec::new()));
|
|
let t3: Tape = Rc::new(RefCell::new(Vec::new()));
|
|
let t4: Tape = Rc::new(RefCell::new(Vec::new()));
|
|
let tout: Tape = Rc::new(RefCell::new(Vec::new()));
|
|
|
|
let weights = vec![0.5, 0.3, 0.2];
|
|
|
|
let nodes: Vec<Box<dyn aura_core::Node>> = vec![
|
|
Box::new(Sma::new(2)),
|
|
Box::new(Sma::new(3)),
|
|
Box::new(Sma::new(4)),
|
|
Box::new(LinComb::new(weights.clone())),
|
|
Box::new(Recorder::new(t2.clone())),
|
|
Box::new(Recorder::new(t3.clone())),
|
|
Box::new(Recorder::new(t4.clone())),
|
|
Box::new(Recorder::new(tout.clone())),
|
|
];
|
|
|
|
let sources = vec![SourceSpec {
|
|
kind: ScalarKind::F64,
|
|
targets: vec![
|
|
Target { node: SMA2, slot: 0 },
|
|
Target { node: SMA3, slot: 0 },
|
|
Target { node: SMA4, slot: 0 },
|
|
],
|
|
}];
|
|
|
|
let edges = vec![
|
|
Edge { from: SMA2, to: LINCOMB, slot: 0, from_field: 0 },
|
|
Edge { from: SMA3, to: LINCOMB, slot: 1, from_field: 0 },
|
|
Edge { from: SMA4, to: LINCOMB, slot: 2, from_field: 0 },
|
|
Edge { from: SMA2, to: REC2, slot: 0, from_field: 0 },
|
|
Edge { from: SMA3, to: REC3, slot: 0, from_field: 0 },
|
|
Edge { from: SMA4, to: REC4, slot: 0, from_field: 0 },
|
|
Edge { from: LINCOMB, to: REC_OUT, slot: 0, from_field: 0 },
|
|
];
|
|
|
|
let mut harness = Harness::bootstrap(nodes, sources, edges).expect("bootstrap");
|
|
|
|
let prices: &[f64] = &[10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0];
|
|
let stream: Vec<(Timestamp, Scalar)> = prices
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &p)| (Timestamp(i as i64), Scalar::F64(p)))
|
|
.collect();
|
|
|
|
harness.run(vec![stream]);
|
|
|
|
let (a, b, c, out) = (t2.borrow(), t3.borrow(), t4.borrow(), tout.borrow());
|
|
println!("sma2 rows={}, sma3 rows={}, sma4 rows={}, lincomb rows={}", a.len(), b.len(), c.len(), out.len());
|
|
|
|
// Sma(4) warms up last (needs 4 samples -> first value at t=3), so the 3-leg
|
|
// barrier means LinComb's first output is at t=3.
|
|
let first_out_ts = out.first().map(|&(t, _)| t.0);
|
|
println!("LinComb first fires at t={:?} (expect t=3, the slowest leg's warm-up)", first_out_ts);
|
|
assert_eq!(first_out_ts, Some(3), "3-leg warm-up barrier");
|
|
|
|
println!("\n t sma2 sma3 sma4 lincomb expected(0.5,0.3,0.2)");
|
|
for &(ts, combined) in out.iter() {
|
|
let s2 = a.iter().find(|&&(t, _)| t == ts).map(|&(_, v)| v).unwrap();
|
|
let s3 = b.iter().find(|&&(t, _)| t == ts).map(|&(_, v)| v).unwrap();
|
|
let s4 = c.iter().find(|&&(t, _)| t == ts).map(|&(_, v)| v).unwrap();
|
|
let expected = 0.5 * s2 + 0.3 * s3 + 0.2 * s4;
|
|
let ok = (combined - expected).abs() < 1e-12;
|
|
println!(
|
|
" {:>2} {:>7.3} {:>7.3} {:>7.3} {:>8.4} {:>8.4} {}",
|
|
ts.0, s2, s3, s4, combined, expected, if ok { "OK" } else { "MISMATCH" }
|
|
);
|
|
assert!(ok, "3-leg weighted sum mismatch at t={}", ts.0);
|
|
}
|
|
println!("\nall 3-leg weighted sums match = OK");
|
|
}
|