Skip to content

Commit b9708e6

Browse files
authored
[Utilities] allow penalty relaxation for ScalarNonlinear constraints (#2875)
1 parent 19e4c0d commit b9708e6

File tree

2 files changed

+234
-48
lines changed

2 files changed

+234
-48
lines changed

src/Utilities/penalty_relaxation.jl

Lines changed: 132 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -71,71 +71,143 @@ function _change_sense_to_min_if_necessary(
7171
::Type{T},
7272
model::MOI.ModelLike,
7373
) where {T}
74-
sense = MOI.get(model, MOI.ObjectiveSense())
75-
if sense != MOI.FEASIBILITY_SENSE
76-
return sense
74+
if MOI.get(model, MOI.ObjectiveSense()) != MOI.FEASIBILITY_SENSE
75+
return
7776
end
7877
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
7978
f = zero(MOI.ScalarAffineFunction{T})
8079
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
81-
return MOI.MIN_SENSE
80+
return
8281
end
8382

84-
function MOI.modify(
83+
function _add_penalty_to_objective(
84+
model::MOI.ModelLike,
85+
::Type{F},
86+
penalty::MOI.ScalarAffineFunction{T},
87+
) where {
88+
T,
89+
F<:Union{
90+
MOI.VariableIndex,
91+
MOI.ScalarAffineFunction{T},
92+
MOI.ScalarQuadraticFunction{T},
93+
MOI.ScalarNonlinearFunction,
94+
},
95+
}
96+
f = MOI.get(model, MOI.ObjectiveFunction{F}())
97+
g = if MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE
98+
MOI.Utilities.operate(+, T, f, penalty)
99+
else
100+
MOI.Utilities.operate(-, T, f, penalty)
101+
end
102+
MOI.set(model, MOI.ObjectiveFunction{typeof(g)}(), g)
103+
return
104+
end
105+
106+
function _add_penalty_to_objective(
107+
::MOI.ModelLike,
108+
::Type{F},
109+
::MOI.ScalarAffineFunction,
110+
) where {F}
111+
return error(
112+
"Cannot perform `ScalarPenaltyRelaxation` with an objective function of type `$F`",
113+
)
114+
end
115+
116+
function _relax_constraint(
117+
::Type{T},
85118
model::MOI.ModelLike,
86119
ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet},
87-
relax::ScalarPenaltyRelaxation{T},
88120
) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}}
89-
sense = _change_sense_to_min_if_necessary(T, model)
90-
y = MOI.add_variable(model)
91-
z = MOI.add_variable(model)
92-
MOI.add_constraint(model, y, MOI.GreaterThan(zero(T)))
93-
MOI.add_constraint(model, z, MOI.GreaterThan(zero(T)))
94-
MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T)))
95-
MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T)))
96-
scale = sense == MOI.MIN_SENSE ? one(T) : -one(T)
97-
a = scale * relax.penalty
98-
O = MOI.get(model, MOI.ObjectiveFunctionType())
99-
obj = MOI.ObjectiveFunction{O}()
100-
MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a))
101-
MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a))
102-
return one(T) * y + one(T) * z
121+
x = MOI.add_variables(model, 2)
122+
MOI.add_constraint.(model, x, MOI.GreaterThan(zero(T)))
123+
MOI.modify(model, ci, MOI.ScalarCoefficientChange(x[1], one(T)))
124+
MOI.modify(model, ci, MOI.ScalarCoefficientChange(x[2], -one(T)))
125+
return x
103126
end
104127

105-
function MOI.modify(
128+
function _relax_constraint(
129+
::Type{T},
106130
model::MOI.ModelLike,
107131
ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}},
108-
relax::ScalarPenaltyRelaxation{T},
109132
) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}}
110-
sense = _change_sense_to_min_if_necessary(T, model)
111-
# Performance optimization: we don't need the z relaxation variable.
112-
y = MOI.add_variable(model)
113-
MOI.add_constraint(model, y, MOI.GreaterThan(zero(T)))
114-
MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T)))
115-
scale = sense == MOI.MIN_SENSE ? one(T) : -one(T)
116-
a = scale * relax.penalty
117-
O = MOI.get(model, MOI.ObjectiveFunctionType())
118-
obj = MOI.ObjectiveFunction{O}()
119-
MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a))
120-
return one(T) * y
133+
x = MOI.add_variable(model)
134+
MOI.add_constraint(model, x, MOI.GreaterThan(zero(T)))
135+
MOI.modify(model, ci, MOI.ScalarCoefficientChange(x, one(T)))
136+
return [x]
121137
end
122138

