Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First pass at a feasibility checker. #2466

Merged
merged 23 commits into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions docs/src/manual/solutions.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ FEASIBLE_POINT::ResultStatusCode = 1
```
Other common returns are `MOI.NO_SOLUTION`, and `MOI.INFEASIBILITY_CERTIFICATE`.
The first means that the solver doesn't have a solution to return, and the
second means that the primal solution is a certificate of dual infeasbility (a
second means that the primal solution is a certificate of dual infeasbility (a
primal unbounded ray).

You can also use [`has_values`](@ref), which returns `true` if there is a
Expand Down Expand Up @@ -200,7 +200,7 @@ FEASIBLE_POINT::ResultStatusCode = 1
```
Other common returns are `MOI.NO_SOLUTION`, and `MOI.INFEASIBILITY_CERTIFICATE`.
The first means that the solver doesn't have a solution to return, and the
second means that the dual solution is a certificate of primal infeasbility (a
second means that the dual solution is a certificate of primal infeasbility (a
dual unbounded ray).

You can also use [`has_duals`](@ref), which returns `true` if there is a
Expand Down Expand Up @@ -258,7 +258,7 @@ And data, a 2-element Array{Float64,1}:

## Recommended workflow

The recommended workflow for solving a model and querying the solution is
The recommended workflow for solving a model and querying the solution is
something like the following:
```jldoctest solutions
if termination_status(model) == MOI.OPTIMAL
Expand Down Expand Up @@ -427,7 +427,7 @@ For instance, this is how you can use this functionality:

```julia
using JuMP
model = Model() # You must use a solver that supports conflict refining/IIS
model = Model() # You must use a solver that supports conflict refining/IIS
# computation, like CPLEX or Gurobi
@variable(model, x >= 0)
@constraint(model, c1, x >= 2)
Expand Down Expand Up @@ -489,3 +489,58 @@ for i in 2:result_count(model)
end
end
```

## Checking feasibility of solutions

To check the feasibility of a primal solution, use
[`primal_feasibility_report`](@ref), which takes a `model`, a dictionary mapping
each variable to a primal solution value (defaults to the last solved solution),
and a tolerance `atol` (defaults to `0.0`).

The function returns a dictionary which maps the infeasible constraint
references to the distance between the primal value of the constraint and the
nearest point in the corresponding set. A point is classed as infeasible if the
distance is greater than the supplied tolerance `atol`.

```jldoctest feasibility; filter=["x integer => 0.1", "c1 : x + y ≤ 1.95 => 0.01"]
julia> model = Model(GLPK.Optimizer);

julia> @variable(model, x >= 1, Int);

julia> @variable(model, y);

julia> @constraint(model, c1, x + y <= 1.95);

julia> point = Dict(x => 1.9, y => 0.06);

julia> primal_feasibility_report(model, point)
Dict{Any,Float64} with 2 entries:
c1 : x + y ≤ 1.95 => 0.01
x integer => 0.1

julia> primal_feasibility_report(model, point; atol = 0.02)
Dict{Any,Float64} with 1 entry:
x integer => 0.1
```

If the point is feasible, an empty dictionary is returned:
```jldoctest feasibility
julia> primal_feasibility_report(model, Dict(x => 1.0, y => 0.0))
Dict{Any,Float64} with 0 entries
```

To use the primal solution from a solve, omit the `point` argument:
```jldoctest feasibility
julia> optimize!(model)

julia> primal_feasibility_report(model)
Dict{Any,Float64} with 0 entries
```

Pass `skip_mising = true` to skip constraints which contain variables that are
not in `point`:
```jldoctest feasibility
julia> primal_feasibility_report(model, Dict(x => 2.1); skip_missing = true)
Dict{Any,Float64} with 1 entry:
x integer => 0.1
```
6 changes: 6 additions & 0 deletions docs/src/reference/solutions.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,9 @@ SensitivityReport
lp_objective_perturbation_range
lp_rhs_perturbation_range
```

## Feasibility

```@docs
primal_feasibility_report
```
1 change: 1 addition & 0 deletions src/JuMP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,7 @@ include("lp_sensitivity.jl")
include("lp_sensitivity2.jl")
include("callbacks.jl")
include("file_formats.jl")
include("feasibility_checker.jl")

# JuMP exports everything except internal symbols, which are defined as those
# whose name starts with an underscore. Macros whose names start with
Expand Down
210 changes: 210 additions & 0 deletions src/feasibility_checker.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

using LinearAlgebra

function _last_primal_solution(model::Model)
if !has_values(model)
error(
"No primal solution is available. You must provide a point at " *
"which to check feasibility.",
)
end
return Dict(v => value(v) for v in all_variables(model))
end

"""
primal_feasibility_report(
model::Model,
point::AbstractDict{VariableRef,Float64} = _last_primal_solution(model),
atol::Float64 = 0.0,
skip_missing::Bool = false,
)::Dict{Any,Float64}

Given a dictionary `point`, which maps variables to primal values, return a
dictionary mapping the constraint reference of each constraint in `model` to the
distance between the point and the nearest feasible point, if the distance is
greater than `atol`.

## Notes

* If `skip_missing = true`, constraints containing variables that are not in
`point` will be ignored.
* If `skip_missing = false` and a partial primal solution is provided, an error
will be thrown.
* If no point is provided, the primal solution from the last time the model was
solved is used.

## Examples

```jldoctest; setup=:(using JuMP)
julia> model = Model();

julia> @variable(model, 0.5 <= x <= 1);

julia> primal_feasibility_report(model, Dict(x => 0.2))
Dict{Any,Float64} with 1 entry:
x ≥ 0.5 => 0.3
```
"""
function primal_feasibility_report(
model::Model,
point::AbstractDict{VariableRef,Float64} = _last_primal_solution(model);
atol::Float64 = 0.0,
skip_missing::Bool = false,
)
function point_f(x::VariableRef)
fx = get(point, x, missing)
if ismissing(fx) && !skip_missing
error(
"point does not contain a value for variable $x. Provide a " *
"value, or pass `skip_missing = true`.",
)
end
return fx
end
violated_constraints = Dict{Any,Float64}()
for (F, S) in list_of_constraint_types(model)
_add_infeasible_constraints(
model,
F,
S,
violated_constraints,
point_f,
atol,
)
end
if num_nl_constraints(model) > 0
if skip_missing
error(
"`skip_missing = true` is not allowed when nonlinear " *
"constraints are present.",
)
end
_add_infeasible_nonlinear_constraints(
model,
violated_constraints,
point_f,
atol,
)
end
return violated_constraints
end

function _add_infeasible_constraints(
model::Model,
::Type{F},
::Type{S},
violated_constraints::Dict{Any,Float64},
point_f::Function,
atol::Float64,
) where {F,S}
for con in all_constraints(model, F, S)
obj = constraint_object(con)
d = _distance_to_set(value.(obj.func, point_f), obj.set)
if d > atol
violated_constraints[con] = d
end
end
return
end

function _add_infeasible_nonlinear_constraints(
model::Model,
violated_constraints::Dict{Any,Float64},
point_f::Function,
atol::Float64,
)
evaluator = NLPEvaluator(model)
MOI.initialize(evaluator, Symbol[])
g = zeros(num_nl_constraints(model))
MOI.eval_constraint(evaluator, g, point_f.(all_variables(model)))
for (i, con) in enumerate(model.nlp_data.nlconstr)
d = max(0.0, con.lb - g[i], g[i] - con.ub)
if d > atol
cref =
ConstraintRef(model, NonlinearConstraintIndex(i), ScalarShape())
violated_constraints[cref] = d
end
end
return
end

function _distance_to_set(::Any, set::MOI.AbstractSet)
return error(
"Feasibility checker for set type $(typeof(set)) has not been " *
"implemented yet.",
)
end

_distance_to_set(::Missing, ::MOI.AbstractSet) = 0.0

###
### MOI.AbstractScalarSets
###

function _distance_to_set(x::T, set::MOI.LessThan{T}) where {T<:Real}
return max(x - set.upper, zero(T))
end

function _distance_to_set(x::T, set::MOI.GreaterThan{T}) where {T<:Real}
return max(set.lower - x, zero(T))
end

function _distance_to_set(x::T, set::MOI.EqualTo{T}) where {T<:Number}
return abs(set.value - x)
end

function _distance_to_set(x::T, set::MOI.Interval{T}) where {T<:Real}
return max(x - set.upper, set.lower - x, zero(T))
end

function _distance_to_set(x::T, ::MOI.ZeroOne) where {T<:Real}
return min(abs(x - zero(T)), abs(x - one(T)))
end

function _distance_to_set(x::T, ::MOI.Integer) where {T<:Real}
return abs(x - round(x))
end

function _distance_to_set(x::T, set::MOI.Semicontinuous{T}) where {T<:Real}
return min(max(x - set.upper, set.lower - x, zero(T)), abs(x))
end

function _distance_to_set(x::T, set::MOI.Semiinteger{T}) where {T<:Real}
d = max(ceil(set.lower) - x, x - floor(set.upper), abs(x - round(x)))
return min(d, abs(x))
end

###
### MOI.AbstractVectorSets
###

function _check_dimension(v::AbstractVector, s)
if length(v) != MOI.dimension(s)
throw(DimensionMismatch("Mismatch between value and set"))
end
return
end

function _distance_to_set(x::Vector{T}, set::MOI.Nonnegatives) where {T<:Real}
_check_dimension(x, set)
return LinearAlgebra.norm(max(-xi, zero(T)) for xi in x)
end

function _distance_to_set(x::Vector{T}, set::MOI.Nonpositives) where {T<:Real}
_check_dimension(x, set)
return LinearAlgebra.norm(max(xi, zero(T)) for xi in x)
end

function _distance_to_set(x::Vector{T}, set::MOI.Zeros) where {T<:Number}
_check_dimension(x, set)
return LinearAlgebra.norm(x)
end

function _distance_to_set(x::Vector{T}, set::MOI.Reals) where {T<:Real}
_check_dimension(x, set)
return zero(T)
end
Loading