Skip to content

Commit bbfc8f3

Browse files
Merge pull request #1323 from SciML/cutest_local
Improve CUTEst benchmarks
2 parents 3d44359 + a8cbec1 commit bbfc8f3

File tree

9 files changed

+4313
-2
lines changed

9 files changed

+4313
-2
lines changed
0 Bytes
Binary file not shown.

.buildkite/run_benchmark.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ steps:
1919
- JuliaCI/julia#v1:
2020
version: "1.10"
2121
- staticfloat/sandbox:
22-
rootfs_url: "https://github.com/thazhemadam/openmodelica-rootfs-image/releases/download/v1.23.0/rootfs-openmodelica-v1.23.0.amd64.tar.gz"
23-
rootfs_treehash: "82970243dc4f188e599a976abc20951f4aba2912"
22+
rootfs_url: "https://github.com/thazhemadam/SciMLBenchmarks-rootfs-image/releases/download/v1.0.1/sciml-benchmarks-rootfs-image-v1.0.1.amd64.tar.gz"
23+
rootfs_treehash: "1ca61dc27d89849ed03af6c9babdbd4042ddd4f2"
2424
uid: 1000
2525
gid: 1000
2626
workspaces:
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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

Comments
 (0)