Files
claude a32dc38d18 feat(std): meaning lines for all 27 shipped node schemas
C29 compile/unit seam, task 3 of the self-description plan: every
aura-std NodeSchema literal threads its one-line doc. Four texts were
corrected against the actual eval/finalize semantics rather than taken
from the plan table verbatim: Delay is a lag-N register (not one-step),
GatedRecorder flushes an ungated final row at finalize, Latch is a
level-sensitive set/reset register (captures no input), SeriesReducer
emits its single summary row at finalize (not per cycle).

Gate: cargo build -p aura-std --lib clean; full std test run follows at
the all-crates gate once strategy/backtest/engine thread their sites
(the earlier per-crate test gate was unsatisfiable in isolation --
std's dev-deps pull crates whose sites belong to later tasks).

refs #316
2026-07-23 15:25:16 +02:00

128 lines
4.6 KiB
Rust
Raw Permalink 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.
//! `Select` — the stateless 2:1 mux `bool × f64 × f64 -> f64`: forwards
//! `then` when `cond` is true, `otherwise` when it is false.
//!
//! The missing RTL standard cell (#281): the generic consumer of the Bool
//! plane back into F64. Until it existed, conditional values required
//! arithmetic contortions; with it, `count_true` composes as
//! `CumSum(Select(cond, 1.0, 0.0))` — no Counter primitive needed.
//!
//! Three inputs (`cond: Bool`, `then: F64`, `otherwise: F64`, each
//! `Firing::Any`), one `f64` output, no params, stateless. Emits `None`
//! until **all three** legs are warm — the mux reads both data inputs, as
//! [`Gt`](crate::Gt) requires both of its legs.
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
/// Stateless 2:1 mux: `out = if cond { then } else { otherwise }`, decided
/// fresh each fired cycle. Emits `None` until all three legs are warm (C8).
pub struct Select {
out: [Cell; 1],
}
impl Select {
/// Build a `Select` node.
pub fn new() -> Self {
Self { out: [Cell::from_f64(0.0)] }
}
/// The param-generic recipe for a blueprint primitive: paramless, builds
/// through `Select::new`.
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"Select",
NodeSchema {
inputs: vec![
PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "cond".into() },
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "then".into() },
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "otherwise".into() },
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
doc: "chooses between two inputs by a boolean selector",
},
|_| Box::new(Select::new()),
)
}
}
impl Default for Select {
fn default() -> Self {
Self::new()
}
}
impl Node for Select {
fn lookbacks(&self) -> Vec<usize> {
vec![1, 1, 1]
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
let cond = ctx.bool_in(0);
let then = ctx.f64_in(1);
let otherwise = ctx.f64_in(2);
if cond.is_empty() || then.is_empty() || otherwise.is_empty() {
return None; // all three legs required (C8 warm-up gate)
}
let c = cond.get(0).unwrap_or(false);
self.out[0] = Cell::from_f64(if c { then[0] } else { otherwise[0] });
Some(&self.out)
}
fn label(&self) -> String {
"Select".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use aura_core::{AnyColumn, Scalar, Timestamp};
fn mux_inputs() -> Vec<AnyColumn> {
vec![
AnyColumn::with_capacity(ScalarKind::Bool, 1),
AnyColumn::with_capacity(ScalarKind::F64, 1),
AnyColumn::with_capacity(ScalarKind::F64, 1),
]
}
#[test]
fn select_muxes_per_cycle_by_cond() {
// PROPERTY: out = if cond { then } else { otherwise }, re-decided each
// cycle (stateless) — the 2:1 mux, both branches exercised plus a flip.
let mut node = Select::new();
let mut inputs = mux_inputs();
let cases = [((true, 1.0, 9.0), 1.0), ((false, 1.0, 9.0), 9.0), ((true, 5.0, 7.0), 5.0)];
for ((c, t, o), want) in cases {
inputs[0].push(Scalar::bool(c)).unwrap();
inputs[1].push(Scalar::f64(t)).unwrap();
inputs[2].push(Scalar::f64(o)).unwrap();
let got = node.eval(Ctx::new(&inputs, Timestamp(0)));
assert_eq!(got, Some([Cell::from_f64(want)].as_slice()));
}
}
#[test]
fn select_is_none_until_all_three_legs_warm() {
// The mux reads BOTH data inputs: a cold otherwise-leg gates the output
// even while cond is true and then is present.
let mut node = Select::new();
let mut inputs = mux_inputs();
inputs[0].push(Scalar::bool(true)).unwrap();
inputs[1].push(Scalar::f64(1.0)).unwrap();
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);
inputs[2].push(Scalar::f64(9.0)).unwrap();
assert_eq!(
node.eval(Ctx::new(&inputs, Timestamp(0))),
Some([Cell::from_f64(1.0)].as_slice())
);
}
#[test]
fn input_slots_are_named_cond_then_otherwise() {
let names: Vec<String> =
Select::builder().schema().inputs.iter().map(|p| p.name.clone()).collect();
assert_eq!(names, ["cond", "then", "otherwise"]);
}
}