Variables

Variables

What is a JuMP variable?

The term variable in mathematical optimization has many meanings. Here, we distinguish between the following three types of variables:

  1. optimization variables, which are the mathematical $x$ in the problem $\max{f_0(x) | f_i(x) \in S_i}$.
  2. Julia variables, which are bindings between a name and a value, for example x = 1. (See here for the Julia docs.)
  3. JuMP variables, which are instances of the JuMP.VariableRef struct defined by JuMP that contains a reference to an optimization variable in a model. (Extra for experts: the VariableRef struct is a thin wrapper around a MOI.VariableIndex, and also contains a reference to the JuMP model.)

To illustrate these three types of variables, consider the following JuMP code (the full syntax is explained below):

julia> model = Model()
A JuMP Model

julia> @variable(model, x[1:2])
2-element Array{VariableRef,1}:
 x[1]
 x[2]

This code does three things:

  1. it adds two optimization variables to model
  2. it creates two JuMP variables that act as references to those optimization variables
  3. it binds those JuMP variables as a vector with two elements to the Julia variable x.

To reduce confusion, we will attempt, where possible, to always refer to variables with their corresponding prefix.

Warn

Creating two JuMP variables with the same name results in an error at runtime.

JuMP variables can have attributes, such as names or an initial primal start value. We illustrate the name attribute in the following example:

julia> @variable(model, y, basename="decision variable")
decision variable

This code does four things:

  1. it adds one optimization variable to model
  2. it creates one JuMP variable that acts as a reference to that optimization variable
  3. it binds the JuMP variable to the Julia variable y
  4. it tells JuMP that the name attribute of this JuMP variable is "decision

variable". JuMP uses the value of basename when it has to print the variable as a string.

For example, when we print y at the REPL we get:

julia> y
decision variable

Because y is a Julia variable, we can bind it to a different value. For example, if we write:

julia> y = 1
1

y is no longer a binding to a JuMP variable. This does not mean that the JuMP variable has been destroyed. It still exists and is still a reference to the same optimization variable. The binding can be reset by querying the model for the symbol as it was written in the @variable macro. For example:

julia> model[:y]
decision variable

This act of looking up the JuMP variable by using the symbol is most useful when composing JuMP models across multiple functions, as illustrated by the following example:

function add_component_to_model(model::JuMP.Model)
    x = model[:x]
    # ... code that uses x
end
function build_model()
    model = Model()
    @variable(model, x)
    add_component_to_model(model)
end
# TODO(@odow): add a section on looking up by string

Now that we understand the difference between optimization, JuMP, and Julia variables, we can introduce more of the functionality of the @variable macro.

Variable bounds

We have already seen the basic usage of the @variable macro. The next extension is to add lower- and upper-bounds to each optimization variable. This can be done as follows:

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

In the above examples, x_free represents an unbounded optimization variable, x_lower represents an optimization variable with a lower bound and so forth.

Note

When creating a variable with only a lower-bound or an upper-bound, and the value of the bound is not a numeric literal, the name must appear on the left-hand side. Putting the name on the right-hand side will result in an error. For example:

@variable(model, 1 <= x)  # works
a = 1
@variable(model, a <= x)  # errors

We can query whether an optimization variable has a lower- or upper-bound via the JuMP.has_lower_bound and JuMP.has_upper_bound functions. For example:

julia> JuMP.has_lower_bound(x_free)
false

julia> JuMP.has_upper_bound(x_upper)
true

If a variable has a lower or upper bound, we can query the value of it via the JuMP.lower_bound and JuMP.upper_bound functions. For example:

julia> JuMP.lower_bound(x_interval)
2.0

julia> JuMP.upper_bound(x_interval)
3.0

Querying the value of a bound that does not exist will result in an error.

Instead of using the <= and >= syntax, we can also use the lower_bound and upper_bound keyword arguments. For example:

julia> @variable(model, x, lower_bound=1, upper_bound=2)
x

julia> JuMP.lower_bound(x)
1.0

Another option is to use the JuMP.set_lower_bound and JuMP.set_upper_bound functions. These can also be used to modify an existing variable bound. For example:

julia> @variable(model, x >= 1)
x

julia> JuMP.lower_bound(x)
1.0

julia> JuMP.set_lower_bound(x, 2)

julia> JuMP.lower_bound(x)
2.0

Finally, we can delete variable bounds using JuMP.delete_lower_bound and JuMP.delete_upper_bound:

julia> @variable(model, 1 <= x <= 2)
x

julia> JuMP.lower_bound(x)
1.0

julia> JuMP.delete_lower_bound(x)

julia> JuMP.has_lower_bound(x)
false

julia> JuMP.upper_bound(x)
2.0

