Skip to content

Commit

Permalink
[Bridges] add Bridges.Constraint.SlackBridgePrimalDualStart (#2194)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Jun 11, 2023
1 parent 597f5aa commit 6b2192a
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 0 deletions.
76 changes: 76 additions & 0 deletions src/Bridges/Objective/bridges/slack.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,79 @@ 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 `nothing` 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,
::Nothing,
)
return # Silently ignore if the model does not support.
end

# Pretend that every model supports, and silently skip in set if unsupported
MOI.supports_fallback(::MOI.ModelLike, ::SlackBridgePrimalDualStart) = true

function MOI.set(
model::MOI.ModelLike,
::SlackBridgePrimalDualStart,
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))
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,
::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, nothing)
end
end
return
end
43 changes: 43 additions & 0 deletions test/Bridges/Objective/slack.jl
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,49 @@ 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)
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
@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()
attr = MOI.Bridges.Objective.SlackBridgePrimalDualStart()
inner = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}())
# Check that setting on blank model doesn't error.
@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, attr, nothing)
return
end

end # module

TestObjectiveSlack.runtests()

0 comments on commit 6b2192a

Please sign in to comment.