Copy_model() equivalent for BilevelJuMP.jl

Is there a way to copy a JuMP model and use it as a building block for the lower level problem in BilevelJuMP.jl. For example:

using JuMP, BilevelJuMP
model = Model()
@variable(model, ...)
@constraint(model, ...)

#Defining bilevel model
new_model = BilevelModel(GLPK.Optimizer, mode = BilevelJuMP.SOS1Mode())

@variable(Lower(new_model ), ...)
@variable(Upper(new_model ), ...)

#Defining the upper model using usual BilevelJuMP syntax
@objective(Upper(new_model ), Min, ...)
@constraints(Upper(new_model ), begin
...
end)

#Made up function (copies the variables and constraints from model to new_model)
copy_model(model, lower(new_model))

#Adding more constraints to the lower level problem (probably using reference_map)
@constraints(Lower(new_model ), begin
...
end)

I see that we have copy_model function in JuMP which returns a copy and reference map of a JuMP model. Is there a way I can use it or create a modified version of it to copy a model to the lower level problem of a bilevel model?

Thank you!

What’s the use-case for this? In general, JuMP extensions don’t compose well because they maintain extra state.

I don’t have a concrete use-case, but I was just thinking if we have a JuMP model already defined (probably using it in some other part of a project), then it might save us some effort to redefine everything using the BilevelJuMP.jl syntax if we have such a copy function.
However, I suppose it would be easier to simply use dualization.jl to create a dual of the problem (which if I am not wrong is a JuMP model again) and maybe create a copy of it using copy_model (if needed) and then directly formulate a single-level problem.

If you have a JuMP model, you probably had a function (or many) that built that.
For instance:

function model_builder()
    model = Model()
    # add variables and constraints
    return model
end
model = model_builder()

You could change to:

model = Model()
function model_builder(model)
    # add variables and constraints
    return
end
model_builder(model)
# at this point `model` would be equivalent to the first version

you can re-use the builder for the lower level of a bilvel problem like this:

model = BilevelModel(...)
function model_builder(model)
    # add variables and constraints
    return
end
model_builder(Lower(model))
# this would load the lower level

In this case you don’t even need to pre-build a model an use a copy. Just re-use a builder function.

3 Likes

Related tutorial: Design patterns for larger models · JuMP

1 Like

@joaquimg This works perfectly as I would want it to. Thank you. I have one additional question – when I try to print out the variables for each level, it counts the inner-level variables as variables in the upper-level problem and vice-versa. To show what I mean:

using JuMP, BilevelJuMP, Gurobi

model = BilevelModel(Gurobi.Optimizer, mode = BilevelJuMP.SOS1Mode())

@variable(Lower(model), x)
@variable(Upper(model), y)

@objective(Upper(model), Min, 3x + y)
@constraints(Upper(model), begin
    x <= 5
    y <= 8
    y >= 0
end)

@objective(Lower(model), Min, -x)
@constraints(Lower(model), begin
     x +  y <= 8
    4x +  y >= 8
    2x +  y <= 13
    2x - 7y <= 0
end)

Now that we have the bilevel model defined, let’s check the variables:

all_variables(model.lower)
#returns
2-element Vector{VariableRef}:
 x
 y   #Should not return this

all_variables(model.upper)
#returns
2-element Vector{VariableRef}:
 x   #Should not return this
 y 

Is this the correct way to access the variables or am I missing something? Just for completeness, I also checked the all_constraints function and it worked as intended.

Very helpful reference! I will spend some time going through this. Thank you.

This is intended behavior. Linking variables (variables that might appear both in upper and lower models) are considered variables for both as they might appear in constraints in both levels.
If you have variables that surely wont be in both levels, you can create them with UpperOnly(model)/LowerOnly(model) (these are considered advanced features though),

That sounds good; however, if I create these exclusive variables using just Lower(model)/Upper(model) does the package figure out itself that it belongs to only one level. So I guess my question is, for exclusive variables should I create them only using LowerOnly(model)/UpperOnly(model)? Is it incorrect if I define them using the usual Lower(model)/Upper(model)? For example, I tried:

using JuMP, Gurobi

model = BilevelModel(Gurobi.Optimizer, mode = BilevelJuMP.SOS1Mode())

@variable(Lower(model), x)
@variable(Lower(model), z) #this variable is exclusive to the lower level
@variable(Upper(model), y)

@objective(Upper(model), Min, 3x + y)
@constraints(Upper(model), begin
    x <= 5
    y <= 8
    y >= 0
end)

@objective(Lower(model), Min, -x + z)
@constraints(Lower(model), begin
     x +  y <= 8z
    4x +  y >= 8
    2x +  y <= 13
    2x - 7y <= 0
end)

On enquiring the variables in each level it returns that z belongs to both levels:

all_variables(model.lower) 
#returns
2-element Vector{VariableRef}:
 x
 y  
 z

all_variables(model.upper)
#returns
2-element Vector{VariableRef}:
 x   
 y 
 z

When I use LowerOnly(model), I don’t see the above behaviour.

No.

Yes, but this is not necessary. There is no problem assuming that all variables might appear in both levels. I do not recommend this feature for beginers.

1 Like

Sounds good. Thanks for the clarifications!

1 Like