Skip to content

Commit 00f3978

Browse files
committed
Move the variables and the parameter convention into ModelWithQuad
ModelWithQuad now owns a MOI.Utilities.VariablesContainer and implements MOI.add_variable, guaranteeing variable indices 1:n like MOI.Utilities.MatrixOfConstraints, so the EvaluatorWithQuad no longer remaps the QP block. Parameters are added with MOI.add_constrained_variable: their indices are offset by _PARAMETER_OFFSET (moved here from Ipopt), QPBlockData is back to the simple offset-based _is_parameter instead of the parameters dictionary, and its parameters vector aliases the parameter storage of the inner Nonlinear.Model, so solvers no longer sync parameter values before a solve.
1 parent e655a28 commit 00f3978

3 files changed

Lines changed: 234 additions & 171 deletions

File tree

src/Nonlinear/model_with_quad.jl

Lines changed: 131 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,33 @@
1111
objective_sink::Symbol = :none,
1212
) where {T,M}
1313
14-
A model layer that stores affine and quadratic objectives and constraints in
15-
a [`QPBlockData`](@ref) and forwards everything else to the `inner` model,
16-
typically a [`Model`](@ref).
14+
A model layer that owns the variables of the model, stores affine and
15+
quadratic objectives and constraints in a [`QPBlockData`](@ref), and forwards
16+
everything else to the `inner` model, typically a [`Model`](@ref).
1717
1818
`ModelWithQuad(inner)` and `ModelWithQuad{T}(inner)` create an empty
1919
[`QPBlockData`](@ref), with `T` defaulting to `Float64`.
2020
21+
Add variables with `MOI.add_variable`: the layer guarantees that the variable
22+
indices are `1:n`, like `MOI.Utilities.MatrixOfConstraints`. Add parameters
23+
with `MOI.add_constrained_variable(model, ::MOI.Parameter)`: parameters get
24+
indices offset by [`_PARAMETER_OFFSET`](@ref), their values are stored in the
25+
inner model through [`add_parameter`](@ref), and `qp.parameters` aliases that
26+
storage, so a parameter update is visible to both blocks.
27+
2128
Add constraints with [`add_constraint`](@ref) or `MOI.add_constraint`, and
2229
set the objective with [`set_objective`](@ref): affine and quadratic
2330
functions are routed to the QP block, everything else to the inner model.
2431
`objective_sink` records where the objective currently lives (`:none`,
2532
`:quad` or `:inner`).
2633
2734
Create the corresponding evaluator, [`EvaluatorWithQuad`](@ref), with
28-
`Evaluator(model, backend, ordered_variables)`, or construct it directly from
29-
an inner `MOI.AbstractNLPEvaluator`. The rows of the QP block come first,
30-
followed by the rows of the inner evaluator.
31-
32-
Functions added to the QP block may contain parameters, following the
33-
convention documented in [`QPBlockData`](@ref): a variable is a parameter if
34-
and only if its index is a key of `qp.parameters`.
35+
`Evaluator(model, backend)`, or construct it directly from an inner
36+
`MOI.AbstractNLPEvaluator`. The rows of the QP block come first, followed by
37+
the rows of the inner evaluator.
3538
"""
3639
mutable struct ModelWithQuad{T,M}
40+
variables::MOI.Utilities.VariablesContainer{T}
3741
qp::QPBlockData{T}
3842
inner::M
3943
objective_sink::Symbol # :none, :quad or :inner
@@ -43,7 +47,14 @@ mutable struct ModelWithQuad{T,M}
4347
inner::M;
4448
objective_sink::Symbol = :none,
4549
) where {T,M}
46-
return new{T,M}(qp, inner, objective_sink)
50+
model = new{T,M}(
51+
MOI.Utilities.VariablesContainer{T}(),
52+
qp,
53+
inner,
54+
objective_sink,
55+
)
56+
_share_parameters(model.qp, model.inner)
57+
return model
4758
end
4859
end
4960

@@ -53,6 +64,83 @@ end
5364

5465
ModelWithQuad(inner) = ModelWithQuad{Float64}(inner)
5566

67+
# The QP block reads the parameter values from the inner model's storage.
68+
_share_parameters(::QPBlockData, ::Any) = nothing
69+
70+
function _share_parameters(qp::QPBlockData{Float64}, inner::Model)
71+
qp.parameters = inner.parameters
72+
return
73+
end
74+
75+
# The variables and the parameters.
76+
77+
MOI.add_variable(model::ModelWithQuad) = MOI.add_variable(model.variables)
78+
79+
function MOI.add_constrained_variable(
80+
model::ModelWithQuad{T},
81+
set::MOI.Parameter{T},
82+
) where {T}
83+
p = add_parameter(model.inner, set.value)
84+
x = MOI.VariableIndex(_PARAMETER_OFFSET + p.value)
85+
ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}}(x.value)
86+
return x, ci
87+
end
88+
89+
function MOI.is_valid(model::ModelWithQuad, x::MOI.VariableIndex)
90+
if _is_parameter(x)
91+
return 1 <= x.value - _PARAMETER_OFFSET <= length(model.qp.parameters)
92+
end
93+
return MOI.is_valid(model.variables, x)
94+
end
95+
96+
function MOI.is_valid(
97+
model::ModelWithQuad{T},
98+
ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}},
99+
) where {T}
100+
return MOI.is_valid(model, MOI.VariableIndex(ci.value))
101+
end
102+
103+
function MOI.get(
104+
model::ModelWithQuad{T},
105+
::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.Parameter{T}},
106+
) where {T}
107+
return length(model.qp.parameters)
108+
end
109+
110+
function MOI.get(
111+
model::ModelWithQuad{T},
112+
::MOI.ListOfConstraintIndices{F,S},
113+
) where {T,F<:MOI.VariableIndex,S<:MOI.Parameter{T}}
114+
n = length(model.qp.parameters)
115+
return MOI.ConstraintIndex{F,S}.(_PARAMETER_OFFSET .+ (1:n))
116+
end
117+
118+
function MOI.get(
119+
model::ModelWithQuad{T},
120+
::MOI.ConstraintFunction,
121+
ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}},
122+
) where {T}
123+
return MOI.VariableIndex(ci.value)
124+
end
125+
126+
function MOI.get(
127+
model::ModelWithQuad{T},
128+
::MOI.ConstraintSet,
129+
ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}},
130+
) where {T}
131+
return MOI.Parameter(model.qp.parameters[ci.value-_PARAMETER_OFFSET])
132+
end
133+
134+
function MOI.set(
135+
model::ModelWithQuad{T},
136+
::MOI.ConstraintSet,
137+
ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Parameter{T}},
138+
set::MOI.Parameter{T},
139+
) where {T}
140+
model.qp.parameters[ci.value-_PARAMETER_OFFSET] = set.value
141+
return
142+
end
143+
56144
"""
57145
Base.length(model::ModelWithQuad)
58146
@@ -204,101 +292,63 @@ end
204292
EvaluatorWithQuad(
205293
model::ModelWithQuad,
206294
inner::MOI.AbstractNLPEvaluator,
207-
ordered_variables::Vector{MOI.VariableIndex},
208295
) <: MOI.AbstractNLPEvaluator
209296
210297
The evaluator of a [`ModelWithQuad`](@ref) layer. It implements the
211298
[`MOI.AbstractNLPEvaluator`](@ref) interface: the rows of the QP block come
212299
first, followed by the rows of `inner`, and the Jacobian and Hessian product
213300
callbacks compose the contributions of the two blocks.
214301
215-
Create it with `Evaluator(model::ModelWithQuad, backend, ordered_variables)`,
216-
which recursively creates the evaluator of the inner model, or construct it
217-
directly from an existing inner evaluator.
302+
Create it with `Evaluator(model::ModelWithQuad, backend)`, which recursively
303+
creates the evaluator of the inner model, or construct it directly from an
304+
existing inner evaluator.
218305
219-
During [`MOI.initialize`](@ref), the variables of the QP block are mapped to
220-
their consecutive 1-based index in `ordered_variables`; variables absent from
221-
`ordered_variables` are parameters and keep their index.
306+
The QP block is evaluated as stored: [`ModelWithQuad`](@ref) owns the
307+
variables of the model, so their indices are the columns `1:n` and no
308+
remapping is needed.
222309
"""
223310
mutable struct EvaluatorWithQuad{T,M,E<:MOI.AbstractNLPEvaluator} <:
224311
MOI.AbstractNLPEvaluator
225312
model::ModelWithQuad{T,M}
226313
inner::E
227-
ordered_variables::Vector{MOI.VariableIndex}
228-
# A copy of `model.qp` with the variables mapped to their consecutive
229-
# 1-based index in `ordered_variables`, and the number of entries of its
230-
# Jacobian and of its Hessian of the Lagrangian. Rebuilt during
231-
# `MOI.initialize`.
232-
qp::QPBlockData{T}
314+
# The number of entries of the Jacobian and of the Hessian of the
315+
# Lagrangian of the QP block, computed during `MOI.initialize`.
233316
qp_nnzj::Int
234317
qp_nnzh::Int
235318

236319
function EvaluatorWithQuad(
237320
model::ModelWithQuad{T,M},
238321
inner::E,
239-
ordered_variables::Vector{MOI.VariableIndex},
240322
) where {T,M,E<:MOI.AbstractNLPEvaluator}
241-
return new{T,M,E}(
242-
model,
243-
inner,
244-
ordered_variables,
245-
QPBlockData{T}(),
246-
0,
247-
0,
248-
)
323+
return new{T,M,E}(model, inner, 0, 0)
249324
end
250325
end
251326

252327
function Evaluator(
253328
model::ModelWithQuad,
254329
backend::AbstractAutomaticDifferentiation,
255-
ordered_variables::Vector{MOI.VariableIndex},
256330
)
257-
inner = Evaluator(model.inner, backend, ordered_variables)
258-
return EvaluatorWithQuad(model, inner, ordered_variables)
331+
vars = MOI.get(model.variables, MOI.ListOfVariableIndices())
332+
inner = Evaluator(model.inner, backend, vars)
333+
return EvaluatorWithQuad(model, inner)
259334
end
260335

261336
function MOI.features_available(d::EvaluatorWithQuad)
262337
features = MOI.features_available(d.inner)
263338
return filter(f -> f in (:Grad, :Jac, :JacVec, :Hess, :HessVec), features)
264339
end
265340

266-
function MOI.initialize(
267-
d::EvaluatorWithQuad{T},
268-
features::Vector{Symbol},
269-
) where {T}
270-
index_map = Dict{MOI.VariableIndex,MOI.VariableIndex}(
271-
x => MOI.VariableIndex(i) for (i, x) in enumerate(d.ordered_variables)
272-
)
273-
# Variables absent from `ordered_variables` are parameters: they keep
274-
# their index, which the `parameters` dictionary uses as key.
275-
fmap = v::MOI.VariableIndex -> get(index_map, v, v)
276-
src = d.model.qp
277-
qp = QPBlockData{T}()
278-
qp.objective = MOI.Utilities.map_indices(fmap, src.objective)
279-
qp.objective_function_type = src.objective_function_type
280-
for f in src.constraints
281-
push!(qp.constraints, MOI.Utilities.map_indices(fmap, f))
282-
end
283-
append!(qp.g_L, src.g_L)
284-
append!(qp.g_U, src.g_U)
285-
append!(qp.mult_g, src.mult_g)
286-
append!(qp.function_type, src.function_type)
287-
append!(qp.bound_type, src.bound_type)
288-
# Alias, do not copy: parameter values updated in the model between
289-
# solves must be visible to the evaluator without re-initializing.
290-
qp.parameters = src.parameters
291-
d.qp = qp
292-
d.qp_nnzj = length(MOI.jacobian_structure(qp))
293-
d.qp_nnzh = length(MOI.hessian_lagrangian_structure(qp))
341+
function MOI.initialize(d::EvaluatorWithQuad, features::Vector{Symbol})
342+
d.qp_nnzj = length(MOI.jacobian_structure(d.model.qp))
343+
d.qp_nnzh = length(MOI.hessian_lagrangian_structure(d.model.qp))
294344
MOI.initialize(d.inner, features)
295345
return
296346
end
297347

298348
function MOI.eval_objective(d::EvaluatorWithQuad{T}, x) where {T}
299349
sink = d.model.objective_sink
300350
if sink == :quad
301-
return MOI.eval_objective(d.qp, x)
351+
return MOI.eval_objective(d.model.qp, x)
302352
elseif sink == :inner
303353
return MOI.eval_objective(d.inner, x)
304354
else
@@ -309,7 +359,7 @@ end
309359
function MOI.eval_objective_gradient(d::EvaluatorWithQuad{T}, grad, x) where {T}
310360
sink = d.model.objective_sink
311361
if sink == :quad
312-
MOI.eval_objective_gradient(d.qp, grad, x)
362+
MOI.eval_objective_gradient(d.model.qp, grad, x)
313363
elseif sink == :inner
314364
MOI.eval_objective_gradient(d.inner, grad, x)
315365
else
@@ -319,15 +369,15 @@ function MOI.eval_objective_gradient(d::EvaluatorWithQuad{T}, grad, x) where {T}
319369
end
320370

321371
function MOI.eval_constraint(d::EvaluatorWithQuad, g, x)
322-
m = length(d.qp)
323-
MOI.eval_constraint(d.qp, view(g, 1:m), x)
372+
m = length(d.model.qp)
373+
MOI.eval_constraint(d.model.qp, view(g, 1:m), x)
324374
MOI.eval_constraint(d.inner, view(g, (m+1):length(g)), x)
325375
return
326376
end
327377

328378
function MOI.jacobian_structure(d::EvaluatorWithQuad)
329-
J = MOI.jacobian_structure(d.qp)
330-
offset = length(d.qp)
379+
J = MOI.jacobian_structure(d.model.qp)
380+
offset = length(d.model.qp)
331381
# An evaluator is only required to implement `jacobian_structure` if it
332382
# supports `:Jac`. If the inner evaluator does not (it then must not have
333383
# any rows for the stack to be usable), append nothing.
@@ -340,24 +390,24 @@ function MOI.jacobian_structure(d::EvaluatorWithQuad)
340390
end
341391

342392
function MOI.eval_constraint_jacobian(d::EvaluatorWithQuad, J, x)
343-
MOI.eval_constraint_jacobian(d.qp, J, x)
393+
MOI.eval_constraint_jacobian(d.model.qp, J, x)
344394
MOI.eval_constraint_jacobian(d.inner, view(J, (d.qp_nnzj+1):length(J)), x)
345395
return
346396
end
347397

348398
function MOI.hessian_lagrangian_structure(d::EvaluatorWithQuad)
349-
H = MOI.hessian_lagrangian_structure(d.qp)
399+
H = MOI.hessian_lagrangian_structure(d.model.qp)
350400
if :Hess in MOI.features_available(d.inner)
351401
append!(H, MOI.hessian_lagrangian_structure(d.inner))
352402
end
353403
return H
354404
end
355405

356406
function MOI.eval_hessian_lagrangian(d::EvaluatorWithQuad, H, x, σ, μ)
357-
m = length(d.qp)
358-
# If the objective is not in the QP block, `d.qp.objective` is zero, so
407+
m = length(d.model.qp)
408+
# If the objective is not in the QP block, `d.model.qp.objective` is zero, so
359409
# passing `σ` is harmless; and vice versa for the inner evaluator.
360-
MOI.eval_hessian_lagrangian(d.qp, H, x, σ, view(μ, 1:m))
410+
MOI.eval_hessian_lagrangian(d.model.qp, H, x, σ, view(μ, 1:m))
361411
MOI.eval_hessian_lagrangian(
362412
d.inner,
363413
view(H, (d.qp_nnzh+1):length(H)),
@@ -372,14 +422,14 @@ end
372422
# block write its own rows.
373423
function MOI.eval_constraint_jacobian_product(d::EvaluatorWithQuad, y, x, w)
374424
fill!(y, zero(eltype(y)))
375-
m = length(d.qp)
425+
m = length(d.model.qp)
376426
MOI.eval_constraint_jacobian_product(
377427
d.inner,
378428
view(y, (m+1):length(y)),
379429
x,
380430
w,
381431
)
382-
_add_constraint_jacobian_product(d.qp, y, x, w)
432+
_add_constraint_jacobian_product(d.model.qp, y, x, w)
383433
return
384434
end
385435

@@ -393,14 +443,14 @@ function MOI.eval_constraint_jacobian_transpose_product(
393443
w,
394444
)
395445
fill!(y, zero(eltype(y)))
396-
m = length(d.qp)
446+
m = length(d.model.qp)
397447
MOI.eval_constraint_jacobian_transpose_product(
398448
d.inner,
399449
y,
400450
x,
401451
view(w, (m+1):length(w)),
402452
)
403-
_add_constraint_jacobian_transpose_product(d.qp, y, x, view(w, 1:m))
453+
_add_constraint_jacobian_transpose_product(d.model.qp, y, x, view(w, 1:m))
404454
return
405455
end
406456

@@ -413,7 +463,7 @@ function MOI.eval_hessian_lagrangian_product(
413463
μ,
414464
)
415465
fill!(H, zero(eltype(H)))
416-
m = length(d.qp)
466+
m = length(d.model.qp)
417467
MOI.eval_hessian_lagrangian_product(
418468
d.inner,
419469
H,
@@ -422,7 +472,7 @@ function MOI.eval_hessian_lagrangian_product(
422472
σ,
423473
view(μ, (m+1):length(μ)),
424474
)
425-
_add_hessian_lagrangian_product(d.qp, H, x, v, σ, view(μ, 1:m))
475+
_add_hessian_lagrangian_product(d.model.qp, H, x, v, σ, view(μ, 1:m))
426476
return
427477
end
428478

0 commit comments

Comments
 (0)