Containers

JuMP provides specialized containers similar to AxisArrays that enable multi-dimensional arrays with non-integer indices.

These containers are created automatically by JuMP's macros. Each macro has the same basic syntax:

@macroname(model, name[key1=index1, index2; optional_condition], other stuff)

The containers are generated by the name[key1=index1, index2; optional_condition] syntax. Everything else is specific to the particular macro.

Containers can be named, e.g., name[key=index], or unnamed, e.g., [key=index]. We call unnamed containers anonymous.

We call the bits inside the square brackets and before the ; the index sets. The index sets can be named, e.g., [i = 1:4], or they can be unnamed, e.g., [1:4].

We call the bit inside the square brackets and after the ; the condition. Conditions are optional.

In addition to the standard JuMP macros like @variable and @constraint, which construct containers of variables and constraints respectively, you can use Containers.@container to construct containers with arbitrary elements.

We will use this macro to explain the three types of containers that are natively supported by JuMP: Array, Containers.DenseAxisArray, and Containers.SparseAxisArray.

Array

An Array is created when the index sets are rectangular and the index sets are of the form 1:n.

julia> Containers.@container(x[i = 1:2, j = 1:3], (i, j))
2×3 Array{Tuple{Int64,Int64},2}:
 (1, 1)  (1, 2)  (1, 3)
 (2, 1)  (2, 2)  (2, 3)

The result is just a normal Julia array, so you can do all the usual things.

Slicing

Arrays can be sliced

julia> x[:, 1]
2-element Array{Tuple{Int64,Int64},1}:
 (1, 1)
 (2, 1)

julia> x[2, :]
3-element Array{Tuple{Int64,Int64},1}:
 (2, 1)
 (2, 2)
 (2, 3)

Looping

Use eachindex to loop over the elements:

julia> for key in eachindex(x)
           println(x[key])
       end
(1, 1)
(2, 1)
(1, 2)
(2, 2)
(1, 3)
(2, 3)

Get the index sets

Use axes to obtain the index sets:

julia> axes(x)
(Base.OneTo(2), Base.OneTo(3))

Broadcasting

Broadcasting over an Array returns an Array

julia> swap(x::Tuple) = (last(x), first(x))
swap (generic function with 1 method)

julia> swap.(x)
2×3 Array{Tuple{Int64,Int64},2}:
 (1, 1)  (2, 1)  (3, 1)
 (1, 2)  (2, 2)  (3, 2)

DenseAxisArray

A Containers.DenseAxisArray is created when the index sets are rectangular, but not of the form 1:n. The index sets can be of any type.

julia> x = Containers.@container([i = 1:2, j = [:A, :B]], (i, j))
2-dimensional DenseAxisArray{Tuple{Int64,Symbol},2,...} with index sets:
    Dimension 1, Base.OneTo(2)
    Dimension 2, Symbol[:A, :B]
And data, a 2×2 Array{Tuple{Int64,Symbol},2}:
 (1, :A)  (1, :B)
 (2, :A)  (2, :B)

Slicing

DenseAxisArrays can be sliced

julia> x[:, :A]
1-dimensional DenseAxisArray{Tuple{Int64,Symbol},1,...} with index sets:
    Dimension 1, Base.OneTo(2)
And data, a 2-element Array{Tuple{Int64,Symbol},1}:
 (1, :A)
 (2, :A)

julia> x[1, :]
1-dimensional DenseAxisArray{Tuple{Int64,Symbol},1,...} with index sets:
    Dimension 1, Symbol[:A, :B]
And data, a 2-element Array{Tuple{Int64,Symbol},1}:
 (1, :A)
 (1, :B)

Looping

Use eachindex to loop over the elements:

julia> for key in eachindex(x)
           println(x[key])
       end
(1, :A)
(2, :A)
(1, :B)
(2, :B)

Get the index sets

Use axes to obtain the index sets:

julia> axes(x)
(Base.OneTo(2), Symbol[:A, :B])

Broadcasting

Broadcasting over a DenseAxisArray returns a DenseAxisArray

julia> swap(x::Tuple) = (last(x), first(x))
swap (generic function with 1 method)

julia> swap.(x)
2-dimensional DenseAxisArray{Tuple{Symbol,Int64},2,...} with index sets:
    Dimension 1, Base.OneTo(2)
    Dimension 2, Symbol[:A, :B]
And data, a 2×2 Array{Tuple{Symbol,Int64},2}:
 (:A, 1)  (:B, 1)
 (:A, 2)  (:B, 2)

SparseAxisArray

A Containers.SparseAxisArray is created when the index sets are non-rectangular. This occurs in two circumstances:

An index depends on a prior index:

