From fe25f3999449d4d2464e9d6a6c91bfe21d81c144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 19 Mar 2026 10:23:26 +0100 Subject: [PATCH 1/4] Implement scalar coefficient change for MatrixOfConstraints --- src/Utilities/matrix_of_constraints.jl | 34 +++++++ src/Utilities/sparse_matrix.jl | 21 ++++ test/Utilities/test_matrix_of_constraints.jl | 100 +++++++++++++++++++ 3 files changed, 155 insertions(+) diff --git a/src/Utilities/matrix_of_constraints.jl b/src/Utilities/matrix_of_constraints.jl index c6268ab9c4..0c6b0715cb 100644 --- a/src/Utilities/matrix_of_constraints.jl +++ b/src/Utilities/matrix_of_constraints.jl @@ -246,6 +246,22 @@ and [`MOI.VectorConstantChange`](@ref) for [`MatrixOfConstraints`](@ref). """ function modify_constants end +""" + modify_coefficients( + coefficients, + row::Integer, + col::Integer, + new_coefficient, + ) + +Modify `coefficients` in-place to store `new_coefficient` at position +`(row, col)`. + +This function must be implemented to enable +[`MOI.ScalarCoefficientChange`](@ref) for [`MatrixOfConstraints`](@ref). +""" +function modify_coefficients end + ### ### Interface for the .sets field ### @@ -698,6 +714,24 @@ function MOI.modify( return end +function MOI.modify( + model::MatrixOfConstraints, + ci::MOI.ConstraintIndex, + change::MOI.ScalarCoefficientChange, +) + try + modify_coefficients( + model.coefficients, + rows(model, ci), + change.variable.value, + change.new_coefficient, + ) + catch + throw(MOI.ModifyConstraintNotAllowed(ci, change)) + end + return +end + function modify_constants( b::AbstractVector{T}, row::Integer, diff --git a/src/Utilities/sparse_matrix.jl b/src/Utilities/sparse_matrix.jl index fe33c363d7..1e588b0ceb 100644 --- a/src/Utilities/sparse_matrix.jl +++ b/src/Utilities/sparse_matrix.jl @@ -161,6 +161,27 @@ 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 + elseif !iszero(new_coefficient) + error( + "Cannot set a new non-zero coefficient at ($row, $col) because " * + "no entry exists in the sparse matrix. Adding new entries to a " * + "`MutableSparseMatrixCSC` after `final_touch` is not supported.", + ) + end + return +end + """ Base.convert( ::Type{SparseMatrixCSC{Tv,Ti}}, diff --git a/test/Utilities/test_matrix_of_constraints.jl b/test/Utilities/test_matrix_of_constraints.jl index 0f48a2c24b..ea965f824d 100644 --- a/test/Utilities/test_matrix_of_constraints.jl +++ b/test/Utilities/test_matrix_of_constraints.jl @@ -636,6 +636,106 @@ 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)) + @test_throws( + MOI.ModifyConstraintNotAllowed, + MOI.modify(model, c, MOI.ScalarCoefficientChange(y, 3)), + ) + return +end + function test_modify_vectorsets() model = _new_VectorSets() src = MOI.Utilities.Model{Int}() From 64d2db66cbc67328d2d6efd810db633c38b60701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 19 Mar 2026 10:34:10 +0100 Subject: [PATCH 2/4] Add docstring --- src/Utilities/sparse_matrix.jl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Utilities/sparse_matrix.jl b/src/Utilities/sparse_matrix.jl index 1e588b0ceb..2aa69aa1e3 100644 --- a/src/Utilities/sparse_matrix.jl +++ b/src/Utilities/sparse_matrix.jl @@ -210,6 +210,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, From 2c032a7a18a557cc860936d59f4bd680e9f6b39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 19 Mar 2026 10:57:27 +0100 Subject: [PATCH 3/4] Don't use try-catch --- src/Utilities/matrix_of_constraints.jl | 26 ++++++++++++++++---------- src/Utilities/sparse_matrix.jl | 9 ++------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Utilities/matrix_of_constraints.jl b/src/Utilities/matrix_of_constraints.jl index 0c6b0715cb..503b149110 100644 --- a/src/Utilities/matrix_of_constraints.jl +++ b/src/Utilities/matrix_of_constraints.jl @@ -252,10 +252,11 @@ function modify_constants end row::Integer, col::Integer, new_coefficient, - ) + )::Bool Modify `coefficients` in-place to store `new_coefficient` at position -`(row, col)`. +`(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). @@ -719,15 +720,20 @@ function MOI.modify( ci::MOI.ConstraintIndex, change::MOI.ScalarCoefficientChange, ) - try - modify_coefficients( - model.coefficients, - rows(model, ci), - change.variable.value, - change.new_coefficient, + 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`", + ), ) - catch - throw(MOI.ModifyConstraintNotAllowed(ci, change)) end return end diff --git a/src/Utilities/sparse_matrix.jl b/src/Utilities/sparse_matrix.jl index 2aa69aa1e3..45b831c03b 100644 --- a/src/Utilities/sparse_matrix.jl +++ b/src/Utilities/sparse_matrix.jl @@ -172,14 +172,9 @@ function modify_coefficients( r = _shift(row, OneBasedIndexing(), _indexing(A)) if idx <= last(range) && A.rowval[idx] == r A.nzval[idx] = new_coefficient - elseif !iszero(new_coefficient) - error( - "Cannot set a new non-zero coefficient at ($row, $col) because " * - "no entry exists in the sparse matrix. Adding new entries to a " * - "`MutableSparseMatrixCSC` after `final_touch` is not supported.", - ) + return true end - return + return iszero(new_coefficient) end """ From 93302f69d215bbf83cdc9e95a45b9b520ece2ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 19 Mar 2026 11:05:41 +0100 Subject: [PATCH 4/4] Check error better --- test/Utilities/test_matrix_of_constraints.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/Utilities/test_matrix_of_constraints.jl b/test/Utilities/test_matrix_of_constraints.jl index ea965f824d..256fb2a931 100644 --- a/test/Utilities/test_matrix_of_constraints.jl +++ b/test/Utilities/test_matrix_of_constraints.jl @@ -729,9 +729,15 @@ function test_modify_scalar_coefficient_change_no_entry() 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, - MOI.modify(model, c, MOI.ScalarCoefficientChange(y, 3)), + 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