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

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

Поскольку язык JuMP встроен в Julia, перед изучением JuMP важно знать некоторые основы Julia.

Это руководство представляет собой сжатый экспресс-курс по основам Julia. Ресурсы, дающие более полное представление о Julia, можно найти здесь.

Установка Julia

Если вы не уверены в обратном, скорее всего, вам нужна 64-разрядная версия.

Далее вам понадобится среда IDE для разработки. Популярным выбором является VS Code, поэтому следуйте этим инструкциям по установке.

Julia также можно использовать с интерактивными скриптами Jupyter или интерактивными скриптами Pluto.jl в реактивном режиме.

REPL Julia

Основной способ взаимодействия с Julia — через REPL (Read Evaluate Print Loop, цикл «чтение — вычисление — вывод»). Для доступа к REPL запустите исполняемый файл Julia, чтобы перейти к командной строке julia>, а затем начните вводить код:

julia> 1 + 1
2

Более длинные программы следует писать в виде скриптов в текстовых файлах, которые запускаются так:

julia> include("path/to/file.jl")

Из-за задержки при запуске Julia скрипты из командной строки, как в следующем случае, запускаются медленно:

$ julia path/to/file.jl

Вместо этого используйте REPL или интерактивный скрипт, а также получите дополнительные сведения в разделе The "time-to-first-solve" issue.

Блоки кода в этой документации

В этой документации часть примеров кода предназначена для выполнения в командной строке julia>, а часть — иным образом.

Командная строка Julia в основном используется для демонстрации коротких фрагментов кода, а их выходные данные такие же, как при запуске из REPL.

Блоки без julia> можно копировать и вставлять в REPL. Причина их использования в том, что они позволяют выводить более содержательные данные, например графики или LaTeX, в веб- и PDF-версиях документации. При их выполнении в REPL выходные данные могут быть иными.

Получение справки

или пометьте вопрос тегом jump.

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

help?> print
search: print println printstyled sprint isprint prevind parentindices precision escape_string

  print([io::IO], xs...)

  Write to io (or to the default output stream stdout if io is not given) a canonical
  (un-decorated) text representation. The representation used by print includes minimal formatting
  and tries to avoid Julia-specific details.

  print falls back to calling show, so most types should just define show. Define print if your
  type has a separate "plain" representation. For example, show displays strings with quotes, and
  print displays strings without quotes.

  string returns the output of print as a string.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> print("Hello World!")
  Hello World!
  julia> io = IOBuffer();

  julia> print(io, "Hello", ' ', :World!)

  julia> String(take!(io))
  "Hello World!"

Числа и арифметические операции

Так как мы будем решать задачи оптимизации, нам придется использовать много математики. К счастью, язык Julia отлично подходит для математических вычислений, и в нем есть все стандартные операторы:

julia> 1 + 1
2

julia> 1 - 2
-1

julia> 2 * 2
4

julia> 4 / 5
0.8

julia> 3^2
9

Заметили, что в Julia не выводится .0 после некоторых чисел? Julia — динамический язык, то есть объявлять тип переменной явным образом не требуется. Однако на внутреннем уровне Julia назначает тип каждой переменной. Проверить тип любого объекта можно с помощью функции typeof:

julia> typeof(1)
Int64

julia> typeof(1.0)
Float64

Здесь 1 имеет тип Int64, то есть это целое число с 64-разрядной точностью, а 1.0 имеет тип Float64, то есть это число с плавающей запятой с 64-разрядной точностью.

Если вы не знакомы с числами с плавающей запятой, обязательно прочитайте раздел Floating point numbers.

Комплексные числа создаются с помощью im:

julia> x = 2 + 1im
2 + 1im

julia> real(x)
2

julia> imag(x)
1

julia> typeof(x)
Complex{Int64}

julia> x * (1 - 2im)
4 - 3im

