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
40 changes: 40 additions & 0 deletions src/Utilities/matrix_of_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,23 @@ and [`MOI.VectorConstantChange`](@ref) for [`MatrixOfConstraints`](@ref).
"""
function modify_constants end

"""
modify_coefficients(
coefficients,
row::Integer,
col::Integer,
new_coefficient,
)::Bool

Modify `coefficients` in-place to store `new_coefficient` at position
`(row, col)`. Return `true` if the entry existed and was modified, and `false`
if no entry exists at `(row, col)` in the sparse structure and `new_coefficient` is nonzero.

This function must be implemented to enable
[`MOI.ScalarCoefficientChange`](@ref) for [`MatrixOfConstraints`](@ref).
"""
function modify_coefficients end

###
### Interface for the .sets field
###
Expand Down Expand Up @@ -698,6 +715,29 @@ function MOI.modify(
return
end

function MOI.modify(
model::MatrixOfConstraints,
ci::MOI.ConstraintIndex,
change::MOI.ScalarCoefficientChange,
)
if !modify_coefficients(
model.coefficients,
rows(model, ci),
change.variable.value,
change.new_coefficient,
)
throw(
MOI.ModifyConstraintNotAllowed(
ci,
change,
"cannot set a new non-zero coefficient because no entry " *
"exists in the sparse matrix of `MatrixOfConstraints`",
),
)
end
return
end

function modify_constants(
b::AbstractVector{T},
row::Integer,
Expand Down
27 changes: 27 additions & 0 deletions src/Utilities/sparse_matrix.jl
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,22 @@ function load_terms(
return
end

function modify_coefficients(
A::Union{MutableSparseMatrixCSC{Tv},SparseArrays.SparseMatrixCSC{Tv}},
row::Integer,
col::Integer,
new_coefficient::Tv,
) where {Tv}
idx = _first_in_column(A, row, col)
range = SparseArrays.nzrange(A, col)
r = _shift(row, OneBasedIndexing(), _indexing(A))
if idx <= last(range) && A.rowval[idx] == r
A.nzval[idx] = new_coefficient
return true
end
return iszero(new_coefficient)
end

"""
Base.convert(
::Type{SparseMatrixCSC{Tv,Ti}},
Expand Down Expand Up @@ -189,6 +205,17 @@ _indexing(A::MutableSparseMatrixCSC) = A.indexing

_indexing(::SparseArrays.SparseMatrixCSC) = OneBasedIndexing()

"""
_first_in_column(
A::Union{MutableSparseMatrixCSC,SparseArrays.SparseMatrixCSC},
row::Integer,
col::Integer,
)

Return the index of the first non-zero entry in the column `col` that has a row
index greater than or equal to `row`.
If no such entry exists, return `last(SparseArrays.nzrange(A, col)) + 1`.
"""
function _first_in_column(
A::Union{MutableSparseMatrixCSC,SparseArrays.SparseMatrixCSC},
row::Integer,
Expand Down
106 changes: 106 additions & 0 deletions test/Utilities/test_matrix_of_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,112 @@ function test_set_types_fallback()
return
end

function test_modify_scalar_coefficient_change()
model = MOI.Utilities.GenericOptimizer{
Int,
MOI.Utilities.ObjectiveContainer{Int},
MOI.Utilities.VariablesContainer{Int},
MOI.Utilities.MatrixOfConstraints{
Int,
MOI.Utilities.MutableSparseMatrixCSC{
Int,
Int,
MOI.Utilities.OneBasedIndexing,
},
MOI.Utilities.Hyperrectangle{Int},
ScalarSets{Int},
},
}()
x = MOI.add_variable(model)
y = MOI.add_variable(model)
func = 2x + 3y
set = MOI.EqualTo(1)
c = MOI.add_constraint(model, func, set)
MOI.Utilities.final_touch(model, nothing)
f = MOI.get(model, MOI.ConstraintFunction(), c)
@test f ≈ 2x + 3y
MOI.modify(model, c, MOI.ScalarCoefficientChange(x, 5))
f = MOI.get(model, MOI.ConstraintFunction(), c)
@test f ≈ 5x + 3y
MOI.modify(model, c, MOI.ScalarCoefficientChange(y, 0))
f = MOI.get(model, MOI.ConstraintFunction(), c)
@test f ≈ 5x + 0y
return
end

function test_modify_scalar_coefficient_change_zero_based()
model = MOI.Utilities.GenericOptimizer{
Float64,
MOI.Utilities.ObjectiveContainer{Float64},
MOI.Utilities.VariablesContainer{Float64},
MOI.Utilities.MatrixOfConstraints{
Float64,
MOI.Utilities.MutableSparseMatrixCSC{
Float64,
Int,
MOI.Utilities.ZeroBasedIndexing,
},
MOI.Utilities.Hyperrectangle{Float64},
ScalarSets{Float64},
},
}()
src = MOI.Utilities.Model{Float64}()
MOI.Utilities.loadfromstring!(
src,
"""
variables: x, y
minobjective: x + y
c: x + 2.0 * y <= 3.0
""",
)
index_map = MOI.copy_to(model, src)
c = MOI.get(model, MOI.ConstraintIndex, "c")
x = index_map[MOI.get(src, MOI.VariableIndex, "x")]
y = index_map[MOI.get(src, MOI.VariableIndex, "y")]
f = MOI.get(model, MOI.ConstraintFunction(), c)
@test f ≈ 1.0x + 2.0y
MOI.modify(model, c, MOI.ScalarCoefficientChange(x, 4.0))
f = MOI.get(model, MOI.ConstraintFunction(), c)
@test f ≈ 4.0x + 2.0y
return
end

function test_modify_scalar_coefficient_change_no_entry()
model = MOI.Utilities.GenericOptimizer{
Int,
MOI.Utilities.ObjectiveContainer{Int},
MOI.Utilities.VariablesContainer{Int},
MOI.Utilities.MatrixOfConstraints{
Int,
MOI.Utilities.MutableSparseMatrixCSC{
Int,
Int,
MOI.Utilities.OneBasedIndexing,
},
MOI.Utilities.Hyperrectangle{Int},
ScalarSets{Int},
},
}()
x = MOI.add_variable(model)
y = MOI.add_variable(model)
func = 2x
set = MOI.EqualTo(1)
c = MOI.add_constraint(model, func, set)
MOI.Utilities.final_touch(model, nothing)
MOI.modify(model, c, MOI.ScalarCoefficientChange(y, 0))
change = MOI.ScalarCoefficientChange(y, 3)
@test_throws(
MOI.ModifyConstraintNotAllowed(
c,
change,
"cannot set a new non-zero coefficient because no entry " *
"exists in the sparse matrix of `MatrixOfConstraints`",
),
MOI.modify(model, c, change),
)
return
end

function test_modify_vectorsets()
model = _new_VectorSets()
src = MOI.Utilities.Model{Int}()
Expand Down
Loading