Skip to content

Commit 9e7cc73

Browse files
committed
First pass at a feasibility checker.
This is a significant item for JuMP 1.0, but there is debate on how to define and implement distances for different sets. See https://github.com/matbesancon/MathOptSetDistances.jl for a WIP package exploring the options. This PR also only addresses primal feasibility. See joaquimg/FeasibilityOptInterface.jl#1 for a WIP attempt at dual feasibility as well. In future, the _distance_to_set definitions will be merged into MOI. This PR is more about defining the JuMP-level interface so we can change internal details without breaking compatibility of JuMP 1.0.
1 parent c432f66 commit 9e7cc73

File tree

5 files changed

+188
-6
lines changed

5 files changed

+188
-6
lines changed

docs/src/reference/solutions.md

+2
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,6 @@ lp_objective_perturbation_range
4949
lp_rhs_perturbation_range
5050
lp_sensitivity_report
5151
SensitivityReport
52+
53+
primal_feasibility_report
5254
```

docs/src/solutions.md

+28-6
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ end
118118
```
119119

120120
!!! warning
121-
Querying solution information after modifying a solved model is undefined
121+
Querying solution information after modifying a solved model is undefined
122122
behavior, and solvers may throw an error or return incorrect results.
123-
Modifications include adding, deleting, or modifying any variable,
124-
objective, or constraint. Instead of modify then query, query the results
123+
Modifications include adding, deleting, or modifying any variable,
124+
objective, or constraint. Instead of modify then query, query the results
125125
first, then modify the problem. For example:
126126
```julia
127127
model = Model(GLPK.Optimizer)
@@ -324,11 +324,11 @@ Functions for querying the solutions, e.g., [`primal_status`](@ref) and
324324
used to specify which result to return.
325325

326326
!!! warning
327-
Even if [`termination_status`](@ref) is `MOI.OPTIMAL`, some of the returned
327+
Even if [`termination_status`](@ref) is `MOI.OPTIMAL`, some of the returned
328328
solutions may be suboptimal! However, if the solver found at least one
329329
optimal solution, then `result = 1` will always return an optimal solution.
330-
Use [`objective_value`](@ref) to assess the quality of the remaining
331-
solutions.
330+
Use [`objective_value`](@ref) to assess the quality of the remaining
331+
solutions.
332332

333333
```julia
334334
using JuMP
@@ -353,3 +353,25 @@ for i in 2:result_count(model)
353353
end
354354
end
355355
```
356+
357+
## Checking feasibility of solutions
358+
359+
To check the feasibility of a primal solution, use
360+
[`primal_feasibility_report`](@ref). The returned `report` is `nothing` if the
361+
point is feasible. Otherwise, it is a dictionary mapping the infeasible
362+
constraint references to the distance between the point and the nearest point in
363+
the set.
364+
365+
```@example
366+
using JuMP
367+
model = Model()
368+
@variable(model, x >= 1, Int)
369+
@constraint(model, c1, x <= 1.95)
370+
point = Dict(x => 2.5)
371+
report = primal_feasibility_report(model, point)
372+
```
373+
374+
To use the point from a previous solve, use:
375+
```julia
376+
point = Dict(v => value(v) for v in all_variables(model))
377+
```

src/JuMP.jl

+1
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,7 @@ include("lp_sensitivity.jl")
11091109
include("lp_sensitivity2.jl")
11101110
include("callbacks.jl")
11111111
include("file_formats.jl")
1112+
include("feasibility_checker.jl")
11121113

11131114
# JuMP exports everything except internal symbols, which are defined as those
11141115
# whose name starts with an underscore. Macros whose names start with

src/feasibility_checker.jl

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
6+
"""
7+
primal_feasibility_report(
8+
model::Model,
9+
[point::Dict{VariableRef,Float64}];
10+
atol::Float64 = 1e-8,
11+
)::Union{Nothing,Dict{Any,Float64}}
12+
13+
Check the primal feasibility of `model` at the point given by the dictionary
14+
`point` mapping variables to primal values. If a variable is not given in
15+
`point`, the value is assumed to be `0.0`.
16+
17+
If the point is feasible, this function returns `nothing`. If infeasible, this
18+
function returns a dictionary mapping the constriant reference of each violated
19+
constraint to the distance it is from being feasible.
20+
21+
To obtain `point` from a solution of a solved model, use:
22+
```julia
23+
point = Dict(v => value(v) for v in all_variables(model))
24+
```
25+
"""
26+
function primal_feasibility_report(
27+
model::Model,
28+
point::Dict{VariableRef,Float64};
29+
atol::Float64 = 1e-8,
30+
)
31+
point_f = x -> get(point, x, 0.0)
32+
violated_constraints = Dict{Any,Float64}()
33+
for (F, S) in list_of_constraint_types(model)
34+
# This loop is not type-stable because `F` and `S` change, but it should
35+
# be fine because no one should be using this in performance-critical
36+
# code.
37+
for con in all_constraints(model, F, S)
38+
obj = constraint_object(con)
39+
d = _distance_to_set(value(obj.func, point_f), obj.set)
40+
if d > atol
41+
violated_constraints[con] = d
42+
end
43+
end
44+
end
45+
return length(violated_constraints) == 0 ? nothing : violated_constraints
46+
end
47+
48+
function _distance_to_set(::Any, set::MOI.AbstractSet)
49+
error(
50+
"Feasibility checker for set type $(typeof(set)) has not been " *
51+
"implemented yet."
52+
)
53+
end
54+
55+
function _distance_to_set(x::Float64, set::MOI.LessThan{Float64})
56+
return max(x - set.upper, 0.0)
57+
end
58+
59+
function _distance_to_set(x::Float64, set::MOI.GreaterThan{Float64})
60+
return max(set.lower - x, 0.0)
61+
end
62+
63+
function _distance_to_set(x::Float64, set::MOI.EqualTo{Float64})
64+
return abs(set.value - x)
65+
end
66+
67+
function _distance_to_set(x::Float64, set::MOI.Interval{Float64})
68+
return max(x - set.upper, set.lower - x, 0.0)
69+
end
70+
71+
function _distance_to_set(x::Float64, ::MOI.ZeroOne)
72+
return min(abs(x - 0.0), abs(x - 1.0))
73+
end
74+
75+
function _distance_to_set(x::Float64, ::MOI.Integer)
76+
return abs(x - round(Int, x))
77+
end

test/feasibility_checker.jl

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
6+
module TestFeasibilityChecker
7+
8+
using JuMP
9+
using Test
10+
11+
function test_distance_to_set()
12+
@test JuMP._distance_to_set(1.0, MOI.LessThan(2.0)) 0.0
13+
@test JuMP._distance_to_set(1.0, MOI.LessThan(0.5)) 0.5
14+
@test JuMP._distance_to_set(1.0, MOI.GreaterThan(2.0)) 1.0
15+
@test JuMP._distance_to_set(1.0, MOI.GreaterThan(0.5)) 0.0
16+
@test JuMP._distance_to_set(1.0, MOI.EqualTo(2.0)) 1.0
17+
@test JuMP._distance_to_set(1.0, MOI.EqualTo(0.5)) 0.5
18+
@test JuMP._distance_to_set(1.0, MOI.Interval(1.0, 2.0)) 0.0
19+
@test JuMP._distance_to_set(0.5, MOI.Interval(1.0, 2.0)) 0.5
20+
@test JuMP._distance_to_set(2.75, MOI.Interval(1.0, 2.0)) 0.75
21+
@test JuMP._distance_to_set(0.6, MOI.ZeroOne()) 0.4
22+
@test JuMP._distance_to_set(-0.01, MOI.ZeroOne()) 0.01
23+
@test JuMP._distance_to_set(1.01, MOI.ZeroOne()) 0.01
24+
@test JuMP._distance_to_set(0.6, MOI.Integer()) 0.4
25+
@test JuMP._distance_to_set(3.1, MOI.Integer()) 0.1
26+
@test JuMP._distance_to_set(-0.01, MOI.Integer()) 0.01
27+
@test JuMP._distance_to_set(1.01, MOI.Integer()) 0.01
28+
end
29+
30+
function test_feasible()
31+
model = Model()
32+
@variable(model, x, Bin)
33+
@variable(model, 0 <= y <= 2, Int)
34+
@variable(model, z == 0.5)
35+
@constraint(model, x + y + z >= 0.5)
36+
@test primal_feasibility_report(model, Dict(z => 0.5)) === nothing
37+
end
38+
39+
function test_bounds()
40+
model = Model()
41+
@variable(model, x, Bin)
42+
@variable(model, 0 <= y <= 2, Int)
43+
@variable(model, z == 0.5)
44+
report = primal_feasibility_report(model, Dict(x => 0.1, y => 2.1))
45+
@test report[BinaryRef(x)] 0.1
46+
@test report[UpperBoundRef(y)] 0.1
47+
@test report[IntegerRef(y)] 0.1
48+
@test report[FixRef(z)] 0.5
49+
@test length(report) == 4
50+
end
51+
52+
function test_affine()
53+
model = Model()
54+
@variable(model, x)
55+
@constraint(model, c1, x <= 0.5)
56+
@constraint(model, c2, x >= 1.25)
57+
@constraint(model, c3, x == 1.1)
58+
@constraint(model, c4, 0 <= x <= 0.5)
59+
report = primal_feasibility_report(model, Dict(x => 1.0))
60+
@test report[c1] 0.5
61+
@test report[c2] 0.25
62+
@test report[c3] 0.1
63+
@test report[c4] 0.5
64+
@test length(report) == 4
65+
end
66+
67+
function runtests()
68+
for name in names(@__MODULE__; all = true)
69+
if !startswith("$(name)", "test_")
70+
continue
71+
end
72+
@testset "$(name)" begin
73+
getfield(@__MODULE__, name)()
74+
end
75+
end
76+
end
77+
78+
end
79+
80+
TestFeasibilityChecker.runtests()

0 commit comments

Comments
 (0)