Фигурные скобки заключают в себя так называемые параметры типа. Complex{Int64} можно расшифровать как «комплексное число, вещественная и мнимая части которого представлены типом Int64». Если вызвать typeof(1.0 + 2.0im), типом будет Complex{Float64}, то есть комплексное число, части которого представлены типом Float64.

Есть еще ряд интересных возможностей, например иррациональное представление числа π.

julia> π
π = 3.1415926535897...

Для ввода буквы π (и большинства других греческих букв) введите \pi, а затем нажмите клавишу [TAB].

julia> typeof(π)
Irrational{:π}

Однако при выполнении математических действий с иррациональными числами они преобразуются в тип Float64:

julia> typeof(2π / 3)
Float64

Числа с плавающей запятой

Если вы не знакомы с числами с плавающей запятой, обязательно внимательно прочитайте этот раздел.

Float64 — это аппроксимация с плавающей запятой вещественного числа с использованием 64 битов данных.

Поскольку это всего лишь аппроксимация, то, что справедливо в математике, может не соблюдаться на компьютере. Например:

julia> 0.1 * 3 == 0.3
false

Вот более сложный пример:

julia> sin(2π / 3) == √3 / 2
false

Чтобы получить символ , введите \sqrt, а затем нажмите клавишу [TAB].

Посмотрим, в чем разница:

julia> 0.1 * 3 - 0.3
5.551115123125783e-17

julia> sin(2π / 3) - √3 / 2
1.1102230246251565e-16

Эти числа малы, но отличны от нуля.

Чтобы понять причину такого различия, подумайте, как бы мы записали 1 / 3 и 2 / 3, используя только четыре цифры после десятичной запятой. Мы бы записали 1 / 3 как 0.3333, а 2 / 3 как 0.6667. Поэтому, несмотря на то, что 2 * (1 / 3) == 2 / 3, мы получаем 2 * 0.3333 == 0.6666 != 0.6667.

Попробуем сделать то же самое, используя оператор ≈ (\approx + [TAB]) вместо ==:

julia> 0.1 * 3 ≈ 0.3
true

julia> sin(2π / 3) ≈ √3 / 2
true

 — это элегантный способ вызова функции isapprox:

julia> isapprox(sin(2π / 3), √3 / 2; atol = 1e-8)
true

Плавающая запятая является причиной того, что решатели используют допуски при решении моделей оптимизации. Одна из распространенных ошибок — проверка двоичной переменной на равенство нулю с помощью value(z) == 0. Всегда помните о необходимости использовать функцию наподобие isapprox при сравнении чисел с плавающей запятой.

Обратите внимание, что isapprox всегда возвращает false, если одно из сравниваемых чисел равно 0 и при этом atol равно нулю (значение по умолчанию).

julia> 1e-300 ≈ 0.0
false

Поэтому если один из аргументов может быть равен нулю, обязательно устанавливайте ненулевое значение atol.

julia> isapprox(1e-9, 0.0; atol = 1e-8)
true

На сайте Gurobi есть хорошая серия статей о влиянии плавающей запятой на оптимизацию.

Из-за невнимательности арифметические операции с плавающей запятой могут приводить к самым разным проблемам. Например:

julia> 1 + 1e-16 == 1
true

Оказывается, что числа с плавающей запятой даже не ассоциативны:

julia> (1 + 1e-16) - 1e-16 == 1 + (1e-16 - 1e-16)
false

Важно отметить, что эта проблема касается не только Julia. Она имеется в каждом языке программирования (попробуйте сделать то же самое, например, в Python).

Векторы, матрицы и массивы

Как и в MATLAB, в Julia есть встроенная поддержка векторов, матриц и тензоров; все они представлены массивами различной размерности. Векторы состоят из элементов, разделенных запятыми, в квадратных скобках:

julia> b = [5, 6]
2-element Vector{Int64}:
 5
 6

Для построения матриц можно разделять столбцы пробелами, а строки — точками с запятой:

julia> A = [1.0 2.0; 3.0 4.0]
2×2 Matrix{Float64}:
 1.0  2.0
 3.0  4.0

