From a66c5467f5ec48b03dd898ee29438a6affd6d71d Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 12 Sep 2022 17:08:58 +1200 Subject: [PATCH 01/20] [Utilities] add FeasibilityRelaxation --- src/Utilities/Utilities.jl | 2 +- src/Utilities/feasibility_relaxation.jl | 176 ++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/Utilities/feasibility_relaxation.jl diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index 03e2b23fd3..59457111dc 100644 --- a/src/Utilities/Utilities.jl +++ b/src/Utilities/Utilities.jl @@ -77,7 +77,7 @@ include("mockoptimizer.jl") include("cachingoptimizer.jl") include("universalfallback.jl") include("print.jl") - +include("feasibility_relaxation.jl") include("lazy_iterators.jl") include("precompile.jl") diff --git a/src/Utilities/feasibility_relaxation.jl b/src/Utilities/feasibility_relaxation.jl new file mode 100644 index 0000000000..ae04e5c5c8 --- /dev/null +++ b/src/Utilities/feasibility_relaxation.jl @@ -0,0 +1,176 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +""" + FeasibilityRelaxation( + penalties = Dict{MOI.ConstraintIndex,Float64}(), + ) <: MOI.AbstractModelAttribute + +A model attribute that, when set, destructively modifies the model in-place to +create a feasibility relxation. + +!!! warning + This is a destructive routine that modifies the model in-place. If you don't + want to modify the original model, use `copy_model` to create a copy before + setting this attribute. + +## Reformulation + +The feasibility relaxation modifies constraints of the form ``f(x) \\in S`` into +``f(x) + y - z \\in S``, where ``y, z \\ge 0``, and then it introduces a penalty +term into the objective of ``a \\times (y + z)`` (if minimizing, else ``-a``), +where `a` is the value in the `penalties` dictionary associated with the +constraint that is being relaxed. If no value exists, the default is `1.0`. + +The feasibility relaxation is limited to modifying constraint types for which +`MOI.supports(model, ::FeasibilityRelaxation, MOI.ConstraintIndex{F,S})` is +`true`. By default, this is only true for [`MOI.ScalarAffineFunction`](@ref) and +[`MOI.MOI.ScalarQuadraticFunction`](@ref) constraints in the linear sets +[`MOI.LessThan`](@ref), [`MOI.GreaterThan`](@ref), [`MOI.EqualTo`](@ref) and +[`MOI.Interval`](@ref). It does not include variable bound or integrality +constraints, because these cannot be modified in-place. + +## Example + +```jldoctest +julia> model = MOI.Utilities.Model{Float64}(); + +julia> x = MOI.add_variable(model); + +julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); + +julia> MOI.set(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) + +julia> print(model) +Minimize ScalarAffineFunction{Float64}: + 0.0 + 2.0 v[2] + +Subject to: + +ScalarAffineFunction{Float64}-in-LessThan{Float64} + 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 + +VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 +``` +""" +mutable struct FeasibilityRelaxation{T} <: MOI.AbstractModelAttribute + penalties::Dict{MOI.ConstraintIndex,T} + scale::T + function FeasibilityRelaxation(p::Dict{MOI.ConstraintIndex,T}) where {T} + return new{T}(p, zero(T)) + end +end + +function FeasibilityRelaxation() + return FeasibilityRelaxation(Dict{MOI.ConstraintIndex,Float64}()) +end + +function FeasibilityRelaxation(d::Dict{<:MOI.ConstraintIndex,T}) where {T} + return FeasibilityRelaxation(convert(Dict{MOI.ConstraintIndex,T}, d)) +end + +function MOI.set( + model::MOI.ModelLike, + relax::FeasibilityRelaxation{T}, +) where {T} + sense = MOI.get(model, MOI.ObjectiveSense()) + if sense == MOI.FEASIBILITY_SENSE + relax.scale = one(T) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + f = zero(MOI.ScalarAffineFunction{T}) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + elseif sense == MOI.MIN_SENSE + relax.scale = one(T) + elseif sense == MOI.MAX_SENSE + relax.scale = -one(T) + end + for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) + MOI.set(model, relax, F, S) + end + return +end + +function MOI.set( + model::MOI.ModelLike, + relax::FeasibilityRelaxation, + ::Type{F}, + ::Type{S}, +) where {F,S} + if MOI.supports(model, relax, MOI.ConstraintIndex{F,S}) + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + MOI.set(model, relax, ci) + end + end + return +end + +function MOI.supports( + ::MOI.ModelLike, + ::FeasibilityRelaxation, + ::Type{MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + return true +end + +function MOI.supports_fallback( + ::MOI.ModelLike, + ::FeasibilityRelaxation, + ::Type{MOI.ConstraintIndex{F,S}}, +) where {F,S} + return false +end + +function MOI.set( + model::MOI.ModelLike, + relax::FeasibilityRelaxation, + ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + y = MOI.add_variable(model) + z = MOI.add_variable(model) + MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) + MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) + a = relax.scale * get(relax.penalties, ci, one(T)) + O = MOI.get(model, MOI.ObjectiveFunctionType()) + obj = MOI.ObjectiveFunction{O}() + MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) + MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) + return +end + +function MOI.set( + model::MOI.ModelLike, + relax::FeasibilityRelaxation, + ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + # Performance optimization: we don't need the z relaxation variable. + y = MOI.add_variable(model) + MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) + a = relax.scale * get(relax.penalties, ci, one(T)) + O = MOI.get(model, MOI.ObjectiveFunctionType()) + obj = MOI.ObjectiveFunction{O}() + MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) + return +end + +function MOI.set( + model::MOI.ModelLike, + relax::FeasibilityRelaxation, + ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + # Performance optimization: we don't need the y relaxation variable. + z = MOI.add_variable(model) + MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) + a = relax.scale * get(relax.penalties, ci, one(T)) + O = MOI.get(model, MOI.ObjectiveFunctionType()) + obj = MOI.ObjectiveFunction{O}() + MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) + return +end From 12e970d6036aae9305b7f5942c0de81e2ea7c27e Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 13 Sep 2022 11:22:55 +1200 Subject: [PATCH 02/20] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: BenoƮt Legat --- src/Utilities/feasibility_relaxation.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Utilities/feasibility_relaxation.jl b/src/Utilities/feasibility_relaxation.jl index ae04e5c5c8..dee59037f8 100644 --- a/src/Utilities/feasibility_relaxation.jl +++ b/src/Utilities/feasibility_relaxation.jl @@ -10,7 +10,7 @@ ) <: MOI.AbstractModelAttribute A model attribute that, when set, destructively modifies the model in-place to -create a feasibility relxation. +create a feasibility relaxation. !!! warning This is a destructive routine that modifies the model in-place. If you don't @@ -35,7 +35,7 @@ constraints, because these cannot be modified in-place. ## Example -```jldoctest +```jldoctest; setup=:(import MathOptInterface; const MOI = MathOptInterface) julia> model = MOI.Utilities.Model{Float64}(); julia> x = MOI.add_variable(model); From b963d18831d6b8a764dac75eb443b538cce8608b Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 13 Sep 2022 12:34:19 +1200 Subject: [PATCH 03/20] Add tests --- src/Utilities/feasibility_relaxation.jl | 2 +- test/Utilities/feasibility_relaxation.jl | 234 +++++++++++++++++++++++ 2 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 test/Utilities/feasibility_relaxation.jl diff --git a/src/Utilities/feasibility_relaxation.jl b/src/Utilities/feasibility_relaxation.jl index dee59037f8..42f1a42260 100644 --- a/src/Utilities/feasibility_relaxation.jl +++ b/src/Utilities/feasibility_relaxation.jl @@ -111,7 +111,7 @@ end function MOI.supports( ::MOI.ModelLike, ::FeasibilityRelaxation, - ::Type{MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}}, + ::Type{<:MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} return true end diff --git a/test/Utilities/feasibility_relaxation.jl b/test/Utilities/feasibility_relaxation.jl new file mode 100644 index 0000000000..e966a5bca7 --- /dev/null +++ b/test/Utilities/feasibility_relaxation.jl @@ -0,0 +1,234 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module TestFeasibilityRelaxation + +using Test +using MathOptInterface + +const MOI = MathOptInterface + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function _test_roundtrip(src_str, relaxed_str) + model = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!(model, src_str) + MOI.set(model, MOI.Utilities.FeasibilityRelaxation()) + dest = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!(dest, relaxed_str) + MOI.Bridges._test_structural_identical(model, dest) + return +end + +function test_relax_bounds() + _test_roundtrip( + """ + variables: x, y + minobjective: x + y + x >= 0.0 + y <= 0.0 + x in ZeroOne() + y in Integer() + """, + """ + variables: x, y + minobjective: x + y + x >= 0.0 + y <= 0.0 + x in ZeroOne() + y in Integer() + """, + ) + return +end + +function test_relax_affine_lessthan() + _test_roundtrip( + """ + variables: x, y + minobjective: x + y + c1: x + y <= 1.0 + """, + """ + variables: x, y, a + minobjective: x + y + a + c1: x + y + -1.0 * a <= 1.0 + a >= 0.0 + """, + ) + return +end + +function test_relax_affine_lessthan_max() + _test_roundtrip( + """ + variables: x, y + maxobjective: x + y + c1: x + y <= 1.0 + """, + """ + variables: x, y, a + maxobjective: x + y + -1.0 * a + c1: x + y + -1.0 * a <= 1.0 + a >= 0.0 + """, + ) + return +end + +function test_relax_affine_lessthan_no_objective() + _test_roundtrip( + """ + variables: x, y + c1: x + y <= 1.0 + """, + """ + variables: x, y, a + minobjective: 1.0 * a + c1: x + y + -1.0 * a <= 1.0 + a >= 0.0 + """, + ) + return +end + +function test_relax_affine_lessthan_quad_objective() + _test_roundtrip( + """ + variables: x, y + maxobjective: 1.0 * x * y + c1: x + y <= 1.0 + """, + """ + variables: x, y, a + maxobjective: 1.0 * x * y + -1.0 * a + c1: x + y + -1.0 * a <= 1.0 + a >= 0.0 + """, + ) + return +end + +function test_relax_affine_greaterthan() + _test_roundtrip( + """ + variables: x, y + minobjective: x + y + c1: x + y >= 1.0 + """, + """ + variables: x, y, a + minobjective: x + y + a + c1: x + y + 1.0 * a >= 1.0 + a >= 0.0 + """, + ) + return +end + +function test_relax_affine_equalto() + _test_roundtrip( + """ + variables: x, y + minobjective: x + y + c1: x + y == 1.0 + """, + """ + variables: x, y, a, b + minobjective: x + y + a + b + c1: x + y + 1.0 * a + -1.0 * b == 1.0 + a >= 0.0 + b >= 0.0 + """, + ) + return +end + +function test_relax_affine_interval() + _test_roundtrip( + """ + variables: x, y + minobjective: x + y + c1: x + y in Interval(5.0, 6.0) + """, + """ + variables: x, y, a, b + minobjective: x + y + a + b + c1: x + y + 1.0 * a + -1.0 * b in Interval(5.0, 6.0) + a >= 0.0 + b >= 0.0 + """, + ) + return +end + +function test_relax_quadratic_lessthan() + _test_roundtrip( + """ + variables: x, y + maxobjective: x + y + c1: 1.0 * x * x + 2.0 * x * y <= 1.0 + """, + """ + variables: x, y, a + maxobjective: x + y + -1.0 * a + c1: 1.0 * x * x + 2.0 * x * y + -1.0 * a <= 1.0 + a >= 0.0 + """, + ) + return +end + +function test_relax_quadratic_greaterthanthan() + _test_roundtrip( + """ + variables: x, y + maxobjective: x + y + c1: 1.0 * x * x + 2.0 * x * y >= 1.0 + """, + """ + variables: x, y, a + maxobjective: x + y + -1.0 * a + c1: 1.0 * x * x + 2.0 * x * y + 1.0 * a >= 1.0 + a >= 0.0 + """, + ) + return +end + +function test_penalties() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + c = MOI.add_constraint(model, 1.0 * x, MOI.EqualTo(2.0)) + MOI.set(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) + @test sprint(print, model) === """ + Minimize ScalarAffineFunction{Float64}: + 0.0 + 2.0 v[2] + 2.0 v[3] + + Subject to: + + ScalarAffineFunction{Float64}-in-EqualTo{Float64} + 0.0 + 1.0 v[1] + 1.0 v[2] - 1.0 v[3] == 2.0 + + VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 + v[3] >= 0.0 + """ + return +end + +end + +TestFeasibilityRelaxation.runtests() From f006454e279e9708c1f55acd154c4c816b96f95e Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 14 Sep 2022 10:46:30 +1200 Subject: [PATCH 04/20] Add docs --- docs/src/submodules/Utilities/overview.md | 30 ++++++++++++++++++++++ docs/src/submodules/Utilities/reference.md | 6 +++++ 2 files changed, 36 insertions(+) diff --git a/docs/src/submodules/Utilities/overview.md b/docs/src/submodules/Utilities/overview.md index 8a47059ea8..b25e55cfc6 100644 --- a/docs/src/submodules/Utilities/overview.md +++ b/docs/src/submodules/Utilities/overview.md @@ -315,6 +315,36 @@ $$ \begin{aligned} In IJulia, calling `print` or ending a cell with [`Utilities.latex_formulation`](@ref) will render the model in LaTeX. +## Utilities.FeasibilityRelaxation + +Set the [`Utilities.FeasibilityRelaxation`](@ref) attribute to relax the +problem by adding penalized slack variables to the constraints. This is helpful +when debugging sources of infeasible models. + +```jldoctest +julia> model = MOI.Utilities.Model{Float64}(); + +julia> x = MOI.add_variable(model); + +julia> MOI.set(model, MOI.VariableName(), x, "x") + +julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); + +julia> MOI.set(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) + +julia> print(model) +Minimize ScalarAffineFunction{Float64}: + 0.0 + 2.0 v[2] + +Subject to: + +ScalarAffineFunction{Float64}-in-LessThan{Float64} + 0.0 + 1.0 x - 1.0 v[2] <= 2.0 + +VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 +``` + ## Utilities.MatrixOfConstraints The constraints of [`Utilities.Model`](@ref) are stored as a vector of tuples diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index b585a2f38c..8bebd57735 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -90,6 +90,12 @@ Utilities.identity_index_map Utilities.ModelFilter ``` +## Feasibility relaxation + +```@docs +Utilities.FeasibilityRelaxation +``` + ## MatrixOfConstraints ```@docs From 95012eb9027a671331572ffe90a6b3ddaeeb31d8 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 14 Sep 2022 11:02:59 +1200 Subject: [PATCH 05/20] Update src/Utilities/feasibility_relaxation.jl --- src/Utilities/feasibility_relaxation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utilities/feasibility_relaxation.jl b/src/Utilities/feasibility_relaxation.jl index 42f1a42260..f946a1871c 100644 --- a/src/Utilities/feasibility_relaxation.jl +++ b/src/Utilities/feasibility_relaxation.jl @@ -28,7 +28,7 @@ constraint that is being relaxed. If no value exists, the default is `1.0`. The feasibility relaxation is limited to modifying constraint types for which `MOI.supports(model, ::FeasibilityRelaxation, MOI.ConstraintIndex{F,S})` is `true`. By default, this is only true for [`MOI.ScalarAffineFunction`](@ref) and -[`MOI.MOI.ScalarQuadraticFunction`](@ref) constraints in the linear sets +[`MOI.ScalarQuadraticFunction`](@ref) constraints in the linear sets [`MOI.LessThan`](@ref), [`MOI.GreaterThan`](@ref), [`MOI.EqualTo`](@ref) and [`MOI.Interval`](@ref). It does not include variable bound or integrality constraints, because these cannot be modified in-place. From 405692cc7bd35fe0915b8403ca0b7ebfa746b082 Mon Sep 17 00:00:00 2001 From: odow Date: Sun, 9 Oct 2022 11:33:59 +1300 Subject: [PATCH 06/20] Switch to MOI.modify --- docs/src/submodules/Utilities/overview.md | 2 +- src/Utilities/feasibility_relaxation.jl | 73 +++++++++++------------ test/Utilities/feasibility_relaxation.jl | 4 +- 3 files changed, 37 insertions(+), 42 deletions(-) diff --git a/docs/src/submodules/Utilities/overview.md b/docs/src/submodules/Utilities/overview.md index b25e55cfc6..69995cc90a 100644 --- a/docs/src/submodules/Utilities/overview.md +++ b/docs/src/submodules/Utilities/overview.md @@ -330,7 +330,7 @@ julia> MOI.set(model, MOI.VariableName(), x, "x") julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); -julia> MOI.set(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) +julia> MOI.modify(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) julia> print(model) Minimize ScalarAffineFunction{Float64}: diff --git a/src/Utilities/feasibility_relaxation.jl b/src/Utilities/feasibility_relaxation.jl index f946a1871c..21c09ef9e7 100644 --- a/src/Utilities/feasibility_relaxation.jl +++ b/src/Utilities/feasibility_relaxation.jl @@ -7,15 +7,15 @@ """ FeasibilityRelaxation( penalties = Dict{MOI.ConstraintIndex,Float64}(), - ) <: MOI.AbstractModelAttribute + ) <: MOI.AbstractFunctionModification -A model attribute that, when set, destructively modifies the model in-place to -create a feasibility relaxation. +A problem modifier that, when passed to [`MOI.modify`](@ref), destructively +modifies the model in-place to create a feasibility relaxation. !!! warning This is a destructive routine that modifies the model in-place. If you don't - want to modify the original model, use `copy_model` to create a copy before - setting this attribute. + want to modify the original model, use `JuMP.copy_model` to create a copy + before setting this attribute. ## Reformulation @@ -25,13 +25,18 @@ term into the objective of ``a \\times (y + z)`` (if minimizing, else ``-a``), where `a` is the value in the `penalties` dictionary associated with the constraint that is being relaxed. If no value exists, the default is `1.0`. -The feasibility relaxation is limited to modifying constraint types for which -`MOI.supports(model, ::FeasibilityRelaxation, MOI.ConstraintIndex{F,S})` is -`true`. By default, this is only true for [`MOI.ScalarAffineFunction`](@ref) and -[`MOI.ScalarQuadraticFunction`](@ref) constraints in the linear sets -[`MOI.LessThan`](@ref), [`MOI.GreaterThan`](@ref), [`MOI.EqualTo`](@ref) and -[`MOI.Interval`](@ref). It does not include variable bound or integrality -constraints, because these cannot be modified in-place. +When `S` is [`MOI.LessThan`](@ref) or [`MOI.GreaterThan`](@ref), we omit `y` or +`z` respectively as a performance optimization. + +## Supported constraint types + +The feasibility relaxation is currently limited to modifying +[`MOI.ScalarAffineFunction`](@ref) and [`MOI.ScalarQuadraticFunction`](@ref) +constraints in the linear sets [`MOI.LessThan`](@ref), [`MOI.GreaterThan`](@ref), +[`MOI.EqualTo`](@ref) and [`MOI.Interval`](@ref). + +It does not include variable bound or integrality constraints, because these +cannot be modified in-place. ## Example @@ -42,7 +47,7 @@ julia> x = MOI.add_variable(model); julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); -julia> MOI.set(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) +julia> MOI.modify(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) julia> print(model) Minimize ScalarAffineFunction{Float64}: @@ -57,7 +62,7 @@ VariableIndex-in-GreaterThan{Float64} v[2] >= 0.0 ``` """ -mutable struct FeasibilityRelaxation{T} <: MOI.AbstractModelAttribute +mutable struct FeasibilityRelaxation{T} <: MOI.AbstractFunctionModification penalties::Dict{MOI.ConstraintIndex,T} scale::T function FeasibilityRelaxation(p::Dict{MOI.ConstraintIndex,T}) where {T} @@ -73,7 +78,7 @@ function FeasibilityRelaxation(d::Dict{<:MOI.ConstraintIndex,T}) where {T} return FeasibilityRelaxation(convert(Dict{MOI.ConstraintIndex,T}, d)) end -function MOI.set( +function MOI.modify( model::MOI.ModelLike, relax::FeasibilityRelaxation{T}, ) where {T} @@ -89,45 +94,35 @@ function MOI.set( relax.scale = -one(T) end for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) - MOI.set(model, relax, F, S) + _modify_feasibility_relaxation(model, relax, F, S) end return end -function MOI.set( +function _modify_feasibility_relaxation( model::MOI.ModelLike, relax::FeasibilityRelaxation, ::Type{F}, ::Type{S}, ) where {F,S} - if MOI.supports(model, relax, MOI.ConstraintIndex{F,S}) - for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - MOI.set(model, relax, ci) - end + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + MOI.modify(model, ci, relax) end return end -function MOI.supports( +function MOI.modify( ::MOI.ModelLike, + ::MOI.ConstraintIndex, ::FeasibilityRelaxation, - ::Type{<:MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}}, -) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - return true +) + return # Silently skip modifying other constraint types. end -function MOI.supports_fallback( - ::MOI.ModelLike, - ::FeasibilityRelaxation, - ::Type{MOI.ConstraintIndex{F,S}}, -) where {F,S} - return false -end - -function MOI.set( +function MOI.modify( model::MOI.ModelLike, - relax::FeasibilityRelaxation, ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, + relax::FeasibilityRelaxation, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} y = MOI.add_variable(model) z = MOI.add_variable(model) @@ -143,10 +138,10 @@ function MOI.set( return end -function MOI.set( +function MOI.modify( model::MOI.ModelLike, - relax::FeasibilityRelaxation, ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, + relax::FeasibilityRelaxation, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} # Performance optimization: we don't need the z relaxation variable. y = MOI.add_variable(model) @@ -159,10 +154,10 @@ function MOI.set( return end -function MOI.set( +function MOI.modify( model::MOI.ModelLike, - relax::FeasibilityRelaxation, ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, + relax::FeasibilityRelaxation, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} # Performance optimization: we don't need the y relaxation variable. z = MOI.add_variable(model) diff --git a/test/Utilities/feasibility_relaxation.jl b/test/Utilities/feasibility_relaxation.jl index e966a5bca7..d40bcb7a16 100644 --- a/test/Utilities/feasibility_relaxation.jl +++ b/test/Utilities/feasibility_relaxation.jl @@ -25,7 +25,7 @@ end function _test_roundtrip(src_str, relaxed_str) model = MOI.Utilities.Model{Float64}() MOI.Utilities.loadfromstring!(model, src_str) - MOI.set(model, MOI.Utilities.FeasibilityRelaxation()) + MOI.modify(model, MOI.Utilities.FeasibilityRelaxation()) dest = MOI.Utilities.Model{Float64}() MOI.Utilities.loadfromstring!(dest, relaxed_str) MOI.Bridges._test_structural_identical(model, dest) @@ -212,7 +212,7 @@ function test_penalties() model = MOI.Utilities.Model{Float64}() x = MOI.add_variable(model) c = MOI.add_constraint(model, 1.0 * x, MOI.EqualTo(2.0)) - MOI.set(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) + MOI.modify(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) @test sprint(print, model) === """ Minimize ScalarAffineFunction{Float64}: 0.0 + 2.0 v[2] + 2.0 v[3] From c8719ee77b49b9f62695ebdc8df316b13bd8904d Mon Sep 17 00:00:00 2001 From: odow Date: Sun, 9 Oct 2022 12:09:54 +1300 Subject: [PATCH 07/20] Fix --- src/Utilities/feasibility_relaxation.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Utilities/feasibility_relaxation.jl b/src/Utilities/feasibility_relaxation.jl index 21c09ef9e7..7036b98f53 100644 --- a/src/Utilities/feasibility_relaxation.jl +++ b/src/Utilities/feasibility_relaxation.jl @@ -7,7 +7,7 @@ """ FeasibilityRelaxation( penalties = Dict{MOI.ConstraintIndex,Float64}(), - ) <: MOI.AbstractFunctionModification + ) A problem modifier that, when passed to [`MOI.modify`](@ref), destructively modifies the model in-place to create a feasibility relaxation. @@ -62,7 +62,7 @@ VariableIndex-in-GreaterThan{Float64} v[2] >= 0.0 ``` """ -mutable struct FeasibilityRelaxation{T} <: MOI.AbstractFunctionModification +mutable struct FeasibilityRelaxation{T} penalties::Dict{MOI.ConstraintIndex,T} scale::T function FeasibilityRelaxation(p::Dict{MOI.ConstraintIndex,T}) where {T} @@ -122,7 +122,7 @@ end function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, - relax::FeasibilityRelaxation, + relax::FeasibilityRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} y = MOI.add_variable(model) z = MOI.add_variable(model) @@ -141,7 +141,7 @@ end function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, - relax::FeasibilityRelaxation, + relax::FeasibilityRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} # Performance optimization: we don't need the z relaxation variable. y = MOI.add_variable(model) @@ -157,7 +157,7 @@ end function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, - relax::FeasibilityRelaxation, + relax::FeasibilityRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} # Performance optimization: we don't need the y relaxation variable. z = MOI.add_variable(model) From c9cd47569e0d28b4db7b6d5734aaaaff4e62e855 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sun, 9 Oct 2022 15:59:55 +1300 Subject: [PATCH 08/20] Update overview.md --- docs/src/submodules/Utilities/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/submodules/Utilities/overview.md b/docs/src/submodules/Utilities/overview.md index 69995cc90a..ec583ebc37 100644 --- a/docs/src/submodules/Utilities/overview.md +++ b/docs/src/submodules/Utilities/overview.md @@ -317,7 +317,7 @@ $$ \begin{aligned} ## Utilities.FeasibilityRelaxation -Set the [`Utilities.FeasibilityRelaxation`](@ref) attribute to relax the +Pass [`Utilities.FeasibilityRelaxation`](@ref) to [`modify`](@ref) to relax the problem by adding penalized slack variables to the constraints. This is helpful when debugging sources of infeasible models. From 5a4a06e58aaeb3ae8cdb1bf2a8054f7005f39af5 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 28 Oct 2022 10:22:50 +1300 Subject: [PATCH 09/20] Rename to PenaltyRelaxation --- docs/src/submodules/Utilities/overview.md | 6 +- docs/src/submodules/Utilities/reference.md | 2 +- src/Utilities/Utilities.jl | 2 +- ...ty_relaxation.jl => penalty_relaxation.jl} | 106 ++++++++++++------ ...ty_relaxation.jl => penalty_relaxation.jl} | 8 +- 5 files changed, 82 insertions(+), 42 deletions(-) rename src/Utilities/{feasibility_relaxation.jl => penalty_relaxation.jl} (63%) rename test/Utilities/{feasibility_relaxation.jl => penalty_relaxation.jl} (95%) diff --git a/docs/src/submodules/Utilities/overview.md b/docs/src/submodules/Utilities/overview.md index ec583ebc37..a5f88e4f10 100644 --- a/docs/src/submodules/Utilities/overview.md +++ b/docs/src/submodules/Utilities/overview.md @@ -315,9 +315,9 @@ $$ \begin{aligned} In IJulia, calling `print` or ending a cell with [`Utilities.latex_formulation`](@ref) will render the model in LaTeX. -## Utilities.FeasibilityRelaxation +## Utilities.PenaltyRelaxation -Pass [`Utilities.FeasibilityRelaxation`](@ref) to [`modify`](@ref) to relax the +Pass [`Utilities.PenaltyRelaxation`](@ref) to [`modify`](@ref) to relax the problem by adding penalized slack variables to the constraints. This is helpful when debugging sources of infeasible models. @@ -330,7 +330,7 @@ julia> MOI.set(model, MOI.VariableName(), x, "x") julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); -julia> MOI.modify(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) +julia> MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 2.0))) julia> print(model) Minimize ScalarAffineFunction{Float64}: diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index 8bebd57735..33b5271a87 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -93,7 +93,7 @@ Utilities.ModelFilter ## Feasibility relaxation ```@docs -Utilities.FeasibilityRelaxation +Utilities.PenaltyRelaxation ``` ## MatrixOfConstraints diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index 59457111dc..cef6386531 100644 --- a/src/Utilities/Utilities.jl +++ b/src/Utilities/Utilities.jl @@ -77,7 +77,7 @@ include("mockoptimizer.jl") include("cachingoptimizer.jl") include("universalfallback.jl") include("print.jl") -include("feasibility_relaxation.jl") +include("penalty_relaxation.jl") include("lazy_iterators.jl") include("precompile.jl") diff --git a/src/Utilities/feasibility_relaxation.jl b/src/Utilities/penalty_relaxation.jl similarity index 63% rename from src/Utilities/feasibility_relaxation.jl rename to src/Utilities/penalty_relaxation.jl index 7036b98f53..d98ebd6ad4 100644 --- a/src/Utilities/feasibility_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -5,12 +5,13 @@ # in the LICENSE.md file or at https://opensource.org/licenses/MIT. """ - FeasibilityRelaxation( - penalties = Dict{MOI.ConstraintIndex,Float64}(), + PenaltyRelaxation( + penalties = Dict{MOI.ConstraintIndex,Float64}(); + default::Union{Nothing,T} = 1.0, ) A problem modifier that, when passed to [`MOI.modify`](@ref), destructively -modifies the model in-place to create a feasibility relaxation. +modifies the model in-place to create a penalized relaxation of the constraints. !!! warning This is a destructive routine that modifies the model in-place. If you don't @@ -19,18 +20,23 @@ modifies the model in-place to create a feasibility relaxation. ## Reformulation -The feasibility relaxation modifies constraints of the form ``f(x) \\in S`` into +The penalty relaxation modifies constraints of the form ``f(x) \\in S`` into ``f(x) + y - z \\in S``, where ``y, z \\ge 0``, and then it introduces a penalty term into the objective of ``a \\times (y + z)`` (if minimizing, else ``-a``), where `a` is the value in the `penalties` dictionary associated with the -constraint that is being relaxed. If no value exists, the default is `1.0`. +constraint that is being relaxed. If no value exists, the default is `default`. When `S` is [`MOI.LessThan`](@ref) or [`MOI.GreaterThan`](@ref), we omit `y` or `z` respectively as a performance optimization. +## Relax a subset of constraints + +To relax a subset of constraints, pass a `penalties` dictionary` and set +`default = nothing`. + ## Supported constraint types -The feasibility relaxation is currently limited to modifying +The penalty relaxation is currently limited to modifying [`MOI.ScalarAffineFunction`](@ref) and [`MOI.ScalarQuadraticFunction`](@ref) constraints in the linear sets [`MOI.LessThan`](@ref), [`MOI.GreaterThan`](@ref), [`MOI.EqualTo`](@ref) and [`MOI.Interval`](@ref). @@ -38,7 +44,9 @@ constraints in the linear sets [`MOI.LessThan`](@ref), [`MOI.GreaterThan`](@ref) It does not include variable bound or integrality constraints, because these cannot be modified in-place. -## Example +To modify variable bounds, rewrite them as linear constraints. + +## Examples ```jldoctest; setup=:(import MathOptInterface; const MOI = MathOptInterface) julia> model = MOI.Utilities.Model{Float64}(); @@ -47,7 +55,7 @@ julia> x = MOI.add_variable(model); julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); -julia> MOI.modify(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) +julia> MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = 2.0)) julia> print(model) Minimize ScalarAffineFunction{Float64}: @@ -55,6 +63,28 @@ Minimize ScalarAffineFunction{Float64}: Subject to: +ScalarAffineFunction{Float64}-in-LessThan{Float64} + 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 + +VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 +``` + +```jldoctest; setup=:(import MathOptInterface; const MOI = MathOptInterface) +julia> model = MOI.Utilities.Model{Float64}(); + +julia> x = MOI.add_variable(model); + +julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); + +julia> MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 3.0))) + +julia> print(model) +Minimize ScalarAffineFunction{Float64}: + 0.0 + 3.0 v[2] + +Subject to: + ScalarAffineFunction{Float64}-in-LessThan{Float64} 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 @@ -62,51 +92,55 @@ VariableIndex-in-GreaterThan{Float64} v[2] >= 0.0 ``` """ -mutable struct FeasibilityRelaxation{T} +mutable struct PenaltyRelaxation{T} + default::Union{Nothing,T} penalties::Dict{MOI.ConstraintIndex,T} - scale::T - function FeasibilityRelaxation(p::Dict{MOI.ConstraintIndex,T}) where {T} - return new{T}(p, zero(T)) + + function PenaltyRelaxation( + p::Dict{MOI.ConstraintIndex,T}; + default::T = one(T), + ) where {T} + return new{T}(default, p) end end -function FeasibilityRelaxation() - return FeasibilityRelaxation(Dict{MOI.ConstraintIndex,Float64}()) +function PenaltyRelaxation(; kwargs...) + return PenaltyRelaxation(Dict{MOI.ConstraintIndex,Float64}(); kwargs...) end -function FeasibilityRelaxation(d::Dict{<:MOI.ConstraintIndex,T}) where {T} - return FeasibilityRelaxation(convert(Dict{MOI.ConstraintIndex,T}, d)) +function PenaltyRelaxation( + d::Dict{<:MOI.ConstraintIndex,T}; + kwargs..., +) where {T} + return PenaltyRelaxation(convert(Dict{MOI.ConstraintIndex,T}, d); kwargs...) end function MOI.modify( model::MOI.ModelLike, - relax::FeasibilityRelaxation{T}, + relax::PenaltyRelaxation{T}, ) where {T} sense = MOI.get(model, MOI.ObjectiveSense()) if sense == MOI.FEASIBILITY_SENSE - relax.scale = one(T) MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) f = zero(MOI.ScalarAffineFunction{T}) MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) - elseif sense == MOI.MIN_SENSE - relax.scale = one(T) - elseif sense == MOI.MAX_SENSE - relax.scale = -one(T) end for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) - _modify_feasibility_relaxation(model, relax, F, S) + _modify_penalty_relaxation(model, relax, F, S) end return end -function _modify_feasibility_relaxation( +function _modify_penalty_relaxation( model::MOI.ModelLike, - relax::FeasibilityRelaxation, + relax::PenaltyRelaxation, ::Type{F}, ::Type{S}, ) where {F,S} for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - MOI.modify(model, ci, relax) + if relax.default !== nothing || haskey(relax.penalties, ci) + MOI.modify(model, ci, relax) + end end return end @@ -114,7 +148,7 @@ end function MOI.modify( ::MOI.ModelLike, ::MOI.ConstraintIndex, - ::FeasibilityRelaxation, + ::PenaltyRelaxation, ) return # Silently skip modifying other constraint types. end @@ -122,7 +156,7 @@ end function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, - relax::FeasibilityRelaxation{T}, + relax::PenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} y = MOI.add_variable(model) z = MOI.add_variable(model) @@ -130,7 +164,9 @@ function MOI.modify( MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - a = relax.scale * get(relax.penalties, ci, one(T)) + sense = MOI.get(model, MOI.ObjectiveSense()) + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) + a = scale * get(relax.penalties, ci, relax.default) O = MOI.get(model, MOI.ObjectiveFunctionType()) obj = MOI.ObjectiveFunction{O}() MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) @@ -141,13 +177,15 @@ end function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, - relax::FeasibilityRelaxation{T}, + relax::PenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} # Performance optimization: we don't need the z relaxation variable. y = MOI.add_variable(model) MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) - a = relax.scale * get(relax.penalties, ci, one(T)) + sense = MOI.get(model, MOI.ObjectiveSense()) + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) + a = scale * get(relax.penalties, ci, relax.default) O = MOI.get(model, MOI.ObjectiveFunctionType()) obj = MOI.ObjectiveFunction{O}() MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) @@ -157,13 +195,15 @@ end function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, - relax::FeasibilityRelaxation{T}, + relax::PenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} # Performance optimization: we don't need the y relaxation variable. z = MOI.add_variable(model) MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - a = relax.scale * get(relax.penalties, ci, one(T)) + sense = MOI.get(model, MOI.ObjectiveSense()) + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) + a = scale * get(relax.penalties, ci, relax.default) O = MOI.get(model, MOI.ObjectiveFunctionType()) obj = MOI.ObjectiveFunction{O}() MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) diff --git a/test/Utilities/feasibility_relaxation.jl b/test/Utilities/penalty_relaxation.jl similarity index 95% rename from test/Utilities/feasibility_relaxation.jl rename to test/Utilities/penalty_relaxation.jl index d40bcb7a16..c306d36ef4 100644 --- a/test/Utilities/feasibility_relaxation.jl +++ b/test/Utilities/penalty_relaxation.jl @@ -4,7 +4,7 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. -module TestFeasibilityRelaxation +module TestPenaltyRelaxation using Test using MathOptInterface @@ -25,7 +25,7 @@ end function _test_roundtrip(src_str, relaxed_str) model = MOI.Utilities.Model{Float64}() MOI.Utilities.loadfromstring!(model, src_str) - MOI.modify(model, MOI.Utilities.FeasibilityRelaxation()) + MOI.modify(model, MOI.Utilities.PenaltyRelaxation()) dest = MOI.Utilities.Model{Float64}() MOI.Utilities.loadfromstring!(dest, relaxed_str) MOI.Bridges._test_structural_identical(model, dest) @@ -212,7 +212,7 @@ function test_penalties() model = MOI.Utilities.Model{Float64}() x = MOI.add_variable(model) c = MOI.add_constraint(model, 1.0 * x, MOI.EqualTo(2.0)) - MOI.modify(model, MOI.Utilities.FeasibilityRelaxation(Dict(c => 2.0))) + MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 2.0))) @test sprint(print, model) === """ Minimize ScalarAffineFunction{Float64}: 0.0 + 2.0 v[2] + 2.0 v[3] @@ -231,4 +231,4 @@ end end -TestFeasibilityRelaxation.runtests() +TestPenaltyRelaxation.runtests() From 7c7470e455fce533970df5c1489aa6950fed1583 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 28 Oct 2022 10:34:42 +1300 Subject: [PATCH 10/20] Warn on unsupported types --- src/Utilities/penalty_relaxation.jl | 17 +++++++---- test/Utilities/penalty_relaxation.jl | 44 +++++++++++++++++----------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index d98ebd6ad4..635c3177df 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -115,10 +115,7 @@ function PenaltyRelaxation( return PenaltyRelaxation(convert(Dict{MOI.ConstraintIndex,T}, d); kwargs...) end -function MOI.modify( - model::MOI.ModelLike, - relax::PenaltyRelaxation{T}, -) where {T} +function MOI.modify(model::MOI.ModelLike, relax::PenaltyRelaxation{T}) where {T} sense = MOI.get(model, MOI.ObjectiveSense()) if sense == MOI.FEASIBILITY_SENSE MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) @@ -147,10 +144,18 @@ end function MOI.modify( ::MOI.ModelLike, - ::MOI.ConstraintIndex, + ci::MOI.ConstraintIndex, ::PenaltyRelaxation, ) - return # Silently skip modifying other constraint types. + # We use this fallback to avoid ambiguity errors that would occur if we + # added {F,S} directly to the argument. + _eltype(::MOI.ConstraintIndex{F,S}) where {F,S} = F, S + F, S = _eltype(ci) + @warn( + "Skipping PenaltyRelaxation of constraints of type $F-in-$S", + maxlog = 1, + ) + return end function MOI.modify( diff --git a/test/Utilities/penalty_relaxation.jl b/test/Utilities/penalty_relaxation.jl index c306d36ef4..9b6444c991 100644 --- a/test/Utilities/penalty_relaxation.jl +++ b/test/Utilities/penalty_relaxation.jl @@ -33,24 +33,34 @@ function _test_roundtrip(src_str, relaxed_str) end function test_relax_bounds() - _test_roundtrip( - """ - variables: x, y - minobjective: x + y - x >= 0.0 - y <= 0.0 - x in ZeroOne() - y in Integer() - """, - """ - variables: x, y - minobjective: x + y - x >= 0.0 - y <= 0.0 - x in ZeroOne() - y in Integer() - """, + src_str = """ + variables: x, y + minobjective: x + y + x >= 0.0 + y <= 0.0 + x in ZeroOne() + y in Integer() + """ + relaxed_str = """ + variables: x, y + minobjective: x + y + x >= 0.0 + y <= 0.0 + x in ZeroOne() + y in Integer() + """ + model = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!(model, src_str) + @test_logs( + (:warn,), + (:warn,), + (:warn,), + (:warn,), + MOI.modify(model, MOI.Utilities.PenaltyRelaxation()), ) + dest = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!(dest, relaxed_str) + MOI.Bridges._test_structural_identical(model, dest) return end From 56ae024bc24c852e9bf67bd80929f360f5b688e4 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 28 Oct 2022 10:51:45 +1300 Subject: [PATCH 11/20] Return a map from MOI.modify --- src/Utilities/penalty_relaxation.jl | 23 +++--- test/Utilities/penalty_relaxation.jl | 103 ++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 12 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index 635c3177df..64c194a816 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -98,7 +98,7 @@ mutable struct PenaltyRelaxation{T} function PenaltyRelaxation( p::Dict{MOI.ConstraintIndex,T}; - default::T = one(T), + default::Union{Nothing,T} = one(T), ) where {T} return new{T}(default, p) end @@ -122,24 +122,29 @@ function MOI.modify(model::MOI.ModelLike, relax::PenaltyRelaxation{T}) where {T} f = zero(MOI.ScalarAffineFunction{T}) MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) end + map = Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}() for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) - _modify_penalty_relaxation(model, relax, F, S) + _modify_penalty_relaxation(map, model, relax, F, S) end - return + return map end function _modify_penalty_relaxation( + map::Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}, model::MOI.ModelLike, relax::PenaltyRelaxation, ::Type{F}, ::Type{S}, -) where {F,S} +) where {T,F,S} for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) if relax.default !== nothing || haskey(relax.penalties, ci) - MOI.modify(model, ci, relax) + f = MOI.modify(model, ci, relax) + if f !== nothing + map[ci] = f + end end end - return + return map end function MOI.modify( @@ -176,7 +181,7 @@ function MOI.modify( obj = MOI.ObjectiveFunction{O}() MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - return + return one(T) * y + one(T) * z end function MOI.modify( @@ -194,7 +199,7 @@ function MOI.modify( O = MOI.get(model, MOI.ObjectiveFunctionType()) obj = MOI.ObjectiveFunction{O}() MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) - return + return one(T) * y end function MOI.modify( @@ -212,5 +217,5 @@ function MOI.modify( O = MOI.get(model, MOI.ObjectiveFunctionType()) obj = MOI.ObjectiveFunction{O}() MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - return + return one(T) * z end diff --git a/test/Utilities/penalty_relaxation.jl b/test/Utilities/penalty_relaxation.jl index 9b6444c991..0d89d6cb21 100644 --- a/test/Utilities/penalty_relaxation.jl +++ b/test/Utilities/penalty_relaxation.jl @@ -25,7 +25,10 @@ end function _test_roundtrip(src_str, relaxed_str) model = MOI.Utilities.Model{Float64}() MOI.Utilities.loadfromstring!(model, src_str) - MOI.modify(model, MOI.Utilities.PenaltyRelaxation()) + map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation()) + for (c, v) in map + @test v isa MOI.ScalarAffineFunction{Float64} + end dest = MOI.Utilities.Model{Float64}() MOI.Utilities.loadfromstring!(dest, relaxed_str) MOI.Bridges._test_structural_identical(model, dest) @@ -218,11 +221,105 @@ function test_relax_quadratic_greaterthanthan() return end -function test_penalties() +function test_penalty_dict() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + c = MOI.add_constraint(model, 1.0 * x, MOI.EqualTo(2.0)) + map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 2.0))) + @test map[c] isa MOI.ScalarAffineFunction{Float64} + @test sprint(print, model) === """ + Minimize ScalarAffineFunction{Float64}: + 0.0 + 2.0 v[2] + 2.0 v[3] + + Subject to: + + ScalarAffineFunction{Float64}-in-EqualTo{Float64} + 0.0 + 1.0 v[1] + 1.0 v[2] - 1.0 v[3] == 2.0 + + VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 + v[3] >= 0.0 + """ + return +end + +function test_default() model = MOI.Utilities.Model{Float64}() x = MOI.add_variable(model) c = MOI.add_constraint(model, 1.0 * x, MOI.EqualTo(2.0)) - MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 2.0))) + map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = 2.0)) + @test map[c] isa MOI.ScalarAffineFunction{Float64} + @test sprint(print, model) === """ + Minimize ScalarAffineFunction{Float64}: + 0.0 + 2.0 v[2] + 2.0 v[3] + + Subject to: + + ScalarAffineFunction{Float64}-in-EqualTo{Float64} + 0.0 + 1.0 v[1] + 1.0 v[2] - 1.0 v[3] == 2.0 + + VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 + v[3] >= 0.0 + """ + return +end + +function test_default_nothing() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + c = MOI.add_constraint(model, 1.0 * x, MOI.EqualTo(2.0)) + map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = nothing)) + @test !haskey(map, c) + @test sprint(print, model) === """ + Minimize ScalarAffineFunction{Float64}: + 0.0 + + Subject to: + + ScalarAffineFunction{Float64}-in-EqualTo{Float64} + 0.0 + 1.0 v[1] == 2.0 + """ + return +end + +function test_brige_optimizer() + model = MOI.instantiate( + MOI.Utilities.Model{Float64}; + with_bridge_type = Float64, + ) + x = MOI.add_variable(model) + c = MOI.add_constraint(model, 1.0 * x, MOI.EqualTo(2.0)) + map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = 2.0)) + @test map[c] isa MOI.ScalarAffineFunction{Float64} + @test sprint(print, model) === """ + Minimize ScalarAffineFunction{Float64}: + 0.0 + 2.0 v[2] + 2.0 v[3] + + Subject to: + + ScalarAffineFunction{Float64}-in-EqualTo{Float64} + 0.0 + 1.0 v[1] + 1.0 v[2] - 1.0 v[3] == 2.0 + + VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 + v[3] >= 0.0 + """ + return +end + +function test_caching_optimizer() + model = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()), + Float64, + ), + ) + x = MOI.add_variable(model) + c = MOI.add_constraint(model, 1.0 * x, MOI.EqualTo(2.0)) + map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = 2.0)) + @test map[c] isa MOI.ScalarAffineFunction{Float64} @test sprint(print, model) === """ Minimize ScalarAffineFunction{Float64}: 0.0 + 2.0 v[2] + 2.0 v[3] From 93eca46ad5600bf861de935a737d0479868d1f96 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 28 Oct 2022 11:02:38 +1300 Subject: [PATCH 12/20] Fix docs --- docs/src/submodules/Utilities/overview.md | 5 ++++- src/Utilities/penalty_relaxation.jl | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/src/submodules/Utilities/overview.md b/docs/src/submodules/Utilities/overview.md index a5f88e4f10..4f37b4a26a 100644 --- a/docs/src/submodules/Utilities/overview.md +++ b/docs/src/submodules/Utilities/overview.md @@ -330,7 +330,7 @@ julia> MOI.set(model, MOI.VariableName(), x, "x") julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); -julia> MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 2.0))) +julia> map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 2.0))); julia> print(model) Minimize ScalarAffineFunction{Float64}: @@ -343,6 +343,9 @@ ScalarAffineFunction{Float64}-in-LessThan{Float64} VariableIndex-in-GreaterThan{Float64} v[2] >= 0.0 + +julia> map[c] +MathOptInterface.ScalarAffineFunction{Float64}(MathOptInterface.ScalarAffineTerm{Float64}[MathOptInterface.ScalarAffineTerm{Float64}(1.0, MathOptInterface.VariableIndex(2))], 0.0) ``` ## Utilities.MatrixOfConstraints diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index 64c194a816..1633adf6f5 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -29,9 +29,17 @@ constraint that is being relaxed. If no value exists, the default is `default`. When `S` is [`MOI.LessThan`](@ref) or [`MOI.GreaterThan`](@ref), we omit `y` or `z` respectively as a performance optimization. +## Return value + +`MOI.modify(model, PenaltyRelaxation())` returns a +`Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction}` that maps constraint +indices to a [`MOI.ScalarAffineFunction`](@ref) comprised of `y + z`. In an +optimal solution, query the value of these functions to compute the violation of +each constraint. + ## Relax a subset of constraints -To relax a subset of constraints, pass a `penalties` dictionary` and set +To relax a subset of constraints, pass a `penalties` dictionary and set `default = nothing`. ## Supported constraint types From e0d064fc1bb9b48adf9bad79c9e103d27795bb78 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 28 Oct 2022 11:16:07 +1300 Subject: [PATCH 13/20] Update doctest --- src/Utilities/penalty_relaxation.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index 1633adf6f5..c55af796f8 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -63,7 +63,7 @@ julia> x = MOI.add_variable(model); julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); -julia> MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = 2.0)) +julia> map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = 2.0)); julia> print(model) Minimize ScalarAffineFunction{Float64}: @@ -76,6 +76,9 @@ ScalarAffineFunction{Float64}-in-LessThan{Float64} VariableIndex-in-GreaterThan{Float64} v[2] >= 0.0 + +julia> map[c] isa MOI.ScalarAffineFunction{Float64} +true ``` ```jldoctest; setup=:(import MathOptInterface; const MOI = MathOptInterface) @@ -85,7 +88,7 @@ julia> x = MOI.add_variable(model); julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); -julia> MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 3.0))) +julia> map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 3.0))); julia> print(model) Minimize ScalarAffineFunction{Float64}: @@ -98,6 +101,9 @@ ScalarAffineFunction{Float64}-in-LessThan{Float64} VariableIndex-in-GreaterThan{Float64} v[2] >= 0.0 + +julia> map[c] isa MOI.ScalarAffineFunction{Float64} +true ``` """ mutable struct PenaltyRelaxation{T} From 2bfb486a85ef81f2c22b0ce23bc71dfe3e0242d0 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 28 Oct 2022 12:00:07 +1300 Subject: [PATCH 14/20] Fix warning --- src/Utilities/penalty_relaxation.jl | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index c55af796f8..ed1a0c9e09 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -138,7 +138,11 @@ function MOI.modify(model::MOI.ModelLike, relax::PenaltyRelaxation{T}) where {T} end map = Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}() for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) - _modify_penalty_relaxation(map, model, relax, F, S) + if MOI.supports(model, relax, MOI.ConstraintIndex{F,S}) + _modify_penalty_relaxation(map, model, relax, F, S) + else + @warn("Skipping PenaltyRelaxation of constraints of type $F-in-$S") + end end return map end @@ -161,20 +165,20 @@ function _modify_penalty_relaxation( return map end -function MOI.modify( +function MOI.supports( + ::MOI.ModelLike, + ::PenaltyRelaxation{T}, + ::Type{<:MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + return true +end + +function MOI.supports( ::MOI.ModelLike, - ci::MOI.ConstraintIndex, ::PenaltyRelaxation, + ::Type{<:MOI.ConstraintIndex}, ) - # We use this fallback to avoid ambiguity errors that would occur if we - # added {F,S} directly to the argument. - _eltype(::MOI.ConstraintIndex{F,S}) where {F,S} = F, S - F, S = _eltype(ci) - @warn( - "Skipping PenaltyRelaxation of constraints of type $F-in-$S", - maxlog = 1, - ) - return + return false end function MOI.modify( From a5bbee7a539634aa008b35c94f4b4d279a3f373e Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 16 Nov 2022 08:59:12 +1300 Subject: [PATCH 15/20] Add ScalarPenaltyRelaxation --- docs/src/submodules/Utilities/overview.md | 28 +++ docs/src/submodules/Utilities/reference.md | 3 +- src/Utilities/penalty_relaxation.jl | 233 +++++++++++++-------- 3 files changed, 178 insertions(+), 86 deletions(-) diff --git a/docs/src/submodules/Utilities/overview.md b/docs/src/submodules/Utilities/overview.md index 4f37b4a26a..f60400b9c0 100644 --- a/docs/src/submodules/Utilities/overview.md +++ b/docs/src/submodules/Utilities/overview.md @@ -348,6 +348,34 @@ julia> map[c] MathOptInterface.ScalarAffineFunction{Float64}(MathOptInterface.ScalarAffineTerm{Float64}[MathOptInterface.ScalarAffineTerm{Float64}(1.0, MathOptInterface.VariableIndex(2))], 0.0) ``` +You can also modify a single constraint using [`Utilities.ScalarPenaltyRelaxation`](@ref): +```jldoctest +julia> model = MOI.Utilities.Model{Float64}(); + +julia> x = MOI.add_variable(model); + +julia> MOI.set(model, MOI.VariableName(), x, "x") + +julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); + +julia> f = MOI.modify(model, c, MOI.Utilities.ScalarPenaltyRelaxation(2.0)); + +julia> print(model) +Minimize ScalarAffineFunction{Float64}: + 0.0 + 2.0 v[2] + +Subject to: + +ScalarAffineFunction{Float64}-in-LessThan{Float64} + 0.0 + 1.0 x - 1.0 v[2] <= 2.0 + +VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 + +julia> f +MathOptInterface.ScalarAffineFunction{Float64}(MathOptInterface.ScalarAffineTerm{Float64}[MathOptInterface.ScalarAffineTerm{Float64}(1.0, MathOptInterface.VariableIndex(2))], 0.0) +``` + ## Utilities.MatrixOfConstraints The constraints of [`Utilities.Model`](@ref) are stored as a vector of tuples diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index 33b5271a87..608eb78037 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -90,10 +90,11 @@ Utilities.identity_index_map Utilities.ModelFilter ``` -## Feasibility relaxation +## Penalty relaxation ```@docs Utilities.PenaltyRelaxation +Utilities.ScalarPenaltyRelaxation ``` ## MatrixOfConstraints diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index ed1a0c9e09..af06cba13b 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -4,6 +4,139 @@ # Use of this source code is governed by an MIT-style license that can be found # in the LICENSE.md file or at https://opensource.org/licenses/MIT. +""" + ScalarPenaltyRelaxation(penalty::T) where {T} + +A problem modifier that, when passed to [`MOI.modify`](@ref), destructively +modifies the constraint in-place to create a penalized relaxation of the +constraint. + +!!! warning + This is a destructive routine that modifies the constraint in-place. If you + don't want to modify the original model, use `JuMP.copy_model` to create a + copy before calling [`MOI.modify`](@ref). + +## Reformulation + +The penalty relaxation modifies constraints of the form ``f(x) \\in S`` into +``f(x) + y - z \\in S``, where ``y, z \\ge 0``, and then it introduces a penalty +term into the objective of ``a \\times (y + z)`` (if minimizing, else ``-a``), +where `a` is the value in the `penalties` dictionary associated with the +constraint that is being relaxed. If no value exists, the default is `default`. + +When `S` is [`MOI.LessThan`](@ref) or [`MOI.GreaterThan`](@ref), we omit `y` or +`z` respectively as a performance optimization. + +## Return value + +`MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty))` returns a +[`MOI.ScalarAffineFunction`](@ref) comprised of `y + z`. In an optimal solution, +query the value of this function to compute the violation of the constraint. + +## Examples + +```jldoctest; setup=:(import MathOptInterface; const MOI = MathOptInterface) +julia> model = MOI.Utilities.Model{Float64}(); + +julia> x = MOI.add_variable(model); + +julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); + +julia> f = MOI.modify(model, c, MOI.Utilities.ScalarPenaltyRelaxation(2.0)); + +julia> print(model) +Minimize ScalarAffineFunction{Float64}: + 0.0 + 2.0 v[2] + +Subject to: + +ScalarAffineFunction{Float64}-in-LessThan{Float64} + 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 + +VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 + +julia> f isa MOI.ScalarAffineFunction{Float64} +true +``` +""" +struct ScalarPenaltyRelaxation{T} + penalty::T +end + +function MOI.supports( + ::MOI.ModelLike, + ::ScalarPenaltyRelaxation{T}, + ::Type{<:MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + return true +end + +function MOI.supports( + ::MOI.ModelLike, + ::ScalarPenaltyRelaxation, + ::Type{<:MOI.ConstraintIndex}, +) + return false +end + +function MOI.modify( + model::MOI.ModelLike, + ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, + relax::ScalarPenaltyRelaxation{T}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + y = MOI.add_variable(model) + z = MOI.add_variable(model) + MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) + MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) + sense = MOI.get(model, MOI.ObjectiveSense()) + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) + a = scale * relax.penalty + O = MOI.get(model, MOI.ObjectiveFunctionType()) + obj = MOI.ObjectiveFunction{O}() + MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) + MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) + return one(T) * y + one(T) * z +end + +function MOI.modify( + model::MOI.ModelLike, + ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, + relax::ScalarPenaltyRelaxation{T}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + # Performance optimization: we don't need the z relaxation variable. + y = MOI.add_variable(model) + MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) + sense = MOI.get(model, MOI.ObjectiveSense()) + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) + a = scale * relax.penalty + O = MOI.get(model, MOI.ObjectiveFunctionType()) + obj = MOI.ObjectiveFunction{O}() + MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) + return one(T) * y +end + +function MOI.modify( + model::MOI.ModelLike, + ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, + relax::ScalarPenaltyRelaxation{T}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + # Performance optimization: we don't need the y relaxation variable. + z = MOI.add_variable(model) + MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) + MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) + sense = MOI.get(model, MOI.ObjectiveSense()) + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) + a = scale * relax.penalty + O = MOI.get(model, MOI.ObjectiveFunctionType()) + obj = MOI.ObjectiveFunction{O}() + MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) + return one(T) * z +end + """ PenaltyRelaxation( penalties = Dict{MOI.ConstraintIndex,Float64}(); @@ -16,7 +149,7 @@ modifies the model in-place to create a penalized relaxation of the constraints. !!! warning This is a destructive routine that modifies the model in-place. If you don't want to modify the original model, use `JuMP.copy_model` to create a copy - before setting this attribute. + before calling [`MOI.modify`](@ref). ## Reformulation @@ -24,7 +157,10 @@ The penalty relaxation modifies constraints of the form ``f(x) \\in S`` into ``f(x) + y - z \\in S``, where ``y, z \\ge 0``, and then it introduces a penalty term into the objective of ``a \\times (y + z)`` (if minimizing, else ``-a``), where `a` is the value in the `penalties` dictionary associated with the -constraint that is being relaxed. If no value exists, the default is `default`. +constraint that is being relaxed. + +If no value exists for the constraint in `penalties`, the penalty is `default`. +If `default` is also `nothing`, then the constraint is skipped. When `S` is [`MOI.LessThan`](@ref) or [`MOI.GreaterThan`](@ref), we omit `y` or `z` respectively as a performance optimization. @@ -138,11 +274,7 @@ function MOI.modify(model::MOI.ModelLike, relax::PenaltyRelaxation{T}) where {T} end map = Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}() for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) - if MOI.supports(model, relax, MOI.ConstraintIndex{F,S}) - _modify_penalty_relaxation(map, model, relax, F, S) - else - @warn("Skipping PenaltyRelaxation of constraints of type $F-in-$S") - end + _modify_penalty_relaxation(map, model, relax, F, S) end return map end @@ -155,85 +287,16 @@ function _modify_penalty_relaxation( ::Type{S}, ) where {T,F,S} for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - if relax.default !== nothing || haskey(relax.penalties, ci) - f = MOI.modify(model, ci, relax) - if f !== nothing - map[ci] = f - end + penalty = get(relax.penalties, ci, relax.default) + if penalty === nothing + continue end + attr = ScalarPenaltyRelaxation(penalty) + if !MOI.supports(model, attr, MOI.ConstraintIndex{F,S}) + @warn("Skipping PenaltyRelaxation of constraints of type $F-in-$S") + return + end + map[ci] = MOI.modify(model, ci, attr) end return map end - -function MOI.supports( - ::MOI.ModelLike, - ::PenaltyRelaxation{T}, - ::Type{<:MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}}, -) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - return true -end - -function MOI.supports( - ::MOI.ModelLike, - ::PenaltyRelaxation, - ::Type{<:MOI.ConstraintIndex}, -) - return false -end - -function MOI.modify( - model::MOI.ModelLike, - ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, - relax::PenaltyRelaxation{T}, -) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - y = MOI.add_variable(model) - z = MOI.add_variable(model) - MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) - MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) - MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) - MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - sense = MOI.get(model, MOI.ObjectiveSense()) - scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) - a = scale * get(relax.penalties, ci, relax.default) - O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.ObjectiveFunction{O}() - MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) - MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - return one(T) * y + one(T) * z -end - -function MOI.modify( - model::MOI.ModelLike, - ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, - relax::PenaltyRelaxation{T}, -) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - # Performance optimization: we don't need the z relaxation variable. - y = MOI.add_variable(model) - MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) - MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) - sense = MOI.get(model, MOI.ObjectiveSense()) - scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) - a = scale * get(relax.penalties, ci, relax.default) - O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.ObjectiveFunction{O}() - MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) - return one(T) * y -end - -function MOI.modify( - model::MOI.ModelLike, - ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, - relax::PenaltyRelaxation{T}, -) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - # Performance optimization: we don't need the y relaxation variable. - z = MOI.add_variable(model) - MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) - MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - sense = MOI.get(model, MOI.ObjectiveSense()) - scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) - a = scale * get(relax.penalties, ci, relax.default) - O = MOI.get(model, MOI.ObjectiveFunctionType()) - obj = MOI.ObjectiveFunction{O}() - MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) - return one(T) * z -end From c1ce388bd4da71a9a777db4c8aa45c652eebc87f Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 16 Nov 2022 09:17:32 +1300 Subject: [PATCH 16/20] Fix FEASIBILITY_SENSE cases --- src/Utilities/penalty_relaxation.jl | 26 +++++++++++++++++--------- test/Utilities/penalty_relaxation.jl | 24 ++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index af06cba13b..581dda0c19 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -80,18 +80,32 @@ function MOI.supports( return false end +function _change_set_to_min_if_necessary( + ::Type{T}, + model::MOI.ModelLike, +) where {T} + sense = MOI.get(model, MOI.ObjectiveSense()) + if sense != MOI.FEASIBILITY_SENSE + return sense + end + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + f = zero(MOI.ScalarAffineFunction{T}) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + return MOI.MIN_SENSE +end + function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, relax::ScalarPenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + sense = _change_set_to_min_if_necessary(T, model) y = MOI.add_variable(model) z = MOI.add_variable(model) MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - sense = MOI.get(model, MOI.ObjectiveSense()) scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) a = scale * relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) @@ -106,11 +120,11 @@ function MOI.modify( ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, relax::ScalarPenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + sense = _change_set_to_min_if_necessary(T, model) # Performance optimization: we don't need the z relaxation variable. y = MOI.add_variable(model) MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) - sense = MOI.get(model, MOI.ObjectiveSense()) scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) a = scale * relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) @@ -124,11 +138,11 @@ function MOI.modify( ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, relax::ScalarPenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + sense = _change_set_to_min_if_necessary(T, model) # Performance optimization: we don't need the y relaxation variable. z = MOI.add_variable(model) MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) - sense = MOI.get(model, MOI.ObjectiveSense()) scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) a = scale * relax.penalty O = MOI.get(model, MOI.ObjectiveFunctionType()) @@ -266,12 +280,6 @@ function PenaltyRelaxation( end function MOI.modify(model::MOI.ModelLike, relax::PenaltyRelaxation{T}) where {T} - sense = MOI.get(model, MOI.ObjectiveSense()) - if sense == MOI.FEASIBILITY_SENSE - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - f = zero(MOI.ScalarAffineFunction{T}) - MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) - end map = Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}() for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) _modify_penalty_relaxation(map, model, relax, F, S) diff --git a/test/Utilities/penalty_relaxation.jl b/test/Utilities/penalty_relaxation.jl index 0d89d6cb21..2e7c067130 100644 --- a/test/Utilities/penalty_relaxation.jl +++ b/test/Utilities/penalty_relaxation.jl @@ -272,8 +272,7 @@ function test_default_nothing() map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = nothing)) @test !haskey(map, c) @test sprint(print, model) === """ - Minimize ScalarAffineFunction{Float64}: - 0.0 + Feasibility Subject to: @@ -336,6 +335,27 @@ function test_caching_optimizer() return end +function test_scalar_penalty_relaxation() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)) + f = MOI.modify(model, c, MOI.Utilities.ScalarPenaltyRelaxation(2.0)) + @test f isa MOI.ScalarAffineFunction{Float64} + @test sprint(print, model) === """ + Minimize ScalarAffineFunction{Float64}: + 0.0 + 2.0 v[2] + + Subject to: + + ScalarAffineFunction{Float64}-in-LessThan{Float64} + 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 + + VariableIndex-in-GreaterThan{Float64} + v[2] >= 0.0 + """ + return end +end # module + TestPenaltyRelaxation.runtests() From 594bf9485fc4f9bccb08eead9d065eaee0cc3cbf Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 16 Nov 2022 10:03:24 +1300 Subject: [PATCH 17/20] Fix docstring --- src/Utilities/penalty_relaxation.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index 581dda0c19..f1d9fa50fd 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -46,15 +46,15 @@ julia> f = MOI.modify(model, c, MOI.Utilities.ScalarPenaltyRelaxation(2.0)); julia> print(model) Minimize ScalarAffineFunction{Float64}: - 0.0 + 2.0 v[2] + 0.0 + 2.0 v[2] Subject to: ScalarAffineFunction{Float64}-in-LessThan{Float64} - 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 + 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 VariableIndex-in-GreaterThan{Float64} - v[2] >= 0.0 + v[2] >= 0.0 julia> f isa MOI.ScalarAffineFunction{Float64} true From 2a0fece47e709cf65f42c035383940f356601c9a Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 22 Nov 2022 07:50:28 +1300 Subject: [PATCH 18/20] Respond to code review --- src/Utilities/penalty_relaxation.jl | 60 +++++++++++++---------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index f1d9fa50fd..b09d98645d 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -21,8 +21,7 @@ constraint. The penalty relaxation modifies constraints of the form ``f(x) \\in S`` into ``f(x) + y - z \\in S``, where ``y, z \\ge 0``, and then it introduces a penalty term into the objective of ``a \\times (y + z)`` (if minimizing, else ``-a``), -where `a` is the value in the `penalties` dictionary associated with the -constraint that is being relaxed. If no value exists, the default is `default`. +where ``a`` is `penalty` When `S` is [`MOI.LessThan`](@ref) or [`MOI.GreaterThan`](@ref), we omit `y` or `z` respectively as a performance optimization. @@ -60,26 +59,14 @@ julia> f isa MOI.ScalarAffineFunction{Float64} true ``` """ -struct ScalarPenaltyRelaxation{T} +struct ScalarPenaltyRelaxation{T} # <: MOI.AbstractFunctionModification + # We don't make this a subtype of AbstractFunctionModification to avoid some + # ambiguities with generic methods in Utilities and Bridges. The underlying + # reason is that these reformulations can be written using the high-level + # MOI API, so we don't need special handling for bridges and utilities. penalty::T end -function MOI.supports( - ::MOI.ModelLike, - ::ScalarPenaltyRelaxation{T}, - ::Type{<:MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}}, -) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - return true -end - -function MOI.supports( - ::MOI.ModelLike, - ::ScalarPenaltyRelaxation, - ::Type{<:MOI.ConstraintIndex}, -) - return false -end - function _change_set_to_min_if_necessary( ::Type{T}, model::MOI.ModelLike, @@ -94,6 +81,15 @@ function _change_set_to_min_if_necessary( return MOI.MIN_SENSE end +function MOI.modify( + ::MOI.ModelLike, + ::MOI.ConstraintIndex, + ::ScalarPenaltyRelaxation, +) + # A generic fallback if modification is not supported. + return nothing +end + function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, @@ -167,17 +163,13 @@ modifies the model in-place to create a penalized relaxation of the constraints. ## Reformulation -The penalty relaxation modifies constraints of the form ``f(x) \\in S`` into -``f(x) + y - z \\in S``, where ``y, z \\ge 0``, and then it introduces a penalty -term into the objective of ``a \\times (y + z)`` (if minimizing, else ``-a``), -where `a` is the value in the `penalties` dictionary associated with the -constraint that is being relaxed. +See [`Utilities.ScalarPenaltyRelaxation`](@ref) for details of the +reformulation. -If no value exists for the constraint in `penalties`, the penalty is `default`. -If `default` is also `nothing`, then the constraint is skipped. - -When `S` is [`MOI.LessThan`](@ref) or [`MOI.GreaterThan`](@ref), we omit `y` or -`z` respectively as a performance optimization. +For each constraint `ci`, the penalty passed to [`Utilities.ScalarPenaltyRelaxation`](@ref) +is `get(penalties, ci, default)`. If the value is `nothing`, because `ci` does +not exist in `penalties` and `default = nothing`, then the constraint is +skipped. ## Return value @@ -299,12 +291,12 @@ function _modify_penalty_relaxation( if penalty === nothing continue end - attr = ScalarPenaltyRelaxation(penalty) - if !MOI.supports(model, attr, MOI.ConstraintIndex{F,S}) - @warn("Skipping PenaltyRelaxation of constraints of type $F-in-$S") + delta = MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty)) + if delta === nothing + @warn("Skipping PenaltyRelaxation for constraints of type $F-in-$S") return end - map[ci] = MOI.modify(model, ci, attr) + map[ci] = delta end - return map + return end From 1981e0afd926c1cee778225bcfd3eb67d3daa7e0 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 22 Nov 2022 10:31:16 +1300 Subject: [PATCH 19/20] Update docstring --- src/Utilities/penalty_relaxation.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index b09d98645d..16d7008f98 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -28,9 +28,9 @@ When `S` is [`MOI.LessThan`](@ref) or [`MOI.GreaterThan`](@ref), we omit `y` or ## Return value -`MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty))` returns a -[`MOI.ScalarAffineFunction`](@ref) comprised of `y + z`. In an optimal solution, -query the value of this function to compute the violation of the constraint. +`MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty))` returns `y + z` as a +[`MOI.ScalarAffineFunction`](@ref). In an optimal solution, query the value of +this function to compute the violation of the constraint. ## Examples @@ -174,10 +174,10 @@ skipped. ## Return value `MOI.modify(model, PenaltyRelaxation())` returns a -`Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction}` that maps constraint -indices to a [`MOI.ScalarAffineFunction`](@ref) comprised of `y + z`. In an -optimal solution, query the value of these functions to compute the violation of -each constraint. +`Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction}` that maps each constraint +index to the corresponding `y + z` as a [`MOI.ScalarAffineFunction`](@ref). In +an optimal solution, query the value of these functions to compute the violation +of each constraint. ## Relax a subset of constraints From a2d19fe789f4398ff6ec418a558add657022a6f2 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 24 Nov 2022 13:04:12 +1300 Subject: [PATCH 20/20] Update --- src/Utilities/penalty_relaxation.jl | 30 ++++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index 16d7008f98..d323e0cccf 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -67,7 +67,7 @@ struct ScalarPenaltyRelaxation{T} # <: MOI.AbstractFunctionModification penalty::T end -function _change_set_to_min_if_necessary( +function _change_sense_to_min_if_necessary( ::Type{T}, model::MOI.ModelLike, ) where {T} @@ -81,21 +81,12 @@ function _change_set_to_min_if_necessary( return MOI.MIN_SENSE end -function MOI.modify( - ::MOI.ModelLike, - ::MOI.ConstraintIndex, - ::ScalarPenaltyRelaxation, -) - # A generic fallback if modification is not supported. - return nothing -end - function MOI.modify( model::MOI.ModelLike, ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, relax::ScalarPenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - sense = _change_set_to_min_if_necessary(T, model) + sense = _change_sense_to_min_if_necessary(T, model) y = MOI.add_variable(model) z = MOI.add_variable(model) MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) @@ -116,7 +107,7 @@ function MOI.modify( ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, relax::ScalarPenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - sense = _change_set_to_min_if_necessary(T, model) + sense = _change_sense_to_min_if_necessary(T, model) # Performance optimization: we don't need the z relaxation variable. y = MOI.add_variable(model) MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) @@ -134,7 +125,7 @@ function MOI.modify( ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, relax::ScalarPenaltyRelaxation{T}, ) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} - sense = _change_set_to_min_if_necessary(T, model) + sense = _change_sense_to_min_if_necessary(T, model) # Performance optimization: we don't need the y relaxation variable. z = MOI.add_variable(model) MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) @@ -291,12 +282,15 @@ function _modify_penalty_relaxation( if penalty === nothing continue end - delta = MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty)) - if delta === nothing - @warn("Skipping PenaltyRelaxation for constraints of type $F-in-$S") - return + try + map[ci] = MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty)) + catch err + if err isa MethodError && err.f == MOI.modify + @warn("Skipping PenaltyRelaxation for ConstraintIndex{$F,$S}") + return + end + rethrow(err) end - map[ci] = delta end return end