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