Skip to content

Commit 554dcc3

Browse files
authored
Reuse existing data structures on re-solve (#516)
1 parent 3a7aed2 commit 554dcc3

1 file changed

Lines changed: 119 additions & 103 deletions

File tree

ext/IpoptMathOptInterfaceExt/MOI_wrapper.jl

Lines changed: 119 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
5959
vector_nonlinear_oracle_constraints::Vector{
6060
Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache},
6161
}
62+
jacobian_sparsity::Vector{Tuple{Int,Int}}
63+
hessian_sparsity::Union{Nothing,Vector{Tuple{Int,Int}}}
64+
needs_new_inner::Bool
65+
has_only_linear_constraints::Bool
6266

6367
function Optimizer()
6468
return new(
@@ -84,6 +88,10 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
8488
0,
8589
MOI.Nonlinear.SparseReverseMode(),
8690
Tuple{MOI.VectorOfVariables,_VectorNonlinearOracleCache}[],
91+
Tuple{Int,Int}[],
92+
nothing,
93+
true,
94+
false,
8795
)
8896
end
8997
end
@@ -132,6 +140,10 @@ function MOI.empty!(model::Optimizer)
132140
model.barrier_iterations = 0
133141
# SKIP: model.ad_backend
134142
empty!(model.vector_nonlinear_oracle_constraints)
143+
empty!(model.jacobian_sparsity)
144+
model.hessian_sparsity = nothing
145+
model.needs_new_inner = true
146+
model.has_only_linear_constraints = true
135147
return
136148
end
137149

@@ -405,7 +417,7 @@ function MOI.set(
405417
set::S,
406418
) where {S<:_SETS}
407419
MOI.set(model.variables, MOI.ConstraintSet(), ci, set)
408-
model.inner = nothing
420+
model.needs_new_inner = true
409421
return
410422
end
411423

@@ -484,7 +496,7 @@ function MOI.set(
484496
S<:_SETS,
485497
}
486498
MOI.set(model.qp_data, MOI.ConstraintSet(), ci, set)
487-
model.inner = nothing
499+
model.needs_new_inner = true
488500
return
489501
end
490502

