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

Начало работы с JuMP

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

Если у вас нет опыта работы с Julia, начните с руководства Getting started with Julia.

Что такое 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 в текущей области. Более явно это можно было бы записать так:

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

Символ ! после слова optimize — это часть имени. В нем нет ничего особенного. В Julia принято соглашение, что имена функций, которые изменяют свои аргументы, должны заканчиваться на !. Хороший пример — функция push!.

Теперь посмотрим, какую информацию о решении можно запросить. Начнем с 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

Перед вызовом функций решения, таких как value или objective_value, обязательно проверьте, нашел ли решатель решение. Обычно рабочий процесс происходит так:

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

Некоторые решатели не поддерживают direct_model!

Параметры решателей

Параметры передаются в решатели с помощью 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 позволяет создавать двоичные и целочисленные переменные. Двоичные переменные ограничены множеством , а целочисленные — множеством .

Целочисленные переменные

Чтобы создать целочисленную переменную, передайте Int:

julia> @variable(model, integer_x, Int)
integer_x

Либо установите именованный аргумент integer в значение true:

julia> @variable(model, integer_z, integer = true)
integer_z

Двоичные переменные

Чтобы создать двоичную переменную, передайте Bin:

julia> @variable(model, binary_x, Bin)
binary_x

Либо установите именованный аргумент binary в значение true:

julia> @variable(model, binary_z, binary = true)
binary_z

Основные сведения об ограничениях

Чтобы пояснить некоторые базовые принципы ограничений, нам потребуется новая модель:

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

При многократном вызове @objective предыдущая целевая функция перезаписывается. Это может быть полезно для решения одной и той же задачи с разными целевыми функциями.

Векторизированный синтаксис

Добавлять ограничения и целевую функцию в 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