Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Utilities] add PenaltyRelaxation #1995

Merged
merged 20 commits into from
Nov 29, 2022
61 changes: 61 additions & 0 deletions docs/src/submodules/Utilities/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,67 @@ $$ \begin{aligned}
In IJulia, calling `print` or ending a cell with
[`Utilities.latex_formulation`](@ref) will render the model in LaTeX.

## Utilities.PenaltyRelaxation

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.

```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> map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(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

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
Expand Down
7 changes: 7 additions & 0 deletions docs/src/submodules/Utilities/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ Utilities.identity_index_map
Utilities.ModelFilter
```

## Penalty relaxation

```@docs
Utilities.PenaltyRelaxation
Utilities.ScalarPenaltyRelaxation
```

## MatrixOfConstraints

```@docs
Expand Down
2 changes: 1 addition & 1 deletion src/Utilities/Utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ include("mockoptimizer.jl")
include("cachingoptimizer.jl")
include("universalfallback.jl")
include("print.jl")

include("penalty_relaxation.jl")
include("lazy_iterators.jl")

include("precompile.jl")
Expand Down
302 changes: 302 additions & 0 deletions src/Utilities/penalty_relaxation.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
# 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.

"""
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 `penalty`

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} # <: 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 _change_set_to_min_if_necessary(
odow marked this conversation as resolved.
Show resolved Hide resolved
::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(
::MOI.ModelLike,
::MOI.ConstraintIndex,
::ScalarPenaltyRelaxation,
)
# A generic fallback if modification is not supported.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is inconsistent, if a modification is not supported, an error should be thrown instead. Is that useful for VariableIndex only ? Then we can check whether it is VariableIndex before calling modify.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's for any set that isn't supported.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We either need supports or we need this fallback.

Copy link
Member

@blegat blegat Nov 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's for any set that isn't supported.

Isn't the method below defined for any AbstractScalarSet ?
So this is in case an MOI extension defines another type of AbstractScalarFunction ?
We could extend the definition of supports but then we need to add supports methods for solvers.

Another option is to add a keyword to the modify that is model-wise like ignore_unsupported.
If this is false, we just modify and let the error propagate.
Otherwise, we use a try-catch and handle ModifyConstraintNotAllowed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the try-catch.

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)
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)))
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}}}
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)))
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}}}
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)))
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}();
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 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 calling [`MOI.modify`](@ref).

## Reformulation

See [`Utilities.ScalarPenaltyRelaxation`](@ref) for details of the
reformulation.

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

`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
odow marked this conversation as resolved.
Show resolved Hide resolved
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
`default = nothing`.

## Supported constraint types

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).

It does not include variable bound or integrality constraints, because these
cannot be modified in-place.

To modify variable bounds, rewrite them as linear constraints.

## 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> map = MOI.modify(model, MOI.Utilities.PenaltyRelaxation(default = 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> map[c] isa MOI.ScalarAffineFunction{Float64}
true
```

```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> map = 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

VariableIndex-in-GreaterThan{Float64}
v[2] >= 0.0

julia> map[c] isa MOI.ScalarAffineFunction{Float64}
true
```
"""
mutable struct PenaltyRelaxation{T}
default::Union{Nothing,T}
penalties::Dict{MOI.ConstraintIndex,T}

function PenaltyRelaxation(
p::Dict{MOI.ConstraintIndex,T};
default::Union{Nothing,T} = one(T),
) where {T}
return new{T}(default, p)
end
end

function PenaltyRelaxation(; kwargs...)
return PenaltyRelaxation(Dict{MOI.ConstraintIndex,Float64}(); kwargs...)
end

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::PenaltyRelaxation{T}) where {T}
map = Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}}()
for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent())
_modify_penalty_relaxation(map, model, relax, F, S)
end
return map
end

function _modify_penalty_relaxation(
map::Dict{MOI.ConstraintIndex,MOI.ScalarAffineFunction{T}},
model::MOI.ModelLike,
relax::PenaltyRelaxation,
::Type{F},
::Type{S},
) where {T,F,S}
for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
penalty = get(relax.penalties, ci, relax.default)
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
end
map[ci] = delta
end
return
end
Loading