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:
parent
163ed2a133
commit
a8a2f70ae6
5 changed files with 327 additions and 5 deletions
186
library/compiler-builtins/libm/crates/libm-test/src/domain.rs
Normal file
186
library/compiler-builtins/libm/crates/libm-test/src/domain.rs
Normal 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;
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,))
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue