Ограничения
Язык JuMP основан на API MathOptInterface (MOI). По этой причине в JuMP используется следующая стандартная форма представления задач:
Каждое ограничение состоит из функции и множества. Например, называется не ограничением меньше или равно, а ограничением скалярная аффинная функция в «меньше чем», где функция принадлежит множеству меньше чем ]. Ограничения, состоящие из различных типов функций и множеств, сокращенно называются функцией во множестве.
На этой странице объясняется, как записывать различные типы ограничений в JuMP. Сведения о нелинейных ограничениях см. в разделе Nonlinear Modeling.
Добавление ограничения
Ограничение добавляется в модель JuMP с помощью макроса @constraint
. Используемый синтаксис зависит от типа добавляемого ограничения.
Добавление линейного ограничения
Линейные ограничения создаются с помощью макроса @constraint
:
julia> model = Model();
julia> @variable(model, x[1:3]);
julia> @constraint(model, c1, sum(x) <= 1)
c1 : x[1] + x[2] + x[3] ≤ 1
julia> @constraint(model, c2, x[1] + 2 * x[3] >= 2)
c2 : x[1] + 2 x[3] ≥ 2
julia> @constraint(model, c3, sum(i * x[i] for i in 1:3) == 3)
c3 : x[1] + 2 x[2] + 3 x[3] = 3
julia> @constraint(model, c4, 4 <= 2 * x[2] <= 5)
c4 : 2 x[2] ∈ [4, 5]
Нормализация
JuMP нормализует ограничения, перенося все члены, содержащие переменные, в левую часть, а все свободные члены — в правую часть. В результате получается следующее.
julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, c, 2x + 1 <= 4x + 4)
c : -2 x ≤ 3
Добавление квадратичного ограничения
Помимо аффинных функций, JuMP поддерживает ограничения с квадратичными членами. Например:
julia> model = Model();
julia> @variable(model, x[i=1:2])
2-element Vector{VariableRef}:
x[1]
x[2]
julia> @variable(model, t >= 0)
t
julia> @constraint(model, my_q, x[1]^2 + x[2]^2 <= t^2)
my_q : x[1]² + x[2]² - t² ≤ 0
Так как решатели могут учитывать то, что ограничение является квадратичным, добавлять квадратичные ограничения предпочтительнее с помощью макроса |
Векторизованные ограничения
Добавлять ограничения в JuMP можно также с использованием векторизованной линейной алгебры. Например:
julia> model = Model();
julia> @variable(model, x[i=1:2])
2-element Vector{VariableRef}:
x[1]
x[2]
julia> A = [1 2; 3 4]
2×2 Matrix{Int64}:
1 2
3 4
julia> b = [5, 6]
2-element Vector{Int64}:
5
6
julia> @constraint(model, con_vector, A * x == b)
con_vector : [x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ Zeros()
julia> @constraint(model, con_scalar, A * x .== b)
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.EqualTo{Float64}}, ScalarShape}}:
con_scalar : x[1] + 2 x[2] = 5
con_scalar : 3 x[1] + 4 x[2] = 6
Ограничения ==
и .==
похожи, но между ними есть тонкое различие. В первом случае создается одно ограничение, а именно ограничение «MOI.VectorAffineFunction
в MOI.Zeros
». Во втором случае создается вектор ограничений «MOI.ScalarAffineFunction
в MOI.EqualTo
».
Выбор формулировки зависит от решателя и от того, что требуется сделать с объектом ограничения con_vector
или con_scalar
.
-
Если используется конический решатель, ожидается, что двойственностью для
con_vector
будетVector{Float64}
, и строка в ограничении не будет удаляться, выберите формулировку==
. -
Если используется решатель, принимающий список скалярных ограничений, например HiGHS, или нужно удалить часть ограничения либо обратиться к одной строке ограничения, например
dual(con_scalar[2])
, используйте трансляцию.==
.
JuMP преобразует оба ограничения в другую форму, если это требуется решателю, но эффективнее будет сразу выбрать правильный формат.
В качестве операторов сравнения в ограничении можно также использовать <=
, .<=
, >=
и .>=
.
julia> @constraint(model, A * x <= b)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ Nonpositives()
julia> @constraint(model, A * x .<= b)
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
x[1] + 2 x[2] ≤ 5
3 x[1] + 4 x[2] ≤ 6
julia> @constraint(model, A * x >= b)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ Nonnegatives()
julia> @constraint(model, A * x .>= b)
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.GreaterThan{Float64}}, ScalarShape}}:
x[1] + 2 x[2] ≥ 5
3 x[1] + 4 x[2] ≥ 6
Неравенства матриц
Неравенства матриц не поддерживаются из-за общей неоднозначности между поэлементными неравенствами и ограничением PSDCone
.
julia> model = Model();
julia> @variable(model, x[1:2, 1:2], Symmetric);
julia> @variable(model, y[1:2, 1:2], Symmetric);
julia> @constraint(model, x >= y)
ERROR: At none:1: `@constraint(model, x >= y)`:
The syntax `x >= y` is ambiguous for matrices because we cannot tell if
you intend a positive semidefinite constraint or an elementwise
inequality.
To create a positive semidefinite constraint, pass `PSDCone()` or
`HermitianPSDCone()`:
```julia
@constraint(model, x >= y, PSDCone())
```
To create an element-wise inequality, pass `Nonnegatives()`, or use
broadcasting:
```julia
@constraint(model, x >= y, Nonnegatives())
# или
@constraint(model, x .>= y)
```
Stacktrace:
[...]
Вместо этого используйте Set inequality syntax, чтобы задать множество, например PSDCone
или Nonnegatives
:
julia> @constraint(model, x >= y, PSDCone())
[x[1,1] - y[1,1] x[1,2] - y[1,2]
⋯ x[2,2] - y[2,2]] ∈ PSDCone()
julia> @constraint(model, x >= y, Nonnegatives())
[x[1,1] - y[1,1] x[1,2] - y[1,2]
⋯ x[2,2] - y[2,2]] ∈ Nonnegatives()
julia> @constraint(model, x >= y, Nonpositives())
[x[1,1] - y[1,1] x[1,2] - y[1,2]
⋯ x[2,2] - y[2,2]] ∈ Nonpositives()
julia> @constraint(model, x >= y, Zeros())
[x[1,1] - y[1,1] x[1,2] - y[1,2]
⋯ x[2,2] - y[2,2]] ∈ Zeros()
Особые случаи
Есть два исключения: если результат левой части минус правая часть представляет собой матрицу LinearAlgebra.Symmetric
или матрицу LinearAlgebra.Hermitian
, можно использовать синтаксис равенства без трансляции:
julia> using LinearAlgebra
julia> model = Model();
julia> @variable(model, X[1:2, 1:2], Symmetric)
2×2 Symmetric{VariableRef, Matrix{VariableRef}}:
X[1,1] X[1,2]
X[1,2] X[2,2]
julia> @constraint(model, X == LinearAlgebra.I)
[X[1,1] - 1 X[1,2]
⋯ X[2,2] - 1] ∈ Zeros()
При этом в матрицу ограничений добавляются только три строки, так как симметричные ограничения избыточны. Напротив, синтаксис трансляции добавляет четыре линейных ограничения:
julia> @constraint(model, X .== LinearAlgebra.I)
2×2 Matrix{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.EqualTo{Float64}}, ScalarShape}}:
X[1,1] = 1 X[1,2] = 0
X[1,2] = 0 X[2,2] = 1
То же самое верно для матриц LinearAlgebra.Hermitian
:
julia> using LinearAlgebra
julia> model = Model();
julia> @variable(model, X[1:2, 1:2] in HermitianPSDCone())
2×2 Hermitian{GenericAffExpr{ComplexF64, VariableRef}, Matrix{GenericAffExpr{ComplexF64, VariableRef}}}:
real(X[1,1]) real(X[1,2]) + imag(X[1,2]) im
real(X[1,2]) - imag(X[1,2]) im real(X[2,2])
julia> @constraint(model, X == LinearAlgebra.I)
[real(X[1,1]) - 1 real(X[1,2]) + imag(X[1,2]) im
real(X[1,2]) - imag(X[1,2]) im real(X[2,2]) - 1] ∈ Zeros()
julia> @constraint(model, X .== LinearAlgebra.I)
2×2 Matrix{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{ComplexF64}, MathOptInterface.EqualTo{ComplexF64}}, ScalarShape}}:
real(X[1,1]) = 1 real(X[1,2]) + imag(X[1,2]) im = 0
real(X[1,2]) - imag(X[1,2]) im = 0 real(X[2,2]) = 1
Контейнеры ограничений
Макрос @constraint
](../api.md#JuMP.@constraint-Tuple) поддерживает создание коллекций ограничений. Здесь мы рассмотрим синтаксис лишь вкратце, а более подробные сведения можно получить в разделе [Constraint containers.
Создание массивов ограничений:
julia> model = Model();
julia> @variable(model, x[1:3]);
julia> @constraint(model, c[i=1:3], x[i] <= i^2)
3-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
c[1] : x[1] ≤ 1
c[2] : x[2] ≤ 4
c[3] : x[3] ≤ 9
julia> c[2]
c[2] : x[2] ≤ 4
Множества могут быть любого типа Julia, поддерживающего итерацию:
julia> model = Model();
julia> @variable(model, x[1:3]);
julia> @constraint(model, c[i=2:3, ["red", "blue"]], x[i] <= i^2)
2-dimensional DenseAxisArray{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape},2,...} with index sets:
Dimension 1, 2:3
Dimension 2, ["red", "blue"]
And data, a 2×2 Matrix{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
c[2,red] : x[2] ≤ 4 c[2,blue] : x[2] ≤ 4
c[3,red] : x[3] ≤ 9 c[3,blue] : x[3] ≤ 9
julia> c[2, "red"]
c[2,red] : x[2] ≤ 4
Множества могут зависеть от предыдущих индексов:
julia> model = Model();
julia> @variable(model, x[1:3]);
julia> @constraint(model, c[i=1:3, j=i:3], x[i] <= j)
JuMP.Containers.SparseAxisArray{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}, 2, Tuple{Int64, Int64}} with 6 entries:
[1, 1] = c[1,1] : x[1] ≤ 1
[1, 2] = c[1,2] : x[1] ≤ 2
[1, 3] = c[1,3] : x[1] ≤ 3
[2, 2] = c[2,2] : x[2] ≤ 2
[2, 3] = c[2,3] : x[2] ≤ 3
[3, 3] = c[3,3] : x[3] ≤ 3
Элементы множеств можно фильтровать, используя синтаксис ;
:
julia> model = Model();
julia> @variable(model, x[1:9]);
julia> @constraint(model, c[i=1:9; mod(i, 3) == 0], x[i] <= i)
JuMP.Containers.SparseAxisArray{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}, 1, Tuple{Int64}} with 3 entries:
[3] = c[3] : x[3] ≤ 3
[6] = c[6] : x[6] ≤ 6
[9] = c[9] : x[9] ≤ 9
Регистрация ограничений
При создании ограничений JuMP регистрирует их в модели, используя соответствующий символ. Получить зарегистрированное имя можно посредством model[:key]
:
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> @constraint(model, my_c, 2x <= 1)
my_c : 2 x ≤ 1
julia> model
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 1
├ num_constraints: 1
│ └ AffExpr in MOI.LessThan{Float64}: 1
└ Names registered in the model
└ :my_c, :x
julia> model[:my_c] === my_c
true
Анонимные ограничения
Так как JuMP регистрирует ограничения внутри модели, создание двух ограничений с одинаковыми именами повышает вероятность случайных ошибок: #error:
julia> model = Model();
julia> @variable(model, x)
x
julia> @constraint(model, c, 2x <= 1)
c : 2 x ≤ 1
julia> @constraint(model, c, 2x <= 1)
ERROR: An object of name c is already attached to this model. If this
is intended, consider using the anonymous construction syntax, for example,
`x = @variable(model, [1:N], ...)` where the name of the object does
not appear inside the macro.
Alternatively, use `unregister(model, :c)` to first unregister
the existing name from the model. Note that this will not delete the
object; it will just remove the reference at `model[:c]`.
[...]
Частой причиной возникновения этой ошибки является добавление ограничений в цикле.
В качестве обходного пути JuMP предоставляет анонимные ограничения. Создадим анонимное ограничение, опустив аргумент имени:
julia> model = Model();
julia> @variable(model, x);
julia> c = @constraint(model, 2x <= 1)
2 x ≤ 1
Создадим контейнер анонимных ограничений, опустив имя перед [
:
julia> model = Model();
julia> @variable(model, x[1:3]);
julia> c = @constraint(model, [i = 1:3], x[i] <= i)
3-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
x[1] ≤ 1
x[2] ≤ 2
x[3] ≤ 3
Имена ограничений
Помимо символа, с которым регистрируются ограничения, у ограничений есть имя в виде строки String
, которое используется при выводе данных и записи в файлы.
Для получения и задания имени ограничения используйте методы name(::JuMP.ConstraintRef)
и set_name(::JuMP.ConstraintRef, ::String)
:
julia> model = Model(); @variable(model, x);
julia> @constraint(model, con, x <= 1)
con : x ≤ 1
julia> name(con)
"con"
julia> set_name(con, "my_con_name")
julia> con
my_con_name : x ≤ 1
Переопределить имя по умолчанию можно с помощью именованного аргумента base_name
:
julia> model = Model(); @variable(model, x);
julia> con = @constraint(model, [i=1:2], x <= i, base_name = "my_con")
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
my_con[1] : x ≤ 1
my_con[2] : x ≤ 2
Обратите внимание, что имена применяются к каждому элементу контейнера ограничений, а не к самому контейнеру:
julia> name(con[1])
"my_con[1]"
julia> set_name(con[1], "c")
julia> con
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
c : x ≤ 1
my_con[2] : x ≤ 2
Для некоторых моделей задание строкового имени для каждого ограничения может занимать неоправданно большую долю общего времени, затрачиваемого на построение модели. Чтобы отключить имена типа
Дополнительные сведения см. в разделе Disable string names. |
Получение ограничения по имени
Чтобы получить ограничение из модели, используйте метод constraint_by_name
:
julia> constraint_by_name(model, "c")
c : x ≤ 1
Если такого имени нет, возвращается nothing
:
julia> constraint_by_name(model, "bad_name")
С помощью constraint_by_name
можно искать только отдельные ограничения. Код наподобие следующего не будет работать:
julia> model = Model(); @variable(model, x);
julia> con = @constraint(model, [i=1:2], x <= i, base_name = "my_con")
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
my_con[1] : x ≤ 1
my_con[2] : x ≤ 2
julia> constraint_by_name(model, "my_con")
Для поиска коллекции ограничений не используйте метод constraint_by_name
. Вместо этого зарегистрируйте их с помощью синтаксиса model[:key] = value
:
julia> model = Model(); @variable(model, x);
julia> model[:con] = @constraint(model, [i=1:2], x <= i, base_name = "my_con")
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
my_con[1] : x ≤ 1
my_con[2] : x ≤ 2
julia> model[:con]
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
my_con[1] : x ≤ 1
my_con[2] : x ≤ 2
Строковые имена, символьные имена и привязки
У новых пользователей часто возникают затруднения с ограничениями. Частично проблема заключается в расхождении между именем, с которым зарегистрировано ограничение, и именем типа String
, используемым для вывода.
Вот краткое описание различий.
-
Ограничения создаются с помощью макроса
@constraint
. -
Ограничения могут быть именованными или анонимными.
-
Именованные ограничения имеют вид
@constraint(model, c, expr)
. Для именованных ограничений:-
Именем типа
String
ограничения является"c"
. -
Создается переменная Julia
c
, которая привязываетc
к ограничению JuMP. -
Имя
:c
регистрируется в модели как ключ со значениемc
.
-
-
Анонимные ограничения имеют вид
c = @constraint(model, expr)
. Для анонимных ограничений:-
Именем типа
String
ограничения является""
. -
Вы управляете именем переменной Julia, используемой в качестве привязки.
-
Имя не регистрируется как ключ в модели.
-
-
Именованный аргумент
base_name
может переопределять имя типаString
ограничения. -
Имена можно регистрировать в модели вручную с помощью
model[:key] = value
.
Вот пример различий:
julia> model = Model();
julia> @variable(model, x)
x
julia> c_binding = @constraint(model, 2x <= 1, base_name = "c")
c : 2 x ≤ 1
julia> model
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 1
├ num_constraints: 1
│ └ AffExpr in MOI.LessThan{Float64}: 1
└ Names registered in the model
└ :x
julia> c
ERROR: UndefVarError: `c` not defined
julia> c_binding
c : 2 x ≤ 1
julia> name(c_binding)
"c"
julia> model[:c_register] = c_binding
c : 2 x ≤ 1
julia> model
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 1
├ num_constraints: 1
│ └ AffExpr in MOI.LessThan{Float64}: 1
└ Names registered in the model
└ :c_register, :x
julia> model[:c_register]
c : 2 x ≤ 1
julia> model[:c_register] === c_binding
true
julia> c
ERROR: UndefVarError: `c` not defined
Макрос @constraints
При большом количестве вызовов @constraint
используйте макрос @constraints
, чтобы улучшить удобочитаемость:
julia> model = Model();
julia> @variable(model, x);
julia> @constraints(model, begin
2x <= 1
c, x >= -1
end)
(2 x ≤ 1, c : x ≥ -1)
julia> print(model)
Feasibility
Subject to
c : x ≥ -1
2 x ≤ 1
Макрос @constraints
возвращает кортеж определенных ограничений.
Двойственность
В JuMP используется понятие конической двойственности из MathOptInterface. Для линейных программ допустимая двойственность для ограничения >=
неотрицательна, а допустимая двойственность для ограничения <=
неположительна. Для ограничения равенства двойственность зависит от направления привязки.
Определение двойственности в JuMP не зависит от целевого назначения. То есть знак допустимых двойственных решений, связанных с ограничением, зависит от направления ограничения, а не от того, является ли задача задачей максимизации или минимизации. Это соглашение отличается от определения двойственности линейного программирования в некоторых популярных учебниках. Если в линейной программе требуется определение из учебника, вероятно, лучше будет воспользоваться методами |
Доступ к двойственному значению, связанному с ограничением в последнем решении, можно получить с помощью функции dual
. Чтобы проверить, есть ли у модели двойственное решение, доступное для запроса, используйте метод has_duals
. Например:
julia> model = Model(HiGHS.Optimizer);
julia> set_silent(model)
julia> @variable(model, x)
x
julia> @constraint(model, con, x <= 1)
con : x ≤ 1
julia> @objective(model, Min, -2x)
-2 x
julia> has_duals(model)
false
julia> optimize!(model)
julia> has_duals(model)
true
julia> dual(con)
-2.0
julia> @objective(model, Max, 2x)
2 x
julia> optimize!(model)
julia> dual(con)
-2.0
В помощь пользователям, которые не так хорошо знакомы с понятием конической двойственности, JuMP предоставляет метод shadow_price
. Он возвращает значение, которое можно интерпретировать как улучшение целевой функции при бесконечно малом ослаблении (в масштабе одной единицы) в правой части ограничения. shadow_price
можно использовать только для линейных ограничений с оператором сравнения <=
, >=
или ==
.
В приведенном выше примере dual(con)
возвращает -2.0
независимо от назначения оптимизации. Однако во втором случае при назначении оптимизации Max
метод shadow_price
возвращает следующее:
julia> shadow_price(con)
2.0
Двойственность границ переменных
Чтобы запросить двойственные переменные, связанные с границей переменной, сначала получите ссылку на ограничение, используя один из методов UpperBoundRef
, LowerBoundRef
или FixRef
, а затем вызовите dual
для возвращенной ссылки на ограничение. Функция reduced_cost
может упростить этот процесс, так как она возвращает теневую стоимость активной границы переменной (или ноль, если активной границы нет).
julia> model = Model(HiGHS.Optimizer);
julia> set_silent(model)
julia> @variable(model, x <= 1)
x
julia> @objective(model, Min, -2x)
-2 x
julia> optimize!(model)
julia> dual(UpperBoundRef(x))
-2.0
julia> reduced_cost(x)
-2.0
Изменение свободного члена
В этом разделе описывается, как изменить свободный член ограничения. Сделать это можно несколькими способами; мы рассмотрим три варианта.
Вариант 1. Изменение правой части
Для изменения правого (свободного) члена линейного или квадратичного ограничения используйте метод set_normalized_rhs
. Для запроса члена в правой части используйте метод normalized_rhs
.
julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, con, 2x <= 1)
con : 2 x ≤ 1
julia> set_normalized_rhs(con, 3)
julia> con
con : 2 x ≤ 3
julia> normalized_rhs(con)
3.0
|
Вариант 2. Использование фиксированных переменных
В случае сложных ограничений, например, состоящих из нескольких компонентов, каждый из которых имеет свободный член, может быть трудно вычислить стандартную форму члена в правой части.
На этот случай JuMP дает возможность фиксировать значения переменных с помощью функции fix
. При фиксации переменной ее нижняя и верхняя границы устанавливаются в одно и то же значение. Таким образом, изменения свободного члена можно имитировать, добавив новую переменную и фиксируя ее с разными значениями. Вот пример:
julia> model = Model();
julia> @variable(model, x);
julia> @variable(model, const_term)
const_term
julia> @constraint(model, con, 2x <= const_term + 1)
con : 2 x - const_term ≤ 1
julia> fix(const_term, 1.0)
Теперь ограничение con
эквивалентно 2x <= 2
.
При передаче задачи в решатель фиксированные переменные не заменяются константами. Следовательно, даже если член |
Вариант 3. Изменение свободного члена функции
Третий вариант — использование метода add_to_function_constant
. Указанная константа прибавляется к функции ограничения «func
в set
». В следующем примере прибавление 2
к функции приводит к удалению 2
из правой части:
julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, con, 2x <= 1)
con : 2 x ≤ 1
julia> add_to_function_constant(con, 2)
julia> con
con : 2 x ≤ -1
julia> normalized_rhs(con)
-1.0
В случае интервальных ограничений константа удаляется из каждой границы:
julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, con, 0 <= 2x + 1 <= 2)
con : 2 x ∈ [-1, 1]
julia> add_to_function_constant(con, 3)
julia> con
con : 2 x ∈ [-4, -2]
Изменение переменного коэффициента
Скалярные ограничения
Чтобы изменить коэффициенты линейного члена в ограничении, используйте метод set_normalized_coefficient
. Чтобы запросить текущий коэффициент, используйте метод normalized_coefficient
.
julia> model = Model();
julia> @variable(model, x[1:2]);
julia> @constraint(model, con, 2x[1] + x[2] <= 1)
con : 2 x[1] + x[2] ≤ 1
julia> set_normalized_coefficient(con, x[2], 0)
julia> con
con : 2 x[1] ≤ 1
julia> normalized_coefficient(con, x[2])
0.0
Для изменения квадратичных членов передайте две переменные:
julia> model = Model();
julia> @variable(model, x[1:2]);
julia> @constraint(model, con, x[1]^2 + x[1] * x[2] <= 1)
con : x[1]² + x[1]*x[2] ≤ 1
julia> set_normalized_coefficient(con, x[1], x[1], 2)
julia> set_normalized_coefficient(con, x[1], x[2], 3)
julia> con
con : 2 x[1]² + 3 x[1]*x[2] ≤ 1
julia> normalized_coefficient(con, x[1], x[1])
2.0
julia> normalized_coefficient(con, x[1], x[2])
3.0
|
Векторные ограничения
Чтобы изменить коэффициенты векторнозначного ограничения, используйте метод set_normalized_coefficient
.
julia> model = Model();
julia> @variable(model, x)
x
julia> @constraint(model, con, [2x + 3x, 4x] in MOI.Nonnegatives(2))
con : [5 x, 4 x] ∈ MathOptInterface.Nonnegatives(2)
julia> set_normalized_coefficient(con, x, [(1, 3.0)])
julia> con
con : [3 x, 4 x] ∈ MathOptInterface.Nonnegatives(2)
julia> set_normalized_coefficient(con, x, [(1, 2.0), (2, 5.0)])
julia> con
con : [2 x, 5 x] ∈ MathOptInterface.Nonnegatives(2)
Удаление ограничения
Чтобы удалить ограничение из модели, используйте метод delete
. Чтобы проверить, принадлежит ли ограничение модели и не было ли оно удалено, используйте метод is_valid
.
julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, con, 2x <= 1)
con : 2 x ≤ 1
julia> is_valid(model, con)
true
julia> delete(model, con)
julia> is_valid(model, con)
false
Удаление ограничения не отменяет регистрацию символьной ссылки в модели. Поэтому создание нового ограничения с тем же именем приведет к ошибке:
julia> @constraint(model, con, 2x <= 1)
ERROR: An object of name con is already attached to this model. If this
is intended, consider using the anonymous construction syntax, for example,
`x = @variable(model, [1:N], ...)` where the name of the object does
not appear inside the macro.
Alternatively, use `unregister(model, :con)` to first unregister
the existing name from the model. Note that this will not delete the
object; it will just remove the reference at `model[:con]`.
[...]
После вызова delete
вызовите unregister
, чтобы удалить символьную ссылку:
julia> unregister(model, :con)
julia> @constraint(model, con, 2x <= 1)
con : 2 x ≤ 1
Метод |
Начальные значения
Чтобы указать начальное значение (также называемое мягким началом) для прямого и двойственного решений ограничения, используйте методы set_start_value
и set_dual_start_value
.
Чтобы запросить начальное значение для прямого и двойственного решений ограничения, используйте методы start_value
и dual_start_value
. Если начальное значение не задано, эти методы возвращают nothing
.
julia> model = Model();
julia> @variable(model, x)
x
julia> @constraint(model, con, x >= 10)
con : x ≥ 10
julia> start_value(con)
julia> set_start_value(con, 10.0)
julia> start_value(con)
10.0
julia> dual_start_value(con)
julia> set_dual_start_value(con, 2)
julia> dual_start_value(con)
2.0
Для векторнозначных ограничений требуется вектор:
julia> model = Model();
julia> @variable(model, x[1:3])
3-element Vector{VariableRef}:
x[1]
x[2]
x[3]
julia> @constraint(model, con, x in SecondOrderCone())
con : [x[1], x[2], x[3]] in MathOptInterface.SecondOrderCone(3)
julia> dual_start_value(con)
julia> set_dual_start_value(con, [1.0, 2.0, 3.0])
julia> dual_start_value(con)
3-element Vector{Float64}:
1.0
2.0
3.0
Упростить задание начальных значений для всех переменных и ограничений в модели может метод |
Контейнеры ограничений
Как и в случае с переменными (см. раздел Variable containers), JuMP предоставляет механизм для создания компактных групп ограничений. Ссылки на такие группы ограничений возвращаются в контейнерах. Поддерживаются три типа контейнеров ограничений: Array
, DenseAxisArray
и SparseAxisArray
. Далее рассматривается каждый из них.
Дополнительные сведения о контейнерах см. в разделе Containers. |
Массивы
Вот один из способов добавления компактной группы ограничений:
julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, con[i = 1:3], i * x <= i + 1)
3-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
con[1] : x ≤ 2
con[2] : 2 x ≤ 3
con[3] : 3 x ≤ 4
JuMP возвращает ссылки на три ограничения в массиве Array
, который привязан к переменной Julia con
. К этому массиву можно обращаться и создавать срезы так же, как и к любому массиву Julia:
julia> con[1]
con[1] : x ≤ 2
julia> con[2:3]
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
con[2] : 2 x ≤ 3
con[3] : 3 x ≤ 4
Также можно создавать анонимные контейнеры, опуская имя (например, con
) перед квадратными скобками:
julia> con = @constraint(model, [i = 1:2], i * x <= i + 1)
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
x ≤ 2
2 x ≤ 3
Как и в случае с @variable
, JuMP формирует массив Array
ограничений, если во время компиляции определяет, что индексы представляют собой целочисленные диапазоны, начинающиеся с единицы. Поэтому при con[1:b]
создается массив Array
, а при con[a:b]
— нет. Особым случаем является con[Base.OneTo(n)]
, при котором создается Array
. Если JuMP не удается определить, что индексы представляют собой начинающиеся с единицы целочисленные диапазоны (например, в случае con[a:b]
), вместо этого создается массив DenseAxisArray
.
DenseAxisArray
Синтаксис построения массива DenseAxisArray
ограничений очень похож на синтаксис построения массива DenseAxisArray
переменных.
julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, con[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}}:
con[1,2] : x ≤ 3 con[1,3] : x ≤ 4
con[2,2] : 2 x ≤ 3 con[2,3] : 2 x ≤ 4
SparseAxisArray
Синтаксис построения массива SparseAxisArray
ограничений очень похож на синтаксис построения массива SparseAxisArray
переменных.
julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, con[i = 1:2, j = 1:2; i != j], i * x <= j + 1)
JuMP.Containers.SparseAxisArray{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}, 2, Tuple{Int64, Int64}} with 2 entries:
[1, 2] = con[1,2] : x ≤ 3
[2, 1] = con[2,1] : 2 x ≤ 2
При большом количестве измерений индексов и высокой степени разреженности см. раздел Performance considerations. |
Принудительное задание типа контейнера
При создании контейнера ограничений JuMP пытается выбрать наиболее ограниченный тип контейнера, в котором могут храниться ограничения. Однако поскольку это происходит во время анализа, не всегда выбор будет оптимальным. Как и в случае с @variable
, тип контейнера можно задать принудительно с помощью именованного аргумента container
. Синтаксис и обоснование см. в документации по переменным.
Ограничения с одинаковыми индексами
Контейнеры часто применяются для создания ограничений на основе множества индексов. Однако зачастую индексы могут повторяться:
julia> model = Model();
julia> @variable(model, x[1:2]);
julia> @variable(model, y[1:2]);
julia> @constraints(model, begin
[i=1:2, j=1:2, k=1:2], i * x[j] <= k
[i=1:2, j=1:2, k=1:2], i * y[j] <= k
end);
Такой код трудно читать, и приходится часто выполнять копирование. Более удобным для восприятия является цикл for:
julia> for i=1:2, j=1:2, k=1:2
@constraints(model, begin
i * x[j] <= k
i * y[j] <= k
end)
end
Доступ к ограничениям модели
Для запроса типов ограничений «функция во множестве» в модели используйте метод list_of_constraint_types
:
julia> model = Model();
julia> @variable(model, x[i=1:2] >= i, Int);
julia> @constraint(model, x[1] + x[2] <= 1);
julia> list_of_constraint_types(model)
3-element Vector{Tuple{Type, Type}}:
(AffExpr, MathOptInterface.LessThan{Float64})
(VariableRef, MathOptInterface.GreaterThan{Float64})
(VariableRef, MathOptInterface.Integer)
Для определенного сочетания типов функции и множества используйте num_constraints
для получения количества ограничений и all_constraints
для доступа к списку ссылок на них:
julia> num_constraints(model, VariableRef, MOI.Integer)
2
julia> cons = all_constraints(model, VariableRef, MOI.Integer)
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.VariableIndex, MathOptInterface.Integer}, ScalarShape}}:
x[1] integer
x[2] integer
Можно также подсчитать общее количество ограничений в модели, но при этом необходимо явно указать, следует ли подсчитывать ограничения VariableRef
, такие как ограничения границ и целочисленности:
julia> num_constraints(model; count_variable_in_set_constraints = true)
5
julia> num_constraints(model; count_variable_in_set_constraints = false)
1
То же самое верно для all_constraints
:
julia> all_constraints(model; include_variable_in_set_constraints = true)
5-element Vector{ConstraintRef}:
x[1] + x[2] ≤ 1
x[1] ≥ 1
x[2] ≥ 2
x[1] integer
x[2] integer
julia> all_constraints(model; include_variable_in_set_constraints = false)
1-element Vector{ConstraintRef}:
x[1] + x[2] ≤ 1
Если необходим более детальный контроль того, какие ограничения следует включить в подсчет, используйте такой вариант:
julia> sum(
num_constraints(model, F, S) for
(F, S) in list_of_constraint_types(model) if F != VariableRef
)
1
Используйте constraint_object
, чтобы получить экземпляр объекта AbstractConstraint
, в котором хранятся данные ограничения:
julia> con = constraint_object(cons[1])
ScalarConstraint{VariableRef, MathOptInterface.Integer}(x[1], MathOptInterface.Integer())
julia> con.func
x[1]
julia> con.set
MathOptInterface.Integer()
Ограничения MathOptInterface
Так как язык JuMP основан на интерфейсе MathOptInterface, вы можете добавлять любые ограничения, поддерживаемые MathOptInterface, используя синтаксис «функция во множестве». Список поддерживаемых функций и множеств см. в разделе Standard form problem.
В качестве псевдонима модуля |
import MathOptInterface as MOI
Например, следующие два ограничения эквивалентны:
julia> model = Model();
julia> @variable(model, x[1:3]);
julia> @constraint(model, 2 * x[1] <= 1)
2 x[1] ≤ 1
julia> @constraint(model, 2 * x[1] in MOI.LessThan(1.0))
2 x[1] ≤ 1
Можно также использовать любое множество, определенное в MathOptInterface:
julia> @constraint(model, x - [1; 2; 3] in MOI.Nonnegatives(3))
[x[1] - 1, x[2] - 2, x[3] - 3] ∈ MathOptInterface.Nonnegatives(3)
julia> @constraint(model, x in MOI.ExponentialCone())
[x[1], x[2], x[3]] ∈ MathOptInterface.ExponentialCone()
В остальных разделах на этой странице описываются функции и синтаксис, повышающие удобство в стандартных ситуациях моделирования, подобные определенному в JuMP синтаксису |
Синтаксис неравенства множеств
Для удобства моделирования доступен синтаксис @constraint(model, x >= y, Set())
, являющийся сокращением для @constraint(model, x - y in Set())
.
Таким образом, следующие вызовы эквивалентны:
julia> model = Model();
julia> @variable(model, x[1:2]);
julia> y = [0.5, 0.75];
julia> @constraint(model, x >= y, MOI.Nonnegatives(2))
[x[1] - 0.5, x[2] - 0.75] ∈ MathOptInterface.Nonnegatives(2)
julia> @constraint(model, x - y in MOI.Nonnegatives(2))
[x[1] - 0.5, x[2] - 0.75] ∈ MathOptInterface.Nonnegatives(2)
Ненулевые константы в этом синтаксисе не поддерживаются:
julia> @constraint(model, x >= 1, MOI.Nonnegatives(2))
ERROR: Operation `sub_mul` between `Vector{VariableRef}` and `Int64` is not allowed. This most often happens when you write a constraint like `x >= y` where `x` is an array and `y` is a constant. Use the broadcast syntax `x .- y >= 0` instead.
Stacktrace:
[...]
Используйте вместо этого такой вызов:
julia> @constraint(model, x .- 1 >= 0, MOI.Nonnegatives(2))
[x[1] - 1, x[2] - 1] ∈ MathOptInterface.Nonnegatives(2)
Синтаксис |
Конусные ограничения второго порядка
SecondOrderCone
ограничивает переменные t
и x
следующим множеством:
и . Добавить его можно следующим образом:
julia> model = Model();
julia> @variable(model, t)
t
julia> @variable(model, x[1:2])
2-element Vector{VariableRef}:
x[1]
x[2]
julia> @constraint(model, [t; x] in SecondOrderCone())
[t, x[1], x[2]] ∈ MathOptInterface.SecondOrderCone(3)
Повернутые конусные ограничения второго порядка
RotatedSecondOrderCone
Ограничивает переменные t
, u
и x
следующим множеством:
и . Добавить его можно следующим образом:
julia> model = Model();
julia> @variable(model, t)
t
julia> @variable(model, u)
u
julia> @variable(model, x[1:2])
2-element Vector{VariableRef}:
x[1]
x[2]
julia> @constraint(model, [t; u; x] in RotatedSecondOrderCone())
[t, u, x[1], x[2]] ∈ MathOptInterface.RotatedSecondOrderCone(4)
Особые упорядоченные множества типа 1
В особом упорядоченном множестве типа 1 (часто обозначаемом как SOS-I или SOS1) максимум один элемент может принимать ненулевое значение.
Ограничения SOS-I создаются с помощью множества SOS1
:
julia> model = Model();
julia> @variable(model, x[1:3])
3-element Vector{VariableRef}:
x[1]
x[2]
x[3]
julia> @constraint(model, x in SOS1())
[x[1], x[2], x[3]] in MathOptInterface.SOS1{Float64}([1.0, 2.0, 3.0])
Хотя упорядочение переменных и не является обязательным условием для допустимости, оно может быть полезно для решателей (например, переменные представляют собой различные фабрики, которые необходимо построить, может быть построена максимум одна фабрика, а фабрики можно упорядочить по стоимости). Для упорядочения можно указать вектор весов, по которым будут упорядочены переменные.
Например, в следующем ограничении:
julia> @constraint(model, x in SOS1([3.1, 1.2, 2.3]))
[x[1], x[2], x[3]] in MathOptInterface.SOS1{Float64}([3.1, 1.2, 2.3])
переменные x
имеют приоритет x[2]
, x[3]
, x[1]
.
Особые упорядоченные множества типа 2
В особом упорядоченном множестве типа 2 (SOS-II) не более двух элементов могут быть ненулевыми, и если есть два ненулевых элемента, они должны быть последовательными в порядке, обуславливаемом вектором весов.
Ограничения SOS-II создаются с помощью множества SOS2
:
julia> @constraint(model, x in SOS2([3.0, 1.0, 2.0]))
[x[1], x[2], x[3]] in MathOptInterface.SOS2{Float64}([3.0, 1.0, 2.0])
Возможные ненулевые пары — (x[1]
, x[3]
) и (x[2]
, x[3]
):
Если вектор весов не указан, JuMP осуществляет упорядочение согласно 1:length(x)
:
julia> @constraint(model, x in SOS2())
[x[1], x[2], x[3]] in MathOptInterface.SOS2{Float64}([1.0, 2.0, 3.0])
Индикаторные ограничения
Индикаторные ограничения состоят из двоичной переменной и линейного ограничения. Ограничение действует, когда двоичная переменная принимает значение 1
. Ограничение может действовать либо не действовать, когда двоичная переменная принимает значение 0
.
Применить ограничение x + y <= 1
, когда двоичная переменная a
равна 1
, можно так:
julia> model = Model();
julia> @variable(model, x)
x
julia> @variable(model, y)
y
julia> @variable(model, a, Bin)
a
julia> @constraint(model, a --> {x + y <= 1})
a --> {x + y ≤ 1}
Если ограничение должно действовать, когда a
равно нулю, добавьте !
или ¬
перед двоичной переменной.
julia> @constraint(model, !a --> {x + y <= 1})
!a --> {x + y ≤ 1}
В левой части индикаторного ограничения нельзя использовать выражение. |
Полуопределенные ограничения
Чтобы ограничить матрицу положительно полуопределенной формой (PSD), используйте PSDCone
.
julia> model = Model();
julia> @variable(model, X[1:2, 1:2])
2×2 Matrix{VariableRef}:
X[1,1] X[1,2]
X[2,1] X[2,2]
julia> @constraint(model, X >= 0, PSDCone())
[X[1,1] X[1,2]
X[2,1] X[2,2]] ∈ PSDCone()
По возможности лучше создавать матрицу Semidefinite variables с помощью макроса |
Неравенство X >= Y
двух квадратных матриц X
и Y
интерпретируется как ограничение X - Y
положительно полуопределенной формой.
julia> Y = [1 2; 2 1]
2×2 Matrix{Int64}:
1 2
2 1
julia> @constraint(model, X >= Y, PSDCone())
[X[1,1] - 1 X[1,2] - 2
X[2,1] - 2 X[2,2] - 1] ∈ PSDCone()
Синтаксис |
Симметрия
Решатели, поддерживающие ограничения PSD, обычно ожидают передачи символьно симметричной матрицы, имеющей одно и то же выражение в соответствующих недиагональных элементах. В нашем примере выражения элементов (1, 2)
и (2, 1)
— это соответственно X[1,2] - 2
и X[2,1] - 2
, то есть различаются.
Чтобы привести моделируемое ограничение к требуемому виду, решатели могут добавлять ограничение равенства X[1,2] - 2 == X[2,1] - 2
, обеспечивающее симметрию. Явно сообщить решателю, что матрица симметрична, можно с помощью LinearAlgebra.Symmetric
:
julia> import LinearAlgebra
julia> Z = [X[1, 1] X[1, 2]; X[1, 2] X[2, 2]]
2×2 Matrix{VariableRef}:
X[1,1] X[1,2]
X[1,2] X[2,2]
julia> @constraint(model, LinearAlgebra.Symmetric(Z) >= 0, PSDCone())
[X[1,1] X[1,2]
⋯ X[2,2]] ∈ PSDCone()
Обратите внимание, что элементы нижнего треугольника игнорируются, даже если они различаются, поэтому используйте этот способ с осторожностью:
julia> @constraint(model, LinearAlgebra.Symmetric(X) >= 0, PSDCone())
[X[1,1] X[1,2]
⋯ X[2,2]] ∈ PSDCone()
(Обратите внимание, что ошибка не возникает несмотря на то, что матрица X
не является симметричной.)
Ограничения комплементарности
Смешанное ограничение комплементарности F(x) ⟂ x
заключается в нахождении такого x
в интервале [lb, ub]
, что верно следующее:
-
F(x) == 0
, еслиlb < x < ub
-
F(x) >= 0
, еслиlb == x
-
F(x) <= 0
, еслиx == ub
В JuMP смешанные ограничения комплементарности поддерживаются посредством complements(F(x), x)
или F(x) ⟂ x
в макросе @constraint
. Интервальное множество [lb, ub]
получается из границ переменной x
.
Например, определить задачу 2x - 1 ⟂ x
с x ∈ [0, ∞)
можно так:
julia> model = Model();
julia> @variable(model, x >= 0)
x
julia> @constraint(model, 2x - 1 ⟂ x)
[2 x - 1, x] ∈ MathOptInterface.Complements(2)
У этой задачи есть уникальное решение в точке x = 0.5
.
Оператор перпендикулярности ⟂
можно ввести в большинстве редакторов (и в REPL Julia), набрав \perp<tab>
.
Есть и альтернативный подход, не требующий символа ⟂
, — используйте функцию complements
следующим образом:
julia> @constraint(model, complements(2x - 1, x))
[2 x - 1, x] ∈ MathOptInterface.Complements(2)
В обоих случаях сопоставление F(x)
передается в первом аргументе, а соответствующая переменная x
— во втором.
Также поддерживаются векторнозначные ограничения комплементарности:
julia> @variable(model, -2 <= y[1:2] <= 2)
2-element Vector{VariableRef}:
y[1]
y[2]
julia> M = [1 2; 3 4]
2×2 Matrix{Int64}:
1 2
3 4
julia> q = [5, 6]
2-element Vector{Int64}:
5
6
julia> @constraint(model, M * y + q ⟂ y)
[y[1] + 2 y[2] + 5, 3 y[1] + 4 y[2] + 6, y[1], y[2]] ∈ MathOptInterface.Complements(4)
Логические ограничения
Чтобы добавить логическое ограничение (множество MOI.EqualTo{Bool}
), используйте оператор :=
с правым членом типа Bool
:
julia> model = GenericModel{Bool}();
julia> @variable(model, x[1:2]);
julia> @constraint(model, x[1] || x[2] := true)
x[1] || x[2] = true
julia> @constraint(model, x[1] && x[2] := false)
x[1] && x[2] = false
julia> model
A JuMP Model
├ value_type: Bool
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 2
├ num_constraints: 2
│ └ GenericNonlinearExpr{GenericVariableRef{Bool}} in MOI.EqualTo{Bool}: 2
└ Names registered in the model
└ :x
Логические ограничения не следует добавлять с помощью оператора ==
, так как JuMP изменит форму записи на lhs - rhs = 0
, а также по той причине, что ограничения вида a == b == c
требуют скобок для различения вариантов (a == b) == c
и a == (b == c)
. В свою очередь, a == b := c
эквивалентно (a == b) := c
:
julia> model = Model();
julia> @variable(model, x[1:2]);
julia> rhs = false
false
julia> @constraint(model, (x[1] == x[2]) == rhs)
(x[1] == x[2]) - 0.0 = 0
julia> @constraint(model, x[1] == x[2] := rhs)
x[1] == x[2] = false