Skip to content

Commit 83835d0

Browse files
authored
Merge pull request rust-lang#348 from tgross35/function-domains
Introduce generators that respect function domains
2 parents 400e196 + f4d97cd commit 83835d0

File tree

15 files changed

+1694
-57
lines changed

15 files changed

+1694
-57
lines changed

ci/run.sh

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,26 @@ esac
6262
cargo check --no-default-features
6363
cargo check --features "force-soft-floats"
6464

65+
# Always enable `unstable-float` since it expands available API but does not
66+
# change any implementations.
67+
extra_flags="$extra_flags --features unstable-float"
68+
6569
if [ "${BUILD_ONLY:-}" = "1" ]; then
6670
cmd="cargo build --target $target --package libm"
6771
$cmd
68-
$cmd --features "unstable-intrinsics"
72+
$cmd --features unstable-intrinsics
6973

7074
echo "can't run tests on $target; skipping"
7175
else
7276
cmd="cargo test --all --target $target $extra_flags"
7377

74-
# stable by default
78+
# Test without intrinsics
7579
$cmd
7680
$cmd --release
7781

78-
# unstable with a feature
79-
$cmd --features "unstable-intrinsics"
80-
$cmd --release --features "unstable-intrinsics"
82+
# Test with intrinsic use
83+
$cmd --features unstable-intrinsics
84+
$cmd --release --features unstable-intrinsics
8185