julia> JuMP.delete_upper_bound(x)

julia> JuMP.has_upper_bound(x)
false

Variable containers

In the examples above, we have mostly created scalar variables. By scalar, we mean that the Julia variable is bound to exactly one JuMP variable. However, it is often useful to create collections of JuMP variables inside more complicated datastructures.

JuMP provides a mechanism for creating three types of these datastructures, which we refer to as containers. The three types are Arrays, JuMPArrays, and Dictionaries. We explain each of these in the following.

Arrays

We have already seen the creation of an array of JuMP variables with the x[1:2] syntax. This can naturally be extended to create multi-dimensional arrays of JuMP variables. For example:

julia> @variable(model, x[1:2, 1:2])
2×2 Array{VariableRef,2}:
 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 Array{VariableRef,1}:
 x[2,1]
 x[2,2]

We can also name each index, and variable bounds can depend upon the indices:

julia> @variable(model, x[i=1:2, j=1:2] >= 2i + j)
2×2 Array{VariableRef,2}:
 x[1,1]  x[1,2]
 x[2,1]  x[2,2]

julia> JuMP.lower_bound.(x)
2×2 Array{Float64,2}:
 3.0  4.0
 5.0  6.0

JuMP 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 work, but x[a:b] will throw an error.

JuMPArrays

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 JuMPArray. For example:

julia> @variable(model, x[1:2, [:A,:B]])
2-dimensional JuMPArray{VariableRef,2,...} with index sets:
    Dimension 1, 1:2
    Dimension 2, Symbol[:A, :B]
And data, a 2×2 Array{VariableRef,2}:
 x[1,A]  x[1,B]
 x[2,A]  x[2,B]

JuMPArray's can be indexed and sliced as follows:

julia> x[1, :A]
x[1,A]

julia> x[2, :]
1-dimensional JuMPArray{VariableRef,1,...} with index sets:
    Dimension 1, Symbol[:A, :B]
And data, a 2-element Array{VariableRef,1}:
 x[2,A]
 x[2,B]

Similarly to the Array case, the indices in a JuMPArray can be named, and the bounds can depend upon these names. For example:

julia> @variable(model, x[i=2:3, j=1:2:3] >= 0.5i + j)
2-dimensional JuMPArray{VariableRef,2,...} with index sets:
    Dimension 1, 2:3
    Dimension 2, 1:2:3
And data, a 2×2 Array{VariableRef,2}:
 x[2,1]  x[2,3]
 x[3,1]  x[3,3]

julia> JuMP.lower_bound.(x)
2-dimensional JuMPArray{Float64,2,...} with index sets:
    Dimension 1, 2:3
    Dimension 2, 1:2:3
And data, a 2×2 Array{Float64,2}:
 2.0  4.0
 2.5  4.5

Dictionaries

The third datatype that JuMP supports the efficient creation of are dictionaries. These dictionaries are created when the indices do not form a rectangular set. One example is when indices have a dependence upon previous indices (called triangular indexing). JuMP supports this as follows:

julia> @variable(model, x[i=1:2, j=i:2])
Dict{Any,VariableRef} with 3 entries:
  (1, 2) => x[1,2]
  (2, 2) => x[2,2]
  (1, 1) => x[1,1]

x is a standard Julia dictionary. Therefore, slicing cannot be performed.

We can also conditionally create variables via a JuMP-specific syntax. This sytax appends a comparison check that depends upon the named indices and is separated from the indices by a semi-colon (;). For example:

julia> @variable(model, x[i=1:4; mod(i, 2)==0])
Dict{Any,VariableRef} with 2 entries:
  4 => x[4]
  2 => x[2]

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 JuMPArray, and a JuMPArray before a dictionary. However, because this happens at compile time, it does not always make the best choice. To illustrate this, consider the following example:

julia> A = 1:2
1:2

julia> @variable(model, x[A])
1-dimensional JuMPArray{VariableRef,1,...} with index sets:
    Dimension 1, 1:2
And data, a 2-element Array{VariableRef,1}:
 x[1]
 x[2]

Since the value (and type) of A is unknown at compile time, JuMP is unable to infer that A is a one-based integer range. Therefore, JuMP creates a JuMPArray, 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 Array{VariableRef,1}:
 y[1]
 y[2]

JuMP now creates a vector of JuMP variables, instead of a JuMPArray. Note that choosing an invalid container type will throw an error.

Integrality shortcuts

Adding integrality constraints to a model such as @constraint(model, x in MOI.ZeroOne()) and @constraint(model, x in MOI.Integer()) is a common operation. Therefore, JuMP supports two shortcuts for adding such constraints.

Binary (ZeroOne) constraints

