Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tikz figure export #105

Merged
merged 16 commits into from
Nov 2, 2023
2 changes: 2 additions & 0 deletions src/BenchmarkProfiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
50 changes: 36 additions & 14 deletions src/performance_profiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -155,6 +157,23 @@ function performance_profile(
)
end

"""
function performance_profile_data_mat(T;kwargs...)
d-monnet marked this conversation as resolved.
Show resolved Hide resolved

Retruns `performance_profile_data` output (vectors) as matrices. Matrices are padded with NaN if necessary.
d-monnet marked this conversation as resolved.
Show resolved Hide resolved
"""
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...)

Expand All @@ -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.
d-monnet marked this conversation as resolved.
Show resolved Hide resolved
- `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, ...
Expand All @@ -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]
Expand Down
153 changes: 153 additions & 0 deletions src/tikz_export.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
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

* `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 with of the plots.
d-monnet marked this conversation as resolved.
Show resolved Hide resolved
* `xlabel::String = ""` : x axis label. If empty, uses the one returns by `performance_profile_axis_labels`.
d-monnet marked this conversation as resolved.
Show resolved Hide resolved
* `ylabel::String = ""` : x axis label. If empty, uses the one returns by `performance_profile_axis_labels`.
d-monnet marked this conversation as resolved.
Show resolved Hide resolved

Other keyword arguments are passed `performance_profile_data`.
d-monnet marked this conversation as resolved.
Show resolved Hide resolved

"""
function export_performance_profile_tikz(
T::Matrix{Float64},
filename::String;
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 = "",
kwargs...)



logscale = true
if haskey(kwargs,:logscale)
logscale = kwargs[:logscale]

Check warning on line 52 in src/tikz_export.jl

View check run for this annotation

Codecov / codecov/patch

src/tikz_export.jl#L52

Added line #L52 was not covered by tests
end
d-monnet marked this conversation as resolved.
Show resolved Hide resolved
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)

# some offsets
axis_tik_l = 0.2 # tick length
lgd_offset = 0.5 # space between figure and legend box
lgd_box_length = 3. # legend box length
lgd_plot_length = 0.7 # legend plot length
lgd_v_offset = 0.7 # vertical space between legend items

label_val = [0.2,0.25,0.5,1] # possible graduation labels along axes are multiples of label_val elements times 10^n
ymax = 1.0
d-monnet marked this conversation as resolved.
Show resolved Hide resolved
y_grad = collect(0.:ymax/(nygrad-1):ymax)

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]...]
#x_grad[end] <= xmax || (pop!(x_grad))
xmax=max(x_grad[end],xmax)
to_int(x) = isinteger(x) ? Int(x) : x

xratio = xlim/xmax
yratio = ylim/ymax
open(filename, "w") do io
println(io, "\\begin{tikzpicture}")
# 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_tik_l) 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_tik_l) node [pos=0, below] {$(to_int(x_grad[i]))};")
end

Check warning on line 102 in src/tikz_export.jl

View check run for this annotation

Codecov / codecov/patch

src/tikz_export.jl#L100-L102

Added lines #L100 - L102 were not covered by tests
end
for i in eachindex(y_grad)
println(io, "\\draw[line width=$linewidth] (0,$(y_grad[i]*yratio)) -- ($axis_tik_l,$(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 *= "($(xlim+lgd_offset),$(ylim-j*lgd_v_offset)) -- ($(xlim+lgd_offset+lgd_plot_length),$(ylim-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] ($(xlim+lgd_offset-0.1),$ylim) -- ($(xlim+lgd_offset+lgd_box_length),$ylim) -- ($(xlim+lgd_offset+lgd_box_length),$(ylim-lgd_v_offset*(length(solvernames)+1))) -- ($(xlim+lgd_offset-0.1),$(ylim-lgd_v_offset*(length(solvernames)+1))) -- cycle;")
println(io,"\\end{tikzpicture}")
end
end
8 changes: 8 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,12 @@ 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.tex"
export_performance_profile_tikz(T,filename)
@test isfile(filename)
rm(filename)
end
end