From 5d73c3b9bad57896eb90d405293dfbba31fcbffd Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Feb 2021 13:00:15 +1300 Subject: [PATCH 01/22] 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 https://github.com/joaquimg/FeasibilityOptInterface.jl/pull/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. --- docs/src/manual/solutions.md | 30 +++++++++++-- docs/src/reference/solutions.md | 6 +++ src/JuMP.jl | 1 + src/feasibility_checker.jl | 77 +++++++++++++++++++++++++++++++ test/feasibility_checker.jl | 80 +++++++++++++++++++++++++++++++++ 5 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 src/feasibility_checker.jl create mode 100644 test/feasibility_checker.jl diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index bed1c878bf0..48b265d9a30 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -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 @@ -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 @@ -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 @@ -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) @@ -489,3 +489,25 @@ 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). The returned `report` is `nothing` if the +point is feasible. Otherwise, it is a dictionary mapping the infeasible +constraint references to the distance between the point and the nearest point in +the set. + +```@example +using JuMP +model = Model() +@variable(model, x >= 1, Int) +@constraint(model, c1, x <= 1.95) +point = Dict(x => 2.5) +report = primal_feasibility_report(model, point) +``` + +To use the point from a previous solve, use: +```julia +point = Dict(v => value(v) for v in all_variables(model)) +``` diff --git a/docs/src/reference/solutions.md b/docs/src/reference/solutions.md index a9bcadb6d3a..c1b559d1f74 100644 --- a/docs/src/reference/solutions.md +++ b/docs/src/reference/solutions.md @@ -72,3 +72,9 @@ SensitivityReport lp_objective_perturbation_range lp_rhs_perturbation_range ``` + +## Feasibility + +```@docs +primal_feasibility_report +``` diff --git a/src/JuMP.jl b/src/JuMP.jl index 4c097356790..19f3b84f268 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -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 diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl new file mode 100644 index 00000000000..a5dea446008 --- /dev/null +++ b/src/feasibility_checker.jl @@ -0,0 +1,77 @@ +# 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/. + +""" + primal_feasibility_report( + model::Model, + [point::Dict{VariableRef,Float64}]; + atol::Float64 = 1e-8, + )::Union{Nothing,Dict{Any,Float64}} + +Check the primal feasibility of `model` at the point given by the dictionary +`point` mapping variables to primal values. If a variable is not given in +`point`, the value is assumed to be `0.0`. + +If the point is feasible, this function returns `nothing`. If infeasible, this +function returns a dictionary mapping the constriant reference of each violated +constraint to the distance it is from being feasible. + +To obtain `point` from a solution of a solved model, use: +```julia +point = Dict(v => value(v) for v in all_variables(model)) +``` +""" +function primal_feasibility_report( + model::Model, + point::Dict{VariableRef,Float64}; + atol::Float64 = 1e-8, +) + point_f = x -> get(point, x, 0.0) + violated_constraints = Dict{Any,Float64}() + for (F, S) in list_of_constraint_types(model) + # This loop is not type-stable because `F` and `S` change, but it should + # be fine because no one should be using this in performance-critical + # code. + 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 + end + return length(violated_constraints) == 0 ? nothing : violated_constraints +end + +function _distance_to_set(::Any, set::MOI.AbstractSet) + error( + "Feasibility checker for set type $(typeof(set)) has not been " * + "implemented yet." + ) +end + +function _distance_to_set(x::Float64, set::MOI.LessThan{Float64}) + return max(x - set.upper, 0.0) +end + +function _distance_to_set(x::Float64, set::MOI.GreaterThan{Float64}) + return max(set.lower - x, 0.0) +end + +function _distance_to_set(x::Float64, set::MOI.EqualTo{Float64}) + return abs(set.value - x) +end + +function _distance_to_set(x::Float64, set::MOI.Interval{Float64}) + return max(x - set.upper, set.lower - x, 0.0) +end + +function _distance_to_set(x::Float64, ::MOI.ZeroOne) + return min(abs(x - 0.0), abs(x - 1.0)) +end + +function _distance_to_set(x::Float64, ::MOI.Integer) + return abs(x - round(Int, x)) +end diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl new file mode 100644 index 00000000000..3b238ab0762 --- /dev/null +++ b/test/feasibility_checker.jl @@ -0,0 +1,80 @@ +# 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/. + +module TestFeasibilityChecker + +using JuMP +using Test + +function test_distance_to_set() + @test JuMP._distance_to_set(1.0, MOI.LessThan(2.0)) ≈ 0.0 + @test JuMP._distance_to_set(1.0, MOI.LessThan(0.5)) ≈ 0.5 + @test JuMP._distance_to_set(1.0, MOI.GreaterThan(2.0)) ≈ 1.0 + @test JuMP._distance_to_set(1.0, MOI.GreaterThan(0.5)) ≈ 0.0 + @test JuMP._distance_to_set(1.0, MOI.EqualTo(2.0)) ≈ 1.0 + @test JuMP._distance_to_set(1.0, MOI.EqualTo(0.5)) ≈ 0.5 + @test JuMP._distance_to_set(1.0, MOI.Interval(1.0, 2.0)) ≈ 0.0 + @test JuMP._distance_to_set(0.5, MOI.Interval(1.0, 2.0)) ≈ 0.5 + @test JuMP._distance_to_set(2.75, MOI.Interval(1.0, 2.0)) ≈ 0.75 + @test JuMP._distance_to_set(0.6, MOI.ZeroOne()) ≈ 0.4 + @test JuMP._distance_to_set(-0.01, MOI.ZeroOne()) ≈ 0.01 + @test JuMP._distance_to_set(1.01, MOI.ZeroOne()) ≈ 0.01 + @test JuMP._distance_to_set(0.6, MOI.Integer()) ≈ 0.4 + @test JuMP._distance_to_set(3.1, MOI.Integer()) ≈ 0.1 + @test JuMP._distance_to_set(-0.01, MOI.Integer()) ≈ 0.01 + @test JuMP._distance_to_set(1.01, MOI.Integer()) ≈ 0.01 +end + +function test_feasible() + model = Model() + @variable(model, x, Bin) + @variable(model, 0 <= y <= 2, Int) + @variable(model, z == 0.5) + @constraint(model, x + y + z >= 0.5) + @test primal_feasibility_report(model, Dict(z => 0.5)) === nothing +end + +function test_bounds() + model = Model() + @variable(model, x, Bin) + @variable(model, 0 <= y <= 2, Int) + @variable(model, z == 0.5) + report = primal_feasibility_report(model, Dict(x => 0.1, y => 2.1)) + @test report[BinaryRef(x)] ≈ 0.1 + @test report[UpperBoundRef(y)] ≈ 0.1 + @test report[IntegerRef(y)] ≈ 0.1 + @test report[FixRef(z)] ≈ 0.5 + @test length(report) == 4 +end + +function test_affine() + model = Model() + @variable(model, x) + @constraint(model, c1, x <= 0.5) + @constraint(model, c2, x >= 1.25) + @constraint(model, c3, x == 1.1) + @constraint(model, c4, 0 <= x <= 0.5) + report = primal_feasibility_report(model, Dict(x => 1.0)) + @test report[c1] ≈ 0.5 + @test report[c2] ≈ 0.25 + @test report[c3] ≈ 0.1 + @test report[c4] ≈ 0.5 + @test length(report) == 4 +end + +function runtests() + for name in names(@__MODULE__; all = true) + if !startswith("$(name)", "test_") + continue + end + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end +end + +end + +TestFeasibilityChecker.runtests() From a76aeca19ab69f335d343901df71620a4e913769 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Feb 2021 13:27:03 +1300 Subject: [PATCH 02/22] Add test for unsupported case --- test/feasibility_checker.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index 3b238ab0762..6b7c58fcb5e 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -25,6 +25,10 @@ function test_distance_to_set() @test JuMP._distance_to_set(3.1, MOI.Integer()) ≈ 0.1 @test JuMP._distance_to_set(-0.01, MOI.Integer()) ≈ 0.01 @test JuMP._distance_to_set(1.01, MOI.Integer()) ≈ 0.01 + @test_throws( + ErrorException, + JuMP._distance_to_set([1.0], MOI.Zeros(1)), + ) end function test_feasible() From 63f2f0babf0f66dece1036c06c1b7bc4c255b95b Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Feb 2021 13:55:58 +1300 Subject: [PATCH 03/22] Extend to some vector sets --- src/feasibility_checker.jl | 88 ++++++++++++++++++++++++++++++++----- test/feasibility_checker.jl | 79 ++++++++++++++++++++++++++++++--- 2 files changed, 149 insertions(+), 18 deletions(-) diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index a5dea446008..e8173af5f1f 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -3,6 +3,8 @@ # 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 + """ primal_feasibility_report( model::Model, @@ -36,7 +38,7 @@ function primal_feasibility_report( # code. for con in all_constraints(model, F, S) obj = constraint_object(con) - d = _distance_to_set(value(obj.func, point_f), obj.set) + d = _distance_to_set(value.(obj.func, point_f), obj.set) if d > atol violated_constraints[con] = d end @@ -52,26 +54,88 @@ function _distance_to_set(::Any, set::MOI.AbstractSet) ) end -function _distance_to_set(x::Float64, set::MOI.LessThan{Float64}) - return max(x - set.upper, 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::Float64, set::MOI.GreaterThan{Float64}) - return max(set.lower - x, 0.0) +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::Float64, set::MOI.EqualTo{Float64}) +function _distance_to_set(x::T, set::MOI.EqualTo{T}) where {T<:Real} return abs(set.value - x) end -function _distance_to_set(x::Float64, set::MOI.Interval{Float64}) - return max(x - set.upper, set.lower - x, 0.0) +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::Float64, ::MOI.ZeroOne) - return min(abs(x - 0.0), abs(x - 1.0)) +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::Float64, ::MOI.Integer) - return abs(x - round(Int, x)) +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<:Real} + _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 + diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index 6b7c58fcb5e..6b4bd5245fd 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -8,27 +8,79 @@ module TestFeasibilityChecker using JuMP using Test -function test_distance_to_set() +function test_unsupported() + @test_throws( + ErrorException, + JuMP._distance_to_set([1.0, 1.0], MOI.Complements(1)), + ) +end + +function test_lessthan() @test JuMP._distance_to_set(1.0, MOI.LessThan(2.0)) ≈ 0.0 @test JuMP._distance_to_set(1.0, MOI.LessThan(0.5)) ≈ 0.5 +end + +function test_greaterthan() @test JuMP._distance_to_set(1.0, MOI.GreaterThan(2.0)) ≈ 1.0 @test JuMP._distance_to_set(1.0, MOI.GreaterThan(0.5)) ≈ 0.0 +end + +function test_equalto() @test JuMP._distance_to_set(1.0, MOI.EqualTo(2.0)) ≈ 1.0 @test JuMP._distance_to_set(1.0, MOI.EqualTo(0.5)) ≈ 0.5 +end + +function test_interval() @test JuMP._distance_to_set(1.0, MOI.Interval(1.0, 2.0)) ≈ 0.0 @test JuMP._distance_to_set(0.5, MOI.Interval(1.0, 2.0)) ≈ 0.5 @test JuMP._distance_to_set(2.75, MOI.Interval(1.0, 2.0)) ≈ 0.75 +end + +function test_zeroone() @test JuMP._distance_to_set(0.6, MOI.ZeroOne()) ≈ 0.4 @test JuMP._distance_to_set(-0.01, MOI.ZeroOne()) ≈ 0.01 @test JuMP._distance_to_set(1.01, MOI.ZeroOne()) ≈ 0.01 +end + +function test_integer() @test JuMP._distance_to_set(0.6, MOI.Integer()) ≈ 0.4 @test JuMP._distance_to_set(3.1, MOI.Integer()) ≈ 0.1 @test JuMP._distance_to_set(-0.01, MOI.Integer()) ≈ 0.01 @test JuMP._distance_to_set(1.01, MOI.Integer()) ≈ 0.01 - @test_throws( - ErrorException, - JuMP._distance_to_set([1.0], MOI.Zeros(1)), - ) +end + +function test_semicontinuous() + s = MOI.Semicontinuous(2.0, 4.0) + @test JuMP._distance_to_set(-2.0, s) ≈ 2.0 + @test JuMP._distance_to_set(0.5, s) ≈ 0.5 + @test JuMP._distance_to_set(1.9, s) ≈ 0.1 + @test JuMP._distance_to_set(2.1, s) ≈ 0.0 + @test JuMP._distance_to_set(4.1, s) ≈ 0.1 +end + +function test_semiintger() + s = MOI.Semiinteger(1.9, 4.0) + @test JuMP._distance_to_set(-2.0, s) ≈ 2.0 + @test JuMP._distance_to_set(0.5, s) ≈ 0.5 + @test JuMP._distance_to_set(1.9, s) ≈ 0.1 + @test JuMP._distance_to_set(2.1, s) ≈ 0.1 + @test JuMP._distance_to_set(4.1, s) ≈ 0.1 +end + +function test_nonnegatives() + @test JuMP._distance_to_set([-1.0, 1.0], MOI.Nonnegatives(2)) ≈ 1.0 +end + +function test_nonpositives() + @test JuMP._distance_to_set([-1.0, 1.0], MOI.Nonpositives(2)) ≈ 1.0 +end + +function test_reals() + @test JuMP._distance_to_set([-1.0, 1.0], MOI.Reals(2)) ≈ 0.0 +end + +function test_zeros() + @test JuMP._distance_to_set([-1.0, 1.0], MOI.Zeros(2)) ≈ sqrt(2) end function test_feasible() @@ -53,7 +105,7 @@ function test_bounds() @test length(report) == 4 end -function test_affine() +function test_scalar_affine() model = Model() @variable(model, x) @constraint(model, c1, x <= 0.5) @@ -68,6 +120,21 @@ function test_affine() @test length(report) == 4 end +function test_vector_affine() + model = Model() + @variable(model, x[1:3]) + @constraint(model, c1, x in MOI.Nonnegatives(3)) + @constraint(model, c2, x in MOI.Nonpositives(3)) + @constraint(model, c3, x in MOI.Reals(3)) + @constraint(model, c4, x in MOI.Zeros(3)) + report = primal_feasibility_report(model, Dict(x[1] => 1.0, x[2] => -1.0)) + @test report[c1] ≈ 1.0 + @test report[c2] ≈ 1.0 + @test !haskey(report, c3) + @test report[c4] ≈ sqrt(2) + @test length(report) == 3 +end + function runtests() for name in names(@__MODULE__; all = true) if !startswith("$(name)", "test_") From 1a42c640750c3691fd1090b5b84bacdb1b29de1b Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Feb 2021 14:08:31 +1300 Subject: [PATCH 04/22] More tests --- test/feasibility_checker.jl | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index 6b4bd5245fd..6573af23b0f 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -120,7 +120,22 @@ function test_scalar_affine() @test length(report) == 4 end -function test_vector_affine() +function test_scalar_quadratic() + model = Model() + @variable(model, x) + @constraint(model, c1, x^2 + x <= 0.5) + @constraint(model, c2, x^2 - x >= 1.25) + @constraint(model, c3, x^2 + x == 1.1) + @constraint(model, c4, 0 <= x^2 + x <= 0.5) + report = primal_feasibility_report(model, Dict(x => 1.0)) + @test report[c1] ≈ 1.5 + @test report[c2] ≈ 1.25 + @test report[c3] ≈ 0.9 + @test report[c4] ≈ 1.5 + @test length(report) == 4 +end + +function test_vector() model = Model() @variable(model, x[1:3]) @constraint(model, c1, x in MOI.Nonnegatives(3)) @@ -135,6 +150,21 @@ function test_vector_affine() @test length(report) == 3 end +function test_vector_affine() + model = Model() + @variable(model, x[1:3]) + @constraint(model, c1, 2 * x in MOI.Nonnegatives(3)) + @constraint(model, c2, 2 * x in MOI.Nonpositives(3)) + @constraint(model, c3, 2 * x in MOI.Reals(3)) + @constraint(model, c4, 2 * x in MOI.Zeros(3)) + report = primal_feasibility_report(model, Dict(x[1] => 1.0, x[2] => -1.0)) + @test report[c1] ≈ 2.0 + @test report[c2] ≈ 2.0 + @test !haskey(report, c3) + @test report[c4] ≈ sqrt(8) + @test length(report) == 3 +end + function runtests() for name in names(@__MODULE__; all = true) if !startswith("$(name)", "test_") From 013978170c9879053ffb946956574376581e6432 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Feb 2021 15:29:26 +1300 Subject: [PATCH 05/22] Add joaquimg suggestions --- src/feasibility_checker.jl | 8 ++++---- test/feasibility_checker.jl | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index e8173af5f1f..f4faccae765 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -8,7 +8,7 @@ using LinearAlgebra """ primal_feasibility_report( model::Model, - [point::Dict{VariableRef,Float64}]; + [point::AbstractDict{VariableRef,Float64}]; atol::Float64 = 1e-8, )::Union{Nothing,Dict{Any,Float64}} @@ -27,7 +27,7 @@ point = Dict(v => value(v) for v in all_variables(model)) """ function primal_feasibility_report( model::Model, - point::Dict{VariableRef,Float64}; + point::AbstractDict{VariableRef,Float64}; atol::Float64 = 1e-8, ) point_f = x -> get(point, x, 0.0) @@ -66,7 +66,7 @@ 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<:Real} +function _distance_to_set(x::T, set::MOI.EqualTo{T}) where {T<:Number} return abs(set.value - x) end @@ -125,7 +125,7 @@ end function _distance_to_set( x::Vector{T}, set::MOI.Zeros -) where {T<:Real} +) where {T<:Number} _check_dimension(x, set) return LinearAlgebra.norm(x) end diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index 6573af23b0f..e499ff0712b 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -68,18 +68,34 @@ function test_semiintger() end function test_nonnegatives() + @test_throws( + DimensionMismatch, + JuMP._distance_to_set([-1.0, 1.0], MOI.Nonnegatives(1)) + ) @test JuMP._distance_to_set([-1.0, 1.0], MOI.Nonnegatives(2)) ≈ 1.0 end function test_nonpositives() + @test_throws( + DimensionMismatch, + JuMP._distance_to_set([-1.0, 1.0], MOI.Nonpositives(1)) + ) @test JuMP._distance_to_set([-1.0, 1.0], MOI.Nonpositives(2)) ≈ 1.0 end function test_reals() + @test_throws( + DimensionMismatch, + JuMP._distance_to_set([-1.0, 1.0], MOI.Reals(1)) + ) @test JuMP._distance_to_set([-1.0, 1.0], MOI.Reals(2)) ≈ 0.0 end function test_zeros() + @test_throws( + DimensionMismatch, + JuMP._distance_to_set([-1.0, 1.0], MOI.Zeros(1)) + ) @test JuMP._distance_to_set([-1.0, 1.0], MOI.Zeros(2)) ≈ sqrt(2) end From 9a3cee816c521e1d9786b3ab4fd9c3bb633c17a1 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Feb 2021 16:09:22 +1300 Subject: [PATCH 06/22] Add nonlinear constraints to checker --- src/feasibility_checker.jl | 17 +++++++++++++++++ test/feasibility_checker.jl | 14 ++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index f4faccae765..549c30ee6fd 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -44,6 +44,23 @@ function primal_feasibility_report( end end end + if num_nl_constraints(model) > 0 + 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 + end return length(violated_constraints) == 0 ? nothing : violated_constraints end diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index e499ff0712b..2aaee6aeac4 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -181,6 +181,20 @@ function test_vector_affine() @test length(report) == 3 end +function test_nonlinear() + model = Model() + @variable(model, x) + @NLconstraint(model, c1, sin(x) <= 0.0) + @NLconstraint(model, c2, sin(x) <= 1.0) + @NLconstraint(model, c3, exp(x) >= 2.0) + @NLconstraint(model, c4, x + x^2 - x^3 == 0.5) + report = primal_feasibility_report(model, Dict(x => 0.5)) + @test report[c1] ≈ sin(0.5) + @test !haskey(report, c2) + @test report[c3] ≈ 2 - exp(0.5) + @test report[c4] ≈ 0.125 +end + function runtests() for name in names(@__MODULE__; all = true) if !startswith("$(name)", "test_") From 0c37ce071ea3ea535dc21323bd9d75ba67f3d5fc Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Feb 2021 16:20:33 +1300 Subject: [PATCH 07/22] Improve documentation --- docs/src/manual/solutions.md | 28 +++++++++++++++++++--------- src/feasibility_checker.jl | 11 +++++++---- test/feasibility_checker.jl | 18 +++++++++++------- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index 48b265d9a30..f95c3f25e72 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -493,21 +493,31 @@ end ## Checking feasibility of solutions To check the feasibility of a primal solution, use -[`primal_feasibility_report`](@ref). The returned `report` is `nothing` if the -point is feasible. Otherwise, it is a dictionary mapping the infeasible -constraint references to the distance between the point and the nearest point in -the set. - -```@example +[`primal_feasibility_report`](@ref), which takes a `model`, a dictionary mapping +each variable to a primal solution value, and a tolerance `atol`. If a variable +is not given in dictionary, the value is assumed to be `0.0`. + +The function returns a dictionary which maps the infeasible constraint +references to the distance between the point and the nearest point in the +corresponding set. A point is classed as infeasible if the distance is greater +than a supplied tolerance `atol`, and feasible otherwise. +```@example feasibility using JuMP model = Model() @variable(model, x >= 1, Int) -@constraint(model, c1, x <= 1.95) +@variable(model, y) +@constraint(model, c1, x + y <= 1.95) point = Dict(x => 2.5) -report = primal_feasibility_report(model, point) +report = primal_feasibility_report(model, point; atol = 1e-6) +``` + +If the point is feasible, this function returns `nothing`. +```@example feasibility +point = Dict(x => 1.0) +report = primal_feasibility_report(model, point; atol = 1e-6) ``` -To use the point from a previous solve, use: +To obtain the primal point from a previous solve, use: ```julia point = Dict(v => value(v) for v in all_variables(model)) ``` diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index 549c30ee6fd..ba07b8fcb71 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -9,16 +9,19 @@ using LinearAlgebra primal_feasibility_report( model::Model, [point::AbstractDict{VariableRef,Float64}]; - atol::Float64 = 1e-8, + atol::Float64, )::Union{Nothing,Dict{Any,Float64}} Check the primal feasibility of `model` at the point given by the dictionary `point` mapping variables to primal values. If a variable is not given in `point`, the value is assumed to be `0.0`. +A point is classed as feasible if the distance between the point and the set is +less than or equal to `atol`. + If the point is feasible, this function returns `nothing`. If infeasible, this -function returns a dictionary mapping the constriant reference of each violated -constraint to the distance it is from being feasible. +function returns a dictionary mapping the constraint reference of each violated +constraint to the distance from feasibility. To obtain `point` from a solution of a solved model, use: ```julia @@ -28,7 +31,7 @@ point = Dict(v => value(v) for v in all_variables(model)) function primal_feasibility_report( model::Model, point::AbstractDict{VariableRef,Float64}; - atol::Float64 = 1e-8, + atol::Float64, ) point_f = x -> get(point, x, 0.0) violated_constraints = Dict{Any,Float64}() diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index 2aaee6aeac4..ac302c1b4c3 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -105,7 +105,8 @@ function test_feasible() @variable(model, 0 <= y <= 2, Int) @variable(model, z == 0.5) @constraint(model, x + y + z >= 0.5) - @test primal_feasibility_report(model, Dict(z => 0.5)) === nothing + report = primal_feasibility_report(model, Dict(z => 0.5); atol = 1e-6) + @test report === nothing end function test_bounds() @@ -113,7 +114,8 @@ function test_bounds() @variable(model, x, Bin) @variable(model, 0 <= y <= 2, Int) @variable(model, z == 0.5) - report = primal_feasibility_report(model, Dict(x => 0.1, y => 2.1)) + point = Dict(x => 0.1, y => 2.1) + report = primal_feasibility_report(model, point; atol = 1e-6) @test report[BinaryRef(x)] ≈ 0.1 @test report[UpperBoundRef(y)] ≈ 0.1 @test report[IntegerRef(y)] ≈ 0.1 @@ -128,7 +130,7 @@ function test_scalar_affine() @constraint(model, c2, x >= 1.25) @constraint(model, c3, x == 1.1) @constraint(model, c4, 0 <= x <= 0.5) - report = primal_feasibility_report(model, Dict(x => 1.0)) + report = primal_feasibility_report(model, Dict(x => 1.0); atol = 1e-6) @test report[c1] ≈ 0.5 @test report[c2] ≈ 0.25 @test report[c3] ≈ 0.1 @@ -143,7 +145,7 @@ function test_scalar_quadratic() @constraint(model, c2, x^2 - x >= 1.25) @constraint(model, c3, x^2 + x == 1.1) @constraint(model, c4, 0 <= x^2 + x <= 0.5) - report = primal_feasibility_report(model, Dict(x => 1.0)) + report = primal_feasibility_report(model, Dict(x => 1.0); atol = 1e-6) @test report[c1] ≈ 1.5 @test report[c2] ≈ 1.25 @test report[c3] ≈ 0.9 @@ -158,7 +160,8 @@ function test_vector() @constraint(model, c2, x in MOI.Nonpositives(3)) @constraint(model, c3, x in MOI.Reals(3)) @constraint(model, c4, x in MOI.Zeros(3)) - report = primal_feasibility_report(model, Dict(x[1] => 1.0, x[2] => -1.0)) + point = Dict(x[1] => 1.0, x[2] => -1.0) + report = primal_feasibility_report(model, point; atol = 1e-6) @test report[c1] ≈ 1.0 @test report[c2] ≈ 1.0 @test !haskey(report, c3) @@ -173,7 +176,8 @@ function test_vector_affine() @constraint(model, c2, 2 * x in MOI.Nonpositives(3)) @constraint(model, c3, 2 * x in MOI.Reals(3)) @constraint(model, c4, 2 * x in MOI.Zeros(3)) - report = primal_feasibility_report(model, Dict(x[1] => 1.0, x[2] => -1.0)) + point = Dict(x[1] => 1.0, x[2] => -1.0) + report = primal_feasibility_report(model, point; atol = 1e-6) @test report[c1] ≈ 2.0 @test report[c2] ≈ 2.0 @test !haskey(report, c3) @@ -188,7 +192,7 @@ function test_nonlinear() @NLconstraint(model, c2, sin(x) <= 1.0) @NLconstraint(model, c3, exp(x) >= 2.0) @NLconstraint(model, c4, x + x^2 - x^3 == 0.5) - report = primal_feasibility_report(model, Dict(x => 0.5)) + report = primal_feasibility_report(model, Dict(x => 0.5); atol = 1e-6) @test report[c1] ≈ sin(0.5) @test !haskey(report, c2) @test report[c3] ≈ 2 - exp(0.5) From d746b838692027d8172a1f7e4b33134013e458e4 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Feb 2021 16:55:08 +1300 Subject: [PATCH 08/22] Doc changes --- docs/src/manual/solutions.md | 1 + src/feasibility_checker.jl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index f95c3f25e72..4f886359542 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -515,6 +515,7 @@ If the point is feasible, this function returns `nothing`. ```@example feasibility point = Dict(x => 1.0) report = primal_feasibility_report(model, point; atol = 1e-6) +report === nothing ``` To obtain the primal point from a previous solve, use: diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index ba07b8fcb71..bf1384a5321 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -8,7 +8,7 @@ using LinearAlgebra """ primal_feasibility_report( model::Model, - [point::AbstractDict{VariableRef,Float64}]; + point::AbstractDict{VariableRef,Float64}; atol::Float64, )::Union{Nothing,Dict{Any,Float64}} From 7736778a3abc42bde79e65b5cd8d0837ec404f10 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 16 Feb 2021 08:28:01 +1300 Subject: [PATCH 09/22] Refactor feasibility_checker --- src/feasibility_checker.jl | 127 ++++++++++++++++++++++++------------ test/feasibility_checker.jl | 35 +++++++--- 2 files changed, 114 insertions(+), 48 deletions(-) diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index bf1384a5321..3ae3167e8a2 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -8,63 +8,110 @@ using LinearAlgebra """ primal_feasibility_report( model::Model, - point::AbstractDict{VariableRef,Float64}; - atol::Float64, - )::Union{Nothing,Dict{Any,Float64}} + [point::AbstractDict{VariableRef,Float64}]; + atol::Float64 = 0.0, + )::Dict{Any,Float64} -Check the primal feasibility of `model` at the point given by the dictionary -`point` mapping variables to primal values. If a variable is not given in -`point`, the value is assumed to be `0.0`. +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. -A point is classed as feasible if the distance between the point and the set is -less than or equal to `atol`. +Use `atol` to exclude constraints for which the distance is less-than or +equal-to `atol`. `atol` defaults to `0.0`. -If the point is feasible, this function returns `nothing`. If infeasible, this -function returns a dictionary mapping the constraint reference of each violated -constraint to the distance from feasibility. +## Notes -To obtain `point` from a solution of a solved model, use: -```julia -point = Dict(v => value(v) for v in all_variables(model)) + * If a variable is not given in `point`, the value is assumed to be `0.0`. + * If no point is provided, the primal solution from the last time the model was + solved is used. + +## Examples + +```jldoctest; setup=:(using JuMP) +model = Model() +@variable(model, 0.5 <= x <= 1) +primal_feasibility_report(model, Dict(x => 0.2)) + +# output + +Dict{Any,Float64} with 1 entry: + x ≥ 0.5 => 0.3 ``` """ function primal_feasibility_report( model::Model, point::AbstractDict{VariableRef,Float64}; - atol::Float64, + atol::Float64 = 0.0, ) point_f = x -> get(point, x, 0.0) violated_constraints = Dict{Any,Float64}() for (F, S) in list_of_constraint_types(model) - # This loop is not type-stable because `F` and `S` change, but it should - # be fine because no one should be using this in performance-critical - # code. - 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 + _add_infeasible_constraints( + model, F, S, violated_constraints, point_f, atol + ) end if num_nl_constraints(model) > 0 - 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 + _add_infeasible_nonlinear_constraints( + model, violated_constraints, point_f, atol + ) + end + return violated_constraints +end + +function primal_feasibility_report(model::Model; atol::Float64 = 0.0) + if !has_values(model) + error( + "No primal solution is available. You must provide a point at " * + "which to check feasibility." + ) + end + return primal_feasibility_report( + model, + Dict(v => value(v) for v in all_variables(model)); + atol = atol, + ) +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 length(violated_constraints) == 0 ? nothing : violated_constraints + return end function _distance_to_set(::Any, set::MOI.AbstractSet) diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index ac302c1b4c3..479fb8c687a 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -99,14 +99,33 @@ function test_zeros() @test JuMP._distance_to_set([-1.0, 1.0], MOI.Zeros(2)) ≈ sqrt(2) end +function test_no_solution() + model = Model() + @variable(model, x, Bin) + @test_throws ErrorException primal_feasibility_report(model) +end + +function test_primal_solution() + model = Model(() -> MOIU.MockOptimizer(MOIU.Model{Float64}())) + @variable(model, x, Bin) + optimize!(model) + mock = backend(model).optimizer.model + MOI.set(mock, MOI.TerminationStatus(), MOI.OPTIMAL) + MOI.set(mock, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) + MOI.set(mock, MOI.VariablePrimal(), optimizer_index(x), 1.0) + report = primal_feasibility_report(model) + @test isempty(report) +end + + function test_feasible() model = Model() @variable(model, x, Bin) @variable(model, 0 <= y <= 2, Int) @variable(model, z == 0.5) @constraint(model, x + y + z >= 0.5) - report = primal_feasibility_report(model, Dict(z => 0.5); atol = 1e-6) - @test report === nothing + report = primal_feasibility_report(model, Dict(z => 0.5)) + @test isempty(report) end function test_bounds() @@ -115,7 +134,7 @@ function test_bounds() @variable(model, 0 <= y <= 2, Int) @variable(model, z == 0.5) point = Dict(x => 0.1, y => 2.1) - report = primal_feasibility_report(model, point; atol = 1e-6) + report = primal_feasibility_report(model, point) @test report[BinaryRef(x)] ≈ 0.1 @test report[UpperBoundRef(y)] ≈ 0.1 @test report[IntegerRef(y)] ≈ 0.1 @@ -130,7 +149,7 @@ function test_scalar_affine() @constraint(model, c2, x >= 1.25) @constraint(model, c3, x == 1.1) @constraint(model, c4, 0 <= x <= 0.5) - report = primal_feasibility_report(model, Dict(x => 1.0); atol = 1e-6) + report = primal_feasibility_report(model, Dict(x => 1.0)) @test report[c1] ≈ 0.5 @test report[c2] ≈ 0.25 @test report[c3] ≈ 0.1 @@ -145,7 +164,7 @@ function test_scalar_quadratic() @constraint(model, c2, x^2 - x >= 1.25) @constraint(model, c3, x^2 + x == 1.1) @constraint(model, c4, 0 <= x^2 + x <= 0.5) - report = primal_feasibility_report(model, Dict(x => 1.0); atol = 1e-6) + report = primal_feasibility_report(model, Dict(x => 1.0)) @test report[c1] ≈ 1.5 @test report[c2] ≈ 1.25 @test report[c3] ≈ 0.9 @@ -161,7 +180,7 @@ function test_vector() @constraint(model, c3, x in MOI.Reals(3)) @constraint(model, c4, x in MOI.Zeros(3)) point = Dict(x[1] => 1.0, x[2] => -1.0) - report = primal_feasibility_report(model, point; atol = 1e-6) + report = primal_feasibility_report(model, point) @test report[c1] ≈ 1.0 @test report[c2] ≈ 1.0 @test !haskey(report, c3) @@ -177,7 +196,7 @@ function test_vector_affine() @constraint(model, c3, 2 * x in MOI.Reals(3)) @constraint(model, c4, 2 * x in MOI.Zeros(3)) point = Dict(x[1] => 1.0, x[2] => -1.0) - report = primal_feasibility_report(model, point; atol = 1e-6) + report = primal_feasibility_report(model, point) @test report[c1] ≈ 2.0 @test report[c2] ≈ 2.0 @test !haskey(report, c3) @@ -192,7 +211,7 @@ function test_nonlinear() @NLconstraint(model, c2, sin(x) <= 1.0) @NLconstraint(model, c3, exp(x) >= 2.0) @NLconstraint(model, c4, x + x^2 - x^3 == 0.5) - report = primal_feasibility_report(model, Dict(x => 0.5); atol = 1e-6) + report = primal_feasibility_report(model, Dict(x => 0.5)) @test report[c1] ≈ sin(0.5) @test !haskey(report, c2) @test report[c3] ≈ 2 - exp(0.5) From 451c246cf50584c400056e4c444305551411f6e6 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 16 Feb 2021 08:38:55 +1300 Subject: [PATCH 10/22] Update docs --- docs/src/manual/solutions.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index 4f886359542..b8f96712bc1 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -501,9 +501,10 @@ The function returns a dictionary which maps the infeasible constraint references to the distance between the point and the nearest point in the corresponding set. A point is classed as infeasible if the distance is greater than a supplied tolerance `atol`, and feasible otherwise. + ```@example feasibility -using JuMP -model = Model() +using JuMP, GLPK +model = Model(GLPK.Optimizer) @variable(model, x >= 1, Int) @variable(model, y) @constraint(model, c1, x + y <= 1.95) @@ -511,14 +512,14 @@ point = Dict(x => 2.5) report = primal_feasibility_report(model, point; atol = 1e-6) ``` -If the point is feasible, this function returns `nothing`. +If the point is feasible, this function returns an empty dictionary. ```@example feasibility point = Dict(x => 1.0) report = primal_feasibility_report(model, point; atol = 1e-6) -report === nothing ``` -To obtain the primal point from a previous solve, use: -```julia -point = Dict(v => value(v) for v in all_variables(model)) +To use the primal solution from a solve, omit the `point` argument: +```@example feasibility +optimize!(model) +report = primal_feasibility_report(model) ``` From 5e07edc50fa5e54d7fe0aaf160e9f7bbf7311d4a Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 16 Feb 2021 15:43:40 +1300 Subject: [PATCH 11/22] Tweak docstring --- src/feasibility_checker.jl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index 3ae3167e8a2..7a45583f76f 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -14,10 +14,8 @@ using LinearAlgebra 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. - -Use `atol` to exclude constraints for which the distance is less-than or -equal-to `atol`. `atol` defaults to `0.0`. +distance between the point and the nearest feasible point, if the distance is +greater than `atol`. ## Notes @@ -28,12 +26,11 @@ equal-to `atol`. `atol` defaults to `0.0`. ## Examples ```jldoctest; setup=:(using JuMP) -model = Model() -@variable(model, 0.5 <= x <= 1) -primal_feasibility_report(model, Dict(x => 0.2)) +julia> model = Model(); -# output +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 ``` From 847863a3a1a91ca007025814b318af96103b0889 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 24 Feb 2021 10:35:55 +1300 Subject: [PATCH 12/22] Add a default argument and handle missing --- src/feasibility_checker.jl | 16 ++++++++++++++-- test/feasibility_checker.jl | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index 7a45583f76f..3ee98009f5b 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -10,6 +10,7 @@ using LinearAlgebra model::Model, [point::AbstractDict{VariableRef,Float64}]; atol::Float64 = 0.0, + default::Union{Float64,Missing} = 0.0, )::Dict{Any,Float64} Given a dictionary `point`, which maps variables to primal values, return a @@ -19,7 +20,9 @@ greater than `atol`. ## Notes - * If a variable is not given in `point`, the value is assumed to be `0.0`. + * If a variable is not given in `point`, the value is assumed to be `default`. + If `default = missing`, constraints which contain a missing variable are + skipped. * If no point is provided, the primal solution from the last time the model was solved is used. @@ -39,8 +42,9 @@ function primal_feasibility_report( model::Model, point::AbstractDict{VariableRef,Float64}; atol::Float64 = 0.0, + default::Union{Float64,Missing} = 0.0, ) - point_f = x -> get(point, x, 0.0) + point_f = x -> get(point, x, default) violated_constraints = Dict{Any,Float64}() for (F, S) in list_of_constraint_types(model) _add_infeasible_constraints( @@ -48,6 +52,12 @@ function primal_feasibility_report( ) end if num_nl_constraints(model) > 0 + if ismissing(default) + error( + "`default` cannot be `missing` when nonlinear constraints " * + "are present.", + ) + end _add_infeasible_nonlinear_constraints( model, violated_constraints, point_f, atol ) @@ -118,6 +128,8 @@ function _distance_to_set(::Any, set::MOI.AbstractSet) ) end +_distance_to_set(::Missing, ::MOI.AbstractSet) = 0.0 + ### ### MOI.AbstractScalarSets ### diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index 479fb8c687a..69f2846ad26 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -128,6 +128,17 @@ function test_feasible() @test isempty(report) end +function test_missing() + model = Model() + @variable(model, x, Bin) + @variable(model, 0 <= y <= 2, Int) + @variable(model, z == 0.5) + @constraint(model, x + y + z >= 0.5) + report = primal_feasibility_report(model, Dict(z => 0.0), default = missing) + @test report[FixRef(z)] == 0.5 + @test length(report) == 1 +end + function test_bounds() model = Model() @variable(model, x, Bin) @@ -218,6 +229,19 @@ function test_nonlinear() @test report[c4] ≈ 0.125 end +function test_nonlinear_missing() + model = Model() + @variable(model, x) + @NLconstraint(model, c1, sin(x) <= 0.0) + @test_throws( + ErrorException( + "`default` cannot be `missing` when nonlinear constraints are " * + "present.", + ), + primal_feasibility_report(model, Dict(x => 0.5); default = missing) + ) +end + function runtests() for name in names(@__MODULE__; all = true) if !startswith("$(name)", "test_") From 484f2ac3d000568663bdfba7f1aaf1ce60ea08f6 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 24 Feb 2021 11:02:22 +1300 Subject: [PATCH 13/22] Update docs --- docs/src/manual/solutions.md | 66 ++++++++++++++++++++++++++---------- src/feasibility_checker.jl | 9 ++--- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index b8f96712bc1..988c5e05b9f 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -494,32 +494,62 @@ end 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, and a tolerance `atol`. If a variable -is not given in dictionary, the value is assumed to be `0.0`. +each variable to a primal solution value (defaults to the last solved solution), +a tolerance `atol` (defaults to `0.0`), and a `default` value for variables not +given in dictionary (defaults to `0.0`). The function returns a dictionary which maps the infeasible constraint references to the distance between the point and the nearest point in the corresponding set. A point is classed as infeasible if the distance is greater -than a supplied tolerance `atol`, and feasible otherwise. +than the supplied tolerance `atol`. -```@example feasibility -using JuMP, GLPK -model = Model(GLPK.Optimizer) -@variable(model, x >= 1, Int) -@variable(model, y) -@constraint(model, c1, x + y <= 1.95) -point = Dict(x => 2.5) -report = primal_feasibility_report(model, point; atol = 1e-6) +```jldoctest feasibility +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: + x integer => 0.1 + c1 : x + y ≤ 1.95 => 0.01 + +julia> primal_feasibility_report(model, point; atol = 0.02) +Dict{Any,Float64} with 1 entry: + x integer => 0.1 ``` -If the point is feasible, this function returns an empty dictionary. -```@example feasibility -point = Dict(x => 1.0) -report = primal_feasibility_report(model, point; atol = 1e-6) +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: -```@example feasibility -optimize!(model) -report = primal_feasibility_report(model) +```jldoctest feasibility +julia> optimize!(model) + +julia> primal_feasibility_report(model) +Dict{Any,Float64} with 0 entries +``` + +Use the `default` keyword argument to provide a value for variables not in the +`point`: +```jldoctest feasibility +julia> primal_feasibility_report(model, Dict(x => 1.0); default = 1.5) +Dict{Any,Float64} with 1 entry: + c1 : x + y ≤ 1.95 => 0.55 +``` + +Pass `default = missing` to skip constraints which contain variables that are +not in `point`: +```jldoctest feasibility +julia> primal_feasibility_report(model, Dict(x => 2.1); default = missing) +Dict{Any,Float64} with 1 entry: + x integer => 0.1 ``` diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index 3ee98009f5b..703221f995f 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -65,18 +65,15 @@ function primal_feasibility_report( return violated_constraints end -function primal_feasibility_report(model::Model; atol::Float64 = 0.0) +function primal_feasibility_report(model::Model; kwargs...) if !has_values(model) error( "No primal solution is available. You must provide a point at " * "which to check feasibility." ) end - return primal_feasibility_report( - model, - Dict(v => value(v) for v in all_variables(model)); - atol = atol, - ) + point = Dict(v => value(v) for v in all_variables(model)) + return primal_feasibility_report(model, point; kwargs...) end function _add_infeasible_constraints( From 4f5aa6fd2133391cb32a788f9a90ab7e1aa942ab Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 24 Feb 2021 11:36:41 +1300 Subject: [PATCH 14/22] Update solutions.md --- docs/src/manual/solutions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index 988c5e05b9f..ab79b186fa1 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -521,7 +521,7 @@ Dict{Any,Float64} with 2 entries: julia> primal_feasibility_report(model, point; atol = 0.02) Dict{Any,Float64} with 1 entry: - x integer => 0.1 + x integer => 0.1 ``` If the point is feasible, an empty dictionary is returned: From 849839b9edb502138a598c2c23b3d2babd7f4d59 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 24 Feb 2021 11:57:25 +1300 Subject: [PATCH 15/22] Update solutions.md --- docs/src/manual/solutions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index ab79b186fa1..a803483a2c1 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -516,8 +516,8 @@ julia> point = Dict(x => 1.9, y => 0.06); julia> primal_feasibility_report(model, point) Dict{Any,Float64} with 2 entries: - x integer => 0.1 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: From 25e399d753178f958116dfa422752542f20cdd51 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 26 Feb 2021 10:33:57 +1300 Subject: [PATCH 16/22] Add skip_missing argument --- docs/src/manual/solutions.md | 15 +++------------ src/feasibility_checker.jl | 26 +++++++++++++++++--------- test/feasibility_checker.jl | 32 ++++++++++++++++++++++++-------- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index a803483a2c1..70bd44d2962 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -495,8 +495,7 @@ end 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), -a tolerance `atol` (defaults to `0.0`), and a `default` value for variables not -given in dictionary (defaults to `0.0`). +a tolerance `atol` (defaults to `0.0`). The function returns a dictionary which maps the infeasible constraint references to the distance between the point and the nearest point in the @@ -538,18 +537,10 @@ julia> primal_feasibility_report(model) Dict{Any,Float64} with 0 entries ``` -Use the `default` keyword argument to provide a value for variables not in the -`point`: -```jldoctest feasibility -julia> primal_feasibility_report(model, Dict(x => 1.0); default = 1.5) -Dict{Any,Float64} with 1 entry: - c1 : x + y ≤ 1.95 => 0.55 -``` - -Pass `default = missing` to skip constraints which contain variables that are +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); default = missing) +julia> primal_feasibility_report(model, Dict(x => 2.1); skip_missing = true) Dict{Any,Float64} with 1 entry: x integer => 0.1 ``` diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index 703221f995f..12e5cf6ca09 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -10,7 +10,7 @@ using LinearAlgebra model::Model, [point::AbstractDict{VariableRef,Float64}]; atol::Float64 = 0.0, - default::Union{Float64,Missing} = 0.0, + skip_missing::Bool = false, )::Dict{Any,Float64} Given a dictionary `point`, which maps variables to primal values, return a @@ -20,9 +20,8 @@ greater than `atol`. ## Notes - * If a variable is not given in `point`, the value is assumed to be `default`. - If `default = missing`, constraints which contain a missing variable are - skipped. + * If `skip_missing = true`, constraints containing variables that are not in + `point` will be ignored. * If no point is provided, the primal solution from the last time the model was solved is used. @@ -42,9 +41,18 @@ function primal_feasibility_report( model::Model, point::AbstractDict{VariableRef,Float64}; atol::Float64 = 0.0, - default::Union{Float64,Missing} = 0.0, + skip_missing::Bool = false, ) - point_f = x -> get(point, x, default) + function point_f(x) + 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( @@ -52,10 +60,10 @@ function primal_feasibility_report( ) end if num_nl_constraints(model) > 0 - if ismissing(default) + if skip_missing error( - "`default` cannot be `missing` when nonlinear constraints " * - "are present.", + "`skip_missing = true` is not allowed when nonlinear " * + "constraints are present.", ) end _add_infeasible_nonlinear_constraints( diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index 69f2846ad26..888580ea13c 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -124,7 +124,7 @@ function test_feasible() @variable(model, 0 <= y <= 2, Int) @variable(model, z == 0.5) @constraint(model, x + y + z >= 0.5) - report = primal_feasibility_report(model, Dict(z => 0.5)) + report = primal_feasibility_report(model, Dict(x => 0.0, y => 0.0, z => 0.5)) @test isempty(report) end @@ -134,17 +134,33 @@ function test_missing() @variable(model, 0 <= y <= 2, Int) @variable(model, z == 0.5) @constraint(model, x + y + z >= 0.5) - report = primal_feasibility_report(model, Dict(z => 0.0), default = missing) + report = primal_feasibility_report( + model, + Dict(z => 0.0), + skip_missing = true, + ) @test report[FixRef(z)] == 0.5 @test length(report) == 1 end +function test_missing_error() + model = Model() + @variable(model, x, Bin) + @variable(model, 0 <= y <= 2, Int) + point = Dict(x => 0.1) + err = ErrorException( + "point does not contain a value for variable $(y). Provide a value, " * + "or pass `skip_missing = true`.", + ) + @test_throws err primal_feasibility_report(model, point) +end + function test_bounds() model = Model() @variable(model, x, Bin) @variable(model, 0 <= y <= 2, Int) @variable(model, z == 0.5) - point = Dict(x => 0.1, y => 2.1) + point = Dict(x => 0.1, y => 2.1, z => 0.0) report = primal_feasibility_report(model, point) @test report[BinaryRef(x)] ≈ 0.1 @test report[UpperBoundRef(y)] ≈ 0.1 @@ -190,7 +206,7 @@ function test_vector() @constraint(model, c2, x in MOI.Nonpositives(3)) @constraint(model, c3, x in MOI.Reals(3)) @constraint(model, c4, x in MOI.Zeros(3)) - point = Dict(x[1] => 1.0, x[2] => -1.0) + point = Dict(x[1] => 1.0, x[2] => -1.0, x[3] => 0.0) report = primal_feasibility_report(model, point) @test report[c1] ≈ 1.0 @test report[c2] ≈ 1.0 @@ -206,7 +222,7 @@ function test_vector_affine() @constraint(model, c2, 2 * x in MOI.Nonpositives(3)) @constraint(model, c3, 2 * x in MOI.Reals(3)) @constraint(model, c4, 2 * x in MOI.Zeros(3)) - point = Dict(x[1] => 1.0, x[2] => -1.0) + point = Dict(x[1] => 1.0, x[2] => -1.0, x[3] => 0.0) report = primal_feasibility_report(model, point) @test report[c1] ≈ 2.0 @test report[c2] ≈ 2.0 @@ -235,10 +251,10 @@ function test_nonlinear_missing() @NLconstraint(model, c1, sin(x) <= 0.0) @test_throws( ErrorException( - "`default` cannot be `missing` when nonlinear constraints are " * - "present.", + "`skip_missing = true` is not allowed when nonlinear constraints " * + "are present.", ), - primal_feasibility_report(model, Dict(x => 0.5); default = missing) + primal_feasibility_report(model, Dict(x => 0.5); skip_missing = true) ) end From ac0097826e1c50e026ef66912ef72b28ee685c4e Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 1 Mar 2021 16:55:39 +1300 Subject: [PATCH 17/22] Refactor how default point is provided --- src/feasibility_checker.jl | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index 12e5cf6ca09..3beddeda2e4 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -5,10 +5,20 @@ 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}]; + point::AbstractDict{VariableRef,Float64} = _last_primal_solution(model), atol::Float64 = 0.0, skip_missing::Bool = false, )::Dict{Any,Float64} @@ -39,11 +49,11 @@ Dict{Any,Float64} with 1 entry: """ function primal_feasibility_report( model::Model, - point::AbstractDict{VariableRef,Float64}; + point::AbstractDict{VariableRef,Float64} = _last_primal_solution(model); atol::Float64 = 0.0, skip_missing::Bool = false, ) - function point_f(x) + function point_f(x::VariableRef) fx = get(point, x, missing) if ismissing(fx) && !skip_missing error( @@ -73,17 +83,6 @@ function primal_feasibility_report( return violated_constraints end -function primal_feasibility_report(model::Model; kwargs...) - if !has_values(model) - error( - "No primal solution is available. You must provide a point at " * - "which to check feasibility." - ) - end - point = Dict(v => value(v) for v in all_variables(model)) - return primal_feasibility_report(model, point; kwargs...) -end - function _add_infeasible_constraints( model::Model, ::Type{F}, From da9b58e165c88fd0b12037524f77e0ef2f5b5767 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 26 Feb 2021 20:49:13 +1300 Subject: [PATCH 18/22] Update solutions.md --- docs/src/manual/solutions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index 70bd44d2962..97bdc6dcc64 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -495,12 +495,12 @@ end 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), -a tolerance `atol` (defaults to `0.0`). +and a tolerance `atol` (defaults to `0.0`). The function returns a dictionary which maps the infeasible constraint -references to the distance between the point and the nearest point in the -corresponding set. A point is classed as infeasible if the distance is greater -than the supplied tolerance `atol`. +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 julia> model = Model(GLPK.Optimizer); From b9b995d68e05e4cf8e30f0f58eb47b5d96bbde83 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 19 Mar 2021 08:56:45 +1300 Subject: [PATCH 19/22] Clarify docs --- docs/src/manual/solutions.md | 4 ++-- src/feasibility_checker.jl | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index 97bdc6dcc64..4edeaba4794 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -498,8 +498,8 @@ 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 +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 diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index 3beddeda2e4..4124f50a2f4 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -32,6 +32,8 @@ greater than `atol`. * 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. From 156f2c1943eb3d50a48590dc85c05bd3525c45d5 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 22 Mar 2021 08:37:18 +1300 Subject: [PATCH 20/22] Fix doctest and formatting --- docs/src/manual/solutions.md | 2 +- src/feasibility_checker.jl | 53 ++++++++++++++---------------------- test/feasibility_checker.jl | 11 +++----- 3 files changed, 25 insertions(+), 41 deletions(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index 4edeaba4794..f6717d21c2f 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -502,7 +502,7 @@ 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 +```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); diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index 4124f50a2f4..29e8e8952d8 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -9,7 +9,7 @@ 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." + "which to check feasibility.", ) end return Dict(v => value(v) for v in all_variables(model)) @@ -68,7 +68,12 @@ function primal_feasibility_report( 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 + model, + F, + S, + violated_constraints, + point_f, + atol, ) end if num_nl_constraints(model) > 0 @@ -79,7 +84,10 @@ function primal_feasibility_report( ) end _add_infeasible_nonlinear_constraints( - model, violated_constraints, point_f, atol + model, + violated_constraints, + point_f, + atol, ) end return violated_constraints @@ -116,11 +124,8 @@ function _add_infeasible_nonlinear_constraints( 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(), - ) + cref = + ConstraintRef(model, NonlinearConstraintIndex(i), ScalarShape()) violated_constraints[cref] = d end end @@ -128,9 +133,9 @@ function _add_infeasible_nonlinear_constraints( end function _distance_to_set(::Any, set::MOI.AbstractSet) - error( + return error( "Feasibility checker for set type $(typeof(set)) has not been " * - "implemented yet." + "implemented yet.", ) end @@ -169,11 +174,7 @@ function _distance_to_set(x::T, set::MOI.Semicontinuous{T}) where {T<:Real} 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)), - ) + d = max(ceil(set.lower) - x, x - floor(set.upper), abs(x - round(x))) return min(d, abs(x)) end @@ -188,36 +189,22 @@ function _check_dimension(v::AbstractVector, s) return end -function _distance_to_set( - x::Vector{T}, - set::MOI.Nonnegatives, -) where {T<:Real} +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} +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} +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} +function _distance_to_set(x::Vector{T}, set::MOI.Reals) where {T<:Real} _check_dimension(x, set) return zero(T) end - diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index 888580ea13c..cbe473efd5e 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -117,14 +117,14 @@ function test_primal_solution() @test isempty(report) end - function test_feasible() model = Model() @variable(model, x, Bin) @variable(model, 0 <= y <= 2, Int) @variable(model, z == 0.5) @constraint(model, x + y + z >= 0.5) - report = primal_feasibility_report(model, Dict(x => 0.0, y => 0.0, z => 0.5)) + report = + primal_feasibility_report(model, Dict(x => 0.0, y => 0.0, z => 0.5)) @test isempty(report) end @@ -134,11 +134,8 @@ function test_missing() @variable(model, 0 <= y <= 2, Int) @variable(model, z == 0.5) @constraint(model, x + y + z >= 0.5) - report = primal_feasibility_report( - model, - Dict(z => 0.0), - skip_missing = true, - ) + report = + primal_feasibility_report(model, Dict(z => 0.0), skip_missing = true) @test report[FixRef(z)] == 0.5 @test length(report) == 1 end From 54d3f340e53219a594f883bc8d189aa83cf33acd Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 22 Mar 2021 12:19:01 +1300 Subject: [PATCH 21/22] Update feasibility_checker.jl --- src/feasibility_checker.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/feasibility_checker.jl b/src/feasibility_checker.jl index 29e8e8952d8..b99fb1a3acd 100644 --- a/src/feasibility_checker.jl +++ b/src/feasibility_checker.jl @@ -24,9 +24,11 @@ end )::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`. +dictionary whose keys are the constraints with an infeasibility greater than the +supplied tolerance `atol`. The value corresponding to each key is the respective +infeasibilility. Infeasibility is defined as the distance between the primal +value of the constraint (see `MOI.ConstraintPrimal`) and the nearest point by +Euclidean distance in the corresponding set. ## Notes From ffedca7cfd3590ac789977428cd1979dd38b8549 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 22 Mar 2021 13:40:33 +1300 Subject: [PATCH 22/22] Revise filter --- docs/src/manual/solutions.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/src/manual/solutions.md b/docs/src/manual/solutions.md index f6717d21c2f..8010c84b017 100644 --- a/docs/src/manual/solutions.md +++ b/docs/src/manual/solutions.md @@ -502,7 +502,11 @@ 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"] +```@meta +# Add a filter here because the output of the dictionary is not ordered, and +# changes in printing order will cause the doctest to fail. +``` +```jldoctest feasibility; filter=[r"x.+?\=\> 0.1", r"c1.+? \=\> 0.01"] julia> model = Model(GLPK.Optimizer); julia> @variable(model, x >= 1, Int);