Skip to main content

dashu_float/math/
hyper.rs

1//! Hyperbolic functions, built from the cancellation-free `exp_m1` / `ln_1p` primitives:
2//!
3//! - `sinh(x) = (exp_m1(x) - exp_m1(-x)) / 2`
4//! - `cosh(x) = (exp_m1(x) + exp_m1(-x)) / 2 + 1`
5//! - `tanh(x) = exp_m1(2x) / (exp_m1(2x) + 2)`
6//! - `asinh(x) = sign(x) · ln_1p(|x| + x²/(sqrt(x²+1)+1))`
7//! - `acosh(x) = ln_1p((x-1) + sqrt((x-1)(x+1)))`  (x ≥ 1)
8//! - `atanh(x) = ln_1p(2x/(1-x)) / 2`  (|x| < 1)
9//!
10//! The `exp_m1` / `ln_1p` forms avoid the catastrophic cancellation that the naive
11//! `(exp(x)-exp(-x))/2` and `ln(1+…)` formulas suffer for small arguments. Special
12//! values follow IEEE 754: infinities are values (not errors) for the forward functions
13//! and `asinh`; `acosh(x<1)` and `atanh(|x|>1)` are domain errors.
14
15use crate::{
16    error::{assert_limited_precision, FpError},
17    exp::exp_overflows,
18    fbig::FBig,
19    math::{
20        cache::{reborrow_cache, ConstCache},
21        FpResult,
22    },
23    repr::{Context, Repr, Word},
24    round::{ErrorBounds, Round},
25};
26use dashu_base::{Abs, AbsOrd, Approximation::Exact, Sign};
27
28impl<R: ErrorBounds> Context<R> {
29    /// Hyperbolic sine.
30    pub fn sinh<const B: Word>(
31        &self,
32        x: &Repr<B>,
33        mut cache: Option<&mut ConstCache>,
34    ) -> FpResult<FBig<R, B>> {
35        if x.is_infinite() {
36            return Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self)));
37        }
38        assert_limited_precision(self.precision);
39        if x.significand.is_zero() {
40            // sinh(±0) = ±0
41            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
42        }
43        // Hoist the exp overflow out of the Ziv closure (it can't return Err): sinh(±huge) = ±inf.
44        if exp_overflows::<R, B>(self, x, &mut cache) {
45            return Err(FpError::Overflow(x.sign()));
46        }
47
48        // sinh(x) = (exp_m1(x) - exp_m1(-x)) / 2  (cancellation-free). `exp_m1` is itself Ziv-correct
49        // at the working precision, so only the subtraction/divide rounding contributes to the
50        // radius (a few working-ULPs, scaled by the `exp_m1(x) ≈ 2·sinh(x)` magnitude ratio).
51        let initial_guard = self.base_guard_digits::<B>() + 10;
52        Ok(self.ziv(initial_guard, |guard| {
53            let work = Context::<R>::new(self.precision + guard);
54            let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
55            let ep = work
56                .exp_m1(&x_f.repr, reborrow_cache(&mut cache))
57                .unwrap()
58                .value();
59            let em = work
60                .exp_m1(&(-x_f.clone()).repr, reborrow_cache(&mut cache))
61                .unwrap()
62                .value();
63            let result = (ep - em) / 2i32;
64            let radius = result.ulp() * 12;
65            (result, radius)
66        }))
67    }
68
69    /// Hyperbolic cosine.
70    pub fn cosh<const B: Word>(
71        &self,
72        x: &Repr<B>,
73        mut cache: Option<&mut ConstCache>,
74    ) -> FpResult<FBig<R, B>> {
75        if x.is_infinite() {
76            // cosh(±inf) = +inf
77            return Ok(Exact(FBig::new(Repr::infinity(), *self)));
78        }
79        assert_limited_precision(self.precision);
80        if x.significand.is_zero() {
81            // cosh(±0) = 1
82            return Ok(Exact(FBig::new(Repr::one(), *self)));
83        }
84
85        // Hoist the exp overflow out of the Ziv closure: cosh(±huge) = +inf (always positive).
86        if exp_overflows::<R, B>(self, x, &mut cache) {
87            return Err(FpError::Overflow(Sign::Positive));
88        }
89        // cosh(x) = (exp_m1(x) + exp_m1(-x)) / 2 + 1 (no cancellation: same-sign sum). `exp_m1` is
90        // Ziv-correct at the working precision; the radius is a few working-ULPs (the `exp_m1(x) ≈
91        // 2·cosh(x)` magnitude ratio, plus the trailing +1).
92        let initial_guard = self.base_guard_digits::<B>() + 10;
93        Ok(self.ziv(initial_guard, |guard| {
94            let work = Context::<R>::new(self.precision + guard);
95            let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
96            let ep = work
97                .exp_m1(&x_f.repr, reborrow_cache(&mut cache))
98                .unwrap()
99                .value();
100            let em = work
101                .exp_m1(&(-x_f.clone()).repr, reborrow_cache(&mut cache))
102                .unwrap()
103                .value();
104            let result = (ep + em) / 2i32 + FBig::<R, B>::ONE;
105            let radius = result.ulp() * 14;
106            (result, radius)
107        }))
108    }
109
110    /// Simultaneously compute `sinh(x)` and `cosh(x)` (context layer). Returns
111    /// `(sinh_result, cosh_result)` where each is a [`FpResult`].
112    ///
113    /// This is more efficient than calling [`sinh`](Context::sinh) and [`cosh`](Context::cosh)
114    /// separately, since the two share the `exp_m1(±x)` sub-computations.
115    pub fn sinh_cosh<const B: Word>(
116        &self,
117        x: &Repr<B>,
118        mut cache: Option<&mut ConstCache>,
119    ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
120        if x.is_infinite() {
121            return (
122                Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self))),
123                Ok(Exact(FBig::new(Repr::infinity(), *self))),
124            );
125        }
126        assert_limited_precision(self.precision);
127        if x.significand.is_zero() {
128            return (
129                Ok(Exact(FBig::new(signed_zero_repr(x), *self))),
130                Ok(Exact(FBig::new(Repr::one(), *self))),
131            );
132        }
133
134        // Hoist the exp overflow out of the Ziv closure: sinh(±huge) = ±inf, cosh(±huge) = +inf.
135        if exp_overflows::<R, B>(self, x, &mut cache) {
136            return (Err(FpError::Overflow(x.sign())), Err(FpError::Overflow(Sign::Positive)));
137        }
138        // sinh = (ep - em)/2; cosh = (ep + em)/2 + 1, sharing the two `exp_m1` calls. Certified as a
139        // pair via `ziv_pair` (retry while either endpoint straddles a boundary).
140        let initial_guard = self.base_guard_digits::<B>() + 10;
141        let (sinh_r, cosh_r) = self.ziv_pair(initial_guard, |guard| {
142            let work = Context::<R>::new(self.precision + guard);
143            let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
144            let ep = work
145                .exp_m1(&x_f.repr, reborrow_cache(&mut cache))
146                .unwrap()
147                .value();
148            let em = work
149                .exp_m1(&(-x_f.clone()).repr, reborrow_cache(&mut cache))
150                .unwrap()
151                .value();
152            let sinh_val = (ep.clone() - em.clone()) / 2i32;
153            let cosh_val = (ep + em) / 2i32 + FBig::<R, B>::ONE;
154            let sinh_radius = sinh_val.ulp() * 12;
155            let cosh_radius = cosh_val.ulp() * 14;
156            ((sinh_val, sinh_radius), (cosh_val, cosh_radius))
157        });
158        (Ok(sinh_r), Ok(cosh_r))
159    }
160
161    /// Hyperbolic tangent.
162    pub fn tanh<const B: Word>(
163        &self,
164        x: &Repr<B>,
165        mut cache: Option<&mut ConstCache>,
166    ) -> FpResult<FBig<R, B>> {
167        if x.is_infinite() {
168            // tanh(±inf) = ±1
169            let one = FBig::new(Repr::one(), *self);
170            return Ok(Exact(if x.sign() == Sign::Negative {
171                -one
172            } else {
173                one
174            }));
175        }
176        assert_limited_precision(self.precision);
177        if x.significand.is_zero() {
178            // tanh(±0) = ±0
179            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
180        }
181
182        // tanh(x) = exp_m1(2x) / (exp_m1(2x) + 2). `exp_m1(2x)` is Ziv-correct at the working
183        // precision. For large positive x it overflows → tanh = +1 (returned inline as an exact
184        // value); for large negative x, exp_m1(2x) → -1 (finite), so tanh → -1 naturally.
185        let initial_guard = self.base_guard_digits::<B>() + 10;
186        Ok(self.ziv(initial_guard, |guard| {
187            let work = Context::<R>::new(self.precision + guard);
188            let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
189            let two_x = x_f * 2i32;
190            match work.exp_m1(&two_x.repr, reborrow_cache(&mut cache)) {
191                Err(FpError::Overflow(_)) => (FBig::<R, B>::ONE, FBig::<R, B>::ZERO), // exact +1
192                Ok(e) => {
193                    let e = e.value();
194                    let result = e.clone() / (e + 2i32);
195                    let radius = result.ulp() * 12;
196                    (result, radius)
197                }
198                Err(other) => unreachable!("exp_m1 on finite input: {other:?}"),
199            }
200        }))
201    }
202
203    /// Inverse hyperbolic sine.
204    pub fn asinh<const B: Word>(
205        &self,
206        x: &Repr<B>,
207        mut cache: Option<&mut ConstCache>,
208    ) -> FpResult<FBig<R, B>> {
209        if x.is_infinite() {
210            return Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self)));
211        }
212        assert_limited_precision(self.precision);
213        if x.significand.is_zero() {
214            // asinh(±0) = ±0
215            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
216        }
217
218        // asinh(x) = sign(x) · ln_1p(|x| + x²/(sqrt(x²+1)+1)) — the x²/(sqrt+1) form avoids the
219        // `sqrt(x²+1) − 1` cancellation near 0. `ln_1p`/`ln`/`sqrt` are Ziv-correct at the working
220        // precision, so the radius is a few working-ULPs of accumulated arithmetic. The `|x|` so
221        // large that `x²` overflows arm falls back to the asymptotic `sign·ln(2|x|)`.
222        let initial_guard = self.base_guard_digits::<B>() + 10;
223        Ok(self.ziv(initial_guard, |guard| {
224            let work = Context::<R>::new(self.precision + guard);
225            let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
226            let sign = x_f.sign();
227            let abs_x = x_f.abs();
228            let res = match work.sqr(&abs_x.repr) {
229                Ok(x_sq) => {
230                    let x_sq = x_sq.value();
231                    let sqrt_plus_one = work
232                        .sqrt(&(x_sq.clone() + FBig::<R, B>::ONE).repr)
233                        .unwrap()
234                        .value()
235                        + FBig::<R, B>::ONE;
236                    let arg = abs_x.clone() + x_sq / sqrt_plus_one;
237                    work.ln_1p(&arg.repr, reborrow_cache(&mut cache))
238                        .unwrap()
239                        .value()
240                }
241                // |x| so large that x² overflows: asinh(x) ≈ sign·ln(2|x|).
242                Err(FpError::Overflow(_)) => work
243                    .ln(&(abs_x.clone() * 2i32).repr, reborrow_cache(&mut cache))
244                    .unwrap()
245                    .value(),
246                Err(other) => unreachable!("sqr: {other:?}"),
247            };
248            let result = apply_sign(res, sign);
249            let radius = result.ulp() * 14;
250            (result, radius)
251        }))
252    }
253
254    /// Inverse hyperbolic cosine. Domain: `x ≥ 1`.
255    pub fn acosh<const B: Word>(
256        &self,
257        x: &Repr<B>,
258        mut cache: Option<&mut ConstCache>,
259    ) -> FpResult<FBig<R, B>> {
260        if x.is_infinite() {
261            if x.sign() == Sign::Negative {
262                return Err(FpError::OutOfDomain);
263            }
264            return Ok(Exact(FBig::new(Repr::infinity(), *self)));
265        }
266        assert_limited_precision(self.precision);
267        // domain x ≥ 1 (acosh(1) = 0 is handled below; x < 1 is an error)
268        if x.sign() == Sign::Negative
269            || FBig::<R, B>::new(x.clone(), *self)
270                .abs_cmp(&FBig::ONE)
271                .is_lt()
272        {
273            return Err(FpError::OutOfDomain);
274        }
275        if x.is_one() {
276            return Ok(Exact(FBig::new(Repr::zero(), *self)));
277        }
278
279        // acosh(x) = ln_1p((x-1) + sqrt((x-1)(x+1))) — the (x-1)(x+1) form avoids the `x²−1`
280        // cancellation near x = 1. `ln_1p`/`ln`/`sqrt` are Ziv-correct at the working precision;
281        // the radius is a few working-ULPs (generous for the near-x=1 cancellation). The `(x-1)(x+1)`
282        // overflow arm falls back to the asymptotic `ln(2x)`.
283        let initial_guard = self.base_guard_digits::<B>() + 10;
284        Ok(self.ziv(initial_guard, |guard| {
285            let work = Context::<R>::new(self.precision + guard);
286            let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
287            let xm1 = &x_f - FBig::<R, B>::ONE;
288            let xp1 = &x_f + FBig::<R, B>::ONE;
289            let res = match work.mul(&xm1.repr, &xp1.repr) {
290                Ok(prod) => {
291                    let arg = xm1.clone() + work.sqrt(&prod.value().repr).unwrap().value();
292                    work.ln_1p(&arg.repr, reborrow_cache(&mut cache))
293                        .unwrap()
294                        .value()
295                }
296                // (x-1)(x+1) overflowed: acosh(x) ≈ ln(2x).
297                Err(FpError::Overflow(_)) => work
298                    .ln(&(x_f.clone() * 2i32).repr, reborrow_cache(&mut cache))
299                    .unwrap()
300                    .value(),
301                Err(other) => unreachable!("mul: {other:?}"),
302            };
303            let radius = res.ulp() * 16;
304            (res, radius)
305        }))
306    }
307
308    /// Inverse hyperbolic tangent. Domain: `-1 < x < 1` (`x = ±1` → ±∞, `|x| > 1` is an error).
309    pub fn atanh<const B: Word>(
310        &self,
311        x: &Repr<B>,
312        mut cache: Option<&mut ConstCache>,
313    ) -> FpResult<FBig<R, B>> {
314        if x.is_infinite() {
315            return Err(FpError::OutOfDomain);
316        }
317        assert_limited_precision(self.precision);
318        if x.significand.is_zero() {
319            // atanh(±0) = ±0
320            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
321        }
322        // domain |x| < 1: |x| = 1 → ±∞ (value), |x| > 1 → error
323        match FBig::<R, B>::new(x.clone(), *self).abs_cmp(&FBig::ONE) {
324            core::cmp::Ordering::Greater => return Err(FpError::OutOfDomain),
325            core::cmp::Ordering::Equal => {
326                return Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self)));
327            }
328            _ => {}
329        }
330
331        // atanh(x) = ln_1p(2x/(1-x)) / 2. `ln_1p` is Ziv-correct at the working precision; the
332        // radius is a few working-ULPs (generous: the `2x/(1-x)` division amplifies as |x| → 1, but
333        // the result grows there too, so its ULP keeps the bound sound — Ziv retries near |x|=1).
334        let initial_guard = self.base_guard_digits::<B>() + 10;
335        Ok(self.ziv(initial_guard, |guard| {
336            let work = Context::<R>::new(self.precision + guard);
337            let x_f = FBig::<R, B>::new(work.repr_round_ref(x).value(), work);
338            let ratio = (x_f.clone() * 2i32) / (FBig::<R, B>::ONE - &x_f);
339            let res = work
340                .ln_1p(&ratio.repr, reborrow_cache(&mut cache))
341                .unwrap()
342                .value();
343            let result = res / 2i32;
344            let radius = result.ulp() * 16;
345            (result, radius)
346        }))
347    }
348}
349
350impl<R: ErrorBounds, const B: Word> FBig<R, B> {
351    /// Calculate the hyperbolic sine of the floating point number.
352    ///
353    /// # Examples
354    ///
355    /// ```
356    /// # use core::str::FromStr;
357    /// # use dashu_base::ParseError;
358    /// # use dashu_float::DBig;
359    /// let a = DBig::from_str("0.5000000")?;
360    /// assert_eq!(a.sinh(), DBig::from_str("0.52109531")?);
361    /// # Ok::<(), ParseError>(())
362    /// ```
363    #[inline]
364    pub fn sinh(&self) -> Self {
365        self.context.unwrap_fp(self.context.sinh(&self.repr, None))
366    }
367
368    /// Calculate the hyperbolic cosine of the floating point number.
369    ///
370    /// # Examples
371    ///
372    /// ```
373    /// # use core::str::FromStr;
374    /// # use dashu_base::ParseError;
375    /// # use dashu_float::DBig;
376    /// let a = DBig::from_str("0.5000000")?;
377    /// assert_eq!(a.cosh(), DBig::from_str("1.127626")?);
378    /// # Ok::<(), ParseError>(())
379    /// ```
380    #[inline]
381    pub fn cosh(&self) -> Self {
382        self.context.unwrap_fp(self.context.cosh(&self.repr, None))
383    }
384
385    /// Simultaneously calculate the hyperbolic sine and cosine of the number.
386    ///
387    /// This is more efficient than calling [`sinh`](FBig::sinh) and [`cosh`](FBig::cosh)
388    /// separately, since the two share the `exp_m1(±x)` sub-computations.
389    ///
390    /// # Examples
391    ///
392    /// ```
393    /// # use core::str::FromStr;
394    /// # use dashu_base::ParseError;
395    /// # use dashu_float::DBig;
396    /// let a = DBig::from_str("0.5000000")?;
397    /// let (s, c) = a.sinh_cosh();
398    /// assert_eq!(s, DBig::from_str("0.52109531")?);
399    /// assert_eq!(c, DBig::from_str("1.127626")?);
400    /// # Ok::<(), ParseError>(())
401    /// ```
402    #[inline]
403    pub fn sinh_cosh(&self) -> (Self, Self) {
404        let (s, c) = self.context.sinh_cosh(&self.repr, None);
405        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
406    }
407
408    /// Calculate the hyperbolic tangent of the floating point number.
409    ///
410    /// # Examples
411    ///
412    /// ```
413    /// # use core::str::FromStr;
414    /// # use dashu_base::ParseError;
415    /// # use dashu_float::DBig;
416    /// let a = DBig::from_str("0.5000000")?;
417    /// assert_eq!(a.tanh(), DBig::from_str("0.46211716")?);
418    /// # Ok::<(), ParseError>(())
419    /// ```
420    #[inline]
421    pub fn tanh(&self) -> Self {
422        self.context.unwrap_fp(self.context.tanh(&self.repr, None))
423    }
424
425    /// Calculate the inverse hyperbolic sine of the floating point number.
426    ///
427    /// # Examples
428    ///
429    /// ```
430    /// # use core::str::FromStr;
431    /// # use dashu_base::ParseError;
432    /// # use dashu_float::DBig;
433    /// let a = DBig::from_str("0.5000000")?;
434    /// assert_eq!(a.asinh(), DBig::from_str("0.48121183")?);
435    /// # Ok::<(), ParseError>(())
436    /// ```
437    #[inline]
438    pub fn asinh(&self) -> Self {
439        self.context.unwrap_fp(self.context.asinh(&self.repr, None))
440    }
441
442    /// Calculate the inverse hyperbolic cosine of the floating point number.
443    ///
444    /// # Panics
445    ///
446    /// Panics if the number is less than 1 (out of domain).
447    ///
448    /// # Examples
449    ///
450    /// ```
451    /// # use core::str::FromStr;
452    /// # use dashu_base::ParseError;
453    /// # use dashu_float::DBig;
454    /// let a = DBig::from_str("2.000000")?;
455    /// assert_eq!(a.acosh(), DBig::from_str("1.316958")?);
456    /// # Ok::<(), ParseError>(())
457    /// ```
458    #[inline]
459    pub fn acosh(&self) -> Self {
460        self.context.unwrap_fp(self.context.acosh(&self.repr, None))
461    }
462
463    /// Calculate the inverse hyperbolic tangent of the floating point number.
464    ///
465    /// # Panics
466    ///
467    /// Panics if the absolute value is greater than or equal to 1 (out of domain;
468    /// `|x| = 1` is infinite and `|x| > 1` is not real).
469    ///
470    /// # Examples
471    ///
472    /// ```
473    /// # use core::str::FromStr;
474    /// # use dashu_base::ParseError;
475    /// # use dashu_float::DBig;
476    /// let a = DBig::from_str("0.5000000")?;
477    /// assert_eq!(a.atanh(), DBig::from_str("0.54930614")?);
478    /// # Ok::<(), ParseError>(())
479    /// ```
480    #[inline]
481    pub fn atanh(&self) -> Self {
482        self.context.unwrap_fp(self.context.atanh(&self.repr, None))
483    }
484}
485
486/// `±0` `Repr` carrying the sign of `x` (used by the odd hyperbolics at zero input).
487fn signed_zero_repr<const B: Word>(x: &Repr<B>) -> Repr<B> {
488    if x.is_neg_zero() {
489        Repr::neg_zero()
490    } else {
491        Repr::zero()
492    }
493}
494
495/// Negate `v` when `sign` is `Negative` (used to apply `sign(x)` in `asinh`).
496fn apply_sign<R: Round, const B: Word>(v: FBig<R, B>, sign: Sign) -> FBig<R, B> {
497    if sign == Sign::Negative {
498        -v
499    } else {
500        v
501    }
502}