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

Support Logical Complements #118

Merged
merged 4 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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_compliment` argument to prevent creating uncessary binary variables when reformulating:

```julia

@variable(model, Y1, Logical)
@variable(model, Y2, Logical, logical_compliment = 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_compliment` argument to prevent creating uncessary binary variables when reformulating:

```julia

@variable(model, Y1, Logical)
@variable(model, Y2, Logical, logical_compliment = 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_compliment.(lvrefs))
_error("Can only use logical compliment variables in Disjunctions " *
"with two disjuncts.")
elseif length(lvrefs) == 2 && any(has_logical_compliment.(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 compliment variables in a disjunction, " *
"both logical variables must be the compliment 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_compliment.(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 compliment is used
if any(has_logical_compliment.(structure))
_error("Logical compliment 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_compliment.(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_compliment::Union{Nothing, LogicalVariableRef}`: The logical compliment of
this variable if there is one.
"""
struct LogicalVariable <: JuMP.AbstractVariable
fix_value::Union{Nothing, Bool}
start_value::Union{Nothing, Bool}
logical_compliment::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
Loading