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, EstimatedLog2, 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        // For sufficiently large |x|, tanh(x) = sign(x)·(1 − δ) with 0 < δ = 2/(e^{2|x|}+1) below
392        // half an ulp of ±1, so its rounding is fully determined (Up/Zero just above −1 and
393        // Down/Zero just below +1 step one ulp toward zero; the other modes stay at ±1) —
394        // short-circuit to the mode-aware endpoint, mirroring `exp_m1`'s extreme-negative case.
395        // The Ziv loop cannot certify that result: the working-precision mid collapses onto
396        // exactly ±1, which sits on the boundary of a directed rounding preimage (one-sided), so
397        // the containment test never resolves and the loop climbs to a working precision of
398        // ~2|x|/ln 2 — millions of digits for a plain input like tanh(10⁵) — before the residual
399        // becomes representable. The cutoff is δ < half-ulp(±1): ±1 sits on a power-of-B
400        // boundary, so the spacing just inside it is B^-p, i.e. 2·e^{−2|x|} < B^{−p}/2 ⟺
401        // |x| > (p·ln B + ln 4)/2. Compare the lower bound of log2|x| against an upper bound of
402        // log2(threshold) so a borderline input still falls through to Ziv (which converges there
403        // on the first attempt — its initial working precision already exceeds 2|x|/ln 2 < p + 2).
404        let thresh = self.precision as f32 * B.log2_est() * core::f32::consts::LN_2 / 2.0
405            + core::f32::consts::LN_2;
406        if x.log2_bounds().0 > thresh.log2_bounds().1 {
407            return Ok(self.near_one_endpoint::<B>(x.sign()));
408        }
409
410        // tanh(x) = exp_m1(2x) / (exp_m1(2x) + 2). `exp_m1(2x)` comes from the Ball-based
411        // `exp_compute` on the *exact* `2x = x + x` (so `exp_compute` folds the input's own
412        // rounding into the radius); the division's rounding is tracked mechanically. Inputs
413        // saturating at ±1 were short-circuited above, so `exp_compute`'s reduction quotient can
414        // no longer overflow `isize` on this path either.
415        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
416        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
417        // the growing work precision, as before). Sized once, outside the loop.
418        let initial_guard = self.base_guard_digits::<B>()
419            + 10
420            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
421        self.ziv(initial_guard, |guard| {
422            let work = Context::<mode::HalfEven>::new(self.precision + guard);
423            let n = 1usize << (work.precision.bit_len() / 2);
424            let two_x = x + x;
425            match work.exp_compute::<B>(&two_x, work.precision, true, n, reborrow_cache(&mut cache))
426            {
427                Ok(e) => {
428                    let wp = work.precision;
429                    let two = Ball::exact_int(IBig::from(2), wp);
430                    Ok(e.div(&e.add(&two, wp)?, wp)?
431                        .to_value_radius::<R>(&Context::<R>::new(wp)))
432                }
433                Err(other) => unreachable!("exp_m1 on finite input: {other:?}"),
434            }
435        })
436    }
437
438    /// Inverse hyperbolic sine.
439    pub fn asinh<const B: Word>(
440        &self,
441        x: &Repr<B>,
442        mut cache: Option<&mut ConstCache>,
443    ) -> FpResult<FBig<R, B>> {
444        if x.is_infinite() {
445            return Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self)));
446        }
447        assert_limited_precision(self.precision);
448        if x.significand.is_zero() {
449            // asinh(±0) = ±0
450            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
451        }
452
453        // asinh(x) = sign(x) · ln_1p(|x| + x²/(sqrt(x²+1)+1)) — the x²/(sqrt+1) form avoids the
454        // `sqrt(x²+1) − 1` cancellation near 0. The composition is tracked as a [`Ball`] from the
455        // rounded input up: the input's own rounding, the sqr, sqrt, division and the `ln_1p`
456        // input error all propagate mechanically (a hand-picked per-op ulp count cannot see the
457        // operand errors inherited through the chain). The `|x|` so large that `x²` overflows arm
458        // falls back to the asymptotic `sign·ln(2|x|)` on the exact `2|x|` (whose input rounding
459        // `ln_compute` folds itself).
460        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
461        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
462        // the growing work precision, as before). Sized once, outside the loop.
463        let initial_guard = self.base_guard_digits::<B>()
464            + 10
465            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
466        self.ziv(initial_guard, |guard| {
467            let work = Context::<mode::HalfEven>::new(self.precision + guard);
468            let sign = x.sign();
469            let wp = work.precision;
470            let x_ball = Ball::from_rounded(work.repr_round_ref(x), wp);
471            let abs_x_ball = if sign == Sign::Negative {
472                -x_ball
473            } else {
474                x_ball
475            };
476            let one = Ball::exact_int(IBig::ONE, wp);
477            let res = match abs_x_ball.sqr(wp) {
478                Ok(x_sq_ball) => {
479                    let sqrt_plus_one = x_sq_ball.add(&one, wp)?.sqrt(wp)?.add(&one, wp)?;
480                    let arg = abs_x_ball.add(&x_sq_ball.div(&sqrt_plus_one, wp)?, wp)?;
481                    work.ln_1p_ball::<B>(&arg, reborrow_cache(&mut cache))
482                }
483                // |x| so large that x² overflows: asinh(x) ≈ sign·ln(2|x|).
484                Err(FpError::Overflow(_)) => {
485                    let two_abs = Repr::new((x.significand() * IBig::from(2)).abs(), x.exponent());
486                    work.ln_compute::<B>(
487                        &two_abs,
488                        work.precision,
489                        false,
490                        reborrow_cache(&mut cache),
491                    )
492                }
493                Err(other) => unreachable!("sqr: {other:?}"),
494            }?;
495            let result = if sign == Sign::Negative { -res } else { res };
496            Ok(result.to_value_radius::<R>(&Context::<R>::new(wp)))
497        })
498    }
499
500    /// Inverse hyperbolic cosine. Domain: `x ≥ 1`.
501    pub fn acosh<const B: Word>(
502        &self,
503        x: &Repr<B>,
504        mut cache: Option<&mut ConstCache>,
505    ) -> FpResult<FBig<R, B>> {
506        if x.is_infinite() {
507            if x.sign() == Sign::Negative {
508                return Err(FpError::OutOfDomain);
509            }
510            return Ok(Exact(FBig::new(Repr::infinity(), *self)));
511        }
512        assert_limited_precision(self.precision);
513        // Domain x ≥ 1, compared *exactly* (no precision argument) because `1` is a vertical
514        // tangent of `acosh`: `1 + 10⁻¹⁵⁵` at 100 digits rounds onto `1`, but its true value is
515        // `√2·10⁻⁷⁸`, not `0`.
516        if x.sign() == Sign::Negative {
517            return Err(FpError::OutOfDomain);
518        }
519        match repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None) {
520            core::cmp::Ordering::Less => return Err(FpError::OutOfDomain),
521            core::cmp::Ordering::Equal => return Ok(Exact(FBig::new(Repr::zero(), *self))),
522            core::cmp::Ordering::Greater => {}
523        }
524
525        // acosh(x) = ln_1p((x-1) + sqrt((x-1)(x+1))) — the (x-1)(x+1) form avoids the `x²−1`
526        // cancellation near x = 1. The composition is tracked as a [`Ball`] from the rounded input
527        // up: the input's own rounding flows into `x−1`/`x+1` and through the product, sqrt,
528        // addition and the `ln_1p` input error mechanically. (The input rounding is what a
529        // per-op ulp count on the *result* of `x−1` misses: near x = 1 the subtraction cancels
530        // the value — and its ulp — down by |x−1|, while the inherited operand error stays at
531        // ulp(x); an under-estimated radius then lets Ziv certify the wrong neighbour of a tie.)
532        // The `(x-1)(x+1)` overflow arm falls back to the asymptotic `ln(2x)` on the exact `2x`
533        // (whose input rounding `ln_compute` folds itself).
534        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
535        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
536        // the growing work precision, as before). Sized once, outside the loop.
537        let initial_guard = self.base_guard_digits::<B>()
538            + 10
539            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
540        self.ziv(initial_guard, |guard| {
541            let work = Context::<mode::HalfEven>::new(self.precision + guard);
542            let wp = work.precision;
543            let x_ball = Ball::from_rounded(work.repr_round_ref(x), wp);
544            let one = Ball::exact_int(IBig::ONE, wp);
545            let xm1_ball = x_ball.sub(&one, wp)?;
546            let xp1_ball = x_ball.add(&one, wp)?;
547            let res = match xm1_ball.mul(&xp1_ball, wp) {
548                Ok(prod_ball) => {
549                    let arg = xm1_ball.add(&prod_ball.sqrt(wp)?, wp)?;
550                    work.ln_1p_ball::<B>(&arg, reborrow_cache(&mut cache))
551                }
552                // (x-1)(x+1) overflowed: acosh(x) ≈ ln(2x).
553                Err(FpError::Overflow(_)) => {
554                    let two_x = Repr::new(x.significand() * IBig::from(2), x.exponent());
555                    work.ln_compute::<B>(&two_x, work.precision, false, reborrow_cache(&mut cache))
556                }
557                Err(other) => unreachable!("mul: {other:?}"),
558            }?;
559            Ok(res.to_value_radius::<R>(&Context::<R>::new(wp)))
560        })
561    }
562
563    /// Inverse hyperbolic tangent. Domain: `-1 < x < 1` (`x = ±1` → ±∞, `|x| > 1` is an error).
564    pub fn atanh<const B: Word>(
565        &self,
566        x: &Repr<B>,
567        mut cache: Option<&mut ConstCache>,
568    ) -> FpResult<FBig<R, B>> {
569        if x.is_infinite() {
570            return Err(FpError::OutOfDomain);
571        }
572        assert_limited_precision(self.precision);
573        if x.significand.is_zero() {
574            // atanh(±0) = ±0
575            return Ok(Exact(FBig::new(signed_zero_repr(x), *self)));
576        }
577        // Domain |x| < 1: |x| = 1 → ±∞ (value), |x| > 1 → error. The boundary is compared
578        // *exactly* (no precision argument) because `±1` is a pole of `atanh`: `1 − 10⁻¹⁵⁵`
579        // at 100 digits rounds onto `1` but is the finite `(p+155)·ln(10)/2`, not `∞`.
580        match repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None) {
581            core::cmp::Ordering::Greater => return Err(FpError::OutOfDomain),
582            core::cmp::Ordering::Equal => {
583                return Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self)));
584            }
585            _ => {}
586        }
587
588        // atanh(x) = ln_1p(2x/(1-x)) / 2. The ratio and the `ln_1p` input error are tracked as a
589        // [`Ball`]; near |x| = 1 the `2x/(1-x)` division amplifies, but the Ball tracks it (Ziv
590        // retries there).
591        // `+ pow_chain_guard`: the closure's `exp_compute` runs the `Bⁿ` powering chain, whose
592        // radius slack is charged by the target precision (the in-closure `n` keeps deriving from
593        // the growing work precision, as before). Sized once, outside the loop.
594        let initial_guard = self.base_guard_digits::<B>()
595            + 10
596            + pow_chain_guard::<B>(1usize << (self.precision.bit_len() / 2));
597        self.ziv(initial_guard, |guard| {
598            let work = Context::<mode::HalfEven>::new(self.precision + guard);
599            let wp = work.precision;
600            let x_ball = Ball::from_rounded(work.repr_round_ref(x), wp);
601            let one = Ball::exact_int(IBig::ONE, wp);
602            let ratio = x_ball
603                .scale_int(&IBig::from(2), wp)?
604                .div(&one.sub(&x_ball, wp)?, wp)?;
605            let res = work.ln_1p_ball::<B>(&ratio, reborrow_cache(&mut cache))?;
606            Ok(res
607                .div_int(2, wp)?
608                .to_value_radius::<R>(&Context::<R>::new(wp)))
609        })
610    }
611}
612
613impl<R: ErrorBounds, const B: Word> FBig<R, B> {
614    /// Calculate the hyperbolic sine of the floating point number.
615    ///
616    /// # Examples
617    ///
618    /// ```
619    /// # use core::str::FromStr;
620    /// # use dashu_base::ParseError;
621    /// # use dashu_float::DBig;
622    /// let a = DBig::from_str("0.5000000")?;
623    /// assert_eq!(a.sinh(), DBig::from_str("0.52109531")?);
624    /// # Ok::<(), ParseError>(())
625    /// ```
626    #[inline]
627    pub fn sinh(&self) -> Self {
628        self.context.unwrap_fp(self.context.sinh(&self.repr, None))
629    }
630
631    /// Calculate the hyperbolic cosine of the floating point number.
632    ///
633    /// # Examples
634    ///
635    /// ```
636    /// # use core::str::FromStr;
637    /// # use dashu_base::ParseError;
638    /// # use dashu_float::DBig;
639    /// let a = DBig::from_str("0.5000000")?;
640    /// assert_eq!(a.cosh(), DBig::from_str("1.127626")?);
641    /// # Ok::<(), ParseError>(())
642    /// ```
643    #[inline]
644    pub fn cosh(&self) -> Self {
645        self.context.unwrap_fp(self.context.cosh(&self.repr, None))
646    }
647
648    /// Simultaneously calculate the hyperbolic sine and cosine of the number.
649    ///
650    /// This is more efficient than calling [`sinh`](FBig::sinh) and [`cosh`](FBig::cosh)
651    /// separately, since the two share the `exp_m1(±x)` sub-computations.
652    ///
653    /// # Examples
654    ///
655    /// ```
656    /// # use core::str::FromStr;
657    /// # use dashu_base::ParseError;
658    /// # use dashu_float::DBig;
659    /// let a = DBig::from_str("0.5000000")?;
660    /// let (s, c) = a.sinh_cosh();
661    /// assert_eq!(s, DBig::from_str("0.52109531")?);
662    /// assert_eq!(c, DBig::from_str("1.127626")?);
663    /// # Ok::<(), ParseError>(())
664    /// ```
665    #[inline]
666    pub fn sinh_cosh(&self) -> (Self, Self) {
667        let (s, c) = self.context.sinh_cosh(&self.repr, None);
668        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
669    }
670
671    /// Calculate the hyperbolic sine of the floating point number multiplied by π, i.e.
672    /// `sinh(self·π)`.
673    ///
674    /// Unlike [`sinh`](FBig::sinh), the ×π variant's argument ball is built from the shared π
675    /// constant, so repeated calls at increasing precision reuse the cached Chudnovsky state.
676    ///
677    /// # Examples
678    /// ```
679    /// # use core::str::FromStr;
680    /// # use dashu_base::ParseError;
681    /// # use dashu_float::DBig;
682    /// // sinh(π/2) ≈ 2.30129890
683    /// let a = DBig::from_str("0.50000000")?;
684    /// assert_eq!(a.sinh_pi(), DBig::from_str("2.30129890")?);
685    /// # Ok::<(), ParseError>(())
686    /// ```
687    #[inline]
688    pub fn sinh_pi(&self) -> Self {
689        self.context
690            .unwrap_fp(self.context.sinh_pi(&self.repr, None))
691    }
692
693    /// Calculate the hyperbolic cosine of the floating point number multiplied by π, i.e.
694    /// `cosh(self·π)`.
695    ///
696    /// # Examples
697    /// ```
698    /// # use core::str::FromStr;
699    /// # use dashu_base::ParseError;
700    /// # use dashu_float::DBig;
701    /// // cosh(π/2) ≈ 2.50917848
702    /// let a = DBig::from_str("0.50000000")?;
703    /// assert_eq!(a.cosh_pi(), DBig::from_str("2.50917848")?);
704    /// # Ok::<(), ParseError>(())
705    /// ```
706    #[inline]
707    pub fn cosh_pi(&self) -> Self {
708        self.context
709            .unwrap_fp(self.context.cosh_pi(&self.repr, None))
710    }
711
712    /// Simultaneously calculate the hyperbolic sine and cosine of the number multiplied by π.
713    ///
714    /// This is more efficient than calling [`sinh_pi`](FBig::sinh_pi) and
715    /// [`cosh_pi`](FBig::cosh_pi) separately, since the two share the argument ball and the
716    /// exponential composition.
717    ///
718    /// # Examples
719    /// ```
720    /// # use core::str::FromStr;
721    /// # use dashu_base::ParseError;
722    /// # use dashu_float::DBig;
723    /// let a = DBig::from_str("0.50000000")?;
724    /// let (s, c) = a.sinh_cosh_pi();
725    /// assert_eq!(s, DBig::from_str("2.30129890")?);
726    /// assert_eq!(c, DBig::from_str("2.50917848")?);
727    /// # Ok::<(), ParseError>(())
728    /// ```
729    #[inline]
730    pub fn sinh_cosh_pi(&self) -> (Self, Self) {
731        let (s, c) = self.context.sinh_cosh_pi(&self.repr, None);
732        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
733    }
734
735    /// Calculate the hyperbolic tangent of the floating point number.
736    ///
737    /// # Examples
738    ///
739    /// ```
740    /// # use core::str::FromStr;
741    /// # use dashu_base::ParseError;
742    /// # use dashu_float::DBig;
743    /// let a = DBig::from_str("0.5000000")?;
744    /// assert_eq!(a.tanh(), DBig::from_str("0.46211716")?);
745    /// # Ok::<(), ParseError>(())
746    /// ```
747    #[inline]
748    pub fn tanh(&self) -> Self {
749        self.context.unwrap_fp(self.context.tanh(&self.repr, None))
750    }
751
752    /// Calculate the inverse hyperbolic sine of the floating point number.
753    ///
754    /// # Examples
755    ///
756    /// ```
757    /// # use core::str::FromStr;
758    /// # use dashu_base::ParseError;
759    /// # use dashu_float::DBig;
760    /// let a = DBig::from_str("0.5000000")?;
761    /// assert_eq!(a.asinh(), DBig::from_str("0.48121183")?);
762    /// # Ok::<(), ParseError>(())
763    /// ```
764    #[inline]
765    pub fn asinh(&self) -> Self {
766        self.context.unwrap_fp(self.context.asinh(&self.repr, None))
767    }
768
769    /// Calculate the inverse hyperbolic cosine of the floating point number.
770    ///
771    /// # Panics
772    ///
773    /// Panics if the number is less than 1 (out of domain).
774    ///
775    /// # Examples
776    ///
777    /// ```
778    /// # use core::str::FromStr;
779    /// # use dashu_base::ParseError;
780    /// # use dashu_float::DBig;
781    /// let a = DBig::from_str("2.000000")?;
782    /// assert_eq!(a.acosh(), DBig::from_str("1.316958")?);
783    /// # Ok::<(), ParseError>(())
784    /// ```
785    #[inline]
786    pub fn acosh(&self) -> Self {
787        self.context.unwrap_fp(self.context.acosh(&self.repr, None))
788    }
789
790    /// Calculate the inverse hyperbolic tangent of the floating point number.
791    ///
792    /// # Panics
793    ///
794    /// Panics if the absolute value is greater than or equal to 1 (out of domain;
795    /// `|x| = 1` is infinite and `|x| > 1` is not real).
796    ///
797    /// # Examples
798    ///
799    /// ```
800    /// # use core::str::FromStr;
801    /// # use dashu_base::ParseError;
802    /// # use dashu_float::DBig;
803    /// let a = DBig::from_str("0.5000000")?;
804    /// assert_eq!(a.atanh(), DBig::from_str("0.54930614")?);
805    /// # Ok::<(), ParseError>(())
806    /// ```
807    #[inline]
808    pub fn atanh(&self) -> Self {
809        self.context.unwrap_fp(self.context.atanh(&self.repr, None))
810    }
811}
812
813/// `±0` `Repr` carrying the sign of `x` (used by the odd hyperbolics at zero input).
814fn signed_zero_repr<const B: Word>(x: &Repr<B>) -> Repr<B> {
815    if x.is_neg_zero() {
816        Repr::neg_zero()
817    } else {
818        Repr::zero()
819    }
820}
821
822/// `a ± b` with a magnitude guard: when one operand sits more than `prec + 2` digits below the
823/// other, it is dropped and the radius inflated by one ulp instead — a sound substitute (the
824/// dropped term is < 1 ulp of the dominant one) for the plain ball add/sub, whose midpoint
825/// alignment would try to materialize the astronomically large digit gap between the two
826/// exponentials of `(exp(t) ± exp(−t))/2` (an out-of-memory panic for large |t|, e.g.
827/// `sinh(1e14)`).
828///
829/// The guard is a caller-side shortcut, not the only defence — `repr_round_sum` also collapses
830/// an astronomically low sticky — but it avoids the alignment entirely, which is the expensive
831/// half of the operation.
832fn addsub_guarded<const B: Word>(
833    a: &Ball<B>,
834    b: &Ball<B>,
835    sub: bool,
836    prec: usize,
837) -> Result<Ball<B>, FpError> {
838    let a_dominant = repr_cmp_same_base::<B, true>(&a.mid, &b.mid, None).is_ge();
839    let (big, small) = if a_dominant { (a, b) } else { (b, a) };
840    // `small ≤ big·B^−(prec+2)`, checked against an exponent-only shifted threshold (the log2
841    // comparison inside `abs_cmp` short-circuits the astronomical gaps — no allocation).
842    let threshold = Repr::<B>::new(
843        big.mid.significand().clone(),
844        big.mid
845            .exponent()
846            .saturating_sub(prec.saturating_add(2) as isize),
847    );
848    if repr_cmp_same_base::<B, true>(&small.mid, &threshold, None).is_le() {
849        let mut r = if sub && !a_dominant {
850            -big.clone()
851        } else {
852            big.clone()
853        };
854        // Drop the below-ulp term, replacing it with its bound: one ulp of the dominant value.
855        r.add_error(ulp_mag::<B>(&r.mid, prec));
856        Ok(r)
857    } else if sub {
858        a.sub(b, prec)
859    } else {
860        a.add(b, prec)
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867    use crate::round::mode;
868    use crate::round::Round;
869    use crate::DBig;
870    use alloc::format;
871    use core::str::FromStr;
872    use dashu_base::AbsOrd;
873    use dashu_int::IBig;
874
875    // `sinh`/`cosh` go through `unwrap_fp`, so a huge-|x| overflow saturates to the directed
876    // endpoint: outward (Up) → ±∞ (cosh) / sign·∞ (sinh), inward (Zero) → the largest finite.
877    #[test]
878    fn test_sinh_cosh_directed_overflow() {
879        let p = 53;
880        let max_sig = (IBig::ONE << p) - IBig::ONE;
881        let huge = FBig::<mode::HalfEven, 2>::from_parts(IBig::ONE << 63, 0)
882            .with_precision(p)
883            .value();
884
885        let sinh_up = huge.clone().with_rounding::<mode::Up>().sinh();
886        let sinh_zero = huge.clone().with_rounding::<mode::Zero>().sinh();
887        assert!(
888            sinh_up.repr().is_infinite() && sinh_up.repr().sign() == Sign::Positive,
889            "sinh Up -> +∞"
890        );
891        assert_eq!(sinh_zero.repr().significand(), &max_sig, "sinh Zero -> largest finite");
892        assert_eq!(sinh_zero.repr().exponent(), isize::MAX);
893
894        let cosh_up = huge.clone().with_rounding::<mode::Up>().cosh();
895        let cosh_zero = huge.clone().with_rounding::<mode::Zero>().cosh();
896        assert!(
897            cosh_up.repr().is_infinite() && cosh_up.repr().sign() == Sign::Positive,
898            "cosh Up -> +∞"
899        );
900        assert_eq!(cosh_zero.repr().significand(), &max_sig, "cosh Zero -> largest finite");
901        assert_eq!(cosh_zero.repr().exponent(), isize::MAX);
902
903        // Negative huge: this is the case the closure's sign remap exists for — `exp_m1(−x)`
904        // overflows carrying a positive sign that sinh must flip to negative (and cosh must leave
905        // positive). Under `Up`, a negative overflow rounds inward (largest finite negative) while a
906        // positive overflow reaches +∞.
907        let neg_max_sig = -max_sig.clone();
908        let sinh_neg_up = (-huge.clone()).with_rounding::<mode::Up>().sinh();
909        assert_eq!(sinh_neg_up.repr().sign(), Sign::Negative, "sinh(−huge) sign");
910        assert_eq!(
911            sinh_neg_up.repr().significand(),
912            &neg_max_sig,
913            "sinh(−huge) Up -> largest finite"
914        );
915        assert_eq!(sinh_neg_up.repr().exponent(), isize::MAX);
916        let cosh_neg_up = (-huge.clone()).with_rounding::<mode::Up>().cosh();
917        assert!(
918            cosh_neg_up.repr().is_infinite() && cosh_neg_up.repr().sign() == Sign::Positive,
919            "cosh(−huge) Up -> +∞"
920        );
921
922        // sinh_cosh(−huge) = (largest finite negative, +∞) under Up — per-slot sign remap.
923        let (sh, ch) = (-huge.clone()).with_rounding::<mode::Up>().sinh_cosh();
924        assert_eq!(sh.repr().sign(), Sign::Negative, "sinh_cosh[0](−huge) sign");
925        assert_eq!(
926            sh.repr().significand(),
927            &neg_max_sig,
928            "sinh_cosh[0](−huge) Up -> largest finite"
929        );
930        assert!(
931            ch.repr().is_infinite() && ch.repr().sign() == Sign::Positive,
932            "sinh_cosh[1](−huge) -> +∞"
933        );
934    }
935
936    /// A near-tie argument with more digits than the working precision: the input's own
937    /// rounding (½ ulp of `x`, which survives the `x−1` cancellation untouched) must be part
938    /// of the certified radius. Regression for the mis-rounding this input exhibited (issue
939    /// #102): the true acosh sits 1.18e-133 above the 50-digit midpoint, and the dropped
940    /// input error (~1e-62) let Ziv certify the lower neighbour.
941    fn acosh_near_tie_arg() -> FBig<mode::HalfEven, 10> {
942        use core::str::FromStr;
943        FBig::<mode::HalfEven, 10>::from_str(
944            "1.000008608520579272799119633794615751764072075741189452445358800365413956109281413683821004335915587724859246904",
945        )
946        .unwrap()
947    }
948
949    #[test]
950    fn acosh_rounds_up_above_decimal_tie() {
951        use core::str::FromStr;
952        let x = acosh_near_tie_arg();
953        let y = Context::<mode::HalfEven>::new(50)
954            .acosh(x.repr(), None)
955            .unwrap()
956            .value();
957        assert_eq!(
958            y,
959            FBig::<mode::HalfEven, 10>::from_str(
960                "0.0041493392794990204344718343065778753953054547309876"
961            )
962            .unwrap()
963        );
964    }
965
966    /// The near-tie argument under directed modes must match a high-precision oracle re-rounded
967    /// under the same mode (the definition of correct rounding).
968    #[test]
969    fn acosh_asinh_directed_match_oracle_near_tie() {
970        use core::str::FromStr;
971        let x = acosh_near_tie_arg();
972        let asinh_arg = FBig::<mode::HalfEven, 10>::from_str(
973            "0.0041493392794990204344718343065778753953054547309875500000000000000000000000000000000000000000000000000000001179",
974        )
975        .unwrap();
976        for p in [16usize, 34, 50] {
977            let oracle_acosh = Context::<mode::HalfEven>::new(p + 60)
978                .acosh::<10>(x.repr(), None)
979                .unwrap()
980                .value();
981            let oracle_asinh = Context::<mode::HalfEven>::new(p + 60)
982                .asinh::<10>(asinh_arg.repr(), None)
983                .unwrap()
984                .value();
985            macro_rules! check {
986                ($mode:ty, $name:expr) => {{
987                    let want_acosh = Context::<$mode>::new(p)
988                        .repr_round_ref(&oracle_acosh.repr)
989                        .value();
990                    let got_acosh = Context::<$mode>::new(p)
991                        .acosh::<10>(x.repr(), None)
992                        .unwrap()
993                        .value();
994                    assert_eq!(got_acosh.repr, want_acosh, "{} acosh p={p}", $name);
995                    let want_asinh = Context::<$mode>::new(p)
996                        .repr_round_ref(&oracle_asinh.repr)
997                        .value();
998                    let got_asinh = Context::<$mode>::new(p)
999                        .asinh::<10>(asinh_arg.repr(), None)
1000                        .unwrap()
1001                        .value();
1002                    assert_eq!(got_asinh.repr, want_asinh, "{} asinh p={p}", $name);
1003                }};
1004            }
1005            check!(mode::Down, "Down");
1006            check!(mode::Up, "Up");
1007            check!(mode::Zero, "Zero");
1008            check!(mode::HalfEven, "HalfEven");
1009        }
1010    }
1011
1012    /// Long-digit inputs through every hyperbolic inverse: the result must match a
1013    /// high-precision oracle under each rounding mode (the input's rounding is folded into the
1014    /// radius on every path — sinh/cosh/tanh through `exp_compute`, asinh/acosh/atanh through
1015    /// the ball compositions).
1016    #[test]
1017    fn hyper_directed_match_oracle_long_input() {
1018        use core::str::FromStr;
1019        let args = [
1020            "1.000008608520579272799119633794615751764072075741189452445358800365413956109281413683821004335915587724859246904",
1021            "12.345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456",
1022            "-0.8608520579272799119633794615751764072075741189452445358800365413956109281413683821004335915587724859246904",
1023        ];
1024        for a in args {
1025            let x = FBig::<mode::HalfEven, 10>::from_str(a).unwrap();
1026            let p = 34;
1027            let ctx_h = Context::<mode::HalfEven>::new(p + 60);
1028            let oracle_asinh = ctx_h.asinh::<10>(x.repr(), None).unwrap().value().repr;
1029            let oracle_tanh = ctx_h.tanh::<10>(x.repr(), None).unwrap().value().repr;
1030            let oracle_sinh = ctx_h.sinh::<10>(x.repr(), None).unwrap().value().repr;
1031            let oracle_cosh = ctx_h.cosh::<10>(x.repr(), None).unwrap().value().repr;
1032            let oracle_atanh = if x.abs_cmp(&FBig::ONE).is_lt() {
1033                Some(ctx_h.atanh::<10>(x.repr(), None).unwrap().value().repr)
1034            } else {
1035                None
1036            };
1037            let oracle_acosh =
1038                if !x.repr().significand().is_zero() && x.repr().sign() == Sign::Positive {
1039                    Some(ctx_h.acosh::<10>(x.repr(), None).unwrap().value().repr)
1040                } else {
1041                    None
1042                };
1043            macro_rules! check {
1044                ($mode:ty, $name:expr, $f:ident, $oracle:expr) => {{
1045                    let want = Context::<$mode>::new(p).repr_round_ref(&$oracle).value();
1046                    let got = Context::<$mode>::new(p)
1047                        .$f::<10>(x.repr(), None)
1048                        .unwrap()
1049                        .value();
1050                    assert_eq!(got.repr, want, "{} {} p={p} x={a}", $name, stringify!($f));
1051                }};
1052            }
1053            check!(mode::Down, "Down", asinh, oracle_asinh);
1054            check!(mode::Up, "Up", asinh, oracle_asinh);
1055            check!(mode::Down, "Down", tanh, oracle_tanh);
1056            check!(mode::Up, "Up", tanh, oracle_tanh);
1057            check!(mode::Down, "Down", sinh, oracle_sinh);
1058            check!(mode::Up, "Up", sinh, oracle_sinh);
1059            check!(mode::Down, "Down", cosh, oracle_cosh);
1060            check!(mode::Up, "Up", cosh, oracle_cosh);
1061            if let Some(o) = &oracle_atanh {
1062                check!(mode::Down, "Down", atanh, *o);
1063                check!(mode::Up, "Up", atanh, *o);
1064            }
1065            if let Some(o) = &oracle_acosh {
1066                check!(mode::Down, "Down", acosh, *o);
1067                check!(mode::Up, "Up", acosh, *o);
1068            }
1069        }
1070    }
1071
1072    /// Re-round a high-precision `HalfEven` oracle to precision `p` under the mode under test.
1073    fn reround<R: Round, const B: Word>(hi: &FBig<mode::HalfEven, B>, p: usize) -> FBig<R, B> {
1074        let ctx = Context::<R>::new(p);
1075        FBig::new(ctx.repr_round_ref(hi.repr()).value(), ctx)
1076    }
1077
1078    /// `sinh_pi`/`cosh_pi` vs the high-precision oracle (p + 60 under `HalfEven`, re-rounded
1079    /// under the mode under test), across the four canonical significand widths. The inputs
1080    /// straddle the `|πx| = 1` series/exp boundary from both sides (0.3 → series, 0.5 → exp —
1081    /// where an early version wrongly carried the exp_m1 form's `+1` into the true-exp
1082    /// composition, returning cosh_pi(0.5) = 3.509… instead of 2.509…).
1083    #[test]
1084    fn test_sinh_cosh_pi_oracle() {
1085        let inputs = [
1086            "0.1", "0.2", "0.3", "0.5", "0.7", "1.5", "2.808", "10.1", "123.456", "-0.3", "-1.2",
1087            "-2.808",
1088        ];
1089        for &p in &[20usize, 50, 100, 500] {
1090            for input in inputs {
1091                let x = DBig::from_str(input).unwrap();
1092                let hi_ctx = Context::<mode::HalfEven>::new(p + 60);
1093                let hi_s = hi_ctx.sinh_pi::<10>(x.repr(), None).unwrap().value();
1094                let hi_c = hi_ctx.cosh_pi::<10>(x.repr(), None).unwrap().value();
1095
1096                let got = Context::<mode::HalfEven>::new(p)
1097                    .sinh_pi::<10>(x.repr(), None)
1098                    .unwrap()
1099                    .value();
1100                assert_eq!(
1101                    got,
1102                    reround::<mode::HalfEven, 10>(&hi_s, p),
1103                    "sinh_pi({input}) at p={p}"
1104                );
1105                let got = Context::<mode::Down>::new(p)
1106                    .cosh_pi::<10>(x.repr(), None)
1107                    .unwrap()
1108                    .value();
1109                assert_eq!(
1110                    got,
1111                    reround::<mode::Down, 10>(&hi_c, p),
1112                    "cosh_pi({input}) at p={p} under Down"
1113                );
1114
1115                let (s, c) = Context::<mode::HalfEven>::new(p).sinh_cosh_pi::<10>(x.repr(), None);
1116                assert_eq!(
1117                    s.unwrap().value(),
1118                    reround::<mode::HalfEven, 10>(&hi_s, p),
1119                    "sinh_cosh_pi sinh({input}) at p={p}"
1120                );
1121                assert_eq!(
1122                    c.unwrap().value(),
1123                    reround::<mode::HalfEven, 10>(&hi_c, p),
1124                    "sinh_cosh_pi cosh({input}) at p={p}"
1125                );
1126            }
1127        }
1128    }
1129
1130    /// The two exponentials of `sinh(1e14)` sit ~10¹⁵ digits apart: the ball compositions must
1131    /// drop the negligible one instead of trying to align that gap (an out-of-memory panic
1132    /// before the guarded add existed), and the plain `sinh`/`cosh` must survive the same
1133    /// argument through the core sticky-collapse in the aligned sum.
1134    // The ~10¹⁴-scale argument needs the 64-bit `isize` exponent range; on 32-bit targets the
1135    // overflow guard fires first (its range is ~4000× smaller), so the test is 64-bit-only.
1136    #[cfg(target_pointer_width = "64")]
1137    #[test]
1138    fn test_sinh_huge_argument_no_oom() {
1139        let x = DBig::from_str("189870321059487.19").unwrap();
1140        let ctx = Context::<mode::HalfEven>::new(20);
1141        let v = ctx.sinh::<10>(x.repr(), None).unwrap().value();
1142        assert_eq!(v.repr().sign(), Sign::Positive);
1143        let v = ctx.sinh_pi::<10>(x.repr(), None).unwrap().value();
1144        assert_eq!(v.repr().sign(), Sign::Positive);
1145        let v = ctx.cosh::<10>(x.repr(), None).unwrap().value();
1146        assert_eq!(v.repr().sign(), Sign::Positive);
1147        let v = ctx.cosh_pi::<10>(x.repr(), None).unwrap().value();
1148        assert_eq!(v.repr().sign(), Sign::Positive);
1149        let (s, c) = ctx.sinh_cosh_pi::<10>(x.repr(), None);
1150        assert_eq!(s.unwrap().value().repr().sign(), Sign::Positive);
1151        assert_eq!(c.unwrap().value().repr().sign(), Sign::Positive);
1152        // odd: the negative side flips sinh's sign only
1153        let neg = -x;
1154        assert_eq!(
1155            ctx.sinh_pi::<10>(neg.repr(), None)
1156                .unwrap()
1157                .value()
1158                .repr()
1159                .sign(),
1160            Sign::Negative
1161        );
1162        assert_eq!(
1163            ctx.cosh_pi::<10>(neg.repr(), None)
1164                .unwrap()
1165                .value()
1166                .repr()
1167                .sign(),
1168            Sign::Positive
1169        );
1170    }
1171
1172    // The closure's `exp_compute` runs the `Bⁿ` powering chain, whose radius slack used to cost
1173    // one systematic Ziv retry on non-power-of-two bases (`pow_chain_guard` now charges the chain
1174    // length to the initial guard). Pins a measured retrying point (DBig `sinh 10` @150 and
1175    // @600 both retried exactly once) to first-attempt certification.
1176    #[cfg(feature = "std")]
1177    #[test]
1178    fn sinh_certifies_first_attempt_base10() {
1179        let ctx = Context::<mode::HalfEven>::new(150);
1180        crate::ziv_retries_reset();
1181        let _ = ctx
1182            .sinh::<10>(&Repr::<10>::new(10.into(), 0), None)
1183            .unwrap();
1184        assert_eq!(
1185            crate::ziv_retries(),
1186            0,
1187            "DBig sinh(10) @150 should certify on the first attempt"
1188        );
1189    }
1190
1191    // A result in `10^(0.301·isize::MAX)..10^isize::MAX` is representable as a `Repr` but lies
1192    // past the old `Mag` ceiling (`2^isize::MAX ≈ 10^(0.301·isize::MAX)`): the exported radius
1193    // saturated to `+∞`, which no Ziv attempt can certify, and the loop spun through its
1194    // exponential guard growth for hours before erroring. The `Mag` exponent field is now
1195    // `i128` — wide enough to bound any representable value — so the ×π hyperbolic family
1196    // certifies here in one retry. The input magnitude scales with the target's pointer width
1197    // (the 64-bit magnitude is a genuine overflow on a 32-bit `isize`).
1198    #[cfg(feature = "std")]
1199    #[test]
1200    fn sinh_pi_huge_magnitude_certifies() {
1201        let ctx = Context::<mode::HalfEven>::new(20);
1202        // aim `x` at `0.4·isize::MAX`: the result's decimal exponent is `π·x/ln10 ≈ 0.546·MAX`,
1203        // comfortably past the old ceiling (`0.301·MAX`) and short of the `Repr` range (`MAX`),
1204        // on every pointer width (the 64-bit magnitude would be a genuine overflow on a
1205        // 32-bit `isize`)
1206        let imax = isize::MAX as f64;
1207        let e = (0.4 * imax).log10().floor() as isize;
1208        let sig = (0.4 * imax / 10f64.powi(e as i32)).round() as i64;
1209        let x = Repr::<10>::new(IBig::from(sig), e);
1210        let past_old_ceiling = |r: &Repr<10>| r.exponent() > isize::MAX / 2;
1211
1212        crate::ziv_retries_reset();
1213        let sh = ctx.sinh_pi::<10>(&x, None).unwrap().value();
1214        assert!(crate::ziv_retries() <= 1, "sinh_pi @20 should certify within one retry");
1215        assert!(!sh.repr().is_infinite() && past_old_ceiling(sh.repr()));
1216
1217        crate::ziv_retries_reset();
1218        let ch = ctx.cosh_pi::<10>(&x, None).unwrap().value();
1219        assert!(crate::ziv_retries() <= 1);
1220        assert!(!ch.repr().is_infinite() && past_old_ceiling(ch.repr()));
1221
1222        // the pair shares one evaluation and certifies both parts together
1223        crate::ziv_retries_reset();
1224        let (sh2, ch2) = ctx.sinh_cosh_pi::<10>(&x, None);
1225        assert!(crate::ziv_retries() <= 1);
1226        assert_eq!(sh2.unwrap().value(), sh);
1227        assert_eq!(ch2.unwrap().value(), ch);
1228    }
1229
1230    /// `acosh`/`atanh` compare their `±1` boundaries *exactly*: an argument that merely rounds
1231    /// onto the endpoint is not the endpoint, and both have a vertical tangent there — the true
1232    /// value is `√(2δ)` (acosh) / `ln(2/δ)/2` (atanh), not the endpoint's `0` / `∞`.
1233    ///
1234    /// The rows that *used* to be at risk here are covered: `acosh` takes `√((x−1)(x+1))` and
1235    /// `atanh` divides `2x/(1−x)` by an argument that rounds onto the pole, so both reach the
1236    /// retry path rather than certifying the endpoint — `acosh` via the loop's zero-candidate
1237    /// guard, `atanh` via `ln_1p_ball`'s pole guard (which previously panicked on the `±∞`
1238    /// ratio).
1239    #[test]
1240    fn test_hyperbolic_endpoints_compare_exactly() {
1241        for &p in &[20usize, 50, 100, 500] {
1242            // 10^-n is beyond the initial guard, so the argument rounds onto the endpoint at the
1243            // first work precision for p ≥ 100 (the case that used to snap); at p = 20/50 the rows
1244            // are plain correctness checks.
1245            let n = 3 * p / 2 + 5;
1246            let below = DBig::from_str(&format!("0.{}", "9".repeat(n))).unwrap(); // 1 − 10^-n
1247            let above = DBig::from_str(&format!("1.{}1", "0".repeat(n - 1))).unwrap(); // 1 + 10^-n
1248            let ctx = Context::<mode::HalfEven>::new(p);
1249            let hi = Context::<mode::HalfEven>::new(p + 60);
1250
1251            let a = ctx.atanh::<10>(below.repr(), None).unwrap().value();
1252            assert!(a.repr().is_finite(), "atanh(1 - 10^-{n}) at p={p} is finite, not -inf");
1253            assert_eq!(
1254                a,
1255                hi.atanh::<10>(below.repr(), None)
1256                    .unwrap()
1257                    .value()
1258                    .with_precision(p)
1259                    .value(),
1260                "atanh(1 - 10^-{n}) at p={p}"
1261            );
1262
1263            let c = ctx.acosh::<10>(above.repr(), None).unwrap().value();
1264            assert!(
1265                !c.repr().significand().is_zero(),
1266                "acosh(1 + 10^-{n}) at p={p} is not an exact 0"
1267            );
1268            assert_eq!(
1269                c,
1270                hi.acosh::<10>(above.repr(), None)
1271                    .unwrap()
1272                    .value()
1273                    .with_precision(p)
1274                    .value(),
1275                "acosh(1 + 10^-{n}) at p={p}"
1276            );
1277
1278            // the exact endpoints keep their exact results
1279            let one = Repr::<10>::one();
1280            assert!(ctx
1281                .atanh::<10>(&one, None)
1282                .unwrap()
1283                .value()
1284                .repr()
1285                .is_infinite());
1286            assert!(ctx
1287                .atanh::<10>(&(-Repr::<10>::one()), None)
1288                .unwrap()
1289                .value()
1290                .repr()
1291                .is_infinite());
1292            assert!(ctx
1293                .acosh::<10>(&one, None)
1294                .unwrap()
1295                .value()
1296                .repr()
1297                .significand()
1298                .is_zero());
1299        }
1300    }
1301
1302    /// The expected tanh saturation endpoint: `±1` when the mode stays, otherwise the largest
1303    /// p-bit significand just inside it (`±(2^p − 1) × 2^{−p}`).
1304    fn tanh_endpoint_repr(sign: Sign, p: usize, stays: bool) -> Repr<2> {
1305        if stays {
1306            if sign == Sign::Positive {
1307                Repr::<2>::one()
1308            } else {
1309                Repr::<2>::neg_one()
1310            }
1311        } else {
1312            let next_mag = (dashu_int::UBig::from(1u8) << p) - dashu_int::UBig::from(1u8);
1313            Repr::new(IBig::from_parts(sign, next_mag), -(p as isize))
1314        }
1315    }
1316
1317    // tanh saturates at ±1 for large |x|: the value is sign(x)·(1 − δ) with δ = 2/(e^{2|x|}+1)
1318    // below half an ulp, so the rounding is fully determined — but the Ziv working-precision mid
1319    // collapses onto exactly ±1 (the boundary of a one-sided directed preimage), which the loop
1320    // can never certify: it used to climb toward a working precision of ~2|x|/ln 2 — an
1321    // effective hang for plain inputs like tanh(±1755648) (minutes at |x| = 10⁵, and the
1322    // positive-side "overflow → exact +1" shortcut was unreachable). Such inputs now
1323    // short-circuit to the mode-aware endpoint before the loop, mirroring exp_m1's gate.
1324    #[test]
1325    fn test_tanh_large_argument_saturation() {
1326        // deterministic magnitudes: the reported repro value (±1755648 — both signs used to
1327        // hang), plus decade probes whose loops used to reach ~30k/~300k-bit working precisions.
1328        let mags = [
1329            dashu_int::UBig::from(1755648u64),
1330            dashu_int::UBig::from(10000u64),
1331            dashu_int::UBig::from(100000u64),
1332        ];
1333        for &p in &[20usize, 50, 100, 500] {
1334            for mag in &mags {
1335                for &sign in &[Sign::Positive, Sign::Negative] {
1336                    let x = Repr::<2>::new(IBig::from_parts(sign, mag.clone()), 0);
1337                    // `stays` per mode: nearest and the outward-directed mode keep ±1; the modes
1338                    // rounding toward zero (Down/Zero below +1, Up/Zero above −1) step one ulp in.
1339                    macro_rules! check {
1340                        ($mode:ty, $stays:expr) => {{
1341                            use crate::round::Rounding;
1342                            let r = Context::<$mode>::new(p)
1343                                .tanh::<2>(&x, None)
1344                                .unwrap_or_else(|e| panic!("tanh failed: {e:?}"));
1345                            let name = stringify!($mode);
1346                            let want = tanh_endpoint_repr(sign, p, $stays);
1347                            let got_adj = match &r {
1348                                dashu_base::Approximation::Inexact(_, adj) => *adj,
1349                                _ => panic!("{name} tanh(±{mag}) p={p} must be inexact"),
1350                            };
1351                            let got = r.value();
1352                            assert_eq!(got.repr(), &want, "{name} tanh(±{mag}) p={p}");
1353                            let expected_adj = if $stays {
1354                                Rounding::NoOp
1355                            } else if sign == Sign::Positive {
1356                                Rounding::SubOne
1357                            } else {
1358                                Rounding::AddOne
1359                            };
1360                            assert_eq!(got_adj, expected_adj, "{name} flag tanh(±{mag}) p={p}");
1361                            assert_eq!(
1362                                got.precision(),
1363                                p,
1364                                "{name} endpoint lost the input precision"
1365                            );
1366                        }};
1367                    }
1368                    check!(mode::HalfEven, true);
1369                    check!(mode::HalfAway, true);
1370                    check!(mode::Away, true);
1371                    check!(mode::Up, sign == Sign::Positive);
1372                    check!(mode::Down, sign == Sign::Negative);
1373                    check!(mode::Zero, false);
1374                }
1375            }
1376        }
1377    }
1378
1379    // Below the saturation threshold the value goes through the normal Ziv path — the gate must
1380    // not over-fire. Cross-check directed and nearest modes against a p+60 HalfEven oracle
1381    // (re-rounded under the mode under test), on inputs just under and comfortably under the
1382    // threshold for their precision (p = 20 → threshold ≈ 7.6; p = 50 → ≈ 18.0).
1383    #[test]
1384    fn test_tanh_below_threshold_matches_oracle() {
1385        for &(mag, p) in &[(2i64, 20usize), (7, 20), (10, 50), (17, 50)] {
1386            for &sign in &[Sign::Positive, Sign::Negative] {
1387                let mag_i = IBig::from(mag);
1388                let x = Repr::<2>::new(
1389                    if sign == Sign::Positive {
1390                        mag_i
1391                    } else {
1392                        -mag_i
1393                    },
1394                    0,
1395                );
1396                let hi = Context::<mode::HalfEven>::new(p + 60)
1397                    .tanh::<2>(&x, None)
1398                    .unwrap()
1399                    .value();
1400                macro_rules! check {
1401                    ($mode:ty) => {{
1402                        let want = reround::<$mode, 2>(&hi, p);
1403                        let got = Context::<$mode>::new(p)
1404                            .tanh::<2>(&x, None)
1405                            .unwrap()
1406                            .value();
1407                        assert_eq!(got, want, "{} tanh(±{mag}) p={p}", stringify!($mode));
1408                    }};
1409                }
1410                check!(mode::HalfEven);
1411                check!(mode::Down);
1412                check!(mode::Up);
1413                check!(mode::Zero);
1414            }
1415        }
1416    }
1417}