Skip to content

Commit 7bbee0c

Browse files
authored
Support Logical Complements (#118)
* Support logical compliments * Update error messages * Update terminology * fix typo
1 parent bcc29c8 commit 7bbee0c

14 files changed

Lines changed: 301 additions & 99 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and
8585
@variable(model, Y[1:3], Logical)
8686
```
8787

88+
When making logical variables for disjunctions with only two disjuncts, we can use the `logical_complement` argument to prevent creating uncessary binary variables when reformulating:
89+
90+
```julia
91+
92+
@variable(model, Y1, Logical)
93+
@variable(model, Y2, Logical, logical_complement = Y1) # Y2 ⇔ ¬Y1
94+
```
95+
8896
## Logical Constraints
8997

9098
Two types of logical constraints are supported:

docs/src/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and
8585
@variable(model, Y[1:3], Logical)
8686
```
8787

88+
When making logical variables for disjunctions with only two disjuncts, we can use the `logical_complement` argument to prevent creating uncessary binary variables when reformulating:
89+
90+
```julia
91+
92+
@variable(model, Y1, Logical)
93+
@variable(model, Y2, Logical, logical_complement = Y1) # Y2 ⇔ ¬Y1
94+
```
95+
8896
## Logical Constraints
8997

9098
Two types of logical constraints are supported:

src/bigm.jl

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -171,63 +171,64 @@ function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::BigM)
171171
return lb, ub
172172
end
173173

174+
# Extend reformulate_disjunct_constraint
174175
function reformulate_disjunct_constraint(
175176
model::JuMP.AbstractModel,
176177
con::JuMP.ScalarConstraint{T, S},
177-
bvref::JuMP.AbstractVariableRef,
178+
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
178179
method::BigM
179180
) where {T, S <: _MOI.LessThan}
180181
M = _get_M_value(con.func, con.set, method)
181-
new_func = JuMP.@expression(model, con.func - M*(1-bvref))
182+
new_func = JuMP.@expression(model, con.func - M*(1 - bvref))
182183
reform_con = JuMP.build_constraint(error, new_func, con.set)
183184
return [reform_con]
184185
end
185186
function reformulate_disjunct_constraint(
186187
model::JuMP.AbstractModel,
187188
con::JuMP.VectorConstraint{T, S, R},
188-
bvref::JuMP.AbstractVariableRef,
189+
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
189190
method::BigM
190191
) where {T, S <: _MOI.Nonpositives, R}
191192
M = [_get_M_value(func, con.set, method) for func in con.func]
192193
new_func = JuMP.@expression(model, [i=1:con.set.dimension],
193-
con.func[i] - M[i]*(1-bvref)
194+
con.func[i] - M[i]*(1 - bvref)
194195
)
195196
reform_con = JuMP.build_constraint(error, new_func, con.set)
196197
return [reform_con]
197198
end
198199
function reformulate_disjunct_constraint(
199200
model::JuMP.AbstractModel,
200201
con::JuMP.ScalarConstraint{T, S},
201-
bvref::JuMP.AbstractVariableRef,
202+
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
202203
method::BigM
203204
) where {T, S <: _MOI.GreaterThan}
204205
M = _get_M_value(con.func, con.set, method)
205-
new_func = JuMP.@expression(model, con.func + M*(1-bvref))
206+
new_func = JuMP.@expression(model, con.func + M*(1 - bvref))
206207
reform_con = JuMP.build_constraint(error, new_func, con.set)
207208
return [reform_con]
208209
end
209210
function reformulate_disjunct_constraint(
210211
model::JuMP.AbstractModel,
211212
con::JuMP.VectorConstraint{T, S, R},
212-
bvref::JuMP.AbstractVariableRef,
213+
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
213214
method::BigM
214215
) where {T, S <: _MOI.Nonnegatives, R}
215216
M = [_get_M_value(func, con.set, method) for func in con.func]
216217
new_func = JuMP.@expression(model, [i=1:con.set.dimension],
217-
con.func[i] + M[i]*(1-bvref)
218+
con.func[i] + M[i]*(1 - bvref)
218219
)
219220
reform_con = build_constraint(error, new_func, con.set)
220221
return [reform_con]
221222
end
222223
function reformulate_disjunct_constraint(
223224
model::JuMP.AbstractModel,
224225
con::JuMP.ScalarConstraint{T, S},
225-
bvref::JuMP.AbstractVariableRef,
226+
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
226227
method::BigM
227228
) where {T, S <: Union{_MOI.Interval, _MOI.EqualTo}}
228229
M = _get_M_value(con.func, con.set, method)
229-
new_func_gt = JuMP.@expression(model, con.func + M[1]*(1-bvref))
230-
new_func_lt = JuMP.@expression(model, con.func - M[2]*(1-bvref))
230+
new_func_gt = JuMP.@expression(model, con.func + M[1]*(1 - bvref))
231+
new_func_lt = JuMP.@expression(model, con.func - M[2]*(1 - bvref))
231232
set_values = _set_values(con.set)
232233
reform_con_gt = build_constraint(error, new_func_gt, _MOI.GreaterThan(set_values[1]))
233234
reform_con_lt = build_constraint(error, new_func_lt, _MOI.LessThan(set_values[2]))
@@ -236,15 +237,15 @@ end
236237
function reformulate_disjunct_constraint(
237238
model::JuMP.AbstractModel,
238239
con::JuMP.VectorConstraint{T, S, R},
239-
bvref::JuMP.AbstractVariableRef,
240+
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
240241
method::BigM
241242
) where {T, S <: _MOI.Zeros, R}
242243
M = [_get_M_value(func, con.set, method) for func in con.func]
243244
new_func_nn = JuMP.@expression(model, [i=1:con.set.dimension],
244-
con.func[i] + M[i][1]*(1-bvref)
245+
con.func[i] + M[i][1]*(1 - bvref)
245246
)
246247
new_func_np = JuMP.@expression(model, [i=1:con.set.dimension],
247-
con.func[i] - M[i][2]*(1-bvref)
248+
con.func[i] - M[i][2]*(1 - bvref)
248249
)
249250
reform_con_nn = JuMP.build_constraint(error, new_func_nn, _MOI.Nonnegatives(con.set.dimension))
250251
reform_con_np = JuMP.build_constraint(error, new_func_np, _MOI.Nonpositives(con.set.dimension))

