#[cfg(table_format = "q16_16")]
mod realtime_tests {
use g_math::fixed_point::FixedPoint;
use g_math::fixed_point::canonical::{gmath, evaluate, evaluate_sinhcosh};
include!("data/fasc_ulp_refs_q16.rs");
fn gmath_safe(input: &'static str) -> g_math::fixed_point::canonical::LazyExpr {
if input.starts_with('-') {
let positive: &'static str = unsafe {
std::str::from_utf8_unchecked(
std::slice::from_raw_parts(input.as_ptr().add(1), input.len() - 1),
)
};
-gmath(positive)
} else {
gmath(input)
}
}
#[test]
fn binary_sinhcosh_matches_mpmath() {
let mut failures = Vec::new();
let mut sinh_map: Vec<(i32, i32)> = Vec::new();
let mut cosh_map: Vec<(i32, i32)> = Vec::new();
for &(_input_str, input_raw, expected_raw, func_name) in Q16_REFS {
match func_name {
"sinh" => sinh_map.push((input_raw, expected_raw)),
"cosh" => cosh_map.push((input_raw, expected_raw)),
_ => {}
}
}
assert!(!sinh_map.is_empty(), "no sinh references found");
assert!(!cosh_map.is_empty(), "no cosh references found");
for &(input_raw, sinh_expected) in &sinh_map {
let cosh_expected = cosh_map
.iter()
.find(|(x, _)| *x == input_raw)
.map(|(_, e)| *e)
.expect("no matching cosh ref");
let x = FixedPoint::from_raw(input_raw);
let (s, c) = x.sinhcosh();
let sinh_ulp = (s.raw() as i64 - sinh_expected as i64).abs();
let cosh_ulp = (c.raw() as i64 - cosh_expected as i64).abs();
if sinh_ulp > 1 {
failures.push(format!(
"sinh({}) = {} (expected {}), {} ULP",
input_raw, s.raw(), sinh_expected, sinh_ulp
));
}
if cosh_ulp > 1 {
failures.push(format!(
"cosh({}) = {} (expected {}), {} ULP",
input_raw, c.raw(), cosh_expected, cosh_ulp
));
}
}
assert!(failures.is_empty(), "ULP failures:\n{}", failures.join("\n"));
}
#[test]
fn binary_sinhcosh_matches_separate_calls() {
for &(_input_str, input_raw, _expected_raw, func_name) in Q16_REFS {
if func_name != "sinh" { continue; }
let x = FixedPoint::from_raw(input_raw);
let (fused_s, fused_c) = x.sinhcosh();
let sep_s = x.sinh();
let sep_c = x.cosh();
assert_eq!(fused_s.raw(), sep_s.raw(),
"sinhcosh.sinh != sinh for raw={}", input_raw);
assert_eq!(fused_c.raw(), sep_c.raw(),
"sinhcosh.cosh != cosh for raw={}", input_raw);
}
}
#[test]
fn binary_sinhcosh_identity_tight() {
for &(_input_str, input_raw, _expected, func_name) in Q16_REFS {
if func_name != "sinh" { continue; }
let x = FixedPoint::from_raw(input_raw);
let (s, c) = x.sinhcosh();
let one = FixedPoint::from_int(1);
let diff = (c * c) - (s * s) - one;
assert!(diff.raw().abs() <= 8,
"identity cosh²-sinh²=1 residual too large for raw={}: diff={}",
input_raw, diff.raw());
}
}
#[test]
fn binary_try_sinhcosh_overflow_gate() {
let big = FixedPoint::from_int(15);
let res = big.try_sinhcosh();
assert!(res.is_err(), "try_sinhcosh(15) should overflow Q16.16 storage");
}
#[test]
fn fasc_evaluate_sinhcosh_matches_imperative() {
for &(input_str, input_raw, _exp, func_name) in Q16_REFS {
if func_name != "sinh" { continue; }
let (fasc_s, fasc_c) = evaluate_sinhcosh(&gmath_safe(input_str))
.expect("FASC sinhcosh eval");
let fasc_s_raw = fasc_s.as_binary_storage().expect("binary storage");
let fasc_c_raw = fasc_c.as_binary_storage().expect("binary storage");
let (imp_s, imp_c) = FixedPoint::from_raw(input_raw).sinhcosh();
assert!((fasc_s_raw as i64 - imp_s.raw() as i64).abs() <= 1,
"FASC vs imperative sinh mismatch for {}: {} vs {}",
input_str, fasc_s_raw, imp_s.raw());
assert!((fasc_c_raw as i64 - imp_c.raw() as i64).abs() <= 1,
"FASC vs imperative cosh mismatch for {}: {} vs {}",
input_str, fasc_c_raw, imp_c.raw());
let sin_only = evaluate(&gmath_safe(input_str).sinh()).unwrap()
.as_binary_storage().unwrap();
let cos_only = evaluate(&gmath_safe(input_str).cosh()).unwrap()
.as_binary_storage().unwrap();
assert!((fasc_s_raw as i64 - sin_only as i64).abs() <= 1,
"FASC pair sinh vs FASC single sinh drift at {}", input_str);
assert!((fasc_c_raw as i64 - cos_only as i64).abs() <= 1,
"FASC pair cosh vs FASC single cosh drift at {}", input_str);
}
}
}
#[cfg(table_format = "q32_32")]
mod compact_tests {
use g_math::fixed_point::FixedPoint;
use g_math::fixed_point::canonical::{gmath, evaluate, evaluate_sinhcosh};
include!("data/fasc_ulp_refs_q32.rs");
fn gmath_safe(input: &'static str) -> g_math::fixed_point::canonical::LazyExpr {
if input.starts_with('-') {
let positive: &'static str = unsafe {
std::str::from_utf8_unchecked(
std::slice::from_raw_parts(input.as_ptr().add(1), input.len() - 1),
)
};
-gmath(positive)
} else {
gmath(input)
}
}
#[test]
fn binary_sinhcosh_matches_mpmath() {
let mut failures = Vec::new();
let mut sinh_map: Vec<(i64, i64)> = Vec::new();
let mut cosh_map: Vec<(i64, i64)> = Vec::new();
for &(_input_str, input_raw, expected_raw, func_name) in Q32_REFS {
match func_name {
"sinh" => sinh_map.push((input_raw, expected_raw)),
"cosh" => cosh_map.push((input_raw, expected_raw)),
_ => {}
}
}
assert!(!sinh_map.is_empty() && !cosh_map.is_empty(),
"missing sinh/cosh refs in Q32 dataset");
for &(input_raw, sinh_expected) in &sinh_map {
let cosh_expected = cosh_map.iter()
.find(|(x, _)| *x == input_raw)
.map(|(_, e)| *e)
.expect("no matching cosh ref");
let x = FixedPoint::from_raw(input_raw);
let (s, c) = x.sinhcosh();
let sinh_ulp = (s.raw() as i128 - sinh_expected as i128).abs();
let cosh_ulp = (c.raw() as i128 - cosh_expected as i128).abs();
if sinh_ulp > 1 {
failures.push(format!(
"sinh({}) = {} (expected {}), {} ULP",
input_raw, s.raw(), sinh_expected, sinh_ulp
));
}
if cosh_ulp > 1 {
failures.push(format!(
"cosh({}) = {} (expected {}), {} ULP",
input_raw, c.raw(), cosh_expected, cosh_ulp
));
}
}
assert!(failures.is_empty(), "ULP failures:\n{}", failures.join("\n"));
}
#[test]
fn binary_sinhcosh_matches_separate_calls() {
for &(_input_str, input_raw, _expected, func_name) in Q32_REFS {
if func_name != "sinh" { continue; }
let x = FixedPoint::from_raw(input_raw);
let (fused_s, fused_c) = x.sinhcosh();
let sep_s = x.sinh();
let sep_c = x.cosh();
assert_eq!(fused_s.raw(), sep_s.raw());
assert_eq!(fused_c.raw(), sep_c.raw());
}
}
#[test]
fn binary_sinhcosh_identity_tight() {
for &(_input_str, input_raw, _expected, func_name) in Q32_REFS {
if func_name != "sinh" { continue; }
let x = FixedPoint::from_raw(input_raw);
let (s, c) = x.sinhcosh();
let one = FixedPoint::from_int(1);
let diff = (c * c) - (s * s) - one;
assert!(diff.raw().abs() <= 64,
"identity residual too large for raw={}: diff={}",
input_raw, diff.raw());
}
}
#[test]
fn binary_try_sinhcosh_overflow_gate() {
let big = FixedPoint::from_int(25);
let res = big.try_sinhcosh();
assert!(res.is_err(), "try_sinhcosh(25) should overflow Q32.32 storage");
}
#[test]
fn fasc_evaluate_sinhcosh_matches_imperative() {
for &(input_str, input_raw, _exp, func_name) in Q32_REFS {
if func_name != "sinh" { continue; }
let (fasc_s, fasc_c) = evaluate_sinhcosh(&gmath_safe(input_str))
.expect("FASC sinhcosh eval");
let fasc_s_raw = fasc_s.as_binary_storage().expect("binary storage");
let fasc_c_raw = fasc_c.as_binary_storage().expect("binary storage");
let (imp_s, imp_c) = FixedPoint::from_raw(input_raw).sinhcosh();
assert!((fasc_s_raw as i128 - imp_s.raw() as i128).abs() <= 1);
assert!((fasc_c_raw as i128 - imp_c.raw() as i128).abs() <= 1);
let sin_only = evaluate(&gmath_safe(input_str).sinh()).unwrap()
.as_binary_storage().unwrap();
let cos_only = evaluate(&gmath_safe(input_str).cosh()).unwrap()
.as_binary_storage().unwrap();
assert!((fasc_s_raw as i128 - sin_only as i128).abs() <= 1);
assert!((fasc_c_raw as i128 - cos_only as i128).abs() <= 1);
}
}
}
#[cfg(any(table_format = "q16_16", table_format = "q32_32"))]
mod decimal_tests {
use g_math::fixed_point::DecimalFixed;
use std::str::FromStr;
fn approx_equal<const DECIMALS: u8>(
a: DecimalFixed<DECIMALS>,
b: DecimalFixed<DECIMALS>,
tol_raw: i128,
) -> bool {
(a.raw_value() - b.raw_value()).abs() <= tol_raw
}
#[test]
fn decimal_sinhcosh_matches_separate_calls() {
let inputs = ["0.0", "0.5", "1.0", "-0.5", "-1.0", "2.0"];
for s in &inputs {
let x = DecimalFixed::<4>::from_str(s).expect("decimal parse");
let (fused_s, fused_c) = x.sinhcosh();
let sep_s = x.sinh();
let sep_c = x.cosh();
assert!(approx_equal(fused_s, sep_s, 1),
"decimal sinhcosh.sinh drifts from sinh() at {}: fused={} sep={}",
s, fused_s.raw_value(), sep_s.raw_value());
assert!(approx_equal(fused_c, sep_c, 1),
"decimal sinhcosh.cosh drifts from cosh() at {}: fused={} sep={}",
s, fused_c.raw_value(), sep_c.raw_value());
}
}
#[test]
fn decimal_sinhcosh_identity_tight() {
let inputs = ["0.0", "0.5", "1.0", "-0.5", "-1.0", "2.0"];
for s in &inputs {
let x = DecimalFixed::<4>::from_str(s).expect("decimal parse");
let (sh, ch) = x.sinhcosh();
let one = DecimalFixed::<4>::from_str("1.0").unwrap();
let residual = (ch * ch) - (sh * sh) - one;
assert!(residual.raw_value().abs() <= 8,
"decimal identity residual too large at {}: {}",
s, residual.raw_value());
}
}
#[test]
fn decimal_sinhcosh_zero_is_exact() {
let zero = DecimalFixed::<4>::from_str("0.0").unwrap();
let (s, c) = zero.sinhcosh();
assert_eq!(s.raw_value(), 0, "sinh(0) must be 0");
assert_eq!(c.raw_value(), DecimalFixed::<4>::SCALE,
"cosh(0) must be 1.0 exactly");
}
}