From 5c3659fb473d38ab2298191ac9c5c05149032fef Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 8 Jun 2023 16:38:12 +1200 Subject: [PATCH 1/7] [Bridges] add SlackBridgePrimalDualStart --- src/Bridges/Objective/bridges/slack.jl | 73 ++++++++++++++++++++++++++ test/Bridges/Objective/slack.jl | 40 ++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/src/Bridges/Objective/bridges/slack.jl b/src/Bridges/Objective/bridges/slack.jl index e85892324b..9e3d5acb39 100644 --- a/src/Bridges/Objective/bridges/slack.jl +++ b/src/Bridges/Objective/bridges/slack.jl @@ -174,3 +174,76 @@ function MOI.get( g = MOI.Utilities.remove_variable(f, bridge.slack) return MOI.Utilities.convert_approx(G, g) end + +""" + struct SlackBridgePrimalDualStart <: MOI.AbstractModelAttribute end + +[`Bridges.Objective.SlackBridge`](@ref) introduces a new constraint into the +problem. However, because it is not a constraint bridge, it cannot intercept +calls to set [`ConstraintDualStart`](@ref) or [`ConstraintPrimalStart`](@ref). + +As a work-around, set this attribute to `true` to set the primal and dual +start for the new constraint. This attribute must be set after +[`VariablePrimalStart`](@ref). +""" +struct SlackBridgePrimalDualStart <: MOI.AbstractModelAttribute end + +function MOI.throw_set_error_fallback( + ::MOI.ModelLike, + ::SlackBridgePrimalDualStart, + ::Bool, +) + return # Silently ignore if the model does not support. +end + +function MOI.supports( + ::MOI.ModelLike, + ::SlackBridgePrimalDualStart, + ::Type{<:SlackBridge}, +) + return true +end + +function MOI.set( + model::MOI.ModelLike, + ::SlackBridgePrimalDualStart, + b::SlackBridge{T}, + value::Bool, +) where {T} + @assert value + # ConstraintDual: if the objective function had a dual, it would be `-1` for + # the Lagrangian function to be the same. + if MOI.supports(model, MOI.ConstraintDualStart(), typeof(b.constraint)) + MOI.set(model, MOI.ConstraintDualStart(), b.constraint, -one(T)) + end + # ConstraintPrimal: we should set the slack of f(x) - y to be 0, and the + # start of y to be f(x). + if !MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex) || + !MOI.supports(model, MOI.ConstraintPrimalStart(), typeof(b.constraint)) + return + end + MOI.set(model, MOI.VariablePrimalStart(), b.slack, zero(T)) + f = MOI.get(model, MOI.ConstraintFunction(), b.constraint) + f_val = MOI.Utilities.eval_variables(f) do v + return MOI.get(model, MOI.VariablePrimalStart(), v) + end + f_val -= MOI.constant(MOI.get(model, MOI.ConstraintSet(), b.constraint)) + MOI.set(model, MOI.VariablePrimalStart(), b.slack, f_val) + MOI.set(model, MOI.ConstraintPrimalStart(), b.constraint, zero(T)) + return +end + +function MOI.set( + b::MOI.Bridges.AbstractBridgeOptimizer, + attr::SlackBridgePrimalDualStart, + value, +) + if MOI.Bridges.is_objective_bridged(b) + obj_attr = MOI.ObjectiveFunction{function_type(bridges(b))}() + if MOI.Bridges.is_bridged(b, obj_attr) + bridge = MOI.Bridges.bridge(b, obj_attr) + MOI.set(MOI.Bridges.recursive_model(b), attr, bridge, value) + end + end + return +end diff --git a/test/Bridges/Objective/slack.jl b/test/Bridges/Objective/slack.jl index e1c978db25..0fd102a5ed 100644 --- a/test/Bridges/Objective/slack.jl +++ b/test/Bridges/Objective/slack.jl @@ -514,6 +514,46 @@ function test_deletion_of_variable_in_slacked_objective() return end +function test_SlackBridgePrimalDualStart() + inner = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + model = MOI.Bridges.Objective.Slack{Float64}(inner) + x = MOI.add_variable(model) + MOI.add_constraint(model, x, MOI.GreaterThan(2.0)) + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.1, x)], -1.2) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.VariablePrimalStart(), x, 2.0) + MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), true) + vars = MOI.get(inner, MOI.ListOfVariableIndices()) + primal_start = MOI.get.(inner, MOI.VariablePrimalStart(), vars) + @test primal_start[1] ≈ 2.0 + @test primal_start[2] ≈ 1.1 * 2.0 - 1.2 + F = MOI.ScalarAffineFunction{Float64} + cis = MOI.get(inner, MOI.ListOfConstraintIndices{F,MOI.LessThan{Float64}}()) + @test length(cis) == 1 + @test MOI.get(inner, MOI.ConstraintPrimalStart(), cis[1]) ≈ 0.0 + @test MOI.get(inner, MOI.ConstraintDualStart(), cis[1]) ≈ -1.0 + return +end + +function test_SlackBridgePrimalDualStart_unsupported() + inner = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()) + # Check that setting on blank model doesn't error. + MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), true) + model = MOI.Bridges.Objective.Slack{Float64}(inner) + x = MOI.add_variable(model) + MOI.add_constraint(model, x, MOI.GreaterThan(2.0)) + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.1, x)], -1.2) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + MOI.set(model, MOI.VariablePrimalStart(), x, 2.0) + # Unsupported. Should silently skip without error. + MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), true) + return +end + end # module TestObjectiveSlack.runtests() From 1f5a4a47bb6c5622454b1904e1ca34c94866e972 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 9 Jun 2023 10:53:15 +1200 Subject: [PATCH 2/7] Updates --- src/Bridges/Objective/bridges/slack.jl | 15 +++++++++------ test/Bridges/Objective/slack.jl | 10 ++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Bridges/Objective/bridges/slack.jl b/src/Bridges/Objective/bridges/slack.jl index 9e3d5acb39..22fb02d37d 100644 --- a/src/Bridges/Objective/bridges/slack.jl +++ b/src/Bridges/Objective/bridges/slack.jl @@ -182,7 +182,7 @@ end problem. However, because it is not a constraint bridge, it cannot intercept calls to set [`ConstraintDualStart`](@ref) or [`ConstraintPrimalStart`](@ref). -As a work-around, set this attribute to `true` to set the primal and dual +As a work-around, set this attribute to `nothing` to set the primal and dual start for the new constraint. This attribute must be set after [`VariablePrimalStart`](@ref). """ @@ -191,7 +191,7 @@ struct SlackBridgePrimalDualStart <: MOI.AbstractModelAttribute end function MOI.throw_set_error_fallback( ::MOI.ModelLike, ::SlackBridgePrimalDualStart, - ::Bool, + ::Nothing, ) return # Silently ignore if the model does not support. end @@ -201,6 +201,7 @@ function MOI.supports( ::SlackBridgePrimalDualStart, ::Type{<:SlackBridge}, ) + # Pretend that every model supports, and silently skip in set if unsupported return true end @@ -208,9 +209,8 @@ function MOI.set( model::MOI.ModelLike, ::SlackBridgePrimalDualStart, b::SlackBridge{T}, - value::Bool, + ::Nothing, ) where {T} - @assert value # ConstraintDual: if the objective function had a dual, it would be `-1` for # the Lagrangian function to be the same. if MOI.supports(model, MOI.ConstraintDualStart(), typeof(b.constraint)) @@ -236,13 +236,16 @@ end function MOI.set( b::MOI.Bridges.AbstractBridgeOptimizer, attr::SlackBridgePrimalDualStart, - value, + ::Nothing, ) + # TODO(odow): this might fail if the SlackBridge is not the first bridge in + # the chain, but it should be for our current setup of bridges, so we + # choose to simplify this implementation. if MOI.Bridges.is_objective_bridged(b) obj_attr = MOI.ObjectiveFunction{function_type(bridges(b))}() if MOI.Bridges.is_bridged(b, obj_attr) bridge = MOI.Bridges.bridge(b, obj_attr) - MOI.set(MOI.Bridges.recursive_model(b), attr, bridge, value) + MOI.set(MOI.Bridges.recursive_model(b), attr, bridge, nothing) end end return diff --git a/test/Bridges/Objective/slack.jl b/test/Bridges/Objective/slack.jl index 0fd102a5ed..aec3602600 100644 --- a/test/Bridges/Objective/slack.jl +++ b/test/Bridges/Objective/slack.jl @@ -525,7 +525,7 @@ function test_SlackBridgePrimalDualStart() MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) MOI.set(model, MOI.VariablePrimalStart(), x, 2.0) - MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), true) + MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), nothing) vars = MOI.get(inner, MOI.ListOfVariableIndices()) primal_start = MOI.get.(inner, MOI.VariablePrimalStart(), vars) @test primal_start[1] ≈ 2.0 @@ -541,16 +541,14 @@ end function test_SlackBridgePrimalDualStart_unsupported() inner = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()) # Check that setting on blank model doesn't error. - MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), true) + MOI.set(inner, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), nothing) model = MOI.Bridges.Objective.Slack{Float64}(inner) x = MOI.add_variable(model) - MOI.add_constraint(model, x, MOI.GreaterThan(2.0)) - f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.1, x)], -1.2) MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.1, x)], -1.2) MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) - MOI.set(model, MOI.VariablePrimalStart(), x, 2.0) # Unsupported. Should silently skip without error. - MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), true) + MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), nothing) return end From 3f45c1b50d477d992ceb37fc179095171ff2cc78 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 9 Jun 2023 12:14:00 +1200 Subject: [PATCH 3/7] Update slack.jl --- test/Bridges/Objective/slack.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/Bridges/Objective/slack.jl b/test/Bridges/Objective/slack.jl index aec3602600..9b1007c7b0 100644 --- a/test/Bridges/Objective/slack.jl +++ b/test/Bridges/Objective/slack.jl @@ -525,7 +525,9 @@ function test_SlackBridgePrimalDualStart() MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) MOI.set(model, MOI.VariablePrimalStart(), x, 2.0) - MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), nothing) + attr = MOI.Bridges.Objective.SlackBridgePrimalDualStart() + @test MOI.supports(model, attr) + MOI.set(model, attr, nothing) vars = MOI.get(inner, MOI.ListOfVariableIndices()) primal_start = MOI.get.(inner, MOI.VariablePrimalStart(), vars) @test primal_start[1] ≈ 2.0 @@ -539,16 +541,19 @@ function test_SlackBridgePrimalDualStart() end function test_SlackBridgePrimalDualStart_unsupported() + attr = MOI.Bridges.Objective.SlackBridgePrimalDualStart() inner = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()) # Check that setting on blank model doesn't error. - MOI.set(inner, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), nothing) + @test MOI.supports(inner, attr) + MOI.set(inner, attr, nothing) model = MOI.Bridges.Objective.Slack{Float64}(inner) + @test MOI.supports(model, attr) x = MOI.add_variable(model) MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.1, x)], -1.2) MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) # Unsupported. Should silently skip without error. - MOI.set(model, MOI.Bridges.Objective.SlackBridgePrimalDualStart(), nothing) + MOI.set(model, attr, nothing) return end From ce044ddde1619d3344a9afaed3a9b5e56e139e60 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 9 Jun 2023 12:47:24 +1200 Subject: [PATCH 4/7] Update slack.jl --- src/Bridges/Objective/bridges/slack.jl | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Bridges/Objective/bridges/slack.jl b/src/Bridges/Objective/bridges/slack.jl index 22fb02d37d..8a4d94b70d 100644 --- a/src/Bridges/Objective/bridges/slack.jl +++ b/src/Bridges/Objective/bridges/slack.jl @@ -196,14 +196,8 @@ function MOI.throw_set_error_fallback( return # Silently ignore if the model does not support. end -function MOI.supports( - ::MOI.ModelLike, - ::SlackBridgePrimalDualStart, - ::Type{<:SlackBridge}, -) - # Pretend that every model supports, and silently skip in set if unsupported - return true -end +# Pretend that every model supports, and silently skip in set if unsupported +MOI.supports(::MOI.ModelLike, ::SlackBridgePrimalDualStart) = true function MOI.set( model::MOI.ModelLike, From 715389f6ed854d68cba95346554d729c5e1f4fcf Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 9 Jun 2023 13:24:51 +1200 Subject: [PATCH 5/7] Update slack.jl --- src/Bridges/Objective/bridges/slack.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Bridges/Objective/bridges/slack.jl b/src/Bridges/Objective/bridges/slack.jl index 8a4d94b70d..d6a061c64b 100644 --- a/src/Bridges/Objective/bridges/slack.jl +++ b/src/Bridges/Objective/bridges/slack.jl @@ -199,6 +199,13 @@ end # Pretend that every model supports, and silently skip in set if unsupported MOI.supports(::MOI.ModelLike, ::SlackBridgePrimalDualStart) = true +function MOI.supports( + ::MOI.Bridges.AbstractBridgeOptimizer, + ::SlackBridgePrimalDualStart, +) + return true +end + function MOI.set( model::MOI.ModelLike, ::SlackBridgePrimalDualStart, From 775f8d2c6492d0255fcdba9094e6ac88499b9277 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 9 Jun 2023 15:08:48 +1200 Subject: [PATCH 6/7] Update slack.jl --- src/Bridges/Objective/bridges/slack.jl | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Bridges/Objective/bridges/slack.jl b/src/Bridges/Objective/bridges/slack.jl index d6a061c64b..1e6a30b8b7 100644 --- a/src/Bridges/Objective/bridges/slack.jl +++ b/src/Bridges/Objective/bridges/slack.jl @@ -197,14 +197,7 @@ function MOI.throw_set_error_fallback( end # Pretend that every model supports, and silently skip in set if unsupported -MOI.supports(::MOI.ModelLike, ::SlackBridgePrimalDualStart) = true - -function MOI.supports( - ::MOI.Bridges.AbstractBridgeOptimizer, - ::SlackBridgePrimalDualStart, -) - return true -end +MOI.supports_fallback(::MOI.ModelLike, ::SlackBridgePrimalDualStart) = true function MOI.set( model::MOI.ModelLike, From 2eae1bcf4dbc65fa1301f8c8cb1a2c992b04d3db Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 10 Jun 2023 10:51:36 +1200 Subject: [PATCH 7/7] Update slack.jl --- src/Bridges/Objective/bridges/slack.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Bridges/Objective/bridges/slack.jl b/src/Bridges/Objective/bridges/slack.jl index 1e6a30b8b7..739624b4ff 100644 --- a/src/Bridges/Objective/bridges/slack.jl +++ b/src/Bridges/Objective/bridges/slack.jl @@ -205,6 +205,12 @@ function MOI.set( b::SlackBridge{T}, ::Nothing, ) where {T} + # !!! note + # This attribute should silently skip if the `model` does not support it. + # For other attributes, we set `supports(...) = false`, but this would + # cause `copy_to` to throw an `UnsupportedAttributeError`, which we don't + # want. The solution is to check `supports(model, ...)` in this method, + # and bail if not supported. # ConstraintDual: if the objective function had a dual, it would be `-1` for # the Lagrangian function to be the same. if MOI.supports(model, MOI.ConstraintDualStart(), typeof(b.constraint))