Доступны операции линейной алгебры:

julia> x = A \ b
2-element Vector{Float64}:
 -3.9999999999999987
  4.499999999999999

Здесь мы снова имеем дело с плавающей запятой: x приблизительно равно [-4, 4.5].

julia> A * x
2-element Vector{Float64}:
 5.0
 6.0

julia> A * x ≈ b
true

Обратите внимание, что при умножении векторов и матриц размерность имеет значение. Например, нельзя умножить вектор на вектор:

julia>     b * b
MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})

Closest candidates are:
  *(::Any, ::Any, !Matched::Any, !Matched::Any...)
   @ Base operators.jl:587
  *(!Matched::ChainRulesCore.NoTangent, ::Any)
   @ ChainRulesCore ~/.julia/packages/ChainRulesCore/6Pucz/src/tangent_arithmetic.jl:64
  *(::Any, !Matched::ChainRulesCore.ZeroTangent)
   @ ChainRulesCore ~/.julia/packages/ChainRulesCore/6Pucz/src/tangent_arithmetic.jl:105
  ...

Но умножение транспонированных матриц выполняется:

julia> b' * b
61

julia> b * b'
2×2 Matrix{Int64}:
 25  30
 30  36

Другие распространенные типы

Комментарии

Хотя, строго говоря, комментарии к коду — это не тип, их можно добавлять после символа #:

julia> 1 + 1  # Это комментарий
2

Многострочные комментарии начинаются с символов #= и заканчиваются символами =#:

#=
Here is a
multiline comment
=#

Комментарии можно даже вкладывать в выражения. Иногда это полезно при документировании входных данных функций:

julia> isapprox(
           sin(π),
           0.0;
           #= Здесь нужно явно указать значение atol, потому что выполняется сравнение с 0 =#
           atol = 0.001,
       )
true

Строки

Строки заключаются в двойные кавычки:

julia> typeof("This is Julia")
String

В строках допускаются символы Юникода:

julia> typeof("π is about 3.1415")
String

Для вывода строки используйте println:

julia> println("Hello, World!")
Hello, World!

Для интерполяции значений в строку используйте $():

julia> x = 123
123

julia> println("The value of x is: $(x)")
The value of x is: 123

Для многострочных строк используйте тройные кавычки:

julia> s = """
       Here is
       a
       multiline string
       """
"Here is\na\nmultiline string\n"

julia> println(s)
Here is
a
multiline string

Символы

Symbol в Julia — это структура данных компилятора, представляющая имена переменных Julia.

julia> println("The value of x is: $(eval(:x))")
The value of x is: 123

Здесь мы использовали eval, чтобы продемонстрировать, как в Julia символы Symbol связываются с переменными. Однако в реальном коде вызывать eval не следует. Обычно такой вызов — признак того, что код делает что-то, чего проще добиться иным путем. Попросить совета касательно альтернативных подходов можно на форуме сообщества.

julia> typeof(:x)
Symbol

Объект Symbol можно представить себе как строку String, которая занимает меньше памяти и не может быть изменена.

Преобразование между String и Symbol выполняется с помощью конструкторов этих типов:

julia> String(:abc)
"abc"

julia> Symbol("abc")
:abc

Типом Symbol часто злоупотребляют, используя его вместо String или Enum в случаях, когда один из этих двух типов подходит лучше. В руководстве по стилю JuMP (Style guide) рекомендуется резервировать Symbol только для имен переменных. Дополнительные сведения см. в разделе @enum vs. Symbol.

Кортежи

В Julia широко применяется простая структура данных, называемая кортежем. Кортежи — это неизменяемые коллекции значений. Например:

julia> t = ("hello", 1.2, :foo)
("hello", 1.2, :foo)

julia> typeof(t)
Tuple{String, Float64, Symbol}

К элементам кортежа можно обращаться по индексам, как и в случае с массивами:

julia> t[2]
1.2

Их также можно распаковывать следующим образом:

julia> a, b, c = t
("hello", 1.2, :foo)

