Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seem right, we should get an UnsupportedAttribute error if it's not supported.
We should implement supports for the bridge so that it returns false if the constraint type does not support it. That way, it would be ignored by copy_to ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only ignore the Name attribute. This is a special case because we want to pretend that every solver supports it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, maybe drop a comment here so that people see it's not the common way of handling starting values and it's not copy-pasted out of context

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

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()