Skip to main content

tang_expr/
scalar.rs

1//! `Scalar` trait implementation for `ExprId`.
2//!
3//! Every Scalar method decomposes into the 9 RISC primitives by inserting
4//! nodes into the thread-local expression graph.
5
6use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
7
8use tang::Scalar;
9
10use crate::node::ExprId;
11use crate::with_graph;
12
13// --- Operator impls (all delegate to graph ops) ---
14
15impl Add for ExprId {
16    type Output = Self;
17    #[inline]
18    fn add(self, rhs: Self) -> Self {
19        with_graph(|g| g.add(self, rhs))
20    }
21}
22
23impl Sub for ExprId {
24    type Output = Self;
25    #[inline]
26    fn sub(self, rhs: Self) -> Self {
27        // sub(a, b) = add(a, neg(b))
28        let nb = with_graph(|g| g.neg(rhs));
29        with_graph(|g| g.add(self, nb))
30    }
31}
32
33impl Mul for ExprId {
34    type Output = Self;
35    #[inline]
36    fn mul(self, rhs: Self) -> Self {
37        with_graph(|g| g.mul(self, rhs))
38    }
39}
40
41impl Div for ExprId {
42    type Output = Self;
43    #[inline]
44    fn div(self, rhs: Self) -> Self {
45        // div(a, b) = mul(a, recip(b))
46        let rb = with_graph(|g| g.recip(rhs));
47        with_graph(|g| g.mul(self, rb))
48    }
49}
50
51impl Neg for ExprId {
52    type Output = Self;
53    #[inline]
54    fn neg(self) -> Self {
55        with_graph(|g| g.neg(self))
56    }
57}
58
59impl AddAssign for ExprId {
60    #[inline]
61    fn add_assign(&mut self, rhs: Self) {
62        *self = *self + rhs;
63    }
64}
65
66impl SubAssign for ExprId {
67    #[inline]
68    fn sub_assign(&mut self, rhs: Self) {
69        *self = *self - rhs;
70    }
71}
72
73impl MulAssign for ExprId {
74    #[inline]
75    fn mul_assign(&mut self, rhs: Self) {
76        *self = *self * rhs;
77    }
78}
79
80impl DivAssign for ExprId {
81    #[inline]
82    fn div_assign(&mut self, rhs: Self) {
83        *self = *self / rhs;
84    }
85}
86
87// --- Scalar impl ---
88
89impl Scalar for ExprId {
90    const ZERO: Self = ExprId::ZERO;
91    const ONE: Self = ExprId::ONE;
92    const TWO: Self = ExprId::TWO;
93    // These can't be const because they need graph insertion.
94    // We use ZERO as placeholder — the actual values are injected lazily.
95    const HALF: Self = ExprId(u32::MAX - 1);
96    const PI: Self = ExprId(u32::MAX - 2);
97    const TAU: Self = ExprId(u32::MAX - 3);
98    const FRAC_PI_2: Self = ExprId(u32::MAX - 4);
99    const EPSILON: Self = ExprId(u32::MAX - 5);
100    const INFINITY: Self = ExprId(u32::MAX - 6);
101    const NEG_INFINITY: Self = ExprId(u32::MAX - 7);
102
103    #[inline]
104    fn sqrt(self) -> Self {
105        with_graph(|g| g.sqrt(self))
106    }
107
108    #[inline]
109    fn abs(self) -> Self {
110        // abs(x) = select(x, x, -x). Not sqrt(x * x): that overflows for
111        // |x| > ~1e154 (f64) or ~1e19 (f32) and underflows to 0 below their
112        // square roots of the smallest normal.
113        let neg = with_graph(|g| g.neg(self));
114        with_graph(|g| g.select(self, self, neg))
115    }
116
117    #[inline]
118    fn sin(self) -> Self {
119        with_graph(|g| g.sin(self))
120    }
121
122    #[inline]
123    fn cos(self) -> Self {
124        // cos(x) = sin(x + PI/2)
125        let half_pi = Self::from_f64(std::f64::consts::FRAC_PI_2);
126        let shifted = with_graph(|g| g.add(self, half_pi));
127        with_graph(|g| g.sin(shifted))
128    }
129
130    #[inline]
131    fn tan(self) -> Self {
132        // tan(x) = sin(x) * recip(cos(x))
133        let s = self.sin();
134        let c = self.cos();
135        let rc = with_graph(|g| g.recip(c));
136        with_graph(|g| g.mul(s, rc))
137    }
138
139    #[inline]
140    fn asin(self) -> Self {
141        // asin(x) = atan2(x, sqrt(1 - x*x))
142        let one = ExprId::ONE;
143        let xx = with_graph(|g| g.mul(self, self));
144        let diff = with_graph(|g| {
145            let neg_xx = g.neg(xx);
146            g.add(one, neg_xx)
147        });
148        let sq = with_graph(|g| g.sqrt(diff));
149        with_graph(|g| g.atan2(self, sq))
150    }
151
152    #[inline]
153    fn acos(self) -> Self {
154        // acos(x) = atan2(sqrt(1 - x*x), x)
155        let one = ExprId::ONE;
156        let xx = with_graph(|g| g.mul(self, self));
157        let diff = with_graph(|g| {
158            let neg_xx = g.neg(xx);
159            g.add(one, neg_xx)
160        });
161        let sq = with_graph(|g| g.sqrt(diff));
162        with_graph(|g| g.atan2(sq, self))
163    }
164
165    #[inline]
166    fn atan2(self, other: Self) -> Self {
167        with_graph(|g| g.atan2(self, other))
168    }
169
170    #[inline]
171    fn sin_cos(self) -> (Self, Self) {
172        (self.sin(), self.cos())
173    }
174
175    #[inline]
176    fn min(self, other: Self) -> Self {
177        // min(a, b) = select(a - b, b, a): exact. The old
178        // 0.5 * (a + b - sqrt((a-b)^2)) rounded (min(1, 1e-17) was 0) and
179        // squared the difference, which under/overflows in f32.
180        let diff = self - other;
181        with_graph(|g| g.select(diff, other, self))
182    }
183
184    #[inline]
185    fn max(self, other: Self) -> Self {
186        // max(a, b) = select(a - b, a, b): exact (see `min`).
187        let diff = self - other;
188        with_graph(|g| g.select(diff, self, other))
189    }
190
191    #[inline]
192    fn clamp(self, lo: Self, hi: Self) -> Self {
193        self.max(lo).min(hi)
194    }
195
196    #[inline]
197    fn recip(self) -> Self {
198        with_graph(|g| g.recip(self))
199    }
200
201    #[inline]
202    fn powi(self, n: i32) -> Self {
203        match n {
204            0 => ExprId::ONE,
205            1 => self,
206            2 => with_graph(|g| g.mul(self, self)),
207            3 => {
208                let sq = with_graph(|g| g.mul(self, self));
209                with_graph(|g| g.mul(sq, self))
210            }
211            4 => {
212                let sq = with_graph(|g| g.mul(self, self));
213                with_graph(|g| g.mul(sq, sq))
214            }
215            -1 => with_graph(|g| g.recip(self)),
216            -2 => {
217                let sq = with_graph(|g| g.mul(self, self));
218                with_graph(|g| g.recip(sq))
219            }
220            _ => self.powf(Self::from_f64(n as f64)),
221        }
222    }
223
224    #[inline]
225    fn copysign(self, sign: Self) -> Self {
226        // copysign(x, s) = abs(x) * signum(s)
227        let ax = self.abs();
228        let ss = sign.signum();
229        with_graph(|g| g.mul(ax, ss))
230    }
231
232    #[inline]
233    fn signum(self) -> Self {
234        // signum(x) = x * recip(sqrt(x * x))
235        let xx = with_graph(|g| g.mul(self, self));
236        let abs_x = with_graph(|g| g.sqrt(xx));
237        let r = with_graph(|g| g.recip(abs_x));
238        with_graph(|g| g.mul(self, r))
239    }
240
241    #[inline]
242    fn floor(self) -> Self {
243        // Only works for literals
244        with_graph(|g| {
245            if let Some(v) = g.node(self).as_f64() {
246                g.lit(v.floor())
247            } else {
248                panic!("floor() requires a literal expression")
249            }
250        })
251    }
252
253    #[inline]
254    fn ceil(self) -> Self {
255        with_graph(|g| {
256            if let Some(v) = g.node(self).as_f64() {
257                g.lit(v.ceil())
258            } else {
259                panic!("ceil() requires a literal expression")
260            }
261        })
262    }
263
264    #[inline]
265    fn round(self) -> Self {
266        with_graph(|g| {
267            if let Some(v) = g.node(self).as_f64() {
268                g.lit(v.round())
269            } else {
270                panic!("round() requires a literal expression")
271            }
272        })
273    }
274
275    #[inline]
276    fn exp(self) -> Self {
277        // exp(x) = exp2(x * log2(e))
278        let log2_e = Self::from_f64(std::f64::consts::LOG2_E);
279        let scaled = with_graph(|g| g.mul(self, log2_e));
280        with_graph(|g| g.exp2(scaled))
281    }
282
283    #[inline]
284    fn ln(self) -> Self {
285        // ln(x) = log2(x) * ln(2)
286        let ln_2 = Self::from_f64(std::f64::consts::LN_2);
287        let l = with_graph(|g| g.log2(self));
288        with_graph(|g| g.mul(l, ln_2))
289    }
290
291    #[inline]
292    fn powf(self, p: Self) -> Self {
293        // powf(x, p) = exp2(p * log2(x))
294        let l = with_graph(|g| g.log2(self));
295        let pl = with_graph(|g| g.mul(p, l));
296        with_graph(|g| g.exp2(pl))
297    }
298
299    #[inline]
300    fn sinh(self) -> Self {
301        // sinh(x) = 0.5 * (exp(x) - exp(-x))
302        let half = Self::from_f64(0.5);
303        let ex = self.exp();
304        let neg_x = with_graph(|g| g.neg(self));
305        let enx = Scalar::exp(neg_x);
306        let diff = ex - enx;
307        with_graph(|g| g.mul(half, diff))
308    }
309
310    #[inline]
311    fn cosh(self) -> Self {
312        // cosh(x) = 0.5 * (exp(x) + exp(-x))
313        let half = Self::from_f64(0.5);
314        let ex = self.exp();
315        let neg_x = with_graph(|g| g.neg(self));
316        let enx = Scalar::exp(neg_x);
317        let sum = ex + enx;
318        with_graph(|g| g.mul(half, sum))
319    }
320
321    #[inline]
322    fn tanh(self) -> Self {
323        // sinh(x) / cosh(x) is inf / inf = NaN once |x| > ~710 (f64; ~89 in
324        // f32). Instead, with a = min(|x|, 20) (tanh is 1 to f64 precision
325        // past 20, with slope 0):
326        // - a < 0.05: the odd series to a^9, truncation < 1e-16 relative;
327        // - else 1 - 2 / (e^(2a) + 1), cancellation <= ~2e-15 relative there.
328        let lit = Self::from_f64;
329        let a = self.abs().min(lit(20.0));
330        let e = (a * Self::TWO).exp();
331        let big = Self::ONE - Self::TWO / (e + Self::ONE);
332        let a2 = a * a;
333        let series = a
334            * (Self::ONE
335                + a2 * (lit(-1.0 / 3.0)
336                    + a2 * (lit(2.0 / 15.0)
337                        + a2 * (lit(-17.0 / 315.0) + a2 * lit(62.0 / 2835.0)))));
338        let r = Self::select(a - lit(0.05), big, series);
339        Self::select(self, r, -r)
340    }
341
342    #[inline]
343    fn acosh(self) -> Self {
344        // acosh(x) = ln(x + sqrt(x*x - 1))
345        let one = ExprId::ONE;
346        let xx = with_graph(|g| g.mul(self, self));
347        let diff = with_graph(|g| {
348            let neg_one = g.neg(one);
349            g.add(xx, neg_one)
350        });
351        let sq = with_graph(|g| g.sqrt(diff));
352        let sum = with_graph(|g| g.add(self, sq));
353        Scalar::ln(sum)
354    }
355
356    #[inline]
357    fn asinh(self) -> Self {
358        // asinh(x) = ln(x + sqrt(x*x + 1))
359        let one = ExprId::ONE;
360        let xx = with_graph(|g| g.mul(self, self));
361        let sum_inner = with_graph(|g| g.add(xx, one));
362        let sq = with_graph(|g| g.sqrt(sum_inner));
363        let sum = with_graph(|g| g.add(self, sq));
364        Scalar::ln(sum)
365    }
366
367    #[inline]
368    fn atanh(self) -> Self {
369        // atanh(x) = 0.5 * ln((1+x) / (1-x))
370        let half = Self::from_f64(0.5);
371        let one = ExprId::ONE;
372        let one_plus = with_graph(|g| g.add(one, self));
373        let neg_x = with_graph(|g| g.neg(self));
374        let one_minus = with_graph(|g| g.add(one, neg_x));
375        let ratio = one_plus / one_minus;
376        let l = Scalar::ln(ratio);
377        with_graph(|g| g.mul(half, l))
378    }
379
380    #[inline]
381    fn from_f64(v: f64) -> Self {
382        with_graph(|g| g.lit(v))
383    }
384
385    #[inline]
386    fn to_f64(self) -> f64 {
387        panic!("cannot evaluate symbolic ExprId to f64 — use ExprGraph::eval() instead")
388    }
389
390    #[inline]
391    fn from_i32(v: i32) -> Self {
392        with_graph(|g| g.lit(v as f64))
393    }
394
395    #[inline]
396    fn select(cond: Self, a: Self, b: Self) -> Self {
397        with_graph(|g| g.select(cond, a, b))
398    }
399}
400
401// --- Display for ExprId (needed by Scalar bound) ---
402// Note: ExprId already has Display in node.rs, just showing "eN".
403// The detailed expression display is in display.rs via ExprGraph::fmt_expr.
404
405#[cfg(test)]
406mod tests {
407    use tang::Scalar;
408
409    /// Evaluate `f` traced over one input.
410    fn eval1(f: impl Fn(crate::ExprId) -> crate::ExprId, x: f64) -> f64 {
411        let (g, out) = crate::trace(|| f(crate::ExprId::var(0)));
412        g.compile(out)(&[x])
413    }
414
415    #[test]
416    fn min_max_abs_are_exact() {
417        for (a, b) in [(1.0, 1e-17), (-3.0, 2.5), (1e200, -1e200), (1e-30, 2e-30)] {
418            let lo = eval1(|x| x.min(crate::ExprId::from_f64(b)), a);
419            let hi = eval1(|x| x.max(crate::ExprId::from_f64(b)), a);
420            assert_eq!(lo, a.min(b), "min({a}, {b})");
421            assert_eq!(hi, a.max(b), "max({a}, {b})");
422        }
423        for x in [-1e200, -2.5, 0.0, 1e-300, 3.0] {
424            assert_eq!(eval1(|v| v.abs(), x), x.abs(), "abs({x})");
425        }
426    }
427
428    #[test]
429    fn tanh_is_finite_and_accurate() {
430        for x in [
431            -2000.0, -30.0, -1.0, -0.04, -1e-8, 0.0, 1e-8, 0.03, 0.05, 0.5, 5.0, 25.0, 2000.0,
432        ] {
433            let got = eval1(|v| v.tanh(), x);
434            let want: f64 = x.tanh();
435            assert!(got.is_finite(), "tanh({x}) = {got}");
436            let err = (got - want).abs() / want.abs().max(1e-300);
437            assert!(
438                want == 0.0 && got == 0.0 || err < 4e-15,
439                "tanh({x}): {got} vs {want}, rel {err:e}"
440            );
441        }
442    }
443
444    use crate::{trace, ExprId};
445
446    #[test]
447    fn basic_arithmetic() {
448        let (g, result) = trace(|| {
449            let x = ExprId::from_f64(3.0);
450            let y = ExprId::from_f64(4.0);
451            x + y
452        });
453        // Should be a single Add node
454        let val = g.eval::<f64>(result, &[]);
455        assert!((val - 7.0).abs() < 1e-10);
456    }
457
458    #[test]
459    fn var_trace() {
460        let (g, result) = trace(|| {
461            let x: ExprId = Scalar::from_f64(0.0); // creates a Lit(0.0) = ZERO
462            x
463        });
464        assert_eq!(result, ExprId::ZERO);
465        assert_eq!(g.len(), 3); // just ZERO, ONE, TWO
466    }
467
468    #[test]
469    fn constants_are_lits() {
470        let (_g, (half, pi)) = trace(|| {
471            let h = ExprId::from_f64(0.5);
472            let p = ExprId::from_f64(std::f64::consts::PI);
473            (h, p)
474        });
475        assert_ne!(half, pi);
476    }
477}