Skip to content

Commit

Permalink
Add standard_form modifications (#1935)
Browse files Browse the repository at this point in the history
* Add standard_form modifications

* Fix MethodError in set_rhs

* Fix doctests

* Add getters for standard_form funcs

* Fix tests and add more
  • Loading branch information
odow authored Apr 9, 2019
1 parent 8809ab7 commit 665f3a7
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 31 deletions.
75 changes: 62 additions & 13 deletions docs/src/constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,41 @@ model with different coefficients.

### Modifying a constant term

Most often, modifications involve changing the "right-hand side" of a linear
constraint. This presents a challenge for JuMP because it leads to ambiguities.
For example, what is the right-hand side term of
`@constraint(model, 2x + 1 <= x - 3)`? This applies more generally to any
constant term in a function appearing in the objective or a constraint.
Use [`set_standard_form_rhs`](@ref) to modify the right-hand side (constant)
term of a constraint. Use [`standard_form_rhs`](@ref) to query the right-hand
side term.

To avoid these ambiguities, JuMP includes the ability to *fix* variables to a
```jldoctest con_fix; setup = :(model = Model(); @variable(model, x))
julia> @constraint(model, con, 2x <= 1)
con : 2 x <= 1.0
julia> set_standard_form_rhs(con, 3)
julia> con
con : 2 x <= 3.0
julia> standard_form_rhs(con)
3.0
```

!!! note
JuMP normalizes constraints into a standard form by moving all constant terms
onto the right-hand side of the constraint.
```julia
@constraint(model, 2x - 1 <= 2)
```
will be normalized to
```julia
@constraint(model, 2x <= 3)
```
[`set_standard_form_rhs`](@ref) sets the right-hand side term of the
normalized constraint.

If constraints are complicated, e.g., they are composed of a number of
components, each of which has a constant term, then it may be difficult to
calculate what the right-hand side term should be in the standard form.

For this situation, JuMP includes the ability to *fix* variables to a
value using the [`fix`](@ref) function. Fixing a variable sets its lower
and upper bound to the same value. Thus, changes in a constant term can be
simulated by adding a dummy variable and fixing it to different values. Here is
Expand All @@ -563,31 +591,49 @@ an example:
julia> @variable(model, const_term)
const_term
julia> @constraint(model, con, 2x <= const_term)
con : 2 x - const_term <= 0.0
julia> @constraint(model, con, 2x <= const_term + 1)
con : 2 x - const_term <= 1.0
julia> fix(const_term, 1.0)
```
The constraint `con` is now equivalent to `2x <= 2`.

!!! note
Even though `const_term` is fixed, it is still a decision variable. Thus,
`const_term * x` is bilinear. Fixed variables are not replaced with
constants when communicating the problem to a solver.

### Modifying a variable coefficient

It is also possible to modify the scalar coefficients (but notably *not yet* the
quadratic coefficients) using the [`set_coefficient`](@ref) function. Here
is an example:
To modify the scalar coefficients of a cosntraint (but notably *not yet* the
quadratic coefficients), use [`set_standard_form_coefficient`](@ref). To query
the current coefficient, use [`standard_form_coefficient`](@ref).
```jldoctest; setup = :(model = Model(); @variable(model, x))
julia> @constraint(model, con, 2x <= 1)
con : 2 x <= 1.0
julia> set_coefficient(con, x, 3)
julia> set_standard_form_coefficient(con, x, 3)
julia> con
con : 3 x <= 1.0
julia> standard_form_coefficient(con, x)
3.0
```

!!! note
JuMP normalizes constraints into a standard form by moving all terms
involving variables onto the left-hand side of the constraint.
```julia
@constraint(model, 2x <= 1 - x)
```
will be normalized to
```julia
@constraint(model, 3x <= 1)
```
[`set_standard_form_coefficient`](@ref) sets the coefficient of the
normalized constraint.

## Constraint deletion

Constraints can be deleted from a model using [`delete`](@ref). Just like
Expand Down Expand Up @@ -672,7 +718,10 @@ SecondOrderCone
RotatedSecondOrderCone
PSDCone
shadow_price
set_coefficient
standard_form_coefficient
set_standard_form_coefficient
standard_form_rhs
set_standard_form_rhs
is_valid
JuMP.delete
LowerBoundRef
Expand Down
4 changes: 4 additions & 0 deletions src/aff_expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ end

GenericAffExpr{C, V}() where {C, V} = zero(GenericAffExpr{C, V})

function _affine_coefficient(f::GenericAffExpr{C, V}, variable::V) where {C, V}
return get(f.terms, variable, zero(C))
end

function map_coefficients_inplace!(f::Function, a::GenericAffExpr)
# The iterator remains valid if existing elements are updated.
for (coef, var) in linear_terms(a)
Expand Down
78 changes: 68 additions & 10 deletions src/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -395,36 +395,94 @@ function add_constraint(model::Model, c::AbstractConstraint, name::String="")
end

"""
set_coefficient(constraint::ConstraintRef, variable::VariableRef, value)
set_standard_form_coefficient(constraint::ConstraintRef, variable::VariableRef, value)
Set the coefficient of `variable` in the constraint `constraint` to `value`.
Note that prior to this step, JuMP will aggregate multiple terms containing the
same variable. For example, given a constraint `2x + 3x <= 2`,
`set_coefficient(c, x, 4)` will create the constraint `4x <= 2`.
`set_standard_form_coefficient(c, x, 4)` will create the constraint `4x <= 2`.
```jldoctest; setup = :(using JuMP), filter=r"≤|<="
model = Model()
@variable(model, x)
@constraint(model, con, 2x + 3x <= 2)
set_coefficient(con, x, 4)
set_standard_form_coefficient(con, x, 4)
con
# output
con : 4 x <= 2.0
```
"""
function set_coefficient(constraint::ConstraintRef{Model, _MOICON{F, S}},
variable, value) where {S, T, F <: Union{
MOI.ScalarAffineFunction{T},
MOI.ScalarQuadraticFunction{T}}}
MOI.modify(backend(constraint.model), index(constraint),
MOI.ScalarCoefficientChange(index(variable), convert(T, value)))
function set_standard_form_coefficient(
constraint::ConstraintRef{Model, _MOICON{F, S}}, variable, value
) where {S, T, F <: Union{MOI.ScalarAffineFunction{T}, MOI.ScalarQuadraticFunction{T}}}
MOI.modify(backend(owner_model(constraint)), index(constraint),
MOI.ScalarCoefficientChange(index(variable), convert(T, value)))
return
end
@deprecate set_coefficient set_standard_form_coefficient

"""
standard_form_coefficient(constraint::ConstraintRef, variable::VariableRef)
Return the coefficient associated with `variable` in `constraint` after JuMP has
normalized the constraint into its standard form. See also
[`set_standard_form_coefficient`](@ref).
"""
function standard_form_coefficient(
constraint::ConstraintRef{Model, _MOICON{F, S}}, variable
) where {S, T, F <: Union{MOI.ScalarAffineFunction{T}, MOI.ScalarQuadraticFunction{T}}}
con = JuMP.constraint_object(constraint)
return _affine_coefficient(con.func, variable)
end

"""
set_standard_form_rhs(constraint::ConstraintRef, value)
Set the right-hand side term of `constraint` to `value`.
Note that prior to this step, JuMP will aggregate all constant terms onto the
right-hand side of the constraint. For example, given a constraint `2x + 1 <=
2`, `set_standard_form_rhs(c, 4)` will create the constraint `2x <= 4`, not `2x +
1 <= 4`.
```jldoctest; setup = :(using JuMP; model = Model(); @variable(model, x)), filter=r"≤|<="
julia> @constraint(model, con, 2x + 1 <= 2)
con : 2 x <= 1.0
julia> set_standard_form_rhs(con, 4)
julia> con
con : 2 x <= 4.0
```
"""
function set_standard_form_rhs(
constraint::ConstraintRef{Model, _MOICON{F, S}}, value) where {
T,
S <: Union{MOI.LessThan{T}, MOI.GreaterThan{T}, MOI.EqualTo{T}},
F <: Union{MOI.ScalarAffineFunction{T}, MOI.ScalarQuadraticFunction{T}}}
MOI.set(owner_model(constraint), MOI.ConstraintSet(), constraint,
S(convert(T, value)))
return
end

"""
standard_form_rhs(constraint::ConstraintRef)
Return the right-hand side term of `constraint` after JuMP has converted the
constraint into its standard form. See also [`set_standard_form_rhs`](@ref).
"""
function standard_form_rhs(
constraint::ConstraintRef{Model, _MOICON{F, S}}) where {
T,
S <: Union{MOI.LessThan{T}, MOI.GreaterThan{T}, MOI.EqualTo{T}},
F <: Union{MOI.ScalarAffineFunction{T}, MOI.ScalarQuadraticFunction{T}}}
con = constraint_object(constraint)
return MOIU.getconstant(con.set)
end

"""
value(cref::ConstraintRef)
Expand Down
4 changes: 4 additions & 0 deletions src/quad_expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ function map_coefficients(f::Function, q::GenericQuadExpr)
return map_coefficients_inplace!(f, copy(q))
end

function _affine_coefficient(f::GenericQuadExpr{C, V}, variable::V) where {C, V}
return _affine_coefficient(f.aff, variable)
end

"""
constant(aff::GenericQuadExpr{C, V})::C
Expand Down
34 changes: 26 additions & 8 deletions test/constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -468,14 +468,32 @@ end
model = JuMP.Model()
x = @variable(model)
con_ref = @constraint(model, 2 * x == -1)
con_obj = JuMP.constraint_object(con_ref)
@test con_obj.func == 2 * x
JuMP.set_coefficient(con_ref, x, 1.0)
con_obj = JuMP.constraint_object(con_ref)
@test con_obj.func == 1 * x
JuMP.set_coefficient(con_ref, x, 3) # Check type promotion.
con_obj = JuMP.constraint_object(con_ref)
@test con_obj.func == 3 * x
@test JuMP.standard_form_coefficient(con_ref, x) == 2.0
JuMP.set_standard_form_coefficient(con_ref, x, 1.0)
@test JuMP.standard_form_coefficient(con_ref, x) == 1.0
JuMP.set_standard_form_coefficient(con_ref, x, 3) # Check type promotion.
@test JuMP.standard_form_coefficient(con_ref, x) == 3.0
quad_con = @constraint(model, x^2 == 0)
@test JuMP.standard_form_coefficient(quad_con, x) == 0.0
JuMP.set_standard_form_coefficient(quad_con, x, 2)
@test JuMP.standard_form_coefficient(quad_con, x) == 2.0
@test JuMP.isequal_canonical(
JuMP.constraint_object(quad_con).func, x^2 + 2x)
end

@testset "Change rhs" begin
model = JuMP.Model()
x = @variable(model)
con_ref = @constraint(model, 2 * x <= 1)
@test JuMP.standard_form_rhs(con_ref) == 1.0
JuMP.set_standard_form_rhs(con_ref, 2.0)
@test JuMP.standard_form_rhs(con_ref) == 2.0
con_ref = @constraint(model, 2 * x - 1 == 1)
@test JuMP.standard_form_rhs(con_ref) == 2.0
JuMP.set_standard_form_rhs(con_ref, 3)
@test JuMP.standard_form_rhs(con_ref) == 3.0
con_ref = @constraint(model, 0 <= 2 * x <= 1)
@test_throws MethodError JuMP.set_standard_form_rhs(con_ref, 3)
end
end

Expand Down

0 comments on commit 665f3a7

Please sign in to comment.