//! `Div` — two-input f64 quotient (input 0 divided by input 1), the ratio //! combinator RSI-class signals need (`avg_gain / avg_loss`). IEEE-754 //! division, no error channel: `x / 0.0 -> inf` (signed by `x`'s sign), //! `0.0 / 0.0 -> NaN` — Rust's native `f64` division already follows this, //! so no special-casing is needed. use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind}; /// Two-input f64 quotient: input 0 divided by input 1 (IEEE-754). Emits `None` /// until both inputs have a value. pub struct Div { out: [Cell; 1], } impl Div { /// Build a `Div` node. pub fn new() -> Self { Self { out: [Cell::from_f64(0.0)] } } /// The param-generic recipe for a blueprint primitive: paramless, builds /// through `Div::new`. pub fn builder() -> PrimitiveBuilder { PrimitiveBuilder::new( "Div", NodeSchema { inputs: vec![ PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() }, PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() }, ], output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], }, |_| Box::new(Div::new()), ) } } impl Default for Div { fn default() -> Self { Self::new() } } impl Node for Div { fn lookbacks(&self) -> Vec { 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; } self.out[0] = Cell::from_f64(a[0] / b[0]); Some(&self.out) } fn label(&self) -> String { "Div".to_string() } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Scalar, Timestamp}; #[test] fn div_is_quotient_once_both_inputs_present() { let mut div = Div::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!(div.eval(Ctx::new(&inputs, Timestamp(0))), None); // both present -> a / b inputs[1].push(Scalar::f64(4.0)).unwrap(); assert_eq!(div.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(2.5)].as_slice())); } #[test] fn div_by_zero_is_signed_infinity_zero_over_zero_is_nan() { // The IEEE-754 property that makes Div safe with no error channel: a // nonzero numerator over zero yields a signed infinity, and 0/0 yields // NaN — both representable as plain f64, never a panic or an Err. let mut div = Div::new(); let mut inputs = vec![ AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), ]; inputs[0].push(Scalar::f64(5.0)).unwrap(); inputs[1].push(Scalar::f64(0.0)).unwrap(); let got = div.eval(Ctx::new(&inputs, Timestamp(0))).unwrap(); assert_eq!(got[0].f64(), f64::INFINITY); inputs[0].push(Scalar::f64(0.0)).unwrap(); inputs[1].push(Scalar::f64(0.0)).unwrap(); let got = div.eval(Ctx::new(&inputs, Timestamp(0))).unwrap(); assert!(got[0].f64().is_nan()); } #[test] fn input_slots_are_named_lhs_rhs() { let names: Vec = Div::builder().schema().inputs.iter().map(|p| p.name.clone()).collect(); assert_eq!(names, ["lhs", "rhs"]); } }