Skip to content
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
38 changes: 38 additions & 0 deletions docs/src/manual/modification.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,44 @@ true
[`ScalarCoefficientChange`](@ref) can also be used to modify the objective
function by passing an instance of [`ObjectiveFunction`](@ref).

## Modify quadratic coefficients in a scalar function

Use [`modify`](@ref) and [`ScalarQuadraticCoefficientChange`](@ref) to modify
the quadratic coefficient of a [`ScalarQuadraticFunction`](@ref).

```jldoctest
julia> model = MOI.Utilities.Model{Float64}();

julia> x = MOI.add_variables(model, 2);

julia> c = MOI.add_constraint(
model,
1.0 * x[1] * x[1] + 2.0 * x[1] * x[2],
MOI.EqualTo(1.0),
)
MathOptInterface.ConstraintIndex{MathOptInterface.ScalarQuadraticFunction{Float64}, MathOptInterface.EqualTo{Float64}}(1)

julia> MOI.modify(
model,
c,
MOI.ScalarQuadraticCoefficientChange(x[1], x[1], 3.0),
);

julia> MOI.modify(
model,
c,
MOI.ScalarQuadraticCoefficientChange(x[1], x[2], 4.0),
);

julia> new_f = 1.5 * x[1] * x[1] + 4.0 * x[1] * x[2];

julia> MOI.get(model, MOI.ConstraintFunction(), c) ≈ new_f
true
```

[`ScalarQuadraticCoefficientChange`](@ref) can also be used to modify the
objective function by passing an instance of [`ObjectiveFunction`](@ref).

## Modify affine coefficients in a vector function

