From 665f3a703bd3d18341d8d263e23f55334ac7bec2 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 9 Apr 2019 10:48:37 -0500 Subject: [PATCH] Add standard_form modifications (#1935) * Add standard_form modifications * Fix MethodError in set_rhs * Fix doctests * Add getters for standard_form funcs * Fix tests and add more --- docs/src/constraints.md | 75 ++++++++++++++++++++++++++++++++------- src/aff_expr.jl | 4 +++ src/constraints.jl | 78 +++++++++++++++++++++++++++++++++++------ src/quad_expr.jl | 4 +++ test/constraint.jl | 34 +++++++++++++----- 5 files changed, 164 insertions(+), 31 deletions(-) diff --git a/docs/src/constraints.md b/docs/src/constraints.md index 2724fe96674..69f17e10c2a 100644 --- a/docs/src/constraints.md +++ b/docs/src/constraints.md @@ -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 @@ -563,11 +591,13 @@ 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 @@ -575,19 +605,35 @@ julia> fix(const_term, 1.0) ### 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 @@ -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 diff --git a/src/aff_expr.jl b/src/aff_expr.jl index 562349972bb..53fe7d91581 100644 --- a/src/aff_expr.jl +++ b/src/aff_expr.jl @@ -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) diff --git a/src/constraints.jl b/src/constraints.jl index 8c5415b325b..ecf22ee6bfe 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -395,20 +395,19 @@ 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 @@ -416,15 +415,74 @@ con 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) diff --git a/src/quad_expr.jl b/src/quad_expr.jl index fbdd3bc6ea4..cc7917cc10e 100644 --- a/src/quad_expr.jl +++ b/src/quad_expr.jl @@ -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 diff --git a/test/constraint.jl b/test/constraint.jl index 52a5c14b569..c7fc545f8f4 100644 --- a/test/constraint.jl +++ b/test/constraint.jl @@ -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