Binary optimization variables are constrained to the set $x \in {0, 1}$. (The MOI.ZeroOne set in MathOptInterface.) Binary optimization variables can be created in JuMP by passing Bin as an optional positional argument:

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

We can check if an optimization variable is binary by calling JuMP.is_binary on the JuMP variable:

julia> JuMP.is_binary(x)
true

Binary optimization variables can also be created by setting the binary keyword to true.

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

Integer constraints

Integer optimization variables are constrained to the set $x \in \mathbb{Z}$. (The MOI.Integer set in MathOptInterface.) Integer optimization variables can be created in JuMP by passing Int as an optional positional argument:

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

Integer optimization variables can also be created by setting the integer keyword to true.

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

We can check if an optimization variable is integer by calling JuMP.is_integer on the JuMP variable:

julia> JuMP.is_integer(x)
true

Semidefinite variables

JuMP also supports modeling with semidefinite variables. A square symmetric matrix $X$ is positive semidefinite if all eigenvalues are nonnegative. We can declare a matrix of JuMP variables to be positive semidefinite as follows:

julia> @variable(model, x[1:2, 1:2], PSD)
2×2 LinearAlgebra.Symmetric{VariableRef,Array{VariableRef,2}}:
 x[1,1]  x[1,2]
 x[1,2]  x[2,2]

Note that x must be a square 2-dimensional Array of JuMP variables; it cannot be a JuMP array or a dictionary. (See Variable containers, above, for more on this.)

You can also impose a slightly weaker constraint that the square matrix is only symmetric (instead of positive semidefinite) as follows:

julia> @variable(model, x[1:2, 1:2], Symmetric)
2×2 LinearAlgebra.Symmetric{VariableRef,Array{VariableRef,2}}:
 x[1,1]  x[1,2]
 x[1,2]  x[2,2]

Anonymous JuMP variables