@@ -625,7 +637,7 @@ function MOI.set(
625637
index = MOI.Nonlinear.ConstraintIndex(ci.value)
626638
func = model.nlp_model[index].expression
627639
model.nlp_model.constraints[index] = MOI.Nonlinear.Constraint(func, set)
628-
model.inner = nothing
640+
model.needs_new_inner = true
629641
return
630642
end
631643

@@ -950,7 +962,7 @@ function MOI.set(
950962
sense::MOI.OptimizationSense,
951963
)
952964
model.sense = sense
953-
model.inner = nothing
965+
model.needs_new_inner = true
954966
return
955967
end
956968

@@ -1201,66 +1213,31 @@ end
12011213

12021214
### MOI.optimize!
12031215

1204-
function _setup_model(model::Optimizer)
1205-
vars = MOI.get(model.variables, MOI.ListOfVariableIndices())
1206-
if isempty(vars)
1207-
# Don't attempt to create a problem because Ipopt will error.
1208-
model.invalid_model = true
1209-
return
1210-
end
1211-
if model.nlp_model !== nothing
1212-
model.nlp_data = MOI.NLPBlockData(
1213-
MOI.Nonlinear.Evaluator(model.nlp_model, model.ad_backend, vars),
1214-
)
1215-
end
1216-
has_quadratic_constraints =
1217-
any(isequal(_kFunctionTypeScalarQuadratic), model.qp_data.function_type)
1218-
has_nlp_constraints =
1219-
!isempty(model.nlp_data.constraint_bounds) ||
1220-
!isempty(model.vector_nonlinear_oracle_constraints)
1221-
has_hessian = :Hess in MOI.features_available(model.nlp_data.evaluator)
1222-
for (_, s) in model.vector_nonlinear_oracle_constraints
1223-
if s.set.eval_hessian_lagrangian === nothing
1224-
has_hessian = false
1225-
break
1216+
function _eval_jac_g_cb(model, x, rows, cols, values)
1217+
if values === nothing
1218+
for i in 1:length(model.jacobian_sparsity)
1219+
rows[i], cols[i] = model.jacobian_sparsity[i]
12261220
end
1227-
end
1228-
init_feat = [:Grad]
1229-
if has_hessian
1230-
push!(init_feat, :Hess)
1231-
end
1232-
if has_nlp_constraints
1233-
push!(init_feat, :Jac)
1234-
end
1235-
MOI.initialize(model.nlp_data.evaluator, init_feat)
1236-
jacobian_sparsity = MOI.jacobian_structure(model)
1237-
hessian_sparsity = if has_hessian
1238-
MOI.hessian_lagrangian_structure(model)
12391221
else
1240-
Tuple{Int,Int}[]
1241-
end
1242-
eval_f_cb(x) = MOI.eval_objective(model, x)
1243-
eval_grad_f_cb(x, grad_f) = MOI.eval_objective_gradient(model, grad_f, x)
1244-
eval_g_cb(x, g) = MOI.eval_constraint(model, g, x)
1245-
function eval_jac_g_cb(x, rows, cols, values)
1246-
if values === nothing
1247-
for i in 1:length(jacobian_sparsity)
1248-
rows[i], cols[i] = jacobian_sparsity[i]
1249-
end
1250-
else
1251-
MOI.eval_constraint_jacobian(model, values, x)
1252-
end
1253-
return
1222+
MOI.eval_constraint_jacobian(model, values, x)
12541223
end
1255-
function eval_h_cb(x, rows, cols, obj_factor, lambda, values)
1256-
if values === nothing
1257-
for i in 1:length(hessian_sparsity)
1258-
rows[i], cols[i] = hessian_sparsity[i]
1259-
end
1260-
else
1261-
MOI.eval_hessian_lagrangian(model, values, x, obj_factor, lambda)
1224+
return
1225+
end
1226+
1227+
function _eval_h_cb(model, x, rows, cols, obj_factor, lambda, values)
1228+
if values === nothing
1229+
for (i, v) in enumerate(model.hessian_sparsity::Vector{Tuple{Int,Int}})
1230+
rows[i], cols[i] = v
12621231
end
1263-
return
1232+
else
1233+
MOI.eval_hessian_lagrangian(model, values, x, obj_factor, lambda)
1234+
end
1235+
return
1236+
end
1237+
1238+
function _setup_inner(model::Optimizer)::Ipopt.IpoptProblem
1239+
if !model.needs_new_inner
1240+
return model.inner
12641241
end
12651242
g_L, g_U = copy(model.qp_data.g_L), copy(model.qp_data.g_U)
12661243
for (_, s) in model.vector_nonlinear_oracle_constraints
@@ -1271,61 +1248,105 @@ function _setup_model(model::Optimizer)
12711248
push!(g_L, bound.lower)
12721249
push!(g_U, bound.upper)
12731250
end
1251+
function eval_h_cb(x, rows, cols, obj_factor, lambda, values)
1252+
return _eval_h_cb(model, x, rows, cols, obj_factor, lambda, values)
1253+
end
1254+
has_hessian = model.hessian_sparsity !== nothing
12741255
model.inner = Ipopt.CreateIpoptProblem(
1275-
length(vars),
1256+
length(model.variables.lower),
12761257
model.variables.lower,
12771258
model.variables.upper,
12781259
length(g_L),
12791260
g_L,
12801261
g_U,
1281-
length(jacobian_sparsity),
1282-
length(hessian_sparsity),
1283-
eval_f_cb,
1284-
eval_g_cb,
1285-
eval_grad_f_cb,
1286-
eval_jac_g_cb,
1262+
length(model.jacobian_sparsity),
1263+
has_hessian ? length(model.hessian_sparsity) : 0,
1264+
(x) -> MOI.eval_objective(model, x),
1265+
(x, g) -> MOI.eval_constraint(model, g, x),
1266+
(x, grad_f) -> MOI.eval_objective_gradient(model, grad_f, x),
1267+
(x, rows, cols, values) ->
1268+
_eval_jac_g_cb(model, x, rows, cols, values),
12871269
has_hessian ? eval_h_cb : nothing,
12881270
)
1271+
inner = model.inner::Ipopt.IpoptProblem
12891272
if model.sense == MOI.MIN_SENSE
1290-
Ipopt.AddIpoptNumOption(model.inner, "obj_scaling_factor", 1.0)
1273+
Ipopt.AddIpoptNumOption(inner, "obj_scaling_factor", 1.0)
12911274
elseif model.sense == MOI.MAX_SENSE
1292-
Ipopt.AddIpoptNumOption(model.inner, "obj_scaling_factor", -1.0)
1275+
Ipopt.AddIpoptNumOption(inner, "obj_scaling_factor", -1.0)
12931276
end
12941277
# Ipopt crashes by default if NaN/Inf values are returned from the
12951278
# evaluation callbacks. This option tells Ipopt to explicitly check for them
12961279
# and return Invalid_Number_Detected instead. This setting may result in a
12971280
# minor performance loss and can be overwritten by specifying
12981281
# check_derivatives_for_naninf="no".
1299-
Ipopt.AddIpoptStrOption(model.inner, "check_derivatives_for_naninf", "yes")
1282+
Ipopt.AddIpoptStrOption(inner, "check_derivatives_for_naninf", "yes")
13001283
if !has_hessian
13011284
Ipopt.AddIpoptStrOption(
1302-
model.inner,
1285+
inner,
13031286
"hessian_approximation",
13041287
"limited-memory",
13051288
)
13061289
end
1307-
if !has_nlp_constraints && !has_quadratic_constraints
1308-
Ipopt.AddIpoptStrOption(model.inner, "jac_c_constant", "yes")
1309-
Ipopt.AddIpoptStrOption(model.inner, "jac_d_constant", "yes")
1290+
if model.has_only_linear_constraints
1291+
Ipopt.AddIpoptStrOption(inner, "jac_c_constant", "yes")
1292+
Ipopt.AddIpoptStrOption(inner, "jac_d_constant", "yes")
13101293
if !model.nlp_data.has_objective
1311-
# We turn on this option if all constraints are linear and the
1312-
# objective is linear or quadratic. From the documentation, it's
1313-
# unclear if it may also apply if the constraints are at most
1314-
# quadratic.
1315-
Ipopt.AddIpoptStrOption(model.inner, "hessian_constant", "yes")
1294+
Ipopt.AddIpoptStrOption(inner, "hessian_constant", "yes")
13161295
end
13171296
end
1318-
return
1297+
function _moi_callback(args...)
1298+
# iter_count is args[2]
1299+
model.barrier_iterations = args[2]
1300+
if model.callback !== nothing
1301+
return model.callback(args...)
1302+
end
1303+
return true
1304+
end
1305+
Ipopt.SetIntermediateCallback(inner, _moi_callback)
1306+
model.needs_new_inner = false
1307+
return model.inner
13191308
end
13201309

1321-
function copy_parameters(model::Optimizer)
1322-
if model.nlp_model === nothing
1310+
function _setup_model(model::Optimizer)
1311+
if MOI.get(model, MOI.NumberOfVariables()) == 0
1312+
# Don't attempt to create a problem because Ipopt will error.
1313+
model.invalid_model = true
13231314
return
13241315
end
1325-
empty!(model.qp_data.parameters)
1326-
for (p, index) in model.parameters
1327-
model.qp_data.parameters[p.value] = model.nlp_model[index]
1316+
if model.nlp_model !== nothing
1317+
vars = MOI.get(model.variables, MOI.ListOfVariableIndices())
1318+
model.nlp_data = MOI.NLPBlockData(
1319+
MOI.Nonlinear.Evaluator(model.nlp_model, model.ad_backend, vars),
1320+
)
1321+
end
1322+
has_quadratic_constraints =
1323+
any(isequal(_kFunctionTypeScalarQuadratic), model.qp_data.function_type)
1324+
has_nlp_constraints =
1325+
!isempty(model.nlp_data.constraint_bounds) ||
1326+
!isempty(model.vector_nonlinear_oracle_constraints)
1327+
has_hessian = :Hess in MOI.features_available(model.nlp_data.evaluator)
1328+
for (_, s) in model.vector_nonlinear_oracle_constraints
1329+
if s.set.eval_hessian_lagrangian === nothing
1330+
has_hessian = false
1331+
break
1332+
end
1333+
end
1334+
init_feat = [:Grad]
1335+
if has_hessian
1336+
push!(init_feat, :Hess)
1337+
end
1338+
if has_nlp_constraints
1339+
push!(init_feat, :Jac)
1340+
end
1341+
MOI.initialize(model.nlp_data.evaluator, init_feat)
1342+
model.jacobian_sparsity = MOI.jacobian_structure(model)
1343+
model.hessian_sparsity = nothing
1344+
if has_hessian
1345+
model.hessian_sparsity = MOI.hessian_lagrangian_structure(model)
13281346
end
1347+
model.has_only_linear_constraints =
1348+
!has_nlp_constraints && !has_quadratic_constraints
1349+
model.needs_new_inner = true
13291350
return
13301351
end
13311352

@@ -1337,8 +1358,13 @@ function MOI.optimize!(model::Optimizer)
13371358
if model.invalid_model
13381359
return
13391360
end
1340-
copy_parameters(model)
1341-
inner = model.inner::Ipopt.IpoptProblem
1361+
inner = _setup_inner(model)
1362+
if model.nlp_model !== nothing
1363+
empty!(model.qp_data.parameters)
1364+
for (p, index) in model.parameters
1365+
model.qp_data.parameters[p.value] = model.nlp_model[index]
1366+
end
1367+
end
13421368
# The default print level is `5`
13431369
Ipopt.AddIpoptIntOption(inner, "print_level", model.silent ? 0 : 5)
13441370
# Other misc options that over-ride the ones set above.
@@ -1358,13 +1384,12 @@ function MOI.optimize!(model::Optimizer)
13581384
end
13591385
end
13601386
# Initialize the starting point, projecting variables from 0 onto their
1361-
# bounds if VariablePrimalStart is not provided.
1387+
# bounds if VariablePrimalStart is not provided.
13621388
for i in 1:length(model.variable_primal_start)
1363-
inner.x[i] = if model.variable_primal_start[i] !== nothing
1364-
model.variable_primal_start[i]
1365-
else
1366-
clamp(0.0, model.variables.lower[i], model.variables.upper[i])
1367-
end
1389+
inner.x[i] = something(
1390+
model.variable_primal_start[i],
1391+
clamp(0.0, model.variables.lower[i], model.variables.upper[i]),
1392+
)
13681393
end
13691394
for (i, start) in enumerate(model.qp_data.mult_g)
13701395
inner.mult_g[i] = _dual_start(model, start, -1)
@@ -1384,22 +1409,13 @@ function MOI.optimize!(model::Optimizer)
13841409
inner.mult_x_L[i] = _dual_start(model, model.mult_x_L[i])
13851410
inner.mult_x_U[i] = _dual_start(model, model.mult_x_U[i], -1)
13861411
end
1412+
# Reset timers
13871413
model.barrier_iterations = 0
1388-
function _moi_callback(args...)
1389-
# iter_count is args[2]
1390-
model.barrier_iterations = args[2]
1391-
if model.callback !== nothing
1392-
return model.callback(args...)
1393-
end
1394-
return true
1395-
end
1396-
# Clear timers
13971414
for (_, s) in model.vector_nonlinear_oracle_constraints
13981415
s.eval_f_timer = 0.0
13991416
s.eval_jacobian_timer = 0.0
14001417
s.eval_hessian_lagrangian_timer = 0.0
14011418
end
1402-
Ipopt.SetIntermediateCallback(inner, _moi_callback)
14031419
Ipopt.IpoptSolve(inner)
14041420
model.solve_time = time() - start_time
14051421
return

0 commit comments

Comments
 (0)