Документация Engee

Контейнеры

JuMP предоставляет специальные контейнеры, похожие на AxisArrays. Они позволяют создавать многомерные массивы с нецелочисленными индексами.

Эти контейнеры автоматически создаются макросами JuMP. У всех макросов одинаковый базовый синтаксис:

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

Контейнеры создаются посредством синтаксиса name[key1=index1, index2; optional_condition]. Остальные особенности зависят от конкретного макроса.

Контейнеры могут быть именованными, например name[key=index], или неименованными, например [key=index]. Неименованные контейнеры называются анонимными.

Элементы в квадратных скобках перед символом ; называются множествами индексов. Множества индексов могут быть именованными, например [i = 1:4], или неименованными, например [1:4].

Элемент в квадратных скобках после символа ; называется условием. Условия необязательны.

Помимо стандартных макросов JuMP, таких как @variable и @constraint, которые создают соответственно контейнеры переменных и ограничений, можно создавать контейнеры с произвольными элементами с помощью макроса Containers.@container.

Мы используем этот макрос для объяснения трех типов контейнеров, которые изначально поддерживаются JuMP: Array, Containers.DenseAxisArray и Containers.SparseAxisArray.

Array

Массив Array создается, когда множества индексов являются прямоугольными и имеют форму 1:n.

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

В результате получается обычный массив Julia Array, с которым можно выполнять стандартные действия.

Срезы

Можно создавать срезы массивов.

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

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

Циклы

Для перебора элементов используйте eachindex:

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

Получение множеств индексов

Для получения множеств индексов используйте axes:

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

Трансляция

При трансляции через массив Array возвращается Array.

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

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

Таблицы

Чтобы преобразовать массив Array в вектор Vector{<:NamedTuple}, совместимый с Tables.jl, используйте метод Containers.rowtable:

julia> table = Containers.rowtable(x; header = [:I, :J, :value])
6-element Vector{@NamedTuple{I::Int64, J::Int64, value::Tuple{Int64, Int64}}}:
 (I = 1, J = 1, value = (1, 1))
 (I = 2, J = 1, value = (2, 1))
 (I = 1, J = 2, value = (1, 2))
 (I = 2, J = 2, value = (2, 2))
 (I = 1, J = 3, value = (1, 3))
 (I = 2, J = 3, value = (2, 3))

Так как этот объект поддерживает интерфейс Tables.jl, его можно передать в любую функцию, принимающую таблицу в качестве входных данных:

julia> import DataFrames;

julia> DataFrames.DataFrame(table)
6×3 DataFrame
 Row │ I      J      value
     │ Int64  Int64  Tuple…
─────┼──────────────────────
   1 │     1      1  (1, 1)
   2 │     2      1  (2, 1)
   3 │     1      2  (1, 2)
   4 │     2      2  (2, 2)
   5 │     1      3  (1, 3)
   6 │     2      3  (2, 3)

DenseAxisArray

Массив Containers.DenseAxisArray создается, когда множества индексов являются прямоугольными, но не имеют форму 1:n. Множества индексов могут быть любого типа.

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, [:A, :B]
And data, a 2×2 Matrix{Tuple{Int64, Symbol}}:
 (1, :A)  (1, :B)
 (2, :A)  (2, :B)

Срезы

Можно создавать срезы массивов DenseAxisArray.

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

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

Циклы

Для перебора элементов используйте eachindex:

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

Получение множеств индексов

Для получения множеств индексов используйте axes:

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

Трансляция

При трансляции через массив DenseAxisArray возвращается 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, [:A, :B]
And data, a 2×2 Matrix{Tuple{Symbol, Int64}}:
 (:A, 1)  (:B, 1)
 (:A, 2)  (:B, 2)

Доступ к внутренним данным

Для копирования внутренних данных в новый массив Array используйте Array(x):

julia> Array(x)
2×2 Matrix{Tuple{Int64, Symbol}}:
 (1, :A)  (1, :B)
 (2, :A)  (2, :B)

Для доступа к внутренним данным без копирования используйте x.data.

julia> x.data
2×2 Matrix{Tuple{Int64, Symbol}}:
 (1, :A)  (1, :B)
 (2, :A)  (2, :B)

Таблицы

Чтобы преобразовать массив DenseAxisArray в вектор Vector{<:NamedTuple}, совместимый с Tables.jl, используйте метод Containers.rowtable:

julia> table = Containers.rowtable(x; header = [:I, :J, :value])
4-element Vector{@NamedTuple{I::Int64, J::Symbol, value::Tuple{Int64, Symbol}}}:
 (I = 1, J = :A, value = (1, :A))
 (I = 2, J = :A, value = (2, :A))
 (I = 1, J = :B, value = (1, :B))
 (I = 2, J = :B, value = (2, :B))