Use [`modify`](@ref) and [`MultirowChange`](@ref) to modify a vector of affine
Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/modification.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ AbstractFunctionModification
ScalarConstantChange
VectorConstantChange
ScalarCoefficientChange
ScalarQuadraticCoefficientChange
MultirowChange
```
1 change: 1 addition & 0 deletions docs/src/tutorials/implementing.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ the following `AbstractModification`s:

* [`ScalarConstantChange`](@ref)
* [`ScalarCoefficientChange`](@ref)
* [`ScalarQuadraticCoefficientChange`](@ref)
* [`VectorConstantChange`](@ref)
* [`MultirowChange`](@ref)

Expand Down
31 changes: 29 additions & 2 deletions src/Bridges/bridge_optimizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1160,7 +1160,11 @@ function _modify_bridged_function(
change::MOI.AbstractFunctionModification,
)
if is_bridged(b, ci_or_obj)
MOI.modify(recursive_model(b), bridge(b, ci_or_obj), change)
try
MOI.modify(recursive_model(b), bridge(b, ci_or_obj), change)
catch
MOI.throw_modify_not_allowed(ci_or_obj, change)
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blegat do you have any ideas for this? We can't add a generic fallback for the ::Bridge case of the second argument because then we don't know whether we're changing a constraint or an objective.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean. I would add a fallback in Bridges/bridge.jl. If you want to differentiate, you have Bridges.Objective.AbstractBridge and Bridges.Constraint.AbstractBridge.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But how do we know which ConstraintIndex it refers to?

Copy link
Member

@blegat blegat Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I guess this is an annoying part of this error. This is also why you had to do a try-catch in #2300.
I think we should allow throwing this error without knowing what is the constraint index is.
At the end of the day, after the index maps, this might not be the index of the user anyway.
Or we could allow specifying a bridge instead of an index but then we still need the try catch in #2300

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah a generic ModifyNotAllowed would be simpler. But that could be a separate PR after this is merged?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but then open an issue or leave a comment here to keep not of that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened an issue: #2306. I'll work on it after this PR is merged

else
MOI.modify(b.model, ci_or_obj, change)
end
Expand Down Expand Up @@ -1801,6 +1805,13 @@ function is_bridged(
return is_bridged(b, change.variable)
end

function is_bridged(
b::AbstractBridgeOptimizer,
change::MOI.ScalarQuadraticCoefficientChange,
)
return is_bridged(b, change.variable_1) || is_bridged(b, change.variable_2)
end

function modify_bridged_change(
b::AbstractBridgeOptimizer,
ci,
Expand Down Expand Up @@ -1870,6 +1881,18 @@ function modify_bridged_change(
return
end

function modify_bridged_change(
b::AbstractBridgeOptimizer,
ci_or_obj,
change::MOI.ScalarQuadraticCoefficientChange,
)
return MOI.throw_modify_not_allowed(
ci_or_obj,
change,
"Cannot bridge `ScalarQuadraticCoefficientChange`.",
)
end

function MOI.modify(
b::AbstractBridgeOptimizer,
ci::MOI.ConstraintIndex,
Expand All @@ -1879,7 +1902,11 @@ function MOI.modify(
modify_bridged_change(b, ci, change)
else
if is_bridged(b, ci)
call_in_context(MOI.modify, b, ci, change)
try
call_in_context(MOI.modify, b, ci, change)
catch
MOI.throw_modify_not_allowed(ci, change)
end
else
MOI.modify(b.model, ci, change)
end
Expand Down
41 changes: 41 additions & 0 deletions src/Test/test_modification.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1015,3 +1015,44 @@ function test_modification_incorrect_VariableIndex(
)
return
end

function test_modification_objective_scalarquadraticcoefficientchange(
model::MOI.ModelLike,
config::Config{T},
) where {T}
attr = MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}()
@requires MOI.supports(model, attr)
@requires _supports(config, MOI.modify)
@requires _supports(config, MOI.ScalarQuadraticCoefficientChange)
x = MOI.add_variable(model)
MOI.set(model, attr, T(1) * x * x + T(2) * x + T(3))
@test MOI.get(model, attr) ≈ T(1) * x * x + T(2) * x + T(3)
MOI.modify(model, attr, MOI.ScalarQuadraticCoefficientChange(x, x, T(4)))
@test MOI.get(model, attr) ≈ T(2) * x * x + T(2) * x + T(3)
y = MOI.add_variable(model)
MOI.set(model, attr, T(1) * x * x + T(2) * x * y)
@test MOI.get(model, attr) ≈ T(1) * x * x + T(2) * x * y
MOI.modify(model, attr, MOI.ScalarQuadraticCoefficientChange(x, y, T(4)))
@test MOI.get(model, attr) ≈ T(1) * x * x + T(4) * x * y
return
end

function test_modification_constraint_scalarquadraticcoefficientchange(
model::MOI.ModelLike,
config::Config{T},
) where {T}
F, S = MOI.ScalarQuadraticFunction{T}, MOI.LessThan{T}
@requires MOI.supports_constraint(model, F, S)
@requires _supports(config, MOI.modify)
@requires _supports(config, MOI.ScalarQuadraticCoefficientChange)
x = MOI.add_variable(model)
y = MOI.add_variable(model)
f = T(1) * x * x + T(1) * x * x - T(1) * x * y + T(2) * y * y - T(1) * x * y
c = MOI.add_constraint(model, f, MOI.LessThan(T(1)))
@test MOI.get(model, MOI.ConstraintFunction(), c) ≈ f
g = T(1) * x * x + T(-3) * x * y + T(2) * y * y
MOI.modify(model, c, MOI.ScalarQuadraticCoefficientChange(x, x, T(2)))
MOI.modify(model, c, MOI.ScalarQuadraticCoefficientChange(x, y, -T(3)))
@test MOI.get(model, MOI.ConstraintFunction(), c) ≈ g
return
end
31 changes: 31 additions & 0 deletions src/Utilities/functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,17 @@ function map_indices(
)
end

function map_indices(
index_map::F,
change::MOI.ScalarQuadraticCoefficientChange,
) where {F<:Function}
return MOI.ScalarQuadraticCoefficientChange(
index_map(change.variable_1),
index_map(change.variable_2),
change.new_coefficient,
)
end

function map_indices(
index_map::F,
change::MOI.MultirowChange,
Expand Down Expand Up @@ -1386,6 +1397,26 @@ function modify_function!(
return f
end

function modify_function!(
f::MOI.ScalarQuadraticFunction{T},
change::MOI.ScalarQuadraticCoefficientChange{T},
) where {T}
indices = findall(f.quadratic_terms) do term
return term.variable_1 == change.variable_1 &&
term.variable_2 == change.variable_2
end
for j in reverse(indices)
deleteat!(f.quadratic_terms, j)
end
term = MOI.ScalarQuadraticTerm(
change.new_coefficient,
change.variable_1,
change.variable_2,
)
push!(f.quadratic_terms, term)
return f
end

function _modify_coefficients(
terms::Vector{MOI.VectorAffineTerm{T}},
variable::MOI.VariableIndex,
Expand Down
33 changes: 33 additions & 0 deletions src/functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,39 @@ struct ScalarCoefficientChange{T} <: AbstractFunctionModification
new_coefficient::T
end

"""
ScalarQuadraticCoefficientChange{T}(
variable_1::VariableIndex,
variable_2::VariableIndex,
new_coefficient::T,
)

A struct used to request a change in the quadratic coefficient of a
[`ScalarQuadraticFunction`](@ref).

## Scaling factors

A [`ScalarQuadraticFunction`](@ref) has an implicit `0.5` scaling factor in
front of the `Q` matrix. This modification applies to terms in the `Q` matrix.

If `variable_1 == variable_2`, this modification sets the corresponding diagonal
element of the `Q` matrix to `new_coefficient`.

If `variable_1 != variable_2`, this modification is equivalent to setting both
the corresponding upper- and lower-triangular elements of the `Q` matrix to
`new_coefficient`.

As a consequence:

* to modify the term `x^2` to become `2x^2`, `new_coefficient` must be `4`
* to modify the term `xy` to become `2xy`, `new_coefficient` must be `2`
"""
struct ScalarQuadraticCoefficientChange{T} <: AbstractFunctionModification
variable_1::VariableIndex
variable_2::VariableIndex
new_coefficient::T
end

# !!! developer note
# MultiRowChange is mutable because its `variable` field of an immutable
# type, while `new_coefficients` is of a mutable type, meaning that creating
Expand Down
37 changes: 37 additions & 0 deletions test/Bridges/bridge_optimizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,43 @@ function test_deleting_all_variables_in_bridged_functionize_objective()
return
end

function test_modify_objective_scalar_quadratic_coefficient_change()
T = Float64
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}())
model = MOI.Bridges.Objective.Slack{T}(inner)
x = MOI.add_variable(model)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
attr = MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}()
MOI.set(model, attr, T(1) * x * x + T(2) * x + T(3))
change = MOI.ScalarQuadraticCoefficientChange(x, x, T(4))
@test_throws MOI.ModifyObjectiveNotAllowed MOI.modify(model, attr, change)
return
end

function test_modify_variable_scalar_quadratic_coefficient_change()
T = Float64
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}())
model = MOI.Bridges.Variable.Free{T}(inner)
x = MOI.add_variable(model)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
attr = MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}()
MOI.set(model, attr, T(1) * x * x + T(2) * x + T(3))
change = MOI.ScalarQuadraticCoefficientChange(x, x, T(4))
@test_throws MOI.ModifyObjectiveNotAllowed MOI.modify(model, attr, change)
return
end

function test_modify_constraint_scalar_quadratic_coefficient_change()
T = Float64
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}())
model = MOI.Bridges.Constraint.QuadtoSOC{T}(inner)
x = MOI.add_variable(model)
c = MOI.add_constraint(model, T(1) * x * x + T(2) * x, MOI.LessThan(T(1)))
change = MOI.ScalarQuadraticCoefficientChange(x, x, T(4))
@test_throws MOI.ModifyConstraintNotAllowed MOI.modify(model, c, change)
return
end

end # module

TestBridgeOptimizer.runtests()