Начало работы с JuMP
Что такое JuMP
JuMP (Julia for Mathematical Programming, Julia для математического программирования) — это язык моделирования с открытым исходным кодом, встроенный в Julia. Он позволяет пользователям формулировать различные классы задач оптимизации (линейные, частично целочисленные, квадратичные, конические квадратичные, полуопределенные и нелинейные) в виде легко читаемого кода. Эти задачи можно решать с помощью современных решателей с открытым исходным кодом и коммерческих решателей.
JuMP также упрощает доступ к передовым методам оптимизации посредством высокоуровневого языка.
Что такое решатель
Решатель — это программный пакет, содержащий алгоритмы для нахождения решений задач одного или нескольких классов.
Например, HiGHS — это решатель задач линейного программирования (LP) и частично целочисленного программирования (MIP). Он включает в себя такие алгоритмы, как симплексный метод и метод внутренней точки.
В таблице Supported-solvers перечислены решатели с открытым исходным кодом и коммерческие решатели, которые в настоящее время поддерживает JuMP.
Что такое MathOptInterface
Каждый решатель основан на особых принципах и имеет собственные структуры данных для представления моделей оптимизации и получения результатов.
MathOptInterface (MOI) — это уровень абстракции, который JuMP использует для преобразования задачи, написанной на языке JuMP, в структуры данных, характерные для решателя.
MOI можно использовать напрямую или через интерфейс моделирования более высокого уровня, такой как JuMP.
Поскольку язык JuMP построен на основе MOI, при выводе типов JuMP часто можно увидеть префикс MathOptInterface.
. Однако разбираться в особенностях интерфейса MOI и взаимодействовать с ним требуется только при выполнении сложных задач, таких как создание независимых от решателя обратных вызовов.
Установка
JuMP — это пакет для Julia. Из Julia пакет JuMP устанавливается с помощью встроенного диспетчера пакетов.
import Pkg
Pkg.add("JuMP")
Необходимо также включить пакет Julia с соответствующим решателем. Одним из таких решателей является HiGHS.Optimizer
из пакета HiGHS.jl.
import Pkg
Pkg.add("HiGHS")
Список доступных решателей см. в разделе Installation Guide.
Пример
Давайте решим следующую задачу линейного программирования с помощью JuMP и HiGHS. Сначала рассмотрим полный код решения задачи, а затем разберем его шаг за шагом.
Задача выглядит так:
А вот код для решения задачи:
julia> using JuMP
julia> using HiGHS
julia> model = Model(HiGHS.Optimizer)
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> @variable(model, x >= 0)
x
julia> @variable(model, 0 <= y <= 3)
y
julia> @objective(model, Min, 12x + 20y)
12 x + 20 y
julia> @constraint(model, c1, 6x + 8y >= 100)
c1 : 6 x + 8 y ≥ 100
julia> @constraint(model, c2, 7x + 12y >= 120)
c2 : 7 x + 12 y ≥ 120
julia> print(model)
Min 12 x + 20 y
Subject to
c1 : 6 x + 8 y ≥ 100
c2 : 7 x + 12 y ≥ 120
x ≥ 0
y ≥ 0
y ≤ 3
julia> optimize!(model)
Running HiGHS 1.7.2 (git hash: 5ce7a2753): Copyright (c) 2024 HiGHS under MIT licence terms
Coefficient ranges:
Matrix [6e+00, 1e+01]
Cost [1e+01, 2e+01]
Bound [3e+00, 3e+00]
RHS [1e+02, 1e+02]
Presolving model
2 rows, 2 cols, 4 nonzeros 0s
2 rows, 2 cols, 4 nonzeros 0s
Presolve : Reductions: rows 2(-0); columns 2(-0); elements 4(-0) - Not reduced
Problem not reduced by presolve: solving the LP
Using EKK dual simplex solver - serial
Iteration Objective Infeasibilities num(sum)
0 0.0000000000e+00 Pr: 2(220) 0s
2 2.0500000000e+02 Pr: 0(0) 0s
Model status : Optimal
Simplex iterations: 2
Objective value : 2.0500000000e+02
HiGHS run time : 0.00
julia> termination_status(model)
OPTIMAL::TerminationStatusCode = 1
julia> primal_status(model)
FEASIBLE_POINT::ResultStatusCode = 1
julia> dual_status(model)
FEASIBLE_POINT::ResultStatusCode = 1
julia> objective_value(model)
204.99999999999997
julia> value(x)
15.000000000000005
julia> value(y)
1.249999999999996
julia> shadow_price(c1)
-0.24999999999999922
julia> shadow_price(c2)
-1.5000000000000007
Пошаговый разбор
После установки пакета JuMP для его использования в своей программе введите следующее:
julia> using JuMP
Кроме того, необходимо включить пакет Julia с соответствующим решателем. Здесь мы будем использовать решатель HiGHS.Optimizer
из пакета HiGHS.jl
:
julia> using HiGHS
JuMP строит задачу поэтапно в объекте Model
. Чтобы создать модель, передайте оптимизатор в функцию Model
:
julia> model = Model(HiGHS.Optimizer)
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
Переменные моделируются с помощью @variable
:
julia> @variable(model, x >= 0)
x
Этот макрос создает новый объект Julia |
x = @variable(model, x >= 0)
Но макрос делает это автоматически, избавляя нас от необходимости дважды писать x
.
У переменных могут быть нижние и верхние границы:
julia> @variable(model, 0 <= y <= 30)
y
Целевая функция задается с помощью макроса @objective
:
julia> @objective(model, Min, 12x + 20y)
12 x + 20 y
Ограничения моделируются с помощью макроса @constraint
. Здесь c1
и c2
— это имена ограничений:
julia> @constraint(model, c1, 6x + 8y >= 100)
c1 : 6 x + 8 y ≥ 100
julia> @constraint(model, c2, 7x + 12y >= 120)
c2 : 7 x + 12 y ≥ 120
Для отображения модели вызовите print
:
julia> print(model)
Min 12 x + 20 y
Subject to
c1 : 6 x + 8 y ≥ 100
c2 : 7 x + 12 y ≥ 120
x ≥ 0
y ≥ 0
y ≤ 30
Для решения задачи оптимизации вызовите функцию optimize!
:
julia> optimize!(model)
Running HiGHS 1.7.2 (git hash: 5ce7a2753): Copyright (c) 2024 HiGHS under MIT licence terms
Coefficient ranges:
Matrix [6e+00, 1e+01]
Cost [1e+01, 2e+01]
Bound [3e+01, 3e+01]
RHS [1e+02, 1e+02]
Presolving model
2 rows, 2 cols, 4 nonzeros 0s
2 rows, 2 cols, 4 nonzeros 0s
Presolve : Reductions: rows 2(-0); columns 2(-0); elements 4(-0) - Not reduced
Problem not reduced by presolve: solving the LP
Using EKK dual simplex solver - serial
Iteration Objective Infeasibilities num(sum)
0 0.0000000000e+00 Pr: 2(220) 0s
2 2.0500000000e+02 Pr: 0(0) 0s
Model status : Optimal
Simplex iterations: 2
Objective value : 2.0500000000e+02
HiGHS run time : 0.00
Символ |
Теперь посмотрим, какую информацию о решении можно запросить. Начнем с is_solved_and_feasible
:
julia> is_solved_and_feasible(model)
true
Более подробную информацию о решении можно получить, запросив три типа статусов.
termination_status
сообщает, почему решатель прекратил выполнение.
julia> termination_status(model)
OPTIMAL::TerminationStatusCode = 1
В данном случае решатель нашел оптимальное решение.
Чтобы узнать, нашел ли решатель прямую допустимую точку, проверьте primal_status
:
julia> primal_status(model)
FEASIBLE_POINT::ResultStatusCode = 1
А чтобы узнать, нашел ли решатель двойственную допустимую точку, проверьте dual_status
:
julia> dual_status(model)
FEASIBLE_POINT::ResultStatusCode = 1
Теперь мы знаем, что решатель нашел оптимальное решение и можно запросить прямое и двойственное решения.
Значение целевой функции можно запросить с помощью objective_value
:
julia> objective_value(model)
205.0
Прямое решение — с помощью value
:
julia> value(x)
15.000000000000004
julia> value(y)
1.2499999999999976
А двойственное решение — с помощью shadow_price
:
julia> shadow_price(c1)
-0.24999999999999917
julia> shadow_price(c2)
-1.5000000000000007
Перед вызовом функций решения, таких как |
optimize!(model)
if !is_solved_and_feasible(model)
error("Solver did not find an optimal solution")
end
Вот и все, что касается нашей простой модели. В оставшейся части этого руководства мы более подробно рассмотрим некоторые основные операции JuMP.
Основные сведения о моделях
Чтобы создать модель, передайте оптимизатор:
julia> model = Model(HiGHS.Optimizer)
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
Можно также вызвать set_optimizer
в любой момент до вызова optimize!
:
julia> model = Model()
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> set_optimizer(model, HiGHS.Optimizer)
Для некоторых решателей можно также использовать метод direct_model
, обеспечивающий более эффективную связь с базовым решателем:
julia> model = direct_model(HiGHS.Optimizer())
A JuMP Model
├ mode: DIRECT
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
Некоторые решатели не поддерживают |
Параметры решателей
Параметры передаются в решатели с помощью optimizer_with_attributes
:
julia> model =
Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false))
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
Параметры зависят от решателя. Сведения о доступных параметрах см. в файле сведений в репозитории GitHub соответствующего пакета решателя. Ссылку на страницу каждого решателя на GitHub можно найти в таблице Supported solvers. |
Параметры можно также передавать с помощью set_attribute
:
julia> model = Model(HiGHS.Optimizer)
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> set_attribute(model, "output_flag", false)
Основные сведения о решениях
Выше мы увидели, как использовать termination_status
и primal_status
для анализа решения, возвращаемого решателем.
Однако запрашивать такие атрибуты решения, как value
и objective_value
, следует только в том случае, если решение доступно. Это рекомендуется проверять следующим способом.
julia> function solve_infeasible()
model = Model(HiGHS.Optimizer)
@variable(model, 0 <= x <= 1)
@variable(model, 0 <= y <= 1)
@constraint(model, x + y >= 3)
@objective(model, Max, x + 2y)
optimize!(model)
if !is_solved_and_feasible(model)
@warn("The model was not solved correctly.")
return
end
return value(x), value(y)
end
solve_infeasible (generic function with 1 method)
julia> solve_infeasible()
Running HiGHS 1.7.2 (git hash: 5ce7a2753): Copyright (c) 2024 HiGHS under MIT licence terms
Coefficient ranges:
Matrix [1e+00, 1e+00]
Cost [1e+00, 2e+00]
Bound [1e+00, 1e+00]
RHS [3e+00, 3e+00]
Presolving model
Problem status detected on presolve: Infeasible
Model status : Infeasible
Objective value : 0.0000000000e+00
HiGHS run time : 0.00
ERROR: No LP invertible representation for getDualRay
┌ Warning: The model was not solved correctly.
└ @ Main REPL[1]:9
Основные сведения о переменных
Давайте создадим пустую модель, чтобы пояснить некоторые аспекты синтаксиса переменных:
julia> model = Model()
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
Границы переменной
У всех переменных, которые мы создавали до сих пор, была граница. Можно также создать свободную переменную.
julia> @variable(model, free_x)
free_x
При создании переменной вместо синтаксиса <= и >= можно также использовать именованные аргументы lower_bound
и upper_bound
.
julia> @variable(model, keyword_x, lower_bound = 1, upper_bound = 2)
keyword_x
Запросить факт наличия границы у переменной можно с помощью функций has_lower_bound
и has_upper_bound
. Значения границ можно получить с помощью функций lower_bound
и upper_bound
.
julia> has_upper_bound(keyword_x)
true
julia> upper_bound(keyword_x)
2.0
Обратите внимание: при запросе значения несуществующей границы происходит ошибка.
julia> lower_bound(free_x)
Variable free_x does not have a lower bound.
Контейнеры
Мы уже видели, как добавить одну переменную в модель с помощью макроса @variable
. Теперь рассмотрим способы добавления нескольких переменных в модель.
JuMP предоставляет структуры данных для добавления коллекций переменных в модель. Эти структуры данных называются контейнерами и бывают трех типов: Arrays
, DenseAxisArrays
и SparseAxisArrays
.
Массивы
Массивы JuMP создаются при наличии целочисленных индексов, начинающихся с 1
:
julia> @variable(model, a[1:2, 1:2])
2×2 Matrix{VariableRef}:
a[1,1] a[1,2]
a[2,1] a[2,2]
Элементы в a
индексируются следующим образом:
julia> a[1, 1]
a[1,1]
julia> a[2, :]
2-element Vector{VariableRef}:
a[2,1]
a[2,2]
Создать n-мерную переменную с границами ( ) можно так:
julia> n = 10
10
julia> l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
julia> u = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
julia> @variable(model, l[i] <= x[i = 1:n] <= u[i])
10-element Vector{VariableRef}:
x[1]
x[2]
x[3]
x[4]
x[5]
x[6]
x[7]
x[8]
x[9]
x[10]
Можно также создавать границы переменных, зависящие от индексов:
julia> @variable(model, y[i = 1:2, j = 1:2] >= 2i + j)
2×2 Matrix{VariableRef}:
y[1,1] y[1,2]
y[2,1] y[2,2]
DenseAxisArray
Тип DenseAxisArrays
применяется, когда индексы не являются целочисленными диапазонами, начинающимися с единицы. Синтаксис при этом такой же, за исключением того, что в качестве индекса используется произвольный вектор, а не начинающийся с единицы диапазон:
julia> @variable(model, z[i = 2:3, j = 1:2:3] >= 0)
2-dimensional DenseAxisArray{VariableRef,2,...} with index sets:
Dimension 1, 2:3
Dimension 2, 1:2:3
And data, a 2×2 Matrix{VariableRef}:
z[2,1] z[2,3]
z[3,1] z[3,3]
Индексы не обязательно должны быть целыми числами. Они могут быть любого типа Julia:
julia> @variable(model, w[1:5, ["red", "blue"]] <= 1)
2-dimensional DenseAxisArray{VariableRef,2,...} with index sets:
Dimension 1, Base.OneTo(5)
Dimension 2, ["red", "blue"]
And data, a 5×2 Matrix{VariableRef}:
w[1,red] w[1,blue]
w[2,red] w[2,blue]
w[3,red] w[3,blue]
w[4,red] w[4,blue]
w[5,red] w[5,blue]
Элементы в DenseAxisArray
индексируются следующим образом:
julia> z[2, 1]
z[2,1]
julia> w[2:3, ["red", "blue"]]
2-dimensional DenseAxisArray{VariableRef,2,...} with index sets:
Dimension 1, [2, 3]
Dimension 2, ["red", "blue"]
And data, a 2×2 Matrix{VariableRef}:
w[2,red] w[2,blue]
w[3,red] w[3,blue]
Дополнительные сведения см. в разделе Принудительное задание типа контейнера.
SparseAxisArray
Массивы SparseAxisArrays
создаются, когда индексы не образуют декартово произведение. Например, это верно в случае, когда индексы зависят от предыдущих индексов (так называемое треугольное индексирование):
julia> @variable(model, u[i = 1:2, j = i:3])
SparseAxisArray{VariableRef, 2, Tuple{Int64, Int64}} with 5 entries:
[1, 1] = u[1,1]
[1, 2] = u[1,2]
[1, 3] = u[1,3]
[2, 2] = u[2,2]
[2, 3] = u[2,3]
Переменные можно также создавать условным образом, добавляя проверку сравнения, которая зависит от именованных индексов и отделяется от индексов точкой с запятой (;
):
julia> @variable(model, v[i = 1:9; mod(i, 3) == 0])
SparseAxisArray{VariableRef, 1, Tuple{Int64}} with 3 entries:
[3] = v[3]
[6] = v[6]
[9] = v[9]
Элементы в DenseAxisArray
индексируются следующим образом:
julia> u[1, 2]
u[1,2]
julia> v[[3, 6]]
SparseAxisArray{VariableRef, 1, Tuple{Int64}} with 2 entries:
[3] = v[3]
[6] = v[6]
Целочисленность
JuMP позволяет создавать двоичные и целочисленные переменные. Двоичные переменные ограничены множеством , а целочисленные — множеством .
Основные сведения об ограничениях
Чтобы пояснить некоторые базовые принципы ограничений, нам потребуется новая модель:
julia> model = Model()
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> @variable(model, x)
x
julia> @variable(model, y)
y
julia> @variable(model, z[1:10]);
Контейнеры
Аналогично контейнерам для переменных, JuMP предоставляет Arrays
, DenseAxisArrays
и SparseAxisArrays
для хранения коллекций ограничений. Ниже приведены примеры каждого типа контейнера.
Массивы
Создание массива Array
ограничений:
julia> @constraint(model, [i = 1:3], i * x <= i + 1)
3-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
x ≤ 2
2 x ≤ 3
3 x ≤ 4
DenseAxisArray
Создание массива DenseAxisArray
ограничений:
julia> @constraint(model, [i = 1:2, j = 2:3], i * x <= j + 1)
2-dimensional DenseAxisArray{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape},2,...} with index sets:
Dimension 1, Base.OneTo(2)
Dimension 2, 2:3
And data, a 2×2 Matrix{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
x ≤ 3 x ≤ 4
2 x ≤ 3 2 x ≤ 4
SparseAxisArray
Создание массива SparseAxisArray
ограничений:
julia> @constraint(model, [i = 1:2, j = 1:2; i != j], i * x <= j + 1)
SparseAxisArray{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}, 2, Tuple{Int64, Int64}} with 2 entries:
[1, 2] = x ≤ 3
[2, 1] = 2 x ≤ 2
Ограничения в цикле
Ограничения можно добавлять, используя обычные циклы Julia:
julia> for i in 1:3
@constraint(model, 6x + 4y >= 5i)
end
либо используя циклы for each внутри макроса @constraint
:
julia> @constraint(model, [i in 1:3], 6x + 4y >= 5i)
3-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.GreaterThan{Float64}}, ScalarShape}}:
6 x + 4 y ≥ 5
6 x + 4 y ≥ 10
6 x + 4 y ≥ 15
Можно также создавать такие ограничения, как :
julia> @constraint(model, sum(z[i] for i in 1:10) <= 1)
z[1] + z[2] + z[3] + z[4] + z[5] + z[6] + z[7] + z[8] + z[9] + z[10] ≤ 1
Целевые функции
Целевая функция задается с помощью макроса @objective
:
julia> model = Model(HiGHS.Optimizer)
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> @variable(model, x >= 0)
x
julia> @variable(model, y >= 0)
y
julia> @objective(model, Min, 2x + y)
2 x + y
Чтобы создать целевую функцию максимизации, используйте Max
:
julia> @objective(model, Max, 2x + y)
2 x + y
При многократном вызове |
Векторизированный синтаксис
Добавлять ограничения и целевую функцию в JuMP можно также с использованием векторизованной линейной алгебры. Проиллюстрируем это, решив задачу LP в стандартной форме:
julia> vector_model = Model(HiGHS.Optimizer)
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> A = [1 1 9 5; 3 5 0 8; 2 0 6 13]
3×4 Matrix{Int64}:
1 1 9 5
3 5 0 8
2 0 6 13
julia> b = [7, 3, 5]
3-element Vector{Int64}:
7
3
5
julia> c = [1, 3, 5, 2]
4-element Vector{Int64}:
1
3
5
2
julia> @variable(vector_model, x[1:4] >= 0)
4-element Vector{VariableRef}:
x[1]
x[2]
x[3]
x[4]
julia> @constraint(vector_model, A * x .== b)
3-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.EqualTo{Float64}}, ScalarShape}}:
x[1] + x[2] + 9 x[3] + 5 x[4] = 7
3 x[1] + 5 x[2] + 8 x[4] = 3
2 x[1] + 6 x[3] + 13 x[4] = 5
julia> @objective(vector_model, Min, c' * x)
x[1] + 3 x[2] + 5 x[3] + 2 x[4]
julia> optimize!(vector_model)
Running HiGHS 1.7.2 (git hash: 5ce7a2753): Copyright (c) 2024 HiGHS under MIT licence terms
Coefficient ranges:
Matrix [1e+00, 1e+01]
Cost [1e+00, 5e+00]
Bound [0e+00, 0e+00]
RHS [3e+00, 7e+00]
Presolving model
3 rows, 4 cols, 10 nonzeros 0s
3 rows, 4 cols, 10 nonzeros 0s
Presolve : Reductions: rows 3(-0); columns 4(-0); elements 10(-0) - Not reduced
Problem not reduced by presolve: solving the LP
Using EKK dual simplex solver - serial
Iteration Objective Infeasibilities num(sum)
0 0.0000000000e+00 Pr: 3(13.5) 0s
4 4.9230769231e+00 Pr: 0(0) 0s
Model status : Optimal
Simplex iterations: 4
Objective value : 4.9230769231e+00
HiGHS run time : 0.00
julia> @assert is_solved_and_feasible(vector_model)
julia> objective_value(vector_model)
4.923076923076922