src/constraints.jl

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,30 @@ function _add_indicator_var(
291291
return
292292
end
293293
# check disjunction
294-
function _check_disjunction(_error, lvrefs::AbstractVector{<:LogicalVariableRef}, model::JuMP.AbstractModel)
294+
function _check_disjunction(
295+
_error,
296+
lvrefs::AbstractVector{<:LogicalVariableRef},
297+
model::M
298+
) where {M <: JuMP.AbstractModel}
295299
isequal(unique(lvrefs), lvrefs) || _error("Not all the logical indicator variables are unique.")
296300
for lvref in lvrefs
297301
if !JuMP.is_valid(model, lvref)
298302
_error("`$lvref` is not a valid logical variable reference.")
299303
end
300304
end
305+
if length(lvrefs) != 2 && any(has_logical_complement.(lvrefs))
306+
_error("Can only use logical complement variables in Disjunctions " *
307+
"with two disjuncts.")
308+
elseif length(lvrefs) == 2 && any(has_logical_complement.(lvrefs))
309+
T = JuMP.value_type(M)
310+
V = JuMP.variable_ref_type(M)
311+
expr1 = convert(JuMP.GenericAffExpr{T, V}, binary_variable(first(lvrefs)))
312+
expr2 = 1 - binary_variable(last(lvrefs))
313+
if !JuMP.isequal_canonical(expr1, expr2)
314+
_error("When using logical complement variables in a disjunction, " *
315+
"both logical variables must be the complement of one another.")
316+
end
317+
end
301318
return lvrefs
302319
end
303320

@@ -349,7 +366,7 @@ function _disjunction(
349366
# create the disjunction
350367
dref = _create_disjunction(_error, model, structure, name, false)
351368
# add the exactly one constraint if desired
352-
if exactly1
369+
if exactly1 && !any(has_logical_complement.(structure))
353370
lvars = JuMP.constraint_object(dref).indicators
354371
func = JuMP.model_convert.(model, Any[1, lvars...])
355372
set = _MOIExactly(length(lvars) + 1)
@@ -385,12 +402,17 @@ function _disjunction(
385402
for (kwarg, _) in extra_kwargs
386403
_error("Unrecognized keyword argument $kwarg.")
387404
end
405+
# check that no logical complement is used
406+
if any(has_logical_complement.(structure))
407+
_error("Logical complement variables are not supported for " *
408+
"use in nested disjunctions.")
409+
end
388410
# create the disjunction
389411
dref = _create_disjunction(_error, model, structure, name, true)
390412
obj = constraint_object(dref)
391413
_add_indicator_var(_DisjunctConstraint(obj, tag.indicator), dref, model)
392414
# add the exactly one constraint if desired
393-
if exactly1
415+
if exactly1 && !any(has_logical_complement.(structure))
394416
lvars = JuMP.constraint_object(dref).indicators
395417
func = LogicalVariableRef{M}[tag.indicator, lvars...]
396418
set = _MOIExactly(length(lvars) + 1)

src/datatypes.jl

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
################################################################################
22
# LOGICAL VARIABLES
33
################################################################################
4+
"""
5+
LogicalVariableIndex
6+
7+
A type for storing the index of a [`LogicalVariable`](@ref).
8+
9+
**Fields**
10+
- `value::Int64`: The index value.
11+
"""
12+
struct LogicalVariableIndex
13+
value::Int64
14+
end
15+
16+
"""
17+
LogicalVariableRef{M <: JuMP.AbstractModel}
18+
19+
A type for looking up logical variables.
20+
"""
21+
struct LogicalVariableRef{M <:JuMP.AbstractModel} <: JuMP.AbstractVariableRef
22+
model::M
23+
index::LogicalVariableIndex
24+
end
25+
426
"""
527
LogicalVariable <: JuMP.AbstractVariable
628
@@ -9,10 +31,13 @@ A variable type the logical variables associated with disjuncts in a [`Disjuncti
931
**Fields**
1032
- `fix_value::Union{Nothing, Bool}`: A fixed boolean value if there is one.
1133
- `start_value::Union{Nothing, Bool}`: An initial guess if there is one.
34+
- `logical_complement::Union{Nothing, LogicalVariableRef}`: The logical complement of
35+
this variable if there is one.
1236
"""
1337
struct LogicalVariable <: JuMP.AbstractVariable
1438
fix_value::Union{Nothing, Bool}
1539
start_value::Union{Nothing, Bool}
40+
logical_complement::Union{Nothing, LogicalVariableRef}
1641
end
1742

1843
# Wrapper variable type for including arbitrary tags that will be used for
@@ -66,28 +91,6 @@ mutable struct LogicalVariableData
6691
name::String
6792
end
6893

69-
"""
70-
LogicalVariableIndex
71-
72-
A type for storing the index of a [`LogicalVariable`](@ref).
73-
74-
**Fields**
75-
- `value::Int64`: The index value.
76-
"""
77-
struct LogicalVariableIndex
78-
value::Int64
79-
end
80-
81-
"""
82-
LogicalVariableRef{M <: JuMP.AbstractModel}
83-
84-
A type for looking up logical variables.
85-
"""
86-
struct LogicalVariableRef{M <:JuMP.AbstractModel} <: JuMP.AbstractVariableRef
87-
model::M
88-
index::LogicalVariableIndex
89-
end
90-
9194
################################################################################
9295
# LOGICAL SELECTOR (CARDINALITY) SETS
9396
################################################################################
@@ -384,12 +387,12 @@ end
384387
mutable struct _Hull{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod
385388
value::T
386389
disjunction_variables::Dict{V, Vector{V}}
387-
disjunct_variables::Dict{Tuple{V, V}, V}
390+
disjunct_variables::Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V}
388391
function _Hull(method::Hull{T}, vrefs::Set{V}) where {T, V <: JuMP.AbstractVariableRef}
389392
new{V, T}(
390393
method.value,
391394
Dict{V, Vector{V}}(vref => V[] for vref in vrefs),
392-
Dict{Tuple{V, V}, V}()
395+
Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V}()
393396
)
394397
end
395398
end
@@ -420,7 +423,7 @@ mutable struct GDPData{M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C
420423
exactly1_constraints::Dict{DisjunctionRef{M}, LogicalConstraintRef{M}}
421424

422425
# Indicator variable mappings
423-
indicator_to_binary::Dict{LogicalVariableRef{M}, V}
426+
indicator_to_binary::Dict{LogicalVariableRef{M}, Union{V, JuMP.GenericAffExpr{T, V}}}
424427
indicator_to_constraints::Dict{LogicalVariableRef{M}, Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}}
425428
constraint_to_indicator::Dict{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}, LogicalVariableRef{M}} # needed for deletion
426429

@@ -443,7 +446,7 @@ mutable struct GDPData{M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C
443446
_MOIUC.CleverDict{DisjunctConstraintIndex, ConstraintData}(),
444447
_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction{M}}}(),
445448
Dict{DisjunctionRef{M}, LogicalConstraintRef{M}}(),
446-
Dict{LogicalVariableRef{M}, V}(),
449+
Dict{LogicalVariableRef{M}, Union{V, JuMP.GenericAffExpr{T, V}}}(),
447450
Dict{LogicalVariableRef{M}, Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}}(),
448451
Dict{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}, LogicalVariableRef{M}}(),
449452
Dict{V, Tuple{T, T}}(),

0 commit comments

Comments
 (0)