julia> b
1.2

Значениям также можно присваивать имена, что является удобным способом создания легких структур данных.

julia> t = (word = "hello", num = 1.2, sym = :foo)
(word = "hello", num = 1.2, sym = :foo)

К значениям можно обращаться через точечную нотацию:

julia> t.word
"hello"

Словари

Как и в Python, в Julia есть встроенная поддержка словарей. Словари предоставляют универсальный способ сопоставления ключей со значениями. Например, целочисленные значения могут сопоставляться со строками:

julia> d1 = Dict(1 => "A", 2 => "B", 4 => "D")
Dict{Int64, String} with 3 entries:
  4 => "D"
  2 => "B"
  1 => "A"

И еще раз о типах: Dict{Int64,String} — это словарь с ключами типа Int64 и значениями типа String.

Для поиска значения используется синтаксис с квадратными скобками:

julia> d1[2]
"B"

Словари поддерживают нецелочисленные ключи и смешанные типы данных:

julia> Dict("A" => 1, "B" => 2.5, "D" => 2 - 3im)
Dict{String, Number} with 3 entries:
  "B" => 2.5
  "A" => 1
  "D" => 2-3im

Типы Julia образуют иерархию. Здесь тип значения словаря — Number, что является обобщением для Int64, Float64 и Complex{Int}. Конечные узлы в этой иерархии называются «конкретными» типами, а все остальные — «абстрактными». В общем случае наличие переменных абстрактных типов, таких как Number, может замедлять выполнение кода, поэтому следует стараться, чтобы все элементы в словаре или векторе были одного типа. Например, в этом случае можно было бы представить каждый элемент как Complex{Float64}:

julia> Dict("A" => 1.0 + 0.0im, "B" => 2.5 + 0.0im, "D" => 2.0 - 3.0im)
Dict{String, ComplexF64} with 3 entries:
  "B" => 2.5+0.0im
  "A" => 1.0+0.0im
  "D" => 2.0-3.0im

Словари могут быть вложенными:

julia> d2 = Dict("A" => 1, "B" => 2, "D" => Dict(:foo => 3, :bar => 4))
Dict{String, Any} with 3 entries:
  "B" => 2
  "A" => 1
  "D" => Dict(:bar=>4, :foo=>3)

julia> d2["B"]
2

julia> d2["D"][:foo]
3

Структуры

Пользовательские структуры данных можно определять с помощью struct:

julia> struct MyStruct
           x::Int
           y::String
           z::Dict{Int,Int}
       end


julia> a = MyStruct(1, "a", Dict(2 => 3))
Main.MyStruct(1, "a", Dict(2 => 3))

julia> a.x
1

По умолчанию они неизменяемые:

julia>     a.x = 2
setfield!: immutable struct of type MyStruct cannot be changed

Однако можно объявить и изменяемую структуру mutable struct:

julia> mutable struct MyStructMutable
           x::Int
           y::String
           z::Dict{Int,Int}
       end


julia> a = MyStructMutable(1, "a", Dict(2 => 3))
Main.MyStructMutable(1, "a", Dict(2 => 3))

julia> a.x
1

julia> a.x = 2
2

julia> a
Main.MyStructMutable(2, "a", Dict(2 => 3))

Циклы

В Julia изначально поддерживаются циклы в стиле for-each с синтаксисом for <value> in <collection> end:

julia> for i in 1:5
           println(i)
       end
1
2
3
4
5

Диапазоны строятся в виде start:stop или start:step:stop.

julia> for i in 1.2:1.1:5.6
           println(i)
       end
1.2
2.3
3.4
4.5
5.6

Такой цикл for-each также работает со словарями:

julia> for (key, value) in Dict("A" => 1, "B" => 2.5, "D" => 2 - 3im)
           println("$(key): $(value)")
       end
B: 2.5
A: 1
D: 2 - 3im

Обратите внимание, что в отличие от векторных языков, таких как MATLAB и R, циклы не приводят к значительному снижению производительности в Julia.

