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

Расширения

В JuMP доступны различные способы расширения базовых возможностей моделирования.

Данный раздел все еще находится в стадии разработки. Лучшим источником новых идей и помощи в написании нового расширения JuMP являются существующие расширения JuMP. Вот их примеры:

Совместимость

При написании расширений JuMP следует внимательно изучить гарантии совместимости, предоставляемые JuMP. В частности:

  • Все функции, структуры и константы, которые не начинаются с символа подчеркивания (_), являются общедоступными. Они всегда безопасны в использовании и должны иметь соответствующую документацию.

  • Все идентификаторы, начинающиеся с символа подчеркивания (_), являются частными. Их использование небезопасно, так как их работа может нарушиться в любом выпуске JuMP, включая выпуски исправлений.

  • Если в документации явно не указано иное, все поля структуры являются частными. Их использование небезопасно, так как их работа может нарушиться в любом выпуске JuMP, включая выпуски исправлений. Примером поля, которое можно безопасно использовать, является словарь расширений model.ext, который задокументирован в разделе The extension dictionary.

В целом мы настоятельно рекомендуем использовать только общедоступный API JuMP. Если вам не хватает какой-либо функции, создайте проблему на GitHub.

Однако если вы все же используете частный API (например, потому что запрошенная вами функция еще не реализована), то вам следует строго ограничить версии JuMP, с которыми совместим ваш пакет, в файле Project.toml. Проще всего сделать это с помощью спецификаторов в виде дефиса. Например, если ваш пакет поддерживает все версии JuMP от v1.0.0 до v1.1.1, добавьте такую строку:

JuMP = "1.0.0 - 1.1.1"

В дальнейшем при выпуске каждой новой версии JuMP вам необходимо будет проверять, по-прежнему ли совместим ваш пакет, и обновлять диапазон соответствующим образом.

Определение нового множества

Чтобы определить новое множество для JuMP, создайте подтип типа MOI.AbstractScalarSet или MOI.AbstractVectorSet и реализуйте Base.copy для множества.

julia> struct NewMOIVectorSet <: MOI.AbstractVectorSet
           dimension::Int
       end

julia> Base.copy(x::NewMOIVectorSet) = x

julia> model = Model();

julia> @variable(model, x[1:2]);

julia> @constraint(model, x in NewMOIVectorSet(2))
[x[1], x[2]] ∈ NewMOIVectorSet(2)

Однако для векторных множеств пользователю необходимо указывать аргумент размерности, даже если он выводится из длины x!

Чтобы сделать множество более удобным в использовании, создайте подтип типа AbstractVectorSet и реализуйте moi_set.

julia> struct NewVectorSet <: JuMP.AbstractVectorSet end

julia> JuMP.moi_set(::NewVectorSet, dim::Int) = NewMOIVectorSet(dim)

julia> @constraint(model, x in NewVectorSet())
[x[1], x[2]] ∈ NewMOIVectorSet(2)

Расширение макроса @variable

Так же как в случае с Bin и Int, которые создают двоичные и целочисленные переменные, вы можете расширить макрос @variable для создания новых типов переменных. Поясним на примере, в котором создается тип AddTwice, создающий кортеж из двух переменных JuMP вместо одной.

Сначала создадим структуру. Она может быть любой. Структура содержит объект VariableInfo, в котором хранится информация о привязке, а также о том, является ли переменная двоичной или целочисленной.

julia> struct AddTwice
           info::JuMP.VariableInfo
       end

Далее реализуем метод build_variable, который принимает ::Type{AddTwice} в качестве аргумента и возвращает экземпляр AddTwice. Обратите внимание, что приниматься могут также именованные аргументы.

julia> function JuMP.build_variable(
           _err::Function,
           info::JuMP.VariableInfo,
           ::Type{AddTwice};
           kwargs...
       )
           println("Can also use $kwargs here.")
           return AddTwice(info)
       end

В-третьих, реализуем метод add_variable, который принимает экземпляр AddTwice из предыдущего шага и что-то возвращает. Как правило, метод add_variable должен вызываться в этом месте. Например, наш вызов AddTwice будет добавлять две переменные JuMP.

