Models
JuMP models are the fundamental building block that we use to construct optimization problems. They hold things like the variables and constraints, as well as which solver to use and even solution information.
JuMP uses "optimizer" as a synonym for "solver." Our convention is to use "solver" to refer to the underlying software, and use "optimizer" to refer to the Julia object that wraps the solver. For example, HiGHS
is a solver, and HiGHS.Optimizer
is an optimizer.
See Supported solvers for a list of available solvers.
Create a model
Create a model by passing an optimizer to Model
:
julia> model = Model(HiGHS.Optimizer)
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
If you don't know which optimizer you will be using at creation time, create a model without an optimizer, and then call set_optimizer
at any time prior to optimize!
:
julia> model = Model()
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> set_optimizer(model, HiGHS.Optimizer)
Don't know what the fields Model mode
and CachingOptimizer state
mean? Read the Backends section.
What is the difference?
For most models, there is no difference between passing the optimizer to Model
, and calling set_optimizer
.
However, if an optimizer does not support a constraint in the model, the timing of when an error will be thrown can differ:
- If you pass an optimizer, an error will be thrown when you try to add the constraint.
- If you call
set_optimizer
, an error will be thrown when you try to solve the model viaoptimize!
.
Therefore, most users should pass an optimizer to Model
because it provides the earliest warning that your solver is not suitable for the model you are trying to build. However, if you are modifying a problem by adding and deleting different constraint types, you may need to use set_optimizer
. See Switching optimizer for the relaxed problem for an example of when this is useful.
Reducing time-to-first-solve latency
By default, JuMP uses bridges to reformulate the model you are building into an equivalent model supported by the solver.
However, if your model is already supported by the solver, bridges add latency (read The "time-to-first-solve" issue). This is particularly noticeable for small models.
To reduce the "time-to-first-solve,s" try passing add_bridges = false
.
julia> model = Model(HiGHS.Optimizer; add_bridges = false);
or
julia> model = Model();
julia> set_optimizer(model, HiGHS.Optimizer; add_bridges = false)
However, be wary. If your model and solver combination needs bridges, an error will be thrown:
julia> model = Model(SCS.Optimizer; add_bridges = false);
julia> @variable(model, x)
x
julia> @constraint(model, 2x <= 1)
ERROR: Constraints of type MathOptInterface.ScalarAffineFunction{Float64}-in-MathOptInterface.LessThan{Float64} are not supported by the solver.
If you expected the solver to support your problem, you may have an error in your formulation. Otherwise, consider using a different solver.
The list of available solvers, along with the problem types they support, is available at https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers.
[...]
Solvers which expect environments
Some solvers accept (or require) positional arguments such as a license environment or a path to a binary executable. For these solvers, you can pass a function to Model
which takes zero arguments and returns an instance of the optimizer.
A common use-case for this is passing an environment or sub-solver to the optimizer:
julia> import HiGHS
julia> import MultiObjectiveAlgorithms as MOA
julia> model = Model(() -> MOA.Optimizer(HiGHS.Optimizer))
A JuMP Model
├ solver: MOA[algorithm=MultiObjectiveAlgorithms.Lexicographic, optimizer=HiGHS]
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
Solver options
JuMP uses "attribute" as a synonym for "option." Use optimizer_with_attributes
to create an optimizer with some attributes initialized:
julia> model = Model(
optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false),
)
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
Alternatively, use set_attribute
to set an attribute after the model has been created:
julia> model = Model(HiGHS.Optimizer);
julia> set_attribute(model, "output_flag", false)
julia> get_attribute(model, "output_flag")
false
You can also modify attributes within an optimizer_with_attributes
object:
julia> solver = optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => true);
julia> get_attribute(solver, "output_flag")
true
julia> set_attribute(solver, "output_flag", false)
julia> get_attribute(solver, "output_flag")
false
julia> model = Model(solver);
Changing the number types
By default, the coefficients of affine and quadratic expressions are numbers of type either Float64
or Complex{Float64}
(see Complex number support).
The type Float64
can be changed using the GenericModel
constructor:
julia> model = GenericModel{Rational{BigInt}}();
julia> @variable(model, x)
x
julia> @expression(model, expr, 1 // 3 * x)
1//3 x
julia> typeof(expr)
GenericAffExpr{Rational{BigInt}, GenericVariableRef{Rational{BigInt}}}
Using a value_type
other than Float64
is an advanced operation and should be used only if the underlying solver actually solves the problem using the provided value type.
Nonlinear Modeling is currently restricted to the Float64
number type.
Print the model
By default, show(model)
will print a summary of the problem:
julia> model = Model(); @variable(model, x >= 0); @objective(model, Max, x);
julia> model
A JuMP Model
├ solver: none
├ objective_sense: MAX_SENSE
│ └ objective_function_type: VariableRef
├ num_variables: 1
├ num_constraints: 1
│ └ VariableRef in MOI.GreaterThan{Float64}: 1
└ Names registered in the model
└ :x
Use print
to print the formulation of the model (in IJulia, this will render as LaTeX.
julia> print(model)
Max x
Subject to
x ≥ 0
This format is specific to JuMP and may change in any future release. It is not intended to be an instance format. To write the model to a file, use write_to_file
instead.
Use latex_formulation
to display the model in LaTeX form.
julia> latex_formulation(model)
$$ \begin{aligned}
\max\quad & x\\
\text{Subject to} \quad & x \geq 0\\
\end{aligned} $$
In IJulia (and Documenter), ending a cell in with latex_formulation
will render the model in LaTeX:
latex_formulation(model)
\[ \begin{aligned} \max\quad & x\\ \text{Subject to} \quad & x \geq 0\\ \end{aligned} \]
Turn off output
Use set_silent
and unset_silent
to disable or enable printing output from the solver.
julia> model = Model(HiGHS.Optimizer);
julia> set_silent(model)
julia> unset_silent(model)
Most solvers will also have a solver-specific option to provide finer-grained control over the output. Consult their README's for details.
Set a time limit
Use set_time_limit_sec
, unset_time_limit_sec
, and time_limit_sec
to manage time limits.
julia> model = Model(HiGHS.Optimizer);
julia> set_time_limit_sec(model, 60.0)
julia> time_limit_sec(model)
60.0
julia> unset_time_limit_sec(model)
julia> limit = time_limit_sec(model)
julia> limit === nothing
true
If your time limit is encoded as a Dates.Period
object, use the following code to convert it to Float64
for set_time_limit_sec
:
julia> import Dates
julia> seconds(x::Dates.Period) = 1e-3 * Dates.value(round(x, Dates.Millisecond))
seconds (generic function with 1 method)
julia> set_time_limit_sec(model, seconds(Dates.Hour(1)))
julia> time_limit_sec(model)
3600.0
Some solvers do not support time limits. In these cases, an error will be thrown.
Write a model to file
JuMP can write models to a variety of file-formats using write_to_file
and Base.write
.
For most common file formats, the file type will be detected from the extension.
For example, here is how to write an MPS file:
julia> model = Model();
julia> write_to_file(model, "model.mps")
Other supported file formats include:
.cbf
for the Conic Benchmark Format.lp
for the LP file format.mof.json
for the MathOptFormat.nl
for AMPL's NL file format.rew
for the REW file format.sdpa
and ".dat-s" for the SDPA file format
To write to a specific io::IO
, use Base.write
. Specify the file type by passing a MOI.FileFormats.FileFormat
enum.
julia> model = Model();
julia> io = IOBuffer();
julia> write(io, model; format = MOI.FileFormats.FORMAT_MPS)
Read a model from file
JuMP models can be created from file formats using read_from_file
and Base.read
.
julia> model = read_from_file("model.mps")
A JuMP Model
├ solver: none
├ objective_sense: MIN_SENSE
│ └ objective_function_type: AffExpr
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> seekstart(io);
julia> model2 = read(io, Model; format = MOI.FileFormats.FORMAT_MPS)
A JuMP Model
├ solver: none
├ objective_sense: MIN_SENSE
│ └ objective_function_type: AffExpr
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
Because file formats do not serialize the containers of JuMP variables and constraints, the names in the model will not be registered. Therefore, you cannot access named variables and constraints via model[:x]
. Instead, use variable_by_name
or constraint_by_name
to access specific variables or constraints.
Relax integrality
Use relax_integrality
to remove any integrality constraints from the model, such as integer and binary restrictions on variables. relax_integrality
returns a function that can be later called with zero arguments to re-add the removed constraints:
julia> model = Model();
julia> @variable(model, x, Int)
x
julia> num_constraints(model, VariableRef, MOI.Integer)
1
julia> undo = relax_integrality(model);
julia> num_constraints(model, VariableRef, MOI.Integer)
0
julia> undo()
julia> num_constraints(model, VariableRef, MOI.Integer)
1
Switching optimizer for the relaxed problem
A common reason for relaxing integrality is to compute dual variables of the relaxed problem. However, some mixed-integer linear solvers (for example, Cbc) do not return dual solutions, even if the problem does not have integrality restrictions.
Therefore, after relax_integrality
you should call set_optimizer
with a solver that does support dual solutions, such as Clp.
For example, instead of:
using JuMP, Cbc
model = Model(Cbc.Optimizer)
@variable(model, x, Int)
undo = relax_integrality(model)
optimize!(model)
reduced_cost(x) # Errors
do:
using JuMP, Cbc, Clp
model = Model(Cbc.Optimizer)
@variable(model, x, Int)
undo = relax_integrality(model)
set_optimizer(model, Clp.Optimizer)
optimize!(model)
reduced_cost(x) # Works
Get the matrix representation
Use lp_matrix_data
to return a data structure that represents the matrix form of a linear program.
julia> begin
model = Model()
@variable(model, x >= 1, Bin)
@variable(model, 2 <= y)
@variable(model, 3 <= z <= 4, Int)
@constraint(model, x == 5)
@constraint(model, 2x + 3y <= 6)
@constraint(model, -4y >= 5z + 7)
@constraint(model, -1 <= x + y <= 2)
@objective(model, Max, 1 + 2x)
end;
julia> data = lp_matrix_data(model);
julia> data.A
4×3 SparseArrays.SparseMatrixCSC{Float64, Int64} with 7 stored entries:
1.0 ⋅ ⋅
⋅ -4.0 -5.0
2.0 3.0 ⋅
1.0 1.0 ⋅
julia> data.b_lower
4-element Vector{Float64}:
5.0
7.0
-Inf
-1.0
julia> data.b_upper
4-element Vector{Float64}:
5.0
Inf
6.0
2.0
julia> data.x_lower
3-element Vector{Float64}:
1.0
2.0
3.0
julia> data.x_upper
3-element Vector{Float64}:
Inf
Inf
4.0
julia> data.c
3-element Vector{Float64}:
2.0
0.0
0.0
julia> data.c_offset
1.0
julia> data.sense
MAX_SENSE::OptimizationSense = 1
julia> data.integers
1-element Vector{Int64}:
3
julia> data.binaries
1-element Vector{Int64}:
1
lp_matrix_data
is intentionally limited in the types of problems that it supports and the structure of the matrices it outputs. It is mainly intended as a pedagogical and debugging tool. It should not be used to interface solvers, see Implementing a solver interface instead.
Backends
This section discusses advanced features of JuMP. For new users, you may want to skip this section. You don't need to know how JuMP manages problems behind the scenes to create and solve JuMP models.
A JuMP Model
is a thin layer around a backend of type MOI.ModelLike
that stores the optimization problem and acts as the optimization solver.
However, if you construct a model like Model(HiGHS.Optimizer)
, the backend is not a HiGHS.Optimizer
, but a more complicated object.
From JuMP, the MOI backend can be accessed using the backend
function. Let's see what the backend
of a JuMP Model
is:
julia> model = Model(HiGHS.Optimizer);
julia> b = backend(model)
MOIU.CachingOptimizer
├ state: EMPTY_OPTIMIZER
├ mode: AUTOMATIC
├ model_cache: MOIU.UniversalFallback{MOIU.Model{Float64}}
│ ├ ObjectiveSense: FEASIBILITY_SENSE
│ ├ ObjectiveFunctionType: MOI.ScalarAffineFunction{Float64}
│ ├ NumberOfVariables: 0
│ └ NumberOfConstraints: 0
└ optimizer: MOIB.LazyBridgeOptimizer{HiGHS.Optimizer}
├ Variable bridges: none
├ Constraint bridges: none
├ Objective bridges: none
└ model: A HiGHS model with 0 columns and 0 rows.
Uh oh. Even though we passed a HiGHS.Optimizer
, the backend is a much more complicated object.
CachingOptimizer
A MOIU.CachingOptimizer
is a layer that abstracts the difference between solvers that support incremental modification (for example, they support adding variables one-by-one), and solvers that require the entire problem in a single API call (for example, they only accept the A
, b
and c
matrices of a linear program).
It has two parts:
- A cache, where the model can be built and modified incrementally
julia> b.model_cache MOIU.UniversalFallback{MOIU.Model{Float64}} ├ ObjectiveSense: FEASIBILITY_SENSE ├ ObjectiveFunctionType: MOI.ScalarAffineFunction{Float64} ├ NumberOfVariables: 0 └ NumberOfConstraints: 0
- An optimizer, which is used to solve the problem
julia> b.optimizer MOIB.LazyBridgeOptimizer{HiGHS.Optimizer} ├ Variable bridges: none ├ Constraint bridges: none ├ Objective bridges: none └ model: A HiGHS model with 0 columns and 0 rows.
The LazyBridgeOptimizer section explains what a LazyBridgeOptimizer
is.
The CachingOptimizer
has logic to decide when to copy the problem from the cache to the optimizer, and when it can efficiently update the optimizer in-place.
A CachingOptimizer
may be in one of three possible states:
NO_OPTIMIZER
: The CachingOptimizer does not have any optimizer.EMPTY_OPTIMIZER
: The CachingOptimizer has an empty optimizer, and it is not synchronized with the cached model.ATTACHED_OPTIMIZER
: The CachingOptimizer has an optimizer, and it is synchronized with the cached model.
A CachingOptimizer
has two modes of operation:
AUTOMATIC
: TheCachingOptimizer
changes its state when necessary. For example,optimize!
will automatically callattach_optimizer
(an optimizer must have been previously set). Attempting to add a constraint or perform a modification not supported by the optimizer results in a drop toEMPTY_OPTIMIZER
mode.MANUAL
: The user must change the state of theCachingOptimizer
usingMOIU.reset_optimizer(::JuMP.Model)
,MOIU.drop_optimizer(::JuMP.Model)
, andMOIU.attach_optimizer(::JuMP.Model)
. Attempting to perform an operation in the incorrect state results in an error.
By default Model
will create a CachingOptimizer
in AUTOMATIC
mode.
LazyBridgeOptimizer
The second layer that JuMP applies automatically is a MOI.Bridges.LazyBridgeOptimizer
. A MOI.Bridges.LazyBridgeOptimizer
is an MOI layer that attempts to transform the problem from the formulation provided by the user into an equivalent problem supported by the solver. This may involve adding new variables and constraints to the optimizer. The transformations are selected from a set of known recipes called bridges.
A common example of a bridge is one that splits an interval constraint like @constraint(model, 1 <= x + y <= 2)
into two constraints, @constraint(model, x + y >= 1)
and @constraint(model, x + y <= 2)
.
Use the add_bridges = false
keyword to remove the bridging layer:
julia> model = Model(HiGHS.Optimizer; add_bridges = false)
A JuMP Model
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> backend(model)
MOIU.CachingOptimizer
├ state: EMPTY_OPTIMIZER
├ mode: AUTOMATIC
├ model_cache: MOIU.UniversalFallback{MOIU.Model{Float64}}
│ ├ ObjectiveSense: FEASIBILITY_SENSE
│ ├ ObjectiveFunctionType: MOI.ScalarAffineFunction{Float64}
│ ├ NumberOfVariables: 0
│ └ NumberOfConstraints: 0
└ optimizer: A HiGHS model with 0 columns and 0 rows.
Bridges can be added and removed from a MOI.Bridges.LazyBridgeOptimizer
using add_bridge
and remove_bridge
. Use print_active_bridges
to see which bridges are used to reformulate the model. Read the Example: ellipsoid approximation tutorial for more details.
Unsafe backend
In some advanced use-cases, it is necessary to work with the inner optimization model directly. To access this model, use unsafe_backend
:
julia> backend(model)
MOIU.CachingOptimizer
├ state: EMPTY_OPTIMIZER
├ mode: AUTOMATIC
├ model_cache: MOIU.UniversalFallback{MOIU.Model{Float64}}
│ ├ ObjectiveSense: FEASIBILITY_SENSE
│ ├ ObjectiveFunctionType: MOI.ScalarAffineFunction{Float64}
│ ├ NumberOfVariables: 0
│ └ NumberOfConstraints: 0
└ optimizer: MOIB.LazyBridgeOptimizer{HiGHS.Optimizer}
├ Variable bridges: none
├ Constraint bridges: none
├ Objective bridges: none
└ model: A HiGHS model with 0 columns and 0 rows.
julia> unsafe_backend(model)
A HiGHS model with 0 columns and 0 rows.
backend
and unsafe_backend
are advanced routines. Read their docstrings to understand the caveats of their usage, and only call them if you wish to access low-level solver-specific functions.
Direct mode
Using a CachingOptimizer
results in an additional copy of the model being stored by JuMP in the .model_cache
field. To avoid this overhead, create a JuMP model using direct_model
:
julia> model = direct_model(HiGHS.Optimizer())
A JuMP Model
├ mode: DIRECT
├ solver: HiGHS
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
Solvers that do not support incremental modification do not support direct_model
. An error will be thrown, telling you to use a CachingOptimizer
instead.
The benefit of using direct_model
is that there are no extra layers (for example, Cachingoptimizer
or LazyBridgeOptimizer
) between model
and the provided optimizer:
julia> backend(model)
A HiGHS model with 0 columns and 0 rows.
A downside of direct mode is that there is no bridging layer. Therefore, only constraints which are natively supported by the solver are supported. For example, HiGHS.jl
does not implement quadratic constraints:
julia> model = direct_model(HiGHS.Optimizer());
julia> set_silent(model)
julia> @variable(model, x[1:2]);
julia> @constraint(model, x[1]^2 + x[2]^2 <= 2)
ERROR: Constraints of type MathOptInterface.ScalarQuadraticFunction{Float64}-in-MathOptInterface.LessThan{Float64} are not supported by the solver.
If you expected the solver to support your problem, you may have an error in your formulation. Otherwise, consider using a different solver.
The list of available solvers, along with the problem types they support, is available at https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers.
Stacktrace:
Another downside of direct mode is that the behavior of querying solution information after modifying the problem is solver-specific. This can lead to errors, or the solver silently returning an incorrect value. See OptimizeNotCalled errors for more information.