1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//! Property tests for the hyperbolic functions (pure-Rust identities).
//!
//! These run unconditionally in CI. The residual is checked at a tolerance of a
//! few ulp. Note `cosh²−sinh² = 1` is evaluated on a small range: for large |x|
//! both terms are ≈ e^{2|x|}/4, so subtracting them to get 1 loses ≈2·log₁₀(cosh x)
//! digits, which would exceed the tolerance.
use dashu_float::ops::Abs;
use dashu_float::DBig;
use dashu_int::IBig;
use proptest::prelude::*;
const P: usize = 50;
/// `10^(-P + slack)` — a tolerance in ulp at precision `P` (base 10).
fn tol(slack: isize) -> DBig {
DBig::from_parts(IBig::from(1), -(P as isize) + slack)
}
/// Random `DBig` at precision `P` with value `m * 10^-4` for `m in [min, max]`.
fn x_in(min: i64, max: i64) -> impl Strategy<Value = DBig> {
(min..=max).prop_map(|m| {
DBig::from_parts(IBig::from(m), -4)
.with_precision(P)
.value()
})
}
proptest! {
#![proptest_config(ProptestConfig { cases: 64, ..Default::default() })]
/// cosh²(x) - sinh²(x) == 1 (small |x|: avoids the cancellation for large |x|)
#[test]
fn pythagorean(x in x_in(-10_000, 10_000)) {
let s = x.sinh();
let c = x.cosh();
let resid = (&c * &c - &s * &s - DBig::ONE).abs();
prop_assert!(resid < tol(2));
}
/// sinh(−x) == −sinh(x); cosh(−x) == cosh(x); tanh(−x) == −tanh(x)
#[test]
fn parity(x in x_in(-200_000, 200_000)) {
let s = x.sinh();
let c = x.cosh();
let t = x.tanh();
let nx = -x;
prop_assert!((nx.sinh() + s).abs() < tol(2));
prop_assert!((nx.cosh() - c).abs() < tol(2));
prop_assert!((nx.tanh() + t).abs() < tol(2));
}
/// tanh(x) == sinh(x) / cosh(x)
#[test]
fn tanh_ratio(x in x_in(-200_000, 200_000)) {
let s = x.sinh();
let c = x.cosh();
let t = x.tanh();
prop_assert!((t - s / c).abs() < tol(3));
}
/// asinh(sinh(x)) == x
#[test]
fn round_trip_asinh(x in x_in(-200_000, 200_000)) {
let y = x.sinh().asinh();
prop_assert!((y - x).abs() < tol(2));
}
/// acosh(cosh(x)) == x. cosh(x) → 1 as x → 0, so the round-trip is ill-conditioned for small
/// |x|: acosh's `sqrt` amplifies cosh's rounding error by 1/|x|, making the absolute error grow
/// as ≈ ulp(1)/|x|. The tolerance tracks this with a 1/|x| term on top of the tol(2) floor, so
/// the full range (down to x = 1e-4) is exercised rather than skipped.
#[test]
fn round_trip_acosh(x in x_in(1, 200_000)) {
let y = x.cosh().acosh();
let resid = (y - x.clone()).abs();
let bound = tol(2) + tol(2) / x.abs();
prop_assert!(resid < bound);
}
/// atanh(tanh(x)) == x (small |x|: tanh saturates toward ±1 for large |x|, so the
/// inverse loses ≈0.87·|x| decimal digits regardless of precision)
#[test]
fn round_trip_atanh(x in x_in(-20_000, 20_000)) {
let y = x.tanh().atanh();
prop_assert!((y - x).abs() < tol(2));
}
}