8286
# Make sure benchmarks have correct results
8387
$cmd --benches
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//! Program to write all inputs from a generator to a file, then invoke a Julia script to plot
2+
//! them. Output is in `target/plots`.
3+
//!
4+
//! Requires Julia with the `CairoMakie` dependency.
5+
//!
6+
//! Note that running in release mode by default generates a _lot_ more datapoints, which
7+
//! causes plotting to be extremely slow (some simplification to be done in the script).
8+
9+
use std::fmt::Write as _;
10+
use std::io::{BufWriter, Write};
11+
use std::path::Path;
12+
use std::process::Command;
13+
use std::{env, fs};
14+
15+
use libm_test::domain::HasDomain;
16+
use libm_test::gen::{domain_logspace, edge_cases};
17+
use libm_test::{MathOp, op};
18+
19+
const JL_PLOT: &str = "examples/plot_file.jl";
20+
21+
fn main() {
22+
let manifest_env = env::var("CARGO_MANIFEST_DIR").unwrap();
23+
let manifest_dir = Path::new(&manifest_env);
24+
let out_dir = manifest_dir.join("../../target/plots");
25+
if !out_dir.exists() {
26+
fs::create_dir(&out_dir).unwrap();
27+
}
28+
29+
let jl_script = manifest_dir.join(JL_PLOT);
30+
let mut config = format!(r#"out_dir = "{}""#, out_dir.display());
31+
config.write_str("\n\n").unwrap();
32+
33+
// Plot a few domains with some functions that use them.
34+
plot_one_operator::<op::sqrtf::Routine>(&out_dir, &mut config);
35+
plot_one_operator::<op::cosf::Routine>(&out_dir, &mut config);
36+
plot_one_operator::<op::cbrtf::Routine>(&out_dir, &mut config);
37+
38+
let config_path = out_dir.join("config.toml");
39+
fs::write(&config_path, config).unwrap();
40+
41+
// The script expects a path to `config.toml` to be passed as its only argument
42+
let mut cmd = Command::new("julia");
43+
if cfg!(optimizations_enabled) {
44+
cmd.arg("-O3");
45+
}
46+
cmd.arg(jl_script).arg(config_path);
47+
48+
println!("launching script... {cmd:?}");
49+
cmd.status().unwrap();
50+
}
51+
52+
/// Run multiple generators for a single operator.
53+
fn plot_one_operator<Op>(out_dir: &Path, config: &mut String)
54+
where
55+
Op: MathOp<FTy = f32> + HasDomain<f32>,
56+
{
57+
plot_one_generator(
58+
out_dir,
59+
Op::BASE_NAME.as_str(),
60+
"logspace",
61+
config,
62+
domain_logspace::get_test_cases::<Op>(),
63+
);
64+
plot_one_generator(
65+
out_dir,
66+
Op::BASE_NAME.as_str(),
67+
"edge_cases",
68+
config,
69+
edge_cases::get_test_cases::<Op, _>(),
70+
);
71+
}
72+
73+
/// Plot the output of a single generator.
74+
fn plot_one_generator(
75+
out_dir: &Path,
76+
fn_name: &str,
77+
gen_name: &str,
78+
config: &mut String,
79+
gen: impl Iterator<Item = (f32,)>,
80+
) {
81+
let text_file = out_dir.join(format!("input-{fn_name}-{gen_name}.txt"));
82+
83+
let f = fs::File::create(&text_file).unwrap();
84+
let mut w = BufWriter::new(f);
85+
let mut count = 0u64;
86+
87+
for input in gen {
88+
writeln!(w, "{:e}", input.0).unwrap();
89+
count += 1;
90+
}
91+
92+
w.flush().unwrap();
93+
println!("generated {count} inputs for {fn_name}-{gen_name}");
94+
95+
writeln!(
96+
config,
97+
r#"[[input]]
98+
function = "{fn_name}"
99+
generator = "{gen_name}"
100+
input_file = "{}"
101+
"#,
102+
text_file.to_str().unwrap()
103+
)
104+
.unwrap()
105+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"A quick script for plotting a list of floats.
2+
3+
Takes a path to a TOML file (Julia has builtin TOML support but not JSON) which
4+
specifies a list of source files to plot. Plots are done with both a linear and
5+
a log scale.
6+
7+
Requires [Makie] (specifically CairoMakie) for plotting.
8+
9+
[Makie]: https://docs.makie.org/stable/
10+
"
11+
12+
using CairoMakie
13+
using TOML
14+
15+
function main()::Nothing
16+
CairoMakie.activate!(px_per_unit=10)
17+
config_path = ARGS[1]
18+
19+
cfg = Dict()
20+
open(config_path, "r") do f
21+
cfg = TOML.parse(f)
22+
end
23+
24+
out_dir = cfg["out_dir"]
25+
for input in cfg["input"]
26+
fn_name = input["function"]
27+
gen_name = input["generator"]
28+
input_file = input["input_file"]
29+
30+
plot_one(input_file, out_dir, fn_name, gen_name)
31+
end
32+
end
33+
34+
"Read inputs from a file, create both linear and log plots for one function"
35+
function plot_one(
36+
input_file::String,
37+
out_dir::String,
38+
fn_name::String,
39+
gen_name::String,
40+
)::Nothing
41+
fig = Figure()
42+
43+
lin_out_file = joinpath(out_dir, "plot-$fn_name-$gen_name.png")
44+
log_out_file = joinpath(out_dir, "plot-$fn_name-$gen_name-log.png")
45+
46+
# Map string function names to callable functions
47+
if fn_name == "cos"
48+
orig_func = cos
49+
xlims = (-6.0, 6.0)
50+
xlims_log = (-pi * 10, pi * 10)
51+
elseif fn_name == "cbrt"
52+
orig_func = cbrt
53+
xlims = (-2.0, 2.0)
54+
xlims_log = (-1000.0, 1000.0)
55+
elseif fn_name == "sqrt"
56+
orig_func = sqrt
57+
xlims = (-1.1, 6.0)
58+
xlims_log = (-1.1, 5000.0)
59+
else
60+
println("unrecognized function name `$fn_name`; update plot_file.jl")
61+
exit(1)
62+
end
63+
64+
# Edge cases don't do much beyond +/-1, except for infinity.
65+
if gen_name == "edge_cases"
66+
xlims = (-1.1, 1.1)
67+
xlims_log = (-1.1, 1.1)
68+
end
69+
70+
# Turn domain errors into NaN
71+
func(x) = map_or(x, orig_func, NaN)
72+
73+
# Parse a series of X values produced by the generator
74+
inputs = readlines(input_file)
75+
gen_x = map((v) -> parse(Float32, v), inputs)
76+
77+
do_plot(
78+
fig, gen_x, func, xlims[1], xlims[2],
79+
"$fn_name $gen_name (linear scale)",
80+
lin_out_file, false,
81+
)
82+
83+
do_plot(
84+
fig, gen_x, func, xlims_log[1], xlims_log[2],
85+
"$fn_name $gen_name (log scale)",
86+
log_out_file, true,
87+
)
88+
end
89+
90+
"Create a single plot"
91+
function do_plot(
92+
fig::Figure,
93+
gen_x::Vector{F},
94+
func::Function,
95+
xmin::AbstractFloat,
96+
xmax::AbstractFloat,
97+
title::String,
98+
out_file::String,
99+
logscale::Bool,
100+
)::Nothing where F<:AbstractFloat
101+
println("plotting $title")
102+
103+
# `gen_x` is the values the generator produces. `actual_x` is for plotting a
104+
# continuous function.
105+
input_min = xmin - 1.0
106+
input_max = xmax + 1.0
107+
gen_x = filter((v) -> v >= input_min && v <= input_max, gen_x)
108+
markersize = length(gen_x) < 10_000 ? 6.0 : 4.0
109+
110+
steps = 10_000
111+
if logscale
112+
r = LinRange(symlog10(input_min), symlog10(input_max), steps)
113+
actual_x = sympow10.(r)
114+
xscale = Makie.pseudolog10
115+
else
116+
actual_x = LinRange(input_min, input_max, steps)
117+
xscale = identity
118+
end
119+
120+
gen_y = @. func(gen_x)
121+
actual_y = @. func(actual_x)
122+
123+
ax = Axis(fig[1, 1], xscale=xscale, title=title)
124+
125+
lines!(
126+
ax, actual_x, actual_y, color=(:lightblue, 0.6),
127+
linewidth=6.0, label="true function",
128+
)
129+
scatter!(
130+
ax, gen_x, gen_y, color=(:darkblue, 0.9),
131+
markersize=markersize, label="checked inputs",
132+
)
133+
axislegend(ax, position=:rb, framevisible=false)
134+
135+
save(out_file, fig)
136+
delete!(ax)
137+
end
138+
139+
"Apply a function, returning the default if there is a domain error"
140+
function map_or(
141+
input::AbstractFloat,
142+
f::Function,
143+
default::Any
144+
)::Union{AbstractFloat,Any}
145+
try
146+
return f(input)
147+
catch
148+
return default
149+
end
150+
end
151+
152+
# Operations for logarithms that are symmetric about 0
153+
C = 10
154+
symlog10(x::Number) = sign(x) * (log10(1 + abs(x)/(10^C)))
155+
sympow10(x::Number) = (10^C) * (10^x - 1)
156+
157+
main()

0 commit comments

Comments
 (0)