4ade475dc3
Stateless f64 x f64 -> bool, out = (a > b), STRICT: a == b emits false (a close exactly equal to the previous bar's high is not a breakout). Computes breakout = close15 > prevHigh15 in the session-breakout strategy. First bool-emitting f64 comparator; the operator is topology (relational siblings are separate types, never a swept param). Build-step 2 of milestone 'Strategy node vocabulary I'. closes #85
128 lines
4.5 KiB
Rust
128 lines
4.5 KiB
Rust
//! `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<usize> {
|
||
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];
|
||
// a<b a>b 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"]);
|
||
}
|
||
}
|