Skip to content

Commit c1d8f6a

Browse files
authored
Merge pull request #709 from matbesancon/indicator-cons
Indicator constraint support
2 parents 9970dbd + caeb4c4 commit c1d8f6a

File tree

7 files changed

+334
-4
lines changed

7 files changed

+334
-4
lines changed

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
88
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
99
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1010
Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
11+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1112

1213
[compat]
1314
julia = "1"
1415

1516
[extras]
16-
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1717

1818
[targets]
19-
test = ["Test"]
19+
test = []

docs/src/apireference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ Semicontinuous
251251
Semiinteger
252252
SOS1
253253
SOS2
254+
IndicatorSet
254255
```
255256

256257
Functions for getting and setting properties of sets.

src/Test/intlinear.jl

Lines changed: 234 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,242 @@ function knapsacktest(model::MOI.ModelLike, config::TestConfig)
356356
end
357357
end
358358

359+
function indicator1_test(model::MOI.ModelLike, config::TestConfig)
360+
atol = config.atol
361+
rtol = config.rtol
362+
# linear problem with indicator constraint
363+
# max 2x1 + 3x2
364+
# s.t. x1 + x2 <= 10
365+
# z1 ==> x2 <= 8
366+
# z2 ==> x2 + x1/5 <= 9
367+
# z1 + z2 >= 1
368+
369+
MOI.empty!(model)
370+
@test MOI.is_empty(model)
371+
372+
@test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}())
373+
@test MOI.supports(model, MOI.ObjectiveSense())
374+
@test MOI.supports_constraint(model, MOI.SingleVariable, MOI.ZeroOne)
375+
@test MOI.supports_constraint(model, MOI.SingleVariable, MOI.Interval{Float64})
376+
@test MOI.supports_constraint(model, MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64})
377+
@test MOI.supports_constraint(model, MOI.VectorAffineFunction{Float64}, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, MOI.LessThan{Float64}})
378+
x1 = MOI.add_variable(model)
379+
x2 = MOI.add_variable(model)
380+
z1 = MOI.add_variable(model)
381+
z2 = MOI.add_variable(model)
382+
MOI.add_constraint(model, z1, MOI.ZeroOne())
383+
MOI.add_constraint(model, z2, MOI.ZeroOne())
384+
f1 = MOI.VectorAffineFunction(
385+
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z1)),
386+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
387+
],
388+
[0.0, 0.0]
389+
)
390+
iset1 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(8.0))
391+
MOI.add_constraint(model, f1, iset1)
392+
393+
f2 = MOI.VectorAffineFunction(
394+
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z2)),
395+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(0.2, x1)),
396+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
397+
],
398+
[0.0, 0.0],
399+
)
400+
iset2 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(9.0))
401+
402+
MOI.add_constraint(model, f2, iset2)
403+
404+
# Additional regular constraint.
405+
MOI.add_constraint(model,
406+
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, x2)], 0.0),
407+
MOI.LessThan(10.0),
408+
)
409+
410+
# Disjunction z1 ⋁ z2
411+
MOI.add_constraint(model,
412+
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, z1), MOI.ScalarAffineTerm(1.0, z2)], 0.0),
413+
MOI.GreaterThan(1.0),
414+
)
415+
416+
MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
417+
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 3.0], [x1, x2]), 0.0)
418+
)
419+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
420+
421+
if config.solve
422+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
423+
424+
MOI.optimize!(model)
425+
426+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL
427+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
428+
@test MOI.get(model, MOI.ObjectiveValue()) 28.75 atol=atol rtol=rtol
429+
@test MOI.get(model, MOI.VariablePrimal(), x1) 1.25 atol=atol rtol=rtol
430+
@test MOI.get(model, MOI.VariablePrimal(), x2) 8.75 atol=atol rtol=rtol
431+
@test MOI.get(model, MOI.VariablePrimal(), z1) 0.0 atol=atol rtol=rtol
432+
@test MOI.get(model, MOI.VariablePrimal(), z2) 1.0 atol=atol rtol=rtol
433+
end
434+
end
435+
436+
function indicator2_test(model::MOI.ModelLike, config::TestConfig)
437+
atol = config.atol
438+
rtol = config.rtol
439+
# linear problem with indicator constraint
440+
# max 2x1 + 3x2 - 30 z2
441+
# s.t. x1 + x2 <= 10
442+
# z1 ==> x2 <= 8
443+
# z2 ==> x2 + x1/5 <= 9
444+
# z1 + z2 >= 1
445+
446+
MOI.empty!(model)
447+
@test MOI.is_empty(model)
448+
449+
# This is the same model as indicator_test1, except that the penalty on z2 forces z1 to be 1.
450+
451+
x1 = MOI.add_variable(model)
452+
x2 = MOI.add_variable(model)
453+
z1 = MOI.add_variable(model)
454+
z2 = MOI.add_variable(model)
455+
MOI.add_constraint(model, z1, MOI.ZeroOne())
456+
MOI.add_constraint(model, z2, MOI.ZeroOne())
457+
f1 = MOI.VectorAffineFunction(
458+
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z1)),
459+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
460+
],
461+
[0.0, 0.0]
462+
)
463+
iset1 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(8.0))
464+
MOI.add_constraint(model, f1, iset1)
465+
466+
f2 = MOI.VectorAffineFunction(
467+
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z2)),
468+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(0.2, x1)),
469+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
470+
],
471+
[0.0, 0.0],
472+
)
473+
iset2 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(9.0))
474+
475+
MOI.add_constraint(model, f2, iset2)
476+
477+
# additional regular constraint
478+
MOI.add_constraint(model,
479+
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, x2)], 0.0),
480+
MOI.LessThan(10.0),
481+
)
482+
483+
# disjunction z1 ⋁ z2
484+
MOI.add_constraint(model,
485+
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, z1), MOI.ScalarAffineTerm(1.0, z2)], 0.0),
486+
MOI.GreaterThan(1.0),
487+
)
488+
489+
# objective penalized on z2
490+
MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
491+
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 3.0, -30.0], [x1, x2, z2]), 0.0)
492+
)
493+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
494+
495+
if config.solve
496+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
497+
498+
MOI.optimize!(model)
499+
500+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL
501+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
502+
@test MOI.get(model, MOI.ObjectiveValue()) 28.0 atol=atol rtol=rtol
503+
@test MOI.get(model, MOI.VariablePrimal(), x1) 2.0 atol=atol rtol=rtol
504+
@test MOI.get(model, MOI.VariablePrimal(), x2) 8.0 atol=atol rtol=rtol
505+
@test MOI.get(model, MOI.VariablePrimal(), z1) 1.0 atol=atol rtol=rtol
506+
@test MOI.get(model, MOI.VariablePrimal(), z2) 0.0 atol=atol rtol=rtol
507+
end
508+
end
509+
510+
function indicator3_test(model::MOI.ModelLike, config::TestConfig)
511+
atol = config.atol
512+
rtol = config.rtol
513+
# linear problem with indicator constraint
514+
# similar to indicator1_test with reversed z1
515+
# max 2x1 + 3x2
516+
# s.t. x1 + x2 <= 10
517+
# z1 == 0 ==> x2 <= 8
518+
# z2 == 1 ==> x2 + x1/5 <= 9
519+
# (1-z1) + z2 >= 1 <=> z2 - z1 >= 0
520+
521+
MOI.empty!(model)
522+
@test MOI.is_empty(model)
523+
524+
@test MOI.supports(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}())
525+
@test MOI.supports(model, MOI.ObjectiveSense())
526+
@test MOI.supports_constraint(model, MOI.SingleVariable, MOI.ZeroOne)
527+
@test MOI.supports_constraint(model, MOI.SingleVariable, MOI.Interval{Float64})
528+
@test MOI.supports_constraint(model, MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64})
529+
@test MOI.supports_constraint(model, MOI.VectorAffineFunction{Float64}, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, MOI.LessThan{Float64}})
530+
x1 = MOI.add_variable(model)
531+
x2 = MOI.add_variable(model)
532+
z1 = MOI.add_variable(model)
533+
z2 = MOI.add_variable(model)
534+
MOI.add_constraint(model, z1, MOI.ZeroOne())
535+
MOI.add_constraint(model, z2, MOI.ZeroOne())
536+
f1 = MOI.VectorAffineFunction(
537+
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z1)),
538+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
539+
],
540+
[0.0, 0.0]
541+
)
542+
iset1 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO}(MOI.LessThan(8.0))
543+
MOI.add_constraint(model, f1, iset1)
544+
545+
f2 = MOI.VectorAffineFunction(
546+
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z2)),
547+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(0.2, x1)),
548+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
549+
],
550+
[0.0, 0.0],
551+
)
552+
iset2 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(9.0))
553+
554+
MOI.add_constraint(model, f2, iset2)
555+
556+
# Additional regular constraint.
557+
MOI.add_constraint(model,
558+
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, x2)], 0.0),
559+
MOI.LessThan(10.0),
560+
)
561+
562+
# Disjunction (1-z1) ⋁ z2
563+
MOI.add_constraint(model,
564+
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-1.0, z1), MOI.ScalarAffineTerm(1.0, z2)], 0.0),
565+
MOI.GreaterThan(0.0),
566+
)
567+
568+
MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
569+
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 3.0], [x1, x2]), 0.0)
570+
)
571+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
572+
573+
if config.solve
574+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
575+
576+
MOI.optimize!(model)
577+
578+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL
579+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
580+
@test MOI.get(model, MOI.ObjectiveValue()) 28.75 atol=atol rtol=rtol
581+
@test MOI.get(model, MOI.VariablePrimal(), x1) 1.25 atol=atol rtol=rtol
582+
@test MOI.get(model, MOI.VariablePrimal(), x2) 8.75 atol=atol rtol=rtol
583+
@test MOI.get(model, MOI.VariablePrimal(), z1) 1.0 atol=atol rtol=rtol
584+
@test MOI.get(model, MOI.VariablePrimal(), z2) 1.0 atol=atol rtol=rtol
585+
end
586+
end
587+
359588
const intlineartests = Dict("knapsack" => knapsacktest,
360589
"int1" => int1test,
361590
"int2" => int2test,
362-
"int3" => int3test)
591+
"int3" => int3test,
592+
"indicator1" => indicator1_test,
593+
"indicator2" => indicator2_test,
594+
"indicator3" => indicator3_test,
595+
)
363596

364597
@moitestset intlinear

src/sets.jl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,57 @@ Base.isapprox(a::T, b::T; kwargs...) where {T <: Union{SOS1, SOS2}} = isapprox(a
435435

436436
dimension(s::Union{SOS1, SOS2}) = length(s.weights)
437437

438+
"""
439+
ActivationCondition
440+
441+
Activation condition for an indicator constraint.
442+
The enum value is used as first type parameter of `IndicatorSet{A,S}`.
443+
"""
444+
@enum ActivationCondition begin
445+
ACTIVATE_ON_ZERO
446+
ACTIVATE_ON_ONE
447+
end
448+
449+
"""
450+
IndicatorSet{A, S <: AbstractScalarSet}(set::S)
451+
452+
``\\{((y, x) \\in \\{0, 1\\} \\times \\mathbb{R}^n : y = 0 \\implies x \\in set\\}``
453+
when `A` is `ACTIVATE_ON_ZERO` and
454+
``\\{((y, x) \\in \\{0, 1\\} \\times \\mathbb{R}^n : y = 1 \\implies x \\in set\\}``
455+
when `A` is `ACTIVATE_ON_ONE`.
456+
457+
`S` has to be a sub-type of `AbstractScalarSet`.
458+
`A` is one of the value of the `ActivationCond` enum.
459+
`IndicatorSet` is used with a `VectorAffineFunction` holding
460+
the indicator variable first.
461+
462+
Example: ``\\{(y, x) \\in \\{0, 1\\} \\times \\mathbb{R}^2 : y = 1 \\implies x_1 + x_2 \\leq 9 \\} ``
463+
464+
```julia
465+
f = MOI.VectorAffineFunction(
466+
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z)),
467+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(0.2, x1)),
468+
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
469+
],
470+
[0.0, 0.0],
471+
)
472+
473+
indicator_set = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(9.0))
474+
475+
MOI.add_constraint(model, f, indicator_set)
476+
```
477+
"""
478+
struct IndicatorSet{A, S <: AbstractScalarSet} <: AbstractVectorSet
479+
set::S
480+
IndicatorSet{A}(set::S) where {A, S <: AbstractScalarSet} = new{A,S}(set)
481+
end
482+
483+
dimension(::IndicatorSet) = 2
484+
485+
function Base.copy(set::IndicatorSet{A,S}) where {A,S}
486+
return IndicatorSet{A}(copy(set.set))
487+
end
488+
438489
# isbits types, nothing to copy
439490
function Base.copy(set::Union{Reals, Zeros, Nonnegatives, Nonpositives,
440491
GreaterThan, LessThan, EqualTo, Interval,

test/Test/intlinear.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,15 @@ MOIT.int3test(mock, config)
2828
MOIU.set_mock_optimize!(mock,
2929
(mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1, 0, 0, 1, 1]))
3030
MOIT.knapsacktest(mock, config)
31+
MOIU.set_mock_optimize!(mock,
32+
(mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.25, 8.75, 0., 1.])
33+
)
34+
MOIT.indicator1_test(mock, config)
35+
MOIU.set_mock_optimize!(mock,
36+
(mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [2.0, 8.0, 1., 0.])
37+
)
38+
MOIT.indicator2_test(mock, config)
39+
MOIU.set_mock_optimize!(mock,
40+
(mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.25, 8.75, 1., 1.])
41+
)
42+
MOIT.indicator3_test(mock, config)

test/model.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
2+
const LessThanIndicatorSetOne{T} = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, MOI.LessThan{T}}
3+
const LessThanIndicatorSetZero{T} = MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO, MOI.LessThan{T}}
4+
15
# Needed by test spread over several files, defining it here make it easier to comment out tests
26
# Model supporting every MOI functions and sets
7+
38
MOIU.@model(Model,
49
(MOI.ZeroOne, MOI.Integer),
510
(MOI.EqualTo, MOI.GreaterThan, MOI.LessThan, MOI.Interval,
@@ -10,7 +15,7 @@ MOIU.@model(Model,
1015
MOI.PositiveSemidefiniteConeTriangle, MOI.PositiveSemidefiniteConeSquare,
1116
MOI.RootDetConeTriangle, MOI.RootDetConeSquare, MOI.LogDetConeTriangle,
1217
MOI.LogDetConeSquare),
13-
(MOI.PowerCone, MOI.DualPowerCone, MOI.SOS1, MOI.SOS2),
18+
(MOI.PowerCone, MOI.DualPowerCone, MOI.SOS1, MOI.SOS2, LessThanIndicatorSetOne, LessThanIndicatorSetZero),
1419
(MOI.SingleVariable,),
1520
(MOI.ScalarAffineFunction, MOI.ScalarQuadraticFunction),
1621
(MOI.VectorOfVariables,),

test/sets.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
"""
2+
MutLessThan{T<:Real} <: MOI.AbstractScalarSet
3+
4+
A mutable `LessThan`-like set to test `copy` of indicator set
5+
"""
6+
mutable struct MutLessThan{T<:Real} <: MOI.AbstractScalarSet
7+
upper::T
8+
MutLessThan(v::T) where {T<:Real} = new{T}(v)
9+
end
10+
11+
Base.copy(mlt::MutLessThan) = MutLessThan(Base.copy(mlt.upper))
12+
113
@testset "Sets" begin
214
@testset "Copy" begin
315
@testset "for $S" for S in [MOI.SOS1, MOI.SOS2]
@@ -6,5 +18,21 @@
618
s_copy.weights[1] = 2.0
719
@test s.weights[1] == 1.0
820
end
21+
@testset "IndicatorSet" begin
22+
s1 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(4.0))
23+
s2 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO}(MOI.GreaterThan(4.0))
24+
s1_copy = copy(s1)
25+
s2_copy = copy(s2)
26+
@test s1_copy isa MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}
27+
@test s1 == s1_copy
28+
@test s2_copy isa MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO}
29+
@test s2 == s2_copy
30+
s3 = MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO}(MutLessThan(4.0))
31+
s3_copy = copy(s3)
32+
@test s3.set.upper 4.0
33+
s3_copy.set.upper = 5.0
34+
@test s3.set.upper 4.0
35+
@test s3_copy.set.upper 5.0
36+
end
937
end
1038
end

0 commit comments

Comments
 (0)