Skip to content

Commit

Permalink
Tikz figure export (#105)
Browse files Browse the repository at this point in the history
* add data matrix export function

* add tikz figure export function

* update module

* add tests

* Update src/tikz_export.jl

Co-authored-by: tmigot <[email protected]>

* Update src/tikz_export.jl

Co-authored-by: tmigot <[email protected]>

* Update src/tikz_export.jl

Co-authored-by: tmigot <[email protected]>

* Update src/tikz_export.jl

Co-authored-by: tmigot <[email protected]>

* Update src/performance_profiles.jl

Co-authored-by: tmigot <[email protected]>

* Update src/performance_profiles.jl

Co-authored-by: tmigot <[email protected]>

* 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 <[email protected]>

* add TikzPicture.jl compat

* fix logscale argument

---------

Co-authored-by: tmigot <[email protected]>
  • Loading branch information
d-monnet and tmigot authored Nov 2, 2023
1 parent fb97a65 commit f10841d
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 15 deletions.
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
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

"""
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...)
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
168 changes: 168 additions & 0 deletions src/tikz_export.jl
Original file line number Diff line number Diff line change
@@ -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
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

0 comments on commit f10841d

Please sign in to comment.