Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions src/profiles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ using BenchmarkProfiles, Plots
export performance_profile, profile_solvers

"""
performance_profile(stats, cost, args...; b = PlotsBackend(), kwargs...)
performance_profile(stats, cost; b = PlotsBackend(), kwargs...)

Produce a performance profile comparing solvers in `stats` using the `cost` function.

Inputs:
- `stats::AbstractDict{Symbol,DataFrame}`: pairs of `:solver => df`;
- `cost::Function`: cost function applyed to each `df`. Should return a vector with the cost of solving the problem at each row;
- `cost::Function`: cost function applied to each `df`. Should return a vector with the cost of solving the problem at each row;
- 0 cost is not allowed;
- If the solver did not solve the problem, return Inf or a negative number.
- If the solver did not solve the problem, return `Inf` or a negative number.
- `b::BenchmarkProfiles.AbstractBackend` : backend used for the plot.

Keyword arguments:
- `kwargs...` : keyword arguments forwarded to `BenchmarkProfiles.performance_profile` (backend-specific options).
Example: `logscale = false` to disable log-scaling when supported by the backend.
If several profiles will be produced with variants of the same solvers, `stats` may be an `OrderedDict`, as defined in the
OrderedCollections.jl package.

Expand All @@ -24,21 +27,20 @@ Examples of cost functions:
"""
function performance_profile(
stats::AbstractDict{Symbol, DataFrame},
cost::Function,
args...;
cost::Function;
b::BenchmarkProfiles.AbstractBackend = PlotsBackend(),
kwargs...,
)
solvers = keys(stats)
dfs = (stats[s] for s in solvers)
P = hcat([cost(df) for df in dfs]...)
performance_profile(b, P, string.(solvers), args...; kwargs...)
BenchmarkProfiles.performance_profile(b, P, string.(solvers); kwargs...)
Comment on lines 28 to +37

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported performance_profile wrapper previously accepted extra positional arguments (args...) and forwarded them to BenchmarkProfiles.performance_profile, but this method signature now removes that capability. Since this is part of the public API, this is a breaking change for any callers passing additional positional parameters; consider keeping args... (and forwarding them) or providing a deprecation path instead of removing it outright.

Copilot uses AI. Check for mistakes.
end

