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
Merged
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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"
d-monnet marked this conversation as resolved.
Show resolved Hide resolved

[compat]
CSV = "0.10"
LaTeXStrings = "^1.3"
NaNMath = "0.3, 1"
Requires = "1"
julia = "^1.6"
Tables = "1.11"
julia = "^1.6"

[extras]
PGFPlotsX = "8314cec4-20b6-5062-9cdb-752b83310925"
Expand Down
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

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

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.
- `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
169 changes: 169 additions & 0 deletions src/tikz_export.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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).
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],
kwargs...)

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

Check warning on line 67 in src/tikz_export.jl

View check run for this annotation

Codecov / codecov/patch

src/tikz_export.jl#L67

Added line #L67 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)

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

Check warning on line 116 in src/tikz_export.jl

View check run for this annotation

Codecov / codecov/patch

src/tikz_export.jl#L114-L116

Added lines #L114 - L116 were not covered by tests
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
18 changes: 18 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using BenchmarkProfiles
using LaTeXStrings
using Test
using TikzPictures

@testset "powertick" begin
@test BenchmarkProfiles.powertick("15") == "2¹⁵"
Expand Down Expand Up @@ -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
Loading