|
| 1 | +--- |
| 2 | +title: CUTEst Bounded Constrained Nonlinear Optimization Benchmarks |
| 3 | +author: Alonso M. Cisneros |
| 4 | +--- |
| 5 | + |
| 6 | +# Introduction |
| 7 | + |
| 8 | +CUTEst, the Constraind and Unconstrained Testing Environment is, as the name suggests is a |
| 9 | +collection of around 1500 problems for general nonlinear optimization used to test |
| 10 | +optimization routines. The wrapper |
| 11 | +[CUTEst.jl](https://github.com/JuliaSmoothOptimizers/CUTEst.jl) provides convenient access |
| 12 | +to the problem collection, which we can leverage to test the optimizers made available by |
| 13 | +Optimization.jl. |
| 14 | + |
| 15 | + |
| 16 | +```julia |
| 17 | +using Optimization |
| 18 | +using OptimizationNLPModels |
| 19 | +using CUTEst |
| 20 | +using OptimizationOptimJL |
| 21 | +using OptimizationOptimisers |
| 22 | +using Ipopt |
| 23 | +using OptimizationMOI |
| 24 | +using OptimizationMOI: MOI as MOI |
| 25 | +using DataFrames |
| 26 | +using Plots |
| 27 | +using StatsPlots |
| 28 | +using StatsBase: countmap |
| 29 | + |
| 30 | +optimizers = [ |
| 31 | + ("Ipopt", MOI.OptimizerWithAttributes(Ipopt.Optimizer, |
| 32 | + "max_iter" => 5000, |
| 33 | + "tol" => 1e-6, |
| 34 | + "print_level" => 5)), |
| 35 | +] |
| 36 | + |
| 37 | +function get_stats(sol, optimizer_name) |
| 38 | + # Robustly get solve_time, even if stats or time is missing |
| 39 | + solve_time = try |
| 40 | + hasfield(typeof(sol), :stats) && hasfield(typeof(sol.stats), :time) ? getfield(sol.stats, :time) : NaN |
| 41 | + catch |
| 42 | + NaN |
| 43 | + end |
| 44 | + return (length(sol.u), solve_time, optimizer_name, Symbol(sol.retcode)) |
| 45 | +end |
| 46 | + |
| 47 | +function run_benchmarks(problems, optimizers; chunk_size=1) |
| 48 | + problem = String[] |
| 49 | + n_vars = Int64[] |
| 50 | + secs = Float64[] |
| 51 | + solver = String[] |
| 52 | + retcode = Symbol[] |
| 53 | + optz = length(optimizers) |
| 54 | + n = length(problems) |
| 55 | + @info "Processing $(n) problems with $(optz) optimizers in chunks of $(chunk_size)" |
| 56 | + broadcast(c -> sizehint!(c, optz * n), [problem, n_vars, secs, solver, retcode]) |
| 57 | + for chunk_start in 1:chunk_size:n |
| 58 | + chunk_end = min(chunk_start + chunk_size - 1, n) |
| 59 | + chunk_problems = problems[chunk_start:chunk_end] |
| 60 | + @info "Processing chunk $(div(chunk_start-1, chunk_size)+1)/$(div(n-1, chunk_size)+1): problems $(chunk_start)-$(chunk_end)" |
| 61 | + for (idx, prob_name) in enumerate(chunk_problems) |
| 62 | + current_problem = chunk_start + idx - 1 |
| 63 | + @info "Problem $(current_problem)/$(n): $(prob_name)" |
| 64 | + nlp_prob = nothing |
| 65 | + try |
| 66 | + nlp_prob = CUTEstModel(prob_name) |
| 67 | + if nlp_prob.meta.nvar > 10000 |
| 68 | + @info " Skipping $(prob_name) (too large: $(nlp_prob.meta.nvar) variables)" |
| 69 | + finalize(nlp_prob) |
| 70 | + continue |
| 71 | + end |
| 72 | + prob = OptimizationNLPModels.OptimizationProblem(nlp_prob, Optimization.AutoFiniteDiff()) |
| 73 | + for (optimizer_name, optimizer) in optimizers |
| 74 | + try |
| 75 | + sol = solve(prob, optimizer; maxiters = 1000, maxtime = 30.0) |
| 76 | + @info "✓ Solved $(prob_name) with $(optimizer_name) - Status: $(sol.retcode)" |
| 77 | + vars, time, alg, code = get_stats(sol, optimizer_name) |
| 78 | + push!(problem, prob_name) |
| 79 | + push!(n_vars, vars) |
| 80 | + push!(secs, time) |
| 81 | + push!(solver, alg) |
| 82 | + push!(retcode, code) |
| 83 | + catch e |
| 84 | + push!(problem, prob_name) |
| 85 | + push!(n_vars, nlp_prob !== nothing ? nlp_prob.meta.nvar : -1) |
| 86 | + push!(secs, NaN) |
| 87 | + push!(solver, optimizer_name) |
| 88 | + push!(retcode, :FAILED) |
| 89 | + end |
| 90 | + end |
| 91 | + catch e |
| 92 | + for (optimizer_name, optimizer) in optimizers |
| 93 | + push!(problem, prob_name) |
| 94 | + push!(n_vars, -1) |
| 95 | + push!(secs, NaN) |
| 96 | + push!(solver, optimizer_name) |
| 97 | + push!(retcode, :LOAD_FAILED) |
| 98 | + end |
| 99 | + finally |
| 100 | + if nlp_prob !== nothing |
| 101 | + try |
| 102 | + finalize(nlp_prob) |
| 103 | + catch e |
| 104 | + end |
| 105 | + end |
| 106 | + end |
| 107 | + end |
| 108 | + GC.gc() |
| 109 | + @info "Completed chunk, memory usage cleaned up" |
| 110 | + end |
| 111 | + return DataFrame(problem = problem, n_vars = n_vars, secs = secs, solver = solver, retcode = retcode) |
| 112 | +end |
| 113 | +``` |
| 114 | + |
| 115 | +# Benchmarks |
| 116 | + |
| 117 | +We will be testing the [Ipopt]() and the [LBFGS]() optimizers on these classes of |
| 118 | +problems. |
| 119 | + |
| 120 | +``` |
| 121 | + |
| 122 | +## Equality/Inequality constrained problems with bounded variables |
| 123 | + |
| 124 | +Now we analyze the subset of problems with equality/inequality constraints and whose |
| 125 | +variables are bounded. There are 666 such problems for equality constraints and 244 for inequality constraints. |
| 126 | + |
| 127 | +The following figure shows the results of the same benchmarks previously described for the |
| 128 | +problems on this section. |
| 129 | + |
| 130 | +```julia |
| 131 | +@info "before" |
| 132 | + |
| 133 | +# Select a moderate subset of equality-constrained bounded problems for a realistic mix |
| 134 | +eq_bou_problems = CUTEst.select_sif_problems(min_con=1, only_equ_con=true, only_free_var=false) |
| 135 | +eq_bou_problems = eq_bou_problems[1:min(30, length(eq_bou_problems))] |
| 136 | + # Skip HIER13, BLOWEYA, LUKVLE8, READING2, NINENEW, READING6, DITTERT, CVXQP2, and MSS1 if present |
| 137 | +eq_bou_problems = filter(p -> !(lowercase(p) in ["hier13", "bloweya", "lukvle8", "patternne", "reading2", "ninenew", "reading6", "dittert", "cvxqp2", "mss1"]), eq_bou_problems) |
| 138 | +@info "Testing $(length(eq_bou_problems)) equality-constrained bounded problems (subset)" |
| 139 | + |
| 140 | +# Analysis |
| 141 | +eq_bou_results = run_benchmarks(eq_bou_problems, optimizers; chunk_size=3) |
| 142 | + |
| 143 | +total_attempts = nrow(eq_bou_results) |
| 144 | +successful_codes = [:Success, :MaxIters, :MaxTime, :FirstOrderOptimal] |
| 145 | +successful_results = filter(row -> row.retcode in successful_codes, eq_bou_results) |
| 146 | +successful_attempts = nrow(successful_results) |
| 147 | +success_rate = total_attempts > 0 ? round(successful_attempts / total_attempts * 100, digits=1) : 0 |
| 148 | + |
| 149 | +# Calculate and display success rates |
| 150 | +successful_codes = [:Success, :MaxIters, :MaxTime, :FirstOrderOptimal] |
| 151 | +successful_results = filter(row -> row.retcode in successful_codes, eq_bou_results) |
| 152 | +total_attempts = nrow(eq_bou_results) |
| 153 | +successful_attempts = nrow(successful_results) |
| 154 | +success_rate = total_attempts > 0 ? round(successful_attempts / total_attempts * 100, digits=1) : 0 |
| 155 | + |
| 156 | +println("Full results table for equality-constrained bounded problems:") |
| 157 | +display(eq_bou_results) |
| 158 | + |
| 159 | +println("SUCCESS RATE ANALYSIS (Equality Constrained, Bounded):") |
| 160 | +println("Total attempts: ", total_attempts) |
| 161 | +println("Successful attempts: ", successful_attempts) |
| 162 | +println("Success rate: ", success_rate, "%") |
| 163 | +println("Return code distribution:") |
| 164 | +if total_attempts > 0 |
| 165 | + for (code, count) in sort(collect(pairs(countmap(eq_bou_results.retcode))), by=x->x[2], rev=true) |
| 166 | + println(" ", code, ": ", count, " occurrences") |
| 167 | + end |
| 168 | +else |
| 169 | + println(" No results to analyze") |
| 170 | +end |
| 171 | + |
| 172 | +if nrow(eq_bou_results) > 0 |
| 173 | + @df eq_bou_results scatter(:n_vars, :secs, |
| 174 | + group = :solver, |
| 175 | + xlabel = "n. variables", |
| 176 | + ylabel = "secs.", |
| 177 | + title = "Time to solution by optimizer and number of vars", |
| 178 | + ) |
| 179 | + println("Plotted equality-constrained bounded results.") |
| 180 | +else |
| 181 | + println("No equality-constrained bounded results to plot. DataFrame is empty.") |
| 182 | + println("Attempted problems:") |
| 183 | + println(eq_bou_problems) |
| 184 | +end |
| 185 | +``` |
| 186 | + |
| 187 | +Next, we examine the same relationship for inequality-constrained problems. |
| 188 | + |
| 189 | +```julia |
| 190 | +@info "after4" |
| 191 | + |
| 192 | +# Select a moderate subset of inequality-constrained bounded problems for a realistic mix |
| 193 | +neq_bou_problems = CUTEst.select_sif_problems(min_con=1, only_ineq_con=true, only_free_var=false) |
| 194 | +neq_bou_problems = neq_bou_problems[1:min(30, length(neq_bou_problems))] |
| 195 | + # Skip HIER13, BLOWEYA, CHARDIS1, LUKVLE8, READING2, CVPXQ2, and MSS1 if present (in case they appear in this list too) |
| 196 | +neq_bou_problems = filter(p -> !(lowercase(p) in ("chardis1", "hs67", "hs101", "haifal", "himmelp2", "hanging", "synthes1", "lukvli13", "liswet9", "hs85", "lukvli7", "expfita", "s268")), neq_bou_problems) |
| 197 | +@info "Testing $(length(neq_bou_problems)) inequality-constrained bounded problems (subset)" |
| 198 | + |
| 199 | +# Analysis |
| 200 | +neq_bou_results = run_benchmarks(neq_bou_problems, optimizers; chunk_size=3) |
| 201 | + |
| 202 | +total_attempts = nrow(neq_bou_results) |
| 203 | +successful_codes = [:Success, :MaxIters, :MaxTime, :FirstOrderOptimal] |
| 204 | +successful_results = filter(row -> row.retcode in successful_codes, neq_bou_results) |
| 205 | +successful_attempts = nrow(successful_results) |
| 206 | +success_rate = total_attempts > 0 ? round(successful_attempts / total_attempts * 100, digits=1) : 0 |
| 207 | + |
| 208 | +# Calculate and display success rates |
| 209 | +successful_codes = [:Success, :MaxIters, :MaxTime, :FirstOrderOptimal] |
| 210 | +successful_results = filter(row -> row.retcode in successful_codes, neq_bou_results) |
| 211 | +total_attempts = nrow(neq_bou_results) |
| 212 | +successful_attempts = nrow(successful_results) |
| 213 | +success_rate = total_attempts > 0 ? round(successful_attempts / total_attempts * 100, digits=1) : 0 |
| 214 | + |
| 215 | +println("Full results table for inequality-constrained bounded problems:") |
| 216 | +display(neq_bou_results) |
| 217 | + |
| 218 | +println("SUCCESS RATE ANALYSIS (Inequality Constrained, Bounded):") |
| 219 | +println("Total attempts: ", total_attempts) |
| 220 | +println("Successful attempts: ", successful_attempts) |
| 221 | +println("Success rate: ", success_rate, "%") |
| 222 | +println("Return code distribution:") |
| 223 | +if total_attempts > 0 |
| 224 | + for (code, count) in sort(collect(pairs(countmap(neq_bou_results.retcode))), by=x->x[2], rev=true) |
| 225 | + println(" ", code, ": ", count, " occurrences") |
| 226 | + end |
| 227 | +else |
| 228 | + println(" No results to analyze") |
| 229 | +end |
| 230 | + |
| 231 | +if nrow(neq_bou_results) > 0 |
| 232 | + @df neq_bou_results scatter(:n_vars, :secs, |
| 233 | + group = :solver, |
| 234 | + xlabel = "n. variables", |
| 235 | + ylabel = "secs.", |
| 236 | + title = "Time to solution by optimizer and number of vars", |
| 237 | + ) |
| 238 | + println("Plotted inequality-constrained bounded results.") |
| 239 | +else |
| 240 | + println("No inequality-constrained bounded results to plot. DataFrame is empty.") |
| 241 | + println("Attempted problems:") |
| 242 | + println(neq_bou_problems) |
| 243 | +end |
| 244 | +``` |
| 245 | + |
| 246 | +```julia, echo = false |
| 247 | +using SciMLBenchmarks |
| 248 | +SciMLBenchmarks.bench_footer(WEAVE_ARGS[:folder],WEAVE_ARGS[:file]) |
| 249 | +``` |
0 commit comments