Add interfaces and tests based on function domains

Create a type representing a function's domain and a test that does a
logarithmic sweep of points within the domain.
This commit is contained in:
Trevor Gross 2024-12-19 11:19:01 +00:00
parent 163ed2a133
commit a8a2f70ae6
5 changed files with 327 additions and 5 deletions

View file

@ -0,0 +1,186 @@
//! Traits and operations related to bounds of a function.
use std::fmt;
use std::ops::{self, Bound};
use crate::Float;
/// Representation of a function's domain.
#[derive(Clone, Debug)]
pub struct Domain<T> {
/// Start of the region for which a function is defined (ignoring poles).
pub start: Bound<T>,
/// Endof the region for which a function is defined (ignoring poles).
pub end: Bound<T>,
/// Additional points to check closer around. These can be e.g. undefined asymptotes or
/// inflection points.
pub check_points: Option<fn() -> BoxIter<T>>,
}
type BoxIter<T> = Box<dyn Iterator<Item = T>>;
impl<F: Float> Domain<F> {
/// The start of this domain, saturating at negative infinity.
pub fn range_start(&self) -> F {
match self.start {
Bound::Included(v) => v,
Bound::Excluded(v) => v.next_up(),
Bound::Unbounded => F::NEG_INFINITY,
}
}
/// The end of this domain, saturating at infinity.
pub fn range_end(&self) -> F {
match self.end {
Bound::Included(v) => v,
Bound::Excluded(v) => v.next_down(),
Bound::Unbounded => F::INFINITY,
}
}
}
impl<F: Float> Domain<F> {
/// x ∈
pub const UNBOUNDED: Self =
Self { start: Bound::Unbounded, end: Bound::Unbounded, check_points: None };
/// x ∈ >= 0
pub const POSITIVE: Self =
Self { start: Bound::Included(F::ZERO), end: Bound::Unbounded, check_points: None };
/// x ∈ > 0
pub const STRICTLY_POSITIVE: Self =
Self { start: Bound::Excluded(F::ZERO), end: Bound::Unbounded, check_points: None };
/// Used for versions of `asin` and `acos`.
pub const INVERSE_TRIG_PERIODIC: Self = Self {
start: Bound::Included(F::NEG_ONE),
end: Bound::Included(F::ONE),
check_points: None,
};
/// Domain for `acosh`
pub const ACOSH: Self =
Self { start: Bound::Included(F::ONE), end: Bound::Unbounded, check_points: None };
/// Domain for `atanh`
pub const ATANH: Self = Self {
start: Bound::Excluded(F::NEG_ONE),
end: Bound::Excluded(F::ONE),
check_points: None,
};
/// Domain for `sin`, `cos`, and `tan`
pub const TRIG: Self = Self {
// TODO
check_points: Some(|| Box::new([-F::PI, -F::FRAC_PI_2, F::FRAC_PI_2, F::PI].into_iter())),
..Self::UNBOUNDED
};
/// Domain for `log` in various bases
pub const LOG: Self = Self::STRICTLY_POSITIVE;
/// Domain for `log1p` i.e. `log(1 + x)`
pub const LOG1P: Self =
Self { start: Bound::Excluded(F::NEG_ONE), end: Bound::Unbounded, check_points: None };
/// Domain for `sqrt`
pub const SQRT: Self = Self::POSITIVE;
/// Domain for `gamma`
pub const GAMMA: Self = Self {
check_points: Some(|| {
// Negative integers are asymptotes
Box::new((0..u8::MAX).map(|scale| {
let mut base = F::ZERO;
for _ in 0..scale {
base = base - F::ONE;
}
base
}))
}),
// Whether or not gamma is defined for negative numbers is implementation dependent
..Self::UNBOUNDED
};
/// Domain for `loggamma`
pub const LGAMMA: Self = Self::STRICTLY_POSITIVE;
}
/// Implement on `op::*` types to indicate how they are bounded.
pub trait HasDomain<T>
where
T: Copy + fmt::Debug + ops::Add<Output = T> + ops::Sub<Output = T> + PartialOrd + 'static,
{
const DOMAIN: Domain<T>;
}
/// Implement [`HasDomain`] for both the `f32` and `f64` variants of a function.
macro_rules! impl_has_domain {
($($fn_name:ident => $domain:expr;)*) => {
paste::paste! {
$(
// Implement for f64 functions
impl HasDomain<f64> for $crate::op::$fn_name::Routine {
const DOMAIN: Domain<f64> = Domain::<f64>::$domain;
}
// Implement for f32 functions
impl HasDomain<f32> for $crate::op::[< $fn_name f >]::Routine {
const DOMAIN: Domain<f32> = Domain::<f32>::$domain;
}
)*
}
};
}
// Tie functions together with their domains.
impl_has_domain! {
acos => INVERSE_TRIG_PERIODIC;
acosh => ACOSH;
asin => INVERSE_TRIG_PERIODIC;
asinh => UNBOUNDED;
atan => UNBOUNDED;
atanh => ATANH;
cbrt => UNBOUNDED;
ceil => UNBOUNDED;
cos => TRIG;
cosh => UNBOUNDED;
erf => UNBOUNDED;
exp => UNBOUNDED;
exp10 => UNBOUNDED;
exp2 => UNBOUNDED;
expm1 => UNBOUNDED;
fabs => UNBOUNDED;
floor => UNBOUNDED;
frexp => UNBOUNDED;
ilogb => UNBOUNDED;
j0 => UNBOUNDED;
j1 => UNBOUNDED;
lgamma => LGAMMA;
log => LOG;
log10 => LOG;
log1p => LOG1P;
log2 => LOG;
modf => UNBOUNDED;
rint => UNBOUNDED;
round => UNBOUNDED;
sin => TRIG;
sincos => TRIG;
sinh => UNBOUNDED;
sqrt => SQRT;
tan => TRIG;
tanh => UNBOUNDED;
tgamma => GAMMA;
trunc => UNBOUNDED;
}
/* Manual implementations, these functions don't follow `foo`->`foof` naming */
impl HasDomain<f32> for crate::op::lgammaf_r::Routine {
const DOMAIN: Domain<f32> = Domain::<f32>::LGAMMA;
}
impl HasDomain<f64> for crate::op::lgamma_r::Routine {
const DOMAIN: Domain<f64> = Domain::<f64>::LGAMMA;
}