Так как этот объект поддерживает интерфейс Tables.jl, его можно передать в любую функцию, принимающую таблицу в качестве входных данных:

julia> import DataFrames;

julia> DataFrames.DataFrame(table)
4×3 DataFrame
 Row │ I      J       value
     │ Int64  Symbol  Tuple…
─────┼────────────────────────
   1 │     1  A       (1, :A)
   2 │     2  A       (2, :A)
   3 │     1  B       (1, :B)
   4 │     2  B       (2, :B)

Индексирование по ключевым словам

Если у всех осей есть имена, можно использовать индексирование по ключевым словам:

julia> x[i = 2, j = :A]
(2, :A)

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

SparseAxisArray

Массив Containers.SparseAxisArray создается, когда множества индексов не являются прямоугольными. Это происходит в двух ситуациях:

Индекс зависит от предыдущего индекса:

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, 1]  =  (1, 1)
  [1, 2]  =  (1, 2)
  [2, 2]  =  (2, 2)

Применяется синтаксис [indices; condition]:

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

Здесь имеются множества индексов i = 1:3, j = [:A, :B], за которыми следует символ ; и условие со значением true или false: i > 1.

Срезы

Поддерживается создание срезов:

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

Циклы

Для перебора элементов используйте eachindex:

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

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

Если SparseAxisArray создается с помощью макроса, итерация выполняется построчно, то есть индексы изменяются справа налево. Например, при итерации по x выше перебирается индекс j, а i остается постоянным. Этот порядок отличается от принятого для Base.Array, где итерация происходит по столбцам, то есть индексы изменяются слева направо.

Трансляция

При трансляции через массив SparseAxisArray возвращается SparseAxisArray.

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

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

Таблицы

Чтобы преобразовать массив SparseAxisArray в вектор Vector{<:NamedTuple}, совместимый с Tables.jl, используйте метод Containers.rowtable:

julia> table = Containers.rowtable(x; header = [:I, :J, :value])
4-element Vector{@NamedTuple{I::Int64, J::Symbol, value::Tuple{Int64, Symbol}}}:
 (I = 2, J = :A, value = (2, :A))
 (I = 2, J = :B, value = (2, :B))
 (I = 3, J = :A, value = (3, :A))
 (I = 3, J = :B, value = (3, :B))

Так как этот объект поддерживает интерфейс Tables.jl, его можно передать в любую функцию, принимающую таблицу в качестве входных данных:

julia> import DataFrames;

julia> DataFrames.DataFrame(table)
4×3 DataFrame
 Row │ I      J       value
     │ Int64  Symbol  Tuple…
─────┼────────────────────────
   1 │     2  A       (2, :A)
   2 │     2  B       (2, :B)
   3 │     3  A       (3, :A)
   4 │     3  B       (3, :B)

Индексирование по ключевым словам

Если у всех осей есть имена, можно использовать индексирование по ключевым словам:

julia> x[i = 2, j = :A]
(2, :A)

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

Принудительное задание типа контейнера

Чтобы использовать тип T для контейнера, передайте container = T. Например:

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

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

Можно также передать DenseAxisArray или SparseAxisArray.

Как выбираются различные типы контейнеров

Если компилятор может определить во время компиляции, что множества индексов являются прямоугольными компактными множествами целых чисел, начинающимися с 1, Containers.@container возвращает массив. Это верно в случае, если множества индексов идентифицируются макросом как 1:n:

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

или как экземпляр Base.OneTo:

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

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

Если компилятор определяет во время компиляции, что множество индексов является прямоугольным, но не обязательно имеет форму 1:n, то вместо этого будет создан массив Containers.DenseAxisArray:

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 Matrix{Int64}:
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8

Что произошло здесь? Хотя мы знаем, что set содержит 1:3, во время компиляции typeof(set) имеет значение UnitRange{Int}. Следовательно, Julia не может доказать, что диапазон начинается с 1 (это выясняется только во время выполнения), и по умолчанию создается DenseAxisArray. Случай, когда мы явно написали i = 1:3, сработал, потому что макрос «видит» начальное значение 1 во время компиляции.

Однако если известно, что индексы образуют массив Array, можно принудительно задать тип контейнера с помощью container = Array:

julia> set = 1:3
1:3

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

Вот еще один похожий пример:

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 Matrix{Int64}:
 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 Matrix{Int64}:
 2  3  4  5  6

Наконец, если компилятор не может установить, что множество индексов является прямоугольным, создается массив Containers.SparseAxisArray.

Это происходит, если некоторые индексы зависят от предыдущего индекса:

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

или если во множествах индексов есть условие:

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

Условие может зависеть от нескольких индексов; единственным требованием является то, чтобы это было выражение, возвращающее true или 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