Skip to content

Commit e5d92b2

Browse files
add ParametricVectorQuadraticFunction
1 parent 667cb15 commit e5d92b2

7 files changed

Lines changed: 362 additions & 0 deletions

File tree

src/MOI_wrapper.jl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ function _has_parameters(f::MOI.ScalarQuadraticFunction{T}) where {T}
5151
return false
5252
end
5353

54+
function _has_parameters(f::MOI.VectorQuadraticFunction)
55+
# quadratic part
56+
for qt in f.quadratic_terms
57+
if _has_parameters(qt.scalar_term)
58+
return true
59+
end
60+
end
61+
# affine part
62+
for at in f.affine_terms
63+
if _has_parameters(at.scalar_term)
64+
return true
65+
end
66+
end
67+
return false
68+
end
69+
5470
function _cache_multiplicative_params!(
5571
model::Optimizer{T},
5672
f::ParametricQuadraticFunction{T},
@@ -858,6 +874,23 @@ function MOI.add_constraint(
858874
end
859875
end
860876

877+
function add_constraint(model::Optimizer,
878+
f::MOI.VectorQuadraticFunction{T},
879+
S::MOI.AbstractVectorSet) where {T}
880+
if _has_parameters(f)
881+
pvqf = ParametricVectorQuadraticFunction(f) # strip parameters
882+
ci = MOI.add_constraint(model.optimizer,
883+
pvqf.current_function,
884+
S) # plain MOI call
885+
pvqf.ci = ci # remember link
886+
# cache is a DoubleDict keyed by (F,S) like the other caches
887+
model.vector_quadratic_constraint_cache[pvqf, S] = pvqf
888+
return ci
889+
else
890+
return MOI.add_constraint(model.optimizer, f, S) # non-parametric
891+
end
892+
end
893+
861894
function MOI.delete(
862895
model::Optimizer,
863896
c::MOI.ConstraintIndex{F,S},
@@ -1411,6 +1444,13 @@ function MOI.get(
14111444
return model.quadratic_constraint_cache[F, S]
14121445
end
14131446

1447+
function MOI.get(
1448+
model::Optimizer,
1449+
::DictOfParametricConstraintIndicesAndFunctions{F,S,P},
1450+
) where {F,S,P<:ParametricVectorQuadraticFunction}
1451+
return model.vector_quadratic_constraint_cache[F, S]
1452+
end
1453+
14141454
"""
14151455
NumberOfPureVariables
14161456

src/ParametricOptInterface.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer
137137
quadratic_constraint_cache::DoubleDict{ParametricQuadraticFunction{T}}
138138
# Store original constraint set (inner key)
139139
quadratic_constraint_cache_set::DoubleDict{MOI.AbstractScalarSet}
140+
# Vector quadratic function data
141+
vector_quadratic_constraint_cache::DoubleDict{ParametricVectorQuadraticFunction{T}}
140142

141143
# objective function data
142144
# Clever cache of data (at most one can be !== nothing)
@@ -209,6 +211,7 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer
209211
DoubleDict{MOI.ConstraintIndex}(),
210212
DoubleDict{ParametricQuadraticFunction{T}}(),
211213
DoubleDict{MOI.AbstractScalarSet}(),
214+
DoubleDict{ParametricVectorQuadraticFunction{T}}(),
212215
# objective
213216
nothing,
214217
nothing,

src/duals.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ function _compute_dual_of_parameters!(model::Optimizer{T}) where {T}
99
_update_duals_from_affine_constraints!(model)
1010
_update_duals_from_vector_affine_constraints!(model)
1111
_update_duals_from_quadratic_constraints!(model)
12+
_update_duals_from_vector_quadratic_constraints!(model)
1213
if model.affine_objective_cache !== nothing
1314
_update_duals_from_objective!(model, model.affine_objective_cache)
1415
end
@@ -174,3 +175,33 @@ function _is_additive(model::Optimizer, cp::MOI.ConstraintIndex)
174175
end
175176
return true
176177
end
178+
179+
function _update_duals_from_vector_quadratic_constraints!(model::Optimizer)
180+
for (F, S) in keys(model.vector_quadratic_constraint_cache.dict)
181+
vector_quadratic_constraint_cache_inner = model.vector_quadratic_constraint_cache[F, S]
182+
_compute_parameters_in_ci!(model, vector_quadratic_constraint_cache_inner)
183+
end
184+
return
185+
end
186+
187+
function _compute_parameters_in_ci!(
188+
model::Optimizer{T},
189+
pf::ParametricVectorQuadraticFunction{T},
190+
ci::MOI.ConstraintIndex{F,S},
191+
) where {F,S,T}
192+
cons_dual = MOI.get(model.optimizer, MOI.ConstraintDual(), ci)
193+
for term in pf.p
194+
model.dual_value_of_parameters[p_val(term.scalar_term.variable)] -=
195+
cons_dual[term.output_index] * term.scalar_term.coefficient
196+
end
197+
for term in pf.pp
198+
coef = ifelse(term.scalar_term.variable_1 == term.scalar_term.variable_2, T(1 // 2), T(1))
199+
model.dual_value_of_parameters[p_val(term.scalar_term.variable_1)] -=
200+
coef * cons_dual[term.output_index] * term.scalar_term.coefficient *
201+
MOI.get(model, ParameterValue(), term.scalar_term.variable_2)
202+
model.dual_value_of_parameters[p_val(term.scalar_term.variable_2)] -=
203+
coef * cons_dual[term.output_index] * term.scalar_term.coefficient *
204+
MOI.get(model, ParameterValue(), term.scalar_term.variable_1)
205+
end
206+
return
207+
end

src/parametric_functions.jl

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,3 +529,207 @@ function _update_cache!(f::ParametricVectorAffineFunction{T}, model) where {T}
529529
f.current_constant = _parametric_constant(model, f)
530530
return nothing
531531
end
532+
533+
mutable struct ParametricVectorQuadraticFunction{T}
534+
# constant * parameter * variable (in this order)
535+
pv::Vector{MOI.VectorQuadraticTerm{T}}
536+
# constant * parameter * parameter
537+
pp::Vector{MOI.VectorQuadraticTerm{T}}
538+
# constant * variable * variable
539+
vv::Vector{MOI.VectorQuadraticTerm{T}}
540+
# constant * parameter
541+
p::Vector{MOI.VectorAffineTerm{T}}
542+
# constant * variable
543+
v::Vector{MOI.VectorAffineTerm{T}}
544+
# constant
545+
c::Vector{T}
546+
# to avoid unnecessary lookups in updates
547+
set_constant::Vector{T}
548+
# cache to avoid slow getters
549+
current_constant::Vector{T}
550+
end
551+
552+
function ParametricVectorQuadraticFunction(
553+
f::MOI.VectorQuadraticFunction{T},
554+
) where {T}
555+
v, p = _split_vector_affine_terms(f.affine_terms)
556+
pv, pp, vv = _split_vector_quadratic_terms(f.quadratic_terms)
557+
558+
# Find variables related to parameters in parameter-variable quadratic terms
559+
v_in_pv = Set{MOI.VariableIndex}()
560+
sizehint!(v_in_pv, length(pv))
561+
for term in pv
562+
push!(v_in_pv, term.scalar_term.variable_2)
563+
end
564+
565+
# Only cache affine variable terms that are involved in parameter-variable quadratic terms
566+
v_filtered = Vector{MOI.VectorAffineTerm{T}}()
567+
for term in v
568+
if term.scalar_term.variable in v_in_pv
569+
push!(v_filtered, term)
570+
end
571+
end
572+
573+
return ParametricVectorQuadraticFunction{T}(
574+
pv,
575+
pp,
576+
vv,
577+
p,
578+
v_filtered,
579+
copy(f.constants),
580+
zeros(T, length(f.constants)),
581+
zeros(T, length(f.constants)),
582+
)
583+
end
584+
585+
function vector_quadratic_parameter_variable_terms(f::ParametricVectorQuadraticFunction)
586+
return f.pv
587+
end
588+
589+
function vector_quadratic_parameter_parameter_terms(f::ParametricVectorQuadraticFunction)
590+
return f.pp
591+
end
592+
593+
function vector_quadratic_variable_variable_terms(f::ParametricVectorQuadraticFunction)
594+
return f.vv
595+
end
596+
597+
function vector_affine_parameter_terms(f::ParametricVectorQuadraticFunction)
598+
return f.p
599+
end
600+
601+
function vector_affine_variable_terms(f::ParametricVectorQuadraticFunction)
602+
return f.v
603+
end
604+
605+
function _split_vector_quadratic_terms(
606+
terms::Vector{MOI.VectorQuadraticTerm{T}},
607+
) where {T}
608+
num_vv = 0
609+
num_pp = 0
610+
num_pv = 0
611+
for term in terms
612+
if _is_variable(term.scalar_term.variable_1)
613+
if _is_variable(term.scalar_term.variable_2)
614+
num_vv += 1
615+
else
616+
num_pv += 1
617+
end
618+
else
619+
if _is_variable(term.scalar_term.variable_2)
620+
num_pv += 1
621+
else
622+
num_pp += 1
623+
end
624+
end
625+
end
626+
vv = Vector{MOI.VectorQuadraticTerm{T}}(undef, num_vv)
627+
pp = Vector{MOI.VectorQuadraticTerm{T}}(undef, num_pp)
628+
pv = Vector{MOI.VectorQuadraticTerm{T}}(undef, num_pv)
629+
i_vv = 1
630+
i_pp = 1
631+
i_pv = 1
632+
for term in terms
633+
if _is_variable(term.scalar_term.variable_1)
634+
if _is_variable(term.scalar_term.variable_2)
635+
vv[i_vv] = term
636+
i_vv += 1
637+
else
638+
pv[i_pv] = MOI.VectorQuadraticTerm(
639+
term.output_index,
640+
MOI.ScalarQuadraticTerm(
641+
term.scalar_term.coefficient,
642+
term.scalar_term.variable_2,
643+
term.scalar_term.variable_1,
644+
),
645+
)
646+
i_pv += 1
647+
end
648+
else
649+
if _is_variable(term.scalar_term.variable_2)
650+
pv[i_pv] = term
651+
i_pv += 1
652+
else
653+
pp[i_pp] = term
654+
i_pp += 1
655+
end
656+
end
657+
end
658+
return pv, pp, vv
659+
end
660+
661+
function _parametric_constant(
662+
model,
663+
f::ParametricVectorQuadraticFunction{T},
664+
) where {T}
665+
param_constant = copy(f.c)
666+
for term in vector_affine_parameter_terms(f)
667+
param_constant[term.output_index] +=
668+
term.scalar_term.coefficient *
669+
model.parameters[p_idx(term.scalar_term.variable)]
670+
end
671+
for term in vector_quadratic_parameter_parameter_terms(f)
672+
idx = term.output_index
673+
coef = term.scalar_term.coefficient /
674+
(term.scalar_term.variable_1 == term.scalar_term.variable_2 ? 2 : 1)
675+
param_constant[idx] += coef *
676+
model.parameters[p_idx(term.scalar_term.variable_1)] *
677+
model.parameters[p_idx(term.scalar_term.variable_2)]
678+
end
679+
return param_constant
680+
end
681+
682+
function _delta_parametric_constant(
683+
model,
684+
f::ParametricVectorQuadraticFunction{T},
685+
) where {T}
686+
delta_constant = zeros(T, length(f.c))
687+
for term in vector_affine_parameter_terms(f)
688+
p = p_idx(term.scalar_term.variable)
689+
if !isnan(model.updated_parameters[p])
690+
delta_constant[term.output_index] +=
691+
term.scalar_term.coefficient *
692+
(model.updated_parameters[p] - model.parameters[p])
693+
end
694+
end
695+
for term in vector_quadratic_parameter_parameter_terms(f)
696+
idx = term.output_index
697+
p1 = p_idx(term.scalar_term.variable_1)
698+
p2 = p_idx(term.scalar_term.variable_2)
699+
isnan_1 = isnan(model.updated_parameters[p1])
700+
isnan_2 = isnan(model.updated_parameters[p2])
701+
if !isnan_1 || !isnan_2
702+
new_1 = isnan_1 ? model.parameters[p1] : model.updated_parameters[p1]
703+
new_2 = isnan_2 ? model.parameters[p2] : model.updated_parameters[p2]
704+
coef = term.scalar_term.coefficient /
705+
(term.scalar_term.variable_1 == term.scalar_term.variable_2 ? 2 : 1)
706+
delta_constant[idx] += coef * (new_1 * new_2 - model.parameters[p1] * model.parameters[p2])
707+
end
708+
end
709+
return delta_constant
710+
end
711+
712+
function _update_cache!(f::ParametricVectorQuadraticFunction{T}, model) where {T}
713+
f.current_constant = _parametric_constant(model, f)
714+
return nothing
715+
end
716+
717+
function _original_function(f::ParametricVectorQuadraticFunction{T}) where {T}
718+
return MOI.VectorQuadraticFunction{T}(
719+
vcat(
720+
vector_quadratic_parameter_variable_terms(f),
721+
vector_quadratic_parameter_parameter_terms(f),
722+
vector_quadratic_variable_variable_terms(f),
723+
),
724+
vcat(vector_affine_parameter_terms(f), vector_affine_variable_terms(f)),
725+
f.c,
726+
)
727+
end
728+
729+
function _current_function(f::ParametricVectorQuadraticFunction{T}) where {T}
730+
return MOI.VectorQuadraticFunction{T}(
731+
vector_quadratic_variable_variable_terms(f),
732+
vector_affine_variable_terms(f),
733+
f.current_constant,
734+
)
735+
end

src/update_parameters.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ function update_parameters!(model::Optimizer)
272272
_update_quadratic_constraints!(model)
273273
_update_affine_objective!(model)
274274
_update_quadratic_objective!(model)
275+
_update_vector_quadratic_constraints!(model)
275276

276277
# Update parameters and put NaN to indicate that the parameter has been
277278
# updated
@@ -284,3 +285,36 @@ function update_parameters!(model::Optimizer)
284285

285286
return
286287
end
288+
289+
function _update_vector_quadratic_constraints!(model::Optimizer)
290+
for (F, S) in keys(model.vector_quadratic_constraint_cache.dict)
291+
vector_quadratic_constraint_cache_inner =
292+
model.vector_quadratic_constraint_cache[F, S]
293+
if !isempty(vector_quadratic_constraint_cache_inner)
294+
_update_vector_quadratic_constraints!(
295+
model,
296+
vector_quadratic_constraint_cache_inner,
297+
)
298+
end
299+
end
300+
return
301+
end
302+
303+
function _update_vector_quadratic_constraints!(
304+
model::Optimizer,
305+
vector_quadratic_constraint_cache_inner::DoubleDictInner{F,S,V},
306+
) where {F<:MOI.VectorQuadraticFunction{T},S,V} where {T}
307+
for (inner_ci, pf) in vector_quadratic_constraint_cache_inner
308+
delta_constant = _delta_parametric_constant(model, pf)
309+
if !iszero(sum(abs, delta_constant))
310+
pf.current_constant .+= delta_constant
311+
MOI.modify(
312+
model.optimizer,
313+
inner_ci,
314+
MOI.VectorConstantChange(pf.current_constant),
315+
)
316+
end
317+
# TODO: handle variable coefficients if needed
318+
end
319+
return
320+
end

test/jump_tests.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,3 +1274,18 @@ function test_parameter_Cannot_be_inf_2()
12741274
@test_throws AssertionError MOI.set(model, POI.ParameterValue(), p, Inf)
12751275
return
12761276
end
1277+
1278+
@testset "JuMP PVQF" begin
1279+
model = Model(Optimizer)
1280+
@variable(model, x >= 0)
1281+
p = add_parameter(model, 0.5)
1282+
1283+
@constraint(model, [ p * x + x^2 ; 1 ] .== [ 0 ; 0 ])
1284+
optimize!(model)
1285+
@test termination_status(model) == MOI.LOCALLY_SOLVED
1286+
1287+
set_value(p, 4.0)
1288+
update_parameters!(backend(model))
1289+
optimize!(model)
1290+
@test primal_status(model) == MOI.FEASIBLE_POINT
1291+
end

0 commit comments

Comments
 (0)