From fd738190aa07ab3561999e0baab9894253b0ea70 Mon Sep 17 00:00:00 2001 From: Chris Coey Date: Fri, 16 Oct 2026 16:24:25 -0700 Subject: [PATCH 1/5] [Bridges] fix IndicatorToMILPBridge variable and constraint lists before final_touch --- .../bridges/IndicatorToMILPBridge.jl | 18 ++++++++++-- .../Constraint/test_IndicatorToMILPBridge.jl | 29 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl b/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl index 18ce4616a2..c502422b78 100644 --- a/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl +++ b/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl @@ -138,9 +138,14 @@ function MOI.delete( return end -MOI.get(bridge::IndicatorToMILPBridge, ::MOI.NumberOfVariables)::Int64 = 1 +function MOI.get(bridge::IndicatorToMILPBridge, ::MOI.NumberOfVariables)::Int64 + return bridge.slack === nothing ? 0 : 1 +end function MOI.get(bridge::IndicatorToMILPBridge, ::MOI.ListOfVariableIndices) + if bridge.slack === nothing + return MOI.VariableIndex[] + end return [bridge.slack::MOI.VariableIndex] end @@ -148,13 +153,16 @@ function MOI.get( bridge::IndicatorToMILPBridge{T,F,A,S}, ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},S}, )::Int64 where {T,F,A,S<:Union{MOI.GreaterThan,MOI.EqualTo}} - return 1 + return bridge.slack === nothing ? 0 : 1 end function MOI.get( bridge::IndicatorToMILPBridge{T,F,A,S}, ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},S}, ) where {T,F,A,S} + if bridge.slack === nothing + return MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}[] + end return [bridge.constraint] end @@ -176,6 +184,9 @@ function MOI.get( bridge::IndicatorToMILPBridge{T,F,A,MOI.LessThan{T}}, ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, )::Int64 where {T,F,A} + if bridge.slack === nothing + return 0 + end return 1 + length(bridge.slack_bounds) end @@ -183,6 +194,9 @@ function MOI.get( bridge::IndicatorToMILPBridge{T,F,A,MOI.LessThan{T}}, ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, ) where {T,F,A} + if bridge.slack === nothing + return MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}[] + end return vcat(bridge.constraint, bridge.slack_bounds) end diff --git a/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl b/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl index 063bd6472b..f1cb45bb4c 100644 --- a/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl +++ b/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl @@ -302,6 +302,35 @@ function test_delete_before_final_touch() return end +function test_list_before_final_touch() + F = MOI.ScalarAffineFunction{Float64} + for set in (MOI.LessThan(1.0), MOI.GreaterThan(1.0), MOI.EqualTo(1.0)) + S = typeof(set) + model = MOI.Bridges.Constraint.IndicatorToMILP{Float64}( + MOI.Utilities.Model{Float64}(), + ) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, x[1], MOI.ZeroOne()) + MOI.add_constraint(model, x[2], MOI.Interval(-10.0, 10.0)) + c = MOI.add_constraint(model, 1.0 * x[2], set) + MOI.add_constraint( + model, + MOI.VectorOfVariables(x), + MOI.Indicator{MOI.ACTIVATE_ON_ONE}(set), + ) + @test MOI.get(model, MOI.NumberOfVariables()) == 2 + @test MOI.get(model, MOI.ListOfVariableIndices()) == x + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + MOI.Bridges.final_touch(model) + @test MOI.get(model, MOI.NumberOfVariables()) == 2 + @test MOI.get(model, MOI.ListOfVariableIndices()) == x + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + end + return +end + function test_runtests_error_not_binary() inner = MOI.Utilities.Model{Int}() model = MOI.Bridges.Constraint.IndicatorToMILP{Int}(inner) From 112b99bb0cb70c41a3a263db98e8a3eb98f58eb0 Mon Sep 17 00:00:00 2001 From: Chris Coey Date: Fri, 16 Oct 2026 17:09:37 -0700 Subject: [PATCH 2/5] [Bridges] fix final_touch-related bugs in IndicatorToMILPBridge and ReifiedCountDistinctToMILPBridge --- .../bridges/IndicatorToMILPBridge.jl | 2 +- .../ReifiedCountDistinctToMILPBridge.jl | 3 ++ .../Constraint/test_IndicatorToMILPBridge.jl | 28 +++++++++++++++++++ .../test_ReifiedCountDistinctToMILPBridge.jl | 17 +++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl b/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl index c502422b78..2749fd4860 100644 --- a/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl +++ b/src/Bridges/Constraint/bridges/IndicatorToMILPBridge.jl @@ -133,7 +133,7 @@ function MOI.delete( MOI.delete(model, bridge.constraint) MOI.delete(model, bridge.slack::MOI.VariableIndex) bridge.slack = nothing - empty(bridge.slack_bounds) + empty!(bridge.slack_bounds) bridge.bounds = (typemin(T), typemax(T)) return end diff --git a/src/Bridges/Constraint/bridges/ReifiedCountDistinctToMILPBridge.jl b/src/Bridges/Constraint/bridges/ReifiedCountDistinctToMILPBridge.jl index b823b156cf..d078416aac 100644 --- a/src/Bridges/Constraint/bridges/ReifiedCountDistinctToMILPBridge.jl +++ b/src/Bridges/Constraint/bridges/ReifiedCountDistinctToMILPBridge.jl @@ -204,6 +204,9 @@ function MOI.get( bridge::ReifiedCountDistinctToMILPBridge, ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.ZeroOne}, )::Int64 + if isempty(bridge.variables) + return 0 + end return length(bridge.variables) - 2 end diff --git a/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl b/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl index f1cb45bb4c..3fe176e606 100644 --- a/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl +++ b/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl @@ -248,6 +248,34 @@ function test_resolve_with_modified() return end +function test_resolve_with_modified_repeatedly() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.IndicatorToMILP{Float64}(inner) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, x[1], MOI.ZeroOne()) + c = MOI.add_constraint(model, x[2], MOI.Interval(0.0, 10.0)) + ci = MOI.add_constraint( + model, + MOI.VectorOfVariables(x), + MOI.Indicator{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(5.0)), + ) + F, S = MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64} + for u in (20.0, 30.0, 40.0) + MOI.Bridges.final_touch(model) + @test MOI.get(inner, MOI.NumberOfConstraints{F,S}()) == 2 + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 0 + indices = MOI.get(model.map[ci], MOI.ListOfConstraintIndices{F,S}()) + @test length(indices) == 2 + @test all(MOI.is_valid.(inner, indices)) + MOI.set(model, MOI.ConstraintSet(), c, MOI.Interval(0.0, u)) + end + MOI.delete(model, ci) + @test !MOI.is_valid(model, ci) + @test MOI.get(inner, MOI.NumberOfVariables()) == 2 + @test MOI.get(inner, MOI.NumberOfConstraints{F,S}()) == 0 + return +end + function test_runtests_error_variable() inner = MOI.Utilities.Model{Int}() model = MOI.Bridges.Constraint.IndicatorToMILP{Int}(inner) diff --git a/test/Bridges/Constraint/test_ReifiedCountDistinctToMILPBridge.jl b/test/Bridges/Constraint/test_ReifiedCountDistinctToMILPBridge.jl index aa0cf40d72..100e6af0d0 100644 --- a/test/Bridges/Constraint/test_ReifiedCountDistinctToMILPBridge.jl +++ b/test/Bridges/Constraint/test_ReifiedCountDistinctToMILPBridge.jl @@ -177,6 +177,23 @@ function test_delete_before_final_touch() return end +function test_list_before_final_touch() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.ReifiedCountDistinctToMILP{Int}(inner) + x = MOI.add_variables(model, 4) + c = MOI.add_constraint(model, x[1], MOI.ZeroOne()) + MOI.add_constraint.(model, x[2:4], MOI.Interval(0, 2)) + f = MOI.VectorOfVariables(x) + MOI.add_constraint(model, f, MOI.Reified(MOI.CountDistinct(3))) + F, S = MOI.VariableIndex, MOI.ZeroOne + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + MOI.Bridges.final_touch(model) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + return +end + end # module TestConstraintReifiedCountDistinct.runtests() From e40c9dc6bd87a06a7a0fdc4df4e9ba383d2d5290 Mon Sep 17 00:00:00 2001 From: Chris Coey Date: Fri, 16 Oct 2026 17:37:26 -0700 Subject: [PATCH 3/5] [Bridges] fix SOS1ToMILP, SOS2ToMILP and IntegerToZeroOne constraint lists before final_touch --- .../bridges/IntegerToZeroOneBridge.jl | 5 +++- .../Constraint/bridges/SOS1ToMILPBridge.jl | 5 +++- .../Constraint/bridges/SOS2ToMILPBridge.jl | 5 +++- .../Constraint/test_IndicatorToMILPBridge.jl | 9 ++++++- .../Constraint/test_IntegerToZeroOneBridge.jl | 23 ++++++++++++++++++ .../Constraint/test_SOS1ToMILPBridge.jl | 24 +++++++++++++++++++ .../Constraint/test_SOS2ToMILPBridge.jl | 24 +++++++++++++++++++ 7 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl b/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl index 438ac7312f..bcdca7bec1 100644 --- a/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl +++ b/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl @@ -139,13 +139,16 @@ function MOI.get( bridge::IntegerToZeroOneBridge{T}, ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, )::Int64 where {T} - return 1 + return bridge.last_bounds === nothing ? 0 : 1 end function MOI.get( bridge::IntegerToZeroOneBridge{T}, ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, ) where {T} + if bridge.last_bounds === nothing + return MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[] + end return [bridge.ci] end diff --git a/src/Bridges/Constraint/bridges/SOS1ToMILPBridge.jl b/src/Bridges/Constraint/bridges/SOS1ToMILPBridge.jl index 7dd5d9f0b0..8289cafb34 100644 --- a/src/Bridges/Constraint/bridges/SOS1ToMILPBridge.jl +++ b/src/Bridges/Constraint/bridges/SOS1ToMILPBridge.jl @@ -176,13 +176,16 @@ function MOI.get( bridge::SOS1ToMILPBridge{T}, ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, )::Int64 where {T} - return 1 + return isempty(bridge.variables) ? 0 : 1 end function MOI.get( bridge::SOS1ToMILPBridge{T}, ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, ) where {T} + if isempty(bridge.variables) + return MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[] + end return [bridge.equal_to] end diff --git a/src/Bridges/Constraint/bridges/SOS2ToMILPBridge.jl b/src/Bridges/Constraint/bridges/SOS2ToMILPBridge.jl index d9db185454..e59f02328d 100644 --- a/src/Bridges/Constraint/bridges/SOS2ToMILPBridge.jl +++ b/src/Bridges/Constraint/bridges/SOS2ToMILPBridge.jl @@ -167,13 +167,16 @@ function MOI.get( bridge::SOS2ToMILPBridge{T}, ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, )::Int64 where {T} - return 1 + return isempty(bridge.variables) ? 0 : 1 end function MOI.get( bridge::SOS2ToMILPBridge{T}, ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, ) where {T} + if isempty(bridge.variables) + return MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[] + end return [bridge.equal_to] end diff --git a/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl b/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl index 3fe176e606..c515c5c6a2 100644 --- a/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl +++ b/test/Bridges/Constraint/test_IndicatorToMILPBridge.jl @@ -341,11 +341,18 @@ function test_list_before_final_touch() MOI.add_constraint(model, x[1], MOI.ZeroOne()) MOI.add_constraint(model, x[2], MOI.Interval(-10.0, 10.0)) c = MOI.add_constraint(model, 1.0 * x[2], set) - MOI.add_constraint( + ci = MOI.add_constraint( model, MOI.VectorOfVariables(x), MOI.Indicator{MOI.ACTIVATE_ON_ONE}(set), ) + bridge = MOI.Bridges.bridge(model, ci) + @test MOI.get(bridge, MOI.NumberOfVariables()) == 0 + @test isempty(MOI.get(bridge, MOI.ListOfVariableIndices())) + for G in (S, MOI.LessThan{Float64}) + @test MOI.get(bridge, MOI.NumberOfConstraints{F,G}()) == 0 + @test isempty(MOI.get(bridge, MOI.ListOfConstraintIndices{F,G}())) + end @test MOI.get(model, MOI.NumberOfVariables()) == 2 @test MOI.get(model, MOI.ListOfVariableIndices()) == x @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 diff --git a/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl b/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl index 5c9553d7bb..751e869dc7 100644 --- a/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl +++ b/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl @@ -106,6 +106,29 @@ function test_delete_before_final_touch() return end +function test_list_before_final_touch() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.IntegerToZeroOne{Float64}(inner) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, x[1], MOI.Interval(0.0, 3.0)) + c = MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.EqualTo(2.0)) + ci = MOI.add_constraint(model, x[1], MOI.Integer()) + F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + bridge = MOI.Bridges.bridge(model, ci) + @test MOI.get(bridge, MOI.NumberOfConstraints{F,S}()) == 0 + @test isempty(MOI.get(bridge, MOI.ListOfConstraintIndices{F,S}())) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + dest = MOI.Utilities.Model{Float64}() + MOI.copy_to(dest, model) + @test MOI.get(dest, MOI.NumberOfConstraints{F,S}()) == 1 + MOI.Bridges.final_touch(model) + @test MOI.get(bridge, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + return +end + end # module TestConstraintIntegerToZeroOne.runtests() diff --git a/test/Bridges/Constraint/test_SOS1ToMILPBridge.jl b/test/Bridges/Constraint/test_SOS1ToMILPBridge.jl index 5de4658a87..617f2b46fd 100644 --- a/test/Bridges/Constraint/test_SOS1ToMILPBridge.jl +++ b/test/Bridges/Constraint/test_SOS1ToMILPBridge.jl @@ -164,6 +164,30 @@ function test_bridge_does_not_apply_if_vector_slack_exists() return end +function test_list_before_final_touch() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.SOS1ToMILP{Float64}(inner) + x = MOI.add_variables(model, 3) + MOI.add_constraint.(model, x, MOI.Interval(0.0, 2.0)) + c = MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.EqualTo(1.0)) + f = MOI.VectorOfVariables(x) + ci = MOI.add_constraint(model, f, MOI.SOS1([1.0, 2.0, 3.0])) + F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + bridge = MOI.Bridges.bridge(model, ci) + @test MOI.get(bridge, MOI.NumberOfConstraints{F,S}()) == 0 + @test isempty(MOI.get(bridge, MOI.ListOfConstraintIndices{F,S}())) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + dest = MOI.Utilities.Model{Float64}() + MOI.copy_to(dest, model) + @test MOI.get(dest, MOI.NumberOfConstraints{F,S}()) == 1 + MOI.Bridges.final_touch(model) + @test MOI.get(bridge, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + return +end + end # module TestConstraintSOS1ToMILP.runtests() diff --git a/test/Bridges/Constraint/test_SOS2ToMILPBridge.jl b/test/Bridges/Constraint/test_SOS2ToMILPBridge.jl index 6352483b3e..5e4f04be7f 100644 --- a/test/Bridges/Constraint/test_SOS2ToMILPBridge.jl +++ b/test/Bridges/Constraint/test_SOS2ToMILPBridge.jl @@ -174,6 +174,30 @@ function test_bridge_does_not_apply_if_vector_slack_exists() return end +function test_list_before_final_touch() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.SOS2ToMILP{Float64}(inner) + x = MOI.add_variables(model, 3) + MOI.add_constraint.(model, x, MOI.Interval(0.0, 2.0)) + c = MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.EqualTo(1.0)) + f = MOI.VectorOfVariables(x) + ci = MOI.add_constraint(model, f, MOI.SOS2([1.0, 2.0, 3.0])) + F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + bridge = MOI.Bridges.bridge(model, ci) + @test MOI.get(bridge, MOI.NumberOfConstraints{F,S}()) == 0 + @test isempty(MOI.get(bridge, MOI.ListOfConstraintIndices{F,S}())) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + dest = MOI.Utilities.Model{Float64}() + MOI.copy_to(dest, model) + @test MOI.get(dest, MOI.NumberOfConstraints{F,S}()) == 1 + MOI.Bridges.final_touch(model) + @test MOI.get(bridge, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + return +end + end # module TestConstraintSOS2ToMILP.runtests() From 1d72ea5507dd715b181e0f941c6d1a1d118b691c Mon Sep 17 00:00:00 2001 From: Chris Coey Date: Sat, 17 Oct 2026 00:50:36 -0700 Subject: [PATCH 4/5] [Bridges] fix IntegerToZeroOneBridge when bounds change between final_touch calls --- .../bridges/IntegerToZeroOneBridge.jl | 6 ++++++ .../Constraint/test_IntegerToZeroOneBridge.jl | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl b/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl index bcdca7bec1..4fe9bfddd6 100644 --- a/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl +++ b/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl @@ -108,6 +108,8 @@ function MOI.delete(model::MOI.ModelLike, bridge::IntegerToZeroOneBridge) end MOI.delete(model, bridge.ci) MOI.delete(model, bridge.y) + empty!(bridge.y) + bridge.last_bounds = nothing return end @@ -164,6 +166,10 @@ function MOI.Bridges.final_touch( return nothing # final_touch already called elseif ret[1] == typemin(T) || ret[2] == typemax(T) throw(MOI.Bridges.BridgeRequiresFiniteDomainError(bridge, bridge.x)) + elseif bridge.last_bounds !== nothing + # The bounds have changed since the last call to final_touch. Delete + # the old reformulation and start again. + MOI.delete(model, bridge) end f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(T(1), bridge.x)], T(0)) lb, ub = ceil(Int, ret[1]), floor(Int, ret[2]) diff --git a/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl b/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl index 751e869dc7..a5e3c61fc7 100644 --- a/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl +++ b/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl @@ -94,6 +94,23 @@ function test_final_touch_twice() return end +function test_resolve_with_modified() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Constraint.IntegerToZeroOne{Float64}(inner) + x = MOI.add_variable(model) + c = MOI.add_constraint(model, x, MOI.Interval(0.0, 3.0)) + MOI.add_constraint(model, x, MOI.Integer()) + F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + MOI.Bridges.final_touch(model) + @test MOI.get(inner, MOI.NumberOfVariables()) == 3 + MOI.set(model, MOI.ConstraintSet(), c, MOI.Interval(0.0, 7.0)) + MOI.Bridges.final_touch(model) + @test MOI.get(inner, MOI.NumberOfVariables()) == 4 + @test MOI.get(inner, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 0 + return +end + function test_delete_before_final_touch() inner = MOI.Utilities.Model{Int}() model = MOI.Bridges.Constraint.IntegerToZeroOne{Int}(inner) From fd59fc5374012840b3e8d3a41e3b474dd346459d Mon Sep 17 00:00:00 2001 From: Chris Coey Date: Sat, 17 Oct 2026 01:51:56 -0700 Subject: [PATCH 5/5] [Bridges] rebuild IntegerToZeroOneBridge the same way as the other final_touch bridges --- .../Constraint/bridges/IntegerToZeroOneBridge.jl | 7 +++++-- .../Constraint/test_IntegerToZeroOneBridge.jl | 12 +++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl b/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl index 4fe9bfddd6..7219f53f9d 100644 --- a/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl +++ b/src/Bridges/Constraint/bridges/IntegerToZeroOneBridge.jl @@ -167,9 +167,12 @@ function MOI.Bridges.final_touch( elseif ret[1] == typemin(T) || ret[2] == typemax(T) throw(MOI.Bridges.BridgeRequiresFiniteDomainError(bridge, bridge.x)) elseif bridge.last_bounds !== nothing - # The bounds have changed since the last call to final_touch. Delete - # the old reformulation and start again. + # There is a stored bound, and the current bounds do not match. This + # means the model has been modified since the previous call to + # final_touch. We need to delete the bridge and start again. MOI.delete(model, bridge) + MOI.Bridges.final_touch(bridge, model) + return end f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(T(1), bridge.x)], T(0)) lb, ub = ceil(Int, ret[1]), floor(Int, ret[2]) diff --git a/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl b/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl index a5e3c61fc7..5ca5c17b62 100644 --- a/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl +++ b/test/Bridges/Constraint/test_IntegerToZeroOneBridge.jl @@ -95,18 +95,20 @@ function test_final_touch_twice() end function test_resolve_with_modified() - inner = MOI.Utilities.Model{Float64}() - model = MOI.Bridges.Constraint.IntegerToZeroOne{Float64}(inner) + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.IntegerToZeroOne{Int}(inner) x = MOI.add_variable(model) - c = MOI.add_constraint(model, x, MOI.Interval(0.0, 3.0)) + c = MOI.add_constraint(model, x, MOI.Interval(0, 3)) MOI.add_constraint(model, x, MOI.Integer()) - F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + F, S = MOI.ScalarAffineFunction{Int}, MOI.EqualTo{Int} + @test MOI.get(inner, MOI.NumberOfVariables()) == 1 MOI.Bridges.final_touch(model) @test MOI.get(inner, MOI.NumberOfVariables()) == 3 - MOI.set(model, MOI.ConstraintSet(), c, MOI.Interval(0.0, 7.0)) + MOI.set(model, MOI.ConstraintSet(), c, MOI.Interval(0, 7)) MOI.Bridges.final_touch(model) @test MOI.get(inner, MOI.NumberOfVariables()) == 4 @test MOI.get(inner, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.NumberOfVariables()) == 1 @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 0 return end