From f10841d8d0c3aa4895ca17e66dfb311f0a672f89 Mon Sep 17 00:00:00 2001 From: d-monnet <70266099+d-monnet@users.noreply.github.com> Date: Thu, 2 Nov 2023 14:45:42 -0400 Subject: [PATCH] Tikz figure export (#105) * add data matrix export function * add tikz figure export function * update module * add tests * Update src/tikz_export.jl Co-authored-by: tmigot * Update src/tikz_export.jl Co-authored-by: tmigot * Update src/tikz_export.jl Co-authored-by: tmigot * Update src/tikz_export.jl Co-authored-by: tmigot * Update src/performance_profiles.jl Co-authored-by: tmigot * Update src/performance_profiles.jl Co-authored-by: tmigot * update keyword arguments * add figure export options provided by TizkPictures.jl export options are .tikz, .tex, .svg and .pdf * Update src/performance_profiles.jl Co-authored-by: tmigot * add TikzPicture.jl compat * fix logscale argument --------- Co-authored-by: tmigot --- Project.toml | 4 +- src/BenchmarkProfiles.jl | 2 + src/performance_profiles.jl | 50 ++++++++--- src/tikz_export.jl | 168 ++++++++++++++++++++++++++++++++++++ test/runtests.jl | 18 ++++ 5 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 src/tikz_export.jl diff --git a/Project.toml b/Project.toml index 5cfbd64..7bbeea7 100644 --- a/Project.toml +++ b/Project.toml @@ -9,14 +9,16 @@ NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Requires = "ae029012-a4dd-5104-9daa-d747884805df" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +TikzPictures = "37f6aa50-8035-52d0-81c2-5a1d08754b2d" [compat] CSV = "0.10" LaTeXStrings = "^1.3" NaNMath = "0.3, 1" Requires = "1" -julia = "^1.6" Tables = "1.11" +TikzPictures = "3.5" +julia = "^1.6" [extras] PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925" diff --git a/src/BenchmarkProfiles.jl b/src/BenchmarkProfiles.jl index 2668e7d..a95d7ab 100644 --- a/src/BenchmarkProfiles.jl +++ b/src/BenchmarkProfiles.jl @@ -6,6 +6,7 @@ using Requires using Printf export performance_ratios, performance_profile, performance_profile_data, export_performance_profile +export export_performance_profile_tikz export data_ratios, data_profile export bp_backends, PlotsBackend, UnicodePlotsBackend, PGFPlotsXBackend @@ -30,6 +31,7 @@ end include("performance_profiles.jl") include("data_profiles.jl") +include("tikz_export.jl") """ Replace each number by 2^{number} in a string. diff --git a/src/performance_profiles.jl b/src/performance_profiles.jl index 6c8852b..b301055 100644 --- a/src/performance_profiles.jl +++ b/src/performance_profiles.jl @@ -6,6 +6,8 @@ using CSV, Tables +using CSV, Tables + """Compute performance ratios used to produce a performance profile. There is normally no need to call this function directly. @@ -155,6 +157,23 @@ function performance_profile( ) end +""" + performance_profile_data_mat(T; kwargs...) + +Returns `performance_profile_data` output (vectors) as matrices. Matrices are padded with NaN if necessary. +""" +function performance_profile_data_mat(T::Matrix{Float64};kwargs...) + x_data, y_data, max_ratio = performance_profile_data(T;kwargs...) + max_elem = maximum(length.(x_data)) + for i in eachindex(x_data) + append!(x_data[i],[NaN for i=1:max_elem-length(x_data[i])]) + append!(y_data[i],[NaN for i=1:max_elem-length(y_data[i])]) + end + x_mat = hcat(x_data...) + y_mat = hcat(y_data...) + return x_mat, y_mat +end + """ export_performance_profile(T, filename; solver_names = [], header, kwargs...) @@ -164,14 +183,14 @@ Export a performance profile plot data as .csv file. Profiles data are padded wi * `T :: Matrix{Float64}`: each column of `T` defines the performance data for a solver (smaller is better). Failures on a given problem are represented by a negative value, an infinite value, or `NaN`. -* `filename :: String` : path to the export file. +* `filename :: String` : path to the exported file. ## Keyword Arguments -* `solver_names :: Vector{S}` : names of the solvers -* `header::Vector{String}`: Contains .csv file column names. Note that `header` value does not change columns order in .csv exported files (see Output). +- `solver_names :: Vector{S}` : names of the solvers. +- `header::Vector{String}`: Contains .csv file column names. Note that `header` value does not change columns order in .csv exported files (see Output). -Other keyword arguments are passed `performance_profile_data`. +Other keyword arguments are passed to `performance_profile_data`. Output: File containing profile data in .csv format. Columns are solver1_x, solver1_y, solver2_x, ... @@ -185,23 +204,26 @@ function export_performance_profile( ) where {S <: AbstractString} nsolvers = size(T)[2] - x_data, y_data, max_ratio = performance_profile_data(T; kwargs...) - max_elem = maximum(length.(x_data)) - for i in eachindex(x_data) - append!(x_data[i], [NaN for i = 1:(max_elem - length(x_data[i]))]) - append!(y_data[i], [NaN for i = 1:(max_elem - length(y_data[i]))]) - end - x_mat = hcat(x_data...) - y_mat = hcat(y_data...) - + x_mat, y_mat = performance_profile_data_mat(T;kwargs...) isempty(solver_names) && (solver_names = ["solver_$i" for i = 1:nsolvers]) + if !isempty(header) + header_l = size(T)[2]*2 + length(header) == header_l || error("Header should contain $(header_l) elements") + header = vcat([[sname*"_x",sname*"_y"] for sname in solver_names]...) + end + data = Matrix{Float64}(undef,size(x_mat,1),nsolvers*2) + for i =0:nsolvers-1 + data[:,2*i+1] .= x_mat[:,i+1] + data[:,2*i+2] .= y_mat[:,i+1] + end + if !isempty(header) header_l = size(T)[2] * 2 length(header) == header_l || error("Header should contain $(header_l) elements") header = vcat([[sname * "_x", sname * "_y"] for sname in solver_names]...) end - data = Matrix{Float64}(undef, max_elem, nsolvers * 2) + data = Matrix{Float64}(undef, size(x_mat,1), nsolvers * 2) for i = 0:(nsolvers - 1) data[:, 2 * i + 1] .= x_mat[:, i + 1] data[:, 2 * i + 2] .= y_mat[:, i + 1] diff --git a/src/tikz_export.jl b/src/tikz_export.jl new file mode 100644 index 0000000..0a9fa02 --- /dev/null +++ b/src/tikz_export.jl @@ -0,0 +1,168 @@ +using TikzPictures + +export export_performance_profile_tikz + +""" + function export_performance_profile_tikz(T, filename; kwargs...) + +Export tikz figure of the performance profiles given by `T` in `filename`. + +## Arguments + +* `T :: Matrix{Float64}`: each column of `T` defines the performance data for a solver (smaller is better). + Failures on a given problem are represented by a negative value, an infinite value, or `NaN`. +* `filename :: String` : path to the tikz exported file. + +## Keyword Arguments + +* `file_type = TIKZ` : type of exported file. Options are `TIKZ`(raw tikz code), `TEX`(embeded tikz code, ready to compile), `SVG`, `PDF`. +* `solvernames :: Vector{String} = []` : names of the solvers, should have as many elements as the number of columns of `T`. If empty, use the labels returned by `performance_profile_axis_labels`. +* `xlim::AbstractFloat=10.` : size of the figure along the x axis. /!\\ the legend is added on the right hand side of the figure. +* `ylim::AbstractFloat=10.` : size of the figure along the y axis. +* `nxgrad::Int=5` : number of graduations on the x axis. +* `nygrad::Int=5` : number of graduations on the y axis. +* `grid::Bool=true` : display grid if true. +* `colours::Vector{String} = []` : colours of the plots, should have as many elements as the number of columns of `T`. +* `linestyles::Vector{String} = []` : line style (dashed, dotted, ...) of the plots, should have as many elements as the number of columns of `T`. +* `linewidth::AbstractFloat = 1.0` : line width of the plots. +* `xlabel::String = ""` : x-axis label. If empty, uses the one returned by `performance_profile_axis_labels`. +* `ylabel::String = ""` : y-axis label. If empty, uses the one returned by `performance_profile_axis_labels`. +* `axis_tick_length::AbstractFloat = 0.2` : axis graduation tick length. +* `lgd_pos::Vector = [xlim+0.5,ylim]`, : legend box top left corner coordinates, by default legend is on the left had side of the figure. +* `lgd_plot_length::AbstractFloat = 0.7` : legend curve plot length. +* `lgd_v_offset::AbstractFloat = 0.7` : vertical space between two legend items. +* `lgd_plot_offset::AbstractFloat = 0.1` : space between legend box left side and curve plot. +* `lgd_box_length::AbstractFloat = 3.` : legend box horizontal length. +* `label_val::Vector = [0.2,0.25,0.5,1]` : possible graduation labels along axes are multiples of label_val elements times 10^n (n is automatically selected). +* `logscale::Bool = true` : produce a logarithmic (base 2) performance plot. + +Other keyword arguments are passed to `performance_profile_data`. + +""" +function export_performance_profile_tikz( + T::Matrix{Float64}, + filename::String; + file_type = TIKZ, + solvernames::Vector{String}=String[], + xlim::AbstractFloat=10., + ylim::AbstractFloat=10., + nxgrad::Int=5, + nygrad::Int=5, + grid::Bool=true, + # markers::Vector{S} = String[], + colours::Vector{String} = String[], + linestyles::Vector{String} = String[], + linewidth::AbstractFloat = 1.0, + xlabel::String = "", + ylabel::String = "", + axis_tick_length::AbstractFloat = 0.2, + lgd_pos::Vector = [xlim+0.5,ylim], + lgd_plot_length::AbstractFloat = 0.7, + lgd_v_offset::AbstractFloat = 0.7, + lgd_plot_offset::AbstractFloat = 0.1, + lgd_box_length::AbstractFloat = 3., + label_val::Vector = [0.2,0.25,0.5,1], + logscale::Bool = true, + kwargs...) + + xlabel_def, ylabel_def, solvernames = performance_profile_axis_labels(solvernames, size(T, 2), logscale; kwargs...) + isempty(xlabel) && (xlabel=xlabel_def) + isempty(ylabel) && (ylabel=ylabel_def) + + y_grad = collect(0.:1.0/(nygrad-1):1.0) + + isempty(colours) && (colours = ["black" for _ =1:size(T,2)]) + isempty(linestyles) && (linestyles = ["solid" for _ =1:size(T,2)]) + + x_mat, y_mat = BenchmarkProfiles.performance_profile_data_mat(T;kwargs...) + + # get nice looking graduation on x axis + xmax , _ = findmax(x_mat[.!isnan.(x_mat)]) + dist = xmax/(nxgrad-1) + n=log.(10,dist./label_val) + _, ind = findmin(abs.(n .- round.(n))) + xgrad_dist = label_val[ind]*10^round(n[ind]) + x_grad = [0. , [xgrad_dist*i for i =1 : nxgrad-1]...] + xmax=max(x_grad[end],xmax) + + # get nice looking graduation on y axis + dist = 1.0/(nygrad-1) + n=log.(10,dist./label_val) + _, ind = findmin(abs.(n .- round.(n))) + ygrad_dist = label_val[ind]*10^round(n[ind]) + y_grad = [0. , [ygrad_dist*i for i =1 : nygrad-1]...] + ymax=max(y_grad[end],1.0) + + to_int(x) = isinteger(x) ? Int(x) : x + + xratio = xlim/xmax + yratio = ylim/ymax + io = IOBuffer() + + # axes + println(io, "\\draw[line width=$linewidth] (0,0) -- ($xlim,0);") + println(io, "\\node at ($(xlim/2), -1) {$xlabel};") + println(io, "\\draw[line width=$linewidth] (0,0) -- (0,$ylim);") + println(io, "\\node at (-1,$(ylim/2)) [rotate = 90] {$ylabel};") + # axes graduations and labels, + if logscale + for i in eachindex(x_grad) + println(io, "\\draw[line width=$linewidth] ($(x_grad[i]*xratio),0) -- ($(x_grad[i]*xratio),$axis_tick_length) node [pos=0, below] {\$2^{$(to_int(x_grad[i]))}\$};") + end + else + for i in eachindex(x_grad) + println(io, "\\draw[line width=$linewidth] ($(x_grad[i]*xratio),0) -- ($(x_grad[i]*xratio),$axis_tick_length) node [pos=0, below] {$(to_int(x_grad[i]))};") + end + end + for i in eachindex(y_grad) + println(io, "\\draw[line width=$linewidth] (0,$(y_grad[i]*yratio)) -- ($axis_tick_length,$(y_grad[i]*yratio)) node [pos=0, left] {$(to_int(y_grad[i]))};") + end + # grid + if grid + for i in eachindex(x_grad) + println(io, "\\draw[gray] ($(x_grad[i]*xratio),0) -- ($(x_grad[i]*xratio),$ylim);") + end + for i in eachindex(y_grad) + println(io, "\\draw[gray] (0,$(y_grad[i]*yratio)) -- ($xlim,$(y_grad[i]*yratio)) node [pos=0, left] {$(to_int(y_grad[i]))};") + end + end + + # profiles + for j in eachindex(solvernames) + drawcmd = "\\draw[line width=$linewidth, $(colours[j]), $(linestyles[j]), line width = $linewidth] " + drawcmd *= "($(x_mat[1,j]*xratio),$(y_mat[1,j]*yratio))" + for k in 2:size(x_mat,1) + if isnan(x_mat[k,j]) + break + end + if y_mat[k,j] > 1 # for some reasons last point of profile is set with y=1.1 by data function... + drawcmd *= " -- ($(xmax*xratio),$(y_mat[k-1,j]*yratio)) -- ($(xmax*xratio),$(y_mat[k-1,j]*yratio))" + else + # if !isempty(markers) + # drawcmd *= " -- ($(x_mat[k,j]*xratio),$(y_mat[k-1,j]*yratio)) node[$(colours[j]),draw,$(markers[j]),solid] {} -- ($(x_mat[k,j]*xratio),$(y_mat[k,j]*yratio))" + # else + drawcmd *= " -- ($(x_mat[k,j]*xratio),$(y_mat[k-1,j]*yratio)) -- ($(x_mat[k,j]*xratio),$(y_mat[k,j]*yratio))" + # end + end + end + drawcmd *= ";" + println(io,drawcmd) + end + # legend + for j in eachindex(solvernames) + legcmd = "\\draw[$(colours[j]), $(linestyles[j]), line width = $linewidth] " + legcmd *= "($(lgd_pos[1]+lgd_plot_offset),$(lgd_pos[2]-j*lgd_v_offset)) -- ($(lgd_pos[1]+lgd_plot_offset+lgd_plot_length),$(lgd_pos[2]-j*lgd_v_offset)) node [black,pos=1,right] {$(String(solvernames[j]))}" + # if !isempty(markers) + # legcmd *= " node [midway,draw,$(markers[j]),solid] {}" + # end + legcmd *= ";" + + println(io,legcmd) + end + # legend box + println(io,"\\draw[line width=$linewidth] ($(lgd_pos[1]),$(lgd_pos[2])) -- ($(lgd_pos[1]+lgd_box_length),$(lgd_pos[2])) -- ($(lgd_pos[1]+lgd_box_length),$(lgd_pos[2]-lgd_v_offset*(length(solvernames)+1))) -- ($(lgd_pos[1]),$(lgd_pos[2]-lgd_v_offset*(length(solvernames)+1))) -- cycle;") + + raw_code = String(take!(io)) + tp = TikzPicture(raw_code) + save(file_type(filename),tp) +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index d79f968..43f0ae8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ using BenchmarkProfiles using LaTeXStrings using Test +using TikzPictures @testset "powertick" begin @test BenchmarkProfiles.powertick("15") == "2¹⁵" @@ -72,4 +73,21 @@ if !Sys.isfreebsd() # GR_jll not available, so Plots won't install @test isfile(filename) rm(filename) end + + @testset "tikz export" begin + T = 10 * rand(25, 3) + filename = "tikz_fig" + export_performance_profile_tikz(T,filename) + @test isfile(filename * ".tikz") + rm(filename * ".tikz") + export_performance_profile_tikz(T,filename,file_type = TEX) + @test isfile(filename * ".tex") + rm(filename * ".tex") + export_performance_profile_tikz(T,filename,file_type = SVG) + @test isfile(filename * ".svg") + rm(filename * ".svg") + export_performance_profile_tikz(T,filename,file_type = PDF) + @test isfile(filename * ".pdf") + rm(filename * ".pdf") + end end