From a1918265805e0d3293e903fe379025dc2ca96125 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 7 Sep 2026 14:47:07 +1200 Subject: [PATCH 1/3] [Bridges] fix SplitHyperRectangleBridge with free rows --- .../bridges/SplitHyperRectangleBridge.jl | 147 +++++++++--------- .../test_SplitHyperRectangleBridge.jl | 28 ++-- 2 files changed, 93 insertions(+), 82 deletions(-) diff --git a/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl b/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl index 09a0eb4387..5309b56f08 100644 --- a/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl @@ -28,8 +28,8 @@ mutable struct SplitHyperRectangleBridge{T,G,F} <: AbstractBridge ci::Union{Nothing,MOI.ConstraintIndex{G,MOI.Nonnegatives}} set::MOI.HyperRectangle{T} free_rows::F - free_primal_start::Union{Nothing,Vector{T}} - free_dual_start::Union{Nothing,Vector{T}} + primal_start::Union{Nothing,Vector{T}} + dual_start::Union{Nothing,Vector{T}} function SplitHyperRectangleBridge{T,G,F}( ci::Union{Nothing,MOI.ConstraintIndex{G,MOI.Nonnegatives}}, @@ -202,38 +202,17 @@ function MOI.supports( return MOI.supports(model, attr, MOI.ConstraintIndex{G,MOI.Nonnegatives}) end -_get_free_start(bridge, ::MOI.ConstraintDualStart) = bridge.free_dual_start - -function _set_free_start(bridge, ::MOI.ConstraintDualStart, value) - bridge.free_dual_start = value - return -end - -_get_free_start(bridge, ::MOI.ConstraintPrimalStart) = bridge.free_primal_start - -function _set_free_start(bridge, ::MOI.ConstraintPrimalStart, value) - bridge.free_primal_start = value - return -end - -# This is a punned overload. We use Union{MOI.ConstraintDual,MOI.ConstraintDualStart} -# in MOI.get, so this hits the ConstraintDual branch. Since no constraints are -# ever added, we just assuem that the dual is `0.0` (this is feasible because) -# the set is really `f(x) in Reals()`, so the dual set is `Zeros()` -function _get_free_start( +function MOI.set( + model::MOI.ModelLike, + attr::MOI.ConstraintPrimalStart, bridge::SplitHyperRectangleBridge{T}, - ::MOI.ConstraintDual, + ::Nothing, ) where {T} - return zeros(T, MOI.dimension(bridge.set)) -end - -# The same cannot be said for ConstraintPrimal because we have no mechanism for -# evaluating the primal of the free rows. Throw an error instead. -function _get_free_start( - ::SplitHyperRectangleBridge, - attr::MOI.ConstraintPrimal, -) - return throw(MOI.GetAttributeNotAllowed(attr)) + if bridge.ci !== nothing + MOI.set(model, attr, bridge.ci, nothing) + end + bridge.primal_start = nothing + return end function MOI.set( @@ -242,30 +221,47 @@ function MOI.set( bridge::SplitHyperRectangleBridge{T}, value::AbstractVector{T}, ) where {T} - if bridge.ci === nothing - return _set_free_start(bridge, attr, value) + bridge.primal_start = value + if bridge.ci !== nothing + new_values = vcat( + T[v - l for (v, l) in zip(value, bridge.set.lower) if isfinite(l)], + T[u - v for (v, u) in zip(value, bridge.set.upper) if isfinite(u)], + ) + MOI.set(model, attr, bridge.ci, new_values) end - new_values = vcat( - T[v - l for (v, l) in zip(value, bridge.set.lower) if isfinite(l)], - T[u - v for (v, u) in zip(value, bridge.set.upper) if isfinite(u)], - ) - MOI.set(model, attr, bridge.ci, new_values) return end +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintPrimalStart, + bridge::SplitHyperRectangleBridge, +) + return bridge.primal_start +end + function MOI.get( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, + attr::MOI.ConstraintPrimal, bridge::SplitHyperRectangleBridge{T}, ) where {T} + ret = zeros(T, MOI.dimension(bridge.set)) + if MOI.output_dimension(bridge.free_rows) > 0 + y = MOI.Utilities.eval_variables(bridge.free_rows) do vi + return MOI.get(model, MOI.VariablePrimal(attr.result_index), vi) + end + k = 0 + for (i, (l, u)) in enumerate(zip(bridge.set.lower, bridge.set.upper)) + if !isfinite(l) && !isfinite(u) + k += 1 + ret[i] = y[k] + end + end + end if bridge.ci === nothing - return _get_free_start(bridge, attr) + return ret end values = MOI.get(model, attr, bridge.ci) - if values === nothing - return nothing - end - ret = zeros(T, MOI.dimension(bridge.set)) row = 0 for (i, l) in enumerate(bridge.set.lower) if isfinite(l) @@ -286,33 +282,51 @@ function MOI.set( model::MOI.ModelLike, attr::MOI.ConstraintDualStart, bridge::SplitHyperRectangleBridge{T}, - values::AbstractVector{T}, + ::Nothing, ) where {T} - if bridge.ci === nothing - return _set_free_start(bridge, attr, values) + if bridge.ci !== nothing + MOI.set(model, attr, bridge.ci, nothing) end - set = bridge.set - new_values = vcat( - T[max(T(0), v) for (v, l) in zip(values, set.lower) if isfinite(l)], - T[max(T(0), -v) for (v, u) in zip(values, set.upper) if isfinite(u)], - ) - MOI.set(model, attr, bridge.ci, new_values) + bridge.dual_start = nothing return end +function MOI.set( + model::MOI.ModelLike, + attr::MOI.ConstraintDualStart, + bridge::SplitHyperRectangleBridge{T}, + value::AbstractVector{T}, +) where {T} + bridge.dual_start = value + if bridge.ci !== nothing + set = bridge.set + new_values = vcat( + T[max(T(0), v) for (v, l) in zip(value, set.lower) if isfinite(l)], + T[max(T(0), -v) for (v, u) in zip(value, set.upper) if isfinite(u)], + ) + MOI.set(model, attr, bridge.ci, new_values) + end + return +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintDualStart, + bridge::SplitHyperRectangleBridge, +) + return bridge.dual_start +end + function MOI.get( model::MOI.ModelLike, - attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, + attr::MOI.ConstraintDual, bridge::SplitHyperRectangleBridge{T}, ) where {T} + ret = zeros(T, MOI.dimension(bridge.set)) if bridge.ci === nothing - return _get_free_start(bridge, attr) + return ret # We don't have any rows. {0} is a feasible dual. end values = MOI.get(model, attr, bridge.ci) - if values === nothing - return nothing - end - ret = zeros(T, MOI.dimension(bridge.set)) row = 0 for (i, l) in enumerate(bridge.set.lower) if isfinite(l) @@ -328,16 +342,3 @@ function MOI.get( end return ret end - -function MOI.set( - model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, - bridge::SplitHyperRectangleBridge{T}, - ::Nothing, -) where {T} - if bridge.ci === nothing - return _set_free_start(bridge, attr, nothing) - end - MOI.set(model, attr, bridge.ci, nothing) - return -end diff --git a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl index 84cfc8a8f3..7d8a7ffab6 100644 --- a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl +++ b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl @@ -94,7 +94,7 @@ function test_runtests_free_row() variables: x, z [1.0 * z + -3.0, 3.3 + -1.0 * z] in Nonnegatives(2) """; - constraint_start = 0.0, + constraint_start = 3.0, ) return end @@ -110,16 +110,26 @@ function test_runtests_all_free_rows() variables: x """, ) - inner = MOI.Utilities.Model{Float64}() + return +end + +function test_constraint_primal_free_rows() + inner = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()) model = MOI.Bridges.Constraint.SplitHyperRectangle{Float64}(inner) - x = MOI.add_variable(model) - f = MOI.Utilities.operate(vcat, Float64, 1.0 * x) - c = MOI.add_constraint(model, f, MOI.HyperRectangle([-Inf], [Inf])) - @test MOI.get(model, MOI.ConstraintDual(), c) == [0.0] - @test_throws( - MOI.GetAttributeNotAllowed{MOI.ConstraintPrimal}, - MOI.get(model, MOI.ConstraintPrimal(), c) + x = MOI.add_variables(model, 2) + y = [1.0 * x[1] + 2.0, 3.0 * x[2] + 4.0] + f = MOI.Utilities.operate(vcat, Float64, y...) + set = MOI.HyperRectangle([-Inf, Inf], [Inf, Inf]) + c = MOI.add_constraint(model, f, set) + + MOI.Utilities.set_mock_optimize!( + inner, + mock -> MOI.Utilities.mock_optimize!(mock, [1.0, 2.0]), ) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + @test ≈(MOI.get(model, MOI.ConstraintDual(), c), [0.0, 0.0]) + @test ≈(MOI.get(model, MOI.ConstraintPrimal(), c), [3.0, 10.0]) return end From 441a89607fb4ec90d21d44177ac65dd606fe9bf8 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 7 Sep 2026 16:25:11 +1200 Subject: [PATCH 2/3] Update --- .../test_SplitHyperRectangleBridge.jl | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl index 7d8a7ffab6..b6792038d6 100644 --- a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl +++ b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl @@ -116,20 +116,24 @@ end function test_constraint_primal_free_rows() inner = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()) model = MOI.Bridges.Constraint.SplitHyperRectangle{Float64}(inner) - x = MOI.add_variables(model, 2) - y = [1.0 * x[1] + 2.0, 3.0 * x[2] + 4.0] + x = MOI.add_variables(model, 3) + y = [1.0 * x[1] + 2.0, 3.0 * x[2] + 4.0, 5.0 * x[3] + 6.0] f = MOI.Utilities.operate(vcat, Float64, y...) - set = MOI.HyperRectangle([-Inf, Inf], [Inf, Inf]) + set = MOI.HyperRectangle([-Inf, 1.0, Inf], [Inf, 2.0, Inf]) c = MOI.add_constraint(model, f, set) - + T = Float64 MOI.Utilities.set_mock_optimize!( inner, - mock -> MOI.Utilities.mock_optimize!(mock, [1.0, 2.0]), + mock -> MOI.Utilities.mock_optimize!( + mock, + T[1, -1, 2], + (MOI.VectorAffineFunction{T}, MOI.Nonnegatives) => [T[1, 0]], + ), ) MOI.optimize!(model) @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test ≈(MOI.get(model, MOI.ConstraintDual(), c), [0.0, 0.0]) - @test ≈(MOI.get(model, MOI.ConstraintPrimal(), c), [3.0, 10.0]) + @test ≈(MOI.get(model, MOI.ConstraintDual(), c), [0.0, 1.0, 0.0]) + @test ≈(MOI.get(model, MOI.ConstraintPrimal(), c), [3.0, 1.0, 16.0]) return end From 54729f83f27e216b8648418376d932b7f862e2ff Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 8 Sep 2026 08:56:16 +1200 Subject: [PATCH 3/3] Update --- .../test_SplitHyperRectangleBridge.jl | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl index b6792038d6..33c39ccc98 100644 --- a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl +++ b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl @@ -119,7 +119,7 @@ function test_constraint_primal_free_rows() x = MOI.add_variables(model, 3) y = [1.0 * x[1] + 2.0, 3.0 * x[2] + 4.0, 5.0 * x[3] + 6.0] f = MOI.Utilities.operate(vcat, Float64, y...) - set = MOI.HyperRectangle([-Inf, 1.0, Inf], [Inf, 2.0, Inf]) + set = MOI.HyperRectangle([-Inf, 1.0, -Inf], [Inf, 2.0, Inf]) c = MOI.add_constraint(model, f, set) T = Float64 MOI.Utilities.set_mock_optimize!( @@ -137,6 +137,26 @@ function test_constraint_primal_free_rows() return end +function test_constraint_primal_all_free_rows() + inner = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()) + model = MOI.Bridges.Constraint.SplitHyperRectangle{Float64}(inner) + x = MOI.add_variables(model, 3) + y = [1.0 * x[1] + 2.0, 3.0 * x[2] + 4.0, 5.0 * x[3] + 6.0] + f = MOI.Utilities.operate(vcat, Float64, y...) + set = MOI.HyperRectangle([-Inf, -Inf, -Inf], [Inf, Inf, Inf]) + c = MOI.add_constraint(model, f, set) + T = Float64 + MOI.Utilities.set_mock_optimize!( + inner, + mock -> MOI.Utilities.mock_optimize!(mock, T[1, -1, 2]), + ) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + @test ≈(MOI.get(model, MOI.ConstraintDual(), c), [0.0, 0.0, 0.0]) + @test ≈(MOI.get(model, MOI.ConstraintPrimal(), c), [3.0, 1.0, 16.0]) + return +end + function test_basic_HyperRectangle() model = MOI.Bridges.Constraint.SplitHyperRectangle{Float64}( MOI.Utilities.Model{Float64}(),