julia> function JuMP.add_variable(
           model::JuMP.Model,
           duplicate::AddTwice,
           name::String,
       )
           a = JuMP.add_variable(
               model,
               JuMP.ScalarVariable(duplicate.info),
               "$(name)_a",
            )
           b = JuMP.add_variable(
               model,
               JuMP.ScalarVariable(duplicate.info),
               "$(name)_b",
            )
           return (a, b)
       end

Теперь AddTwice можно передать в @variable так же, как в случае с Bin или Int либо через именованный аргумент variable_type. Однако теперь добавляются две переменные, а не одна.

julia> model = Model();

julia> @variable(model, x[i=1:2], variable_type = AddTwice, kw = i)
Can also use Base.Pairs(:kw => 1) here.
Can also use Base.Pairs(:kw => 2) here.
2-element Vector{Tuple{VariableRef, VariableRef}}:
 (x[1]_a, x[1]_b)
 (x[2]_a, x[2]_b)

julia> num_variables(model)
4

julia> first(x[1])
x[1]_a

julia> last(x[2])
x[2]_b

Расширение макроса @constraint

Работа макроса @constraint состоит из трех шагов, которые можно перехватить и расширить: время анализа, время сборки и время добавления.

Анализ

Чтобы расширить макрос @constraint во время анализа, реализуйте один из следующих методов.

Расширение макроса ограничений во время анализа является сложной операцией и может влиять на существующий синтаксис JuMP. Прежде чем публиковать код, в котором реализуются эти методы, обсудите возможные последствия в чате разработчиков.

Для перехвата выражения на основе поля .head выражения Base.Expr следует реализовать метод parse_constraint_head. Например:

julia> using JuMP

julia> const MutableArithmetics = JuMP._MA;

julia> model = Model(); @variable(model, x);

julia> function JuMP.parse_constraint_head(
           error_fn::Function,
           ::Val{:≔},
           lhs,
           rhs,
       )
           println("Rewriting ≔ as ==")
           new_lhs, parse_code = MutableArithmetics.rewrite(lhs)
           build_code = :(
               build_constraint($(error_fn), $(new_lhs), MOI.EqualTo($(rhs)))
           )
           return false, parse_code, build_code
       end

julia> @constraint(model, x + x ≔ 1.0)
Rewriting ≔ as ==
2 x = 1

Для перехвата выражения вида Expr(:call, op, args...) следует реализовать метод parse_constraint_call. Например:

julia> using JuMP

julia> const MutableArithmetics = JuMP._MA;

julia> model = Model(); @variable(model, x);

julia> function JuMP.parse_constraint_call(
           error_fn::Function,
           is_vectorized::Bool,
           ::Val{:my_equal_to},
           lhs,
           rhs,
       )
           println("Rewriting my_equal_to to ==")
           new_lhs, parse_code = MutableArithmetics.rewrite(lhs)
           build_code = if is_vectorized
               :(build_constraint($(error_fn), $(new_lhs), MOI.EqualTo($(rhs)))
           )
           else
               :(build_constraint.($(error_fn), $(new_lhs), MOI.EqualTo($(rhs))))
           end
           return parse_code, build_code
       end

julia> @constraint(model, my_equal_to(x + x, 1.0))
Rewriting my_equal_to to ==
2 x = 1

При анализе ограничения можно рекурсивно обращаться к вложенному ограничению (например, {expr} в z --> {x <= 1}), вызывая parse_constraint.

Чтобы запретить JuMP продвигать множество до того же типа значения, что и у модели, используйте SkipModelConvertScalarSetWrapper.

Сборка

Чтобы расширить макрос @constraint во время сборки, реализуйте новый метод build_constraint.

Это может быть реализация метода для определенной функции или множества, созданного во время анализа, либо реализация метода, который обрабатывает дополнительные позиционные аргументы.

Метод build_constraint должен возвращать объект AbstractConstraint. Это может быть либо объект AbstractConstraint, уже поддерживаемый JuMP, например ScalarConstraint или VectorConstraint, либо пользовательский объект AbstractConstraint с соответствующим методом add_constraint (см. раздел Добавление).

Самый простой способ расширить макрос @constraint — с помощью дополнительного позиционного аргумента метода build_constraint.

Вот пример добавления дополнительных аргументов в build_constraint.

julia> model = Model(); @variable(model, x);

julia> struct MyConstrType end

