//! `Add` — two-input f64 sum (input 0 plus input 1), the companion to `Sub`. //! Combines two signal streams into one — the most basic combinator for the //! north-star "combine one signal with another" research move (C10). use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind}; /// Two-input f64 sum: input 0 plus input 1. Emits `None` until both inputs /// have a value. /// /// # Firing and warm-up /// /// Both inputs are [`Firing::Any`](aura_core::Firing::Any) — a *mode-A as-of /// join*: the node fires on every cycle in which either leg is fresh (once both /// have produced a value), pairing the fresh leg with the held value of the /// other. Until both legs have a value it emits `None` (no cold-leg-as-`0.0`). /// With heterogeneous sources sharing a timestamp (same `ts` from two sources = /// two distinct cycles, C4), a fired node emits one row per *cycle*, so a /// recorded combined stream may carry more than one row per timestamp. pub struct Add { out: [Cell; 1], } impl Add { /// Build an `Add` node. pub fn new() -> Self { Self { out: [Cell::from_f64(0.0)] } } /// The param-generic recipe for a blueprint primitive: paramless, builds through /// `Add::new`. pub fn builder() -> PrimitiveBuilder { PrimitiveBuilder::new( "Add", NodeSchema { inputs: vec![ PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() }, PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() }, ], output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], }, |_| Box::new(Add::new()), ) } } impl Default for Add { fn default() -> Self { Self::new() } } impl Node for Add { fn lookbacks(&self) -> Vec { vec![1, 1] } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { let a = ctx.f64_in(0); let b = ctx.f64_in(1); if a.is_empty() || b.is_empty() { return None; } self.out[0] = Cell::from_f64(a[0] + b[0]); Some(&self.out) } fn label(&self) -> String { "Add".to_string() } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Scalar, Timestamp}; #[test] fn add_is_sum_once_both_inputs_present() { let mut add = Add::new(); let mut inputs = vec![ AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), ]; // only input 0 present -> None inputs[0].push(Scalar::f64(10.0)).unwrap(); assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), None); // both present -> a + b inputs[1].push(Scalar::f64(4.0)).unwrap(); assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(14.0)].as_slice())); } #[test] fn input_slots_are_named_lhs_rhs() { let a = Add::builder(); let names: Vec<&str> = a.schema().inputs.iter().map(|p| p.name.as_str()).collect(); assert_eq!(names, ["lhs", "rhs"]); } }