Skip to main content

dace_rs/
elementary.rs

1//! Elementary functions of DA values.
2//!
3//! Ports the intrinsic-function layer of `core/dacemath.c` (lines 656-1504):
4//! each function computes the divided derivatives of the scalar function at
5//! the constant part (`xf[k]`, the Taylor coefficients in the non-constant
6//! part) and composes them via the Horner engine `evaluate_series`.
7//! Domain violations panic with the numeric code and message of the C error
8//! table (e.g. 647 "Negative constant part in logarithm").
9
10use crate::context::truncation_order;
11use crate::da::Da;
12use crate::error::{codes, dace_panic};
13use crate::kernels::{multiply, weighted_sum};
14
15/// Evaluate the polynomial with coefficients `xf` (length `nocut + 1`) on the
16/// non-constant part of `a`, by Horner's rule over orders
17/// (`daceEvaluateSeries`).
18///
19/// Exactly as in C, the truncation order is temporarily lowered per Horner
20/// step (`nocut = 1` first, then `nocut - i`) and restored at the end.
21pub(crate) fn evaluate_series(a: &Da, xf: &[f64]) -> Da {
22    let nocut = truncation_order();
23    assert!(
24        xf.len() > nocut as usize,
25        "series coefficient table too short"
26    );
27
28    // Non-constant part of a.
29    let mut inon = a.clone();
30    inon.set_coefficient0(0, 0.0);
31
32    crate::context::set_truncation_order(1);
33    let mut inc = weighted_sum(&inon, xf[nocut as usize], &inon, 0.0);
34    inc = add_double(&inc, xf[(nocut - 1) as usize]);
35
36    let mut i = nocut as i64 - 2;
37    while i >= 0 {
38        crate::context::set_truncation_order(nocut - i as u32);
39        inc = multiply(&inon, &inc);
40        inc = add_double(&inc, xf[i as usize]);
41        i -= 1;
42    }
43
44    crate::context::set_truncation_order(nocut);
45    inc
46}
47
48/// Add a constant to a DA (`daceAddDouble`): copy, then set the constant
49/// coefficient to `cons + c` (flushing per epsilon as usual).
50pub(crate) fn add_double(a: &Da, c: f64) -> Da {
51    let mut r = a.clone();
52    let cons = r.cons();
53    r.set_coefficient0(0, cons + c);
54    r
55}
56
57/// `c - a` (`daceDoubleSubtract`): negate, then add the constant `c`.
58pub(crate) fn double_subtract(a: &Da, c: f64) -> Da {
59    let mut r = weighted_sum(a, -1.0, a, 0.0);
60    let cons = r.cons();
61    r.set_coefficient0(0, cons + c);
62    r
63}
64
65/// Subtract a constant from a DA (`daceSubtractDouble`).
66pub(crate) fn subtract_double(a: &Da, c: f64) -> Da {
67    add_double(a, -c)
68}
69
70/// Divide a DA by a scalar (`daceDivideDouble`).
71///
72/// # Panics
73///
74/// Panics with [`crate::DaceError`] code 641 when `c == 0.0`.
75pub(crate) fn divide_double(a: &Da, c: f64) -> Da {
76    if c == 0.0 {
77        dace_panic(codes::DIVIDING_BY_ZERO, "Dividing by zero");
78    }
79    weighted_sum(a, 1.0 / c, a, 0.0)
80}
81
82/// Multiplicative inverse `1/a` (`daceMultiplicativeInverse`).
83///
84/// # Panics
85///
86/// Panics with [`crate::DaceError`] code 641 when the constant part of `a` is zero.
87pub(crate) fn minv(a: &Da) -> Da {
88    let a0 = a.cons();
89    if a0 == 0.0 {
90        dace_panic(codes::DIVIDING_BY_ZERO, "Dividing by zero");
91    }
92
93    let nocut = truncation_order();
94    if nocut < 5 {
95        // Lower orders: compute the series directly.
96        minv0(a, a0)
97    } else {
98        // Higher orders: Newton iteration.
99        crate::context::set_truncation_order(2);
100        let mut inc = minv0(a, a0);
101        let mut ord: u32 = 3;
102        while ord <= nocut {
103            crate::context::set_truncation_order(nocut.min(2 * ord - 1));
104            let temp = multiply(a, &inc);
105            let temp = double_subtract(&temp, 2.0);
106            inc = multiply(&inc, &temp);
107            ord *= 2;
108        }
109        crate::context::set_truncation_order(nocut);
110        inc
111    }
112}
113
114/// Series-expansion multiplicative inverse (`daceMultiplicativeInverse0`),
115/// for orders below 5 (or as the Newton seed).
116fn minv0(a: &Da, a0: f64) -> Da {
117    let scaled = weighted_sum(a, 1.0 / a0, a, 0.0);
118    let nocut = truncation_order();
119    let mut xf = vec![0.0; nocut as usize + 1];
120    xf[0] = 1.0 / a0;
121    for i in 1..xf.len() {
122        xf[i] = -xf[i - 1];
123    }
124    evaluate_series(&scaled, &xf)
125}
126
127// ---------------------------------------------------------------------------
128// Constant-term-only operations (dacemath.c:661-688)
129// ---------------------------------------------------------------------------
130
131/// Truncate the constant part to an integer (round half to even, as C
132/// `rint`), keeping higher-order terms (`daceTruncate`).
133pub fn trunc(a: &Da) -> Da {
134    let mut r = a.clone();
135    let c = r.cons().round_ties_even();
136    r.set_coefficient0(0, c);
137    r
138}
139
140/// Round the constant part to an integer (half away from zero, as C `round`),
141/// keeping higher-order terms (`daceRound`).
142pub fn round(a: &Da) -> Da {
143    let mut r = a.clone();
144    let c = r.cons().round();
145    r.set_coefficient0(0, c);
146    r
147}
148
149/// Modulo of the constant part by `p`, keeping higher-order terms
150/// (`daceModulo`).
151pub fn modulo(a: &Da, p: f64) -> Da {
152    let mut r = a.clone();
153    let c = r.cons() % p;
154    r.set_coefficient0(0, c);
155    r
156}
157
158// ---------------------------------------------------------------------------
159// Powers and roots (dacemath.c:690-918)
160// ---------------------------------------------------------------------------
161/// Raise `a` to the real power `p` (`dacePowerDouble`): integer powers go
162/// through [`powi`], otherwise a series in `(a - a0)/a0`.
163///
164/// # Panics
165///
166/// Panics with [`crate::DaceError`] code 643 for a non-integer power of a DA with
167/// non-positive constant part.
168pub fn powf(a: &Da, p: f64) -> Da {
169    if p == 0.0 {
170        return Da::constant(1.0);
171    }
172    if p.fract() == 0.0 && p.abs() <= i32::MAX as f64 {
173        return powi(a, p as i32);
174    }
175
176    let a0 = a.cons();
177    if a0 <= 0.0 {
178        dace_panic(
179            codes::NON_INTEGER_POWER_NON_POSITIVE,
180            "Non-integer power of non-positive DA",
181        );
182    }
183
184    let nocut = truncation_order();
185    let mut xf = vec![0.0; nocut as usize + 1];
186    xf[0] = a0.powf(p);
187    for i in 1..xf.len() {
188        xf[i] = xf[i - 1] / i as f64 * (p - (i - 1) as f64);
189    }
190
191    let scaled = divide_double(a, a0);
192    evaluate_series(&scaled, &xf)
193}
194
195/// Raise `a` to the integer power `np` (`dacePower`), by squaring for
196/// `|np| > 4` and hard-coded small cases, with the inverse for negative
197/// powers.
198///
199/// Divergence from C: the C implementation computes negative powers by
200/// calling `daceMultiplicativeInverse(inc, inc)` on the aliased result, and
201/// that Newton iteration is not aliasing safe despite its documentation
202/// note - C returns wrong coefficients for negative powers of non-constant
203/// DAs (verified: C's `pow(A, -2)` disagrees with C's own `minv(sqr(A))`).
204/// This implementation returns the correct value.
205pub fn powi(a: &Da, np: i32) -> Da {
206    match np {
207        0 => Da::constant(1.0),
208        1 => a.clone(),
209        -1 => minv(a),
210        _ => {
211            let abs_np = np.unsigned_abs();
212            let mut result = match abs_np {
213                2 => a.sqr(),
214                3 => multiply(a, &a.sqr()),
215                4 => a.sqr().sqr(),
216                _ => {
217                    // Binary exponentiation (dacePower default branch).
218                    let mut itemp = a.clone();
219                    let mut inc = Da::constant(1.0);
220                    let mut inp = abs_np;
221                    while inp > 0 {
222                        if inp & 1 != 0 {
223                            inc = multiply(&inc, &itemp);
224                        }
225                        inp >>= 1;
226                        if inp > 0 {
227                            itemp = itemp.sqr();
228                        }
229                    }
230                    inc
231                }
232            };
233            if np < 0 {
234                result = minv(&result);
235            }
236            result
237        }
238    }
239}
240
241/// Take the `np`-th root of `a` (`daceRoot`).
242///
243/// # Panics
244///
245/// Panics with [`crate::DaceError`] code 644 for `np == 0`, 645 for an even root of
246/// a DA with non-positive constant part, 646 for an odd root of a zero DA.
247pub fn root(a: &Da, np: i32) -> Da {
248    if np == 0 {
249        dace_panic(codes::ZERO_TH_ROOT, "Zero-th root does not exist");
250    }
251
252    let a0 = a.cons();
253    let iodd = np.unsigned_abs() & 1;
254    if iodd == 0 && a0 <= 0.0 {
255        dace_panic(codes::EVEN_ROOT_NEGATIVE, "Even root of negative DA");
256    } else if iodd == 1 && a0 == 0.0 {
257        dace_panic(codes::ODD_ROOT_ZERO, "Odd root of zero DA");
258    }
259
260    let nocut = truncation_order();
261    let mut xf = vec![0.0; nocut as usize + 1];
262    let mut cr = 1.0 / f64::from(np);
263    xf[0] = a0.abs().powf(cr).copysign(a0);
264    for i in 1..xf.len() {
265        xf[i] = xf[i - 1] / i as f64 * cr;
266        cr -= 1.0;
267    }
268
269    let scaled = divide_double(a, a0);
270    evaluate_series(&scaled, &xf)
271}
272
273/// The square root (`daceSquareRoot`, i.e. `root(a, 2)`).
274///
275/// # Panics
276///
277/// Panics with [`crate::DaceError`] code 645 when the constant part is negative.
278pub fn sqrt(a: &Da) -> Da {
279    root(a, 2)
280}
281
282/// The inverse square root `1/sqrt(a)` (`daceInverseSquareRoot`).
283pub fn isrt(a: &Da) -> Da {
284    root(a, -2)
285}
286
287/// The cubic root (`daceCubicRoot`).
288pub fn cbrt(a: &Da) -> Da {
289    root(a, 3)
290}
291
292/// The inverse cubic root (`daceInverseCubicRoot`).
293pub fn icrt(a: &Da) -> Da {
294    root(a, -3)
295}
296
297/// The hypotenuse `sqrt(a² + b²)` (`daceHypotenuse`).
298pub fn hypot(a: &Da, b: &Da) -> Da {
299    Da::assert_same_context(a, b);
300    root(&(a.sqr() + b.sqr()), 2)
301}
302
303// ---------------------------------------------------------------------------
304// Exponentials and logarithms (dacemath.c:985-1075)
305// ---------------------------------------------------------------------------
306
307/// The exponential (`daceExponential`).
308pub fn exp(a: &Da) -> Da {
309    let nocut = truncation_order();
310    let mut xf = vec![0.0; nocut as usize + 1];
311    xf[0] = a.cons().exp();
312    for i in 1..xf.len() {
313        xf[i] = xf[i - 1] / i as f64;
314    }
315    evaluate_series(a, &xf)
316}
317
318/// The natural logarithm (`daceLogarithm`), as a series in `(a - a0)/a0`.
319///
320/// # Panics
321///
322/// Panics with [`crate::DaceError`] code 647 when the constant part is not positive.
323pub fn log(a: &Da) -> Da {
324    let a0 = a.cons();
325    if a0 <= 0.0 {
326        dace_panic(
327            codes::LOG_NON_POSITIVE,
328            "Negative constant part in logarithm",
329        );
330    }
331
332    let nocut = truncation_order();
333    let mut xf = vec![0.0; nocut as usize + 1];
334    let scaled = divide_double(a, a0);
335    xf[0] = a0.ln();
336    xf[1] = 1.0;
337    for i in 2..xf.len() {
338        xf[i] = -xf[i - 1] / i as f64 * (i - 1) as f64;
339    }
340    evaluate_series(&scaled, &xf)
341}
342
343/// The logarithm in base `b` (`daceLogarithmBase`).
344///
345/// # Panics
346///
347/// Panics with [`crate::DaceError`] code 648 when `b` is not positive.
348pub fn log_base(a: &Da, b: f64) -> Da {
349    if b <= 0.0 {
350        dace_panic(
351            codes::LOG_BASE_POSITIVE,
352            "Base of logarithm must be positive",
353        );
354    }
355    let l = log(a);
356    weighted_sum(&l, 1.0 / b.ln(), &l, 0.0)
357}
358
359/// The decadic logarithm (`daceLogarithm10`).
360pub fn log10(a: &Da) -> Da {
361    log_base(a, 10.0)
362}
363
364/// The binary logarithm (`daceLogarithm2`).
365pub fn log2(a: &Da) -> Da {
366    log_base(a, 2.0)
367}
368
369// ---------------------------------------------------------------------------
370// Trigonometry (dacemath.c:1082-1280)
371// ---------------------------------------------------------------------------
372
373/// The sine (`daceSine`).
374pub fn sin(a: &Da) -> Da {
375    let nocut = truncation_order();
376    let a0 = a.cons();
377    let mut xf = vec![0.0; nocut as usize + 1];
378    xf[0] = a0.sin();
379    xf[1] = a0.cos();
380    for i in 2..xf.len() {
381        xf[i] = -xf[i - 2] / (i * (i - 1)) as f64;
382    }
383    evaluate_series(a, &xf)
384}
385
386/// The cosine (`daceCosine`).
387pub fn cos(a: &Da) -> Da {
388    let nocut = truncation_order();
389    let a0 = a.cons();
390    let mut xf = vec![0.0; nocut as usize + 1];
391    xf[0] = a0.cos();
392    xf[1] = -a0.sin();
393    for i in 2..xf.len() {
394        xf[i] = -xf[i - 2] / (i * (i - 1)) as f64;
395    }
396    evaluate_series(a, &xf)
397}
398
399/// The tangent (`daceTangent`): `sin(a)/cos(a)`.
400///
401/// # Panics
402///
403/// Panics with [`crate::DaceError`] code 649 when the cosine of the constant part
404/// is zero.
405pub fn tan(a: &Da) -> Da {
406    if a.cons().cos() == 0.0 {
407        dace_panic(codes::COS_ZERO_IN_TANGENT, "Cosine is zero in tangent");
408    }
409    let s = sin(a);
410    let c = cos(a);
411    divide_da(&s, &c)
412}
413
414/// The arcsine (`daceArcSine`): `atan(a / sqrt(1 - a²))`.
415///
416/// # Panics
417///
418/// Panics with [`crate::DaceError`] code 650 when `|constant part| >= 1`.
419pub fn asin(a: &Da) -> Da {
420    if a.cons().abs() >= 1.0 {
421        dace_panic(codes::OUT_OF_DOMAIN, "Out of domain");
422    }
423    let d = double_subtract(&a.sqr(), 1.0);
424    let d = sqrt(&d);
425    let q = divide_da(a, &d);
426    atan(&q)
427}
428
429/// The arccosine (`daceArcCosine`): `π/2 - asin(a)`.
430///
431/// # Panics
432///
433/// Panics with [`crate::DaceError`] code 650 when `|constant part| >= 1`.
434pub fn acos(a: &Da) -> Da {
435    if a.cons().abs() >= 1.0 {
436        dace_panic(codes::OUT_OF_DOMAIN, "Out of domain");
437    }
438    let s = asin(a);
439    double_subtract(&s, std::f64::consts::FRAC_PI_2)
440}
441
442/// The arctangent (`daceArcTangent`): integrates `1/(1+x²)` at the constant
443/// part, i.e. a series in `(a - a0)/(1 + a0·a)`.
444pub fn atan(a: &Da) -> Da {
445    let nocut = truncation_order();
446    let a0 = a.cons();
447    let mut xf = vec![0.0; nocut as usize + 1];
448
449    let iarg = {
450        let denom = add_double(&weighted_sum(a, a0, a, 0.0), 1.0);
451        let num = subtract_double(a, a0);
452        divide_da(&num, &denom)
453    };
454
455    let mut s = 1.0;
456    xf[0] = a0.atan();
457    let mut i = 1;
458    while i < xf.len() {
459        xf[i] = s / i as f64;
460        s = -s;
461        i += 2;
462    }
463    evaluate_series(&iarg, &xf)
464}
465
466/// The four-quadrant arctangent `atan2(y, x)` of two DAs, with the sign
467/// correction placing the result in `(-π, π]` (`daceArcTangent2`).
468pub fn atan2(y: &Da, x: &Da) -> Da {
469    Da::assert_same_context(y, x);
470    let cx = x.cons();
471    let cy = y.cons();
472
473    if cx == 0.0 && cy == 0.0 {
474        return Da::constant(0.0);
475    }
476    if cy.abs() > cx.abs() {
477        let t = atan(&divide_da(x, y));
478        if cy < 0.0 {
479            double_subtract(&t, -std::f64::consts::FRAC_PI_2)
480        } else {
481            double_subtract(&t, std::f64::consts::FRAC_PI_2)
482        }
483    } else {
484        let t = atan(&divide_da(y, x));
485        if cx < 0.0 {
486            if cy > 0.0 {
487                add_double(&t, std::f64::consts::PI)
488            } else {
489                add_double(&t, -std::f64::consts::PI)
490            }
491        } else {
492            t
493        }
494    }
495}
496
497/// Division of two DAs: `a * minv(b)` (`daceDivide`).
498///
499/// # Panics
500///
501/// Panics with [`crate::DaceError`] code 641 when `b` has a zero constant part.
502pub(crate) fn divide_da(a: &Da, b: &Da) -> Da {
503    multiply(a, &minv(b))
504}
505
506// ---------------------------------------------------------------------------
507// Hyperbolic functions (dacemath.c:1287-1434)
508// ---------------------------------------------------------------------------
509
510/// The hyperbolic sine (`daceHyperbolicSine`).
511pub fn sinh(a: &Da) -> Da {
512    let nocut = truncation_order();
513    let a0 = a.cons();
514    let mut xf = vec![0.0; nocut as usize + 1];
515    xf[0] = a0.sinh();
516    xf[1] = a0.cosh();
517    for i in 2..xf.len() {
518        xf[i] = xf[i - 2] / (i * (i - 1)) as f64;
519    }
520    evaluate_series(a, &xf)
521}
522
523/// The hyperbolic cosine (`daceHyperbolicCosine`).
524pub fn cosh(a: &Da) -> Da {
525    let nocut = truncation_order();
526    let a0 = a.cons();
527    let mut xf = vec![0.0; nocut as usize + 1];
528    xf[0] = a0.cosh();
529    xf[1] = a0.sinh();
530    for i in 2..xf.len() {
531        xf[i] = xf[i - 2] / (i * (i - 1)) as f64;
532    }
533    evaluate_series(a, &xf)
534}
535
536/// The hyperbolic tangent (`daceHyperbolicTangent`), via the stable
537/// exponential form depending on the sign of the constant part.
538pub fn tanh(a: &Da) -> Da {
539    let a0 = a.cons();
540    if a0 > 0.0 {
541        let t = exp(&weighted_sum(a, -2.0, a, 0.0));
542        let denom = add_double(&t, 1.0);
543        let num = double_subtract(&t, 1.0);
544        divide_da(&num, &denom)
545    } else {
546        let t = exp(&weighted_sum(a, 2.0, a, 0.0));
547        let denom = add_double(&t, 1.0);
548        let num = add_double(&t, -1.0);
549        divide_da(&num, &denom)
550    }
551}
552
553/// The hyperbolic arcsine (`daceHyperbolicArcSine`): `log(a + sqrt(a²+1))`.
554pub fn asinh(a: &Da) -> Da {
555    let s = sqrt(&add_double(&a.sqr(), 1.0));
556    log(&(a.clone() + s))
557}
558
559/// The hyperbolic arccosine (`daceHyperbolicArcCosine`): `log(a + sqrt(a²-1))`.
560///
561/// # Panics
562///
563/// Panics with [`crate::DaceError`] code 650 when the constant part is `<= 1`.
564pub fn acosh(a: &Da) -> Da {
565    if a.cons() <= 1.0 {
566        dace_panic(codes::OUT_OF_DOMAIN, "Out of domain");
567    }
568    let s = sqrt(&subtract_double(&a.sqr(), 1.0));
569    log(&(a.clone() + s))
570}
571
572/// The hyperbolic arctangent (`daceHyperbolicArcTangent`):
573/// `log((1+a)/(1-a))/2`.
574///
575/// # Panics
576///
577/// Panics with [`crate::DaceError`] code 650 when `|constant part| >= 1`.
578pub fn atanh(a: &Da) -> Da {
579    if a.cons().abs() >= 1.0 {
580        dace_panic(codes::OUT_OF_DOMAIN, "Out of domain");
581    }
582    let num = add_double(a, 1.0);
583    let den = double_subtract(a, 1.0);
584    let q = log(&divide_da(&num, &den));
585    weighted_sum(&q, 0.5, &q, 0.0)
586}
587
588// ---------------------------------------------------------------------------
589// Error functions (dacemath.c:1441-1504); scalar values from puruspe
590// ---------------------------------------------------------------------------
591
592/// The error function (`daceErrorFunction`), via the Hermite-polynomial
593/// derivative recursion.
594pub fn erf(a: &Da) -> Da {
595    let nocut = truncation_order();
596    let a0 = a.cons();
597    let mut xf = vec![0.0; nocut as usize + 1];
598    let mut factor = 2.0 * (-a0 * a0).exp() / std::f64::consts::PI.sqrt();
599    xf[0] = puruspe::erf(a0);
600    xf[1] = factor;
601    let mut hi2 = 1.0; // Hermite H_0
602    let mut hi1 = 2.0 * a0; // Hermite H_1
603    for (i, item) in xf.iter_mut().enumerate().skip(2) {
604        factor /= -(i as f64);
605        *item = factor * hi1;
606        let temp = 2.0 * a0 * hi1 - 2.0 * (i - 1) as f64 * hi2;
607        hi2 = hi1;
608        hi1 = temp;
609    }
610    evaluate_series(a, &xf)
611}
612
613/// The complementary error function (`daceComplementaryErrorFunction`).
614pub fn erfc(a: &Da) -> Da {
615    let nocut = truncation_order();
616    let a0 = a.cons();
617    let mut xf = vec![0.0; nocut as usize + 1];
618    let mut factor = -2.0 * (-a0 * a0).exp() / std::f64::consts::PI.sqrt();
619    xf[0] = puruspe::erfc(a0);
620    xf[1] = factor;
621    let mut hi2 = 1.0;
622    let mut hi1 = 2.0 * a0;
623    for (i, item) in xf.iter_mut().enumerate().skip(2) {
624        factor /= -(i as f64);
625        *item = factor * hi1;
626        let temp = 2.0 * a0 * hi1 - 2.0 * (i - 1) as f64 * hi2;
627        hi2 = hi1;
628        hi1 = temp;
629    }
630    evaluate_series(a, &xf)
631}
632
633// ---------------------------------------------------------------------------
634// Method forms on Da
635// ---------------------------------------------------------------------------
636
637macro_rules! method_form {
638    ($($name:ident),* $(,)?) => {
639        $(
640            #[doc = concat!("Method form of [`", stringify!($name), "`](crate::elementary::", stringify!($name), ").")]
641            pub fn $name(&self) -> Da {
642                $name(self)
643            }
644        )*
645    };
646}
647
648impl Da {
649    method_form!(
650        exp, log, log10, log2, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh,
651        atanh, erf, erfc, sqrt, isrt, cbrt, icrt,
652    );
653
654    /// Method form of [`powf`](crate::elementary::powf).
655    pub fn powf(&self, p: f64) -> Da {
656        powf(self, p)
657    }
658
659    /// Method form of [`powi`](crate::elementary::powi).
660    pub fn powi(&self, np: i32) -> Da {
661        powi(self, np)
662    }
663
664    /// Method form of [`root`](crate::elementary::root).
665    pub fn root(&self, np: i32) -> Da {
666        root(self, np)
667    }
668
669    /// Method form of [`log_base`](crate::elementary::log_base).
670    pub fn log_base(&self, b: f64) -> Da {
671        log_base(self, b)
672    }
673
674    /// Method form of [`hypot`](crate::elementary::hypot).
675    pub fn hypot(&self, b: &Da) -> Da {
676        hypot(self, b)
677    }
678
679    /// Method form of [`atan2`](crate::elementary::atan2).
680    pub fn atan2(&self, x: &Da) -> Da {
681        atan2(self, x)
682    }
683
684    /// Method form of [`modulo`](crate::elementary::modulo).
685    pub fn modulo(&self, p: f64) -> Da {
686        modulo(self, p)
687    }
688
689    /// Method form of [`trunc`](crate::elementary::trunc).
690    pub fn trunc(&self) -> Da {
691        trunc(self)
692    }
693
694    /// Method form of [`round`](crate::elementary::round).
695    pub fn round(&self) -> Da {
696        round(self)
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703    use crate::test_support::CONTEXT_LOCK;
704
705    #[test]
706    fn taylor_coefficients() {
707        let _g = CONTEXT_LOCK.lock();
708        crate::context::init(6, 1).unwrap();
709        let x = Da::variable(1);
710
711        // sin(x) = x - x^3/6 + x^5/120
712        let s = sin(&x);
713        assert!((s.get_coefficient(&[1]) - 1.0).abs() < 1e-15);
714        assert!((s.get_coefficient(&[3]) + 1.0 / 6.0).abs() < 1e-15);
715        assert!((s.get_coefficient(&[5]) - 1.0 / 120.0).abs() < 1e-15);
716        assert_eq!(s.size(), 3);
717
718        // exp(x) = 1 + x + x^2/2 + x^3/6 + x^4/24 + x^5/120 + x^6/720
719        let e = exp(&x);
720        let mut fact = 1.0;
721        for k in 0..=6u32 {
722            if k > 0 {
723                fact *= f64::from(k);
724            }
725            assert!(
726                (e.get_coefficient(&[k]) - 1.0 / fact).abs() < 1e-15,
727                "exp coeff {k}"
728            );
729        }
730
731        // atan(x) matches its integrated form 1/(1+x^2)
732        let a = atan(&x);
733        assert!((a.get_coefficient(&[1]) - 1.0).abs() < 1e-15);
734        assert!((a.get_coefficient(&[3]) + 1.0 / 3.0).abs() < 1e-15);
735        assert!((a.get_coefficient(&[5]) - 1.0 / 5.0).abs() < 1e-15);
736
737        // sqrt(1+u) and powi
738        let u = 1.0 + x.clone();
739        let r = sqrt(&(u.clone() * u.clone()));
740        assert!((r.cons() - 1.0).abs() < 1e-12);
741        assert!((r.get_coefficient(&[1]) - 1.0).abs() < 1e-12);
742        let p3 = x.clone().powi(3);
743        assert!((p3.get_coefficient(&[3]) - 1.0).abs() < 1e-15);
744        assert_eq!(p3.size(), 1);
745    }
746
747    #[test]
748    fn identities_two_vars() {
749        let _g = CONTEXT_LOCK.lock();
750        crate::context::init(8, 2).unwrap();
751        let x = Da::variable(1);
752        let y = Da::variable(2);
753        let f = 0.7 + 1.3 * x.clone() - 0.4 * y.clone();
754
755        // sin^2 + cos^2 == 1 (rtol 1e-12)
756        let s2 = sin(&f).sqr() + cos(&f).sqr();
757        assert!((s2.cons() - 1.0).abs() < 1e-12);
758        for m in s2.iter_monomials() {
759            if m.order() == 0 {
760                continue;
761            }
762            assert!(m.c.abs() < 1e-12, "sin^2+cos^2 residual at {:?}", m.jj);
763        }
764
765        // log(exp(f)) == f (rtol 1e-12)
766        let lf = log(&exp(&f));
767        for m in lf.iter_monomials() {
768            let expect = f.get_coefficient(&m.jj);
769            assert!(
770                (m.c - expect).abs() <= 1e-12 * expect.abs().max(1.0),
771                "log(exp) at {:?}: {} vs {}",
772                m.jj,
773                m.c,
774                expect
775            );
776        }
777
778        // tanh via exp, sinh/cosh
779        let th = tanh(&f);
780        let sh = sinh(&f);
781        let ch = cosh(&f);
782        let q = divide_da(&sh, &ch);
783        for (m1, m2) in th.iter_monomials().zip(q.iter_monomials()) {
784            assert_eq!(m1.jj, m2.jj);
785            assert!((m1.c - m2.c).abs() <= 1e-11 * m1.c.abs().max(1.0));
786        }
787
788        // asin/acos derivative check: d/dx asin(x) = 1/sqrt(1-x^2)
789        let g = 0.3 * x.clone();
790        let asg = asin(&g);
791        let d = asg.deriv(1);
792        let expect = weighted_sum(&isrt(&(1.0 - g.clone() * g.clone())), 0.3, &Da::new(), 0.0);
793        for (m1, m2) in d.iter_monomials().zip(expect.iter_monomials()) {
794            assert_eq!(m1.jj, m2.jj);
795            assert!((m1.c - m2.c).abs() <= 1e-11 * m1.c.abs().max(1.0));
796        }
797
798        // atan2 roundtrip
799        let ang = 0.5 + 0.2 * x.clone();
800        let r = 1.2 + 0.3 * y.clone();
801        let yv = r.clone() * sin(&ang.clone());
802        let xv = r.clone() * cos(&ang.clone());
803        let a2 = atan2(&yv, &xv);
804        for m in a2.iter_monomials() {
805            let expect = ang.get_coefficient(&m.jj);
806            assert!(
807                (m.c - expect).abs() <= 1e-11 * expect.abs().max(1.0),
808                "atan2 at {:?}: {} vs {}",
809                m.jj,
810                m.c,
811                expect
812            );
813        }
814
815        // erf: erf(0) = 0, derivative at 0 is 2/sqrt(pi)
816        let z = 0.0 + x.clone();
817        let ez = erf(&z);
818        assert_eq!(ez.cons(), 0.0);
819        assert!((ez.get_coefficient(&[1, 0]) - 2.0 / std::f64::consts::PI.sqrt()).abs() < 1e-14);
820        let ec = erfc(&z);
821        assert!((ec.cons() - 1.0).abs() < 1e-14);
822        assert!((ec.get_coefficient(&[1, 0]) + 2.0 / std::f64::consts::PI.sqrt()).abs() < 1e-14);
823
824        // constant-part-only ops
825        // C daceTruncate uses rint (round half to even): 2.7 -> 3, 2.5 -> 2;
826        // daceRound rounds half away from zero: 2.5 -> 3.
827        let w = 2.5 + x.clone();
828        assert!((w.trunc().cons() - 2.0).abs() < 1e-15);
829        assert!((w.round().cons() - 3.0).abs() < 1e-15);
830        assert!((w.modulo(2.0).cons() - 0.5).abs() < 1e-12);
831
832        // logs in bases
833        assert!((log10(&(Da::constant(100.0))).cons() - 2.0).abs() < 1e-12);
834        assert!((log2(&(Da::constant(8.0))).cons() - 3.0).abs() < 1e-12);
835        assert!((log_base(&(Da::constant(8.0)), 2.0).cons() - 3.0).abs() < 1e-12);
836
837        // roots
838        assert!((cbrt(&(Da::constant(27.0))).cons() - 3.0).abs() < 1e-12);
839        assert!((isrt(&(Da::constant(4.0))).cons() - 0.5).abs() < 1e-12);
840        assert!((icrt(&(Da::constant(8.0))).cons() - 0.5).abs() < 1e-12);
841        assert!((hypot(&(Da::constant(3.0)), &Da::constant(4.0)).cons() - 5.0).abs() < 1e-12);
842        assert!((powf(&(Da::constant(2.0)), 10.0).cons() - 1024.0).abs() < 1e-9);
843
844        // acosh / asinh / atanh sanity
845        assert!((acosh(&(Da::constant(2.0))).cons() - 2.0f64.acosh()).abs() < 1e-12);
846        assert!((asinh(&(Da::constant(1.5))).cons() - 1.5f64.asinh()).abs() < 1e-12);
847        assert!((atanh(&(Da::constant(0.5))).cons() - 0.5f64.atanh()).abs() < 1e-12);
848    }
849
850    #[test]
851    fn domain_panics() {
852        let _g = CONTEXT_LOCK.lock();
853        crate::context::init(6, 2).unwrap();
854
855        fn expect_code(f: impl FnOnce() -> Da + std::panic::UnwindSafe, code: u32) {
856            let err = std::panic::catch_unwind(f).expect_err("must panic");
857            let e = err
858                .downcast_ref::<crate::error::DaceError>()
859                .expect("DaceError payload");
860            assert_eq!(e.code, code, "{}", e);
861        }
862
863        expect_code(|| log(&Da::constant(-1.0)), codes::LOG_NON_POSITIVE);
864        expect_code(|| log(&Da::constant(0.0)), codes::LOG_NON_POSITIVE);
865        // Error 649 (cos(const) == 0 in tan) cannot be triggered by a double
866        // constant: libm cos never returns exact 0.0 (cos(π/2) ~ 6.1e-17),
867        // matching the C library's behavior on the same inputs.
868        expect_code(|| Da::constant(0.0).minv(), codes::DIVIDING_BY_ZERO);
869        expect_code(|| sqrt(&Da::constant(-4.0)), codes::EVEN_ROOT_NEGATIVE);
870        expect_code(|| root(&Da::constant(1.0), 0), codes::ZERO_TH_ROOT);
871        expect_code(|| root(&Da::constant(0.0), 3), codes::ODD_ROOT_ZERO);
872        expect_code(
873            || powf(&Da::constant(-2.0), 0.5),
874            codes::NON_INTEGER_POWER_NON_POSITIVE,
875        );
876        expect_code(|| asin(&Da::constant(1.0)), codes::OUT_OF_DOMAIN);
877        expect_code(|| acos(&Da::constant(-1.5)), codes::OUT_OF_DOMAIN);
878        expect_code(|| acosh(&Da::constant(1.0)), codes::OUT_OF_DOMAIN);
879        expect_code(|| atanh(&Da::constant(-1.0)), codes::OUT_OF_DOMAIN);
880        expect_code(
881            || log_base(&Da::constant(2.0), -1.0),
882            codes::LOG_BASE_POSITIVE,
883        );
884        expect_code(
885            || Da::constant(1.0) / Da::variable(1),
886            codes::DIVIDING_BY_ZERO,
887        );
888    }
889}