julia> function JuMP.build_constraint(
            error_fn::Function,
            f::JuMP.GenericAffExpr,
            set::MOI.EqualTo,
            extra::Type{MyConstrType};
            d = 0,
       )
            new_set = MOI.LessThan(set.value + d)
            return JuMP.build_constraint(error_fn, f, new_set)
       end

julia> @constraint(model, my_con, x == 0, MyConstrType, d = 2)
my_con : x ≤ 2

В каждое ограничение можно передать только один позиционный аргумент. Если расширению необходимо передать несколько аргументов (например, Foo и Bar), они должны быть объединены в один тип аргумента (например, FooBar).

Добавление

build_constraint возвращает объект AbstractConstraint. Чтобы расширить @constraint во время добавления, определите подтип типа AbstractConstraint, реализуйте метод build_constraint для возврата экземпляра нового типа, а затем реализуйте add_constraint.

Вот пример:

julia> model = Model(); @variable(model, x);

julia> struct MyTag
           name::String
       end

julia> struct MyConstraint{S} <: AbstractConstraint
           name::String
           f::AffExpr
           s::S
       end

julia> function JuMP.build_constraint(
            error_fn::Function,
            f::AffExpr,
            set::MOI.AbstractScalarSet,
            extra::MyTag,
       )
            return MyConstraint(extra.name, f, set)
       end

julia> function JuMP.add_constraint(
            model::Model,
            con::MyConstraint,
            name::String,
       )
            return add_constraint(
                model,
                ScalarConstraint(con.f, con.s),
                "$(con.name)[$(name)]",
            )
       end

julia> @constraint(model, my_con, 2x <= 1, MyTag("my_prefix"))
my_prefix[my_con] : 2 x - 1 ≤ 0

Словарь расширений

У каждой модели JuMP есть поле .ext::Dict{Symbol,Any}, которое может использоваться расширениями. Оно полезно, если расширениям макросов @variable и @constraint необходимо сохранять информацию между вызовами.

Наиболее распространенный способ инициализации модели с информацией из словаря .ext — предоставление нового конструктора:

julia> function MyModel()
           model = Model()
           model.ext[:MyModel] = 1
           return model
       end
MyModel (generic function with 1 method)

julia> model = MyModel()
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none

julia> model.ext
Dict{Symbol, Any} with 1 entry:
  :MyModel => 1

Если определяются данные расширения, реализуйте copy_extension_data с поддержкой copy_model.

Определение новых моделей JuMP

Если расширения отдельных вызовов @variable и @constraint недостаточно, можно реализовать новую модель посредством подтипа AbstractModel. Можно также определить новые подтипы AbstractVariableRef для создания различных типов переменных JuMP.

Подобное расширение JuMP — сложная операция. Мы настоятельно рекомендуем подумать, как можно использовать методы, упомянутые в предыдущих разделах, для достижения ваших целей вместо определения новых типов моделей и переменных. Перед выполнением этой задачи проконсультируйтесь в чате разработчиков.

При определении новых типов вам придется реализовать значительное количество методов, а для этого потребуется детальное понимание внутреннего устройства JuMP. Поэтому список методов для реализации в настоящее время не документирован.

Самый простой способ расширить JuMP, определив новый тип модели, — следовать существующему примеру. Простым примером для подражания является модуль JuMPExtension в наборе тестов JuMP. Лучшим примером внешнего расширения JuMP, реализующего AbstractModel, является InfiniteOpt.jl.

Тестирование расширений JuMP

Набор тестов JuMP содержит множество тестов для расширений JuMP. Для запуска этих тестов скопируйте распространяемый по лицензии MIT файл Kokako.jl в тестах JuMP в папку /test, а затем добавьте этот фрагмент в файл /test/runtests.jl:

using MyJuMPExtension
import JuMP
include("Kokako.jl")
const MODULES_TO_TEST = Kokako.include_modules_to_test(JuMP)
Kokako.run_tests(
    MODULES_TO_TEST,
    MyJuMPExtension.MyModel,
    MyJuMPExtension.MyVariableRef;
    test_prefix = "test_extension_",
)

Задание обработчика optimize!

Некоторые расширения требуют изменения задачи после ее создания пользователем, но до вызова optimize!. Для таких ситуаций JuMP предоставляет метод set_optimize_hook, который позволяет перехватить вызов optimize!.

Вот простой пример добавления обработчика оптимизации, который расширяет optimize!, добавляя именованный аргумент silent:

