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

Commit a114878

Browse files
committed
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.
1 parent 842b08b commit a114878

File tree

5 files changed

+327
-5
lines changed

5 files changed

+327
-5
lines changed

crates/libm-test/src/domain.rs

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

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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//! A generator that produces logarithmically spaced values within domain bounds.
2+
3+
use libm::support::{IntTy, MinInt};
4+
5+
use crate::domain::HasDomain;
6+
use crate::op::OpITy;
7+
use crate::{MathOp, logspace};
8+
9+
/// Number of tests to run.
10+
// FIXME(ntests): replace this with a more logical algorithm
11+
const NTESTS: usize = {
12+
if cfg!(optimizations_enabled) {
13+
if crate::emulated()
14+
|| !cfg!(target_pointer_width = "64")
15+
|| cfg!(all(target_arch = "x86_64", target_vendor = "apple"))
16+
{
17+
// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run
18+
// in QEMU.
19+
100_000
20+
} else {
21+
5_000_000
22+
}
23+
} else {
24+
// Without optimizations just run a quick check
25+
800
26+
}
27+
};
28+
29+
/// Create a range of logarithmically spaced inputs within a function's domain.
30+
///
31+
/// This allows us to get reasonably thorough coverage without wasting time on values that are
32+
/// NaN or out of range. Random tests will still cover values that are excluded here.
33+
pub fn get_test_cases<Op>() -> impl Iterator<Item = (Op::FTy,)>
34+
where
35+
Op: MathOp + HasDomain<Op::FTy>,
36+
IntTy<Op::FTy>: TryFrom<usize>,
37+
{
38+
let domain = Op::DOMAIN;
39+
let start = domain.range_start();
40+
let end = domain.range_end();
41+
let steps = OpITy::<Op>::try_from(NTESTS).unwrap_or(OpITy::<Op>::MAX);
42+
logspace(start, end, steps).map(|v| (v,))
43+
}

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: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
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;
7-
use libm_test::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, MathOp, TupleCall};
8+
use libm_test::{
9+
CheckBasis, CheckCtx, CheckOutput, GenerateInput, MathOp, OpFTy, OpRustFn, OpRustRet, TupleCall,
10+
};
811

9-
/// Implement a test against MPFR with random inputs.
12+
/// Test against MPFR with random inputs.
1013
macro_rules! mp_rand_tests {
1114
(
1215
fn_name: $fn_name:ident,
@@ -16,13 +19,14 @@ macro_rules! mp_rand_tests {
1619
#[test]
1720
$(#[$meta])*
1821
fn [< mp_random_ $fn_name >]() {
19-
test_one::<libm_test::op::$fn_name::Routine>();
22+
test_one_random::<libm_test::op::$fn_name::Routine>();
2023
}
2124
}
2225
};
2326
}
2427

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

0 commit comments

Comments
 (0)