123-
function MOI.modify(
139+
function _relax_constraint(
140+
::Type{T},
124141
model::MOI.ModelLike,
125142
ci::MOI.ConstraintIndex{F,MOI.LessThan{T}},
126-
relax::ScalarPenaltyRelaxation{T},
127143
) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}}
128-
sense = _change_sense_to_min_if_necessary(T, model)
129-
# Performance optimization: we don't need the y relaxation variable.
130-
z = MOI.add_variable(model)
131-
MOI.add_constraint(model, z, MOI.GreaterThan(zero(T)))
132-
MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T)))
133-
scale = sense == MOI.MIN_SENSE ? one(T) : -one(T)
134-
a = scale * relax.penalty
144+
x = MOI.add_variable(model)
145+
MOI.add_constraint(model, x, MOI.GreaterThan(zero(T)))
146+
MOI.modify(model, ci, MOI.ScalarCoefficientChange(x, -one(T)))
147+
return [x]
148+
end
149+
150+
function _relax_constraint(
151+
::Type{T},
152+
model::MOI.ModelLike,
153+
ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,S},
154+
) where {T,S<:MOI.AbstractScalarSet}
155+
x, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T)))
156+
y, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T)))
157+
f = MOI.get(model, MOI.ConstraintFunction(), ci)
158+
g = MOI.ScalarNonlinearFunction(
159+
:+,
160+
Any[f, x, MOI.ScalarNonlinearFunction(:-, Any[y])],
161+
)
162+
MOI.set(model, MOI.ConstraintFunction(), ci, g)
163+
return [x, y]
164+
end
165+
166+
function _relax_constraint(
167+
::Type{T},
168+
model::MOI.ModelLike,
169+
ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,MOI.GreaterThan{T}},
170+
) where {T}
171+
x, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T)))
172+
f = MOI.get(model, MOI.ConstraintFunction(), ci)
173+
g = MOI.ScalarNonlinearFunction(:+, [f, x])
174+
MOI.set(model, MOI.ConstraintFunction(), ci, g)
175+
return [x]
176+
end
177+
178+
function _relax_constraint(
179+
::Type{T},
180+
model::MOI.ModelLike,
181+
ci::MOI.ConstraintIndex{MOI.ScalarNonlinearFunction,MOI.LessThan{T}},
182+
) where {T}
183+
x, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(zero(T)))
184+
f = MOI.get(model, MOI.ConstraintFunction(), ci)
185+
g = MOI.ScalarNonlinearFunction(:-, [f, x])
186+
MOI.set(model, MOI.ConstraintFunction(), ci, g)
187+
return [x]
188+
end
189+
190+
function MOI.modify(
191+
model::MOI.ModelLike,
192+
ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet},
193+
relax::ScalarPenaltyRelaxation{T},
194+
) where {
195+
T,
196+
F<:Union{
197+
MOI.ScalarAffineFunction{T},
198+
MOI.ScalarQuadraticFunction{T},
199+
MOI.ScalarNonlinearFunction,
200+
},
201+
}
202+
x = _relax_constraint(T, model, ci)
203+
p = MOI.ScalarAffineFunction(
204+
MOI.ScalarAffineTerm.(relax.penalty, x),
205+
zero(T),
206+
)
207+
_change_sense_to_min_if_necessary(T, model)
135208
O = MOI.get(model, MOI.ObjectiveFunctionType())
136-
obj = MOI.ObjectiveFunction{O}()
137-
MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a))
138-
return one(T) * z
209+
_add_penalty_to_objective(model, O, p)
210+
return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T))
139211
end
140212

