Variables
The term variable in mathematical optimization has many meanings. For example, optimization variables (also called decision variables) are the unknowns $x$ that we are solving for in the problem:
\[\begin{align} & \min_{x \in \mathbb{R}^n} & f_0(x) \\ & \;\;\text{s.t.} & f_i(x) & \in \mathcal{S}_i & i = 1 \ldots m \end{align}\]
To complicate things, Julia uses variable to mean a binding between a name and a value. For example, in the statement:
julia> x = 1
1x is a variable that stores the value 1.
JuMP uses variable in a third way, to mean an instance of the VariableRef struct. JuMP variables are the link between Julia and the optimization variables inside a JuMP model.
This page explains how to create and manage JuMP variables in a variety of contexts.
Create a variable
Create variables using the @variable macro:
julia> model = Model();
julia> @variable(model, x)
x
julia> typeof(x)
VariableRef (alias for GenericVariableRef{Float64})
julia> num_variables(model)
1Here x is a Julia variable that is bound to a VariableRef object, and we have added 1 decision variable to our model.
To make the binding more explicit, we could have written:
julia> model = Model();
julia> x = @variable(model, x)
xbut there is no need to in general; the macro does it for us.
When creating a variable, you can also specify variable bounds:
julia> model = Model();
julia> @variable(model, x_free)
x_free
julia> @variable(model, x_lower >= 0)
x_lower
julia> @variable(model, x_upper <= 1)
x_upper
julia> @variable(model, 2 <= x_interval <= 3)
x_interval
julia> @variable(model, x_fixed == 4)
x_fixed
julia> print(model)
Feasibility
Subject to
x_fixed = 4
x_lower ≥ 0
x_interval ≥ 2
x_upper ≤ 1
x_interval ≤ 3When creating a variable with a single lower- or upper-bound, and the value of the bound is not a numeric literal (for example, 1 or 1.0), the name of the variable must appear on the left-hand side. Putting the name on the right-hand side is an error. For example, to create a variable x:
a = 1
@variable(model, x >= 1) # ✓ Okay
@variable(model, 1.0 <= x) # ✓ Okay
@variable(model, x >= a) # ✓ Okay
@variable(model, a <= x) # × Not okay
@variable(model, x >= 1 / 2) # ✓ Okay
@variable(model, 1 / 2 <= x) # × Not okayContainers of variables
The @variable macro also supports creating collections of JuMP variables. We'll cover some brief syntax here; read the Variable containers section for more details.
You can create arrays of JuMP variables:
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> x[1, 2]
x[1,2]Index sets can be named, and bounds can depend on those names:
julia> model = Model();
julia> @variable(model, sqrt(i) <= x[i = 1:3] <= i^2)
3-element Vector{VariableRef}:
x[1]
x[2]
x[3]
julia> x[2]
x[2]Sets can be any Julia type that supports iteration:
julia> model = Model();
julia> @variable(model, x[i = 2:3, j = 1:2:3, ["red", "blue"]] >= 0)
3-dimensional DenseAxisArray{VariableRef,3,...} with index sets:
Dimension 1, 2:3
Dimension 2, 1:2:3
Dimension 3, ["red", "blue"]
And data, a 2×2×2 Array{VariableRef, 3}:
[:, :, "red"] =
x[2,1,red] x[2,3,red]
x[3,1,red] x[3,3,red]
[:, :, "blue"] =
x[2,1,blue] x[2,3,blue]
x[3,1,blue] x[3,3,blue]
julia> x[2, 1, "red"]
x[2,1,red]Sets can depend upon previous indices:
julia> model = Model();
julia> @variable(model, u[i = 1:2, j = i:3])
JuMP.Containers.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]and we can filter elements in the sets using the ; syntax:
julia> model = Model();
julia> @variable(model, v[i = 1:9; mod(i, 3) == 0])
JuMP.Containers.SparseAxisArray{VariableRef, 1, Tuple{Int64}} with 3 entries:
[3] = v[3]
[6] = v[6]
[9] = v[9]Registered variables
When you create variables, JuMP registers them inside the model using their corresponding symbol. Get a registered name using 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> model
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 1
├ num_constraints: 0
└ Names registered in the model
└ :x
julia> model[:x] === x
trueRegistered names are most useful when you start to write larger models and want to break up the model construction into functions:
julia> function set_objective(model::Model)
@objective(model, Min, 2 * model[:my_x] + 1)
return
end
set_objective (generic function with 1 method)
julia> model = Model();
julia> @variable(model, my_x);
julia> set_objective(model)
julia> print(model)
Min 2 my_x + 1
Subject toAnonymous variables
To reduce the likelihood of accidental bugs, and because JuMP registers variables inside a model, creating two variables with the same name is an error:
julia> model = Model();
julia> @variable(model, x)
x
julia> @variable(model, x)
ERROR: An object of name x 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, :x)` 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[:x]`.
[...]A common reason for encountering this error is adding variables in a loop.
As a work-around, JuMP provides anonymous variables. Create a scalar valued anonymous variable by omitting the name argument:
julia> model = Model();
julia> x = @variable(model)
_[1]Anonymous variables get printed as an underscore followed by a unique index of the variable.
Create a container of anonymous JuMP variables by dropping the name in front of the [:
julia> model = Model();
julia> y = @variable(model, [1:2])
2-element Vector{VariableRef}:
_[1]
_[2]The <= and >= short-hand cannot be used to set bounds on scalar-valued anonymous JuMP variables. Instead, use the lower_bound and upper_bound keywords:
julia> model = Model();
julia> x_lower = @variable(model, lower_bound = 1.0)
_[1]
julia> x_upper = @variable(model, upper_bound = 2.0)
_[2]
julia> x_interval = @variable(model, lower_bound = 3.0, upper_bound = 4.0)
_[3]Variable names
In addition to the symbol that variables are registered with, JuMP variables have a String name that is used for printing and writing to file formats.
Get and set the name of a variable using name and set_name:
julia> model = Model();
julia> @variable(model, x)
x
julia> name(x)
"x"
julia> set_name(x, "my_x_name")
julia> x
my_x_nameOverride the default choice of name using the base_name keyword:
julia> model = Model();
julia> @variable(model, x[i=1:2], base_name = "my_var")
2-element Vector{VariableRef}:
my_var[1]
my_var[2]Note that names apply to each element of the container, not to the container of variables:
julia> name(x[1])
"my_var[1]"
julia> set_name(x[1], "my_x")
julia> x
2-element Vector{VariableRef}:
my_x
my_var[2]For some models, setting the string name of each variable can take a non-trivial portion of the total time required to build the model. Turn off String names by passing set_string_name = false to @variable:
julia> model = Model();
julia> @variable(model, x, set_string_name = false)
_[1]See Disable string names for more information.
Retrieve a variable by name
Retrieve a variable from a model using variable_by_name:
julia> variable_by_name(model, "my_x")
my_xIf the name is not present, nothing will be returned:
julia> variable_by_name(model, "bad_name")You can only look up individual variables using variable_by_name. Something like this will not work:
julia> model = Model();
julia> @variable(model, [i = 1:2], base_name = "my_var")
2-element Vector{VariableRef}:
my_var[1]
my_var[2]
julia> variable_by_name(model, "my_var")To look up a collection of variables, do not use variable_by_name. Instead, register them using the model[:key] = value syntax:
julia> model = Model();
julia> model[:x] = @variable(model, [i = 1:2], base_name = "my_var")
2-element Vector{VariableRef}:
my_var[1]
my_var[2]
julia> model[:x]
2-element Vector{VariableRef}:
my_var[1]
my_var[2]String names, symbolic names, and bindings
It's common for new users to experience confusion relating to JuMP variables. Part of the problem is the overloaded use of "variable" in mathematical optimization, along with the difference between the name that a variable is registered under and the String name used for printing.
Here's a summary of the differences:
- JuMP variables are created using
@variable. - JuMP variables can be named or anonymous.
- Named JuMP variables have the form
@variable(model, x). For named variables:- The
Stringname of the variable is set to"x". - A Julia variable
xis created that bindsxto the JuMP variable. - The name
:xis registered as a key in the model with the valuex.
- The
- Anonymous JuMP variables have the form
x = @variable(model). For anonymous variables:- The
Stringname of the variable is set to"". When printed, this is replaced with"_[i]"whereiis the index of the variable. - You control the name of the Julia variable used as the binding.
- No name is registered as a key in the model.
- The
- The
base_namekeyword can override theStringname of the variable. - You can manually register names in the model via
model[:key] = value
Here's an example that should make things clearer:
julia> model = Model();
julia> x_binding = @variable(model, base_name = "x")
x
julia> model
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 1
├ num_constraints: 0
└ Names registered in the model: none
julia> x
ERROR: UndefVarError: `x` not defined
julia> x_binding
x
julia> name(x_binding)
"x"
julia> model[:x_register] = x_binding
x
julia> model
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 1
├ num_constraints: 0
└ Names registered in the model
└ :x_register
julia> model[:x_register]
x
julia> model[:x_register] === x_binding
true
julia> x
ERROR: UndefVarError: `x` not definedCreate, delete, and modify variable bounds
Query whether a variable has a bound using has_lower_bound, has_upper_bound, and is_fixed:
julia> has_lower_bound(x_free)
false
julia> has_upper_bound(x_upper)
true
julia> is_fixed(x_fixed)
trueIf a variable has a particular bound, query the value of it using lower_bound, upper_bound, and fix_value:
julia> lower_bound(x_interval)
2.0
julia> upper_bound(x_interval)
3.0
julia> fix_value(x_fixed)
4.0Querying the value of a bound that does not exist will result in an error.
Delete variable bounds using delete_lower_bound, delete_upper_bound, and unfix:
julia> delete_lower_bound(x_lower)
julia> has_lower_bound(x_lower)
false
julia> delete_upper_bound(x_upper)
julia> has_upper_bound(x_upper)
false
julia> unfix(x_fixed)
julia> is_fixed(x_fixed)
falseSet or update variable bounds using set_lower_bound, set_upper_bound, and fix:
julia> set_lower_bound(x_lower, 1.1)
julia> set_upper_bound(x_upper, 2.1)
julia> fix(x_fixed, 4.1)Fixing a variable with existing bounds will throw an error. To delete the bounds prior to fixing, use fix(variable, value; force = true).
julia> model = Model();
julia> @variable(model, x >= 1)
x
julia> fix(x, 2)
ERROR: Unable to fix x to 2 because it has existing variable bounds. Consider calling `JuMP.fix(variable, value; force=true)` which will delete existing bounds before fixing the variable.
julia> fix(x, 2; force = true)
julia> fix_value(x)
2.0Use fix instead of @constraint(model, x == 2). The former modifies variable bounds, while the latter adds a new linear constraint to the problem.
Binary variables
Binary variables are constrained to the set $x \in \{0, 1\}$.
Create a binary variable by passing Bin as an optional positional argument:
julia> model = Model();
julia> @variable(model, x, Bin)
xSolvers use tolerances to decide whether a variable satisfies the binary constraint. Thus, the true feasible region is $[-\varepsilon, \varepsilon] \cup [1 - \varepsilon, 1 + \varepsilon]$, where $\varepsilon$ is solver-specific, but typically 1e-6. As a result, you should expect the value(x) of a Bin variable to sometimes take a value like -0.0, 1e-8, or 0.999999.
Check if a variable is binary using is_binary:
julia> is_binary(x)
trueDelete a binary constraint using unset_binary:
julia> unset_binary(x)
julia> is_binary(x)
falseBinary variables can also be created by setting the binary keyword to true:
julia> model = Model();
julia> @variable(model, x, binary=true)
xor by using set_binary:
julia> model = Model();
julia> @variable(model, x)
x
julia> set_binary(x)Integer variables
Integer variables are constrained to the set $x \in \mathbb{Z}$.
Create an integer variable by passing Int as an optional positional argument:
julia> model = Model();
julia> @variable(model, x, Int)
xSolvers use tolerances to decide whether a variable satisfies the integer constraint. Thus, the true feasible region is $\cup_{z \in \mathbb{Z}}[z - \varepsilon, z + \varepsilon]$, where $\varepsilon$ is solver-specific, but typically 1e-6. As a result, you should expect the value(x) of an Int variable to sometimes take a value like 1e-8, or 2.999999.
Check if a variable is integer using is_integer:
julia> is_integer(x)
trueDelete an integer constraint using unset_integer.
julia> unset_integer(x)
julia> is_integer(x)
falseInteger variables can also be created by setting the integer keyword to true:
julia> model = Model();
julia> @variable(model, x, integer=true)
xor by using set_integer:
julia> model = Model();
julia> @variable(model, x)
x
julia> set_integer(x)The relax_integrality function relaxes all integrality constraints in the model, returning a function that can be called to undo the operation later on.
Semi-integer and semi-continuous variables
Semi-continuous variables are constrained to the set $x \in \{0\} \cup [l, u]$.
Create a semi-continuous variable using the Semicontinuous set:
julia> model = Model();
julia> @variable(model, x in Semicontinuous(1.5, 3.5))
xSemi-integer variables are constrained to the set $x \in \{0\} \cup \{l, l+1, \dots, u\}$.
Create a semi-integer variable using the Semiinteger set:
julia> model = Model();
julia> @variable(model, x in Semiinteger(1.0, 3.0))
xStart values
There are two ways to provide a primal starting solution (also called MIP-start or a warmstart) for each variable:
- using the
startkeyword in the@variablemacro - using
set_start_value
The starting value of a variable can be queried using start_value. If no start value has been set, start_value will return nothing.
julia> model = Model();
julia> @variable(model, x)
x
julia> start_value(x)
julia> @variable(model, y, start = 1)
y
julia> start_value(y)
1.0
julia> set_start_value(y, 2)
julia> start_value(y)
2.0The start keyword argument can depend on the indices of a variable container:
julia> model = Model();
julia> @variable(model, z[i = 1:2], start = i^2)
2-element Vector{VariableRef}:
z[1]
z[2]
julia> start_value.(z)
2-element Vector{Float64}:
1.0
4.0Some solvers do not support start values. If a solver does not support start values, an MathOptInterface.UnsupportedAttribute{MathOptInterface.VariablePrimalStart} error will be thrown.
To set the optimal solution from a previous solve as a new starting value, use all_variables to get a vector of all the variables in the model, then run:
x = all_variables(model)
x_solution = value.(x)
set_start_value.(x, x_solution)Alternatively, use set_start_values.
Delete a variable
Use delete to delete a variable from a model. Use is_valid to check if a variable belongs to a model and has not been deleted.
julia> model = Model();
julia> @variable(model, x)
x
julia> is_valid(model, x)
true
julia> delete(model, x)
julia> is_valid(model, x)
falseDeleting a variable does not unregister the corresponding name from the model. Therefore, creating a new variable of the same name will throw an error:
julia> @variable(model, x)
ERROR: An object of name x 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, :x)` 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[:x]`.
[...]After calling delete, call unregister to remove the symbolic reference:
julia> unregister(model, :x)
julia> @variable(model, x)
xdelete does not automatically unregister because we do not distinguish between names that are automatically registered by JuMP macros and names that are manually registered by the user by setting values in object_dictionary. In addition, deleting a variable and then adding a new variable of the same name is an easy way to introduce bugs into your code.
Variable containers
JuMP provides a mechanism for creating collections of variables in three types of data structures, which we refer to as containers.
The three types are Array, Containers.DenseAxisArray, and Containers.SparseAxisArray. We explain each of these in the following.
You can read more about containers in the Containers section.
Arrays
We have already seen the creation of an array of JuMP variables with the x[1:2] syntax. This can be extended to create multi-dimensional arrays of JuMP variables. For example:
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]Arrays of JuMP variables can be indexed and sliced as follows:
julia> x[1, 2]
x[1,2]
julia> x[2, :]
2-element Vector{VariableRef}:
x[2,1]
x[2,2]Variable bounds can depend upon the indices:
julia> model = Model();
julia> @variable(model, x[i=1:2, j=1:2] >= 2i + j)
2×2 Matrix{VariableRef}:
x[1,1] x[1,2]
x[2,1] x[2,2]
julia> lower_bound.(x)
2×2 Matrix{Float64}:
3.0 4.0
5.0 6.0JuMP will form an Array of JuMP variables when it can determine at compile time that the indices are one-based integer ranges. Therefore x[1:b] will create an Array of JuMP variables, but x[a:b] will not. If JuMP cannot determine that the indices are one-based integer ranges (for example, in the case of x[a:b]), JuMP will create a Containers.DenseAxisArray instead.
DenseAxisArrays
We often want to create arrays where the indices are not one-based integer ranges. For example, we may want to create a variable indexed by the name of a product or a location. The syntax is the same as that above, except with an arbitrary vector as an index as opposed to a one-based range. The biggest difference is that instead of returning an Array of JuMP variables, JuMP will return a Containers.DenseAxisArray. For example:
julia> model = Model();
julia> @variable(model, x[1:2, [:A,:B]])
2-dimensional DenseAxisArray{VariableRef,2,...} with index sets:
Dimension 1, Base.OneTo(2)
Dimension 2, [:A, :B]
And data, a 2×2 Matrix{VariableRef}:
x[1,A] x[1,B]
x[2,A] x[2,B]A Containers.DenseAxisArray can be indexed and sliced as follows:
julia> x[1, :A]
x[1,A]
julia> x[2, :]
1-dimensional DenseAxisArray{VariableRef,1,...} with index sets:
Dimension 1, [:A, :B]
And data, a 2-element Vector{VariableRef}:
x[2,A]
x[2,B]Bounds can depend upon indices:
julia> model = Model();
julia> @variable(model, x[i=2:3, j=1:2:3] >= 0.5i + j)
2-dimensional DenseAxisArray{VariableRef,2,...} with index sets:
Dimension 1, 2:3
Dimension 2, 1:2:3
And data, a 2×2 Matrix{VariableRef}:
x[2,1] x[2,3]
x[3,1] x[3,3]
julia> lower_bound.(x)
2-dimensional DenseAxisArray{Float64,2,...} with index sets:
Dimension 1, 2:3
Dimension 2, 1:2:3
And data, a 2×2 Matrix{Float64}:
2.0 4.0
2.5 4.5SparseAxisArrays
The third container type that JuMP natively supports is Containers.SparseAxisArray. These arrays are created when the indices do not form a rectangular set. For example, this applies when indices have a dependence upon previous indices (called triangular indexing). JuMP supports this as follows:
julia> model = Model();
julia> @variable(model, x[i=1:2, j=i:2])
JuMP.Containers.SparseAxisArray{VariableRef, 2, Tuple{Int64, Int64}} with 3 entries:
[1, 1] = x[1,1]
[1, 2] = x[1,2]
[2, 2] = x[2,2]We can also conditionally create variables via a JuMP-specific syntax. This syntax appends a comparison check that depends upon the named indices and is separated from the indices by a semi-colon (;). For example:
julia> model = Model();
julia> @variable(model, x[i=1:4; mod(i, 2)==0])
JuMP.Containers.SparseAxisArray{VariableRef, 1, Tuple{Int64}} with 2 entries:
[2] = x[2]
[4] = x[4]Performance considerations
When using the semi-colon as a filter, JuMP iterates over all indices and evaluates the conditional for each combination. If there are many index dimensions and a large amount of sparsity, this can be inefficient.
For example:
julia> model = Model();
julia> N = 10
10
julia> S = [(1, 1, 1), (N, N, N)]
2-element Vector{Tuple{Int64, Int64, Int64}}:
(1, 1, 1)
(10, 10, 10)
julia> @time @variable(model, x1[i=1:N, j=1:N, k=1:N; (i, j, k) in S])
0.203861 seconds (392.22 k allocations: 23.977 MiB, 99.10% compilation time)
JuMP.Containers.SparseAxisArray{VariableRef, 3, Tuple{Int64, Int64, Int64}} with 2 entries:
[1, 1, 1 ] = x1[1,1,1]
[10, 10, 10] = x1[10,10,10]
julia> @time @variable(model, x2[S])
0.045407 seconds (65.24 k allocations: 3.771 MiB, 99.15% compilation time)
1-dimensional DenseAxisArray{VariableRef,1,...} with index sets:
Dimension 1, [(1, 1, 1), (10, 10, 10)]
And data, a 2-element Vector{VariableRef}:
x2[(1, 1, 1)]
x2[(10, 10, 10)]The first option is slower because it is equivalent to:
julia> model = Model();
julia> x1 = Dict{NTuple{3,Int},VariableRef}()
Dict{Tuple{Int64, Int64, Int64}, VariableRef}()
julia> for i in 1:N
for j in 1:N
for k in 1:N
if (i, j, k) in S
x1[i, j, k] = @variable(model, base_name = "x1[$i,$j,$k]")
end
end
end
end
julia> x1
Dict{Tuple{Int64, Int64, Int64}, VariableRef} with 2 entries:
(1, 1, 1) => x1[1,1,1]
(10, 10, 10) => x1[10,10,10]If performance is a concern, explicitly construct the set of indices instead of using the filtering syntax.
Forcing the container type
When creating a container of JuMP variables, JuMP will attempt to choose the tightest container type that can store the JuMP variables. Thus, it will prefer to create an Array before a Containers.DenseAxisArray and a Containers.DenseAxisArray before a Containers.SparseAxisArray. However, because this happens at compile time, JuMP does not always make the best choice. To illustrate this, consider the following example:
julia> model = Model();
julia> A = 1:2
1:2
julia> @variable(model, x[A])
1-dimensional DenseAxisArray{VariableRef,1,...} with index sets:
Dimension 1, 1:2
And data, a 2-element Vector{VariableRef}:
x[1]
x[2]Since the value (and type) of A is unknown at parsing time, JuMP is unable to infer that A is a one-based integer range. Therefore, JuMP creates a Containers.DenseAxisArray, even though it could store these two variables in a standard one-dimensional Array.
We can share our knowledge that it is possible to store these JuMP variables as an array by setting the container keyword:
julia> @variable(model, y[A], container=Array)
2-element Vector{VariableRef}:
y[1]
y[2]JuMP now creates a Vector of JuMP variables instead of a `Containers.DenseAxisArray. Choosing an invalid container type will throw an error.
User-defined containers
In addition to the built-in container types, you can create your own collections of JuMP variables.
This is a point that users often overlook: you are not restricted to the built-in container types in JuMP.
For example, the following code creates a dictionary with symmetric matrices as the values:
julia> model = Model();
julia> variables = Dict{Symbol,Array{VariableRef,2}}(
key => @variable(model, [1:2, 1:2], Symmetric, base_name = "$(key)")
for key in [:A, :B]
)
Dict{Symbol, Matrix{VariableRef}} with 2 entries:
:A => [A[1,1] A[1,2]; A[1,2] A[2,2]]
:B => [B[1,1] B[1,2]; B[1,2] B[2,2]]Another common scenario is a request to add variables to existing containers, for example:
using JuMP
model = Model()
@variable(model, x[1:2] >= 0)
# Later I want to add
@variable(model, x[3:4] >= 0)This is not possible with the built-in JuMP container types. However, you can use regular Julia types instead:
julia> model = Model();
julia> x = model[:x] = @variable(model, [1:2], lower_bound = 0, base_name = "x")
2-element Vector{VariableRef}:
x[1]
x[2]
julia> append!(x, @variable(model, [1:2], lower_bound = 0, base_name = "y"));
julia> model[:x]
4-element Vector{VariableRef}:
x[1]
x[2]
y[1]
y[2]Sparse arrays
To construct a SparseArrays.SparseMatrixCSC of JuMP variables, create a vector of JuMP variables for the non-zero elements, and then use the SparseArrays.sparse constructor:
julia> import SparseArrays
julia> model = Model();
julia> x = @variable(model, [1:5]);
julia> A = SparseArrays.sparse([1, 1, 2, 2, 3], [1, 2, 2, 3, 3], x)
3×3 SparseArrays.SparseMatrixCSC{VariableRef, Int64} with 5 stored entries:
_[1] _[2] ⋅
⋅ _[3] _[4]
⋅ ⋅ _[5]Semidefinite variables
Declare a square matrix of JuMP variables to be positive semidefinite by passing PSD as a positional argument:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2], PSD)
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]This will ensure that x is symmetric, and that all of its eigenvalues are nonnegative.
x must be a square 2-dimensional Array of JuMP variables; it cannot be a Containers.DenseAxisArray or a Containers.SparseAxisArray.
Use VariableInSetRef to obtain the associated constraint reference:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2], PSD)
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]
julia> c = VariableInSetRef(x)
[x[1,1] x[1,2]
⋯ x[2,2]] ∈ PSDCone()The PSD argument must be provided explicitly to the macro. Passing it via a variable throws an error:
julia> model = Model();
julia> type = :PSD
:PSD
julia> @variable(model, x[1:2, 1:2], type)
ERROR: At none:1: `@variable(model, x[1:2, 1:2], type)`: Unrecognized positional arguments: (:PSD,). (You may have passed it as a positional argument, or as a keyword value to `variable_type`.)
If you're trying to create a JuMP extension, you need to implement `build_variable`. Read the docstring for more details.
Stacktrace:
[...]Instead, pass PSDCone via the x in Set syntax:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2] in PSDCone())
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]
julia> set = PSDCone();
julia> @variable(model, y[1:2, 1:2] in set)
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
y[1,1] y[1,2]
y[1,2] y[2,2]or via the set keyword:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2], set = PSDCone())
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]
julia> set = PSDCone();
julia> @variable(model, y[1:2, 1:2], set = set)
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
y[1,1] y[1,2]
y[1,2] y[2,2]Symmetric variables
Declare a square matrix of JuMP variables to be symmetric (but not necessarily positive semidefinite) by passing Symmetric as an optional positional argument:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2], Symmetric)
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]The Symmetric argument must provided explicitly to the macro. Passing it via a variable throws an error:
julia> model = Model();
julia> type = :Symmetric
:Symmetric
julia> @variable(model, x[1:2, 1:2], type)
ERROR: At none:1: `@variable(model, x[1:2, 1:2], type)`: Unrecognized positional arguments: (:Symmetric,). (You may have passed it as a positional argument, or as a keyword value to `variable_type`.)
If you're trying to create a JuMP extension, you need to implement `build_variable`. Read the docstring for more details.
Stacktrace:
[...]Instead, pass SymmetricMatrixSpace via the x in Set syntax:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2] in SymmetricMatrixSpace())
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]
julia> set = SymmetricMatrixSpace();
julia> @variable(model, y[1:2, 1:2] in set)
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
y[1,1] y[1,2]
y[1,2] y[2,2]or via the set keyword:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2], set = SymmetricMatrixSpace())
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]
julia> set = SymmetricMatrixSpace();
julia> @variable(model, y[1:2, 1:2], set = set)
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
y[1,1] y[1,2]
y[1,2] y[2,2]The @variables macro
If you have many @variable calls, JuMP provides the macro @variables that can improve readability:
julia> model = Model();
julia> @variables(model, begin
x
y[i=1:2] >= i, (start = i, base_name = "Y_$i")
z, Bin
end)
(x, VariableRef[Y_1[1], Y_2[2]], z)
julia> print(model)
Feasibility
Subject to
Y_1[1] ≥ 1
Y_2[2] ≥ 2
z binaryThe @variables macro returns a tuple of the variables that were defined.
Variables constrained on creation
All uses of the @variable macro documented so far translate into separate calls for variable creation and the adding of any bound or integrality constraints.
For example, @variable(model, x >= 0, Int), is equivalent to:
julia> model = Model();
julia> @variable(model, x)
x
julia> set_lower_bound(x, 0.0)
julia> set_integer(x)Importantly, the bound and integrality constraints are added after the variable has been created.
However, some solvers require a set specifying the variable domain to be given when the variable is first created. We say that these variables are constrained on creation.
Use in within @variable to access the special syntax for constraining variables on creation.
For example, the following creates a vector of variables that belong to the SecondOrderCone:
julia> model = Model();
julia> @variable(model, y[1:3] in SecondOrderCone())
3-element Vector{VariableRef}:
y[1]
y[2]
y[3]For contrast, the standard syntax is as follows:
julia> @variable(model, x[1:3])
3-element Vector{VariableRef}:
x[1]
x[2]
x[3]
julia> @constraint(model, x in SecondOrderCone())
[x[1], x[2], x[3]] ∈ MathOptInterface.SecondOrderCone(3)An alternate syntax to x in Set is to use the set keyword of @variable. This is most useful when creating anonymous variables:
julia> model = Model();
julia> x = @variable(model, [1:3], set = SecondOrderCone())
3-element Vector{VariableRef}:
_[1]
_[2]
_[3]To check if a variable was constrained on creation, use is_variable_in_set, and use VariableInSetRef to obtain the associated constraint reference:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2], PSD)
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]
julia> is_variable_in_set(x)
true
julia> c = VariableInSetRef(x)
[x[1,1] x[1,2]
⋯ x[2,2]] ∈ PSDCone()
julia> @variable(model, y)
y
julia> is_variable_in_set(y)
false
julia> @variable(model, z in Semicontinuous(1, 2))
z
julia> is_variable_in_set(z)
true
julia> c_z = VariableInSetRef(z)
z ∈ MathOptInterface.Semicontinuous{Int64}(1, 2)Example: positive semidefinite variables
An alternative to the syntax in Semidefinite variables, declare a matrix of JuMP variables to be positive semidefinite using PSDCone:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2] in PSDCone())
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]
julia> @variable(model, y[1:2, 1:2], set = PSDCone())
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
y[1,1] y[1,2]
y[1,2] y[2,2]Example: symmetric variables
As an alternative to the syntax in Symmetric variables, declare a matrix of JuMP variables to be symmetric using SymmetricMatrixSpace:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2] in SymmetricMatrixSpace())
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
x[1,1] x[1,2]
x[1,2] x[2,2]
julia> @variable(model, y[1:2, 1:2], set = SymmetricMatrixSpace())
2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}:
y[1,1] y[1,2]
y[1,2] y[2,2]Example: skew-symmetric variables
Declare a matrix of JuMP variables to be skew-symmetric using SkewSymmetricMatrixSpace:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2] in SkewSymmetricMatrixSpace())
2×2 Matrix{AffExpr}:
0 x[1,2]
-x[1,2] 0
julia> @variable(model, y[1:2, 1:2], set = SkewSymmetricMatrixSpace())
2×2 Matrix{AffExpr}:
0 y[1,2]
-y[1,2] 0Even though x is a 2 by 2 matrix, only one decision variable is added to model; the remaining elements in x are linear transformations of the single variable.
Example: Hermitian positive semidefinite variables
Declare a matrix of JuMP variables to be Hermitian positive semidefinite using HermitianPSDCone:
julia> model = Model();
julia> @variable(model, H[1:2, 1:2] in HermitianPSDCone())
2×2 LinearAlgebra.Hermitian{GenericAffExpr{ComplexF64, VariableRef}, Matrix{GenericAffExpr{ComplexF64, VariableRef}}}:
real(H[1,1]) real(H[1,2]) + imag(H[1,2]) im
real(H[1,2]) - imag(H[1,2]) im real(H[2,2])This adds 4 real variables in the MOI.HermitianPositiveSemidefiniteConeTriangle:
julia> c = VariableInSetRef(H)
[real(H[1,1]) real(H[1,2]) + imag(H[1,2]) im
real(H[1,2]) - imag(H[1,2]) im real(H[2,2])] ∈ HermitianPSDCone()
julia> o = constraint_object(c);
julia> o.func
4-element Vector{VariableRef}:
real(H[1,1])
real(H[1,2])
real(H[2,2])
imag(H[1,2])
julia> o.set
MathOptInterface.HermitianPositiveSemidefiniteConeTriangle(2)Example: Hermitian variables
Declare a matrix of JuMP variables to be Hermitian using the Hermitian tag:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2], Hermitian)
2×2 LinearAlgebra.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])This is equivalent to declaring the variable in HermitianMatrixSpace:
julia> model = Model();
julia> @variable(model, x[1:2, 1:2] in HermitianMatrixSpace())
2×2 LinearAlgebra.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])Why use variables constrained on creation?
For most users, it does not matter if you use the constrained on creation syntax. Therefore, use whatever syntax you find most convenient.
However, if you use direct_model, you may be forced to use the constrained on creation syntax.
The technical difference between variables constrained on creation and the standard JuMP syntax is that variables constrained on creation calls MOI.add_constrained_variables, while the standard JuMP syntax calls MOI.add_variables and then MOI.add_constraint.
Consult the implementation of solver package you are using to see if your solver requires MOI.add_constrained_variables.
Parameters
Some solvers have explicit support for parameters, which are constants in the model that can be efficiently updated between solves.
JuMP implements parameters by a decision variable constrained on creation to a value of the Parameter set. For example, the following creates two parameters, p[1] and p[2], with parameter values 2.0 and 4.0:
julia> model = Model();
julia> @variable(model, x);
julia> @variable(model, p[i in 1:2] in Parameter(2.0 * i))
2-element Vector{VariableRef}:
p[1]
p[2]Use parameter_value and set_parameter_value to query or update the value of a parameter.
julia> parameter_value.(p)
2-element Vector{Float64}:
2.0
4.0
julia> set_parameter_value(p[2], 3.0)
julia> parameter_value.(p)
2-element Vector{Float64}:
2.0
3.0Use is_parameter and ParameterRef to check if the variable is a parameter and to get the constraint that makes the variable a parameter.
julia> is_parameter(p[1])
true
julia> is_parameter(x)
false
julia> ParameterRef(p[2])
p[2] ∈ MathOptInterface.Parameter{Float64}(3.0)Create anonymous parameters using the set keyword:
julia> anon_parameter = @variable(model, set = Parameter(1.0))
_[4]Limitations
Parameters are implemented as decision variables belonging to the Parameter set. If the solver supports the MOI.Parameter set, it may decide to replace all instances of the parameter variable by the associated constant. If the solver does not support parameters, it will add the parameter as a decision variable with fixed bounds.
The most important implication of this design is that JuMP treats a parameter multiplied by a decision variable as a quadratic expression, even though it is equivalent to a linear expression.
julia> begin
model = Model()
@variable(model, x >= 3)
@variable(model, p in Parameter(2))
@objective(model, Min, p * x)
objective_function_type(model)
end
QuadExpr (alias for GenericQuadExpr{Float64, GenericVariableRef{Float64}})As a consequence, solving a "linear" program with a solver like HiGHS fails:
julia> using HiGHS
julia> begin
model = Model(HiGHS.Optimizer)
set_silent(model)
@variable(model, x >= 3)
@variable(model, p in Parameter(2))
@objective(model, Min, p * x)
optimize!(model)
is_solved_and_feasible(model)
end
falsebecause the model is equivalent to a quadratic program with an indefinite objective:
julia> using HiGHS
julia> begin
model = Model(HiGHS.Optimizer)
set_silent(model)
@variable(model, x >= 3)
@variable(model, p == 2)
@objective(model, Min, p * x)
optimize!(model)
is_solved_and_feasible(model)
end
falseThe quadratic limitation affects only models with multiplicative parameters such as p * x. Functions that depend additively on parameters such as p + x do not have the limitation because the resulting function is still affine.
ParametricOptInterface
To avoid the problem of p * x being an indefinite quadratic, use ParametricOptInterface.jl. ParametricOptInterface provides a POI.Optimizer layer that will substitute each parameter with its numeric value prior to solving. Thus, in the following example, HiGHS will successfully solve a linear program instead of failing to solve a quadratic program.
julia> using HiGHS
julia> import ParametricOptInterface as POI
julia> begin
model = Model(() -> POI.Optimizer(HiGHS.Optimizer()))
set_silent(model)
@variable(model, x >= 3)
@variable(model, p in Parameter(2))
@objective(model, Min, p * x)
optimize!(model)
end
julia> is_solved_and_feasible(model)
true
julia> objective_value(model)
6.0If you use Parameter, then in most cases you should also use ParametricOptInterface.jl. There are two main exceptions:
- your solver natively supports the
MOI.Parameterset (for example, Ipopt.jl) - you have only additive parameters (for example,
x + p), and your solver supports some sort of presolve that can remove fixed variables.
When to use a parameter
Parameters are most useful when solving models in a sequence. For example:
julia> using JuMP, Ipoptjulia> model = Model(Ipopt.Optimizer);julia> set_silent(model)julia> @variable(model, x)xjulia> @variable(model, p in Parameter(1.0))pjulia> @objective(model, Min, (x - p)^2)x² - 2 p*x + p²julia> solution = Dict{Int,Float64}();julia> for p_value in 1:5 set_parameter_value(p, p_value) optimize!(model) assert_is_solved_and_feasible(model) solution[p_value] = value(x) endjulia> solutionDict{Int64, Float64} with 5 entries: 5 => 5.0 4 => 4.0 2 => 2.0 3 => 3.0 1 => 1.0
Using parameters can be faster than creating a new model from scratch with updated data because JuMP is able to avoid repeating a number of steps in processing the model before handing it off to the solver.