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")]