141213
"""
@@ -270,13 +342,20 @@ end
270342

271343
function MOI.modify(model::MOI.ModelLike, relax::PenaltyRelaxation{T}) where {T}
272344
map = Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}()
345+
penalty_expr = zero(MOI.ScalarAffineFunction{T})
273346
for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent())
274-
_modify_penalty_relaxation(map, model, relax, F, S)
347+
_modify_penalty_relaxation(penalty_expr, map, model, relax, F, S)
348+
end
349+
if !isempty(penalty_expr.terms)
350+
_change_sense_to_min_if_necessary(T, model)
351+
O = MOI.get(model, MOI.ObjectiveFunctionType())
352+
_add_penalty_to_objective(model, O, penalty_expr)
275353
end
276354
return map
277355
end
278356

279357
function _modify_penalty_relaxation(
358+
penalty_expr::MOI.ScalarAffineFunction{T},
280359
map::Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}},
281360
model::MOI.ModelLike,
282361
relax::PenaltyRelaxation,
@@ -289,9 +368,14 @@ function _modify_penalty_relaxation(
289368
continue
290369
end
291370
try
292-
map[ci] = MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty))
371+
x = _relax_constraint(T, model, ci)
372+
map[ci] = MOI.ScalarAffineFunction(
373+
MOI.ScalarAffineTerm.(one(T), x),
374+
zero(T),
375+
)
376+
append!(penalty_expr.terms, MOI.ScalarAffineTerm{T}.(penalty, x))
293377
catch err
294-
if err isa MethodError && err.f == MOI.modify
378+
if err isa MethodError && err.f == _relax_constraint
295379
if relax.warn
296380
@warn(
297381
"Skipping PenaltyRelaxation for ConstraintIndex{$F,$S}"

test/Utilities/penalty_relaxation.jl

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,40 @@ function test_relax_no_warn()
8484
return
8585
end
8686

87+
function test_relax_variable_index_objective()
88+
_test_roundtrip(
89+
"""
90+
variables: x, y
91+
minobjective: x
92+
c1: x + y <= 1.0
93+
""",
94+
"""
95+
variables: x, y, a
96+
minobjective: 1.0 * x + 1.0 * a
97+
c1: x + y + -1.0 * a <= 1.0
98+
a >= 0.0
99+
""",
100+
)
101+
return
102+
end
103+
104+
function test_relax_scalar_nonlinear_objective()
105+
_test_roundtrip(
106+
"""
107+
variables: x, y
108+
minobjective: ScalarNonlinearFunction(exp(x))
109+
c1: x + y <= 1.0
110+
""",
111+
"""
112+
variables: x, y, a
113+
minobjective: ScalarNonlinearFunction(+(exp(x), esc(1.0 * a)))
114+
c1: x + y + -1.0 * a <= 1.0
115+
a >= 0.0
116+
""",
117+
)
118+
return
119+
end
120+
87121
function test_relax_affine_lessthan()
88122
_test_roundtrip(
89123
"""
@@ -238,6 +272,58 @@ function test_relax_quadratic_greaterthanthan()
238272
return
239273
end
240274

275+
function test_relax_scalarnonlinear_lessthan()
276+
_test_roundtrip(
277+
"""
278+
variables: x
279+
maxobjective: 1.0 * x
280+
c1: ScalarNonlinearFunction(log(x)) <= 1.0
281+
""",
282+
"""
283+
variables: x, a
284+
maxobjective: 1.0 * x + -1.0 * a
285+
c1: ScalarNonlinearFunction(log(x) - a) <= 1.0
286+
a >= 0.0
287+
""",
288+
)
289+
return
290+
end
291+
292+
function test_relax_scalarnonlinear_greaterthan()
293+
_test_roundtrip(
294+
"""
295+
variables: x
296+
maxobjective: 1.0 * x
297+
c1: ScalarNonlinearFunction(log(x)) >= 1.0
298+
""",
299+
"""
300+
variables: x, a
301+
maxobjective: 1.0 * x + -1.0 * a
302+
c1: ScalarNonlinearFunction(log(x) + a) >= 1.0
303+
a >= 0.0
304+
""",
305+
)
306+
return
307+
end
308+
309+
function test_relax_scalarnonlinear_equalto()
310+
_test_roundtrip(
311+
"""
312+
variables: x
313+
minobjective: 1.0 * x
314+
c1: ScalarNonlinearFunction(log(x)) == 1.0
315+
""",
316+
"""
317+
variables: x, a, b
318+
minobjective: 1.0 * x + 1.0 * a + 1.0 * b
319+
c1: ScalarNonlinearFunction(+(log(x), a, -b)) == 1.0
320+
a >= 0.0
321+
b >= 0.0
322+
""",
323+
)
324+
return
325+
end
326+
241327
function test_penalty_dict()
242328
model = MOI.Utilities.Model{Float64}()
243329
x = MOI.add_variable(model)
@@ -373,6 +459,22 @@ function test_scalar_penalty_relaxation()
373459
return
374460
end
375461

462+
function test_scalar_penalty_relaxation_vector_objective()
463+
model = MOI.Utilities.Model{Float64}()
464+
x = MOI.add_variable(model)
465+
c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0))
466+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
467+
f = MOI.VectorOfVariables([x])
468+
MOI.set(model, MOI.ObjectiveFunction{MOI.VectorOfVariables}(), f)
469+
@test_throws(
470+
ErrorException(
471+
"Cannot perform `ScalarPenaltyRelaxation` with an objective function of type `$(typeof(f))`",
472+
),
473+
MOI.modify(model, c, MOI.Utilities.ScalarPenaltyRelaxation(2.0)),
474+
)
475+
return
476+
end
477+
376478
end # module
377479

378480
TestPenaltyRelaxation.runtests()

0 commit comments

Comments
 (0)