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