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

Add standard_form modifications #1935

Merged
merged 5 commits into from
Apr 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -92,6 +92,10 @@ Base.broadcastable(a::GenericAffExpr) = Ref(a)

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 @@ -74,6 +74,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