feat(aura-std): EqConst — i64->bool equality gate

Stateless gate, out = (value == target): the i64->bool comparator the
session-breakout strategy needs to turn Session's bars_since_open into
isBar3/isBar5Close (two instances bound to 3 and 5). The operator is topology
(a concrete node type), only target is a knob (C11/C12). Build-step 1 of
milestone 'Strategy node vocabulary I'.

closes #84
This commit is contained in:
2026-06-17 16:44:11 +02:00
parent 16adb2b949
commit 550895d5fd
2 changed files with 107 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
//! `EqConst` — a stateless `i64 -> bool` equality gate: `out = (value == target)`.
//!
//! The `i64 -> bool` analogue of a relational comparator (`Gt` is the `f64 -> bool`
//! cousin). Its purpose in the session-breakout strategy is to turn `Session`'s
//! `bars_since_open: i64` into per-bar booleans (`isBar3`, `isBar5Close`) via two
//! instances bound to `3` and `5`.
//!
//! The **operator is topology** — `EqConst` is a concrete node type, not an op
//! selected by a swept param. Only `target` is a knob (C11/C12); the `==` is fixed
//! by the type. (`!=`, `<`, `>` would each be their own node type, never a param.)
//! One `i64` input (`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, ParamSpec, PortSpec, PrimitiveBuilder,
ScalarKind,
};
/// Stateless `i64 -> bool` equality gate: emits `value == target` each cycle.
/// Emits `None` until its input is present (warm-up filter, C8).
pub struct EqConst {
target: i64,
out: [Cell; 1],
}
impl EqConst {
/// Build an equality gate that fires `true` exactly when its input equals
/// `target`.
pub fn new(target: i64) -> Self {
Self { target, out: [Cell::from_bool(false)] }
}
/// The param-generic recipe for a blueprint primitive: declares `target` and
/// builds through `EqConst::new` (the slice is kind-checked before `build`
/// runs, so the typed read is total).
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"EqConst",
NodeSchema {
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "value".into() }],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
params: vec![ParamSpec { name: "target".into(), kind: ScalarKind::I64 }],
},
|p| Box::new(EqConst::new(p[0].i64())),
)
}
}
impl Node for EqConst {
fn lookbacks(&self) -> Vec<usize> {
vec![1]
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
let w = ctx.i64_in(0);
if w.is_empty() {
return None; // not yet warmed up (C8 filter)
}
self.out[0] = Cell::from_bool(w[0] == self.target);
Some(&self.out)
}
fn label(&self) -> String {
format!("EqConst({})", self.target)
}
}
#[cfg(test)]
mod tests {
use super::*;
use aura_core::{AnyColumn, Scalar, Timestamp};
#[test]
fn eqconst_gates_on_equality_with_target() {
// The core property: out == (value == target). Bound to target=3, the gate
// fires true exactly on the matching input and false on every mismatch —
// this is what makes `bars_since_open == 3` the isBar3 boolean.
let node_for_depth = EqConst::new(3);
// size the input column from the node's lookback, as bootstrap will at wiring
let mut inputs =
vec![AnyColumn::with_capacity(ScalarKind::I64, node_for_depth.lookbacks()[0])];
let mut node = node_for_depth;
let feed = [2_i64, 3, 5];
let expect = [Some(false), Some(true), Some(false)];
for (v, want) in feed.iter().zip(expect) {
inputs[0].push(Scalar::i64(*v)).unwrap();
let got = node.eval(Ctx::new(&inputs, Timestamp(0)));
match want {
Some(b) => assert_eq!(got, Some([Cell::from_bool(b)].as_slice())),
None => assert_eq!(got, None),
}
}
}
#[test]
fn eqconst_is_none_until_input_present() {
// a warm-up / empty-input cycle filters (C8) — like every other stateless node
let mut node = EqConst::new(3);
let inputs = vec![AnyColumn::with_capacity(ScalarKind::I64, 1)];
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);
}
}
+2
View File
@@ -17,6 +17,7 @@
mod add;
mod ema;
mod eqconst;
mod exposure;
mod lincomb;
mod recorder;
@@ -25,6 +26,7 @@ mod sma;
mod sub;
pub use add::Add;
pub use ema::Ema;
pub use eqconst::EqConst;
pub use exposure::Exposure;
pub use lincomb::LinComb;
pub use recorder::Recorder;