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.
77 lines
3.1 KiB
Rust
77 lines
3.1 KiB
Rust
//! Example 5 (firing-policy probe) — combine two signals driven by two DIFFERENT
|
||
//! sources that update on different cycles.
|
||
//!
|
||
//! This is the genuinely-heterogeneous combine the north-star reaches for: a
|
||
//! fast price tick × a slower second feed, summed by LinComb. The interesting
|
||
//! question — invisible from the public surface — is LinComb's per-input firing
|
||
//! policy (C6): does a fired-this-cycle leg get combined with the OTHER leg's
|
||
//! *held* value (mode A, as-of join), or does LinComb only fire when both legs
|
||
//! are fresh in the same cycle (mode B barrier)?
|
||
//!
|
||
//! The rustdoc says only "Emits None until all inputs have a value" — it does
|
||
//! not state the firing policy. We feed two sources at interleaved timestamps
|
||
//! and RECORD what LinComb emits, to see which reading the shipped node took.
|
||
//!
|
||
//! source A (f64, fast) ─> LinComb slot 0 ┐
|
||
//! source B (f64, slow) ─> LinComb slot 1 ┴─> Recorder
|
||
|
||
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;
|
||
|
||
use recorder::{Recorder, Tape};
|
||
|
||
fn main() {
|
||
const LINCOMB: usize = 0;
|
||
const REC: usize = 1;
|
||
|
||
let tape: Tape = Rc::new(RefCell::new(Vec::new()));
|
||
|
||
let nodes: Vec<Box<dyn aura_core::Node>> = vec![
|
||
Box::new(LinComb::new(vec![1.0, 1.0])),
|
||
Box::new(Recorder::new(tape.clone())),
|
||
];
|
||
|
||
// Two independent sources, both f64, each feeding one LinComb slot.
|
||
let sources = vec![
|
||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: LINCOMB, slot: 0 }] },
|
||
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: LINCOMB, slot: 1 }] },
|
||
];
|
||
|
||
let edges = vec![Edge { from: LINCOMB, to: REC, slot: 0, from_field: 0 }];
|
||
|
||
let mut harness = Harness::bootstrap(nodes, sources, edges).expect("bootstrap");
|
||
|
||
// Source A fires at t = 0,1,2,3,4,5; source B fires only at t = 1 and t = 4.
|
||
// (Distinct timestamps => distinct cycles, C4.)
|
||
let stream_a: Vec<(Timestamp, Scalar)> = (0..6)
|
||
.map(|i| (Timestamp(i), Scalar::F64(100.0 + i as f64)))
|
||
.collect();
|
||
let stream_b: Vec<(Timestamp, Scalar)> = vec![
|
||
(Timestamp(1), Scalar::F64(10.0)),
|
||
(Timestamp(4), Scalar::F64(20.0)),
|
||
];
|
||
|
||
harness.run(vec![stream_a, stream_b]);
|
||
|
||
let tape = tape.borrow();
|
||
println!("LinComb emitted {} rows from two interleaved sources:", tape.len());
|
||
println!(" (source A: a=100+t at t=0..5; source B: b=10 at t=1, b=20 at t=4)");
|
||
for (ts, v) in tape.iter() {
|
||
println!(" t={:>2} combined={:.4}", ts.0, v);
|
||
}
|
||
println!();
|
||
println!("Interpretation (public surface is SILENT on the firing policy):");
|
||
println!(" - mode A (as-of/hold): once both legs seen, every A-fresh cycle re-fires");
|
||
println!(" combining a's fresh value with b's HELD value.");
|
||
println!(" - mode B (barrier): LinComb fires only on cycles where BOTH legs are");
|
||
println!(" fresh at the same timestamp -> here only t=1 and t=4 would emit.");
|
||
println!(" The recorded rows above reveal which reading shipped.");
|
||
}
|