View file

@ -1,6 +1,7 @@
//! Different generators that can create random or systematic bit patterns.
use crate::GenerateInput;
pub mod domain_logspace;
pub mod random;
/// Helper type to turn any reusable input into a generator.

View file

@ -0,0 +1,43 @@
//! A generator that produces logarithmically spaced values within domain bounds.
use libm::support::{IntTy, MinInt};
use crate::domain::HasDomain;
use crate::op::OpITy;
use crate::{MathOp, logspace};
/// Number of tests to run.
// FIXME(ntests): replace this with a more logical algorithm
const NTESTS: usize = {
if cfg!(optimizations_enabled) {
if crate::emulated()
|| !cfg!(target_pointer_width = "64")
|| cfg!(all(target_arch = "x86_64", target_vendor = "apple"))
{
// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run
// in QEMU.
100_000
} else {
5_000_000
}
} else {
// Without optimizations just run a quick check
800
}
};
/// Create a range of logarithmically spaced inputs within a function's domain.
///
/// This allows us to get reasonably thorough coverage without wasting time on values that are
/// NaN or out of range. Random tests will still cover values that are excluded here.
pub fn get_test_cases<Op>() -> impl Iterator<Item = (Op::FTy,)>
where
Op: MathOp + HasDomain<Op::FTy>,
IntTy<Op::FTy>: TryFrom<usize>,
{
let domain = Op::DOMAIN;
let start = domain.range_start();
let end = domain.range_end();
let steps = OpITy::<Op>::try_from(NTESTS).unwrap_or(OpITy::<Op>::MAX);
logspace(start, end, steps).map(|v| (v,))
}

View file

@ -1,5 +1,6 @@
#![allow(clippy::unusual_byte_groupings)] // sometimes we group by sign_exp_sig
pub mod domain;
mod f8_impl;
pub mod gen;
#[cfg(feature = "test-multiprecision")]

View file

@ -2,11 +2,14 @@
#![cfg(feature = "test-multiprecision")]
use libm_test::gen::{CachedInput, random};
use libm_test::domain::HasDomain;
use libm_test::gen::{CachedInput, domain_logspace, random};
use libm_test::mpfloat::MpOp;
use libm_test::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, MathOp, TupleCall};
use libm_test::{
CheckBasis, CheckCtx, CheckOutput, GenerateInput, MathOp, OpFTy, OpRustFn, OpRustRet, TupleCall,
};
/// Implement a test against MPFR with random inputs.
/// Test against MPFR with random inputs.
macro_rules! mp_rand_tests {
(
fn_name: $fn_name:ident,
@ -16,13 +19,14 @@ macro_rules! mp_rand_tests {
#[test]
$(#[$meta])*
fn [< mp_random_ $fn_name >]() {
test_one::<libm_test::op::$fn_name::Routine>();
test_one_random::<libm_test::op::$fn_name::Routine>();
}
}
};
}
fn test_one<Op>()
/// Test a single routine with random inputs
fn test_one_random<Op>()
where
Op: MathOp + MpOp,
CachedInput: GenerateInput<Op::RustArgs>,
@ -67,3 +71,90 @@ libm_macros::for_each_function! {
nextafterf,
],
}
/// Test against MPFR with generators from a domain.
macro_rules! mp_domain_tests {
(
fn_name: $fn_name:ident,
attrs: [$($meta:meta)*]
) => {
paste::paste! {
#[test]
$(#[$meta])*
fn [< mp_logspace_ $fn_name >]() {
type Op = libm_test::op::$fn_name::Routine;
domain_test_runner::<Op>(domain_logspace::get_test_cases::<Op>());
}
}
};
}
/// Test a single routine against domaine-aware inputs.
fn domain_test_runner<Op>(cases: impl Iterator<Item = (Op::FTy,)>)
where
// Complicated generics...
// The operation must take a single float argument (unary only)
Op: MathOp<RustArgs = (<Op as MathOp>::FTy,)>,
// It must also support multiprecision operations
Op: MpOp,
// And it must have a domain specified
Op: HasDomain<Op::FTy>,
// The single float argument tuple must be able to call the `RustFn` and return `RustRet`
(OpFTy<Op>,): TupleCall<OpRustFn<Op>, Output = OpRustRet<Op>>,
{
let mut mp_vals = Op::new_mp();
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
for input in cases {
let mp_res = Op::run(&mut mp_vals, input);
let crate_res = input.call(Op::ROUTINE);
crate_res.validate(mp_res, input, &ctx).unwrap();
}
}
libm_macros::for_each_function! {
callback: mp_domain_tests,
attributes: [],
skip: [
// Functions with multiple inputs
atan2,
atan2f,
copysign,
copysignf,
fdim,
fdimf,
fma,
fmaf,
fmax,
fmaxf,
fmin,
fminf,
fmod,
fmodf,
hypot,
hypotf,
jn,
jnf,
ldexp,
ldexpf,
nextafter,
nextafterf,
pow,
powf,
remainder,
remainderf,
remquo,
remquof,
scalbn,
scalbnf,
// FIXME: MPFR tests needed
frexp,
frexpf,
ilogb,
ilogbf,
modf,
modff,
],
}