Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit 34b6e2f

Browse files
committed
Add interfaces and a tests for function domains
Create a type representing a function's domain and a test that does a logarithmic space of points within the domain.
1 parent 7f3cd9a commit 34b6e2f

File tree

5 files changed

+331
-4
lines changed

5 files changed

+331
-4
lines changed

crates/libm-test/src/domain.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//! Traits and operations related to bounds of a function.
2+
3+
use std::fmt;
4+
use std::ops::{self, Bound};
5+
6+
use crate::Float;
7+
8+
/// Representation of a function's domain.
9+
pub struct Domain<T> {
10+
/// Start of the region for which a function is defined (ignoring poles).
11+
pub start: Bound<T>,
12+
/// Endof the region for which a function is defined (ignoring poles).
13+
pub end: Bound<T>,
14+
/// Additional points to check closer around. These can be e.g. undefined asymptotes or
15+
/// inflection points.
16+
pub check_points: Option<fn() -> BoxIter<T>>,
17+
}
18+
19+
type BoxIter<T> = Box<dyn Iterator<Item = T>>;
20+
21+
impl<F: Float> Domain<F> {
22+
pub fn range_start(&self) -> F {
23+
match self.start {
24+
Bound::Included(v) | Bound::Excluded(v) => v,
25+
Bound::Unbounded => F::NEG_INFINITY,
26+
}
27+
}
28+
29+
pub fn range_end(&self) -> F {
30+
match self.end {
31+
Bound::Included(v) | Bound::Excluded(v) => v,
32+
Bound::Unbounded => F::INFINITY,
33+
}
34+
}
35+
}
36+
37+
impl<F: Float> Domain<F> {
38+
/// x ∈ ℝ
39+
pub const UNBOUNDED: Self =
40+
Self { start: Bound::Unbounded, end: Bound::Unbounded, check_points: None };
41+
42+
/// x ∈ ℝ >= 0
43+
pub const POSITIVE: Self =
44+
Self { start: Bound::Included(F::ZERO), end: Bound::Unbounded, check_points: None };
45+
46+
/// x ∈ ℝ > 0
47+
pub const STRICTLY_POSITIVE: Self =
48+
Self { start: Bound::Excluded(F::ZERO), end: Bound::Unbounded, check_points: None };
49+
50+
/// Used for versions of `asin` and `acos`.
51+
pub const INVERSE_TRIG_PERIODIC: Self = Self {
52+
start: Bound::Included(F::NEG_ONE),
53+
end: Bound::Included(F::ONE),
54+
check_points: None,
55+
};
56+
57+
/// Domain for `acosh`
58+
pub const ACOSH: Self =
59+
Self { start: Bound::Included(F::ONE), end: Bound::Unbounded, check_points: None };
60+
61+
/// Domain for `atanh`
62+
pub const ATANH: Self = Self {
63+
start: Bound::Excluded(F::NEG_ONE),
64+
end: Bound::Excluded(F::ONE),
65+
check_points: None,
66+
};
67+
68+
/// Domain for `sin`, `cos`, and `tan`
69+
pub const TRIG: Self = Self {
70+
// TODO
71+
check_points: Some(|| Box::new([-F::PI, -F::FRAC_PI_2, F::FRAC_PI_2, F::PI].into_iter())),
72+
..Self::UNBOUNDED
73+
};
74+
75+
/// Domain for `log` in various bases
76+
pub const LOG: Self = Self::STRICTLY_POSITIVE;
77+
78+
/// Domain for `log1p` i.e. `log(1 + x)`
79+
pub const LOG1P: Self =
80+
Self { start: Bound::Excluded(F::NEG_ONE), end: Bound::Unbounded, check_points: None };
81+
82+
/// Domain for `sqrt`
83+
pub const SQRT: Self = Self::POSITIVE;
84+
85+
pub const GAMMA: Self = Self {
86+
check_points: Some(|| {
87+
// Negative integers are asymptotes
88+
Box::new((0..u8::MAX).map(|scale| {
89+
let mut base = F::ZERO;
90+
for _ in 0..scale {
91+
base = base - F::ONE;
92+
}
93+
base
94+
}))
95+
}),
96+
// Whether or not gamma is defined for negative numbers is implementation dependent
97+
..Self::UNBOUNDED
98+
};
99+
100+
pub const LGAMMA: Self = Self::STRICTLY_POSITIVE;
101+
}
102+
103+
/// Implement on `op::*` types to indicate how they are bounded.
104+
pub trait HasDomain<T>
105+
where
106+
T: Copy + fmt::Debug + ops::Add<Output = T> + ops::Sub<Output = T> + PartialOrd + 'static,
107+
{
108+
const D: Domain<T>;
109+
}
110+
111+
/// Implement [`HasDomain`] for both the `f32` and `f64` variants of a function.
112+
macro_rules! impl_has_domain {
113+
($($fn_name:ident => $domain:expr;)*) => {
114+
paste::paste! {
115+
$(
116+
// Implement for f64 functions
117+
impl HasDomain<f64> for $crate::op::$fn_name::Routine {
118+
const D: Domain<f64> = Domain::<f64>::$domain;
119+
}
120+
121+
// Implement for f32 functions
122+
impl HasDomain<f32> for $crate::op::[< $fn_name f >]::Routine {
123+
const D: Domain<f32> = Domain::<f32>::$domain;
124+
}
125+
)*
126+
}
127+
};
128+
}
129+
130+
// Tie functions together with their domains.
131+
impl_has_domain! {
132+
acos => INVERSE_TRIG_PERIODIC;
133+
acosh => ACOSH;
134+
asin => INVERSE_TRIG_PERIODIC;
135+
asinh => UNBOUNDED;
136+
atan => UNBOUNDED;
137+
atanh => ATANH;
138+
cbrt => UNBOUNDED;
139+
ceil => UNBOUNDED;
140+
cos => TRIG;
141+
cosh => UNBOUNDED;
142+
erf => UNBOUNDED;
143+
exp => UNBOUNDED;
144+
exp10 => UNBOUNDED;
145+
exp2 => UNBOUNDED;
146+
expm1 => UNBOUNDED;
147+
fabs => UNBOUNDED;
148+
floor => UNBOUNDED;
149+
frexp => UNBOUNDED;
150+
ilogb => UNBOUNDED;
151+
j0 => UNBOUNDED;
152+
j1 => UNBOUNDED;
153+
lgamma => LGAMMA;
154+
log => LOG;
155+
log10 => LOG;
156+
log1p => LOG1P;
157+
log2 => LOG;
158+
modf => UNBOUNDED;
159+
rint => UNBOUNDED;
160+
round => UNBOUNDED;
161+
sin => TRIG;
162+
sincos => TRIG;
163+
sinh => UNBOUNDED;
164+
sqrt => SQRT;
165+
tan => TRIG;
166+
tanh => UNBOUNDED;
167+
tgamma => GAMMA;
168+
trunc => UNBOUNDED;
169+
}
170+
171+
impl HasDomain<f32> for crate::op::lgammaf_r::Routine {
172+
const D: Domain<f32> = Domain::<f32>::LGAMMA;
173+
}
174+
175+
impl HasDomain<f64> for crate::op::lgamma_r::Routine {
176+
const D: Domain<f64> = Domain::<f64>::LGAMMA;
177+
}

