//! `Gt` — a stateless `f64 × f64 -> bool` comparator: `out = (a > b)`, STRICT //! greater-than. //! //! The first **bool-emitting f64 comparator** in `aura-std` (the `f64` cousin of //! `EqConst`'s `i64 -> bool` gate). Its purpose in the session-breakout strategy //! is `breakout = close15 > prevHigh15`: a fresh 15m close strictly above the //! prior bar's high. **Strict `>` is load-bearing** — a close exactly *equal* to //! the previous high is **not** a breakout, so `a == b` emits `false`. //! //! The **operator is topology** — `Gt` is a concrete node type, not an op //! selected by a swept param. The relational siblings (`Lt`, `Ge`, `Le`, `Eq`) //! would each be their own node type, never a param. Two `f64` inputs //! (`Firing::Any`), one `bool` output, allocation-free on the hot path (the //! single-cell output buffer is sized once at construction, C7). use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind}; /// Stateless `f64 × f64 -> bool` strict comparator: emits `a > b` each cycle. /// Emits `None` until **both** inputs have a value (warm-up gate, C8). pub struct Gt { out: [Cell; 1], } impl Gt { /// Build a `Gt` node. pub fn new() -> Self { Self { out: [Cell::from_bool(false)] } } /// The param-generic recipe for a blueprint primitive: paramless, builds /// through `Gt::new`. pub fn builder() -> PrimitiveBuilder { PrimitiveBuilder::new( "Gt", NodeSchema { inputs: vec![ PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "a".into() }, PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "b".into() }, ], output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }], params: vec![], }, |_| Box::new(Gt::new()), ) } } impl Default for Gt { fn default() -> Self { Self::new() } } impl Node for Gt { 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; // not yet warmed up — both legs required (C8 filter) } self.out[0] = Cell::from_bool(a[0] > b[0]); // STRICT: a == b -> false Some(&self.out) } fn label(&self) -> String { "Gt".to_string() } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Scalar, Timestamp}; #[test] fn gt_is_strict_greater_than_once_both_inputs_present() { // The core property: out == (a > b), STRICT. Three regions are covered, // and the a==b case is the load-bearing one — a close exactly equal to // the previous high is NOT a breakout, so equality emits `false`. let mut node = Gt::new(); let mut inputs = vec![ AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), ]; let a_feed = [1.0_f64, 5.0, 3.0]; let b_feed = [2.0_f64, 2.0, 3.0]; // ab a==b let expect = [Some(false), Some(true), Some(false)]; for ((av, bv), want) in a_feed.iter().zip(b_feed.iter()).zip(expect) { inputs[0].push(Scalar::f64(*av)).unwrap(); inputs[1].push(Scalar::f64(*bv)).unwrap(); let got = node.eval(Ctx::new(&inputs, Timestamp(0))); assert_eq!(got, Some([Cell::from_bool(want.unwrap())].as_slice())); } } #[test] fn gt_is_none_until_both_inputs_present() { // Both-inputs warm-up gate (C8), like `Sub`: only one leg present -> None. let mut node = Gt::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!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); // both present -> a strict-gt bool (10.0 > 4.0 == true) inputs[1].push(Scalar::f64(4.0)).unwrap(); assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_bool(true)].as_slice())); } #[test] fn input_slots_are_named_a_b() { let g = Gt::builder(); let names: Vec<&str> = g.schema().inputs.iter().map(|p| p.name.as_str()).collect(); assert_eq!(names, ["a", "b"]); } }