Modular implementation of optimization problems with JuMP

I just came across the 2019 post Pyomo style "blocks" in JuMP by @jonmat because the more I dig into it, the more I realize that I’m indeed in search of the “namespace” feature mentioned here (and I didn’t know that Pyomo provides this). Indeed, for some other project, I’m may be looking for a Modelica-style object/component oriented structuring of JuMP model.

Now, I understand that in the present state of JuMP it’s indeed possible to implement such things using anonymous variables. However, looking at my “add_variable” wrapper of the classical @variable macro (see below), I feel like I’m just creating a more cumbersome version of @variable (see e.g. the presence of lb and ub keyword args since I cannot use the nice lb <= x <= ub syntax without a macro), for the only reason that I want to store the VariableRef in my container of choice (see model_data::Dict{String,Any}) rather than in the existing global Model container of registered variables.

So my question is: what would be the difficulty to bake this “reference storage hijacking” directly in the @variable macro? Or should I copy-paste the @variable source code and try to adapt it for my use case?

my “add_variable” wrapper

This is the present state of my custom “add_variable” wrapper around @variable (early version discussed in Using anonymous and or symbols to create dynamic variable names? - #3 by pierre-haessig). Main features are:

  • uses anonymous variables to avoid registration in the global namespace of Model
  • but instead store the VariableRef in a Dict
  • and then returns the VariableRef to make it easy to bind it to a similarly name Julia variable in the caller.

but to get these features, as said above, I have to pay the price of a much worse API compared to the original @variable.

"""
    add_var!(model_data::Dict{String,Any}, name::String;
             lb=nothing, ub=nothing, fix=nothing, K::Int=1, stage="")

add a variable to the JuMP `Model` `model_data["model"]` and store the reference
to that variable in `model_data[name]`.

Returns the variable reference.

`name` is also used as the `base_name` of the JuMP variable (used for printing),
with the optional `stage` argument (`Int`, `String`...) used as a suffix.

# Other optional arguments:
- lower and/or upper bounds can be set with `lb` and `ub`
- equal bounds can be set with `fix`
- the variable can be a Vector of length `K` if `K` >1 (timeseries).
"""
function add_var!(model_data::Dict{String,Any}, name::String;
                  lb=nothing, ub=nothing, fix=nothing, K::Int=1, stage="")
    model = model_data["model"]
    name_suffix = "$name$stage"
    
    if K==1
        v = @variable(model, base_name=name_suffix)
    elseif K>1
        v = @variable(model, [1:K], base_name=name_suffix)
    else
        throw(ArgumentError("`K` array size parameter should be >=1"))
    end
    model_data[name] = v

    # Bounds:
    if (lb!==nothing || ub!==nothing) && fix!==nothing
        throw(ArgumentError("`lb` and `ub` bounds cannot be used in conjunction with `fix`"))
    end
    if fix !==nothing
        fix.(v)
    end
    if lb !==nothing
        set_lower_bound.(v, lb)
    end
    if ub !==nothing
        set_upper_bound.(v, ub)
    end
    
    return v
end

which is to be used as

# One shot model setup
md = Dict{String,Any}()
md["model"] = Model(HiGHS.Optimizer)
# add piles of variables:
x  = add_var!(model_data, "x",  lb=0.0, ub=power_rated_gen_max)
y  = add_var!(model_data, "y",  lb=0.0, ub=power_rated_gen_max)