crates/libm-test/src/gen.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Different generators that can create random or systematic bit patterns.
22
33
use crate::GenerateInput;
4+
pub mod domain_logspace;
45
pub mod random;
56

67
/// Helper type to turn any reusable input into a generator.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//! A generator that makes use of domain bounds.
2+
3+
use std::ops::Bound;
4+
5+
use libm::support::{Float, MinInt};
6+
7+
use crate::domain::{Domain, HasDomain};
8+
use crate::{MathOp, logspace};
9+
10+
/// Number of tests to run.
11+
// FIXME(ntests): replace this with a more logical algorithm
12+
const NTESTS: usize = {
13+
if cfg!(optimizations_enabled) {
14+
if crate::emulated()
15+
|| !cfg!(target_pointer_width = "64")
16+
|| cfg!(all(target_arch = "x86_64", target_vendor = "apple"))
17+
{
18+
// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run
19+
// in QEMU.
20+
100_000
21+
} else {
22+
5_000_000
23+
}
24+
} else {
25+
// Without optimizations just run a quick check
26+
800
27+
}
28+
};
29+
30+
/// Create a range of logarithmically spaced inputs within a function's domain.
31+
///
32+
/// This allows us to get reasonably thorough coverage while avoiding values that are NaN or out
33+
/// of range. Random tests will still cover these values that are not included here.
34+
pub fn get_test_cases<Op>() -> impl Iterator<Item = (Op::FTy,)>
35+
where
36+
Op: MathOp + HasDomain<Op::FTy>,
37+
<Op::FTy as Float>::Int: TryFrom<usize>,
38+
{
39+
get_test_cases_inner::<Op::FTy>(Op::D)
40+
}
41+
42+
pub fn get_test_cases_inner<F>(domain: Domain<F>) -> impl Iterator<Item = (F,)>
43+
where
44+
F: Float<Int: TryFrom<usize>>,
45+
{
46+
// We generate logspaced inputs within a specific range, excluding values that are out of
47+
// range in order to make iterations useful (random tests still cover the full range).
48+
let range_start = match domain.start {
49+
Bound::Included(v) | Bound::Excluded(v) => v,
50+
Bound::Unbounded => F::NEG_INFINITY,
51+
};
52+
let range_end = match domain.end {
53+
Bound::Included(v) | Bound::Excluded(v) => v,
54+
Bound::Unbounded => F::INFINITY,
55+
};
56+
57+
let steps = F::Int::try_from(NTESTS).unwrap_or(F::Int::MAX);
58+
logspace(range_start, range_end, steps).map(|v| (v,))
59+
}

