diff --git a/.gitignore b/.gitignore index b6a53275..738075ec 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ Cargo.lock musl/ **.tar.gz + +# Files generated by binaries +build/ diff --git a/crates/libm-test/examples/plot_domains.rs b/crates/libm-test/examples/plot_domains.rs new file mode 100644 index 00000000..97b31b28 --- /dev/null +++ b/crates/libm-test/examples/plot_domains.rs @@ -0,0 +1,58 @@ +//! Program to write all inputs from a generator to a file, then invoke a Julia script +//! to plot them. Requires Julia with the `CairoMakie` dependency. +//! +//! Note that running in release mode by default generates a _lot_ more datapoints, which +//! causes plotting to be extremely slow (some simplification to be done in the script). + +use std::io::{BufWriter, Write}; +use std::path::Path; +use std::process::Command; +use std::{env, fs}; + +use libm_test::domain::Domain; +use libm_test::gen::domain_logspace; + +const JL_PLOT: &str = "examples/plot_file.jl"; + +fn main() { + let out_dir = Path::new("build"); + if !out_dir.exists() { + fs::create_dir(out_dir).unwrap(); + } + + let jl_script = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join(JL_PLOT); + let mut j_args = Vec::new(); + + // Plot a few domains with some functions that use them. + plot_one(out_dir, "sqrt", Domain::SQRT, &mut j_args); + plot_one(out_dir, "cos", Domain::TRIG, &mut j_args); + plot_one(out_dir, "cbrt", Domain::UNBOUNDED, &mut j_args); + + println!("launching script"); + let mut cmd = Command::new("julia"); + if !cfg!(debug_assertions) { + cmd.arg("-O3"); + } + cmd.arg(jl_script).args(j_args).status().unwrap(); +} + +/// Plot a single domain. +fn plot_one(out_dir: &Path, name: &str, domain: Domain, j_args: &mut Vec) { + let base_name = out_dir.join(format!("domain-inputs-{name}")); + let text_file = base_name.with_extension("txt"); + + { + // Scope for file and writer + let f = fs::File::create(&text_file).unwrap(); + let mut w = BufWriter::new(f); + + for input in domain_logspace::get_test_cases_inner::(domain) { + writeln!(w, "{:e}", input.0).unwrap(); + } + w.flush().unwrap(); + } + + // The julia script expects `name1 path1 name2 path2...` args + j_args.push(name.to_owned()); + j_args.push(base_name.to_str().unwrap().to_owned()); +} diff --git a/crates/libm-test/examples/plot_file.jl b/crates/libm-test/examples/plot_file.jl new file mode 100644 index 00000000..ccb2da1c --- /dev/null +++ b/crates/libm-test/examples/plot_file.jl @@ -0,0 +1,113 @@ +"A quick script to for plotting a list of floats. + +Takes a list of floats and a real function, plots both on a graph in both +linear and log scale. Requires [Makie] (specifically CairoMakie) for plotting. + +[Makie]: https://docs.makie.org/stable/ +" + +using CairoMakie + +CairoMakie.activate!(px_per_unit=10) + +"Apply a function, returning the default if there is a domain error" +function map_or_default( + input::AbstractFloat, + f::Function, + default::AbstractFloat +)::AbstractFloat + try + return f(input) + catch + return default + end +end + +"Read inputs from a file, create both linear and log plots" +function plot_one( + fig::Figure, + base_name::String, + fn_name::String, + f::Function; + xlims::Union{Tuple{Any,Any},Nothing}, + xlims_log::Union{Tuple{Any,Any},Nothing}, +)::Nothing + float_file = "$base_name.txt" + lin_out_file = "$base_name.png" + log_out_file = "$base_name-log.png" + + if xlims === nothing + xlims = (-6, 6) + end + if xlims_log === nothing + xlims_log = (xlims[1] * 500, xlims[2] * 500) + end + + inputs = readlines(float_file) + + # Parse floats + x = map((v) -> parse(Float32, v), inputs) + # Apply function to the test points + y = map((v) -> map_or_default(v, f, 0.0), x) + + # Plot the scatter plot of our checked values as well as the continuous function + ax = Axis(fig[1, 1], limits=(xlims, nothing), title=fn_name) + lines!(ax, xlims[1] .. xlims[2], f, color=(:blue, 0.4)) + scatter!(ax, x, y, color=(:green, 0.3)) + save(lin_out_file, fig) + delete!(ax) + + # Same as above on a log scale + ax = Axis( + fig[1, 1], + limits=(xlims_log, nothing), + xscale=Makie.pseudolog10, + title="$fn_name (log scale)" + ) + lines!(ax, xlims_log[1] .. xlims_log[2], f, color=(:blue, 0.4)) + scatter!(ax, x, y, color=(:green, 0.3)) + save(log_out_file, fig) + delete!(ax) +end + +# Args alternate `name1 path1 name2 path2` +fn_names = ARGS[1:2:end] +base_names = ARGS[2:2:end] + +for idx in eachindex(fn_names) + fn_name = fn_names[idx] + base_name = base_names[idx] + + xlims = nothing + xlims_log = nothing + + fig = Figure() + + # Map string function names to callable functions + if fn_name == "cos" + f = cos + xlims_log = (-pi * 10, pi * 10) + elseif fn_name == "cbrt" + f = cbrt + xlims = (-2.0, 2.0) + elseif fn_name == "sqrt" + f = sqrt + xlims = (-1.1, 6.0) + xlims_log = (-1.1, 5000.0) + else + println("unrecognized function name `$fn_name`; update plot_file.jl") + end + + println("plotting $fn_name") + plot_one( + fig, + base_name, + fn_name, + f, + xlims=xlims, + xlims_log=xlims_log, + ) +end + +base_name = ARGS[1] +fn_name = ARGS[2]