Files
Aura/crates/aura-std/src/gt.rs
T
Brummel 4ade475dc3 feat(aura-std): Gt — strict f64->bool greater-than comparator
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
2026-06-17 16:55:19 +02:00

128 lines
4.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `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"]);
}
}