//! `Sqrt` — one-input f64 square root. Turns a variance estimate (price²) back //! into a standard deviation (price), e.g. the last stage of a rolling-stddev //! volatility. Negative inputs are clamped to `0.0` before the root (a variance //! is non-negative; floating rounding can produce a tiny negative). Emits `None` //! until its input has a value. use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind}; /// One-input f64 square root, `sqrt(max(input, 0.0))`. Emits `None` until its /// input has a value (warm-up filter, C8). pub struct Sqrt { out: [Cell; 1], } impl Sqrt { pub fn new() -> Self { Self { out: [Cell::from_f64(0.0)] } } pub fn builder() -> PrimitiveBuilder { PrimitiveBuilder::new( "Sqrt", NodeSchema { inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() }], output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], }, |_| Box::new(Sqrt::new()), ) } } impl Default for Sqrt { fn default() -> Self { Self::new() } } impl Node for Sqrt { fn lookbacks(&self) -> Vec { vec![1] } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { let w = ctx.f64_in(0); if w.is_empty() { return None; } self.out[0] = Cell::from_f64(w[0].max(0.0).sqrt()); Some(&self.out) } fn label(&self) -> String { "Sqrt".to_string() } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Scalar, Timestamp}; #[test] fn sqrt_of_nine_is_three_zero_is_zero() { let mut s = Sqrt::new(); let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; inputs[0].push(Scalar::f64(9.0)).unwrap(); assert_eq!(s.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(3.0)].as_slice())); inputs[0].push(Scalar::f64(0.0)).unwrap(); assert_eq!(s.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(0.0)].as_slice())); } #[test] fn sqrt_clamps_negative_to_zero() { let mut s = Sqrt::new(); let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; inputs[0].push(Scalar::f64(-1e-12)).unwrap(); assert_eq!(s.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(0.0)].as_slice())); } #[test] fn sqrt_is_none_until_input_present() { let mut s = Sqrt::new(); let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; assert_eq!(s.eval(Ctx::new(&inputs, Timestamp(0))), None); } }