From 30b859a66fb2ee10b8bee95a0a2d3a3a045ca7b7 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 18 Aug 2025 10:32:11 +1200 Subject: [PATCH 01/12] [Bridges] fix deleting variable with constraint bridged from scalar to vector --- src/Bridges/bridge_optimizer.jl | 17 +++++++++-------- test/Bridges/bridge_optimizer.jl | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index fff102820a..6eaaeb12cf 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -511,14 +511,6 @@ function _delete_variables_in_variables_constraints( vis::Vector{MOI.VariableIndex}, ) c_map = Constraint.bridges(b)::Constraint.Map - # Delete all `MOI.VectorOfVariables` constraints of these variables. - # We reverse for the same reason as for `VariableIndex` below. - # As the iterators are lazy, when the inner bridge constraint is deleted, - # it won't be part of the iteration. - for ci in - Iterators.reverse(Constraint.vector_of_variables_constraints(c_map)) - _delete_variables_in_vector_of_variables_constraint(b, vis, ci) - end # Delete all `MOI.VariableIndex` constraints of these variables. for vi in vis # If a bridged `VariableIndex` constraints creates a second one, @@ -532,6 +524,15 @@ function _delete_variables_in_variables_constraints( end end end + # Delete all `MOI.VectorOfVariables` constraints of these variables. + # We reverse for the same reason as for `VariableIndex` below. + # As the iterators are lazy, when the inner bridge constraint is deleted, + # it won't be part of the iteration. + for ci in + Iterators.reverse(Constraint.vector_of_variables_constraints(c_map)) + _delete_variables_in_vector_of_variables_constraint(b, vis, ci) + end + return end function _delete_variables_in_bridged_objective( diff --git a/test/Bridges/bridge_optimizer.jl b/test/Bridges/bridge_optimizer.jl index f42a12ee1e..b1a8620a2c 100644 --- a/test/Bridges/bridge_optimizer.jl +++ b/test/Bridges/bridge_optimizer.jl @@ -1463,6 +1463,35 @@ function test_BridgeRequiresFiniteDomainError() return end +MOI.Utilities.@model( + Model2817, + (), + (), + (MOI.Nonnegatives,), + (), + (), + (), + (), + (MOI.VectorAffineFunction,) +); + +function MOI.supports_constraint( + ::Model2817, + ::Type{MOI.VariableIndex}, + ::Type{S}, +) where {S<:MOI.AbstractScalarSet} + return false +end + +function test_issue_2817() + model = MOI.Bridges.full_bridge_optimizer(Model2817{Float64}(), Float64); + x, _ = MOI.add_constrained_variable(model, MOI.Interval(0.0, 1.0)); + MOI.delete(model, x) + @test !MOI.is_valid(model, x) + @test MOI.is_empty(model) + return +end + end # module TestBridgeOptimizer.runtests() From d985953803b22071f50db969b212990a56256ae1 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 18 Aug 2025 11:37:04 +1200 Subject: [PATCH 02/12] F --- src/Bridges/bridge_optimizer.jl | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 6eaaeb12cf..3e1cd1bbc2 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -506,18 +506,30 @@ function _delete_variables_in_vector_of_variables_constraint( end end +""" + _delete_variables_in_variables_constraints( + b::AbstractBridgeOptimizer, + vis::Vector{MOI.VariableIndex}, + ) + +!!! warning + There's a lot of subtle logic in this function. +""" function _delete_variables_in_variables_constraints( b::AbstractBridgeOptimizer, vis::Vector{MOI.VariableIndex}, ) c_map = Constraint.bridges(b)::Constraint.Map - # Delete all `MOI.VariableIndex` constraints of these variables. + # First, we need to delete any scalar constraints associated with these + # variables. + # + # x in S_s --> [x] in S_v for vi in vis - # If a bridged `VariableIndex` constraints creates a second one, - # then we will delete the second one when deleting the first one hence we - # should not delete it again in this loop. - # For this, we reverse the order so that we encounter the first one first - # and we won't delete the second one since `MOI.is_valid(b, ci)` will be `false`. + # If a bridged `VariableIndex` constraints creates a second one, then we + # will delete the second one when deleting the first one hence we should + # not delete it again in this loop. For this, we reverse the order so + # that we encounter the first one first and we won't delete the second + # one since `MOI.is_valid(b, ci)` will be `false`. for ci in Iterators.reverse(Constraint.variable_constraints(c_map, vi)) if MOI.is_valid(b, ci) MOI.delete(b, ci) From c8c841cd6a9cc5af625d7f24672b57260a6ef60d Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 18 Aug 2025 12:21:13 +1200 Subject: [PATCH 03/12] Add test --- test/Bridges/bridge_optimizer.jl | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/Bridges/bridge_optimizer.jl b/test/Bridges/bridge_optimizer.jl index b1a8620a2c..b6464b62af 100644 --- a/test/Bridges/bridge_optimizer.jl +++ b/test/Bridges/bridge_optimizer.jl @@ -1492,6 +1492,35 @@ function test_issue_2817() return end +MOI.Utilities.@model( + Model2817b, + (), + (MOI.LessThan,), + (), + (), + (), + (MOI.ScalarAffineFunction,), + (), + () +); + +function MOI.supports_constraint( + ::Model2817b, + ::Type{MOI.VariableIndex}, + ::Type{S}, +) where {S<:MOI.AbstractScalarSet} + return false +end + +function test_issue_2817b() + model = MOI.instantiate(Model2817b{Float64}; with_bridge_type = Float64) + y, _ = MOI.add_constrained_variables(model, MOI.Nonpositives(1)) + MOI.delete(model, y) + @test !MOI.is_valid(model, y[1]) + @test MOI.is_empty(model) + return +end + end # module TestBridgeOptimizer.runtests() From fe1ee2ff94d28fce02178cc66b94936c326397d7 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 18 Aug 2025 13:46:52 +1200 Subject: [PATCH 04/12] Update --- src/Bridges/bridge_optimizer.jl | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 3e1cd1bbc2..f6d81a0d3b 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -476,6 +476,7 @@ function MOI.is_valid( v_map = Variable.bridges(b)::Variable.Map return MOI.is_valid(v_map, ci) else + # This has the potential to be a false positive. see #2817 return haskey(Constraint.bridges(b), ci) end else @@ -506,6 +507,17 @@ function _delete_variables_in_vector_of_variables_constraint( end end +function _is_added_by_bridge(c_map, ci::MOI.ConstraintIndex{F,S}) where {F,S} + for (i, bridge) in c_map + if i == ci + continue + elseif ci in MOI.get(bridge, MOI.ListOfConstraintIndices{F,S}()) + return true + end + end + return false +end + """ _delete_variables_in_variables_constraints( b::AbstractBridgeOptimizer, @@ -520,10 +532,18 @@ function _delete_variables_in_variables_constraints( vis::Vector{MOI.VariableIndex}, ) c_map = Constraint.bridges(b)::Constraint.Map - # First, we need to delete any scalar constraints associated with these - # variables. + # The point of this function is to delete constraints associated with the + # variables in `vis`. + # + # There are two problematic cases: + # + # 1. A VariableIndex bridged to a VectorOfVariables equivalent. For example, + # x in Interval --> [x] in HyperRectangle + # 2. A VectorOfVariables bridged to a VariableIndex equivalent. For example, + # [x] in Nonnegatives --> x in GreaterThan # - # x in S_s --> [x] in S_v + # First, we need to delete scalar constraints, UNLESS they were added by a + # different constraint bridge. for vi in vis # If a bridged `VariableIndex` constraints creates a second one, then we # will delete the second one when deleting the first one hence we should @@ -531,7 +551,7 @@ function _delete_variables_in_variables_constraints( # that we encounter the first one first and we won't delete the second # one since `MOI.is_valid(b, ci)` will be `false`. for ci in Iterators.reverse(Constraint.variable_constraints(c_map, vi)) - if MOI.is_valid(b, ci) + if MOI.is_valid(b, ci) && !_is_added_by_bridge(c_map, ci) MOI.delete(b, ci) end end From 3ced5b779733ebb23537aa887d804d8161d98367 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 18 Aug 2025 16:56:39 +1200 Subject: [PATCH 05/12] Update --- src/Bridges/bridge_optimizer.jl | 130 ++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 48 deletions(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index f6d81a0d3b..19b81d6889 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -476,7 +476,32 @@ function MOI.is_valid( v_map = Variable.bridges(b)::Variable.Map return MOI.is_valid(v_map, ci) else - # This has the potential to be a false positive. see #2817 + # This return value has the potential to be a false positive: it + # doesn't discriminate between constraints that the user added, and + # constraints that a bridge added that were themselves bridged. + # + # "Fixing" this particular call has a number of wide reaching + # effects because bridges need this to be "true" so that they can + # query attributes of the constraint from `b`. + # + # In most cases a false positive doesn't matter, because we really + # do support querying stuff about it. And also the user needs some + # way of obtaining the correct index, which they won't have except + # by luck/enumeration. + # + # The main place that this is problematic is when we come to delete + # constraints, and particular VariableIndex constraints, because we + # triviallly have their `.value` field from the `.value` of the + # VariableIndex. + # + # Instead of fixing everything though, we implement some extra + # checks when deleting, and we leave the false-positive as-is for + # now. If you, future reader, hit this comment while debugging, we + # might need to revisit this decision. + # + # x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2696 + # x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2817 + # x-ref https://github.com/jump-dev/MathOptInterface.jl/pull/2818 return haskey(Constraint.bridges(b), ci) end else @@ -490,32 +515,49 @@ function _delete_variables_in_vector_of_variables_constraint( ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S}, ) where {S} func = MOI.get(b, MOI.ConstraintFunction(), ci) - variables = copy(func.variables) - if vis == variables + if vis == func.variables MOI.delete(b, ci) - else - for vi in vis - i = findfirst(isequal(vi), variables) - if i !== nothing - if MOI.supports_dimension_update(S) - call_in_context(MOI.delete, b, ci, IndexInVector(i)) - else - MOI.Utilities.throw_delete_variable_in_vov(vi) - end - end + return + end + variables = copy(func.variables) + for vi in vis + i = findfirst(isequal(vi), variables) + if i === nothing + continue + elseif MOI.supports_dimension_update(S) + call_in_context(MOI.delete, b, ci, IndexInVector(i)) + else + MOI.Utilities.throw_delete_variable_in_vov(vi) end end + return end -function _is_added_by_bridge(c_map, ci::MOI.ConstraintIndex{F,S}) where {F,S} - for (i, bridge) in c_map - if i == ci - continue - elseif ci in MOI.get(bridge, MOI.ListOfConstraintIndices{F,S}()) - return true +""" + _is_added_by_bridge( + c_map, + cache::Dict{Any,Any}, + ci::MOI.ConstraintIndex{F,S}, + ) where {F,S} + +Return `true` if `ci` was added by one of the bridges in `c_map`. + +For performance reasons, we store the `ListOfConstraintIndices{F,S}` in `cache`, +so that we don't have to keep lopping through the bridges. +""" +function _is_added_by_bridge( + c_map, + cache::Dict{Any,Any}, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + ret = get!(cache, (F, S)) do + list = MOI.ConstraintIndex{F,S}[] + for bridge in values(c_map) + append!(list, MOI.get(bridge, MOI.ListOfConstraintIndices{F,S}())) end + return list end - return false + return ci in ret end """ @@ -524,45 +566,37 @@ end vis::Vector{MOI.VariableIndex}, ) -!!! warning - There's a lot of subtle logic in this function. +The point of this function is to delete constraints associated with the +variables in `vis`. + +## Warning + +Because of the false positive potential in +`is_valid(::AbstractBridgeOptimizer, MOI.ConstraintIndex)`, we need to ensure +that we delete constraints only if they were not added by a different constraint +bridge, otherwise when we come to delete the parent constraint we'll hit a +runtime error where we have already deleted part of the bridged constraint. + +x-ref https://github.com/jump-dev/MathOptInterface.jl/issues/2817 +x-ref https://github.com/jump-dev/MathOptInterface.jl/pull/2818 """ function _delete_variables_in_variables_constraints( b::AbstractBridgeOptimizer, vis::Vector{MOI.VariableIndex}, ) c_map = Constraint.bridges(b)::Constraint.Map - # The point of this function is to delete constraints associated with the - # variables in `vis`. - # - # There are two problematic cases: - # - # 1. A VariableIndex bridged to a VectorOfVariables equivalent. For example, - # x in Interval --> [x] in HyperRectangle - # 2. A VectorOfVariables bridged to a VariableIndex equivalent. For example, - # [x] in Nonnegatives --> x in GreaterThan - # - # First, we need to delete scalar constraints, UNLESS they were added by a - # different constraint bridge. + cache = Dict{Any,Any}() for vi in vis - # If a bridged `VariableIndex` constraints creates a second one, then we - # will delete the second one when deleting the first one hence we should - # not delete it again in this loop. For this, we reverse the order so - # that we encounter the first one first and we won't delete the second - # one since `MOI.is_valid(b, ci)` will be `false`. - for ci in Iterators.reverse(Constraint.variable_constraints(c_map, vi)) - if MOI.is_valid(b, ci) && !_is_added_by_bridge(c_map, ci) + for ci in Constraint.variable_constraints(c_map, vi) + if !_is_added_by_bridge(c_map, cache, ci) MOI.delete(b, ci) end end end - # Delete all `MOI.VectorOfVariables` constraints of these variables. - # We reverse for the same reason as for `VariableIndex` below. - # As the iterators are lazy, when the inner bridge constraint is deleted, - # it won't be part of the iteration. - for ci in - Iterators.reverse(Constraint.vector_of_variables_constraints(c_map)) - _delete_variables_in_vector_of_variables_constraint(b, vis, ci) + for ci in Constraint.vector_of_variables_constraints(c_map) + if !_is_added_by_bridge(c_map, cache, ci) + _delete_variables_in_vector_of_variables_constraint(b, vis, ci) + end end return end From d885391b5d82f1365cf6e93e02233dabf150e197 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 18 Aug 2025 19:34:08 +1200 Subject: [PATCH 06/12] Update --- src/Bridges/bridge_optimizer.jl | 40 +++++++++++++++++---------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 19b81d6889..107d639ed3 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -517,17 +517,17 @@ function _delete_variables_in_vector_of_variables_constraint( func = MOI.get(b, MOI.ConstraintFunction(), ci) if vis == func.variables MOI.delete(b, ci) - return - end - variables = copy(func.variables) - for vi in vis - i = findfirst(isequal(vi), variables) - if i === nothing - continue - elseif MOI.supports_dimension_update(S) - call_in_context(MOI.delete, b, ci, IndexInVector(i)) - else - MOI.Utilities.throw_delete_variable_in_vov(vi) + else + variables = copy(func.variables) + for vi in vis + i = findfirst(isequal(vi), variables) + if i !== nothing + if MOI.supports_dimension_update(S) + call_in_context(MOI.delete, b, ci, IndexInVector(i)) + else + MOI.Utilities.throw_delete_variable_in_vov(vi) + end + end end end return @@ -536,7 +536,7 @@ end """ _is_added_by_bridge( c_map, - cache::Dict{Any,Any}, + cache::Dict{Any,Set{Int64}}, ci::MOI.ConstraintIndex{F,S}, ) where {F,S} @@ -547,17 +547,19 @@ so that we don't have to keep lopping through the bridges. """ function _is_added_by_bridge( c_map, - cache::Dict{Any,Any}, + cache::Dict{Any,Set{Int64}}, ci::MOI.ConstraintIndex{F,S}, ) where {F,S} ret = get!(cache, (F, S)) do - list = MOI.ConstraintIndex{F,S}[] + set = Set{Int64}() for bridge in values(c_map) - append!(list, MOI.get(bridge, MOI.ListOfConstraintIndices{F,S}())) + for ci in MOI.get(bridge, MOI.ListOfConstraintIndices{F,S}()) + push!(set, ci.value) + end end - return list - end - return ci in ret + return set + end::Set{Int64} + return ci.value in ret end """ @@ -585,7 +587,7 @@ function _delete_variables_in_variables_constraints( vis::Vector{MOI.VariableIndex}, ) c_map = Constraint.bridges(b)::Constraint.Map - cache = Dict{Any,Any}() + cache = Dict{Any,Set{Int64}}() for vi in vis for ci in Constraint.variable_constraints(c_map, vi) if !_is_added_by_bridge(c_map, cache, ci) From 7296348354ff1da3e5828034e171d8c428b1c62d Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 18 Aug 2025 19:35:27 +1200 Subject: [PATCH 07/12] Update --- src/Bridges/bridge_optimizer.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 107d639ed3..94a2b294d1 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -542,8 +542,9 @@ end Return `true` if `ci` was added by one of the bridges in `c_map`. -For performance reasons, we store the `ListOfConstraintIndices{F,S}` in `cache`, -so that we don't have to keep lopping through the bridges. +For performance reasons, we store the index values associated with +`MOI.ListOfConstraintIndices{F,S}` in `cache` so that we don't have to keep +looping through the bridges. """ function _is_added_by_bridge( c_map, From 5100b0d25c52d01a1f0c1e485af74f575ce9b27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 18 Aug 2025 15:02:14 +0200 Subject: [PATCH 08/12] Fix format --- src/Bridges/bridge_optimizer.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 94a2b294d1..2a65541fec 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -554,7 +554,10 @@ function _is_added_by_bridge( ret = get!(cache, (F, S)) do set = Set{Int64}() for bridge in values(c_map) - for ci in MOI.get(bridge, MOI.ListOfConstraintIndices{F,S}()) + for ci in MOI.get( + bridge, + MOI.ListOfConstraintIndices{F,S}(), + ) push!(set, ci.value) end end From 91b653ff519883ca95de18ea3dbfbc55f32a6086 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 19 Aug 2025 11:44:02 +1200 Subject: [PATCH 09/12] Update src/Bridges/bridge_optimizer.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: BenoƮt Legat --- src/Bridges/bridge_optimizer.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 2a65541fec..02598ff409 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -490,7 +490,7 @@ function MOI.is_valid( # by luck/enumeration. # # The main place that this is problematic is when we come to delete - # constraints, and particular VariableIndex constraints, because we + # constraints, and in particular VariableIndex constraints, because we # triviallly have their `.value` field from the `.value` of the # VariableIndex. # From 95a9333bf37910e2bc46a38e4a2678b2b414937e Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 19 Aug 2025 16:52:41 +1200 Subject: [PATCH 10/12] Update --- test/Bridges/bridge_optimizer.jl | 67 +++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/test/Bridges/bridge_optimizer.jl b/test/Bridges/bridge_optimizer.jl index b6464b62af..9a597359b6 100644 --- a/test/Bridges/bridge_optimizer.jl +++ b/test/Bridges/bridge_optimizer.jl @@ -595,6 +595,7 @@ function test_double_deletion_scalar() @test !MOI.Bridges.is_bridged(model, b2.constraint) MOI.delete(model, x) @test !MOI.is_valid(model, x) + @test MOI.is_empty(model) return end @@ -614,6 +615,7 @@ function test_double_deletion_vector() @test !MOI.Bridges.is_bridged(model, b2.constraint) MOI.delete(model, x) @test all(vi -> !MOI.is_valid(model, vi), x) + @test MOI.is_empty(model) return end @@ -1464,7 +1466,7 @@ function test_BridgeRequiresFiniteDomainError() end MOI.Utilities.@model( - Model2817, + Model2817a, (), (), (MOI.Nonnegatives,), @@ -1476,19 +1478,26 @@ MOI.Utilities.@model( ); function MOI.supports_constraint( - ::Model2817, + ::Model2817a, ::Type{MOI.VariableIndex}, ::Type{S}, ) where {S<:MOI.AbstractScalarSet} return false end -function test_issue_2817() - model = MOI.Bridges.full_bridge_optimizer(Model2817{Float64}(), Float64); - x, _ = MOI.add_constrained_variable(model, MOI.Interval(0.0, 1.0)); +function test_issue_2817a() + inner = Model2817a{Float64}() + model = MOI.Bridges.full_bridge_optimizer(inner, Float64); + x, c = MOI.add_constrained_variable(model, MOI.Interval(0.0, 1.0)); + @test isa( + model.constraint_map[c], + MOI.Bridges.Constraint.IntervalToHyperRectangleBridge, + ) MOI.delete(model, x) @test !MOI.is_valid(model, x) @test MOI.is_empty(model) + @test MOI.is_empty(inner) + @test isempty(model.constraint_map) return end @@ -1513,11 +1522,51 @@ function MOI.supports_constraint( end function test_issue_2817b() - model = MOI.instantiate(Model2817b{Float64}; with_bridge_type = Float64) - y, _ = MOI.add_constrained_variables(model, MOI.Nonpositives(1)) - MOI.delete(model, y) - @test !MOI.is_valid(model, y[1]) + inner = Model2817b{Float64}() + model = MOI.Bridges.full_bridge_optimizer(inner, Float64); + x, c = MOI.add_constrained_variables(model, MOI.Nonpositives(1)) + @test isa(model.constraint_map[c], MOI.Bridges.Constraint.ScalarizeBridge) + MOI.delete(model, x) + @test !MOI.is_valid(model, x[1]) + @test MOI.is_empty(model) + @test MOI.is_empty(inner) + @test isempty(model.constraint_map) + return +end + +MOI.Utilities.@model( + Model2817c, + (), + (MOI.GreaterThan, MOI.LessThan,), + (), + (), + (), + (MOI.ScalarAffineFunction,), + (), + () +); + +function MOI.supports_constraint( + ::Model2817c, + ::Type{MOI.VariableIndex}, + ::Type{S}, +) where {S<:Union{MOI.ZeroOne,MOI.Semiinteger,MOI.Semicontinuous}} + return false +end + +function test_issue_2817c() + inner = Model2817c{Float64}() + model = MOI.Bridges.full_bridge_optimizer(inner, Float64); + x, c = MOI.add_constrained_variable(model, MOI.Semiinteger(2.0, 3.0)) + @test isa( + model.constraint_map[c], + MOI.Bridges.Constraint.SemiToBinaryBridge, + ) + MOI.delete(model, x) + @test !MOI.is_valid(model, x) @test MOI.is_empty(model) + @test MOI.is_empty(inner) + @test isempty(model.constraint_map) return end From b0afd592699984a049cc356b7d752f3f76c85727 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 19 Aug 2025 16:56:50 +1200 Subject: [PATCH 11/12] Update --- src/Bridges/Constraint/bridges/SemiToBinaryBridge.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Bridges/Constraint/bridges/SemiToBinaryBridge.jl b/src/Bridges/Constraint/bridges/SemiToBinaryBridge.jl index a3aac980a3..df3c86c3e6 100644 --- a/src/Bridges/Constraint/bridges/SemiToBinaryBridge.jl +++ b/src/Bridges/Constraint/bridges/SemiToBinaryBridge.jl @@ -179,7 +179,6 @@ function MOI.delete(model::MOI.ModelLike, bridge::SemiToBinaryBridge) end MOI.delete(model, bridge.upper_bound_index) MOI.delete(model, bridge.lower_bound_index) - MOI.delete(model, bridge.binary_constraint_index) MOI.delete(model, bridge.binary_variable) return end From ce5d50051cfb07f1ac6623b6fd20929884fa9656 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 19 Aug 2025 18:36:17 +1200 Subject: [PATCH 12/12] Update test/Bridges/bridge_optimizer.jl --- test/Bridges/bridge_optimizer.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Bridges/bridge_optimizer.jl b/test/Bridges/bridge_optimizer.jl index 9a597359b6..4a4c1a3c04 100644 --- a/test/Bridges/bridge_optimizer.jl +++ b/test/Bridges/bridge_optimizer.jl @@ -1537,7 +1537,7 @@ end MOI.Utilities.@model( Model2817c, (), - (MOI.GreaterThan, MOI.LessThan,), + (MOI.GreaterThan, MOI.LessThan), (), (), (),