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    ball::{ulp_mag, ulps, Ball},
17    cmp::repr_cmp_same_base,
18    error::{assert_limited_precision, FpError},
19    exp::pow_chain_guard,
20    fbig::FBig,
21    math::{
22        cache::{reborrow_cache, ConstCache},
23        FpResult,
24    },
25    repr::{Context, Repr, Word},
26    round::{mode, ErrorBounds},
27};
28use dashu_base::{Abs, Approximation::Exact, BitTest, Sign};
29use dashu_int::IBig;
30
31impl<R: ErrorBounds> Context<R> {
32    /// Hyperbolic sine.
33    pub fn sinh<const B: Word>(
34        &self,
35        x: &Repr<B>,
36        mut cache: Option<&mut ConstCache>,
37    ) -> FpResult<FBig<R, B>> {
38        if x.is_infinite() {
39            return Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self)));
40        }
41        assert_limited_precision(self.precision);
42        if x.significand.is_zero() {
43            // sinh(±0) = ±0
44            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
45        }
46        // sinh(x) = (exp_m1(x) - exp_m1(-x)) / 2  (cancellation-free). Both `exp_m1` come from the
47        // Ball-based `exp_compute`; the subtraction/division roundings, the `exp_m1` errors *and
48        // the input's own rounding* are tracked mechanically by the Ball propagation (the raw `x`
49        // is passed in — `exp_compute` rounds it to the working precision and carries the error).
50        // For huge |x|, `exp_m1` overflows inside the closure and propagates; sinh(±huge) = ±inf,
51        // so the sign follows `x` (the propagated error carries an intermediate sign, remapped
52        // below).
53        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
54        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
55        // the growing work precision, as before). Sized once, outside the loop.
56        let initial_guard = self.base_guard_digits::<B>()
57            + 10
58            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
59        self.ziv(initial_guard, |guard| {
60            let work = Context::<mode::HalfEven>::new(self.precision + guard);
61            let n = 1usize << (work.precision.bit_len() / 2);
62            let neg_x = -x.clone();
63            let ep =
64                work.exp_compute::<B>(x, work.precision, true, n, reborrow_cache(&mut cache))?;
65            let em =
66                work.exp_compute::<B>(&neg_x, work.precision, true, n, reborrow_cache(&mut cache))?;
67            let wp = work.precision;
68            let s2 = addsub_guarded(&ep, &em, true, wp)?;
69            let d2 = s2.div_int(2, wp)?;
70            Ok(d2.to_value_radius::<R>(&Context::<R>::new(wp)))
71        })
72        .map_err(|_| FpError::Overflow(x.sign()))
73    }
74
75    /// Hyperbolic cosine.
76    pub fn cosh<const B: Word>(
77        &self,
78        x: &Repr<B>,
79        mut cache: Option<&mut ConstCache>,
80    ) -> FpResult<FBig<R, B>> {
81        if x.is_infinite() {
82            // cosh(±inf) = +inf
83            return Ok(Exact(FBig::new(Repr::infinity(), *self)));
84        }
85        assert_limited_precision(self.precision);
86        if x.significand.is_zero() {
87            // cosh(±0) = 1
88            return Ok(Exact(FBig::new(Repr::one(), *self)));
89        }
90
91        // cosh(x) = (exp_m1(x) + exp_m1(-x)) / 2 + 1 (no cancellation: same-sign sum). Both
92        // `exp_m1` come from the Ball-based `exp_compute`; the sum/divide/+1 roundings and the
93        // input's own rounding (folded by `exp_compute` from the raw `x`) are tracked
94        // mechanically. For huge |x|, `exp_m1` overflows inside the closure and propagates;
95        // cosh(±huge) = +inf (always positive).
96        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
97        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
98        // the growing work precision, as before). Sized once, outside the loop.
99        let initial_guard = self.base_guard_digits::<B>()
100            + 10
101            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
102        self.ziv(initial_guard, |guard| {
103            let work = Context::<mode::HalfEven>::new(self.precision + guard);
104            let n = 1usize << (work.precision.bit_len() / 2);
105            let neg_x = -x.clone();
106            let ep =
107                work.exp_compute::<B>(x, work.precision, true, n, reborrow_cache(&mut cache))?;
108            let em =
109                work.exp_compute::<B>(&neg_x, work.precision, true, n, reborrow_cache(&mut cache))?;
110            let wp = work.precision;
111            let one = Ball::exact_int(IBig::ONE, wp);
112            let half_sum = addsub_guarded(&ep, &em, false, wp)?.div_int(2, wp)?;
113            Ok(addsub_guarded(&half_sum, &one, false, wp)?
114                .to_value_radius::<R>(&Context::<R>::new(wp)))
115        })
116        .map_err(|_| FpError::Overflow(Sign::Positive))
117    }
118
119    /// Simultaneously compute `sinh(x)` and `cosh(x)` (context layer). Returns
120    /// `(sinh_result, cosh_result)` where each is a [`FpResult`].
121    ///
122    /// This is more efficient than calling [`sinh`](Context::sinh) and [`cosh`](Context::cosh)
123    /// separately, since the two share the `exp_m1(±x)` sub-computations.
124    pub fn sinh_cosh<const B: Word>(
125        &self,
126        x: &Repr<B>,
127        mut cache: Option<&mut ConstCache>,
128    ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
129        if x.is_infinite() {
130            return (
131                Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self))),
132                Ok(Exact(FBig::new(Repr::infinity(), *self))),
133            );
134        }
135        assert_limited_precision(self.precision);
136        if x.significand.is_zero() {
137            return (
138                Ok(Exact(FBig::new(signed_zero_repr(x), *self))),
139                Ok(Exact(FBig::new(Repr::one(), *self))),
140            );
141        }
142
143        // sinh = (ep - em)/2; cosh = (ep + em)/2 + 1, sharing the two `exp_m1` calls. Certified as a
144        // pair via `ziv_pair` (retry while either endpoint straddles a boundary); the input's own
145        // rounding is folded by `exp_compute` from the raw `x`. For huge |x|, `exp_m1` overflows
146        // inside the closure and propagates to both slots; sinh(±huge) = ±inf, cosh(±huge) = +inf,
147        // so each slot's overflow sign is remapped below.
148        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
149        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
150        // the growing work precision, as before). Sized once, outside the loop.
151        let initial_guard = self.base_guard_digits::<B>()
152            + 10
153            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
154        let (sinh_r, cosh_r) = self.ziv_pair(initial_guard, |guard| {
155            let work = Context::<mode::HalfEven>::new(self.precision + guard);
156            let n = 1usize << (work.precision.bit_len() / 2);
157            let neg_x = -x.clone();
158            let ep =
159                work.exp_compute::<B>(x, work.precision, true, n, reborrow_cache(&mut cache))?;
160            let em =
161                work.exp_compute::<B>(&neg_x, work.precision, true, n, reborrow_cache(&mut cache))?;
162            let wp = work.precision;
163            let one = Ball::exact_int(IBig::ONE, wp);
164            let sinh_ball = addsub_guarded(&ep, &em, true, wp)?.div_int(2, wp)?;
165            let half_sum = addsub_guarded(&ep, &em, false, wp)?.div_int(2, wp)?;
166            let cosh_ball = addsub_guarded(&half_sum, &one, false, wp)?;
167            let ctx = Context::<R>::new(wp);
168            Ok((sinh_ball.to_value_radius::<R>(&ctx), cosh_ball.to_value_radius::<R>(&ctx)))
169        });
170        (
171            sinh_r.map_err(|_| FpError::Overflow(x.sign())),
172            cosh_r.map_err(|_| FpError::Overflow(Sign::Positive)),
173        )
174    }
175
176    /// Hyperbolic sine of `x·π`, i.e. `sinh(x·π)`.
177    ///
178    /// # Methodology
179    /// The argument ball `t = π·x` (π from the shared constant cache, both roundings tracked by
180    /// the ball arithmetic) feeds the all-positive sinh Maclaurin series for `|t| ≤ 1` — where
181    /// the exponential form's `exp(t) − exp(−t)` would catastrophically cancel — and the
182    /// ball `exp` composition beyond (`|exp(t)| / |exp(−t)| ≥ e²` there, a benign difference).
183    /// Unlike the circular ×π functions there are no rational special points (π·x is
184    /// transcendental for every nonzero rational x), so the zero case is the only exact one.
185    pub fn sinh_pi<const B: Word>(
186        &self,
187        x: &Repr<B>,
188        mut cache: Option<&mut ConstCache>,
189    ) -> FpResult<FBig<R, B>> {
190        if x.is_infinite() {
191            return Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self)));
192        }
193        assert_limited_precision(self.precision);
194        if x.significand.is_zero() {
195            // sinh(±0) = ±0
196            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
197        }
198
199        // For huge |x| the exp composition overflows inside the closure and propagates;
200        // sinh(±π·huge) = ±∞, so the sign follows x.
201        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
202        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
203        // the growing work precision, as before). Sized once, outside the loop.
204        let initial_guard = self.base_guard_digits::<B>()
205            + 10
206            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
207        self.ziv(initial_guard, |guard| {
208            let work = Context::<mode::HalfEven>::new(self.precision + guard);
209            let wp = work.precision;
210            let t = crate::math::trig::pi_scaled_ball(&work, x, reborrow_cache(&mut cache))?;
211            let one = Ball::exact_int(IBig::ONE, wp);
212            let val = if repr_cmp_same_base::<B, true>(&t.mid, &one.mid, None).is_le() {
213                work.sinh_series(&t)?
214            } else {
215                let ep = work.exp_ball::<B>(&t, wp, reborrow_cache(&mut cache))?;
216                let em = work.exp_ball::<B>(&-t.clone(), wp, reborrow_cache(&mut cache))?;
217                addsub_guarded(&ep, &em, true, wp)?.div_int(2, wp)?
218            };
219            Ok(val.to_value_radius::<R>(&Context::<R>::new(wp)))
220        })
221        .map_err(|_| FpError::Overflow(x.sign()))
222    }
223
224    /// Hyperbolic cosine of `x·π`, i.e. `cosh(x·π)`.
225    ///
226    /// # Methodology
227    /// The same `t = π·x` ball as [`sinh_pi`](Self::sinh_pi): the all-positive cosh series for
228    /// `|t| ≤ 1`, the `(exp(t) + exp(−t))/2` sum beyond (same-sign terms, no cancellation).
229    pub fn cosh_pi<const B: Word>(
230        &self,
231        x: &Repr<B>,
232        mut cache: Option<&mut ConstCache>,
233    ) -> FpResult<FBig<R, B>> {
234        if x.is_infinite() {
235            // cosh(±inf) = +inf
236            return Ok(Exact(FBig::new(Repr::infinity(), *self)));
237        }
238        assert_limited_precision(self.precision);
239        if x.significand.is_zero() {
240            // cosh(±0) = 1
241            return Ok(Exact(FBig::new(Repr::one(), *self)));
242        }
243
244        // For huge |x| the exp composition overflows inside the closure and propagates;
245        // cosh(±π·huge) = +∞ (always positive).
246        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
247        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
248        // the growing work precision, as before). Sized once, outside the loop.
249        let initial_guard = self.base_guard_digits::<B>()
250            + 10
251            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
252        self.ziv(initial_guard, |guard| {
253            let work = Context::<mode::HalfEven>::new(self.precision + guard);
254            let wp = work.precision;
255            let t = crate::math::trig::pi_scaled_ball(&work, x, reborrow_cache(&mut cache))?;
256            let one = Ball::exact_int(IBig::ONE, wp);
257            let val = if repr_cmp_same_base::<B, true>(&t.mid, &one.mid, None).is_le() {
258                work.cosh_series(&t)?
259            } else {
260                // cosh(t) = (e^t + e^−t)/2 with the *true* exponentials (no `+1` — that belongs
261                // to the exp_m1 form `cosh` uses below on the radian side).
262                let ep = work.exp_ball::<B>(&t, wp, reborrow_cache(&mut cache))?;
263                let em = work.exp_ball::<B>(&-t.clone(), wp, reborrow_cache(&mut cache))?;
264                addsub_guarded(&ep, &em, false, wp)?.div_int(2, wp)?
265            };
266            Ok(val.to_value_radius::<R>(&Context::<R>::new(wp)))
267        })
268        .map_err(|_| FpError::Overflow(Sign::Positive))
269    }
270
271    /// Simultaneously compute `sinh(x·π)` and `cosh(x·π)` (context layer).
272    ///
273    /// This is more efficient than calling [`sinh_pi`](Self::sinh_pi) and
274    /// [`cosh_pi`](Self::cosh_pi) separately, since the two share the argument ball and the
275    /// exponential composition.
276    pub fn sinh_cosh_pi<const B: Word>(
277        &self,
278        x: &Repr<B>,
279        mut cache: Option<&mut ConstCache>,
280    ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
281        if x.is_infinite() {
282            return (
283                Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self))),
284                Ok(Exact(FBig::new(Repr::infinity(), *self))),
285            );
286        }
287        assert_limited_precision(self.precision);
288        if x.significand.is_zero() {
289            return (
290                Ok(Exact(FBig::new(signed_zero_repr(x), *self))),
291                Ok(Exact(FBig::new(Repr::one(), *self))),
292            );
293        }
294
295        // Certified as a pair via `ziv_pair` (retry while either endpoint straddles a boundary);
296        // the overflow sign is remapped per slot as in `sinh_cosh`.
297        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
298        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
299        // the growing work precision, as before). Sized once, outside the loop.
300        let initial_guard = self.base_guard_digits::<B>()
301            + 10
302            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
303        let (sinh_r, cosh_r) = self.ziv_pair(initial_guard, |guard| {
304            let work = Context::<mode::HalfEven>::new(self.precision + guard);
305            let wp = work.precision;
306            let t = crate::math::trig::pi_scaled_ball(&work, x, reborrow_cache(&mut cache))?;
307            let one = Ball::exact_int(IBig::ONE, wp);
308            let (sh, ch) = if repr_cmp_same_base::<B, true>(&t.mid, &one.mid, None).is_le() {
309                (work.sinh_series(&t)?, work.cosh_series(&t)?)
310            } else {
311                // true exponentials — no `+1` (see `cosh_pi`)
312                let ep = work.exp_ball::<B>(&t, wp, reborrow_cache(&mut cache))?;
313                let em = work.exp_ball::<B>(&-t.clone(), wp, reborrow_cache(&mut cache))?;
314                (
315                    addsub_guarded(&ep, &em, true, wp)?.div_int(2, wp)?,
316                    addsub_guarded(&ep, &em, false, wp)?.div_int(2, wp)?,
317                )
318            };
319            let ctx = Context::<R>::new(wp);
320            Ok((sh.to_value_radius::<R>(&ctx), ch.to_value_radius::<R>(&ctx)))
321        });
322        (
323            sinh_r.map_err(|_| FpError::Overflow(x.sign())),
324            cosh_r.map_err(|_| FpError::Overflow(Sign::Positive)),
325        )
326    }
327
328    /// Near-correct `sinh` series `S(t) = t + t³/3! + t⁵/5! + …` on `|t| ≤ 1`: all terms share
329    /// the sign of t, and the ratio between consecutive terms is `t²/((2k)(2k+1)) ≤ 1/6`, so
330    /// the omitted tail is bounded by the first omitted term (< 1 ulp, as for the sine series).
331    fn sinh_series<const B: Word>(self, x: &Ball<B>) -> Result<Ball<B>, FpError> {
332        let wp = self.precision;
333        let x2 = x.mul(x, wp)?;
334        let mut sum = x.clone();
335        let mut term = x.clone();
336        let mut k = 1usize;
337        loop {
338            term = term.mul(&x2, wp)?.div_int((2 * k) * (2 * k + 1), wp)?;
339            if term.mid_le_ulp_lb(&sum, wp) {
340                break;
341            }
342            sum = sum.add(&term, wp)?;
343            k += 1;
344        }
345        sum.add_error(ulps::<B>(&sum.mid, wp, 2));
346        Ok(sum)
347    }
348
349    /// Near-correct `cosh` series `C(t) = 1 + t²/2! + t⁴/4! + …` on `|t| ≤ 1`
350    /// (see [`sinh_series`](Self::sinh_series)).
351    fn cosh_series<const B: Word>(self, x: &Ball<B>) -> Result<Ball<B>, FpError> {
352        let wp = self.precision;
353        let x2 = x.mul(x, wp)?;
354        let one = Ball::exact_int(IBig::ONE, wp);
355        let mut sum = one.clone();
356        let mut term = one;
357        let mut k = 1usize;
358        loop {
359            term = term.mul(&x2, wp)?.div_int((2 * k) * (2 * k - 1), wp)?;
360            if term.mid_le_ulp_lb(&sum, wp) {
361                break;
362            }
363            sum = sum.add(&term, wp)?;
364            k += 1;
365        }
366        sum.add_error(ulps::<B>(&sum.mid, wp, 2));
367        Ok(sum)
368    }
369
370    /// Hyperbolic tangent.
371    pub fn tanh<const B: Word>(
372        &self,
373        x: &Repr<B>,
374        mut cache: Option<&mut ConstCache>,
375    ) -> FpResult<FBig<R, B>> {
376        if x.is_infinite() {
377            // tanh(±inf) = ±1
378            let one = FBig::new(Repr::one(), *self);
379            return Ok(Exact(if x.sign() == Sign::Negative {
380                -one
381            } else {
382                one
383            }));
384        }
385        assert_limited_precision(self.precision);
386        if x.significand.is_zero() {
387            // tanh(±0) = ±0
388            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
389        }
390
391        // tanh(x) = exp_m1(2x) / (exp_m1(2x) + 2). `exp_m1(2x)` comes from the Ball-based
392        // `exp_compute` on the *exact* `2x = x + x` (so `exp_compute` folds the input's own
393        // rounding into the radius); the division's rounding is tracked mechanically. For large
394        // positive x it overflows → tanh = +1 (returned inline as an exact value); for large
395        // negative x, exp_m1(2x) → -1 (finite), so tanh → -1 naturally.
396        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
397        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
398        // the growing work precision, as before). Sized once, outside the loop.
399        let initial_guard = self.base_guard_digits::<B>()
400            + 10
401            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
402        self.ziv(initial_guard, |guard| {
403            let work = Context::<mode::HalfEven>::new(self.precision + guard);
404            let n = 1usize << (work.precision.bit_len() / 2);
405            let two_x = x + x;
406            match work.exp_compute::<B>(&two_x, work.precision, true, n, reborrow_cache(&mut cache))
407            {
408                Err(FpError::Overflow(_)) => Ok((FBig::<R, B>::ONE, FBig::<R, B>::ZERO)), // exact +1
409                Ok(e) => {
410                    let wp = work.precision;
411                    let two = Ball::exact_int(IBig::from(2), wp);
412                    Ok(e.div(&e.add(&two, wp)?, wp)?
413                        .to_value_radius::<R>(&Context::<R>::new(wp)))
414                }
415                Err(other) => unreachable!("exp_m1 on finite input: {other:?}"),
416            }
417        })
418    }
419
420    /// Inverse hyperbolic sine.
421    pub fn asinh<const B: Word>(
422        &self,
423        x: &Repr<B>,
424        mut cache: Option<&mut ConstCache>,
425    ) -> FpResult<FBig<R, B>> {
426        if x.is_infinite() {
427            return Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self)));
428        }
429        assert_limited_precision(self.precision);
430        if x.significand.is_zero() {
431            // asinh(±0) = ±0
432            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
433        }
434
435        // asinh(x) = sign(x) · ln_1p(|x| + x²/(sqrt(x²+1)+1)) — the x²/(sqrt+1) form avoids the
436        // `sqrt(x²+1) − 1` cancellation near 0. The composition is tracked as a [`Ball`] from the
437        // rounded input up: the input's own rounding, the sqr, sqrt, division and the `ln_1p`
438        // input error all propagate mechanically (a hand-picked per-op ulp count cannot see the
439        // operand errors inherited through the chain). The `|x|` so large that `x²` overflows arm
440        // falls back to the asymptotic `sign·ln(2|x|)` on the exact `2|x|` (whose input rounding
441        // `ln_compute` folds itself).
442        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
443        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
444        // the growing work precision, as before). Sized once, outside the loop.
445        let initial_guard = self.base_guard_digits::<B>()
446            + 10
447            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
448        self.ziv(initial_guard, |guard| {
449            let work = Context::<mode::HalfEven>::new(self.precision + guard);
450            let sign = x.sign();
451            let wp = work.precision;
452            let x_ball = Ball::from_rounded(work.repr_round_ref(x), wp);
453            let abs_x_ball = if sign == Sign::Negative {
454                -x_ball
455            } else {
456                x_ball
457            };
458            let one = Ball::exact_int(IBig::ONE, wp);
459            let res = match abs_x_ball.sqr(wp) {
460                Ok(x_sq_ball) => {
461                    let sqrt_plus_one = x_sq_ball.add(&one, wp)?.sqrt(wp)?.add(&one, wp)?;
462                    let arg = abs_x_ball.add(&x_sq_ball.div(&sqrt_plus_one, wp)?, wp)?;
463                    work.ln_1p_ball::<B>(&arg, reborrow_cache(&mut cache))
464                }
465                // |x| so large that x² overflows: asinh(x) ≈ sign·ln(2|x|).
466                Err(FpError::Overflow(_)) => {
467                    let two_abs = Repr::new((x.significand() * IBig::from(2)).abs(), x.exponent());
468                    work.ln_compute::<B>(
469                        &two_abs,
470                        work.precision,
471                        false,
472                        reborrow_cache(&mut cache),
473                    )
474                }
475                Err(other) => unreachable!("sqr: {other:?}"),
476            }?;
477            let result = if sign == Sign::Negative { -res } else { res };
478            Ok(result.to_value_radius::<R>(&Context::<R>::new(wp)))
479        })
480    }
481
482    /// Inverse hyperbolic cosine. Domain: `x ≥ 1`.
483    pub fn acosh<const B: Word>(
484        &self,
485        x: &Repr<B>,
486        mut cache: Option<&mut ConstCache>,
487    ) -> FpResult<FBig<R, B>> {
488        if x.is_infinite() {
489            if x.sign() == Sign::Negative {
490                return Err(FpError::OutOfDomain);
491            }
492            return Ok(Exact(FBig::new(Repr::infinity(), *self)));
493        }
494        assert_limited_precision(self.precision);
495        // Domain x ≥ 1, compared *exactly* (no precision argument) because `1` is a vertical
496        // tangent of `acosh`: `1 + 10⁻¹⁵⁵` at 100 digits rounds onto `1`, but its true value is
497        // `√2·10⁻⁷⁸`, not `0`.
498        if x.sign() == Sign::Negative {
499            return Err(FpError::OutOfDomain);
500        }
501        match repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None) {
502            core::cmp::Ordering::Less => return Err(FpError::OutOfDomain),
503            core::cmp::Ordering::Equal => return Ok(Exact(FBig::new(Repr::zero(), *self))),
504            core::cmp::Ordering::Greater => {}
505        }
506
507        // acosh(x) = ln_1p((x-1) + sqrt((x-1)(x+1))) — the (x-1)(x+1) form avoids the `x²−1`
508        // cancellation near x = 1. The composition is tracked as a [`Ball`] from the rounded input
509        // up: the input's own rounding flows into `x−1`/`x+1` and through the product, sqrt,
510        // addition and the `ln_1p` input error mechanically. (The input rounding is what a
511        // per-op ulp count on the *result* of `x−1` misses: near x = 1 the subtraction cancels
512        // the value — and its ulp — down by |x−1|, while the inherited operand error stays at
513        // ulp(x); an under-estimated radius then lets Ziv certify the wrong neighbour of a tie.)
514        // The `(x-1)(x+1)` overflow arm falls back to the asymptotic `ln(2x)` on the exact `2x`
515        // (whose input rounding `ln_compute` folds itself).
516        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
517        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
518        // the growing work precision, as before). Sized once, outside the loop.
519        let initial_guard = self.base_guard_digits::<B>()
520            + 10
521            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
522        self.ziv(initial_guard, |guard| {
523            let work = Context::<mode::HalfEven>::new(self.precision + guard);
524            let wp = work.precision;
525            let x_ball = Ball::from_rounded(work.repr_round_ref(x), wp);
526            let one = Ball::exact_int(IBig::ONE, wp);
527            let xm1_ball = x_ball.sub(&one, wp)?;
528            let xp1_ball = x_ball.add(&one, wp)?;
529            let res = match xm1_ball.mul(&xp1_ball, wp) {
530                Ok(prod_ball) => {
531                    let arg = xm1_ball.add(&prod_ball.sqrt(wp)?, wp)?;
532                    work.ln_1p_ball::<B>(&arg, reborrow_cache(&mut cache))
533                }
534                // (x-1)(x+1) overflowed: acosh(x) ≈ ln(2x).
535                Err(FpError::Overflow(_)) => {
536                    let two_x = Repr::new(x.significand() * IBig::from(2), x.exponent());
537                    work.ln_compute::<B>(&two_x, work.precision, false, reborrow_cache(&mut cache))
538                }
539                Err(other) => unreachable!("mul: {other:?}"),
540            }?;
541            Ok(res.to_value_radius::<R>(&Context::<R>::new(wp)))
542        })
543    }
544
545    /// Inverse hyperbolic tangent. Domain: `-1 < x < 1` (`x = ±1` → ±∞, `|x| > 1` is an error).
546    pub fn atanh<const B: Word>(
547        &self,
548        x: &Repr<B>,
549        mut cache: Option<&mut ConstCache>,
550    ) -> FpResult<FBig<R, B>> {
551        if x.is_infinite() {
552            return Err(FpError::OutOfDomain);
553        }
554        assert_limited_precision(self.precision);
555        if x.significand.is_zero() {
556            // atanh(±0) = ±0
557            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
558        }
559        // Domain |x| < 1: |x| = 1 → ±∞ (value), |x| > 1 → error. The boundary is compared
560        // *exactly* (no precision argument) because `±1` is a pole of `atanh`: `1 − 10⁻¹⁵⁵`
561        // at 100 digits rounds onto `1` but is the finite `(p+155)·ln(10)/2`, not `∞`.
562        match repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None) {
563            core::cmp::Ordering::Greater => return Err(FpError::OutOfDomain),
564            core::cmp::Ordering::Equal => {
565                return Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self)));
566            }
567            _ => {}
568        }
569
570        // atanh(x) = ln_1p(2x/(1-x)) / 2. The ratio and the `ln_1p` input error are tracked as a
571        // [`Ball`]; near |x| = 1 the `2x/(1-x)` division amplifies, but the Ball tracks it (Ziv
572        // retries there).
573        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
574        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
575        // the growing work precision, as before). Sized once, outside the loop.
576        let initial_guard = self.base_guard_digits::<B>()
577            + 10
578            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
579        self.ziv(initial_guard, |guard| {
580            let work = Context::<mode::HalfEven>::new(self.precision + guard);
581            let wp = work.precision;
582            let x_ball = Ball::from_rounded(work.repr_round_ref(x), wp);
583            let one = Ball::exact_int(IBig::ONE, wp);
584            let ratio = x_ball
585                .scale_int(&IBig::from(2), wp)?
586                .div(&one.sub(&x_ball, wp)?, wp)?;
587            let res = work.ln_1p_ball::<B>(&ratio, reborrow_cache(&mut cache))?;
588            Ok(res
589                .div_int(2, wp)?
590                .to_value_radius::<R>(&Context::<R>::new(wp)))
591        })
592    }
593}
594
595impl<R: ErrorBounds, const B: Word> FBig<R, B> {
596    /// Calculate the hyperbolic sine of the floating point number.
597    ///
598    /// # Examples
599    ///
600    /// ```
601    /// # use core::str::FromStr;
602    /// # use dashu_base::ParseError;
603    /// # use dashu_float::DBig;
604    /// let a = DBig::from_str("0.5000000")?;
605    /// assert_eq!(a.sinh(), DBig::from_str("0.52109531")?);
606    /// # Ok::<(), ParseError>(())
607    /// ```
608    #[inline]
609    pub fn sinh(&self) -> Self {
610        self.context.unwrap_fp(self.context.sinh(&self.repr, None))
611    }
612
613    /// Calculate the hyperbolic cosine of the floating point number.
614    ///
615    /// # Examples
616    ///
617    /// ```
618    /// # use core::str::FromStr;
619    /// # use dashu_base::ParseError;
620    /// # use dashu_float::DBig;
621    /// let a = DBig::from_str("0.5000000")?;
622    /// assert_eq!(a.cosh(), DBig::from_str("1.127626")?);
623    /// # Ok::<(), ParseError>(())
624    /// ```
625    #[inline]
626    pub fn cosh(&self) -> Self {
627        self.context.unwrap_fp(self.context.cosh(&self.repr, None))
628    }
629
630    /// Simultaneously calculate the hyperbolic sine and cosine of the number.
631    ///
632    /// This is more efficient than calling [`sinh`](FBig::sinh) and [`cosh`](FBig::cosh)
633    /// separately, since the two share the `exp_m1(±x)` sub-computations.
634    ///
635    /// # Examples
636    ///
637    /// ```
638    /// # use core::str::FromStr;
639    /// # use dashu_base::ParseError;
640    /// # use dashu_float::DBig;
641    /// let a = DBig::from_str("0.5000000")?;
642    /// let (s, c) = a.sinh_cosh();
643    /// assert_eq!(s, DBig::from_str("0.52109531")?);
644    /// assert_eq!(c, DBig::from_str("1.127626")?);
645    /// # Ok::<(), ParseError>(())
646    /// ```
647    #[inline]
648    pub fn sinh_cosh(&self) -> (Self, Self) {
649        let (s, c) = self.context.sinh_cosh(&self.repr, None);
650        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
651    }
652
653    /// Calculate the hyperbolic sine of the floating point number multiplied by π, i.e.
654    /// `sinh(self·π)`.
655    ///
656    /// Unlike [`sinh`](FBig::sinh), the ×π variant's argument ball is built from the shared π
657    /// constant, so repeated calls at increasing precision reuse the cached Chudnovsky state.
658    ///
659    /// # Examples
660    /// ```
661    /// # use core::str::FromStr;
662    /// # use dashu_base::ParseError;
663    /// # use dashu_float::DBig;
664    /// // sinh(π/2) ≈ 2.30129890
665    /// let a = DBig::from_str("0.50000000")?;
666    /// assert_eq!(a.sinh_pi(), DBig::from_str("2.30129890")?);
667    /// # Ok::<(), ParseError>(())
668    /// ```
669    #[inline]
670    pub fn sinh_pi(&self) -> Self {
671        self.context
672            .unwrap_fp(self.context.sinh_pi(&self.repr, None))
673    }
674
675    /// Calculate the hyperbolic cosine of the floating point number multiplied by π, i.e.
676    /// `cosh(self·π)`.
677    ///
678    /// # Examples
679    /// ```
680    /// # use core::str::FromStr;
681    /// # use dashu_base::ParseError;
682    /// # use dashu_float::DBig;
683    /// // cosh(π/2) ≈ 2.50917848
684    /// let a = DBig::from_str("0.50000000")?;
685    /// assert_eq!(a.cosh_pi(), DBig::from_str("2.50917848")?);
686    /// # Ok::<(), ParseError>(())
687    /// ```
688    #[inline]
689    pub fn cosh_pi(&self) -> Self {
690        self.context
691            .unwrap_fp(self.context.cosh_pi(&self.repr, None))
692    }
693
694    /// Simultaneously calculate the hyperbolic sine and cosine of the number multiplied by π.
695    ///
696    /// This is more efficient than calling [`sinh_pi`](FBig::sinh_pi) and
697    /// [`cosh_pi`](FBig::cosh_pi) separately, since the two share the argument ball and the
698    /// exponential composition.
699    ///
700    /// # Examples
701    /// ```
702    /// # use core::str::FromStr;
703    /// # use dashu_base::ParseError;
704    /// # use dashu_float::DBig;
705    /// let a = DBig::from_str("0.50000000")?;
706    /// let (s, c) = a.sinh_cosh_pi();
707    /// assert_eq!(s, DBig::from_str("2.30129890")?);
708    /// assert_eq!(c, DBig::from_str("2.50917848")?);
709    /// # Ok::<(), ParseError>(())
710    /// ```
711    #[inline]
712    pub fn sinh_cosh_pi(&self) -> (Self, Self) {
713        let (s, c) = self.context.sinh_cosh_pi(&self.repr, None);
714        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
715    }
716
717    /// Calculate the hyperbolic tangent of the floating point number.
718    ///
719    /// # Examples
720    ///
721    /// ```
722    /// # use core::str::FromStr;
723    /// # use dashu_base::ParseError;
724    /// # use dashu_float::DBig;
725    /// let a = DBig::from_str("0.5000000")?;
726    /// assert_eq!(a.tanh(), DBig::from_str("0.46211716")?);
727    /// # Ok::<(), ParseError>(())
728    /// ```
729    #[inline]
730    pub fn tanh(&self) -> Self {
731        self.context.unwrap_fp(self.context.tanh(&self.repr, None))
732    }
733
734    /// Calculate the inverse hyperbolic sine of the floating point number.
735    ///
736    /// # Examples
737    ///
738    /// ```
739    /// # use core::str::FromStr;
740    /// # use dashu_base::ParseError;
741    /// # use dashu_float::DBig;
742    /// let a = DBig::from_str("0.5000000")?;
743    /// assert_eq!(a.asinh(), DBig::from_str("0.48121183")?);
744    /// # Ok::<(), ParseError>(())
745    /// ```
746    #[inline]
747    pub fn asinh(&self) -> Self {
748        self.context.unwrap_fp(self.context.asinh(&self.repr, None))
749    }
750
751    /// Calculate the inverse hyperbolic cosine of the floating point number.
752    ///
753    /// # Panics
754    ///
755    /// Panics if the number is less than 1 (out of domain).
756    ///
757    /// # Examples
758    ///
759    /// ```
760    /// # use core::str::FromStr;
761    /// # use dashu_base::ParseError;
762    /// # use dashu_float::DBig;
763    /// let a = DBig::from_str("2.000000")?;
764    /// assert_eq!(a.acosh(), DBig::from_str("1.316958")?);
765    /// # Ok::<(), ParseError>(())
766    /// ```
767    #[inline]
768    pub fn acosh(&self) -> Self {
769        self.context.unwrap_fp(self.context.acosh(&self.repr, None))
770    }
771
772    /// Calculate the inverse hyperbolic tangent of the floating point number.
773    ///
774    /// # Panics
775    ///
776    /// Panics if the absolute value is greater than or equal to 1 (out of domain;
777    /// `|x| = 1` is infinite and `|x| > 1` is not real).
778    ///
779    /// # Examples
780    ///
781    /// ```
782    /// # use core::str::FromStr;
783    /// # use dashu_base::ParseError;
784    /// # use dashu_float::DBig;
785    /// let a = DBig::from_str("0.5000000")?;
786    /// assert_eq!(a.atanh(), DBig::from_str("0.54930614")?);
787    /// # Ok::<(), ParseError>(())
788    /// ```
789    #[inline]
790    pub fn atanh(&self) -> Self {
791        self.context.unwrap_fp(self.context.atanh(&self.repr, None))
792    }
793}
794
795/// `±0` `Repr` carrying the sign of `x` (used by the odd hyperbolics at zero input).
796fn signed_zero_repr<const B: Word>(x: &Repr<B>) -> Repr<B> {
797    if x.is_neg_zero() {
798        Repr::neg_zero()
799    } else {
800        Repr::zero()
801    }
802}
803
804/// `a ± b` with a magnitude guard: when one operand sits more than `prec + 2` digits below the
805/// other, it is dropped and the radius inflated by one ulp instead — a sound substitute (the
806/// dropped term is < 1 ulp of the dominant one) for the plain ball add/sub, whose midpoint
807/// alignment would try to materialize the astronomically large digit gap between the two
808/// exponentials of `(exp(t) ± exp(−t))/2` (an out-of-memory panic for large |t|, e.g.
809/// `sinh(1e14)`).
810///
811/// The guard is a caller-side shortcut, not the only defence — `repr_round_sum` also collapses
812/// an astronomically low sticky — but it avoids the alignment entirely, which is the expensive
813/// half of the operation.
814fn addsub_guarded<const B: Word>(
815    a: &Ball<B>,
816    b: &Ball<B>,
817    sub: bool,
818    prec: usize,
819) -> Result<Ball<B>, FpError> {
820    let a_dominant = repr_cmp_same_base::<B, true>(&a.mid, &b.mid, None).is_ge();
821    let (big, small) = if a_dominant { (a, b) } else { (b, a) };
822    // `small ≤ big·B^−(prec+2)`, checked against an exponent-only shifted threshold (the log2
823    // comparison inside `abs_cmp` short-circuits the astronomical gaps — no allocation).
824    let threshold = Repr::<B>::new(
825        big.mid.significand().clone(),
826        big.mid
827            .exponent()
828            .saturating_sub(prec.saturating_add(2) as isize),
829    );
830    if repr_cmp_same_base::<B, true>(&small.mid, &threshold, None).is_le() {
831        let mut r = if sub && !a_dominant {
832            -big.clone()
833        } else {
834            big.clone()
835        };
836        // Drop the below-ulp term, replacing it with its bound: one ulp of the dominant value.
837        r.add_error(ulp_mag::<B>(&r.mid, prec));
838        Ok(r)
839    } else if sub {
840        a.sub(b, prec)
841    } else {
842        a.add(b, prec)
843    }
844}
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849    use crate::round::mode;
850    use crate::round::Round;
851    use crate::DBig;
852    use alloc::format;
853    use core::str::FromStr;
854    use dashu_base::AbsOrd;
855    use dashu_int::IBig;
856
857    // `sinh`/`cosh` go through `unwrap_fp`, so a huge-|x| overflow saturates to the directed
858    // endpoint: outward (Up) → ±∞ (cosh) / sign·∞ (sinh), inward (Zero) → the largest finite.
859    #[test]
860    fn test_sinh_cosh_directed_overflow() {
861        let p = 53;
862        let max_sig = (IBig::ONE << p) - IBig::ONE;
863        let huge = FBig::<mode::HalfEven, 2>::from_parts(IBig::ONE << 63, 0)
864            .with_precision(p)
865            .value();
866
867        let sinh_up = huge.clone().with_rounding::<mode::Up>().sinh();
868        let sinh_zero = huge.clone().with_rounding::<mode::Zero>().sinh();
869        assert!(
870            sinh_up.repr().is_infinite() && sinh_up.repr().sign() == Sign::Positive,
871            "sinh Up -> +∞"
872        );
873        assert_eq!(sinh_zero.repr().significand(), &max_sig, "sinh Zero -> largest finite");
874        assert_eq!(sinh_zero.repr().exponent(), isize::MAX);
875
876        let cosh_up = huge.clone().with_rounding::<mode::Up>().cosh();
877        let cosh_zero = huge.clone().with_rounding::<mode::Zero>().cosh();
878        assert!(
879            cosh_up.repr().is_infinite() && cosh_up.repr().sign() == Sign::Positive,
880            "cosh Up -> +∞"
881        );
882        assert_eq!(cosh_zero.repr().significand(), &max_sig, "cosh Zero -> largest finite");
883        assert_eq!(cosh_zero.repr().exponent(), isize::MAX);
884
885        // Negative huge: this is the case the closure's sign remap exists for — `exp_m1(−x)`
886        // overflows carrying a positive sign that sinh must flip to negative (and cosh must leave
887        // positive). Under `Up`, a negative overflow rounds inward (largest finite negative) while a
888        // positive overflow reaches +∞.
889        let neg_max_sig = -max_sig.clone();
890        let sinh_neg_up = (-huge.clone()).with_rounding::<mode::Up>().sinh();
891        assert_eq!(sinh_neg_up.repr().sign(), Sign::Negative, "sinh(−huge) sign");
892        assert_eq!(
893            sinh_neg_up.repr().significand(),
894            &neg_max_sig,
895            "sinh(−huge) Up -> largest finite"
896        );
897        assert_eq!(sinh_neg_up.repr().exponent(), isize::MAX);
898        let cosh_neg_up = (-huge.clone()).with_rounding::<mode::Up>().cosh();
899        assert!(
900            cosh_neg_up.repr().is_infinite() && cosh_neg_up.repr().sign() == Sign::Positive,
901            "cosh(−huge) Up -> +∞"
902        );
903
904        // sinh_cosh(−huge) = (largest finite negative, +∞) under Up — per-slot sign remap.
905        let (sh, ch) = (-huge.clone()).with_rounding::<mode::Up>().sinh_cosh();
906        assert_eq!(sh.repr().sign(), Sign::Negative, "sinh_cosh[0](−huge) sign");
907        assert_eq!(
908            sh.repr().significand(),
909            &neg_max_sig,
910            "sinh_cosh[0](−huge) Up -> largest finite"
911        );
912        assert!(
913            ch.repr().is_infinite() && ch.repr().sign() == Sign::Positive,
914            "sinh_cosh[1](−huge) -> +∞"
915        );
916    }
917
918    /// A near-tie argument with more digits than the working precision: the input's own
919    /// rounding (½ ulp of `x`, which survives the `x−1` cancellation untouched) must be part
920    /// of the certified radius. Regression for the mis-rounding this input exhibited (issue
921    /// #102): the true acosh sits 1.18e-133 above the 50-digit midpoint, and the dropped
922    /// input error (~1e-62) let Ziv certify the lower neighbour.
923    fn acosh_near_tie_arg() -> FBig<mode::HalfEven, 10> {
924        use core::str::FromStr;
925        FBig::<mode::HalfEven, 10>::from_str(
926            "1.000008608520579272799119633794615751764072075741189452445358800365413956109281413683821004335915587724859246904",
927        )
928        .unwrap()
929    }
930
931    #[test]
932    fn acosh_rounds_up_above_decimal_tie() {
933        use core::str::FromStr;
934        let x = acosh_near_tie_arg();
935        let y = Context::<mode::HalfEven>::new(50)
936            .acosh(x.repr(), None)
937            .unwrap()
938            .value();
939        assert_eq!(
940            y,
941            FBig::<mode::HalfEven, 10>::from_str(
942                "0.0041493392794990204344718343065778753953054547309876"
943            )
944            .unwrap()
945        );
946    }
947
948    /// The near-tie argument under directed modes must match a high-precision oracle re-rounded
949    /// under the same mode (the definition of correct rounding).
950    #[test]
951    fn acosh_asinh_directed_match_oracle_near_tie() {
952        use core::str::FromStr;
953        let x = acosh_near_tie_arg();
954        let asinh_arg = FBig::<mode::HalfEven, 10>::from_str(
955            "0.0041493392794990204344718343065778753953054547309875500000000000000000000000000000000000000000000000000000001179",
956        )
957        .unwrap();
958        for p in [16usize, 34, 50] {
959            let oracle_acosh = Context::<mode::HalfEven>::new(p + 60)
960                .acosh::<10>(x.repr(), None)
961                .unwrap()
962                .value();
963            let oracle_asinh = Context::<mode::HalfEven>::new(p + 60)
964                .asinh::<10>(asinh_arg.repr(), None)
965                .unwrap()
966                .value();
967            macro_rules! check {
968                ($mode:ty, $name:expr) => {{
969                    let want_acosh = Context::<$mode>::new(p)
970                        .repr_round_ref(&oracle_acosh.repr)
971                        .value();
972                    let got_acosh = Context::<$mode>::new(p)
973                        .acosh::<10>(x.repr(), None)
974                        .unwrap()
975                        .value();
976                    assert_eq!(got_acosh.repr, want_acosh, "{} acosh p={p}", $name);
977                    let want_asinh = Context::<$mode>::new(p)
978                        .repr_round_ref(&oracle_asinh.repr)
979                        .value();
980                    let got_asinh = Context::<$mode>::new(p)
981                        .asinh::<10>(asinh_arg.repr(), None)
982                        .unwrap()
983                        .value();
984                    assert_eq!(got_asinh.repr, want_asinh, "{} asinh p={p}", $name);
985                }};
986            }
987            check!(mode::Down, "Down");
988            check!(mode::Up, "Up");
989            check!(mode::Zero, "Zero");
990            check!(mode::HalfEven, "HalfEven");
991        }
992    }
993
994    /// Long-digit inputs through every hyperbolic inverse: the result must match a
995    /// high-precision oracle under each rounding mode (the input's rounding is folded into the
996    /// radius on every path — sinh/cosh/tanh through `exp_compute`, asinh/acosh/atanh through
997    /// the ball compositions).
998    #[test]
999    fn hyper_directed_match_oracle_long_input() {
1000        use core::str::FromStr;
1001        let args = [
1002            "1.000008608520579272799119633794615751764072075741189452445358800365413956109281413683821004335915587724859246904",
1003            "12.345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456",
1004            "-0.8608520579272799119633794615751764072075741189452445358800365413956109281413683821004335915587724859246904",
1005        ];
1006        for a in args {
1007            let x = FBig::<mode::HalfEven, 10>::from_str(a).unwrap();
1008            let p = 34;
1009            let ctx_h = Context::<mode::HalfEven>::new(p + 60);
1010            let oracle_asinh = ctx_h.asinh::<10>(x.repr(), None).unwrap().value().repr;
1011            let oracle_tanh = ctx_h.tanh::<10>(x.repr(), None).unwrap().value().repr;
1012            let oracle_sinh = ctx_h.sinh::<10>(x.repr(), None).unwrap().value().repr;
1013            let oracle_cosh = ctx_h.cosh::<10>(x.repr(), None).unwrap().value().repr;
1014            let oracle_atanh = if x.abs_cmp(&FBig::ONE).is_lt() {
1015                Some(ctx_h.atanh::<10>(x.repr(), None).unwrap().value().repr)
1016            } else {
1017                None
1018            };
1019            let oracle_acosh =
1020                if !x.repr().significand().is_zero() && x.repr().sign() == Sign::Positive {
1021                    Some(ctx_h.acosh::<10>(x.repr(), None).unwrap().value().repr)
1022                } else {
1023                    None
1024                };
1025            macro_rules! check {
1026                ($mode:ty, $name:expr, $f:ident, $oracle:expr) => {{
1027                    let want = Context::<$mode>::new(p).repr_round_ref(&$oracle).value();
1028                    let got = Context::<$mode>::new(p)
1029                        .$f::<10>(x.repr(), None)
1030                        .unwrap()
1031                        .value();
1032                    assert_eq!(got.repr, want, "{} {} p={p} x={a}", $name, stringify!($f));
1033                }};
1034            }
1035            check!(mode::Down, "Down", asinh, oracle_asinh);
1036            check!(mode::Up, "Up", asinh, oracle_asinh);
1037            check!(mode::Down, "Down", tanh, oracle_tanh);
1038            check!(mode::Up, "Up", tanh, oracle_tanh);
1039            check!(mode::Down, "Down", sinh, oracle_sinh);
1040            check!(mode::Up, "Up", sinh, oracle_sinh);
1041            check!(mode::Down, "Down", cosh, oracle_cosh);
1042            check!(mode::Up, "Up", cosh, oracle_cosh);
1043            if let Some(o) = &oracle_atanh {
1044                check!(mode::Down, "Down", atanh, *o);
1045                check!(mode::Up, "Up", atanh, *o);
1046            }
1047            if let Some(o) = &oracle_acosh {
1048                check!(mode::Down, "Down", acosh, *o);
1049                check!(mode::Up, "Up", acosh, *o);
1050            }
1051        }
1052    }
1053
1054    /// Re-round a high-precision `HalfEven` oracle to precision `p` under the mode under test.
1055    fn reround<R: Round, const B: Word>(hi: &FBig<mode::HalfEven, B>, p: usize) -> FBig<R, B> {
1056        let ctx = Context::<R>::new(p);
1057        FBig::new(ctx.repr_round_ref(hi.repr()).value(), ctx)
1058    }
1059
1060    /// `sinh_pi`/`cosh_pi` vs the high-precision oracle (p + 60 under `HalfEven`, re-rounded
1061    /// under the mode under test), across the four canonical significand widths. The inputs
1062    /// straddle the `|πx| = 1` series/exp boundary from both sides (0.3 → series, 0.5 → exp —
1063    /// where an early version wrongly carried the exp_m1 form's `+1` into the true-exp
1064    /// composition, returning cosh_pi(0.5) = 3.509… instead of 2.509…).
1065    #[test]
1066    fn test_sinh_cosh_pi_oracle() {
1067        let inputs = [
1068            "0.1", "0.2", "0.3", "0.5", "0.7", "1.5", "2.808", "10.1", "123.456", "-0.3", "-1.2",
1069            "-2.808",
1070        ];
1071        for &p in &[20usize, 50, 100, 500] {
1072            for input in inputs {
1073                let x = DBig::from_str(input).unwrap();
1074                let hi_ctx = Context::<mode::HalfEven>::new(p + 60);
1075                let hi_s = hi_ctx.sinh_pi::<10>(x.repr(), None).unwrap().value();
1076                let hi_c = hi_ctx.cosh_pi::<10>(x.repr(), None).unwrap().value();
1077
1078                let got = Context::<mode::HalfEven>::new(p)
1079                    .sinh_pi::<10>(x.repr(), None)
1080                    .unwrap()
1081                    .value();
1082                assert_eq!(
1083                    got,
1084                    reround::<mode::HalfEven, 10>(&hi_s, p),
1085                    "sinh_pi({input}) at p={p}"
1086                );
1087                let got = Context::<mode::Down>::new(p)
1088                    .cosh_pi::<10>(x.repr(), None)
1089                    .unwrap()
1090                    .value();
1091                assert_eq!(
1092                    got,
1093                    reround::<mode::Down, 10>(&hi_c, p),
1094                    "cosh_pi({input}) at p={p} under Down"
1095                );
1096
1097                let (s, c) = Context::<mode::HalfEven>::new(p).sinh_cosh_pi::<10>(x.repr(), None);
1098                assert_eq!(
1099                    s.unwrap().value(),
1100                    reround::<mode::HalfEven, 10>(&hi_s, p),
1101                    "sinh_cosh_pi sinh({input}) at p={p}"
1102                );
1103                assert_eq!(
1104                    c.unwrap().value(),
1105                    reround::<mode::HalfEven, 10>(&hi_c, p),
1106                    "sinh_cosh_pi cosh({input}) at p={p}"
1107                );
1108            }
1109        }
1110    }
1111
1112    /// The two exponentials of `sinh(1e14)` sit ~10¹⁵ digits apart: the ball compositions must
1113    /// drop the negligible one instead of trying to align that gap (an out-of-memory panic
1114    /// before the guarded add existed), and the plain `sinh`/`cosh` must survive the same
1115    /// argument through the core sticky-collapse in the aligned sum.
1116    // The ~10¹⁴-scale argument needs the 64-bit `isize` exponent range; on 32-bit targets the
1117    // overflow guard fires first (its range is ~4000× smaller), so the test is 64-bit-only.
1118    #[cfg(target_pointer_width = "64")]
1119    #[test]
1120    fn test_sinh_huge_argument_no_oom() {
1121        let x = DBig::from_str("189870321059487.19").unwrap();
1122        let ctx = Context::<mode::HalfEven>::new(20);
1123        let v = ctx.sinh::<10>(x.repr(), None).unwrap().value();
1124        assert_eq!(v.repr().sign(), Sign::Positive);
1125        let v = ctx.sinh_pi::<10>(x.repr(), None).unwrap().value();
1126        assert_eq!(v.repr().sign(), Sign::Positive);
1127        let v = ctx.cosh::<10>(x.repr(), None).unwrap().value();
1128        assert_eq!(v.repr().sign(), Sign::Positive);
1129        let v = ctx.cosh_pi::<10>(x.repr(), None).unwrap().value();
1130        assert_eq!(v.repr().sign(), Sign::Positive);
1131        let (s, c) = ctx.sinh_cosh_pi::<10>(x.repr(), None);
1132        assert_eq!(s.unwrap().value().repr().sign(), Sign::Positive);
1133        assert_eq!(c.unwrap().value().repr().sign(), Sign::Positive);
1134        // odd: the negative side flips sinh's sign only
1135        let neg = -x;
1136        assert_eq!(
1137            ctx.sinh_pi::<10>(neg.repr(), None)
1138                .unwrap()
1139                .value()
1140                .repr()
1141                .sign(),
1142            Sign::Negative
1143        );
1144        assert_eq!(
1145            ctx.cosh_pi::<10>(neg.repr(), None)
1146                .unwrap()
1147                .value()
1148                .repr()
1149                .sign(),
1150            Sign::Positive
1151        );
1152    }
1153
1154    // The closure's `exp_compute` runs the `Bⁿ` powering chain, whose radius slack used to cost
1155    // one systematic Ziv retry on non-power-of-two bases (`pow_chain_guard` now charges the chain
1156    // length to the initial guard). Pins a measured retrying point (DBig `sinh 10` @150 and
1157    // @600 both retried exactly once) to first-attempt certification.
1158    #[cfg(feature = "std")]
1159    #[test]
1160    fn sinh_certifies_first_attempt_base10() {
1161        let ctx = Context::<mode::HalfEven>::new(150);
1162        crate::ziv_retries_reset();
1163        let _ = ctx
1164            .sinh::<10>(&Repr::<10>::new(10.into(), 0), None)
1165            .unwrap();
1166        assert_eq!(
1167            crate::ziv_retries(),
1168            0,
1169            "DBig sinh(10) @150 should certify on the first attempt"
1170        );
1171    }
1172
1173    // A result in `10^(0.301·isize::MAX)..10^isize::MAX` is representable as a `Repr` but lies
1174    // past the old `Mag` ceiling (`2^isize::MAX ≈ 10^(0.301·isize::MAX)`): the exported radius
1175    // saturated to `+∞`, which no Ziv attempt can certify, and the loop spun through its
1176    // exponential guard growth for hours before erroring. The `Mag` exponent field is now
1177    // `i128` — wide enough to bound any representable value — so the ×π hyperbolic family
1178    // certifies here in one retry. The input magnitude scales with the target's pointer width
1179    // (the 64-bit magnitude is a genuine overflow on a 32-bit `isize`).
1180    #[cfg(feature = "std")]
1181    #[test]
1182    fn sinh_pi_huge_magnitude_certifies() {
1183        let ctx = Context::<mode::HalfEven>::new(20);
1184        // aim `x` at `0.4·isize::MAX`: the result's decimal exponent is `π·x/ln10 ≈ 0.546·MAX`,
1185        // comfortably past the old ceiling (`0.301·MAX`) and short of the `Repr` range (`MAX`),
1186        // on every pointer width (the 64-bit magnitude would be a genuine overflow on a
1187        // 32-bit `isize`)
1188        let imax = isize::MAX as f64;
1189        let e = (0.4 * imax).log10().floor() as isize;
1190        let sig = (0.4 * imax / 10f64.powi(e as i32)).round() as i64;
1191        let x = Repr::<10>::new(IBig::from(sig), e);
1192        let past_old_ceiling = |r: &Repr<10>| r.exponent() > isize::MAX / 2;
1193
1194        crate::ziv_retries_reset();
1195        let sh = ctx.sinh_pi::<10>(&x, None).unwrap().value();
1196        assert!(crate::ziv_retries() <= 1, "sinh_pi @20 should certify within one retry");
1197        assert!(!sh.repr().is_infinite() && past_old_ceiling(sh.repr()));
1198
1199        crate::ziv_retries_reset();
1200        let ch = ctx.cosh_pi::<10>(&x, None).unwrap().value();
1201        assert!(crate::ziv_retries() <= 1);
1202        assert!(!ch.repr().is_infinite() && past_old_ceiling(ch.repr()));
1203
1204        // the pair shares one evaluation and certifies both parts together
1205        crate::ziv_retries_reset();
1206        let (sh2, ch2) = ctx.sinh_cosh_pi::<10>(&x, None);
1207        assert!(crate::ziv_retries() <= 1);
1208        assert_eq!(sh2.unwrap().value(), sh);
1209        assert_eq!(ch2.unwrap().value(), ch);
1210    }
1211
1212    /// `acosh`/`atanh` compare their `±1` boundaries *exactly*: an argument that merely rounds
1213    /// onto the endpoint is not the endpoint, and both have a vertical tangent there — the true
1214    /// value is `√(2δ)` (acosh) / `ln(2/δ)/2` (atanh), not the endpoint's `0` / `∞`.
1215    ///
1216    /// The rows that *used* to be at risk here are covered: `acosh` takes `√((x−1)(x+1))` and
1217    /// `atanh` divides `2x/(1−x)` by an argument that rounds onto the pole, so both reach the
1218    /// retry path rather than certifying the endpoint — `acosh` via the loop's zero-candidate
1219    /// guard, `atanh` via `ln_1p_ball`'s pole guard (which previously panicked on the `±∞`
1220    /// ratio).
1221    #[test]
1222    fn test_hyperbolic_endpoints_compare_exactly() {
1223        for &p in &[20usize, 50, 100, 500] {
1224            // 10^-n is beyond the initial guard, so the argument rounds onto the endpoint at the
1225            // first work precision for p ≥ 100 (the case that used to snap); at p = 20/50 the rows
1226            // are plain correctness checks.
1227            let n = 3 * p / 2 + 5;
1228            let below = DBig::from_str(&format!("0.{}", "9".repeat(n))).unwrap(); // 1 − 10^-n
1229            let above = DBig::from_str(&format!("1.{}1", "0".repeat(n - 1))).unwrap(); // 1 + 10^-n
1230            let ctx = Context::<mode::HalfEven>::new(p);
1231            let hi = Context::<mode::HalfEven>::new(p + 60);
1232
1233            let a = ctx.atanh::<10>(below.repr(), None).unwrap().value();
1234            assert!(a.repr().is_finite(), "atanh(1 - 10^-{n}) at p={p} is finite, not -inf");
1235            assert_eq!(
1236                a,
1237                hi.atanh::<10>(below.repr(), None)
1238                    .unwrap()
1239                    .value()
1240                    .with_precision(p)
1241                    .value(),
1242                "atanh(1 - 10^-{n}) at p={p}"
1243            );
1244
1245            let c = ctx.acosh::<10>(above.repr(), None).unwrap().value();
1246            assert!(
1247                !c.repr().significand().is_zero(),
1248                "acosh(1 + 10^-{n}) at p={p} is not an exact 0"
1249            );
1250            assert_eq!(
1251                c,
1252                hi.acosh::<10>(above.repr(), None)
1253                    .unwrap()
1254                    .value()
1255                    .with_precision(p)
1256                    .value(),
1257                "acosh(1 + 10^-{n}) at p={p}"
1258            );
1259
1260            // the exact endpoints keep their exact results
1261            let one = Repr::<10>::one();
1262            assert!(ctx
1263                .atanh::<10>(&one, None)
1264                .unwrap()
1265                .value()
1266                .repr()
1267                .is_infinite());
1268            assert!(ctx
1269                .atanh::<10>(&(-Repr::<10>::one()), None)
1270                .unwrap()
1271                .value()
1272                .repr()
1273                .is_infinite());
1274            assert!(ctx
1275                .acosh::<10>(&one, None)
1276                .unwrap()
1277                .value()
1278                .repr()
1279                .significand()
1280                .is_zero());
1281        }
1282    }
1283}