Files
Aura/crates/aura-std/src/latch.rs
T
Brummel d858caf67b chore: scrub dangling references to deleted specs/plans from sources
docs/specs and docs/plans were retired (prior commit); the source/test
comments that cited them ("spec 0050 §4.1", "spec §Testing N", "per spec",
"the spec's ...") now point at nothing. Strip every such pointer while
preserving the technical substance, the design-ledger contract refs
(C1/C11/C20/C34/C12.1/...), and the Gitea issue refs (#41).

Comment/doc edits only across 20 files — no logic change; full workspace
suite green, clippy clean.
2026-06-18 11:16:28 +02:00

144 lines
5.4 KiB
Rust

//! `Latch` — a level-sensitive set/reset register that emits an `f64` exposure
//! magnitude (`1.0` latched / `0.0` flat), the C5 SR-register of the
//! session-breakout vocabulary.
//!
//! Two `bool` inputs (`set`, `reset`, `Firing::Any`); one `f64` output `value`;
//! no params. **Emits `f64` directly**, not `bool`: a held position *is*
//! exposure `+1`, flat *is* `0.0`, so the output feeds [`SimBroker`](crate::SimBroker)'s
//! `f64` `exposure` slot with no cast — this is the resolution of the `bool → f64`
//! seam (no separate `Exposure`/cast node).
//!
//! **State.** A persisted `held: f64`, initial `0.0`, mutated in `eval` and
//! carried across cycles in the struct — exactly how [`SimBroker`](crate::SimBroker)
//! holds `prev_price`/`prev_exposure` across cycles.
//!
//! **Logic — level SR latch, RESET-DOMINANT.** Per fired eval: if `reset` is on
//! → `held = 0.0`; else if `set` is on → `held = 1.0`; else `held` is unchanged
//! (sample-and-hold). Reset wins when both pulse (defensive default; in the
//! strategy bar3 ≠ bar5 so they never coincide).
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
/// Level-sensitive set/reset register emitting an `f64` exposure magnitude:
/// `held` → `1.0` on `set`, → `0.0` on `reset` (reset-dominant), held across
/// quiet cycles. Emits `None` only while **both** input legs are still cold
/// (nothing latched yet); a present-but-false leg is read as "no pulse".
pub struct Latch {
held: f64, // persisted exposure magnitude: 1.0 = long, 0.0 = flat
out: [Cell; 1],
}
impl Latch {
/// Build a `Latch` node, initially flat (`held = 0.0`).
pub fn new() -> Self {
Self { held: 0.0, out: [Cell::from_f64(0.0)] }
}
/// The param-generic recipe for a blueprint primitive: paramless, builds
/// through `Latch::new`.
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"Latch",
NodeSchema {
inputs: vec![
PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "set".into() },
PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "reset".into() },
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![],
},
|_| Box::new(Latch::new()),
)
}
}
impl Default for Latch {
fn default() -> Self {
Self::new()
}
}
impl Node for Latch {
fn lookbacks(&self) -> Vec<usize> {
vec![1, 1]
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
let set = ctx.bool_in(0);
let reset = ctx.bool_in(1);
if set.is_empty() && reset.is_empty() {
return None; // cold: nothing latched yet (downstream reads flat 0.0)
}
// An absent/empty leg is read as "no pulse"; a present leg pulses when true.
let set_on = set.get(0).unwrap_or(false);
let reset_on = reset.get(0).unwrap_or(false);
if reset_on {
self.held = 0.0; // reset dominates (defensive default)
} else if set_on {
self.held = 1.0;
} // else: sample-and-hold — `held` unchanged
self.out[0] = Cell::from_f64(self.held);
Some(&self.out)
}
fn label(&self) -> String {
"Latch".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use aura_core::{AnyColumn, Scalar, Timestamp};
fn two_bool_inputs() -> Vec<AnyColumn> {
vec![
AnyColumn::with_capacity(ScalarKind::Bool, 1),
AnyColumn::with_capacity(ScalarKind::Bool, 1),
]
}
#[test]
fn latch_holds_set_until_reset_reset_dominant() {
// The core property: a level SR latch that sample-and-HOLDS its f64
// exposure magnitude across quiet bars, with RESET dominant when both
// pulse. Both legs are present every cycle (as in the real graph: set =
// `entry`, reset = `isBar5Close`, both fresh bool streams each bar).
let mut node = Latch::new();
let mut inputs = two_bool_inputs();
// (set, reset) -> expected held
let cases = [
((false, false), 0.0), // initial: flat
((true, false), 1.0), // set latches long
((false, false), 1.0), // HOLD across a quiet bar (load-bearing sample-and-hold)
((false, true), 0.0), // reset goes flat
((false, false), 0.0), // hold flat
((true, true), 0.0), // reset-dominant: both on -> flat (pins the priority)
];
for ((set, reset), want) in cases {
inputs[0].push(Scalar::bool(set)).unwrap();
inputs[1].push(Scalar::bool(reset)).unwrap();
let got = node.eval(Ctx::new(&inputs, Timestamp(0)));
assert_eq!(got, Some([Cell::from_f64(want)].as_slice()));
}
}
#[test]
fn latch_is_none_while_both_legs_cold() {
// Before any input on either leg: nothing latched yet -> None (downstream
// reads SimBroker's cold-leg-as-0.0, i.e. flat).
let mut node = Latch::new();
let inputs = two_bool_inputs();
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);
}
#[test]
fn input_slots_are_named_set_reset() {
let l = Latch::builder();
let names: Vec<&str> = l.schema().inputs.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["set", "reset"]);
}
}