From 79f84438ffcdffbe0b49a874976d964e0b0c493d Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Thu, 30 Oct 2025 08:05:45 +0530 Subject: [PATCH 01/16] solver documentation update --- src/run_solver.jl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/run_solver.jl b/src/run_solver.jl index c09a30e..05752c7 100644 --- a/src/run_solver.jl +++ b/src/run_solver.jl @@ -24,6 +24,24 @@ benchmark (default: `[:name, :nvar, :ncon, :status, :elapsed_time, :objective, : * `prune`: do not include skipped problems in the final statistics (default: `true`); * any other keyword argument to be passed to the solver. +#### Solver-specific statistics +* Solvers can attach solver-specific information to the returned execution + statistics object (the `s` returned by the solver). Fields contained in + `s.solver_specific` that are scalar (i.e. not `AbstractVector`) are + automatically appended as extra columns to the returned `DataFrame`. +* Columns for solver-specific keys are created when the first successful + solver run returns such keys. If earlier problems were skipped or raised + exceptions, those earlier rows will contain `missing` for these columns. +* Vector-valued entries in `s.solver_specific` are not added as individual + columns (they are ignored by `solve_problems` when creating columns). +* When a problem is skipped or an exception occurs, the corresponding + solver-specific columns are filled with `missing` for that row. +* To set solver-specific values from inside a solver you can use the + solver's API / callbacks (see tests for examples where a callback calls + `set_solver_specific!(stats, :key, value)`) — the key will appear as a + column in the final statistics (if scalar) for all subsequently processed + problems. + #### Return value * a `DataFrame` where each row is a problem, minus the skipped ones if `prune` is true. """ From daf3d57d2eea020ecbf074c656dac4626e0ad356 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Thu, 30 Oct 2025 17:16:38 +0530 Subject: [PATCH 02/16] added above coltitle --- src/run_solver.jl | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/run_solver.jl b/src/run_solver.jl index 05752c7..2c49bc9 100644 --- a/src/run_solver.jl +++ b/src/run_solver.jl @@ -19,28 +19,15 @@ Apply a solver to a set of problems. (default: `x->false`); * `colstats::Vector{Symbol}`: summary statistics for the logger to output during the benchmark (default: `[:name, :nvar, :ncon, :status, :elapsed_time, :objective, :dual_feas, :primal_feas]`); +* `solver_specific`: scalar entries in a solver's `s.solver_specific` are + appended as extra columns to the returned `DataFrame` (vector entries are + ignored); columns are created when first encountered and earlier skipped/exception + rows contain `missing` for these columns. * `info_hdr_override::Dict{Symbol,String}`: header overrides for the summary statistics (default: use default headers); * `prune`: do not include skipped problems in the final statistics (default: `true`); * any other keyword argument to be passed to the solver. -#### Solver-specific statistics -* Solvers can attach solver-specific information to the returned execution - statistics object (the `s` returned by the solver). Fields contained in - `s.solver_specific` that are scalar (i.e. not `AbstractVector`) are - automatically appended as extra columns to the returned `DataFrame`. -* Columns for solver-specific keys are created when the first successful - solver run returns such keys. If earlier problems were skipped or raised - exceptions, those earlier rows will contain `missing` for these columns. -* Vector-valued entries in `s.solver_specific` are not added as individual - columns (they are ignored by `solve_problems` when creating columns). -* When a problem is skipped or an exception occurs, the corresponding - solver-specific columns are filled with `missing` for that row. -* To set solver-specific values from inside a solver you can use the - solver's API / callbacks (see tests for examples where a callback calls - `set_solver_specific!(stats, :key, value)`) — the key will appear as a - column in the final statistics (if scalar) for all subsequently processed - problems. #### Return value * a `DataFrame` where each row is a problem, minus the skipped ones if `prune` is true. From a380c20fbd79b5faad851c6d7822811ae9f22907 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Mon, 3 Nov 2025 06:46:02 +0530 Subject: [PATCH 03/16] Remove extra newline in run_solver.jl documentation --- src/run_solver.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/run_solver.jl b/src/run_solver.jl index 2c49bc9..35399e8 100644 --- a/src/run_solver.jl +++ b/src/run_solver.jl @@ -28,7 +28,6 @@ benchmark (default: `[:name, :nvar, :ncon, :status, :elapsed_time, :objective, : * `prune`: do not include skipped problems in the final statistics (default: `true`); * any other keyword argument to be passed to the solver. - #### Return value * a `DataFrame` where each row is a problem, minus the skipped ones if `prune` is true. """ From 712d6a2f34b59a92fd2aeb424911c57a2ccb3c47 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Thu, 6 Nov 2025 06:47:07 +0530 Subject: [PATCH 04/16] Update src/run_solver.jl Co-authored-by: Tangi Migot --- src/run_solver.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/run_solver.jl b/src/run_solver.jl index 35399e8..7a4344e 100644 --- a/src/run_solver.jl +++ b/src/run_solver.jl @@ -18,11 +18,7 @@ Apply a solver to a set of problems. * `skipif::Function`: function to be applied to a problem and return whether to skip it (default: `x->false`); * `colstats::Vector{Symbol}`: summary statistics for the logger to output during the -benchmark (default: `[:name, :nvar, :ncon, :status, :elapsed_time, :objective, :dual_feas, :primal_feas]`); -* `solver_specific`: scalar entries in a solver's `s.solver_specific` are - appended as extra columns to the returned `DataFrame` (vector entries are - ignored); columns are created when first encountered and earlier skipped/exception - rows contain `missing` for these columns. +benchmark (default: `[:name, :nvar, :ncon, :status, :elapsed_time, :objective, :dual_feas, :primal_feas]`). Solver's `solver_specific` scalar entries are appended as extra columns. * `info_hdr_override::Dict{Symbol,String}`: header overrides for the summary statistics (default: use default headers); * `prune`: do not include skipped problems in the final statistics (default: `true`); From 2609f5e3d9cbd5fdfedc32bbc1117ec9394324a8 Mon Sep 17 00:00:00 2001 From: Tangi Migot Date: Sun, 16 Nov 2025 12:05:27 -0500 Subject: [PATCH 05/16] Update src/run_solver.jl --- src/run_solver.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/run_solver.jl b/src/run_solver.jl index 7a4344e..6b61730 100644 --- a/src/run_solver.jl +++ b/src/run_solver.jl @@ -18,7 +18,7 @@ Apply a solver to a set of problems. * `skipif::Function`: function to be applied to a problem and return whether to skip it (default: `x->false`); * `colstats::Vector{Symbol}`: summary statistics for the logger to output during the -benchmark (default: `[:name, :nvar, :ncon, :status, :elapsed_time, :objective, :dual_feas, :primal_feas]`). Solver's `solver_specific` scalar entries are appended as extra columns. +benchmark (default: `[:name, :nvar, :ncon, :status, :elapsed_time, :objective, :dual_feas, :primal_feas]`), solver's `solver_specific` scalar entries are appended as extra columns; * `info_hdr_override::Dict{Symbol,String}`: header overrides for the summary statistics (default: use default headers); * `prune`: do not include skipped problems in the final statistics (default: `true`); From 6c513273eec9227973b93cc0e834b1853f1bb810 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 2 Dec 2025 07:45:03 +0530 Subject: [PATCH 06/16] Forward bp_kwargs and plot_kwargs in profiles; skip pkgbmark when repo dirty for local tests --- src/profiles.jl | 12 +++++++++--- test/pkgbmark.jl | 11 +++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index 257271f..c472879 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -27,12 +27,13 @@ function performance_profile( cost::Function, args...; b::BenchmarkProfiles.AbstractBackend = PlotsBackend(), + bp_kwargs::Dict=Dict(), 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...) + performance_profile(b, P, string.(solvers), args...; bp_kwargs..., kwargs...) end """ @@ -69,6 +70,8 @@ function profile_solvers( width::Int = 400, height::Int = 400, b::BenchmarkProfiles.AbstractBackend = PlotsBackend(), + bp_kwargs::Dict=Dict(), + plot_kwargs::Dict=Dict(), kwargs..., ) solvers = collect(keys(stats)) @@ -90,6 +93,7 @@ function profile_solvers( palette = colors, title = costnames[1], legend = :bottomright, + bp_kwargs..., ), ] nsolvers > 2 && xlabel!(ps[1], "") @@ -101,6 +105,7 @@ function profile_solvers( palette = colors, title = costnames[k], legend = false, + bp_kwargs..., ) nsolvers > 2 && xlabel!(p, "") ylabel!(p, "") @@ -118,11 +123,11 @@ 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 = 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 = performance_profile(b, Ps[k], string.(pair), palette = clrs, legend = false, bp_kwargs...) ipairs < npairs && xlabel!(p, "") ylabel!(p, "") push!(ps, p) @@ -134,6 +139,7 @@ function profile_solvers( ps..., layout = (1 + ipairs, ncosts), size = (ncosts * width, (1 + ipairs) * height); + plot_kwargs..., kwargs..., ) end diff --git a/test/pkgbmark.jl b/test/pkgbmark.jl index efa3ea3..42afac9 100644 --- a/test/pkgbmark.jl +++ b/test/pkgbmark.jl @@ -9,6 +9,17 @@ function test_pkgbmark() get(ENV, "GITHUB_REPOSITORY", "") != "JuliaSmoothOptimizers/SolverBenchmark.jl" return end + # Skip benchmarking tests if the repository has uncommitted changes. + # PkgBenchmark refuses to benchmark a specific commit when the working tree is dirty. + try + git_status = chomp(read(`git status --porcelain`, String)) + if !isempty(git_status) + @info "Skipping package-benchmark tests because repository is dirty" + return + end + catch e + @warn "Could not determine git status; proceeding with pkgbmark tests: $e" + end results = PkgBenchmark.benchmarkpkg("SolverBenchmark", script = joinpath(@__DIR__, "bmark_suite.jl")) From 6319f463df30ad123d13b5540986a1a42d0e35c3 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 2 Dec 2025 07:55:03 +0530 Subject: [PATCH 07/16] Document bp_kwargs and plot_kwargs in profiles docstrings --- src/profiles.jl | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index c472879..a412065 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -4,17 +4,22 @@ using BenchmarkProfiles, Plots export performance_profile, profile_solvers """ - performance_profile(stats, cost, args...; b = PlotsBackend(), kwargs...) + performance_profile(stats, cost, args...; b = PlotsBackend(), bp_kwargs = Dict(), 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: +- `bp_kwargs::Dict` : a `Dict` of keyword arguments forwarded to `BenchmarkProfiles.performance_profile` (backend-specific options). + Example: `bp_kwargs = Dict(:logscale => false)` to disable log-scaling when supported by the backend. +- `kwargs...` : additional keyword arguments forwarded to the plotting routines used 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. @@ -37,15 +42,15 @@ function performance_profile( end """ - p = profile_solvers(stats, costs, costnames; - width = 400, height = 400, - b = PlotsBackend(), kwargs...) + p = profile_solvers(stats, costs, costnames; + width = 400, height = 400, + b = PlotsBackend(), bp_kwargs = Dict(), plot_kwargs = Dict(), kwargs...) Produce performance profiles comparing `solvers` based on the data in `stats`. Inputs: - `stats::AbstractDict{Symbol,DataFrame}`: a dictionary of `DataFrame`s containing the - benchmark results per solver (e.g., produced by `bmark_results_to_dataframes()`) + benchmark results per solver (e.g., produced by `bmark_results_to_dataframes()`) - `costs::Vector{Function}`: a vector of functions specifying the measures to use in the profiles - `costnames::Vector{String}`: names to be used as titles of the profiles. @@ -53,8 +58,11 @@ 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 backend `performance_profile` calls + (see `BenchmarkProfiles.performance_profile` — backend-specific options such as `:logscale`). +- `plot_kwargs::Dict` : a `Dict` of keyword arguments forwarded to the final `plot` call that assembles the profiles. -Additional `kwargs` are passed to the `plot` call. +Additional `kwargs` are passed to the `plot` call for backwards compatibility. Output: A Plots.jl plot representing a set of performance profiles comparing the solvers. From 373eb772a8af07c6fcf2d306967387cadc894fe8 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 2 Dec 2025 07:56:47 +0530 Subject: [PATCH 08/16] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/profiles.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index a412065..d2edd06 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -32,7 +32,7 @@ function performance_profile( cost::Function, args...; b::BenchmarkProfiles.AbstractBackend = PlotsBackend(), - bp_kwargs::Dict=Dict(), + bp_kwargs::Dict = Dict(), kwargs..., ) solvers = keys(stats) @@ -79,7 +79,7 @@ function profile_solvers( height::Int = 400, b::BenchmarkProfiles.AbstractBackend = PlotsBackend(), bp_kwargs::Dict=Dict(), - plot_kwargs::Dict=Dict(), + plot_kwargs::Dict = Dict(), kwargs..., ) solvers = collect(keys(stats)) From 12e9810cc935dbebb9456476c1a69e94c4536037 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 2 Dec 2025 07:56:58 +0530 Subject: [PATCH 09/16] Add tests for bp_kwargs and plot_kwargs forwarding in profiles --- test/profiles_kwargs.jl | 58 +++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 59 insertions(+) create mode 100644 test/profiles_kwargs.jl diff --git a/test/profiles_kwargs.jl b/test/profiles_kwargs.jl new file mode 100644 index 0000000..2730766 --- /dev/null +++ b/test/profiles_kwargs.jl @@ -0,0 +1,58 @@ +using Test +using DataFrames +using BenchmarkProfiles +using Plots + +# Use the package under test +using SolverBenchmark + +# A small capture backend to test that bp_kwargs are forwarded to +# BenchmarkProfiles.performance_profile +struct CaptureBackend <: BenchmarkProfiles.AbstractBackend end + +struct CapturedPlot + labels::Vector{String} + kwargs::NamedTuple +end + +# Extend BenchmarkProfiles.performance_profile for our CaptureBackend +function BenchmarkProfiles.performance_profile(::CaptureBackend, P, labels...; kwargs...) + CapturedPlot(collect(labels), kwargs) +end + +# Intercept Plots.plot when passed our CapturedPlot objects so we can +# inspect the keyword arguments forwarded to the final plot call. +function Plots.plot(ps::CapturedPlot...; kwargs...) + return (plots = ps, plot_kwargs = kwargs) +end + +@testset "profiles: bp_kwargs and plot_kwargs forwarding" begin + # Build minimal stats: two solvers with simple dataframes + 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"] + + # Test bp_kwargs forwarded to backend + result = profile_solvers(stats, costs, costnames; b = CaptureBackend(), bp_kwargs = Dict(:logscale => false)) + @test typeof(result) == Tuple + # The inner performance_profile returns CapturedPlot objects stored in result.plots + plots = result[:plots] + @test length(plots) >= 1 + first_plot = plots[1] + @test first_plot.kwargs[:logscale] == false + + # Test plot_kwargs forwarded to final plot call, and that kwargs also work + result2 = profile_solvers(stats, costs, costnames; b = CaptureBackend(), bp_kwargs = Dict(:logscale => true), plot_kwargs = Dict(:title => "T"), legend = false) + @test typeof(result2) == Tuple + @test result2[:plot_kwargs][:title] == "T" + @test result2[:plot_kwargs][:legend] == false + + # Test both bp_kwargs and plot_kwargs can be used simultaneously without conflict + result3 = profile_solvers(stats, costs, costnames; b = CaptureBackend(), bp_kwargs = Dict(:foo => 1), plot_kwargs = Dict(:bar => 2), extra = 3) + @test result3[:plots][1].kwargs[:foo] == 1 + @test result3[:plot_kwargs][:bar] == 2 + @test result3[:plot_kwargs][:extra] == 3 +end diff --git a/test/runtests.jl b/test/runtests.jl index 8357b50..aa6432e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -24,3 +24,4 @@ include("test-tables.jl") include("profiles.jl") include("pkgbmark.jl") include("test_bmark.jl") +include("profiles_kwargs.jl") From 9ac4c10359732fdaee4e9faf28a67d6bdcd4be84 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 2 Dec 2025 07:58:45 +0530 Subject: [PATCH 10/16] Update tests for bp_kwargs and plot_kwargs handling Refactor tests to use CaptureBackend for profiling and plotting. --- test/profiles_kwargs.jl | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/test/profiles_kwargs.jl b/test/profiles_kwargs.jl index 2730766..742d423 100644 --- a/test/profiles_kwargs.jl +++ b/test/profiles_kwargs.jl @@ -1,13 +1,9 @@ using Test using DataFrames +using SolverBenchmark using BenchmarkProfiles using Plots -# Use the package under test -using SolverBenchmark - -# A small capture backend to test that bp_kwargs are forwarded to -# BenchmarkProfiles.performance_profile struct CaptureBackend <: BenchmarkProfiles.AbstractBackend end struct CapturedPlot @@ -15,19 +11,15 @@ struct CapturedPlot kwargs::NamedTuple end -# Extend BenchmarkProfiles.performance_profile for our CaptureBackend function BenchmarkProfiles.performance_profile(::CaptureBackend, P, labels...; kwargs...) CapturedPlot(collect(labels), kwargs) end -# Intercept Plots.plot when passed our CapturedPlot objects so we can -# inspect the keyword arguments forwarded to the final plot call. function Plots.plot(ps::CapturedPlot...; kwargs...) return (plots = ps, plot_kwargs = kwargs) end @testset "profiles: bp_kwargs and plot_kwargs forwarding" begin - # Build minimal stats: two solvers with simple dataframes df1 = DataFrame(a = [1.0, 2.0]) df2 = DataFrame(a = [2.0, 3.0]) stats = Dict(:s1 => df1, :s2 => df2) @@ -35,22 +27,19 @@ end costs = [df -> df.a] costnames = ["a"] - # Test bp_kwargs forwarded to backend result = profile_solvers(stats, costs, costnames; b = CaptureBackend(), bp_kwargs = Dict(:logscale => false)) @test typeof(result) == Tuple - # The inner performance_profile returns CapturedPlot objects stored in result.plots + plots = result[:plots] @test length(plots) >= 1 first_plot = plots[1] @test first_plot.kwargs[:logscale] == false - # Test plot_kwargs forwarded to final plot call, and that kwargs also work result2 = profile_solvers(stats, costs, costnames; b = CaptureBackend(), bp_kwargs = Dict(:logscale => true), plot_kwargs = Dict(:title => "T"), legend = false) @test typeof(result2) == Tuple @test result2[:plot_kwargs][:title] == "T" @test result2[:plot_kwargs][:legend] == false - # Test both bp_kwargs and plot_kwargs can be used simultaneously without conflict result3 = profile_solvers(stats, costs, costnames; b = CaptureBackend(), bp_kwargs = Dict(:foo => 1), plot_kwargs = Dict(:bar => 2), extra = 3) @test result3[:plots][1].kwargs[:foo] == 1 @test result3[:plot_kwargs][:bar] == 2 From 9320274e4f80890cf0a8b5aac18e743b3e3f6a48 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 2 Dec 2025 08:13:00 +0530 Subject: [PATCH 11/16] passing tests for bp_kwargs and plot_kwargs forwarding --- src/profiles.jl | 18 +++++++++--------- test/profiles_kwargs.jl | 26 ++++++++++++++------------ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index d2edd06..76d2442 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -38,7 +38,7 @@ function performance_profile( 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...; bp_kwargs..., kwargs...) + BenchmarkProfiles.performance_profile(b, P, string.(solvers); bp_kwargs..., kwargs...) end """ @@ -94,26 +94,26 @@ function profile_solvers( # profiles with all solvers ps = [ - performance_profile( + BenchmarkProfiles.performance_profile( b, Ps[1], - string.(solvers), + string.(solvers); + bp_kwargs..., palette = colors, title = costnames[1], legend = :bottomright, - bp_kwargs..., ), ] nsolvers > 2 && xlabel!(ps[1], "") for k = 2:ncosts - p = performance_profile( + p = BenchmarkProfiles.performance_profile( b, Ps[k], - string.(solvers), + string.(solvers); + bp_kwargs..., palette = colors, title = costnames[k], legend = false, - bp_kwargs..., ) nsolvers > 2 && xlabel!(p, "") ylabel!(p, "") @@ -131,11 +131,11 @@ 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, bp_kwargs...) + p = BenchmarkProfiles.performance_profile(b, Ps[1], string.(pair); bp_kwargs..., palette = clrs, legend = :bottomright) ipairs < npairs && xlabel!(p, "") push!(ps, p) for k = 2:ncosts - p = performance_profile(b, Ps[k], string.(pair), palette = clrs, legend = false, bp_kwargs...) + p = BenchmarkProfiles.performance_profile(b, Ps[k], string.(pair); bp_kwargs..., palette = clrs, legend = false) ipairs < npairs && xlabel!(p, "") ylabel!(p, "") push!(ps, p) diff --git a/test/profiles_kwargs.jl b/test/profiles_kwargs.jl index 742d423..f536675 100644 --- a/test/profiles_kwargs.jl +++ b/test/profiles_kwargs.jl @@ -8,11 +8,13 @@ struct CaptureBackend <: BenchmarkProfiles.AbstractBackend end struct CapturedPlot labels::Vector{String} - kwargs::NamedTuple + kwargs::Any end -function BenchmarkProfiles.performance_profile(::CaptureBackend, P, labels...; kwargs...) - CapturedPlot(collect(labels), kwargs) +# 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...) @@ -28,20 +30,20 @@ end costnames = ["a"] result = profile_solvers(stats, costs, costnames; b = CaptureBackend(), bp_kwargs = Dict(:logscale => false)) - @test typeof(result) == Tuple - + @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 first_plot.kwargs[:logscale] == false + @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), plot_kwargs = Dict(:title => "T"), legend = false) - @test typeof(result2) == Tuple - @test result2[:plot_kwargs][:title] == "T" - @test result2[:plot_kwargs][: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), plot_kwargs = Dict(:bar => 2), extra = 3) - @test result3[:plots][1].kwargs[:foo] == 1 - @test result3[:plot_kwargs][:bar] == 2 - @test result3[:plot_kwargs][: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 From e6c33eec51a057d7b095e8ab27091b2f8edc919c Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 2 Dec 2025 08:17:49 +0530 Subject: [PATCH 12/16] JuliaFormatter --- src/profiles.jl | 20 +++++++++++++++++--- test/profiles_kwargs.jl | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index 76d2442..6b34017 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -78,7 +78,7 @@ function profile_solvers( width::Int = 400, height::Int = 400, b::BenchmarkProfiles.AbstractBackend = PlotsBackend(), - bp_kwargs::Dict=Dict(), + bp_kwargs::Dict = Dict(), plot_kwargs::Dict = Dict(), kwargs..., ) @@ -131,11 +131,25 @@ function profile_solvers( Ps = [hcat([Float64.(cost(df)) for df in dfs]...) for cost in costs] clrs = [colors[i], colors[j]] - p = BenchmarkProfiles.performance_profile(b, Ps[1], string.(pair); bp_kwargs..., palette = clrs, legend = :bottomright) + p = BenchmarkProfiles.performance_profile( + b, + Ps[1], + string.(pair); + bp_kwargs..., + palette = clrs, + legend = :bottomright, + ) ipairs < npairs && xlabel!(p, "") push!(ps, p) for k = 2:ncosts - p = BenchmarkProfiles.performance_profile(b, Ps[k], string.(pair); bp_kwargs..., palette = clrs, legend = false) + p = BenchmarkProfiles.performance_profile( + b, + Ps[k], + string.(pair); + bp_kwargs..., + palette = clrs, + legend = false, + ) ipairs < npairs && xlabel!(p, "") ylabel!(p, "") push!(ps, p) diff --git a/test/profiles_kwargs.jl b/test/profiles_kwargs.jl index f536675..1672cf4 100644 --- a/test/profiles_kwargs.jl +++ b/test/profiles_kwargs.jl @@ -12,7 +12,12 @@ struct CapturedPlot end # Extend BenchmarkProfiles.performance_profile for our CaptureBackend -function BenchmarkProfiles.performance_profile(::CaptureBackend, P::Matrix{<:Number}, labels::Vector{<:AbstractString}; kwargs...) +function BenchmarkProfiles.performance_profile( + ::CaptureBackend, + P::Matrix{<:Number}, + labels::Vector{<:AbstractString}; + kwargs..., +) labs = string.(labels) CapturedPlot(labs, kwargs) end @@ -29,7 +34,13 @@ end costs = [df -> df.a] costnames = ["a"] - result = profile_solvers(stats, costs, costnames; b = CaptureBackend(), bp_kwargs = Dict(:logscale => false)) + 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] @@ -37,12 +48,28 @@ end 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), plot_kwargs = Dict(:title => "T"), legend = false) + result2 = profile_solvers( + stats, + costs, + costnames; + b = CaptureBackend(), + bp_kwargs = Dict(:logscale => true), + plot_kwargs = Dict(: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), plot_kwargs = Dict(:bar => 2), extra = 3) + result3 = profile_solvers( + stats, + costs, + costnames; + b = CaptureBackend(), + bp_kwargs = Dict(:foo => 1), + plot_kwargs = Dict(: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 From 5b4b4a7d43e09d51cbe8e7a0e8b435e52fc565e3 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 2 Dec 2025 08:21:40 +0530 Subject: [PATCH 13/16] Update profiles.jl --- src/profiles.jl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index 6b34017..8f62131 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -42,15 +42,15 @@ function performance_profile( end """ - p = profile_solvers(stats, costs, costnames; - width = 400, height = 400, - b = PlotsBackend(), bp_kwargs = Dict(), plot_kwargs = Dict(), kwargs...) + p = profile_solvers(stats, costs, costnames; + width = 400, height = 400, + b = PlotsBackend(), bp_kwargs = Dict(), plot_kwargs = Dict(), kwargs...) Produce performance profiles comparing `solvers` based on the data in `stats`. Inputs: - `stats::AbstractDict{Symbol,DataFrame}`: a dictionary of `DataFrame`s containing the - benchmark results per solver (e.g., produced by `bmark_results_to_dataframes()`) + benchmark results per solver (e.g., produced by `bmark_results_to_dataframes()`) - `costs::Vector{Function}`: a vector of functions specifying the measures to use in the profiles - `costnames::Vector{String}`: names to be used as titles of the profiles. @@ -58,11 +58,10 @@ 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 backend `performance_profile` calls - (see `BenchmarkProfiles.performance_profile` — backend-specific options such as `:logscale`). +- `bp_kwargs::Dict` : a `Dict` of keyword arguments forwarded to the backend `performance_profile` calls. - `plot_kwargs::Dict` : a `Dict` of keyword arguments forwarded to the final `plot` call that assembles the profiles. -Additional `kwargs` are passed to the `plot` call for backwards compatibility. +Additional `kwargs` are passed to the `plot` call. Output: A Plots.jl plot representing a set of performance profiles comparing the solvers. From edede0992ceb5851d1051208a0fba09e00e9254f Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Sat, 20 Dec 2025 09:53:27 +0530 Subject: [PATCH 14/16] Remove redundant kwargs parameters - Remove unused args... from performance_profile - Remove redundant bp_kwargs from performance_profile (both bp_kwargs and kwargs were splatted into same call) - Remove redundant plot_kwargs from profile_solvers (both plot_kwargs and kwargs were splatted into same call) - Update documentation to reflect simpler API - Update tests accordingly --- src/profiles.jl | 24 +++++++----------------- test/profiles_kwargs.jl | 6 +++--- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index 8f62131..709fe6b 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -4,7 +4,7 @@ using BenchmarkProfiles, Plots export performance_profile, profile_solvers """ - performance_profile(stats, cost, args...; b = PlotsBackend(), bp_kwargs = Dict(), kwargs...) + performance_profile(stats, cost; b = PlotsBackend(), kwargs...) Produce a performance profile comparing solvers in `stats` using the `cost` function. @@ -16,10 +16,8 @@ Inputs: - `b::BenchmarkProfiles.AbstractBackend` : backend used for the plot. Keyword arguments: -- `bp_kwargs::Dict` : a `Dict` of keyword arguments forwarded to `BenchmarkProfiles.performance_profile` (backend-specific options). - Example: `bp_kwargs = Dict(:logscale => false)` to disable log-scaling when supported by the backend. -- `kwargs...` : additional keyword arguments forwarded to the plotting routines used by the backend. - +- `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. @@ -29,22 +27,20 @@ Examples of cost functions: """ function performance_profile( stats::AbstractDict{Symbol, DataFrame}, - cost::Function, - args...; + cost::Function; b::BenchmarkProfiles.AbstractBackend = PlotsBackend(), - bp_kwargs::Dict = Dict(), kwargs..., ) solvers = keys(stats) dfs = (stats[s] for s in solvers) P = hcat([cost(df) for df in dfs]...) - BenchmarkProfiles.performance_profile(b, P, string.(solvers); bp_kwargs..., kwargs...) + BenchmarkProfiles.performance_profile(b, P, string.(solvers); kwargs...) end """ p = profile_solvers(stats, costs, costnames; width = 400, height = 400, - b = PlotsBackend(), bp_kwargs = Dict(), plot_kwargs = Dict(), kwargs...) + b = PlotsBackend(), bp_kwargs = Dict(), kwargs...) Produce performance profiles comparing `solvers` based on the data in `stats`. @@ -59,12 +55,8 @@ Keyword inputs: - `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 backend `performance_profile` calls. -- `plot_kwargs::Dict` : a `Dict` of keyword arguments forwarded to the final `plot` call that assembles the profiles. - -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 @@ -78,7 +70,6 @@ function profile_solvers( height::Int = 400, b::BenchmarkProfiles.AbstractBackend = PlotsBackend(), bp_kwargs::Dict = Dict(), - plot_kwargs::Dict = Dict(), kwargs..., ) solvers = collect(keys(stats)) @@ -160,7 +151,6 @@ function profile_solvers( ps..., layout = (1 + ipairs, ncosts), size = (ncosts * width, (1 + ipairs) * height); - plot_kwargs..., kwargs..., ) end diff --git a/test/profiles_kwargs.jl b/test/profiles_kwargs.jl index 1672cf4..0ee83ae 100644 --- a/test/profiles_kwargs.jl +++ b/test/profiles_kwargs.jl @@ -26,7 +26,7 @@ function Plots.plot(ps::CapturedPlot...; kwargs...) return (plots = ps, plot_kwargs = kwargs) end -@testset "profiles: bp_kwargs and plot_kwargs forwarding" begin +@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) @@ -54,7 +54,7 @@ end costnames; b = CaptureBackend(), bp_kwargs = Dict(:logscale => true), - plot_kwargs = Dict(:title => "T"), + title = "T", legend = false, ) @test isa(result2, NamedTuple) @@ -67,7 +67,7 @@ end costnames; b = CaptureBackend(), bp_kwargs = Dict(:foo => 1), - plot_kwargs = Dict(:bar => 2), + bar = 2, extra = 3, ) @test (:foo in keys(result3[:plots][1].kwargs)) && result3[:plots][1].kwargs[:foo] == 1 From 02cb208df4e0d321e3acbc741204aa0130be8e55 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Sat, 20 Dec 2025 09:53:46 +0530 Subject: [PATCH 15/16] Removing unwanted test change --- test/pkgbmark.jl | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/pkgbmark.jl b/test/pkgbmark.jl index 42afac9..efa3ea3 100644 --- a/test/pkgbmark.jl +++ b/test/pkgbmark.jl @@ -9,17 +9,6 @@ function test_pkgbmark() get(ENV, "GITHUB_REPOSITORY", "") != "JuliaSmoothOptimizers/SolverBenchmark.jl" return end - # Skip benchmarking tests if the repository has uncommitted changes. - # PkgBenchmark refuses to benchmark a specific commit when the working tree is dirty. - try - git_status = chomp(read(`git status --porcelain`, String)) - if !isempty(git_status) - @info "Skipping package-benchmark tests because repository is dirty" - return - end - catch e - @warn "Could not determine git status; proceeding with pkgbmark tests: $e" - end results = PkgBenchmark.benchmarkpkg("SolverBenchmark", script = joinpath(@__DIR__, "bmark_suite.jl")) From f2202e9a09af59d0b487d6293526fe2529c227a6 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Fri, 3 Apr 2026 11:02:58 +0530 Subject: [PATCH 16/16] Apply suggestions from code review Co-authored-by: Dominique --- src/profiles.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index 709fe6b..71ca624 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -54,7 +54,7 @@ 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 backend `performance_profile` calls. +- `bp_kwargs::Dict` : a `Dict` of keyword arguments forwarded to the `performance_profile` backend calls. 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 @@ -88,10 +88,10 @@ function profile_solvers( b, Ps[1], string.(solvers); - bp_kwargs..., palette = colors, title = costnames[1], legend = :bottomright, + bp_kwargs..., ), ] nsolvers > 2 && xlabel!(ps[1], "") @@ -100,10 +100,10 @@ function profile_solvers( b, Ps[k], string.(solvers); - bp_kwargs..., palette = colors, title = costnames[k], legend = false, + bp_kwargs..., ) nsolvers > 2 && xlabel!(p, "") ylabel!(p, "") @@ -125,9 +125,9 @@ function profile_solvers( b, Ps[1], string.(pair); - bp_kwargs..., palette = clrs, legend = :bottomright, + bp_kwargs..., ) ipairs < npairs && xlabel!(p, "") push!(ps, p) @@ -136,9 +136,9 @@ function profile_solvers( b, Ps[k], string.(pair); - bp_kwargs..., palette = clrs, legend = false, + bp_kwargs..., ) ipairs < npairs && xlabel!(p, "") ylabel!(p, "")