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}