julia> Containers.@container([i = 1:2, j = i:2], (i, j))
JuMP.Containers.SparseAxisArray{Tuple{Int64,Int64},2,Tuple{Int64,Int64}} with 3 entries:
  [1, 2]  =  (1, 2)
  [2, 2]  =  (2, 2)
  [1, 1]  =  (1, 1)

The [indices; condition] syntax is used:

julia> x = Containers.@container([i = 1:3, j = [:A, :B]; i > 1 && j == :B], (i, j))
JuMP.Containers.SparseAxisArray{Tuple{Int64,Symbol},2,Tuple{Int64,Symbol}} with 2 entries:
  [2, B]  =  (2, :B)
  [3, B]  =  (3, :B)

Here we have the index sets i = 1:3, j = [:A, :B], followed by ;, and then a condition, which evaluates to true or false: i > 1 && j == :B.

Slicing

Slicing is not supported.

julia> x[:, :B]
ERROR: ArgumentError: Indexing with `:` is not supported by Containers.SparseAxisArray
[...]

Looping

Use eachindex to loop over the elements:

julia> for key in eachindex(x)
           println(x[key])
       end
(2, :B)
(3, :B)

Broadcasting

Broadcasting over a SparseAxisArray returns a SparseAxisArray

julia> swap(x::Tuple) = (last(x), first(x))
swap (generic function with 1 method)

julia> swap.(x)
JuMP.Containers.SparseAxisArray{Tuple{Symbol,Int64},2,Tuple{Int64,Symbol}} with 2 entries:
  [2, B]  =  (:B, 2)
  [3, B]  =  (:B, 3)

How different container types are chosen

If the compiler can prove at compile time that the index sets are rectangular, and indexed by a compact set of integers that start at 1, Containers.@container will return an array. This is the case if your index sets are visible to the macro as 1:n:

julia> Containers.@container([i=1:3, j=1:5], i + j)
3×5 Array{Int64,2}:
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8

or an instance of Base.OneTo:

julia> set = Base.OneTo(3)
Base.OneTo(3)

julia> Containers.@container([i=set, j=1:5], i + j)
3×5 Array{Int64,2}:
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8

If the compiler can prove that the index set is rectangular, but not necessarily of the form 1:n at compile time, then a Containers.DenseAxisArray will be constructed instead:

julia> set = 1:3
1:3

julia> Containers.@container([i=set, j=1:5], i + j)
2-dimensional DenseAxisArray{Int64,2,...} with index sets:
    Dimension 1, 1:3
    Dimension 2, Base.OneTo(5)
And data, a 3×5 Array{Int64,2}:
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8
Info

What happened here? Although we know that set contains 1:3, at compile time the typeof(set) is a UnitRange{Int}. Therefore, Julia can't prove that the range starts at 1 (it only finds this out at runtime), and it defaults to a DenseAxisArray. The case where we explicitly wrote i = 1:3 worked because the macro can "see" the 1 at compile time.

However, if you know that the indices really do form an Array, you can force the container type with container = Array:

julia> set = 1:3
1:3

julia> Containers.@container([i=set, j=1:5], i + j, container = Array)
3×5 Array{Int64,2}:
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8

Here's another example with something similar:

julia> a = 1
1

julia> Containers.@container([i=a:3, j=1:5], i + j)
2-dimensional DenseAxisArray{Int64,2,...} with index sets:
    Dimension 1, 1:3
    Dimension 2, Base.OneTo(5)
And data, a 3×5 Array{Int64,2}:
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8

julia> Containers.@container([i=1:a, j=1:5], i + j)
1×5 Array{Int64,2}:
 2  3  4  5  6

Finally, if the compiler cannot prove that the index set is rectangular, a Containers.SparseAxisArray will be created.

This occurs when some indices depend on a previous one:

julia> Containers.@container([i=1:3, j=1:i], i + j)
JuMP.Containers.SparseAxisArray{Int64,2,Tuple{Int64,Int64}} with 6 entries:
  [3, 1]  =  4
  [3, 2]  =  5
  [3, 3]  =  6
  [2, 2]  =  4
  [1, 1]  =  2
  [2, 1]  =  3

or if there is a condition on the index sets:

julia> Containers.@container([i = 1:5; isodd(i)], i^2)
JuMP.Containers.SparseAxisArray{Int64,1,Tuple{Int64}} with 3 entries:
  [3]  =  9
  [5]  =  25
  [1]  =  1

The condition can depend on multiple indices; it just needs to be a function that returns true or false:

julia> condition(i, j) = isodd(i) && iseven(j)
condition (generic function with 1 method)

julia> Containers.@container([i = 1:2, j = 1:4; condition(i, j)], i + j)
JuMP.Containers.SparseAxisArray{Int64,2,Tuple{Int64,Int64}} with 2 entries:
  [1, 2]  =  3
  [1, 4]  =  5