//! `Scale` — a stateless `f64 -> f64` multiply-by-constant: `out = input * factor`. //! The standard scalar-gain operator (the `*` is fixed by the type; `factor` is the //! only knob). One f64 input, one f64 output, allocation-free on the hot path. use aura_core::{ Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, ScalarKind, }; /// Stateless scalar gain: emits `input * factor` each cycle. Emits `None` until its /// input is present (warm-up filter, C8). pub struct Scale { factor: f64, out: [Cell; 1], } impl Scale { /// Build a gain node that multiplies its input by `factor`. pub fn new(factor: f64) -> Self { Self { factor, out: [Cell::from_f64(0.0)] } } /// The param-generic recipe for a blueprint primitive: declares `factor` and /// builds through `Scale::new` (the slice is kind-checked before `build` runs). pub fn builder() -> PrimitiveBuilder { PrimitiveBuilder::new( "Scale", NodeSchema { inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "signal".into() }], output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }], }, |p| Box::new(Scale::new(p[0].f64())), ) } } impl Node for Scale { fn lookbacks(&self) -> Vec { vec![1] } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { let w = ctx.f64_in(0); if w.is_empty() { return None; // not yet warmed up (C8 filter) } self.out[0] = Cell::from_f64(w[0] * self.factor); Some(&self.out) } fn label(&self) -> String { format!("Scale({})", self.factor) } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Scalar, Timestamp}; #[test] fn scale_multiplies_input_by_factor() { let mut node = Scale::new(2.0); let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, node.lookbacks()[0])]; inputs[0].push(Scalar::f64(3.0)).unwrap(); assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(6.0)].as_slice())); } #[test] fn scale_is_none_until_input_present() { let mut node = Scale::new(2.0); let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); } }