diff --git a/src/constraints.jl b/src/constraints.jl index b5176857c5e..110fb200788 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -731,7 +731,7 @@ function add_constraint( end """ - set_normalized_rhs(constraint::ConstraintRef, value) + set_normalized_rhs(constraint::ConstraintRef, value::Number) Set the right-hand side term of `constraint` to `value`. @@ -758,7 +758,7 @@ con : 2 x ≤ 4 """ function set_normalized_rhs( con_ref::ConstraintRef{<:AbstractModel,MOI.ConstraintIndex{F,S}}, - value, + value::Number, ) where { T, S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, @@ -773,6 +773,60 @@ function set_normalized_rhs( return end +""" + set_normalized_rhs( + constraints::AbstractVector{<:ConstraintRef}, + values::AbstractVector{<:Number} + ) + +Set the right-hand side terms of all `constraints` to `values`. + +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_normalized_rhs([con], [4])` will create the constraint `2x <= 4`, not `2x + +1 <= 4`. + +## Example + +```jldoctest; filter=r"≤|<=" +julia> model = Model(); + +julia> @variable(model, x); + +julia> @constraint(model, con1, 2x + 1 <= 2) +con1 : 2 x ≤ 1 + +julia> @constraint(model, con2, 3x + 2 <= 4) +con2 : 3 x ≤ 2 + +julia> set_normalized_rhs([con1, con2], [4, 5]) + +julia> con1 +con1 : 2 x ≤ 4 + +julia> con2 +con2 : 3 x ≤ 5 +``` +""" +function set_normalized_rhs( + constraints::AbstractVector{ + <:ConstraintRef{<:AbstractModel,MOI.ConstraintIndex{F,S}}, + }, + values::AbstractVector{<:Number}, +) where { + T, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, + F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}, +} + MOI.set( + backend(owner_model(first(constraints))), + MOI.ConstraintSet(), + index.(constraints), + S.(convert.(T, values)), + ) + return +end + """ normalized_rhs(constraint::ConstraintRef) diff --git a/src/objective.jl b/src/objective.jl index 2992557d81f..f75b645a66b 100644 --- a/src/objective.jl +++ b/src/objective.jl @@ -450,7 +450,10 @@ function set_objective_coefficient( coeff::Real, ) where {T} if _nlp_objective_function(model) !== nothing - error("A nonlinear objective is already set in the model") + error( + "A nonlinear objective created by the legacy `@NLobjective` is " * + "set in the model. This does not support modification.", + ) end coeff_t = convert(T, coeff)::T F = objective_function_type(model) @@ -491,11 +494,92 @@ function _set_objective_coefficient( return end +""" + set_objective_coefficient( + model::GenericModel, + variables::Vector{<:GenericVariableRef}, + coefficients::Vector{<:Real}, + ) + +Set multiple linear objective coefficients associated with `variables` to +`coefficients`, in a single call. + +Note: this function will throw an error if a nonlinear objective is set. + +## Example + +```jldoctest +julia> model = Model(); + +julia> @variable(model, x); + +julia> @variable(model, y); + +julia> @objective(model, Min, 3x + 2y + 1) +3 x + 2 y + 1 + +julia> set_objective_coefficient(model, [x, y], [5, 4]) + +julia> objective_function(model) +5 x + 4 y + 1 +``` +""" +function set_objective_coefficient( + model::GenericModel{T}, + variables::AbstractVector{<:GenericVariableRef{T}}, + coeffs::AbstractVector{<:Real}, +) where {T} + if _nlp_objective_function(model) !== nothing + error( + "A nonlinear objective created by the legacy `@NLobjective` is " * + "set in the model. This does not support modification.", + ) + end + n, m = length(variables), length(coeffs) + if !(n == m) + msg = "The number of variables ($n) and coefficients ($m) must match" + throw(DimensionMismatch(msg)) + end + F = objective_function_type(model) + _set_objective_coefficient(model, variables, convert.(T, coeffs), F) + model.is_model_dirty = true + return +end + +function _set_objective_coefficient( + model::GenericModel{T}, + variables::AbstractVector{<:GenericVariableRef{T}}, + coeffs::AbstractVector{<:T}, + ::Type{GenericVariableRef{T}}, +) where {T} + new_objective = LinearAlgebra.dot(coeffs, variables) + current_obj = objective_function(model)::GenericVariableRef{T} + if !(current_obj in variables) + add_to_expression!(new_objective, current_obj) + end + set_objective_function(model, new_objective) + return +end + +function _set_objective_coefficient( + model::GenericModel{T}, + variables::AbstractVector{<:GenericVariableRef{T}}, + coeffs::AbstractVector{<:T}, + ::Type{F}, +) where {T,F} + MOI.modify( + backend(model), + MOI.ObjectiveFunction{moi_function_type(F)}(), + MOI.ScalarCoefficientChange.(index.(variables), coeffs), + ) + return +end + """ set_objective_coefficient( model::GenericModel{T}, variable_1::GenericVariableRef{T}, - variable_1::GenericVariableRef{T}, + variable_2::GenericVariableRef{T}, coefficient::Real, ) where {T} @@ -529,7 +613,10 @@ function set_objective_coefficient( coeff::Real, ) where {T} if _nlp_objective_function(model) !== nothing - error("A nonlinear objective is already set in the model") + error( + "A nonlinear objective created by the legacy `@NLobjective` is " * + "set in the model. This does not support modification.", + ) end coeff_t = convert(T, coeff)::T F = moi_function_type(objective_function_type(model)) @@ -572,3 +659,96 @@ function _set_objective_coefficient( ) return end + +""" + set_objective_coefficient( + model::GenericModel{T}, + variables_1::AbstractVector{<:GenericVariableRef{T}}, + variables_2::AbstractVector{<:GenericVariableRef{T}}, + coefficients::AbstractVector{<:Real}, + ) where {T} + +Set multiple quadratic objective coefficients associated with `variables_1` and +`variables_2` to `coefficients`, in a single call. + +Note: this function will throw an error if a nonlinear objective is set. + +## Example + +```jldoctest +julia> model = Model(); + +julia> @variable(model, x[1:2]); + +julia> @objective(model, Min, x[1]^2 + x[1] * x[2]) +x[1]² + x[1]*x[2] + +julia> set_objective_coefficient(model, [x[1], x[1]], [x[1], x[2]], [2, 3]) + +julia> objective_function(model) +2 x[1]² + 3 x[1]*x[2] +``` +""" +function set_objective_coefficient( + model::GenericModel{T}, + variables_1::AbstractVector{<:GenericVariableRef{T}}, + variables_2::AbstractVector{<:GenericVariableRef{T}}, + coeffs::AbstractVector{<:Real}, +) where {T} + if _nlp_objective_function(model) !== nothing + error( + "A nonlinear objective created by the legacy `@NLobjective` is " * + "set in the model. This does not support modification.", + ) + end + n1, n2, m = length(variables_1), length(variables_2), length(coeffs) + if !(n1 == n2 == m) + msg = "The number of variables ($n1, $n2) and coefficients ($m) must match" + throw(DimensionMismatch(msg)) + end + coeffs_t = convert.(T, coeffs) + F = moi_function_type(objective_function_type(model)) + _set_objective_coefficient(model, variables_1, variables_2, coeffs_t, F) + model.is_model_dirty = true + return +end + +function _set_objective_coefficient( + model::GenericModel{T}, + variables_1::AbstractVector{<:V}, + variables_2::AbstractVector{<:V}, + coeffs::AbstractVector{<:T}, + ::Type{F}, +) where {T,F,V<:GenericVariableRef{T}} + new_obj = GenericQuadExpr{T,V}() + add_to_expression!(new_obj, objective_function(model)) + for (c, x, y) in zip(coeffs, variables_1, variables_2) + add_to_expression!(new_obj, c, x, y) + end + set_objective_function(model, new_obj) + return +end + +function _set_objective_coefficient( + model::GenericModel{T}, + variables_1::AbstractVector{<:GenericVariableRef{T}}, + variables_2::AbstractVector{<:GenericVariableRef{T}}, + coeffs::AbstractVector{<:T}, + ::Type{MOI.ScalarQuadraticFunction{T}}, +) where {T} + for (i, x, y) in zip(eachindex(coeffs), variables_1, variables_2) + if x == y + coeffs[i] *= T(2) + end + end + MOI.modify( + backend(model), + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}(), + MOI.ScalarQuadraticCoefficientChange.( + index.(variables_1), + index.(variables_2), + coeffs, + ), + ) + return +end diff --git a/src/variables.jl b/src/variables.jl index 64e96907e8a..daa7003ff89 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -2525,7 +2525,7 @@ end set_normalized_coefficient( constraint::ConstraintRef, variable::GenericVariableRef, - value, + value::Number, ) Set the coefficient of `variable` in the constraint `constraint` to `value`. @@ -2566,6 +2566,62 @@ function set_normalized_coefficient( return end +""" + set_normalized_coefficient( + constraints::AbstractVector{<:ConstraintRef}, + variables::AbstractVector{<:GenericVariableRef}, + values::AbstractVector{<:Number}, + ) + +Set multiple coefficient of `variables` in the constraints `constraints` to +`values`. + +Note that prior to this step, JuMP will aggregate multiple terms containing the +same variable. For example, given a constraint `2x + 3x <= 2`, +`set_normalized_coefficient(con, [x], [4])` will create the constraint `4x <= 2`. + +## Example + +```jldoctest; filter=r"≤|<=" +julia> model = Model(); + +julia> @variable(model, x) +x + +julia> @variable(model, y) +y + +julia> @constraint(model, con, 2x + 3x + 4y <= 2) +con : 5 x + 4 y ≤ 2 + +julia> set_normalized_coefficient([con, con], [x, y], [6, 7]) + +julia> con +con : 6 x + 7 y ≤ 2 +``` +""" +function set_normalized_coefficient( + constraints::AbstractVector{ + <:ConstraintRef{<:AbstractModel,<:MOI.ConstraintIndex{F}}, + }, + variables::AbstractVector{<:AbstractVariableRef}, + coeffs::AbstractVector{<:Number}, +) where {T,F<:Union{MOI.ScalarAffineFunction{T},MOI.ScalarQuadraticFunction{T}}} + c, n, m = length(constraints), length(variables), length(coeffs) + if !(c == n == m) + msg = "The number of constraints ($c), variables ($n) and coefficients ($m) must match" + throw(DimensionMismatch(msg)) + end + model = owner_model(first(constraints)) + MOI.modify( + backend(model), + index.(constraints), + MOI.ScalarCoefficientChange.(index.(variables), convert.(T, coeffs)), + ) + model.is_model_dirty = true + return +end + """ set_normalized_coefficients( con_ref::ConstraintRef, @@ -2669,6 +2725,72 @@ function set_normalized_coefficient( return end +""" + set_normalized_coefficient( + constraints::AbstractVector{<:ConstraintRef}, + variables_1:AbstractVector{<:GenericVariableRef}, + variables_2:AbstractVector{<:GenericVariableRef}, + values::AbstractVector{<:Number}, + ) + +Set multiple quadratic coefficients associated with `variables_1` and +`variables_2` in the constraints `constraints` to `values`. + +Note that prior to this step, JuMP will aggregate multiple terms containing the +same variable. For example, given a constraint `2x^2 + 3x^2 <= 2`, +`set_normalized_coefficient(con, [x], [x], [4])` will create the constraint +`4x^2 <= 2`. + +## Example + +```jldoctest; filter=r"≤|<=" +julia> model = Model(); + +julia> @variable(model, x[1:2]); + +julia> @constraint(model, con, 2x[1]^2 + 3 * x[1] * x[2] + x[2] <= 2) +con : 2 x[1]² + 3 x[1]*x[2] + x[2] ≤ 2 + +julia> set_normalized_coefficient([con, con], [x[1], x[1]], [x[1], x[2]], [4, 5]) + +julia> con +con : 4 x[1]² + 5 x[1]*x[2] + x[2] ≤ 2 +``` +""" +function set_normalized_coefficient( + constraints::AbstractVector{ + <:ConstraintRef{<:AbstractModel,<:MOI.ConstraintIndex{F}}, + }, + variables_1::AbstractVector{<:AbstractVariableRef}, + variables_2::AbstractVector{<:AbstractVariableRef}, + coeffs::AbstractVector{<:Number}, +) where {T,F<:MOI.ScalarQuadraticFunction{T}} + c, m = length(constraints), length(coeffs) + n1, n2 = length(variables_1), length(variables_1) + if !(c == n1 == n2 == m) + msg = "The number of constraints ($c), variables ($n1, $n2) and coefficients ($m) must match" + throw(DimensionMismatch(msg)) + end + new_coeffs = convert.(T, coeffs) + for (i, x, y) in zip(eachindex(new_coeffs), variables_1, variables_2) + if x == y + new_coeffs[i] *= T(2) + end + end + model = owner_model(first(constraints)) + MOI.modify( + backend(model), + index.(constraints), + MOI.ScalarQuadraticCoefficientChange.( + index.(variables_1), + index.(variables_2), + new_coeffs, + ), + ) + model.is_model_dirty = true + return +end + """ normalized_coefficient( constraint::ConstraintRef, diff --git a/test/test_constraint.jl b/test/test_constraint.jl index 36246e52d95..08c89342fbc 100644 --- a/test/test_constraint.jl +++ b/test/test_constraint.jl @@ -980,7 +980,35 @@ function test_change_coefficient() return end -function test_change_coefficients() +function test_change_coefficient_batch() + model = Model() + x = @variable(model) + y = @variable(model) + con_ref = @constraint(model, 2 * x + 3 * y == -1) + @test normalized_coefficient(con_ref, x) == 2.0 + @test normalized_coefficient(con_ref, y) == 3.0 + set_normalized_coefficient([con_ref, con_ref], [x, y], [1.0, 4.0]) + @test normalized_coefficient(con_ref, x) == 1.0 + @test normalized_coefficient(con_ref, y) == 4.0 + set_normalized_coefficient([con_ref, con_ref], [x, y], [3, 4]) # Check type promotion. + @test normalized_coefficient(con_ref, x) == 3.0 + @test normalized_coefficient(con_ref, y) == 4.0 + quad_con = @constraint(model, x^2 == 0) + @test normalized_coefficient(quad_con, x) == 0.0 + set_normalized_coefficient([quad_con, quad_con], [x, y], [2, 7]) + @test normalized_coefficient(quad_con, x) == 2.0 + @test normalized_coefficient(quad_con, y) == 7.0 + @test isequal_canonical(constraint_object(quad_con).func, x^2 + 2x + 7y) + @test_throws( + DimensionMismatch( + "The number of constraints (1), variables (2) and coefficients (2) must match", + ), + set_normalized_coefficient([con_ref], [x, y], [4, 5]), + ) + return +end + +function test_change_coefficients_vector_function() model = Model() @variable(model, x) @constraint(model, con, [2x + 3x, 4x] in MOI.Nonnegatives(2)) @@ -1008,6 +1036,29 @@ function test_change_rhs() return end +function test_change_rhs_batch() + model = Model() + x = @variable(model) + con_ref1 = @constraint(model, 2 * x <= 1) + con_ref2 = @constraint(model, 3 * x <= 2) + @test normalized_rhs(con_ref1) == 1.0 + @test normalized_rhs(con_ref2) == 2.0 + set_normalized_rhs([con_ref1, con_ref2], [3.0, 4.0]) + @test normalized_rhs(con_ref1) == 3.0 + @test normalized_rhs(con_ref2) == 4.0 + con_ref1 = @constraint(model, 2 * x - 1 == 1) + con_ref2 = @constraint(model, 2 * x - 1 == 2) + @test normalized_rhs(con_ref1) == 2.0 + @test normalized_rhs(con_ref2) == 3.0 + set_normalized_rhs([con_ref1, con_ref2], [3.0, 4.0]) + @test normalized_rhs(con_ref1) == 3.0 + @test normalized_rhs(con_ref2) == 4.0 + con_ref1 = @constraint(model, 0 >= 2 * x) + con_ref2 = @constraint(model, 2 * x <= 1) + @test_throws MethodError set_normalized_rhs([con_ref1, con_ref2], [3, 3]) + return +end + function test_add_to_function_constant_scalar() model = Model() x = @variable(model) @@ -1806,4 +1857,22 @@ function test_set_normalized_coefficient_quadratic() return end +function test_set_normalized_coefficient_quadratic_batch() + model = Model() + @variable(model, x[1:2]) + @constraint(model, con, 2x[1]^2 + 3 * x[1] * x[2] + x[2] <= 2) + @test normalized_coefficient(con, x[1], x[1]) == 2.0 + @test normalized_coefficient(con, x[1], x[2]) == 3.0 + set_normalized_coefficient([con, con], [x[1], x[1]], [x[1], x[2]], [4, 5]) + @test normalized_coefficient(con, x[1], x[1]) == 4.0 + @test normalized_coefficient(con, x[1], x[2]) == 5.0 + @test_throws( + DimensionMismatch( + "The number of constraints (1), variables (2, 2) and coefficients (2) must match", + ), + set_normalized_coefficient([con], [x[1], x[1]], [x[1], x[2]], [4, 5]), + ) + return +end + end # module diff --git a/test/test_objective.jl b/test/test_objective.jl index 3bf23fa2622..553b0865243 100644 --- a/test/test_objective.jl +++ b/test/test_objective.jl @@ -50,6 +50,66 @@ function test_objective_coef_update_linear_objective_changes() return end +function test_objective_coef_update_linear_objective_error() + model = Model() + @variable(model, x[1:2]) + @NLobjective(model, Min, x[1] * x[2]) + @test_throws( + ErrorException( + "A nonlinear objective created by the legacy `@NLobjective` is " * + "set in the model. This does not support modification.", + ), + set_objective_coefficient(model, x[1], 2), + ) + return +end + +function test_objective_coef_update_linear_objective_batch_error() + model = Model() + @variable(model, x[1:2]) + @NLobjective(model, Min, x[1] * x[2]) + @test_throws( + ErrorException( + "A nonlinear objective created by the legacy `@NLobjective` is " * + "set in the model. This does not support modification.", + ), + set_objective_coefficient(model, [x[1], x[2]], [2, 3]), + ) + return +end + +function test_objective_coef_update_linear_objective_batch_dimension_error() + model = Model() + @variable(model, x[1:2]) + @objective(model, Min, x[1] * x[2]) + @test_throws( + DimensionMismatch( + "The number of variables (2) and coefficients (1) must match", + ), + set_objective_coefficient(model, [x[1], x[2]], [2]), + ) + return +end + +function test_objective_coef_batch_update_linear_objective_changes() + model = Model() + @variable(model, x) + @variable(model, y) + @objective(model, Max, x) + set_objective_coefficient(model, [x, y], [4.0, 5.0]) + @test isequal_canonical(objective_function(model), 4x + 5y) + @objective(model, Max, x + y) + set_objective_coefficient(model, [x, y], [4.0, 5.0]) + @test isequal_canonical(objective_function(model), 4x + 5y) + @objective(model, Min, x) + set_objective_coefficient(model, [x], [2.0]) + @test isequal_canonical(objective_function(model), 2x) + @objective(model, Min, x) + set_objective_coefficient(model, [y], [2.0]) + @test isequal_canonical(objective_function(model), x + 2y) + return +end + function test_objective_coef_update_quadratic_objective_changes() model = Model() @variable(model, x) @@ -59,6 +119,16 @@ function test_objective_coef_update_quadratic_objective_changes() return end +function test_objective_coef_update_quadratic_objective_batch_changes() + model = Model() + @variable(model, x) + @variable(model, y) + @objective(model, Max, x^2 + x + y) + set_objective_coefficient(model, [x, y], [4.0, 5.0]) + @test isequal_canonical(objective_function(model), x^2 + 4x + 5y) + return +end + function test_extension_objective_sense_get_set( ModelType = Model, VariableType = VariableRef, @@ -235,17 +305,64 @@ function test_set_objective_coefficient_quadratic() return end +function test_set_objective_coefficient_quadratic_batch() + model = Model() + @variable(model, x[1:2]) + @objective(model, Min, x[1]^2 + x[1] * x[2] + x[1] + 2) + set_objective_coefficient(model, [x[1], x[1]], [x[1], x[2]], [2, 3]) + @test isequal_canonical( + objective_function(model), + 2 * x[1]^2 + 3 * x[1] * x[2] + x[1] + 2, + ) + set_objective_coefficient(model, [x[2]], [x[1]], [4]) + @test isequal_canonical( + objective_function(model), + 2 * x[1]^2 + 4 * x[1] * x[2] + x[1] + 2, + ) + return +end + function test_set_objective_coefficient_quadratic_error() model = Model() @variable(model, x[1:2]) @NLobjective(model, Min, x[1] * x[2]) @test_throws( - ErrorException("A nonlinear objective is already set in the model"), + ErrorException( + "A nonlinear objective created by the legacy `@NLobjective` is " * + "set in the model. This does not support modification.", + ), set_objective_coefficient(model, x[1], x[1], 2), ) return end +function test_set_objective_coefficient_quadratic_batch_error() + model = Model() + @variable(model, x[1:2]) + @NLobjective(model, Min, x[1] * x[2]) + @test_throws( + ErrorException( + "A nonlinear objective created by the legacy `@NLobjective` is " * + "set in the model. This does not support modification.", + ), + set_objective_coefficient(model, [x[1]], [x[1]], [2]), + ) + return +end + +function test_set_objective_coefficient_quadratic_batch_dimension_error() + model = Model() + @variable(model, x[1:2]) + @objective(model, Min, x[1] * x[2]) + @test_throws( + DimensionMismatch( + "The number of variables (1, 1) and coefficients (2) must match", + ), + set_objective_coefficient(model, [x[1]], [x[1]], [2, 3]), + ) + return +end + function test_set_objective_coefficient_quadratic_affine_original() model = Model() @variable(model, x[1:2]) @@ -259,6 +376,18 @@ function test_set_objective_coefficient_quadratic_affine_original() return end +function test_set_objective_coefficient_quadratic_batch_affine_original() + model = Model() + @variable(model, x[1:2]) + @objective(model, Min, x[1] + 2) + set_objective_coefficient(model, [x[1], x[1]], [x[1], x[2]], [2, 3]) + @test isequal_canonical( + objective_function(model), + 2 * x[1]^2 + 3 * x[1] * x[2] + x[1] + 2, + ) + return +end + function test_set_objective_coefficient_quadratic_variable_original() model = Model() @variable(model, x[1:2]) @@ -272,6 +401,18 @@ function test_set_objective_coefficient_quadratic_variable_original() return end +function test_set_objective_coefficient_quadratic_batch_variable_original() + model = Model() + @variable(model, x[1:2]) + @objective(model, Min, x[1]) + set_objective_coefficient(model, [x[1], x[1]], [x[1], x[2]], [2, 3]) + @test isequal_canonical( + objective_function(model), + 2 * x[1]^2 + 3 * x[1] * x[2] + x[1], + ) + return +end + function test_set_objective_coefficient_quadratic_nothing_set() model = Model() @variable(model, x[1:2]) @@ -284,4 +425,15 @@ function test_set_objective_coefficient_quadratic_nothing_set() return end +function test_set_objective_coefficient_quadratic_batch_nothing_set() + model = Model() + @variable(model, x[1:2]) + set_objective_coefficient(model, [x[1], x[1]], [x[1], x[2]], [2, 3]) + @test isequal_canonical( + objective_function(model), + 2 * x[1]^2 + 3 * x[1] * x[2], + ) + return +end + end # module