Skip to content

Commit 17baf18

Browse files
authored
Cleanup Multiplicative Parameters handling (#224)
1 parent efbe988 commit 17baf18

7 files changed

Lines changed: 244 additions & 23 deletions

File tree

src/MOI_wrapper.jl

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ function _cache_multiplicative_params!(
7474
for term in f.pv
7575
push!(model.multiplicative_parameters_pv, term.variable_1.value)
7676
end
77-
for term in f.pp
78-
push!(model.multiplicative_parameters_pp, term.variable_1.value)
79-
push!(model.multiplicative_parameters_pp, term.variable_2.value)
80-
end
8177
return
8278
end
8379

@@ -91,15 +87,22 @@ function _cache_multiplicative_params!(
9187
term.scalar_term.variable_1.value,
9288
)
9389
end
94-
for term in f.pp
95-
push!(
96-
model.multiplicative_parameters_pp,
97-
term.scalar_term.variable_1.value,
98-
)
99-
push!(
100-
model.multiplicative_parameters_pp,
101-
term.scalar_term.variable_2.value,
102-
)
90+
return
91+
end
92+
93+
function _cache_multiplicative_params!(
94+
model::Optimizer{T},
95+
f::ParametricCubicFunction{T},
96+
) where {T}
97+
for term in f.pv
98+
push!(model.multiplicative_parameters_pv, term.variable_1.value)
99+
end
100+
for term in f.pvv
101+
push!(model.multiplicative_parameters_pv, term.index_1.value)
102+
end
103+
for term in f.ppv
104+
push!(model.multiplicative_parameters_pv, term.index_1.value)
105+
push!(model.multiplicative_parameters_pv, term.index_2.value)
103106
end
104107
return
105108
end
@@ -137,7 +140,6 @@ function MOI.is_empty(model::Optimizer)
137140
isempty(model.vector_affine_constraint_cache) &&
138141
#
139142
isempty(model.multiplicative_parameters_pv) &&
140-
isempty(model.multiplicative_parameters_pp) &&
141143
isempty(model.dual_value_of_parameters) &&
142144
model.number_of_parameters_in_model == 0 &&
143145
isempty(model.parameters_in_conflict) &&
@@ -197,8 +199,6 @@ function MOI.empty!(model::Optimizer{T}) where {T}
197199
empty!(model.vector_affine_constraint_cache)
198200
# multiplicative_parameters_pv
199201
empty!(model.multiplicative_parameters_pv)
200-
# multiplicative_parameters_pp
201-
empty!(model.multiplicative_parameters_pp)
202202
# dual_value_of_parameters
203203
empty!(model.dual_value_of_parameters)
204204
# [SKIP] evaluate_duals

src/ParametricOptInterface.jl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,6 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer
186186
ParametricVectorAffineFunction{T},
187187
}
188188
multiplicative_parameters_pv::Set{Int64}
189-
multiplicative_parameters_pp::Set{Int64}
190189
dual_value_of_parameters::Vector{T}
191190
evaluate_duals::Bool
192191
number_of_parameters_in_model::Int64
@@ -254,8 +253,6 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer
254253
DoubleDicts.DoubleDict{ParametricVectorAffineFunction{T}}(),
255254
# multiplicative_parameters_pv
256255
Set{Int64}(),
257-
# multiplicative_parameters_pp
258-
Set{Int64}(),
259256
# dual_value_of_parameters
260257
T[],
261258
# evaluate_duals

src/cubic_objective.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ function MOI.set(
4545
# 5. Clear old caches
4646
_empty_objective_function_caches!(model)
4747

48-
# 6. Store new cache
48+
# 6. Cache multiplicative parameters
49+
_cache_multiplicative_params!(model, cubic_func)
50+
51+
# 7. Store new cache
4952
model.cubic_objective_cache = cubic_func
5053

51-
# 7. Store original for retrieval if option is enabled
54+
# 8. Store original for retrieval if option is enabled
5255
if model.save_original_objective_and_constraints
5356
MOI.set(
5457
model.original_objective_cache,

src/duals.jl

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ function _compute_dual_of_parameters!(model::Optimizer{T}) where {T}
1616
if model.quadratic_objective_cache !== nothing
1717
_update_duals_from_objective!(model, model.quadratic_objective_cache)
1818
end
19+
if model.cubic_objective_cache !== nothing
20+
_update_duals_from_objective!(model, model.cubic_objective_cache)
21+
end
1922
return
2023
end
2124

@@ -116,6 +119,69 @@ function _update_duals_from_objective!(model::Optimizer{T}, pf) where {T}
116119
return
117120
end
118121

122+
function _update_duals_from_objective!(
123+
model::Optimizer{T},
124+
pf::ParametricQuadraticFunction{T},
125+
) where {T}
126+
is_min = MOI.get(model.optimizer, MOI.ObjectiveSense()) == MOI.MIN_SENSE
127+
sign = ifelse(is_min, one(T), -one(T))
128+
# p terms: ∂(c·p_i)/∂p_i = c
129+
for term in pf.p
130+
model.dual_value_of_parameters[p_val(term.variable)] +=
131+
sign * term.coefficient
132+
end
133+
# pp terms: ∂(c·p_i·p_j)/∂p_i = c·p_j
134+
for term in pf.pp
135+
mult = sign * term.coefficient
136+
if term.variable_1 == term.variable_2
137+
mult /= 2
138+
end
139+
model.dual_value_of_parameters[p_val(term.variable_1)] +=
140+
mult * model.parameters[p_idx(term.variable_2)]
141+
model.dual_value_of_parameters[p_val(term.variable_2)] +=
142+
mult * model.parameters[p_idx(term.variable_1)]
143+
end
144+
return
145+
end
146+
147+
function _update_duals_from_objective!(
148+
model::Optimizer{T},
149+
pf::ParametricCubicFunction{T},
150+
) where {T}
151+
is_min = MOI.get(model.optimizer, MOI.ObjectiveSense()) == MOI.MIN_SENSE
152+
sign = ifelse(is_min, one(T), -one(T))
153+
# p terms: ∂(c·p_i)/∂p_i = c
154+
for term in pf.p
155+
model.dual_value_of_parameters[p_val(term.variable)] +=
156+
sign * term.coefficient
157+
end
158+
# pp terms: ∂(c·p_i·p_j)/∂p_i = c·p_j (diagonal: c/2·2·p_i = c·p_i)
159+
for term in pf.pp
160+
mult = sign * term.coefficient
161+
if term.variable_1 == term.variable_2
162+
mult /= 2
163+
end
164+
model.dual_value_of_parameters[p_val(term.variable_1)] +=
165+
mult * model.parameters[p_idx(term.variable_2)]
166+
model.dual_value_of_parameters[p_val(term.variable_2)] +=
167+
mult * model.parameters[p_idx(term.variable_1)]
168+
end
169+
# ppp terms: ∂(c·p_i·p_j·p_k)/∂p_i = c·p_j·p_k
170+
for term in pf.ppp
171+
coef = sign * term.coefficient
172+
p1_val = model.parameters[p_idx(term.index_1)]
173+
p2_val = model.parameters[p_idx(term.index_2)]
174+
p3_val = model.parameters[p_idx(term.index_3)]
175+
model.dual_value_of_parameters[p_val(term.index_1)] +=
176+
coef * p2_val * p3_val
177+
model.dual_value_of_parameters[p_val(term.index_2)] +=
178+
coef * p1_val * p3_val
179+
model.dual_value_of_parameters[p_val(term.index_3)] +=
180+
coef * p1_val * p2_val
181+
end
182+
return
183+
end
184+
119185
function MOI.get(
120186
model::Optimizer{T},
121187
attr::MOI.ConstraintDual,

test/test_JuMP.jl

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,80 @@ function test_jump_dual_objective_max()
576576
return
577577
end
578578

579+
function test_jump_dual_objective_pv()
580+
# p*x is multiplicative → dual query should error
581+
model = Model(() -> POI.Optimizer(HiGHS.Optimizer))
582+
set_silent(model)
583+
@variable(model, x >= 0)
584+
@variable(model, p in Parameter(-2.0))
585+
@objective(model, Min, p * x + x^2)
586+
optimize!(model)
587+
@test_throws ErrorException dual(ParameterRef(p))
588+
return
589+
end
590+
591+
function test_jump_dual_objective_pp_same()
592+
model = Model(() -> POI.Optimizer(HiGHS.Optimizer))
593+
set_silent(model)
594+
@variable(model, x >= 1)
595+
@variable(model, p in Parameter(3.0))
596+
@objective(model, Min, x + p^2)
597+
optimize!(model)
598+
# ∂(p²)/∂p = 2p = 6
599+
@test dual(ParameterRef(p)) 6.0 atol = 1e-4
600+
return
601+
end
602+
603+
function test_jump_dual_objective_pp_different()
604+
model = Model(() -> POI.Optimizer(HiGHS.Optimizer))
605+
set_silent(model)
606+
@variable(model, x >= 1)
607+
@variable(model, p1 in Parameter(2.0))
608+
@variable(model, p2 in Parameter(3.0))
609+
@objective(model, Min, x + p1 * p2)
610+
optimize!(model)
611+
# ∂(p1*p2)/∂p1 = p2 = 3, ∂(p1*p2)/∂p2 = p1 = 2
612+
@test dual(ParameterRef(p1)) 3.0 atol = 1e-4
613+
@test dual(ParameterRef(p2)) 2.0 atol = 1e-4
614+
return
615+
end
616+
617+
function test_jump_dual_objective_mixed_terms()
618+
# min x + 2p + p^2 s.t. x >= p, p = 3
619+
# Optimal: x* = 3, obj = 3 + 6 + 9 = 18
620+
# ∂f/∂p from obj = 2 + 2p = 8
621+
# Constraint x - p >= 0: dual λ = 1, ∂g/∂p = -1, contribution = 1
622+
# Total = 8 + 1 = 9
623+
model = Model(() -> POI.Optimizer(HiGHS.Optimizer))
624+
set_silent(model)
625+
@variable(model, x)
626+
@variable(model, p in Parameter(3.0))
627+
@constraint(model, x >= p)
628+
@objective(model, Min, x + 2 * p + p^2)
629+
optimize!(model)
630+
@test objective_value(model) 18.0 atol = 1e-4
631+
@test dual(ParameterRef(p)) 9.0 atol = 1e-4
632+
return
633+
end
634+
635+
function test_jump_dual_objective_parameter_update()
636+
# min x + p^2 s.t. x >= 1, p = 2 then p = 3
637+
# ∂(p^2)/∂p = 2p
638+
# p=2: dual = 4, p=3: dual = 6
639+
model = Model(() -> POI.Optimizer(HiGHS.Optimizer))
640+
set_silent(model)
641+
@variable(model, x)
642+
@variable(model, p in Parameter(2.0))
643+
@constraint(model, x >= 1)
644+
@objective(model, Min, x + p^2)
645+
optimize!(model)
646+
@test dual(ParameterRef(p)) 4.0 atol = 1e-4
647+
set_parameter_value(p, 3.0)
648+
optimize!(model)
649+
@test dual(ParameterRef(p)) 6.0 atol = 1e-4
650+
return
651+
end
652+
579653
function test_jump_dual_multiple_parameters_1()
580654
model = Model(() -> POI.Optimizer(HiGHS.Optimizer))
581655
set_silent(model)

test/test_MathOptInterface.jl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,8 +1451,16 @@ function test_qp_objective_parameter_times_parameter()
14511451
0.0,
14521452
atol = ATOL,
14531453
)
1454-
@test MOI.get(optimizer, MOI.ConstraintDual(), cy) == 0.0
1455-
@test MOI.get(optimizer, MOI.ConstraintDual(), cz) == 0.0
1454+
@test isapprox(
1455+
MOI.get(optimizer, MOI.ConstraintDual(), cy),
1456+
1.0,
1457+
atol = ATOL,
1458+
)
1459+
@test isapprox(
1460+
MOI.get(optimizer, MOI.ConstraintDual(), cz),
1461+
1.0,
1462+
atol = ATOL,
1463+
)
14561464
MOI.set(optimizer, MOI.ConstraintSet(), cy, MOI.Parameter(2.0))
14571465
MOI.optimize!(optimizer)
14581466
@test isapprox(MOI.get(optimizer, MOI.ObjectiveValue()), 2.0, atol = ATOL)

test/test_cubic.jl

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,6 +1657,79 @@ function test_parametric_objective_type_cubic_error()
16571657
return
16581658
end
16591659

1660+
# ============================================================================
1661+
# Contribution of objective parameters in duals
1662+
# ============================================================================
1663+
1664+
function test_cubic_dual_ppp_terms()
1665+
# min x + p^3 s.t. x >= 1, p = 2
1666+
# Optimal: x = 1, obj = 1 + 8 = 9
1667+
# ∂(p^3)/∂p = 3p^2 = 12
1668+
model = Model(() -> POI.Optimizer(HiGHS.Optimizer()))
1669+
set_silent(model)
1670+
@variable(model, x)
1671+
@variable(model, p in MOI.Parameter(2.0))
1672+
@constraint(model, x >= 1)
1673+
@objective(model, Min, x + p^3)
1674+
optimize!(model)
1675+
@test termination_status(model) in (OPTIMAL, LOCALLY_SOLVED)
1676+
@test dual(ParameterRef(p)) 12.0 atol = ATOL
1677+
return
1678+
end
1679+
1680+
function test_cubic_dual_ppv_terms()
1681+
# min p1*p2*x + x^2 s.t. x >= 0, p1 = 1, p2 = -2
1682+
# ppv is multiplicative → dual query should error
1683+
model = Model(() -> POI.Optimizer(HiGHS.Optimizer()))
1684+
set_silent(model)
1685+
@variable(model, x >= 0)
1686+
@variable(model, p1 in MOI.Parameter(1.0))
1687+
@variable(model, p2 in MOI.Parameter(-2.0))
1688+
@objective(model, Min, p1 * p2 * x + x^2)
1689+
optimize!(model)
1690+
@test termination_status(model) in (OPTIMAL, LOCALLY_SOLVED)
1691+
@test value(x) 1.0 atol = ATOL
1692+
@test_throws ErrorException dual(ParameterRef(p1))
1693+
@test_throws ErrorException dual(ParameterRef(p2))
1694+
return
1695+
end
1696+
1697+
function test_cubic_dual_pvv_terms()
1698+
# min p*x^2 - 3x s.t. 0 <= x <= 10, p = 1
1699+
# pvv is multiplicative → dual query should error
1700+
model = Model(() -> POI.Optimizer(HiGHS.Optimizer()))
1701+
set_silent(model)
1702+
@variable(model, 0 <= x <= 10)
1703+
@variable(model, p in MOI.Parameter(1.0))
1704+
@objective(model, Min, p * x^2 - 3 * x)
1705+
optimize!(model)
1706+
@test termination_status(model) in (OPTIMAL, LOCALLY_SOLVED)
1707+
@test value(x) 1.5 atol = ATOL
1708+
@test_throws ErrorException dual(ParameterRef(p))
1709+
return
1710+
end
1711+
1712+
function test_cubic_dual_ppp_three_distinct()
1713+
# min x + p1*p2*p3 s.t. x >= 1, p1=2, p2=3, p3=4
1714+
# ∂/∂p1 = p2*p3 = 12
1715+
# ∂/∂p2 = p1*p3 = 8
1716+
# ∂/∂p3 = p1*p2 = 6
1717+
model = Model(() -> POI.Optimizer(HiGHS.Optimizer()))
1718+
set_silent(model)
1719+
@variable(model, x)
1720+
@variable(model, p1 in MOI.Parameter(2.0))
1721+
@variable(model, p2 in MOI.Parameter(3.0))
1722+
@variable(model, p3 in MOI.Parameter(4.0))
1723+
@constraint(model, x >= 1)
1724+
@objective(model, Min, x + p1 * p2 * p3)
1725+
optimize!(model)
1726+
@test termination_status(model) in (OPTIMAL, LOCALLY_SOLVED)
1727+
@test dual(ParameterRef(p1)) 12.0 atol = ATOL
1728+
@test dual(ParameterRef(p2)) 8.0 atol = ATOL
1729+
@test dual(ParameterRef(p3)) 6.0 atol = ATOL
1730+
return
1731+
end
1732+
16601733
end # module
16611734

16621735
TestCubic.runtests()

0 commit comments

Comments
 (0)