//! `Const` — a constant-as-param source: emits a fixed `f64` value every cycle //! its clock input fires, ignoring the clock's actual value. Mirrors `EqConst`'s //! constant-as-param pattern (the constant is a bound param, not topology), but //! as a *source*: a zero-input node never evaluates in the total-push engine //! (C8), so `Const` still needs one input purely to be driven by the sim clock //! — its value is discarded. use aura_core::{ Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, ScalarKind, }; /// Emits the bound `value` every cycle its clock input fires; the clock's own /// value is ignored. Emits `None` until the clock input has fired at least once /// (warm-up filter, C8) — same shape as every other stateless node here. pub struct Const { value: f64, out: [Cell; 1], } impl Const { /// Build a constant source bound to `value`. pub fn new(value: f64) -> Self { Self { value, out: [Cell::from_f64(value)] } } /// The param-generic recipe for a blueprint primitive: declares `value` and /// one clock input, builds through `Const::new`. pub fn builder() -> PrimitiveBuilder { PrimitiveBuilder::new( "Const", NodeSchema { inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "clock".into() }], output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "value".into(), kind: ScalarKind::F64 }], doc: "constant-valued stream from a single param", }, |p| Box::new(Const::new(p[0].f64())), ) } } impl Node for Const { 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(self.value); Some(&self.out) } fn label(&self) -> String { format!("Const({})", self.value) } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Scalar, Timestamp}; #[test] fn const_emits_bound_value_regardless_of_clock_value() { // The core property: out == value, always, once the clock has fired at // least once — the clock's own reading is never observed in the output. let mut node = Const::new(7.0); let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; inputs[0].push(Scalar::f64(-100.0)).unwrap(); assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(7.0)].as_slice())); inputs[0].push(Scalar::f64(42.0)).unwrap(); assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(7.0)].as_slice())); } #[test] fn const_is_none_until_clock_present() { let mut node = Const::new(7.0); let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); } }