Pyomo style "blocks" in JuMP

I’m new to Julia/JuMP and I’m wondering if Julia has any capability that replicates “blocks” in Pyomo? In other words the possibility to maintain separate namespaces within your model, and build hierarchical/compositional models from blocks. I’ve come across StructJuMP, but I don’t think this is quite what I’m looking for. Any help would be much appreciated!

3 Likes

The short answer is no.

The longer answer is that you are much freer to structure your code in different ways so that separate namespaces aren’t needed.

In particular, you can use anonymous variables, which aren’t “named” in the Pyomo sense.

Then you can do things like:

model = Model()
x = @variable(model, [1:5])
function foo_constraints(model, y)
    @constraint(model, 2 * y <= 1)
end
foo_constraints(model, x)

You’re also free to wrap variables/constraints/etc inside structs:

struct Block
    x::VariableRef
end
function Block(model::Model)
    x = @variable(model)
    return Block(x)
end

function block_constraint(block::Block)
    model = owner_model(block.x)
    @constraint(model, 2 * block.x <= 1)
end

model = Model()
block = Block(model)
block_constraint(block)

Do you have an example of what you’d like to do with blocks? As @odow described, we encourage users to rely on Julia’s existing constructs for composition, i.e., functions and structs.

Thanks for the responses, this is helpful. So I’m probably more worried about compositional models than namespaces, although that’s helpful to know about anonymous variables. Here’s a simple working example of the kind of thing I’d like to do - excuse the pyomo syntax!

import pyomo.environ as pyo

block_1 = pyo.Block()
block_1.x = pyo.Var(bounds=(0,3))
block_1.p = pyo.Param(initialize=5)

block_2 = pyo.Block()
block_2.x = pyo.Var(bounds=(2,5))
block_2.p = pyo.Param(initialize=-2)

block_3 = pyo.Block()
block_3.block_1 = block_1
block_3.block_2 = block_2

def expr(b):
    return b.block_1.x * b.block_1.p + b.block_2.x * b.block_2.p
block_3.expr = pyo.Expression(rule=expr)

model = pyo.ConcreteModel()
model.block_3 = block_3

def obj_expr(m):
    return m.block_3.expr
model.obj = pyo.Objective(rule=obj_expr)

opt = pyo.SolverFactory("glpk")
opt.solve(model)

print(pyo.value(model.block_3.block_1.x))
print(pyo.value(model.block_3.block_2.x))

A desired feature I haven’t included here would be the ability to reference components from “higher level” blocks on lower level blocks. In general this isn’t a great design pattern, but an example could be defining multiple variables on low level blocks over a single index that is defined at the model (highest) level.

In general, I’d be doing this kind of block construction in a much more programmatic way - main goal is programmatic construction of a model from the bottom up. Also each block could have an arbitrary number of variables, parameters, constraints, expressions etc. Some of these components would be referenced at higher levels of the model.

Is there also anywhere you could point me to that has documentation on struct, can’t seem to find it in the regular JuMP docs. Thanks!

struct is a standard Julia struct.

See Types · The Julia Language.

The “JuMP” version of your Pyomo model could look like:

using JuMP
using GLPK

struct Block
    x::VariableRef
    p::Float64
    function Block(model, lb, ub, p0)
        x = @variable(model, lower_bound = lb, upper_bound = ub)
        return new(x, p0)
    end
end

struct Block3
    block_1::Block
    block_2::Block
end

function expr(b::Block3)
    return b.block_1.x * b.block_1.p + b.block_2.x * b.block_2.p
end

model = Model(with_optimizer(GLPK.Optimizer))
block_1 = Block(model, 0, 3, 5)
block_2 = Block(model, 2, 5, -2)
block_3 = Block3(block_1, block_2)
@objective(model, Min, expr(block_3))
optimize!(model)
value(block_3.block_1.x)
value(block_3.block_2.x)

You could also use Dicts:

using JuMP
using GLPK

function Block(model, lb, ub, p0)
    x = @variable(model, lower_bound = lb, upper_bound = ub)
    return Dict(
        :x => x, 
        :p => p0
    )
end

function block_expr(b)
    return b[:block_1][:x] * b[:block_1][:p] + b[:block_2][:x] * b[:block_2][:p]
end

model = Model(with_optimizer(GLPK.Optimizer))
block_1 = Block(model, 0, 3, 5)
block_2 = Block(model, 2, 5, -2)
block_3 = Dict(
    :block_1 => block_1, 
    :block_2 => block_2
)
@objective(model, Min, block_expr(block_3))
optimize!(model)
value(block_3[:block_1][:x])
value(block_3[:block_2][:x])
1 Like

Thanks, this is super helpful! Final question - I’m likely going to be building up blocks and expressions iteratively. For example, Block3 could contain an arbitrary number of blocks, which may or may not all be added at once, and the expression attached to Block3 could be a sum over all relevant variables on blocks within Block3. Would there be a performance reason to use structs over dicts or vice versa in this context? More generally is there a significant performance loss from using mutable rather than immutable containers in Julia?

1 Like

For example, Block3 could contain an arbitrary number of blocks, which may or may not all be added at once, and the expression attached to Block3 could be a sum over all relevant variables on blocks within Block3.

Use a vector:

struct Block3
    model::Model
    blocks::Vector{Block}
    Block3(model) = new(model, Block[])
end
b = Block3(model)
push!(b.blocks, block_1)
push!(b.blocks, block_2)

function block_expr(b::Block3)
    return @expression(b.model, sum(block.x * block.p for block in b.blocks))
end

In general, JuMP doesn’t provide a lot of “helper” type functions. We expect users to use Julia constructs. If you’re coming from a math. programming background without much programming, there is a learning curve. But in the long run, it lets you structure your code in a much more effective way than if JuMP provided Promo-type functionality.

Would there be a performance reason to use structs over dicts or vice versa in this context? More generally is there a significant performance loss from using mutable rather than immutable containers in Julia?

Short answer is “yes”. But you should try the easiest thing first (e.g., use dictionaries or mutable structs). Then benchmark your code, and only try to optimize the run time if it’s too slow.

For sure, I’ll try diving a bit deeper into the Julia language itself which should illuminate some of the more “helper” type things I want to do. Thanks for the examples!