Порядок выполнения

Порядок выполнения в Julia аналогичен принятому в MATLAB: используются ключевые слова if-elseif-else-end и логические операторы || и && для операций или и и соответственно:

julia> for i in 0:5:15
           if i < 5
               println("$(i) is less than 5")
           elseif i < 10
               println("$(i) is less than 10")
           else
               if i == 10
                   println("the value is 10")
               else
                   println("$(i) is bigger than 10")
               end
           end
       end
0 is less than 5
5 is less than 10
the value is 10
15 is bigger than 10

Включения

Подобно таким языкам, как Haskell и Python, в Julia при построении массивов и словарей поддерживаются простые циклы, называемые включениями.

Список целых чисел по возрастанию:

julia> [i for i in 1:5]
5-element Vector{Int64}:
 1
 2
 3
 4
 5

Матрицы можно строить, включая несколько индексов:

julia> [i * j for i in 1:5, j in 5:10]
5×6 Matrix{Int64}:
  5   6   7   8   9  10
 10  12  14  16  18  20
 15  18  21  24  27  30
 20  24  28  32  36  40
 25  30  35  40  45  50

Для фильтрации некоторых значений можно использовать условные операторы:

julia> [i for i in 1:10 if i % 2 == 1]
5-element Vector{Int64}:
 1
 3
 5
 7
 9

Похожий синтаксис можно использовать для построения словарей:

julia> Dict("$(i)" => i for i in 1:10 if i % 2 == 1)
Dict{String, Int64} with 5 entries:
  "1" => 1
  "5" => 5
  "7" => 7
  "9" => 9
  "3" => 3

Функции

Простая функция определяется следующим образом:

julia> function print_hello()
           return println("hello")
       end
print_hello (generic function with 1 method)

julia> print_hello()
hello

В функцию можно добавлять аргументы:

julia> function print_it(x)
           return println(x)
       end
print_it (generic function with 1 method)

julia> print_it("hello")
hello

julia> print_it(1.234)
1.234

julia> print_it(:my_id)
my_id

Также возможны необязательные именованные аргументы:

julia> function print_it(x; prefix = "value:")
           return println("$(prefix) $(x)")
       end
print_it (generic function with 1 method)

julia> print_it(1.234)
value: 1.234

julia> print_it(1.234; prefix = "val:")
val: 1.234

Ключевое слово return служит для указания возвращаемых значений функции:

julia> function mult(x; y = 2.0)
           return x * y
       end
mult (generic function with 1 method)

julia> mult(4.0)
8.0

julia> mult(4.0; y = 5.0)
20.0

Анонимные функции

Посредством синтаксиса input -> output создается анонимная функция. Они наиболее полезны при передаче в другие функции. Например:

julia> f = x -> x^2
#11 (generic function with 1 method)

julia> f(2)
4

julia> map(x -> x^2, 1:4)
4-element Vector{Int64}:
  1
  4
  9
 16

Параметры типов

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

julia> function foo(x::Int)
           return x^2
       end
foo (generic function with 1 method)

julia> function foo(x::Float64)
           return exp(x)
       end
foo (generic function with 2 methods)

julia> function foo(x::Number)
           return x + 1
       end
foo (generic function with 3 methods)

julia> foo(2)
4

julia> foo(2.0)
7.38905609893065

julia> foo(1 + 1im)
2 + 1im

Но что произойдет, если вызвать foo с неподходящими входными данными?

julia>     foo([1, 2, 3])
MethodError: no method matching foo(::Vector{Int64})

Closest candidates are:
  foo(!Matched::Float64)
   @ Main REPL[2]:1
  foo(!Matched::Int64)
   @ Main REPL[1]:1
  foo(!Matched::Number)
   @ Main REPL[3]:1

Ошибка MethodError означает, что в функцию переданы данные типа, отличного от ожидаемого. В данном случае в сообщении об ошибке говорится о том, что функция не знает, как обрабатывать Vector{Int64}, но знает, как обрабатывать Float64, Int64 и Number.