julia> using JuMP, HiGHS

julia> model = Model(HiGHS.Optimizer);

julia> @variable(model, x >= 1.5, Int);

julia> @objective(model, Min, x);

julia> function silent_hook(model; silent::Bool)
           if silent
               set_silent(model)
           else
               unset_silent(model)
           end
           ## Обязательно установите ignore_optimize_hook = true, либо обращения
           ## к обработчику оптимизации будут происходить рекурсивно!
           return optimize!(model; ignore_optimize_hook = true)
       end
silent_hook (generic function with 1 method)

julia> set_optimize_hook(model, silent_hook)
silent_hook (generic function with 1 method)

julia> optimize!(model; silent = true)

julia> optimize!(model; silent = false)
Coefficient ranges:
  Cost   [1e+00, 1e+00]
  Bound  [2e+00, 2e+00]
Assessing feasibility of MIP using primal feasibility and integrality tolerance of       1e-06
Solution has               num          max          sum
Col     infeasibilities      0            0            0
Integer infeasibilities      0            0            0
Row     infeasibilities      0            0            0
Row     residuals            0            0            0
Presolving model
0 rows, 0 cols, 0 nonzeros  0s
0 rows, 0 cols, 0 nonzeros  0s
Presolve: Optimal

Solving report
  Status            Optimal
  Primal bound      2
  Dual bound        2
  Gap               0% (tolerance: 0.01%)
  Solution status   feasible
                    2 (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.00 (total)
                    0.00 (presolve)
                    0.00 (postsolve)
  Nodes             0
  LP iterations     0 (total)
                    0 (strong br.)
                    0 (separation)
                    0 (heuristics)

Создание новых типов контейнеров

Макросы JuMP (например, @variable) принимают именованный аргумент container для принудительного выбора типа контейнера. По умолчанию JuMP поддерживает container = Array, container = DenseAxisArray, container = SparseAxisArray и container = Auto. Поддержку можно расширить на пользовательские типы, реализовав Containers.container.

Например, вот контейнер, который меняет порядок индексов на обратный:

julia> struct Foo end

julia> function Containers.container(f::Function, indices, ::Type{Foo})
           return reverse([f(i...) for i in indices])
       end

julia> model = Model();

julia> @variable(model, x[1:3], container = Foo)
3-element Vector{VariableRef}:
 x[3]
 x[2]
 x[1]

julia> x[1]
x[3]

julia> @variable(model, y[1:3, 1:2], container = Foo)
3×2 Matrix{VariableRef}:
 y[3,2]  y[3,1]
 y[2,2]  y[2,1]
 y[1,2]  y[1,1]

julia> y[1, 1]
y[3,2]

julia> @variable(model, z[i=1:3; isodd(i)], container = Foo)
2-element Vector{VariableRef}:
 z[3]
 z[1]

julia> z[2]
z[1]

Если вы обычный пользователь, вам не нужно создавать новые типы контейнеров. Вместо этого следуйте инструкциям в разделе User-defined containers и создайте контейнер с использованием стандартного синтаксиса Julia. Например:

julia> model = Model();

julia> @variable(model, x[1:3])
3-element Vector{VariableRef}:
x[1]
x[2]
x[3]

julia> y = reverse(x)
3-element Vector{VariableRef}:
x[3]
x[2]
x[1]

Советы по производительности расширений

Из-за принятой в MathOptInterface схемы ограничений «функция во множестве» при попытке перебрать все ограничения в модели в Julia возникают проблемы со стабильностью типов. Самым простым способом их решения является использование функционального барьера.

Например, вместо следующего кода:

function all_names_slow(model)
    names = Set{String}()
    for ci in all_constraints(model)
        push!(names, name(ci))
    end
    return names
end

используйте такой:

function _function_barrier(names, model, ::Type{F}, ::Type{S}) where {F,S}
    for ci in all_constraints(model, F, S)
        push!(names, name(ci))
    end
    return
end

function all_names_fast(model)
    names = Set{String}()
    for (F, S) in list_of_constraint_types(model)
        _function_barrier(names, model, F, S)
    end
    return names
end

Важно явно указывать тип аргументов F и S. Если оставить их нетипизированными, например function _function_barrier(names, model, F, S), Julia не будет специализировать вызовы функций и производительность не улучшится.