"""
p = profile_solvers(stats, costs, costnames;
width = 400, height = 400,
b = PlotsBackend(), kwargs...)
b = PlotsBackend(), bp_kwargs = Dict(), kwargs...)

Produce performance profiles comparing `solvers` based on the data in `stats`.

Expand All @@ -52,11 +54,9 @@ Keyword inputs:
- `width::Int`: Width of each individual plot (Default: 400)
- `height::Int`: Height of each individual plot (Default: 400)
- `b::BenchmarkProfiles.AbstractBackend` : backend used for the plot.
- `bp_kwargs::Dict` : a `Dict` of keyword arguments forwarded to the `performance_profile` backend calls.

Additional `kwargs` are passed to the `plot` call.

Output:
A Plots.jl plot representing a set of performance profiles comparing the solvers.
Additional `kwargs` are passed to the final `plot` call that assembles the profiles.
The set contains performance profiles comparing all the solvers together on the
measures given in `costs`.
If there are more than two solvers, additional profiles are produced comparing the
Expand All @@ -69,6 +69,7 @@ function profile_solvers(
width::Int = 400,
height::Int = 400,
b::BenchmarkProfiles.AbstractBackend = PlotsBackend(),
bp_kwargs::Dict = Dict(),
kwargs...,
Comment on lines 69 to 73

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bp_kwargs is typed as Dict with a default of Dict(), but keyword-splatting requires keys that can become keyword names (typically Symbol). As written, callers can pass non-Symbol keys and get a runtime error when splatting, and the Dict type annotation also blocks convenient alternatives like NamedTuple/Pairs which are commonly used for kwargs. Consider typing this as AbstractDict{Symbol,<:Any} (or accepting NamedTuple/Pairs as well) and/or validating/coercing keys before splatting.

Copilot uses AI. Check for mistakes.
)
solvers = collect(keys(stats))
Expand All @@ -83,24 +84,26 @@ function profile_solvers(

# profiles with all solvers
ps = [
performance_profile(
BenchmarkProfiles.performance_profile(
b,
Ps[1],
string.(solvers),
string.(solvers);
palette = colors,
title = costnames[1],
legend = :bottomright,
bp_kwargs...,
),
Comment on lines +87 to 95

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bp_kwargs... is appended after explicit keywords like palette, title, and legend. If bp_kwargs contains any of those keys, the call will error due to repeated keyword arguments, making it impossible to override the defaults via bp_kwargs. Consider constructing a single keyword set (e.g., merge defaults with bp_kwargs giving bp_kwargs precedence, or explicitly rejecting conflicting keys with a clear error) to avoid runtime keyword-collision failures.

Copilot uses AI. Check for mistakes.
]
nsolvers > 2 && xlabel!(ps[1], "")
for k = 2:ncosts
p = performance_profile(
p = BenchmarkProfiles.performance_profile(
b,
Ps[k],
string.(solvers),
string.(solvers);
palette = colors,
title = costnames[k],
legend = false,
bp_kwargs...,
)
nsolvers > 2 && xlabel!(p, "")
ylabel!(p, "")
Expand All @@ -118,11 +121,25 @@ function profile_solvers(
Ps = [hcat([Float64.(cost(df)) for df in dfs]...) for cost in costs]

clrs = [colors[i], colors[j]]
p = performance_profile(b, Ps[1], string.(pair), palette = clrs, legend = :bottomright)
p = BenchmarkProfiles.performance_profile(
b,
Ps[1],
string.(pair);
palette = clrs,
legend = :bottomright,
bp_kwargs...,
)
ipairs < npairs && xlabel!(p, "")
push!(ps, p)
for k = 2:ncosts
p = performance_profile(b, Ps[k], string.(pair), palette = clrs, legend = false)
p = BenchmarkProfiles.performance_profile(
b,
Ps[k],
string.(pair);
palette = clrs,
legend = false,
bp_kwargs...,
)
ipairs < npairs && xlabel!(p, "")
ylabel!(p, "")
push!(ps, p)
Expand Down
76 changes: 76 additions & 0 deletions test/profiles_kwargs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Test
using DataFrames
using SolverBenchmark
using BenchmarkProfiles
using Plots

struct CaptureBackend <: BenchmarkProfiles.AbstractBackend end

struct CapturedPlot
labels::Vector{String}
kwargs::Any
end

# Extend BenchmarkProfiles.performance_profile for our CaptureBackend
function BenchmarkProfiles.performance_profile(
::CaptureBackend,
P::Matrix{<:Number},
labels::Vector{<:AbstractString};
kwargs...,
)
labs = string.(labels)
CapturedPlot(labs, kwargs)
end

function Plots.plot(ps::CapturedPlot...; kwargs...)
return (plots = ps, plot_kwargs = kwargs)
end

@testset "profiles: bp_kwargs and kwargs forwarding" begin
df1 = DataFrame(a = [1.0, 2.0])
df2 = DataFrame(a = [2.0, 3.0])
stats = Dict(:s1 => df1, :s2 => df2)

costs = [df -> df.a]
costnames = ["a"]

result = profile_solvers(
stats,
costs,
costnames;
b = CaptureBackend(),
bp_kwargs = Dict(:logscale => false),
)
@test isa(result, NamedTuple)
# The inner performance_profile returns CapturedPlot objects stored in result.plots
plots = result[:plots]
@test length(plots) >= 1
first_plot = plots[1]
@test (:logscale in keys(first_plot.kwargs)) && first_plot.kwargs[:logscale] == false

result2 = profile_solvers(
stats,
costs,
costnames;
b = CaptureBackend(),
bp_kwargs = Dict(:logscale => true),
title = "T",
legend = false,
)
@test isa(result2, NamedTuple)
@test (:title in keys(result2[:plot_kwargs])) && result2[:plot_kwargs][:title] == "T"
@test (:legend in keys(result2[:plot_kwargs])) && result2[:plot_kwargs][:legend] == false

result3 = profile_solvers(
stats,
costs,
costnames;
b = CaptureBackend(),
bp_kwargs = Dict(:foo => 1),
bar = 2,
extra = 3,
)
@test (:foo in keys(result3[:plots][1].kwargs)) && result3[:plots][1].kwargs[:foo] == 1
@test (:bar in keys(result3[:plot_kwargs])) && result3[:plot_kwargs][:bar] == 2
@test (:extra in keys(result3[:plot_kwargs])) && result3[:plot_kwargs][:extra] == 3
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ include("test-tables.jl")
include("profiles.jl")
include("pkgbmark.jl")
include("test_bmark.jl")
include("profiles_kwargs.jl")
Loading