Расширения
В 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
|
При анализе ограничения можно рекурсивно обращаться к вложенному ограничению (например, |
Чтобы запретить JuMP продвигать множество до того же типа значения, что и у модели, используйте SkipModelConvertScalarSetWrapper.
Сборка
Чтобы расширить макрос @constraint во время сборки, реализуйте новый метод build_constraint.
Это может быть реализация метода для определенной функции или множества, созданного во время анализа, либо реализация метода, который обрабатывает дополнительные позиционные аргументы.
Метод build_constraint должен возвращать объект AbstractConstraint. Это может быть либо объект AbstractConstraint, уже поддерживаемый JuMP, например ScalarConstraint или VectorConstraint, либо пользовательский объект AbstractConstraint с соответствующим методом add_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
|
В каждое ограничение можно передать только один позиционный аргумент. Если расширению необходимо передать несколько аргументов (например, |
Добавление
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
|
Важно явно указывать тип аргументов |