Skip to content
49 changes: 49 additions & 0 deletions src/datatypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,52 @@ mutable struct GDPData{M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C
)
end
end

################################################################################
# VARIABLE INFO
################################################################################
"""
VariableProperties{L, U, F, S, SET, T}

A type for storing variable properties and attributes that can be applied to JuMP variables.
This is used to capture and transfer variable information between models during reformulation.

**Fields**
- `info::JuMP.VariableInfo{L, U, F, S}`: JuMP's VariableInfo struct containing bounds, fixed values, start values, and binary/integer constraints.
- `name::String`: The variable name.
- `set::SET`: The constraint set the variable belongs to (if any), obtained via `JuMP.moi_set`.
- `variable_type::T`: The variable type information, critical for extensions.

**Type Parameters**
- `L, U, F, S`: Type parameters from JuMP.VariableInfo for lower bound, upper bound, fixed value, and start value types.
- `SET`: Type of the constraint set the variable belongs to.
- `T`: Type of the variable type information.

**Constructor**
`VariableProperties(vref::JuMP.GenericVariableRef{T})` creates a VariableProperties instance
from a JuMP variable reference, automatically extracting all relevant properties.
"""
mutable struct VariableProperties{L, U, F, S, SET, T}
info::JuMP.VariableInfo{L, U, F, S}
name::String
set::SET
variable_type::T
end

function VariableProperties(vref::JuMP.GenericVariableRef{T}) where T
info = JuMP.VariableInfo(
JuMP.has_lower_bound(vref),
JuMP.has_lower_bound(vref) ? JuMP.lower_bound(vref) : zero(T),
JuMP.has_upper_bound(vref),
JuMP.has_upper_bound(vref) ? JuMP.upper_bound(vref) : zero(T),
JuMP.is_fixed(vref),
JuMP.is_fixed(vref) ? JuMP.fix_value(vref) : zero(T),
!isnothing(JuMP.start_value(vref)),
JuMP.start_value(vref),
JuMP.is_binary(vref),
JuMP.is_integer(vref)
)
name = JuMP.name(vref)
set = JuMP.is_variable_in_set(vref) ? JuMP.moi_set(JuMP.constraint_object(JuMP.VariableInSetRef(vref))) : nothing
return VariableProperties(info, name, set, nothing)
end
52 changes: 15 additions & 37 deletions src/hull.jl
Original file line number Diff line number Diff line change
@@ -1,48 +1,12 @@
################################################################################
# VARIABLE DISAGGREGATION
################################################################################
"""
requires_disaggregation(vref::JuMP.AbstractVariableRef)::Bool

Return a `Bool` whether `vref` requires disaggregation for the [`Hull`](@ref)
reformulation. This is intended as an extension point for interfaces with
DisjunctiveProgramming that use variable reference types that are not
`JuMP.GenericVariableRef`s. Errors if `vref` is not a `JuMP.GenericVariableRef`.
See also [`make_disaggregated_variable`](@ref).
"""
requires_disaggregation(vref::JuMP.GenericVariableRef) = true
function requires_disaggregation(::V) where {V}
error("`Hull` method does not support expressions with variable " *
"references of type `$V`.")
end

"""
make_disaggregated_variable(
model::JuMP.AbstractModel,
vref::JuMP.AbstractVariableRef,
name::String,
lower_bound::Number,
upper_bound::Number
)::JuMP.AbstractVariableRef

Creates and adds a variable to `model` with name `name` and bounds `lower_bound`
and `upper_bound` based on the original variable `vref`. This is used to
create dissagregated variables needed for the [`Hull`](@ref) reformulation.
This is implemented for `model::JuMP.GenericModel` and
`vref::JuMP.GenericVariableRef`, but it serves as an extension point for
interfaces with other model/variable reference types. This also requires
the implementation of [`requires_disaggregation`](@ref).
"""
function make_disaggregated_variable(
model::JuMP.GenericModel,
vref::JuMP.GenericVariableRef,
name,
lb,
ub
)
return JuMP.@variable(model, base_name = name, lower_bound = lb, upper_bound = ub)
end

