Skip to content

Commit

Permalink
Support Logical Complements (#118)
Browse files Browse the repository at this point in the history
* Support logical compliments

* Update error messages

* Update terminology

* fix typo
  • Loading branch information
pulsipher authored Oct 4, 2024
1 parent bcc29c8 commit 7bbee0c
Show file tree
Hide file tree
Showing 14 changed files with 301 additions and 99 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and
@variable(model, Y[1:3], Logical)
```

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:

```julia

@variable(model, Y1, Logical)
@variable(model, Y2, Logical, logical_complement = Y1) # Y2 ⇔ ¬Y1
```

## Logical Constraints

Two types of logical constraints are supported:
Expand Down
8 changes: 8 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and
@variable(model, Y[1:3], Logical)
```

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:

```julia

@variable(model, Y1, Logical)
@variable(model, Y2, Logical, logical_complement = Y1) # Y2 ⇔ ¬Y1
```

## Logical Constraints

Two types of logical constraints are supported:
Expand Down
29 changes: 15 additions & 14 deletions src/bigm.jl
Original file line number Diff line number Diff line change
Expand Up @@ -171,63 +171,64 @@ function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::BigM)
return lb, ub
end

# Extend reformulate_disjunct_constraint
function reformulate_disjunct_constraint(
model::JuMP.AbstractModel,
con::JuMP.ScalarConstraint{T, S},
bvref::JuMP.AbstractVariableRef,
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
method::BigM
) where {T, S <: _MOI.LessThan}
M = _get_M_value(con.func, con.set, method)
new_func = JuMP.@expression(model, con.func - M*(1-bvref))
new_func = JuMP.@expression(model, con.func - M*(1 - bvref))
reform_con = JuMP.build_constraint(error, new_func, con.set)
return [reform_con]
end
function reformulate_disjunct_constraint(
model::JuMP.AbstractModel,
con::JuMP.VectorConstraint{T, S, R},
bvref::JuMP.AbstractVariableRef,
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
method::BigM
) where {T, S <: _MOI.Nonpositives, R}
M = [_get_M_value(func, con.set, method) for func in con.func]
new_func = JuMP.@expression(model, [i=1:con.set.dimension],
con.func[i] - M[i]*(1-bvref)
con.func[i] - M[i]*(1 - bvref)
)
reform_con = JuMP.build_constraint(error, new_func, con.set)
return [reform_con]
end
function reformulate_disjunct_constraint(
model::JuMP.AbstractModel,
con::JuMP.ScalarConstraint{T, S},
bvref::JuMP.AbstractVariableRef,
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
method::BigM
) where {T, S <: _MOI.GreaterThan}
M = _get_M_value(con.func, con.set, method)
new_func = JuMP.@expression(model, con.func + M*(1-bvref))
new_func = JuMP.@expression(model, con.func + M*(1 - bvref))
reform_con = JuMP.build_constraint(error, new_func, con.set)
return [reform_con]
end
function reformulate_disjunct_constraint(
model::JuMP.AbstractModel,
con::JuMP.VectorConstraint{T, S, R},
bvref::JuMP.AbstractVariableRef,
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
method::BigM
) where {T, S <: _MOI.Nonnegatives, R}
M = [_get_M_value(func, con.set, method) for func in con.func]
new_func = JuMP.@expression(model, [i=1:con.set.dimension],
con.func[i] + M[i]*(1-bvref)
con.func[i] + M[i]*(1 - bvref)
)
reform_con = build_constraint(error, new_func, con.set)
return [reform_con]
end
function reformulate_disjunct_constraint(
model::JuMP.AbstractModel,
con::JuMP.ScalarConstraint{T, S},
bvref::JuMP.AbstractVariableRef,
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
method::BigM
) where {T, S <: Union{_MOI.Interval, _MOI.EqualTo}}
M = _get_M_value(con.func, con.set, method)
new_func_gt = JuMP.@expression(model, con.func + M[1]*(1-bvref))
new_func_lt = JuMP.@expression(model, con.func - M[2]*(1-bvref))
new_func_gt = JuMP.@expression(model, con.func + M[1]*(1 - bvref))
new_func_lt = JuMP.@expression(model, con.func - M[2]*(1 - bvref))
set_values = _set_values(con.set)
reform_con_gt = build_constraint(error, new_func_gt, _MOI.GreaterThan(set_values[1]))
reform_con_lt = build_constraint(error, new_func_lt, _MOI.LessThan(set_values[2]))
Expand All @@ -236,15 +237,15 @@ end
function reformulate_disjunct_constraint(
model::JuMP.AbstractModel,
con::JuMP.VectorConstraint{T, S, R},
bvref::JuMP.AbstractVariableRef,
bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr},
method::BigM
) where {T, S <: _MOI.Zeros, R}
M = [_get_M_value(func, con.set, method) for func in con.func]
new_func_nn = JuMP.@expression(model, [i=1:con.set.dimension],
con.func[i] + M[i][1]*(1-bvref)
con.func[i] + M[i][1]*(1 - bvref)
)
new_func_np = JuMP.@expression(model, [i=1:con.set.dimension],
con.func[i] - M[i][2]*(1-bvref)
con.func[i] - M[i][2]*(1 - bvref)
)
reform_con_nn = JuMP.build_constraint(error, new_func_nn, _MOI.Nonnegatives(con.set.dimension))
reform_con_np = JuMP.build_constraint(error, new_func_np, _MOI.Nonpositives(con.set.dimension))
Expand Down
28 changes: 25 additions & 3 deletions src/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,30 @@ function _add_indicator_var(
return
end
# check disjunction
function _check_disjunction(_error, lvrefs::AbstractVector{<:LogicalVariableRef}, model::JuMP.AbstractModel)
function _check_disjunction(
_error,
lvrefs::AbstractVector{<:LogicalVariableRef},
model::M
) where {M <: JuMP.AbstractModel}
isequal(unique(lvrefs), lvrefs) || _error("Not all the logical indicator variables are unique.")
for lvref in lvrefs
if !JuMP.is_valid(model, lvref)
_error("`$lvref` is not a valid logical variable reference.")
end
end
if length(lvrefs) != 2 && any(has_logical_complement.(lvrefs))
_error("Can only use logical complement variables in Disjunctions " *
"with two disjuncts.")
elseif length(lvrefs) == 2 && any(has_logical_complement.(lvrefs))
T = JuMP.value_type(M)
V = JuMP.variable_ref_type(M)
expr1 = convert(JuMP.GenericAffExpr{T, V}, binary_variable(first(lvrefs)))
expr2 = 1 - binary_variable(last(lvrefs))
if !JuMP.isequal_canonical(expr1, expr2)
_error("When using logical complement variables in a disjunction, " *
"both logical variables must be the complement of one another.")
end
end
return lvrefs
end

Expand Down Expand Up @@ -349,7 +366,7 @@ function _disjunction(
# create the disjunction
dref = _create_disjunction(_error, model, structure, name, false)
# add the exactly one constraint if desired
if exactly1
if exactly1 && !any(has_logical_complement.(structure))
lvars = JuMP.constraint_object(dref).indicators
func = JuMP.model_convert.(model, Any[1, lvars...])
set = _MOIExactly(length(lvars) + 1)
Expand Down Expand Up @@ -385,12 +402,17 @@ function _disjunction(
for (kwarg, _) in extra_kwargs
_error("Unrecognized keyword argument $kwarg.")
end
# check that no logical complement is used
if any(has_logical_complement.(structure))
_error("Logical complement variables are not supported for " *
"use in nested disjunctions.")
end
# create the disjunction
dref = _create_disjunction(_error, model, structure, name, true)
obj = constraint_object(dref)
_add_indicator_var(_DisjunctConstraint(obj, tag.indicator), dref, model)
# add the exactly one constraint if desired
if exactly1
if exactly1 && !any(has_logical_complement.(structure))
lvars = JuMP.constraint_object(dref).indicators
func = LogicalVariableRef{M}[tag.indicator, lvars...]
set = _MOIExactly(length(lvars) + 1)
Expand Down
55 changes: 29 additions & 26 deletions src/datatypes.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
################################################################################
# LOGICAL VARIABLES
################################################################################
"""
LogicalVariableIndex
A type for storing the index of a [`LogicalVariable`](@ref).
**Fields**
- `value::Int64`: The index value.
"""
struct LogicalVariableIndex
value::Int64
end

"""
LogicalVariableRef{M <: JuMP.AbstractModel}
A type for looking up logical variables.
"""
struct LogicalVariableRef{M <:JuMP.AbstractModel} <: JuMP.AbstractVariableRef
model::M
index::LogicalVariableIndex
end

"""
LogicalVariable <: JuMP.AbstractVariable
Expand All @@ -9,10 +31,13 @@ A variable type the logical variables associated with disjuncts in a [`Disjuncti
**Fields**
- `fix_value::Union{Nothing, Bool}`: A fixed boolean value if there is one.
- `start_value::Union{Nothing, Bool}`: An initial guess if there is one.
- `logical_complement::Union{Nothing, LogicalVariableRef}`: The logical complement of
this variable if there is one.
"""
struct LogicalVariable <: JuMP.AbstractVariable
fix_value::Union{Nothing, Bool}
start_value::Union{Nothing, Bool}
logical_complement::Union{Nothing, LogicalVariableRef}
end

# Wrapper variable type for including arbitrary tags that will be used for
Expand Down Expand Up @@ -66,28 +91,6 @@ mutable struct LogicalVariableData
name::String
end

"""
LogicalVariableIndex
A type for storing the index of a [`LogicalVariable`](@ref).
**Fields**
- `value::Int64`: The index value.
"""
struct LogicalVariableIndex
value::Int64
end

"""
LogicalVariableRef{M <: JuMP.AbstractModel}
A type for looking up logical variables.
"""
struct LogicalVariableRef{M <:JuMP.AbstractModel} <: JuMP.AbstractVariableRef
model::M
index::LogicalVariableIndex
end

################################################################################
# LOGICAL SELECTOR (CARDINALITY) SETS
################################################################################
Expand Down Expand Up @@ -384,12 +387,12 @@ end
mutable struct _Hull{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod
value::T
disjunction_variables::Dict{V, Vector{V}}
disjunct_variables::Dict{Tuple{V, V}, V}
disjunct_variables::Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V}
function _Hull(method::Hull{T}, vrefs::Set{V}) where {T, V <: JuMP.AbstractVariableRef}
new{V, T}(
method.value,
Dict{V, Vector{V}}(vref => V[] for vref in vrefs),
Dict{Tuple{V, V}, V}()
Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V}()
)
end
end
Expand Down Expand Up @@ -420,7 +423,7 @@ mutable struct GDPData{M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C
exactly1_constraints::Dict{DisjunctionRef{M}, LogicalConstraintRef{M}}

# Indicator variable mappings
indicator_to_binary::Dict{LogicalVariableRef{M}, V}
indicator_to_binary::Dict{LogicalVariableRef{M}, Union{V, JuMP.GenericAffExpr{T, V}}}
indicator_to_constraints::Dict{LogicalVariableRef{M}, Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}}
constraint_to_indicator::Dict{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}, LogicalVariableRef{M}} # needed for deletion

Expand All @@ -443,7 +446,7 @@ mutable struct GDPData{M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C
_MOIUC.CleverDict{DisjunctConstraintIndex, ConstraintData}(),
_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction{M}}}(),
Dict{DisjunctionRef{M}, LogicalConstraintRef{M}}(),
Dict{LogicalVariableRef{M}, V}(),
Dict{LogicalVariableRef{M}, Union{V, JuMP.GenericAffExpr{T, V}}}(),
Dict{LogicalVariableRef{M}, Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}}(),
Dict{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}, LogicalVariableRef{M}}(),
Dict{V, Tuple{T, T}}(),
Expand Down
Loading

0 comments on commit 7bbee0c

Please sign in to comment.