|
| 1 | +# Copyright (c) 2017: Miles Lubin and contributors |
| 2 | +# Copyright (c) 2017: Google Inc. |
| 3 | +# |
| 4 | +# Use of this source code is governed by an MIT-style license that can be found |
| 5 | +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. |
| 6 | + |
| 7 | +""" |
| 8 | + ScalarPenaltyRelaxation(penalty::T) where {T} |
| 9 | +
|
| 10 | +A problem modifier that, when passed to [`MOI.modify`](@ref), destructively |
| 11 | +modifies the constraint in-place to create a penalized relaxation of the |
| 12 | +constraint. |
| 13 | +
|
| 14 | +!!! warning |
| 15 | + This is a destructive routine that modifies the constraint in-place. If you |
| 16 | + don't want to modify the original model, use `JuMP.copy_model` to create a |
| 17 | + copy before calling [`MOI.modify`](@ref). |
| 18 | +
|
| 19 | +## Reformulation |
| 20 | +
|
| 21 | +The penalty relaxation modifies constraints of the form ``f(x) \\in S`` into |
| 22 | +``f(x) + y - z \\in S``, where ``y, z \\ge 0``, and then it introduces a penalty |
| 23 | +term into the objective of ``a \\times (y + z)`` (if minimizing, else ``-a``), |
| 24 | +where ``a`` is `penalty` |
| 25 | +
|
| 26 | +When `S` is [`MOI.LessThan`](@ref) or [`MOI.GreaterThan`](@ref), we omit `y` or |
| 27 | +`z` respectively as a performance optimization. |
| 28 | +
|
| 29 | +## Return value |
| 30 | +
|
| 31 | +`MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty))` returns `y + z` as a |
| 32 | +[`MOI.ScalarAffineFunction`](@ref). In an optimal solution, query the value of |
| 33 | +this function to compute the violation of the constraint. |
| 34 | +
|
| 35 | +## Examples |
| 36 | +
|
| 37 | +```jldoctest; setup=:(import MathOptInterface; const MOI = MathOptInterface) |
| 38 | +julia> model = MOI.Utilities.Model{Float64}(); |
| 39 | +
|
| 40 | +julia> x = MOI.add_variable(model); |
| 41 | +
|
| 42 | +julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); |
| 43 | +
|
| 44 | +julia> f = MOI.modify(model, c, MOI.Utilities.ScalarPenaltyRelaxation(2.0)); |
| 45 | +
|
| 46 | +julia> print(model) |
| 47 | +Minimize ScalarAffineFunction{Float64}: |
| 48 | + 0.0 + 2.0 v[2] |
| 49 | +
|
| 50 | +Subject to: |
| 51 | +
|
| 52 | +ScalarAffineFunction{Float64}-in-LessThan{Float64} |
| 53 | + 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 |
| 54 | +
|
| 55 | +VariableIndex-in-GreaterThan{Float64} |
| 56 | + v[2] >= 0.0 |
| 57 | +
|
| 58 | +julia> f isa MOI.ScalarAffineFunction{Float64} |
| 59 | +true |
| 60 | +``` |
| 61 | +""" |
| 62 | +struct ScalarPenaltyRelaxation{T} # <: MOI.AbstractFunctionModification |
| 63 | + # We don't make this a subtype of AbstractFunctionModification to avoid some |
| 64 | + # ambiguities with generic methods in Utilities and Bridges. The underlying |
| 65 | + # reason is that these reformulations can be written using the high-level |
| 66 | + # MOI API, so we don't need special handling for bridges and utilities. |
| 67 | + penalty::T |
| 68 | +end |
| 69 | + |
| 70 | +function _change_sense_to_min_if_necessary( |
| 71 | + ::Type{T}, |
| 72 | + model::MOI.ModelLike, |
| 73 | +) where {T} |
| 74 | + sense = MOI.get(model, MOI.ObjectiveSense()) |
| 75 | + if sense != MOI.FEASIBILITY_SENSE |
| 76 | + return sense |
| 77 | + end |
| 78 | + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) |
| 79 | + f = zero(MOI.ScalarAffineFunction{T}) |
| 80 | + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) |
| 81 | + return MOI.MIN_SENSE |
| 82 | +end |
| 83 | + |
| 84 | +function MOI.modify( |
| 85 | + model::MOI.ModelLike, |
| 86 | + ci::MOI.ConstraintIndex{F,<:MOI.AbstractScalarSet}, |
| 87 | + relax::ScalarPenaltyRelaxation{T}, |
| 88 | +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} |
| 89 | + sense = _change_sense_to_min_if_necessary(T, model) |
| 90 | + y = MOI.add_variable(model) |
| 91 | + z = MOI.add_variable(model) |
| 92 | + MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) |
| 93 | + MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) |
| 94 | + MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) |
| 95 | + MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) |
| 96 | + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) |
| 97 | + a = scale * relax.penalty |
| 98 | + O = MOI.get(model, MOI.ObjectiveFunctionType()) |
| 99 | + obj = MOI.ObjectiveFunction{O}() |
| 100 | + MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) |
| 101 | + MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) |
| 102 | + return one(T) * y + one(T) * z |
| 103 | +end |
| 104 | + |
| 105 | +function MOI.modify( |
| 106 | + model::MOI.ModelLike, |
| 107 | + ci::MOI.ConstraintIndex{F,MOI.GreaterThan{T}}, |
| 108 | + relax::ScalarPenaltyRelaxation{T}, |
| 109 | +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} |
| 110 | + sense = _change_sense_to_min_if_necessary(T, model) |
| 111 | + # Performance optimization: we don't need the z relaxation variable. |
| 112 | + y = MOI.add_variable(model) |
| 113 | + MOI.add_constraint(model, y, MOI.GreaterThan(zero(T))) |
| 114 | + MOI.modify(model, ci, MOI.ScalarCoefficientChange(y, one(T))) |
| 115 | + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) |
| 116 | + a = scale * relax.penalty |
| 117 | + O = MOI.get(model, MOI.ObjectiveFunctionType()) |
| 118 | + obj = MOI.ObjectiveFunction{O}() |
| 119 | + MOI.modify(model, obj, MOI.ScalarCoefficientChange(y, a)) |
| 120 | + return one(T) * y |
| 121 | +end |
| 122 | + |
| 123 | +function MOI.modify( |
| 124 | + model::MOI.ModelLike, |
| 125 | + ci::MOI.ConstraintIndex{F,MOI.LessThan{T}}, |
| 126 | + relax::ScalarPenaltyRelaxation{T}, |
| 127 | +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} |
| 128 | + sense = _change_sense_to_min_if_necessary(T, model) |
| 129 | + # Performance optimization: we don't need the y relaxation variable. |
| 130 | + z = MOI.add_variable(model) |
| 131 | + MOI.add_constraint(model, z, MOI.GreaterThan(zero(T))) |
| 132 | + MOI.modify(model, ci, MOI.ScalarCoefficientChange(z, -one(T))) |
| 133 | + scale = sense == MOI.MIN_SENSE ? one(T) : -one(T) |
| 134 | + a = scale * relax.penalty |
| 135 | + O = MOI.get(model, MOI.ObjectiveFunctionType()) |
| 136 | + obj = MOI.ObjectiveFunction{O}() |
| 137 | + MOI.modify(model, obj, MOI.ScalarCoefficientChange(z, a)) |
| 138 | + return one(T) * z |
| 139 | +end |
| 140 | + |
| 141 | +""" |
| 142 | + PenaltyRelaxation( |
| 143 | + penalties = Dict{MOI.ConstraintIndex,Float64}(); |
| 144 | + default::Union{Nothing,T} = 1.0, |
| 145 | + ) |
| 146 | +
|
| 147 | +A problem modifier that, when passed to [`MOI.modify`](@ref), destructively |
| 148 | +modifies the model in-place to create a penalized relaxation of the constraints. |
| 149 | +
|
| 150 | +!!! warning |
| 151 | + This is a destructive routine that modifies the model in-place. If you don't |
| 152 | + want to modify the original model, use `JuMP.copy_model` to create a copy |
| 153 | + before calling [`MOI.modify`](@ref). |
| 154 | +
|
| 155 | +## Reformulation |
| 156 | +
|
| 157 | +See [`Utilities.ScalarPenaltyRelaxation`](@ref) for details of the |
| 158 | +reformulation. |
| 159 | +
|
| 160 | +For each constraint `ci`, the penalty passed to [`Utilities.ScalarPenaltyRelaxation`](@ref) |
| 161 | +is `get(penalties, ci, default)`. If the value is `nothing`, because `ci` does |
| 162 | +not exist in `penalties` and `default = nothing`, then the constraint is |
| 163 | +skipped. |
| 164 | +
|
| 165 | +## Return value |
| 166 | +
|
| 167 | +`MOI.modify(model, PenaltyRelaxation())` returns a |
| 168 | +`Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction}` that maps each constraint |
| 169 | +index to the corresponding `y + z` as a [`MOI.ScalarAffineFunction`](@ref). In |
| 170 | +an optimal solution, query the value of these functions to compute the violation |
| 171 | +of each constraint. |
| 172 | +
|
| 173 | +## Relax a subset of constraints |
| 174 | +
|
| 175 | +To relax a subset of constraints, pass a `penalties` dictionary and set |
| 176 | +`default = nothing`. |
| 177 | +
|
| 178 | +## Supported constraint types |
| 179 | +
|
| 180 | +The penalty relaxation is currently limited to modifying |
| 181 | +[`MOI.ScalarAffineFunction`](@ref) and [`MOI.ScalarQuadraticFunction`](@ref) |
| 182 | +constraints in the linear sets [`MOI.LessThan`](@ref), [`MOI.GreaterThan`](@ref), |
| 183 | +[`MOI.EqualTo`](@ref) and [`MOI.Interval`](@ref). |
| 184 | +
|
| 185 | +It does not include variable bound or integrality constraints, because these |
| 186 | +cannot be modified in-place. |
| 187 | +
|
| 188 | +To modify variable bounds, rewrite them as linear constraints. |
| 189 | +
|
| 190 | +## Examples |
| 191 | +
|
| 192 | +```jldoctest; setup=:(import MathOptInterface; const MOI = MathOptInterface) |
| 193 | +julia> model = MOI.Utilities.Model{Float64}(); |
| 194 | +
|
| 195 | +julia> x = MOI.add_variable(model); |
| 196 | +
|
| 197 | +julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); |
| 198 | +
|
| 199 | +julia> map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = 2.0)); |
| 200 | +
|
| 201 | +julia> print(model) |
| 202 | +Minimize ScalarAffineFunction{Float64}: |
| 203 | + 0.0 + 2.0 v[2] |
| 204 | +
|
| 205 | +Subject to: |
| 206 | +
|
| 207 | +ScalarAffineFunction{Float64}-in-LessThan{Float64} |
| 208 | + 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 |
| 209 | +
|
| 210 | +VariableIndex-in-GreaterThan{Float64} |
| 211 | + v[2] >= 0.0 |
| 212 | +
|
| 213 | +julia> map[c] isa MOI.ScalarAffineFunction{Float64} |
| 214 | +true |
| 215 | +``` |
| 216 | +
|
| 217 | +```jldoctest; setup=:(import MathOptInterface; const MOI = MathOptInterface) |
| 218 | +julia> model = MOI.Utilities.Model{Float64}(); |
| 219 | +
|
| 220 | +julia> x = MOI.add_variable(model); |
| 221 | +
|
| 222 | +julia> c = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(2.0)); |
| 223 | +
|
| 224 | +julia> map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(Dict(c => 3.0))); |
| 225 | +
|
| 226 | +julia> print(model) |
| 227 | +Minimize ScalarAffineFunction{Float64}: |
| 228 | + 0.0 + 3.0 v[2] |
| 229 | +
|
| 230 | +Subject to: |
| 231 | +
|
| 232 | +ScalarAffineFunction{Float64}-in-LessThan{Float64} |
| 233 | + 0.0 + 1.0 v[1] - 1.0 v[2] <= 2.0 |
| 234 | +
|
| 235 | +VariableIndex-in-GreaterThan{Float64} |
| 236 | + v[2] >= 0.0 |
| 237 | +
|
| 238 | +julia> map[c] isa MOI.ScalarAffineFunction{Float64} |
| 239 | +true |
| 240 | +``` |
| 241 | +""" |
| 242 | +mutable struct PenaltyRelaxation{T} |
| 243 | + default::Union{Nothing,T} |
| 244 | + penalties::Dict{MOI.ConstraintIndex,T} |
| 245 | + |
| 246 | + function PenaltyRelaxation( |
| 247 | + p::Dict{MOI.ConstraintIndex,T}; |
| 248 | + default::Union{Nothing,T} = one(T), |
| 249 | + ) where {T} |
| 250 | + return new{T}(default, p) |
| 251 | + end |
| 252 | +end |
| 253 | + |
| 254 | +function PenaltyRelaxation(; kwargs...) |
| 255 | + return PenaltyRelaxation(Dict{MOI.ConstraintIndex,Float64}(); kwargs...) |
| 256 | +end |
| 257 | + |
| 258 | +function PenaltyRelaxation( |
| 259 | + d::Dict{<:MOI.ConstraintIndex,T}; |
| 260 | + kwargs..., |
| 261 | +) where {T} |
| 262 | + return PenaltyRelaxation(convert(Dict{MOI.ConstraintIndex,T}, d); kwargs...) |
| 263 | +end |
| 264 | + |
| 265 | +function MOI.modify(model::MOI.ModelLike, relax::PenaltyRelaxation{T}) where {T} |
| 266 | + map = Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}() |
| 267 | + for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) |
| 268 | + _modify_penalty_relaxation(map, model, relax, F, S) |
| 269 | + end |
| 270 | + return map |
| 271 | +end |
| 272 | + |
| 273 | +function _modify_penalty_relaxation( |
| 274 | + map::Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}, |
| 275 | + model::MOI.ModelLike, |
| 276 | + relax::PenaltyRelaxation, |
| 277 | + ::Type{F}, |
| 278 | + ::Type{S}, |
| 279 | +) where {T,F,S} |
| 280 | + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) |
| 281 | + penalty = get(relax.penalties, ci, relax.default) |
| 282 | + if penalty === nothing |
| 283 | + continue |
| 284 | + end |
| 285 | + try |
| 286 | + map[ci] = MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty)) |
| 287 | + catch err |
| 288 | + if err isa MethodError && err.f == MOI.modify |
| 289 | + @warn("Skipping PenaltyRelaxation for ConstraintIndex{$F,$S}") |
| 290 | + return |
| 291 | + end |
| 292 | + rethrow(err) |
| 293 | + end |
| 294 | + end |
| 295 | + return |
| 296 | +end |
0 commit comments