Skip to content
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

indicator to SOS bridge #877

Merged
merged 40 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7506209
indicator to SOS bridge
matbesancon Sep 11, 2019
bf829d9
fix type signatures
matbesancon Sep 11, 2019
f4a32ef
register bridge
matbesancon Sep 11, 2019
11f5032
added tests
matbesancon Sep 11, 2019
b2be67d
ignore benchmarks
matbesancon Sep 11, 2019
62ae22b
remove equalto constraint
matbesancon Sep 16, 2019
94665e7
remove ambiguity
matbesancon Sep 16, 2019
23fd06b
file reformatting
matbesancon Sep 24, 2019
3e54c05
split files
matbesancon Sep 24, 2019
32e2493
Merge branch 'master' into indicator-sos
matbesancon Oct 30, 2019
fc3102d
revisions
matbesancon Oct 30, 2019
4fd52d6
remove redundant constraint from bridge
matbesancon Oct 30, 2019
c5b0b2f
add single bridge
matbesancon Oct 30, 2019
2946115
convert to VariableIndex
matbesancon Oct 30, 2019
3a6a676
add intermediate expression
matbesancon Nov 4, 2019
32d7baa
replace VAF with abstract vector function
matbesancon Nov 5, 2019
cb78e29
fix VAF
matbesancon Nov 5, 2019
de49ca4
Merge branch 'master' into indicator-sos
matbesancon Nov 5, 2019
89ff760
Merge branch 'master' into indicator-sos
matbesancon Nov 7, 2019
c753d2f
added get for set and func
matbesancon Nov 12, 2019
91a4c0a
remove log
matbesancon Nov 12, 2019
6813017
Merge branch 'master' into indicator-sos
matbesancon Nov 12, 2019
2cf8729
added delete
matbesancon Nov 12, 2019
8995b14
test model equals
matbesancon Nov 14, 2019
08fc80f
does not need adding binary
matbesancon Nov 14, 2019
e35ffd1
failing delete test
matbesancon Nov 14, 2019
7a8cd2f
add get for bridge
matbesancon Nov 14, 2019
f7c2e85
add primal start
matbesancon Nov 14, 2019
a01b091
add supports
matbesancon Nov 14, 2019
2fb4b59
added get tests
matbesancon Nov 15, 2019
3603585
more tests
matbesancon Nov 18, 2019
007e23e
fix test types
matbesancon Nov 19, 2019
970d801
added test for mock bridged
matbesancon Nov 20, 2019
7938dd5
use attr
matbesancon Nov 21, 2019
4d4a8f0
replace attr with N
matbesancon Nov 21, 2019
9a7aaca
variable attr propagation
matbesancon Nov 22, 2019
dbe41e2
remove ConstraintPrimal
matbesancon Nov 25, 2019
0c8ad66
reset test VariablePrimal
matbesancon Nov 25, 2019
ec89295
replace rebuilt attribute with attr to propage result number
matbesancon Nov 26, 2019
5d3ab8c
no primal start N
matbesancon Nov 26, 2019
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
docs/build/
docs/site/
Manifest.toml
test/Benchmarks/*.json
5 changes: 3 additions & 2 deletions src/Bridges/Constraint/Constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ const RSOCtoPSD{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{RSOCtoPSDBridge{T}
include("indicator.jl")

"""
add_all_bridges(bridged_model, T::Type)
add_all_bridges(bridged_model, ::Type{T})

Add all bridges defined in the `Bridges.Constraint` submodule to
`bridged_model`. The coefficient type used is `T`.
"""
function add_all_bridges(bridged_model, T::Type)
function add_all_bridges(bridged_model, ::Type{T}) where {T}
blegat marked this conversation as resolved.
Show resolved Hide resolved
MOIB.add_bridge(bridged_model, GreaterToLessBridge{T})
MOIB.add_bridge(bridged_model, LessToGreaterBridge{T})
MOIB.add_bridge(bridged_model, NonnegToNonposBridge{T})
Expand All @@ -94,6 +94,7 @@ function add_all_bridges(bridged_model, T::Type)
# then to `PSD` produces a smaller SDP constraint.
MOIB.add_bridge(bridged_model, RSOCtoPSDBridge{T})
MOIB.add_bridge(bridged_model, IndicatorActiveOnFalseBridge{T})
MOIB.add_bridge(bridged_model, IndicatorSOS1Bridge{T})
return
end

Expand Down
86 changes: 86 additions & 0 deletions src/Bridges/Constraint/indicator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,89 @@ function concrete_bridge_type(::Type{<:IndicatorActiveOnFalseBridge},
::Type{MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO, S}}) where {F<:MOI.VectorAffineFunction, S<:MOI.AbstractScalarSet}
return IndicatorActiveOnFalseBridge{Float64, F, S}
end

## Indicator constraint by SOS1 bridge

"""
matbesancon marked this conversation as resolved.
Show resolved Hide resolved
IndicatorSOS1Bridge{T, BC <: Union{MOI.LessThan{T}, MOI.GreaterThan{T}, MOI.EqualTo{T}}}

The `IndicatorSOS1Bridge` replaces an indicator constraint of the following form:
``z \\in \\mathbb{B}, z == 1 \\implies f(x) \\leq b`` with a SOS1 constraint:
``z \\in \\mathbb{B}, w \\leq 0, f(x) + w \\leq b, SOS1(w, z)``.
`GreaterThan` constraints are handled in a symmetric way:
``z \\in \\mathbb{B}, z == 1 \\implies f(x) \\geq b`` is reformulated as:
``z \\in \\mathbb{B}, w \\geq 0, f(x) + w \\geq b, SOS1(w, z)``.
`EqualTo` constraints are handled without a bound constraint:
``z \\in \\mathbb{B}, z == 1 \\implies f(x) == b`` is reformulated as:
``z \\in \\mathbb{B}, w \\text{ free}, f(x) + w == b, SOS1(w, z)``.

Copy link
Member

Choose a reason for hiding this comment

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

Why not always add a free w so that it works for any indicator constraint (also MOI.Interval, ...) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can we have a free fallback with all constraints but LessThan, GreaterThan, otherwise we add a free, which has to be re-bounded by two variables for LP& MILP solvers

Copy link
Member

Choose a reason for hiding this comment

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

Not sure I understood "which has to be re-bounded by two variables"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry yes, if we create the variable w free, it will be replaced by w+ >= 0, w- >= 0, w = w+ + w- instead of creating a bounded w right away

Copy link
Member

Choose a reason for hiding this comment

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

That's also the case for SDP solvers with the Variable.FreeBridge. Which LP solver does not support free variables ? This transformation is done internally ?
For a solver supporting free variables such constraints may increase the size of the problem, we should probably have informative constraints but that's out of the scope of this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Which LP solver does not support free variables ? This transformation is done internally ?

All solvers (that I am aware of) support free variables, but the transformation needs to be done internally somehow. The simplex algorithm does require positive variables

Copy link
Member

Choose a reason for hiding this comment

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

If you want to use the standard form you have equality constraints and nonnegative variables but then the dual has free variables. Some solvers might solve the dual using the simplex in which case they need all variables to be free. Although that's the textbook simplex so I don't know whether solvers might use a more sophisticated one supporting mixing free and non-free ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you want to use the standard form you have equality constraints and nonnegative variables but then the dual has free variables.

My understanding is that the standard form is a form A x <= b, x >= 0 transformed in Ax + s = b, x, s >= 0. You need to have basic columns in the equality, so starting from a generic A x = b, it will need to change the variable system in [A; -A] x <= [b; -b] and then add slacks, in order to have as many basic columns as constraints.

If `BC <: EqualTo`, `bound_constraint_index` is `nothing`.
"""
struct IndicatorSOS1Bridge{T, BC <: Union{MOI.LessThan{T}, MOI.GreaterThan{T}, MOI.EqualTo{T}}, MaybeBC <: Union{MOI.ConstraintIndex{MOI.SingleVariable, BC}, Nothing}} <: AbstractBridge
w_variable_index::MOI.VariableIndex
bound_constraint_index::MaybeBC
sos_constraint_index::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SOS1{T}}
linear_constraint_index::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, BC}
end

function bridge_constraint(::Type{IndicatorSOS1Bridge{T,BC,MaybeBC}}, model::MOI.ModelLike, f::MOI.VectorAffineFunction{T}, s::MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, BC}) where {T <: Real, BC, MaybeBC}
f_scalars = MOIU.eachscalar(f)
(w, bound_constraint) = _add_bound_constraint!(model, BC)
z = f_scalars[1].terms[1].variable_index
sos_vector = MOI.VectorOfVariables([w, z])
sos_constraint = MOI.add_constraint(model, sos_vector, MOI.SOS1{T}([0.4, 0.6]))
affine_func = copy(f_scalars[2])
push!(
affine_func.terms,
MOI.ScalarAffineTerm{T}(one(T), w)
)
linear_constraint = MOI.add_constraint(model, affine_func, s.set)
return IndicatorSOS1Bridge{T,BC,MaybeBC}(w, bound_constraint, sos_constraint, linear_constraint)
end

function _add_bound_constraint!(model::MOI.ModelLike, ::Type{BC}) where {T <: Real, BC <: Union{MOI.LessThan{T}, MOI.GreaterThan{T}}}
return MOI.add_constrained_variable(model, BC(zero(T)))
end

function _add_bound_constraint!(model::MOI.ModelLike, ::Type{<:MOI.EqualTo})
return (MOI.add_variable(model), nothing)
end

function MOI.supports_constraint(::Type{<:IndicatorSOS1Bridge},
::Type{<:MOI.VectorAffineFunction},
::Type{<:MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, <: Union{MOI.LessThan, MOI.GreaterThan, MOI.EqualTo}}})
return true
end

function MOIB.added_constrained_variable_types(::Type{<:IndicatorSOS1Bridge{T, BC}}) where {T, BC <: Union{MOI.LessThan{T}, MOI.GreaterThan{T}}}
return [(MOI.ZeroOne,), (BC,)]
end

function MOIB.added_constrained_variable_types(::Type{<:IndicatorSOS1Bridge{T, <:MOI.EqualTo}}) where {T}
return [(MOI.ZeroOne,)]
end

function MOIB.added_constraint_types(::Type{<:IndicatorSOS1Bridge{T, BC}}) where {T, BC <: Union{MOI.LessThan{T}, MOI.GreaterThan{T}}}
return [(MOI.SingleVariable, BC),
(MOI.VectorOfVariables, MOI.SOS1{T}),
(MOI.ScalarAffineFunction{T}, BC),
]
end

function MOIB.added_constraint_types(::Type{<:IndicatorSOS1Bridge{T, <:MOI.EqualTo}}) where {T}
return [(MOI.VectorOfVariables, MOI.SOS1{T}),
(MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}),
]
end

function concrete_bridge_type(::Type{<:IndicatorSOS1Bridge},
::Type{<:MOI.VectorAffineFunction},
::Type{MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, S}}) where {T, S<:Union{MOI.LessThan{T}, MOI.GreaterThan{T}}}
return IndicatorSOS1Bridge{T, S, MOI.ConstraintIndex{MOI.SingleVariable, S}}
end

function concrete_bridge_type(::Type{<:IndicatorSOS1Bridge},
::Type{<:MOI.VectorAffineFunction},
::Type{MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, MOI.EqualTo{T}}}) where {T}
return IndicatorSOS1Bridge{T, MOI.EqualTo{T}, Nothing}
end
93 changes: 91 additions & 2 deletions test/Bridges/Constraint/indicator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const MOIB = MathOptInterface.Bridges

include("../utilities.jl")

@testset "Indicator" begin
@testset "Indicator activated on 0" begin
# linear problem with indicator constraint
# similar to indicator1_test with reversed z1
# max 2x1 + 3x2
Expand Down Expand Up @@ -41,7 +41,7 @@ include("../utilities.jl")
BT2 = MOIB.Constraint.concrete_bridge_type(MOIB.Constraint.IndicatorActiveOnFalseBridge, typeof(f1), typeof(iset1))
bridge = MOIB.Constraint.bridge_constraint(BT, model, f1, iset1)

@test BT == BT2
@test BT === BT2
@test bridge isa BT

z1comp = bridge.variable_index
Expand All @@ -53,3 +53,92 @@ include("../utilities.jl")
@test t.coefficient ≈ 1.0
end
end

Copy link
Member

Choose a reason for hiding this comment

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

We should also have a test with basic_constraint_test (see #750) and a test against indicator1, indicator2 or indicator3 (https://github.com/JuliaOpt/MathOptInterface.jl/blob/98a0e472e1757db14b849bbba8fe84597856def2/src/Test/intlinear.jl#L609-L611) with test_model_equals (see #820)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't manage to make that work (yet)

@testset "Indicator by SOS1" begin
# linear problem with indicator constraints
blegat marked this conversation as resolved.
Show resolved Hide resolved
# max 2x1 + 3x2
# s.t. x1 + x2 <= 10
# z1 == 1 ==> x2 <= 8
# z2 == 1 ==> x1 + x2 == 9
# z3 == 1 ==> x1 >= 5
# z1 + z2 >= 1
model = MOIU.MockOptimizer(MOIU.Model{Float64}());
config = MOIT.TestConfig()

x1 = MOI.add_variable(model)
x2 = MOI.add_variable(model)
z1 = MOI.add_variable(model)
z2 = MOI.add_variable(model)
z3 = MOI.add_variable(model)

vc1 = MOI.add_constraint(model, z1, MOI.ZeroOne())
vc2 = MOI.add_constraint(model, z2, MOI.ZeroOne())
vc3 = MOI.add_constraint(model, z3, MOI.ZeroOne())
f1 = MOI.VectorAffineFunction(
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z1)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
],
[0.0, 0.0]
)
iset1 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(8.0))

f2 = MOI.VectorAffineFunction(
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z2)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x1)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
],
[0.0, 0.0]
)
iset2 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.EqualTo(9.0))

f3 = MOI.VectorAffineFunction(
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z3)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x1)),
],
[0.0, 0.0]
)
iset3 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.GreaterThan(5.0))


BT1 = MOIB.Constraint.concrete_bridge_type(MOIB.Constraint.IndicatorSOS1Bridge{Float64}, typeof(f1), typeof(iset1))
bridge1 = MOIB.Constraint.bridge_constraint(BT1, model, f1, iset1)
@test BT1 <: MOIB.Constraint.IndicatorSOS1Bridge{Float64, <:MOI.LessThan, <:MOI.ConstraintIndex}
@test bridge1 isa BT1

BT2 = MOIB.Constraint.concrete_bridge_type(MOIB.Constraint.IndicatorSOS1Bridge{Float64}, typeof(f2), typeof(iset2))
bridge2 = MOIB.Constraint.bridge_constraint(BT2, model, f2, iset2)
@test BT2 <: MOIB.Constraint.IndicatorSOS1Bridge{Float64, <:MOI.EqualTo, Nothing}
@test bridge2 isa BT2

BT3 = MOIB.Constraint.concrete_bridge_type(MOIB.Constraint.IndicatorSOS1Bridge{Float64}, typeof(f3), typeof(iset3))
bridge3 = MOIB.Constraint.bridge_constraint(BT3, model, f3, iset3)
@test BT3 <: MOIB.Constraint.IndicatorSOS1Bridge{Float64, <:MOI.GreaterThan, <:MOI.ConstraintIndex}
@test bridge3 isa BT3

w1 = bridge1.w_variable_index
@test MOI.get(model, MOI.ConstraintFunction(), bridge1.bound_constraint_index) == MOI.SingleVariable(w1)
@test MOI.get(model, MOI.ConstraintSet(), bridge1.bound_constraint_index) == MOI.LessThan(0.0)
@test MOI.get(model, MOI.ConstraintFunction(), bridge1.sos_constraint_index) == MOI.VectorOfVariables([w1, z1])
lin_cons1 = bridge1.linear_constraint_index
lin_func1 = MOI.get(model, MOI.ConstraintFunction(), lin_cons1)
@test lin_func1 ≈ MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x2), MOI.ScalarAffineTerm(1.0, w1)], 0.0)
@test MOI.get(model, MOI.ConstraintSet(), lin_cons1) == MOI.LessThan(8.0)

w2 = bridge2.w_variable_index
@test bridge2.bound_constraint_index === nothing
@test MOI.get(model, MOI.ConstraintFunction(), bridge2.sos_constraint_index) == MOI.VectorOfVariables([w2, z2])
lin_cons2 = bridge2.linear_constraint_index
lin_func2 = MOI.get(model, MOI.ConstraintFunction(), lin_cons2)
@test lin_func2 ≈ MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, x2), MOI.ScalarAffineTerm(1.0, w2)], 0.0)
@test MOI.get(model, MOI.ConstraintSet(), lin_cons2) == MOI.EqualTo(9.0)

w3 = bridge3.w_variable_index
@test MOI.get(model, MOI.ConstraintFunction(), bridge3.bound_constraint_index) == MOI.SingleVariable(w3)
@test MOI.get(model, MOI.ConstraintSet(), bridge3.bound_constraint_index) == MOI.GreaterThan(0.0)
@test MOI.get(model, MOI.ConstraintFunction(), bridge3.sos_constraint_index) == MOI.VectorOfVariables([w3, z3])
lin_cons3 = bridge3.linear_constraint_index
lin_func3 = MOI.get(model, MOI.ConstraintFunction(), lin_cons3)
@test lin_func3 ≈ MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, w3)], 0.0)
@test MOI.get(model, MOI.ConstraintSet(), lin_cons3) == MOI.GreaterThan(5.0)

end
27 changes: 27 additions & 0 deletions test/Bridges/lazy_bridge_optimizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,21 @@ MOIU.@model(ModelNoZeroIndicator,
(MOI.VectorOfVariables,),
(MOI.VectorAffineFunction, MOI.VectorQuadraticFunction))

MOIU.@model(ModelNoIndicator,
(MOI.ZeroOne, MOI.Integer),
(MOI.EqualTo, MOI.GreaterThan, MOI.LessThan, MOI.Interval,
MOI.Semicontinuous, MOI.Semiinteger),
(MOI.Reals, MOI.Zeros, MOI.Nonnegatives, MOI.Nonpositives,
MOI.NormInfinityCone, MOI.NormOneCone,
MOI.SecondOrderCone, MOI.RotatedSecondOrderCone,
MOI.GeometricMeanCone, MOI.ExponentialCone, MOI.DualExponentialCone,
MOI.PositiveSemidefiniteConeTriangle, MOI.PositiveSemidefiniteConeSquare,
MOI.RootDetConeTriangle, MOI.RootDetConeSquare, MOI.LogDetConeTriangle,
MOI.LogDetConeSquare),
(MOI.PowerCone, MOI.DualPowerCone, MOI.SOS1, MOI.SOS2),
(), (MOI.ScalarAffineFunction, MOI.ScalarQuadraticFunction),
(MOI.VectorOfVariables,),
(MOI.VectorAffineFunction, MOI.VectorQuadraticFunction))

@testset "Bridge adding no constraint" begin
mock = MOIU.MockOptimizer(NothingModel{Int}())
Expand Down Expand Up @@ -462,6 +477,18 @@ end
MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO, MOI.LessThan{Float64}})
@test MOI.supports_constraint(full_bridged_mock_indicator, MOI.VectorAffineFunction{Float64},
MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO, MOI.LessThan{Float64}})

mock_sos_indicator = MOIU.MockOptimizer(ModelNoIndicator{Float64}())
full_bridged_mock_sos_indicator = MOIB.full_bridge_optimizer(mock_sos_indicator, Float64)
@test !MOI.supports_constraint(mock_sos_indicator, MOI.VectorAffineFunction{Float64},
MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, MOI.LessThan{Float64}})
@test !MOI.supports_constraint(mock_sos_indicator, MOI.VectorAffineFunction{Float64},
MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, MOI.EqualTo{Float64}})
@test MOI.supports_constraint(full_bridged_mock_sos_indicator, MOI.VectorAffineFunction{Float64},
MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, MOI.LessThan{Float64}})
@test MOI.supports_constraint(full_bridged_mock_sos_indicator, MOI.VectorAffineFunction{Float64},
MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, MOI.EqualTo{Float64}})

@testset "Unslack" begin
for T in [Float64, Int]
no_variable_mock = MOIU.MockOptimizer(NoVariableModel{T}())
Expand Down