In all of the above examples, we have created named JuMP variables. However, it is also possible to create so called anonymous JuMP variables. To create an anonymous JuMP variable, we drop the name of the variable from the macro call. This means dropping the second positional argument if the JuMP variable is a scalar, or dropping the name before the square bracket ([) if a container is being created. For example:

julia> x = @variable(model)
noname

This shows how (model, x) is really short for:

julia> x = model[:x] = @variable(model, basename="x")               
x

An Array of anonymous JuMP variables can be created as follows:

julia> y = @variable(model, [i=1:2])
2-element Array{VariableRef,1}:
 noname
 noname

If necessary, you can store x in model as follows:

julia> model[:x] = x

The <= and >= short-hand cannot be used to set bounds on anonymous JuMP variables. Instead, you should use the lower_bound and upper_bound keywords.

Passing the Bin and Int variable types are also invalid. Instead, you should use the binary and integer keywords.

Thus, the anonymous variant of @variable(model, x[i=1:2] >= i, Int) is:

julia> x = @variable(model, [i=1:2], basename="x", lower_bound=i, integer=true)
2-element Array{VariableRef,1}:
 x[1]
 x[2]

User-defined containers

In the section Variable containers, we explained how JuMP supports the efficient creation of collections of JuMP variables in three types of containers. However, users are also free to create collections of JuMP variables in their own datastructures. For example, the following code creates a dictionary with symmetric matrices as the values:

julia> variables = Dict{Symbol, Array{VariableRef,2}}()
Dict{Symbol,Array{VariableRef,2}} with 0 entries

julia> for key in [:A, :B]
           global variables[key] = @variable(model, [1:2, 1:2])
       end

julia> variables
Dict{Symbol,Array{VariableRef,2}} with 2 entries:
  :A => VariableRef[noname noname; noname noname]
  :B => VariableRef[noname noname; noname noname]

Deleting variables

JuMP supports the deletion of optimization variables. To delete variables, we can use the JuMP.delete method. We can also check whether x is a valid JuMP variable in model using the JuMP.is_valid method:

julia> @variable(model, x)
x

julia> JuMP.is_valid(model, x)
true

julia> JuMP.delete(model, x)

julia> JuMP.is_valid(model, x)
false

Reference

JuMP.@variableMacro.
@variable(model, kwargs...)

Add an anonymous (see Names) variable to the model model described by the keyword arguments kwargs and returns the variable.

@variable(model, expr, args..., kwargs...)

Add a variable to the model model described by the expression expr, the positional arguments args and the keyword arguments kwargs. The expression expr can either be (note that in the following the symbol <= can be used instead of and the symbol >=can be used instead of )

  • of the form varexpr creating variables described by varexpr;
  • of the form varexpr ≤ ub (resp. varexpr ≥ lb) creating variables described by varexpr with upper bounds given by ub (resp. lower bounds given by lb);
  • of the form varexpr == value creating variables described by varexpr with fixed values given by value; or
  • of the form lb ≤ varexpr ≤ ub or ub ≥ varexpr ≥ lb creating variables described by varexpr with lower bounds given by lb and upper bounds given by ub.

The expression varexpr can either be

  • of the form varname creating a scalar real variable of name varname;
  • of the form varname[...] or [...] creating a container of variables (see Containers in macro.

The recognized positional arguments in args are the following:

  • Bin: Sets the variable to be binary, i.e. either 0 or 1.
  • Int: Sets the variable to be integer, i.e. one of ..., -2, -1, 0, 1, 2, ...
  • Symmetric: Only available when creating a square matrix of variables, i.e. when varexpr is of the form varname[1:n,1:n] or varname[i=1:n,j=1:n]. It creates a symmetric matrix of variable, that is, it only creates a new variable for varname[i,j] with i ≤ j and sets varname[j,i] to the same variable as varname[i,j].
  • PSD: The square matrix of variable is both Symmetric and constrained to be positive semidefinite.

The recognized keyword arguments in kwargs are the following:

  • basename: Sets the base name used to generate variable names. It corresponds to the variable name for scalar variable, otherwise, the variable names are basename[...] for each indices ... of the axes axes.
  • lower_bound: Sets the value of the variable lower bound.
  • upper_bound: Sets the value of the variable upper bound.
  • start: Sets the variable starting value used as initial guess in optimization.
  • binary: Sets whether the variable is binary or not.
  • integer: Sets whether the variable is integer or not.
  • variable_type: See the "Note for extending the variable macro" section below.
  • container: Specify the container type, see Containers in macro.

Examples

The following are equivalent ways of creating a variable x of name x with lower bound 0:

# Specify everything in `expr`
@variable(model, x >= 0)
# Specify the lower bound using a keyword argument
@variable(model, x, lower_bound=0)
# Specify everything in `kwargs`
x = @variable(model, basename="x", lower_bound=0)

The following are equivalent ways of creating a JuMPArray of index set [:a, :b] and with respective upper bounds 2 and 3 and names x[a] and `x[b].

ub = Dict(:a => 2, :b => 3)
# Specify everything in `expr`
@variable(model, x[i=keys(ub)] <= ub[i])
# Specify the upper bound using a keyword argument
@variable(model, x[i=keys(ub)], upper_bound=ub[i])

Note for extending the variable macro

The single scalar variable or each scalar variable of the container are created using add_variable(model, build_variable(_error, info, extra_args...; extra_kwargs...)) where

  • model is the model passed to the @variable macro;
  • _error is an error function with a single String argument showing the @variable call in addition to the error message given as argument;
  • info is the VariableInfo struct containing the information gathered in expr, the recognized keyword arguments (except basename and variable_type) and the recognized positional arguments (except Symmetric and PSD);
  • extra_args are the unrecognized positional arguments of args plus the value of the variable_type keyword argument if present. The variable_type keyword argument allows the user to pass a position argument to build_variable without the need to give a positional argument to @variable. In particular, this allows the user to give a positional argument to the build_variable call when using the anonymous single variable syntax @variable(model, kwargs...); and
  • extra_kwargs are the unrecognized keyword argument of kwargs.

Examples

The following creates a variable x of name x with lower_bound 0 as with the first example above but does it without using the @variable macro

info = VariableInfo(true, 0, false, NaN, false, NaN, false, NaN, false, false)
JuMP.add_variable(model, JuMP.build_variable(error, info), "x")

The following creates a JuMPArray of index set [:a, :b] and with respective upper bounds 2 and 3 and names x[a] and x[b] as with the second example above but does it without using the @variable macro

# Without the `@variable` macro
data = Vector{JuMP.variable_type(model)}(undef, length(keys(ub)))
x = JuMPArray(data, keys(ub))
for i in keys(ub)
    info = VariableInfo(false, NaN, true, ub[i], false, NaN, false, NaN, false, false)
    x[i] = JuMP.add_variable(model, JuMP.build_variable(error, info), "x[$i]")
end

The following are equivalent ways of creating a Matrix of size N x N with variables custom variables created with a JuMP extension using the Poly(X) positional argument to specify its variables:

# Using the `@variable` macro
@variable(model, x[1:N,1:N], Symmetric, Poly(X))
# Without the `@variable` macro
x = Matrix{JuMP.variable_type(model, Poly(X))}(N, N)
info = VariableInfo(false, NaN, false, NaN, false, NaN, false, NaN, false, false)
for i in 1:N, j in i:N
    x[i,j] = x[j,i] = JuMP.add_variable(model, build_variable(error, info, Poly(X)), "x[$i,$j]")
end
source