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