Чтобы узнать, какие типы требуются, внимательно прочитайте раздел Closest candidates в сообщении об ошибке.

Трансляция

В приведенном выше примере не было определено, что делать при передаче объекта типа Vector в f. К счастью, в Julia есть удобный синтаксис для поэлементного применения f к массивам. Просто добавьте точку (.) между именем функции и открывающей скобкой (. Это работает для любой функции, включая функции с несколькими аргументами. Например:

julia> foo.([1, 2, 3])
3-element Vector{Int64}:
 1
 4
 9

Получаете ошибку MethodError при вызове функции, принимающей тип Vector, Matrix или Array? Попробуйте воспользоваться трансляцией.

Изменяемые и неизменяемые объекты

Некоторые типы в Julia являются изменяемыми, то есть содержащиеся в них значения можно изменять. Хорошим примером может служить массив. Содержимое массива можно изменять, не создавая новый массив.

Напротив, такие типы, как Float64, являются неизменяемыми. Содержимое Float64 изменить нельзя.

Это необходимо учитывать при передаче типов в функции. Например:

julia> function mutability_example(mutable_type::Vector{Int}, immutable_type::Int)
           mutable_type[1] += 1
           immutable_type += 1
           return
       end
mutability_example (generic function with 1 method)

julia> mutable_type = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> immutable_type = 1
1

julia> mutability_example(mutable_type, immutable_type)


julia> println("mutable_type: $(mutable_type)")
mutable_type: [2, 2, 3]

julia> println("immutable_type: $(immutable_type)")
immutable_type: 1

Поскольку Vector{Int} является изменяемым типом, изменение переменной внутри функции привело к изменению значения за ее пределами. Напротив, изменение immutable_type не привело к изменению значения за пределами функции.

Проверить изменяемость можно с помощью функции isimmutable:

julia> isimmutable([1, 2, 3])
false

julia> isimmutable(1)
true

Диспетчер пакетов

Установка пакетов

Каким бы замечательным ни был базовый язык Julia, в какой-то момент может понадобиться пакет расширения. Некоторые из таких пакетов являются встроенными, например, генерирование случайных чисел обеспечивается пакетом Random стандартной библиотеки. Такие пакеты загружаются с помощью команд using и import.

julia> using Random  # Эквивалент директивы `from Random import *` в Python


julia> import Random  # Эквивалент директивы `import Random` в Python


julia> Random.seed!(33)
Random.TaskLocalRNG()

julia> [rand() for i in 1:10]
10-element Vector{Float64}:
 0.4745319377345316
 0.9650392357070774
 0.8194019096093067
 0.9297749959069098
 0.3127122336048005
 0.9684448191382753
 0.9063743823581542
 0.8386731983150535
 0.5103924401614957
 0.9296414851080324

Для установки пакетов, не входящих в стандартную библиотеку Julia, служит диспетчер пакетов.

Например, JuMP можно установить так:

using Pkg
Pkg.add("JuMP")

Полный перечень зарегистрированных пакетов Julia см. в списке пакетов на JuliaHub.

Иногда может понадобиться использовать незарегистрированный пакет Julia. В этом случае установить пакет можно по URL-адресу репозитория в git.

using Pkg
Pkg.add("https://github.com/user-name/MyPackage.jl.git")

Среды пакетов

По умолчанию команда Pkg.add добавляет пакеты в глобальную среду Julia. Однако в Julia есть также встроенная поддержка виртуальных сред.

Активировать виртуальную среду можно так:

import Pkg; Pkg.activate("/path/to/environment")

Узнать, какие пакеты установлены в текущей среде, можно с помощью Pkg.status().

Мы настоятельно рекомендуем создавать среду Pkg для каждого проекта, создаваемого в Julia, и добавлять в нее только необходимые пакеты, чтобы не добавлять множество пакетов в глобальную среду. Дополнительные сведения по этой теме приводятся в документации по диспетчеру Pkg.