crates/libm-test/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![allow(clippy::unusual_byte_groupings)] // sometimes we group by sign_exp_sig
22

3+
pub mod domain;
34
mod f8_impl;
45
pub mod gen;
56
#[cfg(feature = "test-multiprecision")]

crates/libm-test/tests/multiprecision.rs

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
33
#![cfg(feature = "test-multiprecision")]
44

5-
use libm_test::gen::{CachedInput, random};
5+
use libm_test::domain::HasDomain;
6+
use libm_test::gen::{CachedInput, domain_logspace, random};
67
use libm_test::mpfloat::MpOp;
78
use libm_test::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, MathOp, TupleCall};
89

9-
/// Implement a test against MPFR with random inputs.
10+
/// Test against MPFR with random inputs.
1011
macro_rules! mp_rand_tests {
1112
(
1213
fn_name: $fn_name:ident,
@@ -16,13 +17,14 @@ macro_rules! mp_rand_tests {
1617
#[test]
1718
$(#[$meta])*
1819
fn [< mp_random_ $fn_name >]() {
19-
test_one::<libm_test::op::$fn_name::Routine>();
20+
test_one_random::<libm_test::op::$fn_name::Routine>();
2021
}
2122
}
2223
};
2324
}
2425

25-
fn test_one<Op>()
26+
/// Test a single routine with random inputs
27+
fn test_one_random<Op>()
2628
where
2729
Op: MathOp + MpOp,
2830
CachedInput: GenerateInput<Op::RustArgs>,
@@ -67,3 +69,90 @@ libm_macros::for_each_function! {
6769
nextafterf,
6870
],
6971
}
72+
73+
/// Test against MPFR with generators from a domain.
74+
macro_rules! mp_domain_tests {
75+
(
76+
fn_name: $fn_name:ident,
77+
attrs: [$($meta:meta)*]
78+
) => {
79+
paste::paste! {
80+
#[test]
81+
$(#[$meta])*
82+
fn [< mp_logspace_ $fn_name >]() {
83+
type Op = libm_test::op::$fn_name::Routine;
84+
domain_test_runner::<Op>(domain_logspace::get_test_cases::<Op>());
85+
}
86+
}
87+
};
88+
}
89+
90+
/// Test a single routine against domaine-aware inputs.
91+
fn domain_test_runner<Op>(cases: impl Iterator<Item = (Op::FTy,)>)
92+
where
93+
// Complicated generics...
94+
// The operation must take a single float argument (unary only)
95+
Op: MathOp<RustArgs = (<Op as MathOp>::FTy,)>,
96+
// It must also support multiprecision operations
97+
Op: MpOp,
98+
// And it must have a domain specified
99+
Op: HasDomain<Op::FTy>,
100+
// The single float argument tuple must be able to call the `RustFn` and return `RustRet`
101+
(<Op as MathOp>::FTy,): TupleCall<<Op as MathOp>::RustFn, Output = <Op as MathOp>::RustRet>,
102+
{
103+
let mut mp_vals = Op::new_mp();
104+
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
105+
106+
for input in cases {
107+
let mp_res = Op::run(&mut mp_vals, input);
108+
let crate_res = input.call(Op::ROUTINE);
109+
110+
crate_res.validate(mp_res, input, &ctx).unwrap();
111+
}
112+
}
113+
114+
libm_macros::for_each_function! {
115+
callback: mp_domain_tests,
116+
attributes: [],
117+
skip: [
118+
// Functions with multiple inputs
119+
atan2,
120+
atan2f,
121+
copysign,
122+
copysignf,
123+
fdim,
124+
fdimf,
125+
fma,
126+
fmaf,
127+
fmax,
128+
fmaxf,
129+
fmin,
130+
fminf,
131+
fmod,
132+
fmodf,
133+
hypot,
134+
hypotf,
135+
jn,
136+
jnf,
137+
ldexp,
138+
ldexpf,
139+
nextafter,
140+
nextafterf,
141+
pow,
142+
powf,
143+
remainder,
144+
remainderf,
145+
remquo,
146+
remquof,
147+
scalbn,
148+
scalbnf,
149+
150+
// FIXME: MPFR tests needed
151+
frexp,
152+
frexpf,
153+
ilogb,
154+
ilogbf,
155+
modf,
156+
modff,
157+
],
158+
}

0 commit comments

Comments
 (0)