Skip to content

Commit 8f6081b

Browse files
authored
Add relax_with_penalty (#3140)
1 parent d986530 commit 8f6081b

File tree

7 files changed

+240
-5
lines changed

7 files changed

+240
-5
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1212
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1313

1414
[compat]
15-
MathOptInterface = "1.10"
15+
MathOptInterface = "1.11"
1616
MutableArithmetics = "1"
1717
OrderedCollections = "1"
1818
julia = "1.6"

docs/make.jl

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ if !_FAST
8888
for file in [
8989
joinpath("getting_started", "getting_started_with_julia.md"),
9090
joinpath("getting_started", "getting_started_with_JuMP.md"),
91+
joinpath("getting_started", "debugging.md"),
9192
joinpath("linear", "tips_and_tricks.md"),
9293
]
9394
filename = joinpath(@__DIR__, "src", "tutorials", file)

docs/src/changelog.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
- Added a `result` keyword argument to [`solution_summary`] to allow
1515
summarizing models with multiple solutions (#3138)
16+
- Add [`relax_with_penalty!`](@ref), which is a useful tool when debugging
17+
infeasible models (#3140)
1618

1719
### Other
1820

docs/src/reference/constraints.md

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ normalized_rhs
3333
set_normalized_rhs
3434
3535
add_to_function_constant
36+
relax_with_penalty!
3637
```
3738

3839
## Deletion

docs/src/tutorials/getting_started/debugging.jl

+49-4
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,11 @@ import HiGHS
164164

165165
# A simple example of an infeasible model is:
166166

167-
model = Model(HiGHS.Optimizer)
167+
model = Model(HiGHS.Optimizer);
168168
set_silent(model)
169169
@variable(model, x >= 0)
170170
@objective(model, Max, 2x + 1)
171-
@constraint(model, 2x - 1 <= -2)
171+
@constraint(model, con, 2x - 1 <= -2)
172172

173173
# because the bound says that `x >= 0`, but we can rewrite the constraint to be
174174
# `x <= -1/2`. When the problem is infeasible, JuMP may return one of a number
@@ -230,6 +230,51 @@ termination_status(model)
230230
# solvers such as Gurobi and CPLEX do. If the solver does support computing
231231
# conflicts, read [Conflicts](@ref) for more details.
232232

233+
# ### Penalty relaxation
234+
235+
# Another strategy to debug sources of infeasibility is the
236+
# [`relax_with_penalty!`](@ref) function.
237+
#
238+
# The penalty relaxation modifies constraints of the form ``f(x) \in S`` into
239+
# ``f(x) + y - z \in S``, where ``y, z \ge 0``, and then it introduces a
240+
# penalty term into the objective of ``a \times (y + z)`` (if minimizing, else
241+
# ``-a``), where ``a`` is a penalty.
242+
243+
map = relax_with_penalty!(model)
244+
245+
# Here `map` is a dictionary which maps constraint indices to an affine
246+
# expression representing ``(y + z)``.
247+
248+
# If we optimize the relaxed model, this time we get a feasible solution:
249+
250+
optimize!(model)
251+
termination_status(model)
252+
253+
# Iterate over the contents of `map` to see which constraints are violated:
254+
255+
for (con, penalty) in map
256+
violation = value(penalty)
257+
if violation > 0
258+
println("Constraint `$(name(con))` is violated by $violation")
259+
end
260+
end
261+
262+
# Once you find a violated constraint in the relaxed problem, take a look to see
263+
# if there is a typo or other common mistake in that particular constraint.
264+
265+
# Consult the docstring [`relax_with_penalty!`](@ref) for information on how to
266+
# modify the penalty cost term `a`, either for every constraint in the model or
267+
# a particular subset of the constraints.
268+
269+
# When using [`relax_with_penalty!`](@ref), you should be aware that:
270+
#
271+
# * Variable bounds and integrality restrictions are not relaxed. If the
272+
# problem is still infeasible after calling [`relax_with_penalty!`](@ref),
273+
# check the variable bounds.
274+
# * You cannot undo the penalty relaxation. If you need an unmodified model,
275+
# rebuild the problem, or call [`copy_model`](@ref) before calling
276+
# [`relax_with_penalty!`](@ref).
277+
233278
# ## Debugging an unbounded model
234279

235280
# A model is unbounded if there is no limit on how good the objective value can
@@ -241,7 +286,7 @@ termination_status(model)
241286

242287
# A simple example of an unbounded model is:
243288

244-
model = Model(HiGHS.Optimizer)
289+
model = Model(HiGHS.Optimizer);
245290
set_silent(model)
246291
@variable(model, x >= 0)
247292
@objective(model, Max, 2x + 1)
@@ -288,7 +333,7 @@ termination_status(model)
288333
# the variable must be less-than or equal to the expression of the objective
289334
# function. For example:
290335

291-
model = Model(HiGHS.Optimizer)
336+
model = Model(HiGHS.Optimizer);
292337
set_silent(model)
293338
@variable(model, x >= 0)
294339
## @objective(model, Max, 2x + 1)

src/constraints.jl

+104
Original file line numberDiff line numberDiff line change
@@ -1344,3 +1344,107 @@ function all_constraints(
13441344
append!(ret, all_nonlinear_constraints(model))
13451345
return ret
13461346
end
1347+
1348+
"""
1349+
relax_with_penalty!(
1350+
model::Model,
1351+
[penalties::Dict{ConstraintRef,Float64}];
1352+
[default::Union{Nothing,Real} = nothing,]
1353+
)
1354+
1355+
Destructively modify the model in-place to create a penalized relaxation of the
1356+
constraints.
1357+
1358+
!!! warning
1359+
This is a destructive routine that modifies the model in-place. If you don't
1360+
want to modify the original model, use [`copy_model`](@ref) to create a copy
1361+
before calling [`relax_with_penalty!`](@ref).
1362+
1363+
## Reformulation
1364+
1365+
See [`MOI.Utilities.ScalarPenaltyRelaxation`](@ref) for details of the
1366+
reformulation.
1367+
1368+
For each constraint `ci`, the penalty passed to
1369+
[`MOI.Utilities.ScalarPenaltyRelaxation`](@ref) is `get(penalties, ci, default)`.
1370+
If the value is `nothing`, because `ci` does not exist in `penalties` and
1371+
`default = nothing`, then the constraint is skipped.
1372+
1373+
## Return value
1374+
1375+
This function returns a `Dict{ConstraintRef,AffExpr}` that maps each constraint
1376+
index to the corresponding `y + z` as an [`AffExpr`](@ref). In an optimal
1377+
solution, query the value of these functions to compute the violation of each
1378+
constraint.
1379+
1380+
## Relax a subset of constraints
1381+
1382+
To relax a subset of constraints, pass a `penalties` dictionary and set
1383+
`default = nothing`.
1384+
1385+
## Examples
1386+
1387+
```jldoctest
1388+
julia> function new_model()
1389+
model = Model()
1390+
@variable(model, x)
1391+
@objective(model, Max, 2x + 1)
1392+
@constraint(model, c1, 2x - 1 <= -2)
1393+
@constraint(model, c2, 3x >= 0)
1394+
return model
1395+
end
1396+
new_model (generic function with 1 method)
1397+
1398+
julia> model_1 = new_model();
1399+
1400+
julia> relax_with_penalty!(model_1; default = 2.0)
1401+
Dict{ConstraintRef{Model, C, ScalarShape} where C, AffExpr} with 2 entries:
1402+
c1 : 2 x - _[3] ≤ -1.0 => _[3]
1403+
c2 : 3 x + _[2] ≥ 0.0 => _[2]
1404+
1405+
julia> print(model_1)
1406+
Max 2 x - 2 _[2] - 2 _[3] + 1
1407+
Subject to
1408+
c2 : 3 x + _[2] ≥ 0.0
1409+
c1 : 2 x - _[3] ≤ -1.0
1410+
_[2] ≥ 0.0
1411+
_[3] ≥ 0.0
1412+
1413+
julia> model_2 = new_model();
1414+
1415+
julia> relax_with_penalty!(model_2, Dict(model_2[:c2] => 3.0))
1416+
Dict{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.GreaterThan{Float64}}, ScalarShape}, AffExpr} with 1 entry:
1417+
c2 : 3 x + _[2] ≥ 0.0 => _[2]
1418+
1419+
julia> print(model_2)
1420+
Max 2 x - 3 _[2] + 1
1421+
Subject to
1422+
c2 : 3 x + _[2] ≥ 0.0
1423+
c1 : 2 x ≤ -1.0
1424+
_[2] ≥ 0.0
1425+
```
1426+
"""
1427+
function relax_with_penalty!(
1428+
model::Model,
1429+
penalties::Dict;
1430+
default::Union{Nothing,Real} = nothing,
1431+
)
1432+
if default !== nothing
1433+
default = Float64(default)
1434+
end
1435+
moi_penalties = Dict{MOI.ConstraintIndex,Float64}(
1436+
index(k) => Float64(v) for (k, v) in penalties
1437+
)
1438+
map = MOI.modify(
1439+
backend(model),
1440+
MOI.Utilities.PenaltyRelaxation(moi_penalties; default = default),
1441+
)
1442+
return Dict(
1443+
constraint_ref_with_index(model, k) => jump_function(model, v) for
1444+
(k, v) in map
1445+
)
1446+
end
1447+
1448+
function relax_with_penalty!(model::Model; default::Real = 1.0)
1449+
return relax_with_penalty!(model, Dict(); default = default)
1450+
end

test/constraint.jl

+82
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,88 @@ function test_Model_all_constraints(::Any, ::Any)
12201220
return
12211221
end
12221222

1223+
function test_Model_relax_with_penalty!_default(::Any, ::Any)
1224+
model = Model()
1225+
@variable(model, x >= 0)
1226+
map = relax_with_penalty!(model)
1227+
@test isempty(map)
1228+
@constraint(model, c1, x <= 1)
1229+
@constraint(model, c2, x == 0)
1230+
map = relax_with_penalty!(model)
1231+
@test length(map) == 2
1232+
@test map[c1] isa AffExpr
1233+
@test map[c2] isa AffExpr
1234+
@test num_variables(model) == 4
1235+
@test objective_sense(model) == MOI.MIN_SENSE
1236+
@test objective_function(model) == map[c1] + map[c2]
1237+
return
1238+
end
1239+
1240+
function test_Model_relax_with_penalty!_max(::Any, ::Any)
1241+
model = Model()
1242+
@variable(model, x >= 0)
1243+
@constraint(model, c1, x <= 1)
1244+
@constraint(model, c2, x == 0)
1245+
@objective(model, Max, 1.0 * x + 2.5)
1246+
map = relax_with_penalty!(model)
1247+
@test length(map) == 2
1248+
@test map[c1] isa AffExpr
1249+
@test map[c2] isa AffExpr
1250+
@test num_variables(model) == 4
1251+
@test objective_sense(model) == MOI.MAX_SENSE
1252+
@test objective_function(model) == x + 2.5 - map[c1] - map[c2]
1253+
return
1254+
end
1255+
1256+
function test_Model_relax_with_penalty!_constant(::Any, ::Any)
1257+
model = Model()
1258+
@variable(model, x >= 0)
1259+
map = relax_with_penalty!(model)
1260+
@test isempty(map)
1261+
@constraint(model, c1, x <= 1)
1262+
@constraint(model, c2, x == 0)
1263+
map = relax_with_penalty!(model; default = 2)
1264+
@test length(map) == 2
1265+
@test map[c1] isa AffExpr
1266+
@test map[c2] isa AffExpr
1267+
@test num_variables(model) == 4
1268+
@test objective_sense(model) == MOI.MIN_SENSE
1269+
@test objective_function(model) == 2.0 * map[c1] + 2.0 * map[c2]
1270+
return
1271+
end
1272+
1273+
function test_Model_relax_with_penalty!_specific(::Any, ::Any)
1274+
model = Model()
1275+
@variable(model, x >= 0)
1276+
map = relax_with_penalty!(model)
1277+
@test isempty(map)
1278+
@constraint(model, c1, x <= 1)
1279+
@constraint(model, c2, x == 0)
1280+
map = relax_with_penalty!(model, Dict(c1 => 3.0))
1281+
@test length(map) == 1
1282+
@test map[c1] isa AffExpr
1283+
@test num_variables(model) == 2
1284+
@test objective_sense(model) == MOI.MIN_SENSE
1285+
@test objective_function(model) == 3.0 * map[c1]
1286+
return
1287+
end
1288+
1289+
function test_Model_relax_with_penalty!_specific_with_default(::Any, ::Any)
1290+
model = Model()
1291+
@variable(model, x >= 0)
1292+
map = relax_with_penalty!(model)
1293+
@test isempty(map)
1294+
@constraint(model, c1, x <= 1)
1295+
@constraint(model, c2, x == 0)
1296+
map = relax_with_penalty!(model, Dict(c1 => 3.0); default = 1)
1297+
@test length(map) == 2
1298+
@test map[c1] isa AffExpr
1299+
@test num_variables(model) == 4
1300+
@test objective_sense(model) == MOI.MIN_SENSE
1301+
@test objective_function(model) == 3 * map[c1] + map[c2]
1302+
return
1303+
end
1304+
12231305
function runtests()
12241306
for name in names(@__MODULE__; all = true)
12251307
if !startswith("$(name)", "test_")

0 commit comments

Comments
 (0)