Расширения
В 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
Важно явно указывать тип аргументов |