|
| 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