Skip to content

[Bridges] fix supports for VariableIndex: Take II #1992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 11, 2022
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
55 changes: 47 additions & 8 deletions src/Bridges/bridge_optimizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1345,16 +1345,55 @@ end
function MOI.supports(
b::AbstractBridgeOptimizer,
attr::MOI.AbstractConstraintAttribute,
IndexType::Type{MOI.ConstraintIndex{F,S}},
::Type{MOI.ConstraintIndex{F,S}},
) where {F,S}
if is_bridged(b, F, S)
bridge = Constraint.concrete_bridge_type(b, F, S)
return MOI.supports(recursive_model(b), attr, bridge)
elseif F == MOI.Utilities.variable_function_type(S) && is_bridged(b, S)
bridge = Variable.concrete_bridge_type(b, S)
return MOI.supports(recursive_model(b), attr, bridge)
# !!! warning
# This function is slightly confusing, because we need to account for
# the different ways in which a constraint might be added.
if F == MOI.Utilities.variable_function_type(S)
# These are VariableIndex and VectorOfVariable constraints.
if is_bridged(b, S)
Copy link
Member

Choose a reason for hiding this comment

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

Suppose you have a solver that supports

  • Adding variables constrained on creation in S or T
  • Adding a constraint VAF-in-T

You add variables on creation to S
Then you add a constraint on this vector of variables in T. This is bridged into VAF-in-T.
Here, is_bridged(b, T) is false with a LazyBridgeOptimizer.

There are cases in which both might be possible, it depends whether it was constrained on creation or not. In that case, supports should return true if one of the two cases is true because there is a chance that it is supported.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm going around in circles. Is there a way to tell if a VectorOfVariables was added by add_constrained_variables? Not really just from the type, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

So I think we actually have a much bigger issue that means we can't get this right.

The SingleBridgeOptimizer tests are failing for things like flip_sign.jl, because if you add a VectorOfVariables-in-Nonnegatives constraint, the solver supports the constraint and the attribute. But if you add constrained variables in Nonnegatives, the solver force bridges (even though the model supports it) and the bridge doesn't support the attribute.

But, we can't distinguish whether to return true or false, because given a constraint index we don't know whether it was added as a constraint or a constrained variable.

Copy link
Member

@blegat blegat Sep 8, 2022

Choose a reason for hiding this comment

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

Exactly, given just the type, you cannot know how all constraint of that type will be dealt with.You need to consider all possible cases and return true if at least one case supports it

given a constraint index we don't know whether it was added as a constraint or a constrained variable.

Given a constraint index you can know that but not given just the types

# If S needs to be bridged, it usually means that either there is a
# variable bridge, or that there is a free variable followed by a
# constraint bridge (i.e., the two cases handled below).
#
# However, it might be the case, like the tests in
# Variable/flip_sign.jl, that the model supports F-in-S constraints,
# but force-bridges S sets. If so, we might be in the unusual
# situation where we support the attribute if the index was added
# via add_constraint, but not if it was added via
# add_constrained_variable. Because MOI lacks the ability to tell
# which happened just based on the type, we're going to default to
# asking the variable bridge, at the risk of a false negative.
if is_variable_bridged(b, S)
bridge = Variable.concrete_bridge_type(b, S)
return MOI.supports(recursive_model(b), attr, bridge)
else
bridge = Constraint.concrete_bridge_type(b, F, S)
return MOI.supports(recursive_model(b), attr, bridge)
end
else
# If S doesn't need to be bridged, it usually means that either the
# solver supports add_constrained_variable, or it supports free
# variables and add_constraint.
#
# In some cases, it might be that the solver supports
# add_constrained_variable, but ends up bridging add_constraint.
# Because MOI lacks the ability to tell which one was called based
# on the index type, asking the model might give a false negative
# (we support the attribute via add_constrained_variable, but the
# bridge doesn't via add_constraint because it will be bridged).
return MOI.supports(b.model, attr, MOI.ConstraintIndex{F,S})
end
else
return MOI.supports(b.model, attr, IndexType)
# These are normal add_constraints, so we just check if they are
# bridged.
if is_bridged(b, F, S)
bridge = Constraint.concrete_bridge_type(b, F, S)
return MOI.supports(recursive_model(b), attr, bridge)
else
return MOI.supports(b.model, attr, MOI.ConstraintIndex{F,S})
end
end
end

Expand Down
85 changes: 85 additions & 0 deletions test/Bridges/bridge_optimizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,91 @@ function test_IssueIpopt333_supports_ConstraintDualStart_VariableIndex()
return
end

mutable struct _Issue1992 <: MOI.AbstractOptimizer
supports::Bool
variables::Int
constraints::Int
_Issue1992(flag) = new(flag, 0, 0)
end

function MOI.supports_add_constrained_variables(
::_Issue1992,
::Type{<:Union{MOI.Nonpositives,MOI.Nonnegatives}},
)
return true
end

function MOI.add_constrained_variables(
model::_Issue1992,
set::S,
) where {S<:Union{MOI.Nonpositives,MOI.Nonnegatives}}
model.variables += 1
ci = MOI.ConstraintIndex{MOI.VectorOfVariables,S}(model.variables)
return MOI.VariableIndex.(1:set.dimension), ci
end

function MOI.add_constraint(
model::_Issue1992,
::F,
::S,
) where {F<:MOI.VectorAffineFunction{Float64},S<:MOI.Nonnegatives}
model.constraints += 1
return MOI.ConstraintIndex{F,S}(model.constraints)
end

function MOI.supports_constraint(
::_Issue1992,
::Type{MOI.VectorAffineFunction{Float64}},
::Type{MOI.Nonnegatives},
)
return true
end

function MOI.supports(
model::_Issue1992,
::MOI.ConstraintDualStart,
::Type{MOI.ConstraintIndex{F,MOI.Nonnegatives}},
) where {F<:MOI.VectorAffineFunction{Float64}}
return model.supports
end

function test_Issue1992_supports_ConstraintDualStart_VariableIndex()
# supports should be false
model = MOI.Bridges.full_bridge_optimizer(_Issue1992(false), Float64)
x, _ = MOI.add_constrained_variables(model, MOI.Nonpositives(1))
c = MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.Nonnegatives(1))
@test !MOI.supports(model, MOI.ConstraintDualStart(), typeof(c))
# supports should be true
model = MOI.Bridges.full_bridge_optimizer(_Issue1992(true), Float64)
x, _ = MOI.add_constrained_variables(model, MOI.Nonpositives(1))
c = MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.Nonnegatives(1))
# !!! warning
# This test is broken with a false negative. See the discussion in
# PR#1992.
@test_broken MOI.supports(model, MOI.ConstraintDualStart(), typeof(c))
return
end

function test_bridge_supports_issue_1992()
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
model = MOI.Bridges.Variable.NonposToNonneg{Float64}(inner)
x = MOI.add_variable(model)
c = MOI.add_constraint(
model,
MOI.VectorOfVariables([x]),
MOI.Nonpositives(1),
)
# !!! warning
# This test is broken with a false negative. (Getting and setting the
# attribute works, even though supports is false) See the discussion in
# PR#1992.
@test_broken MOI.supports(model, MOI.ConstraintDualStart(), typeof(c))
@test MOI.get(model, MOI.ConstraintDualStart(), c) === nothing
MOI.set(model, MOI.ConstraintDualStart(), c, [1.0])
@test MOI.get(model, MOI.ConstraintDualStart(), c) == [1.0]
return
end

end # module

TestBridgeOptimizer.runtests()