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

Commit 39e3127

Browse files
committed
Basic domain
Fix up traits Wip up down Tests pass! Cleanup Enable full test range More tests Code cleanup More tests, all pass Cleanup, passing Add visualization scripts Add visualization scripts updates to script clippy clippy clippy Docs update docs docs First pass at wiring things up todo -> unimplemented for f8 since we probably won't implement anything
1 parent 754dace commit 39e3127

File tree

11 files changed

+1565
-5
lines changed

11 files changed

+1565
-5
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77
Cargo.lock
88
musl/
99
**.tar.gz
10+
11+
# Files generated by binaries
12+
build/
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//! Program to write all inputs from a generator to a file, then invoke a Julia script
2+
//! to plot them. Requires Julia with the `CairoMakie` dependency.
3+
//!
4+
//! Note that running in release mode by default generates a _lot_ more datapoints, which
5+
//! causes plotting to be extremely slow (some simplification to be done in the script).
6+
7+
use std::io::{BufWriter, Write};
8+
use std::path::Path;
9+
use std::process::Command;
10+
use std::{env, fs};
11+
12+
use libm_test::domain::{Domain, SqrtDomain, TrigDomain, Unbounded};
13+
use libm_test::gen::domain;
14+
15+
const JL_PLOT: &str = "examples/plot_file.jl";
16+
17+
fn main() {
18+
let out_dir = Path::new("build");
19+
if !out_dir.exists() {
20+
fs::create_dir(out_dir).unwrap();
21+
}
22+
23+
let jl_script = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join(JL_PLOT);
24+
let mut j_args = Vec::new();
25+
26+
// Plot a few domains with some functions that use them.
27+
plot_one::<SqrtDomain>(out_dir, "sqrt", &mut j_args);
28+
plot_one::<TrigDomain>(out_dir, "cos", &mut j_args);
29+
plot_one::<Unbounded>(out_dir, "cbrt", &mut j_args);
30+
31+
println!("launching script");
32+
let mut cmd = Command::new("julia");
33+
if !cfg!(debug_assertions) {
34+
cmd.arg("-O3");
35+
}
36+
cmd.arg(jl_script).args(j_args).status().unwrap();
37+
}
38+
39+
/// Plot a single domain.
40+
fn plot_one<D: Domain<f32>>(out_dir: &Path, name: &str, j_args: &mut Vec<String>) {
41+
let base_name = out_dir.join(format!("domain-inputs-{name}"));
42+
let text_file = base_name.with_extension("txt");
43+
44+
{
45+
// Scope for file and writer
46+
let f = fs::File::create(&text_file).unwrap();
47+
let mut w = BufWriter::new(f);
48+
49+
for input in domain::get_test_cases_for_domain::<f32, D>() {
50+
writeln!(w, "{:e}", input.0).unwrap();
51+
}
52+
w.flush().unwrap();
53+
}
54+
55+
// The julia script expects `name1 path1 name2 path2...` args
56+
j_args.push(name.to_owned());
57+
j_args.push(base_name.to_str().unwrap().to_owned());
58+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"A quick script to for plotting a list of floats.
2+
3+
Takes a list of floats and a real function, plots both on a graph in both
4+
linear and log scale. Requires [Makie] (specifically CairoMakie) for plotting.
5+
6+
[Makie]: https://docs.makie.org/stable/
7+
"
8+
9+
using CairoMakie
10+
11+
CairoMakie.activate!(px_per_unit=10)
12+
13+
"Apply a function, returning the default if there is a domain error"
14+
function map_or_default(
15+
input::AbstractFloat,
16+
f::Function,
17+
default::AbstractFloat
18+
)::AbstractFloat
19+
try
20+
return f(input)
21+
catch
22+
return default
23+
end
24+
end
25+
26+
"Read inputs from a file, create both linear and log plots"
27+
function plot_one(
28+
fig::Figure,
29+
base_name::String,
30+
fn_name::String,
31+
f::Function;
32+
xlims::Union{Tuple{Any,Any},Nothing},
33+
xlims_log::Union{Tuple{Any,Any},Nothing},
34+
)::Nothing
35+
float_file = "$base_name.txt"
36+
lin_out_file = "$base_name.png"
37+
log_out_file = "$base_name-log.png"
38+
39+
if xlims === nothing
40+
xlims = (-6, 6)
41+
end
42+
if xlims_log === nothing
43+
xlims_log = (xlims[1] * 500, xlims[2] * 500)
44+
end
45+
46+
inputs = readlines(float_file)
47+
48+
# Parse floats
49+
x = map((v) -> parse(Float32, v), inputs)
50+
# Apply function to the test points
51+
y = map((v) -> map_or_default(v, f, 0.0), x)
52+
53+
# Plot the scatter plot of our checked values as well as the continuous function
54+
ax = Axis(fig[1, 1], limits=(xlims, nothing), title=fn_name)
55+
lines!(ax, xlims[1] .. xlims[2], f, color=(:blue, 0.4))
56+
scatter!(ax, x, y, color=(:green, 0.3))
57+
save(lin_out_file, fig)
58+
delete!(ax)
59+
60+
# Same as above on a log scale
61+
ax = Axis(
62+
fig[1, 1],
63+
limits=(xlims_log, nothing),
64+
xscale=Makie.pseudolog10,
65+
title="$fn_name (log scale)"
66+
)
67+
lines!(ax, xlims_log[1] .. xlims_log[2], f, color=(:blue, 0.4))
68+
scatter!(ax, x, y, color=(:green, 0.3))
69+
save(log_out_file, fig)
70+
delete!(ax)
71+
end
72+
73+
# Args alternate `name1 path1 name2 path2`
74+
fn_names = ARGS[1:2:end]
75+
base_names = ARGS[2:2:end]
76+
77+
for idx in eachindex(fn_names)
78+
fn_name = fn_names[idx]
79+
base_name = base_names[idx]
80+
81+
xlims = nothing
82+
xlims_log = nothing
83+
84+
fig = Figure()
85+
86+
# Map string function names to callable functions
87+
if fn_name == "cos"
88+
f = cos
89+
xlims_log = (-pi * 10, pi * 10)
90+
elseif fn_name == "cbrt"
91+
f = cbrt
92+
xlims = (-2.0, 2.0)
93+
elseif fn_name == "sqrt"
94+
f = sqrt
95+
xlims = (-1.1, 6.0)
96+
xlims_log = (-1.1, 5000.0)
97+
else
98+
println("unrecognized function name `$fn_name`; update plot_file.jl")
99+
end
100+
101+
println("plotting $fn_name")
102+
plot_one(
103+
fig,
104+
base_name,
105+
fn_name,
106+
f,
107+
xlims=xlims,
108+
xlims_log=xlims_log,
109+
)
110+
end
111+
112+
base_name = ARGS[1]
113+
fn_name = ARGS[2]

crates/libm-test/src/domain.rs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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+
/// A trait to be implemented on types representing a function's domain.
9+
///
10+
/// Since multiple functions share the same domain, this doesn't get implemented on the `op::*`
11+
/// type. Instead, this gets applied to a new unit struct, then the `op::*` type should
12+
/// implement `HasDomain`.
13+
pub trait Domain<T>
14+
where
15+
T: Copy + fmt::Debug + ops::Add<Output = T> + ops::Sub<Output = T> + PartialOrd + 'static,
16+
{
17+
/// The region for which the function is defined. Ignores poles.
18+
const DEFINED: (Bound<T>, Bound<T>);
19+
20+
/// The region, if any, for which the function repeats. Used to test within.
21+
const PERIODIC: Option<(Bound<T>, Bound<T>)> = None;
22+
23+
/// Asymptotes that have a defined value in the float function (but probably not the
24+
/// mathematical function). Returns an `(input, ouptut)` mapping.
25+
fn defined_asymptotes() -> impl Iterator<Item = (T, T)> {
26+
std::iter::empty()
27+
}
28+
29+
/// Additional points to check closer around. These can be e.g. undefined asymptotes or
30+
/// inflection points.
31+
fn check_points() -> impl Iterator<Item = T> {
32+
std::iter::empty()
33+
}
34+
35+
/// How NaNs at certain values should be handled.
36+
fn nan_handling(input: T) -> T {
37+
input
38+
}
39+
40+
/// Helper to get the length of the period in unit `T`.
41+
fn period() -> Option<T> {
42+
Self::PERIODIC?;
43+
let start = Self::period_start();
44+
let end = Self::period_end();
45+
if start > end { Some(start - end) } else { Some(end - start) }
46+
}
47+
48+
/// Helper to get the start of the period. Panics if not period or a bad bound.
49+
fn period_start() -> T {
50+
let start = Self::PERIODIC.unwrap().0;
51+
let (Bound::Included(start) | Bound::Excluded(start)) = start else {
52+
panic!("`Unbounded` in period {:?}", Self::PERIODIC);
53+
};
54+
start
55+
}
56+
57+
/// Helper to get the end of the period. Panics if not period or a bad bound.
58+
fn period_end() -> T {
59+
let end = Self::PERIODIC.unwrap().1;
60+
let (Bound::Included(end) | Bound::Excluded(end)) = end else {
61+
panic!("`Unbounded` in period {:?}", Self::PERIODIC);
62+
};
63+
end
64+
}
65+
}
66+
67+
/* Possible domains */
68+
69+
/// Use for anything basic, no bounds, no asymptotes, etc.
70+
pub struct Unbounded;
71+
impl<F: Float> Domain<F> for Unbounded {
72+
const DEFINED: (Bound<F>, Bound<F>) = unbounded();
73+
}
74+
75+
/// Used for versions of `asin` and `acos`.
76+
pub struct InvTrigPeriodic;
77+
impl<F: Float> Domain<F> for InvTrigPeriodic {
78+
const DEFINED: (Bound<F>, Bound<F>) = (Bound::Included(F::NEG_ONE), Bound::Included(F::ONE));
79+
}
80+
81+
/// Domain for `acosh`
82+
pub struct ACoshDomain;
83+
impl<F: Float> Domain<F> for ACoshDomain {
84+
const DEFINED: (Bound<F>, Bound<F>) = (Bound::Included(F::ONE), Bound::Unbounded);
85+
}
86+
87+
/// Domain for `atanh`
88+
pub struct ATanhDomain;
89+
impl<F: Float> Domain<F> for ATanhDomain {
90+
const DEFINED: (Bound<F>, Bound<F>) = (Bound::Excluded(F::NEG_ONE), Bound::Excluded(F::ONE));
91+
92+
fn defined_asymptotes() -> impl Iterator<Item = (F, F)> {
93+
[(F::NEG_ONE, F::NEG_INFINITY), (F::ONE, F::NEG_INFINITY)].into_iter()
94+
}
95+
}
96+
97+
/// Domain for `sin`, `cos`, and `tan`
98+
pub struct TrigDomain;
99+
impl<F: Float> Domain<F> for TrigDomain {
100+
const DEFINED: (Bound<F>, Bound<F>) = unbounded();
101+
102+
const PERIODIC: Option<(Bound<F>, Bound<F>)> =
103+
Some((Bound::Excluded(F::NEG_PI), Bound::Included(F::PI)));
104+
105+
fn check_points() -> impl Iterator<Item = F> {
106+
[-F::PI, -F::FRAC_PI_2, F::FRAC_PI_2, F::PI].into_iter()
107+
}
108+
}
109+
110+
/// Domain for `log` in various bases
111+
pub struct LogDomain;
112+
impl<F: Float> Domain<F> for LogDomain {
113+
const DEFINED: (Bound<F>, Bound<F>) = strictly_positive();
114+
115+
fn defined_asymptotes() -> impl Iterator<Item = (F, F)> {
116+
[(F::ZERO, F::NEG_INFINITY)].into_iter()
117+
}
118+
}
119+
120+
/// Domain for `log1p` i.e. `log(1 + x)`
121+
pub struct Log1pDomain;
122+
impl<F: Float> Domain<F> for Log1pDomain {
123+
const DEFINED: (Bound<F>, Bound<F>) = (Bound::Excluded(F::NEG_ONE), Bound::Unbounded);
124+
125+
fn defined_asymptotes() -> impl Iterator<Item = (F, F)> {
126+
[(F::NEG_ONE, F::NEG_INFINITY)].into_iter()
127+
}
128+
}
129+
130+
/// Domain for `sqrt`
131+
pub struct SqrtDomain;
132+
impl<F: Float> Domain<F> for SqrtDomain {
133+
const DEFINED: (Bound<F>, Bound<F>) = positive();
134+
}
135+
136+
/// x ∈ ℝ
137+
const fn unbounded<F: Float>() -> (Bound<F>, Bound<F>) {
138+
(Bound::Unbounded, Bound::Unbounded)
139+
}
140+
141+
/// x ∈ ℝ >= 0
142+
const fn positive<F: Float>() -> (Bound<F>, Bound<F>) {
143+
(Bound::Included(F::ZERO), Bound::Unbounded)
144+
}
145+
146+
/// x ∈ ℝ > 0
147+
const fn strictly_positive<F: Float>() -> (Bound<F>, Bound<F>) {
148+
(Bound::Excluded(F::ZERO), Bound::Unbounded)
149+
}
150+
151+
/// Implement on `op::*` types to indicate how they are bounded.
152+
pub trait HasDomain<T>
153+
where
154+
T: Copy + fmt::Debug + ops::Add<Output = T> + ops::Sub<Output = T> + PartialOrd + 'static,
155+
{
156+
type D: Domain<T>;
157+
}
158+
159+
/// Implement [`HasDomain`] for both the `f32` and `f64` variants of a function.
160+
macro_rules! impl_has_domain {
161+
($($fn_name:ident => $domain:ty;)*) => {
162+
paste::paste! {
163+
$(
164+
// Implement for f64 functions
165+
impl HasDomain<f64> for $crate::op::$fn_name::Routine {
166+
type D = $domain;
167+
}
168+
169+
// Implement for f32 functions
170+
impl HasDomain<f32> for $crate::op::[< $fn_name f >]::Routine {
171+
type D = $domain;
172+
}
173+
)*
174+
}
175+
};
176+
}
177+
178+
// Tie functions together with their domains.
179+
impl_has_domain! {
180+
acos => InvTrigPeriodic;
181+
acosh => ACoshDomain;
182+
asin => InvTrigPeriodic;
183+
asinh => Unbounded;
184+
// TODO asymptotes
185+
atan => Unbounded;
186+
atanh => ATanhDomain;
187+
cbrt => Unbounded;
188+
ceil => Unbounded;
189+
cos => TrigDomain;
190+
cosh => Unbounded;
191+
erf => Unbounded;
192+
exp => Unbounded;
193+
exp10 => Unbounded;
194+
exp2 => Unbounded;
195+
expm1 => Unbounded;
196+
fabs => Unbounded;
197+
floor => Unbounded;
198+
frexp => Unbounded;
199+
j0 => Unbounded;
200+
j1 => Unbounded;
201+
log => LogDomain;
202+
log10 => LogDomain;
203+
log1p => Log1pDomain;
204+
log2 => LogDomain;
205+
rint => Unbounded;
206+
round => Unbounded;
207+
sin => TrigDomain;
208+
sinh => Unbounded;
209+
sqrt => SqrtDomain;
210+
tan => TrigDomain;
211+
tanh => Unbounded;
212+
trunc => Unbounded;
213+
}

0 commit comments

Comments
 (0)