Skip to content

Commit

Permalink
Add support for modifying quadratic coefficients (#3658)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow committed Jan 29, 2024
1 parent d7fd827 commit 7cab7ff
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ JuMPDimensionalDataExt = "DimensionalData"
DimensionalData = "0.24"
LinearAlgebra = "<0.0.1, 1.6"
MacroTools = "0.5"
MathOptInterface = "1.19"
MathOptInterface = "1.25.2"
MutableArithmetics = "1.1"
OrderedCollections = "1"
Printf = "<0.0.1, 1.6"
Expand Down
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Ipopt = "=1.6.0"
JSON = "0.21"
JSONSchema = "1"
Literate = "2.8"
MathOptInterface = "=1.25.0"
MathOptInterface = "=1.25.2"
MultiObjectiveAlgorithms = "=1.3.2"
PATHSolver = "=1.7.1"
Plots = "1"
Expand Down
26 changes: 24 additions & 2 deletions docs/src/manual/constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -793,8 +793,7 @@ con : 2 x ∈ [-4, -2]

### Scalar constraints

To modify the coefficients for a linear term (modifying the coefficient of a
quadratic term is not supported) in a constraint, use
To modify the coefficients for a linear term in a constraint, use
[`set_normalized_coefficient`](@ref). To query the current coefficient, use
[`normalized_coefficient`](@ref).
```jldoctest
Expand All @@ -814,6 +813,29 @@ julia> normalized_coefficient(con, x[2])
0.0
```

To modify quadratic terms, pass two variables:
```jldoctest
julia> model = Model();
julia> @variable(model, x[1:2]);
julia> @constraint(model, con, x[1]^2 + x[1] * x[2] <= 1)
con : x[1]² + x[1]*x[2] ≤ 1
julia> set_normalized_coefficient(con, x[1], x[1], 2)
julia> set_normalized_coefficient(con, x[1], x[2], 3)
julia> con
con : 2 x[1]² + 3 x[1]*x[2] ≤ 1
julia> normalized_coefficient(con, x[1], x[1])
2.0
julia> normalized_coefficient(con, x[1], x[2])
3.0
```

!!! warning
[`set_normalized_coefficient`](@ref) sets the coefficient of the normalized
constraint. See [Normalization](@ref) for more details.
Expand Down
22 changes: 19 additions & 3 deletions docs/src/manual/objective.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,25 @@ julia> objective_function(model)
3 x
```

!!! info
There is no way to modify the coefficient of a quadratic term. Set a new
objective instead.
Use [`set_objective_coefficient`](@ref) with two variables to modify a quadratic
objective coefficient:
```jldoctest
julia> model = Model();
julia> @variable(model, x);
julia> @variable(model, y);
julia> @objective(model, Min, x^2 + x * y)
x² + x*y
julia> set_objective_coefficient(model, x, x, 2)
julia> set_objective_coefficient(model, x, y, 3)
julia> objective_function(model)
2 x² + 3 x*y
```

## Modify the objective sense

Expand Down
99 changes: 99 additions & 0 deletions src/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,105 @@ function normalized_coefficient(
return coefficient(constraint_object(con_ref).func, variable)
end

"""
set_normalized_coefficient(
constraint::ConstraintRef,
variable_1:GenericVariableRef,
variable_2:GenericVariableRef,
value,
)
Set the quadratic coefficient associated with `variable_1` and `variable_2` 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^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, x[1], x[1], 4)
julia> set_normalized_coefficient(con, x[1], x[2], 5)
julia> con
con : 4 x[1]² + 5 x[1]*x[2] + x[2] ≤ 2
```
"""
function set_normalized_coefficient(
constraint::ConstraintRef{<:AbstractModel,CI},
# TODO(odow): these are untyped becasue `constraints.jl` is loaded before
# variables.jl
variable_1,
variable_2,
value::Real,
) where {T,CI<:MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T}}}
new_value = convert(T, value)
if variable_1 == variable_2
new_value *= T(2)
end
model = owner_model(constraint)
MOI.modify(
backend(model),
index(constraint),
MOI.ScalarQuadraticCoefficientChange(
index(variable_1),
index(variable_2),
new_value,
),
)
model.is_model_dirty = true
return
end

"""
normalized_coefficient(
constraint::ConstraintRef,
variable_1::GenericVariableRef,
variable_2::GenericVariableRef,
)
Return the quadratic coefficient associated with `variable_1` and `variable_2`
in `constraint` after JuMP has normalized the constraint into its standard form.
See also [`set_normalized_coefficient`](@ref).
## 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> normalized_coefficient(con, x[1], x[1])
2.0
julia> normalized_coefficient(con, x[1], x[2])
3.0
```
"""
function normalized_coefficient(
connstraint::ConstraintRef{<:AbstractModel,CI},
# TODO(odow): these are untyped becasue `constraints.jl` is loaded before
# variables.jl
variable_1,
variable_2,
) where {T,CI<:MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T}}}
con = constraint_object(connstraint)
return coefficient(con.func, variable_1, variable_2)
end

"""
set_normalized_rhs(constraint::ConstraintRef, value)
Expand Down
106 changes: 104 additions & 2 deletions src/objective.jl
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,31 @@ function objective_function(model::GenericModel)
end

"""
set_objective_coefficient(model::GenericModel, variable::GenericVariableRef, coefficient::Real)
set_objective_coefficient(
model::GenericModel,
variable::GenericVariableRef,
coefficient::Real,
)
Set the linear objective coefficient associated with `Variable` to `coefficient`.
Set the linear objective coefficient associated with `variable` to `coefficient`.
Note: this function will throw an error if a nonlinear objective is set.
## Example
```jldoctest
julia> model = Model();
julia> @variable(model, x);
julia> @objective(model, Min, 2x + 1)
2 x + 1
julia> set_objective_coefficient(model, x, 3)
julia> objective_function(model)
3 x + 1
```
"""
function set_objective_coefficient(
model::GenericModel{T},
Expand Down Expand Up @@ -299,3 +319,85 @@ function _set_objective_coefficient(
)
return
end

"""
set_objective_coefficient(
model::GenericModel{T},
variable_1::GenericVariableRef{T},
variable_1::GenericVariableRef{T},
coefficient::Real,
) where {T}
Set the quadratic objective coefficient associated with `variable_1` and
`variable_2` to `coefficient`.
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], 2)
julia> set_objective_coefficient(model, x[1], x[2], 3)
julia> objective_function(model)
2 x[1]² + 3 x[1]*x[2]
```
"""
function set_objective_coefficient(
model::GenericModel{T},
variable_1::GenericVariableRef{T},
variable_2::GenericVariableRef{T},
coeff::Real,
) where {T}
if _nlp_objective_function(model) !== nothing
error("A nonlinear objective is already set in the model")
end
coeff_t = convert(T, coeff)::T
F = moi_function_type(objective_function_type(model))
_set_objective_coefficient(model, variable_1, variable_2, coeff_t, F)
model.is_model_dirty = true
return
end

function _set_objective_coefficient(
model::GenericModel{T},
variable_1::GenericVariableRef{T},
variable_2::GenericVariableRef{T},
coeff::T,
::Type{F},
) where {T,F}
current_obj = objective_function(model)
new_obj = add_to_expression!(coeff * variable_1 * variable_2, current_obj)
set_objective_function(model, new_obj)
return
end

function _set_objective_coefficient(
model::GenericModel{T},
variable_1::GenericVariableRef{T},
variable_2::GenericVariableRef{T},
coeff::T,
::Type{MOI.ScalarQuadraticFunction{T}},
) where {T}
if variable_1 == variable_2
coeff *= T(2)
end
MOI.modify(
backend(model),
MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}(),
MOI.ScalarQuadraticCoefficientChange(
index(variable_1),
index(variable_2),
coeff,
),
)
return
end
13 changes: 13 additions & 0 deletions test/test_constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1793,4 +1793,17 @@ function test_indicator_error()
return
end

function test_set_normalized_coefficient_quadratic()
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, x[1], x[1], 4)
set_normalized_coefficient(con, x[1], x[2], 5)
@test normalized_coefficient(con, x[1], x[1]) == 4.0
@test normalized_coefficient(con, x[1], x[2]) == 5.0
return
end

end # module
Loading

0 comments on commit 7cab7ff

Please sign in to comment.