function _disaggregate_variables(
model::JuMP.AbstractModel,
lvref::LogicalVariableRef,
Expand All @@ -65,7 +29,21 @@ function _disaggregate_variable(
)
#create disaggregated vref
lb, ub = variable_bound_info(vref)
dvref = make_disaggregated_variable(model, vref, "$(vref)_$(lvref)", lb, ub)
T = JuMP.value_type(typeof(model))
info = JuMP.VariableInfo(
true, # has_lb = true
lb, # lower_bound = lb
true, # has_ub = true
ub, # upper_bound = ub
false, # has_fix = false
zero(T), # fixed_value = 0
false, # has_start = false
zero(T), # start = 0
false, # binary = false
false # integer = false
)
properties = VariableProperties(info, "$(vref)_$(lvref)", nothing, nothing)
dvref = create_variable(model, properties)
push!(_reformulation_variables(model), dvref)
#get binary indicator variable
bvref = binary_variable(lvref)
Expand Down
60 changes: 60 additions & 0 deletions src/variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,63 @@ end
function _interrogate_variables(interrogator::Function, other)
error("Cannot extract variables from object of type $(typeof(other)).")
end

################################################################################
# VARIABLE CREATION #
################################################################################

"""
create_variable(model::JuMP.AbstractModel, props::VariableProperties)::JuMP.AbstractVariableRef

Creates and adds a JuMP variable to `model` using the properties specified in `props`.
This function applies all variable attributes from the `VariableProperties` object
including binary/integer constraints, bounds, fixed values, start values, and constraint sets.

The function first creates a variable object using `_make_variable_object`, optionally applies
any constraint sets if `props.set` is not nothing, and then adds the variable to the model
with the specified name.
"""
function create_variable(model::JuMP.AbstractModel, props::VariableProperties)
var = _make_variable_object(props)
if !isnothing(props.set)
var = JuMP.build_variable(error, var, props.set)
end
return JuMP.add_variable(model, var, props.name)
end


"""
_make_variable_object(props::VariableProperties)::Any

Constructs a JuMP variable object from the given `VariableProperties`.
If the `variable_type` field is `nothing`, dispatches to `JuMP.build_variable`
with only the variable info; otherwise, passes `variable_type` as a tag/type.

Returns a JuMP variable object that can be added to a model.
"""
function _make_variable_object(
props::VariableProperties{L, U, F, S, SET, Nothing}
) where {L, U, F, S, SET}
return JuMP.build_variable(error, props.info)
end

function _make_variable_object(props::VariableProperties)
return JuMP.build_variable(error, props.info, props.variable_type)
end


"""
variable_copy(model::JuMP.AbstractModel, vref::JuMP.AbstractVariableRef)::JuMP.AbstractVariableRef

Creates a copy of the variable `vref` in the given `model`. The new variable will have
the same properties (bounds, fixed value, start value, name, and type) as `vref` but will
be added to `model` as a distinct variable. This is useful for transferring variables
between models or duplicating variable definitions in reformulations.
"""
function variable_copy(
model::JuMP.AbstractModel,
vref::JuMP.AbstractVariableRef
)
props = VariableProperties(vref)
return create_variable(model, props)
end
Comment thread
dnguyen227 marked this conversation as resolved.
Comment thread
dnguyen227 marked this conversation as resolved.
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ include("model.jl")
include("jump.jl")
include("variables/query.jl")
include("variables/logical.jl")
include("variables/creation.jl")
include("constraints/selector.jl")
include("constraints/proposition.jl")
include("constraints/disjunct.jl")
Expand Down
3 changes: 0 additions & 3 deletions test/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,3 @@ function JuMP.add_constraint(
return DP._add_logical_constraint(model, c, name)
end
DP.requires_disaggregation(::MyVarRef) = true
function DP.make_disaggregated_variable(model::MyModel, ::MyVarRef, name, lb, ub)
return JuMP.@variable(model, base_name = name, lower_bound = lb, upper_bound = ub)
end
134 changes: 134 additions & 0 deletions test/variables/creation.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
function test_VariableProperties_constructor()
model = Model()

@variable(model, x, lower_bound = 2.5)
x_props = DP.VariableProperties(x)
@test x_props.info.has_lb == true
@test x_props.info.lower_bound == 2.5
@test x_props.info.has_ub == false
@test x_props.info.upper_bound == 0.0
@test x_props.info.has_fix == false
@test x_props.info.fixed_value == 0.0
@test x_props.info.has_start == false
@test x_props.name == "x"
@test x_props.set === nothing
@test x_props.variable_type === nothing

@variable(model, y, start = 3.14)
y_props = DP.VariableProperties(y)
@test y_props.info.has_start == true
@test y_props.info.start == 3.14
@test y_props.info.has_lb == false
@test y_props.info.lower_bound == 0.0

@variable(model, z == 7.5)
z_props = DP.VariableProperties(z)
@test z_props.info.has_fix == true
@test z_props.info.fixed_value == 7.5
@test z_props.info.has_lb == false
@test z_props.info.lower_bound == 0.0
end

function test_make_variable_object()
model = Model()

@variable(model, x, lower_bound = 1.0, upper_bound = 5.0)
props = DP.VariableProperties(x)

modified_info = JuMP.VariableInfo(
props.info.has_lb,
props.info.lower_bound,
props.info.has_ub,
10.0, # Modified upper bound
props.info.has_fix,
props.info.fixed_value,
props.info.has_start,
props.info.start,
props.info.binary,
props.info.integer
)

props.info = modified_info

var_obj = DP._make_variable_object(props)
@test var_obj.info.upper_bound == 10.0
end

function test_create_variable()
model1 = Model()
model2 = Model()

@variable(model1, x, lower_bound = 1.0, upper_bound = 5.0, start = 2.0)
props_no_set = DP.VariableProperties(x)

@test props_no_set.set === nothing

var_no_set = DP.create_variable(model2, props_no_set)

@test var_no_set !== nothing
@test JuMP.name(var_no_set) == "x"
@test JuMP.has_lower_bound(var_no_set) == true
@test JuMP.lower_bound(var_no_set) == 1.0
@test JuMP.has_upper_bound(var_no_set) == true
@test JuMP.upper_bound(var_no_set) == 5.0
@test JuMP.has_start_value(var_no_set) == true
@test JuMP.start_value(var_no_set) == 2.0

@test var_no_set in JuMP.all_variables(model2)
@test !(var_no_set in JuMP.all_variables(model1))
end

function test_complete_workflow()
model1 = Model()
model2 = Model()

@variable(model1, original, lower_bound = 1, upper_bound = 5, start = 3.0)

props = DP.VariableProperties(original)

recreated = DP.create_variable(model2, props)

@test JuMP.has_lower_bound(recreated) == JuMP.has_lower_bound(original)
@test JuMP.lower_bound(recreated) == JuMP.lower_bound(original)
@test JuMP.has_upper_bound(recreated) == JuMP.has_upper_bound(original)
@test JuMP.upper_bound(recreated) == JuMP.upper_bound(original)
@test JuMP.has_start_value(recreated) == JuMP.has_start_value(original)
@test JuMP.start_value(recreated) == JuMP.start_value(original)
@test JuMP.name(recreated) == JuMP.name(original)

@test original in JuMP.all_variables(model1)
@test recreated in JuMP.all_variables(model2)
@test !(original in JuMP.all_variables(model2))
@test !(recreated in JuMP.all_variables(model1))
end

function test_variable_copy()
model1 = Model()
model2 = Model()

@variable(model1, original, lower_bound = 1, upper_bound = 5, start = 3.0)

props = DP.VariableProperties(original)

recreated = DP.create_variable(model2, props)

@test JuMP.has_lower_bound(recreated) == JuMP.has_lower_bound(original)
@test JuMP.lower_bound(recreated) == JuMP.lower_bound(original)
@test JuMP.has_upper_bound(recreated) == JuMP.has_upper_bound(original)
@test JuMP.upper_bound(recreated) == JuMP.upper_bound(original)
@test JuMP.has_start_value(recreated) == JuMP.has_start_value(original)
@test JuMP.start_value(recreated) == JuMP.start_value(original)
@test JuMP.name(recreated) == JuMP.name(original)
println("##################################")
@test original in JuMP.all_variables(model1)
@test recreated in JuMP.all_variables(model2)
@test !(original in JuMP.all_variables(model2))
@test !(recreated in JuMP.all_variables(model1))
end

@testset "Variable Creation" begin
test_VariableProperties_constructor()
test_make_variable_object()
test_create_variable()
test_complete_workflow()
end
Loading