JuMPeR is a modeling language for robust optimization (RO). It is embedded in the Julia programming language, and is an extension to the JuMP modeling language. JuMPeR was created by Iain Dunning. Some of the goals of JuMPeR are
Here is a small example of a robust 0/1 knapsack problem:
$$
\begin{alignat}{2}
\max \ & \sum_{i=1}^{n} p_i x_i \\
\text{subject to} \ & \sum_{i=1}^{n} \left( w_i + \hat{w}_i z_i \right) x_i \leq C \forall \mathbf{z} \in U \\
& \mathbf{x} \in \left\{0,1\right\}^n \\
\text{where} \ & U = \left\{ \mathbf{z} \in \left[0,1\right]^n, \sum_{i=1}^n z_i \leq \Gamma \right\}
\end{alignat}
$$
which we can write in JuMPeR as
using JuMP, JuMPeR
n, C, Γ = 10, 3, 4
p, w = rand(n), rand(n)
σ = 0.2 * rand(n) .* w
knap = RobustModel()
@defVar(knap, x[1:n], Bin)
@defUnc(knap, 0 <= z[1:n] <= 1)
@setObjective(knap, Max, sum{p[i]*x[i], i=1:n})
@addConstraint(knap, sum{(w[i]+σ[i]*z[i])*x[i], i=1:n} <= C)
@addConstraint(knap, sum{z[i], i=1:n} <= Γ)
solve(knap)
As JuMPeR builds on JuMP, you should be comfortable with the basics of modeling with JuMP before learning JuMPeR.
JuMPeR is a registered Julia package, so you can simply install it with
Pkg.add("JuMPeR")
Note that both JuMP and JuMPeR do not come with a solver - you'll need to install one. You'll need to be a bit careful about making sure an appropriate solver is installed, for example:
In this section we'll dive straight into modeling RO problems with the default oracle, and defer discussion of exactly what an oracle is (in the JuMPeR context) until later. You can think of them as a black box that JuMPeR asks how to solve a problem. They can reformulate constraints with uncertain parameters, and approximate them with cutting planes. The default oracle, called GeneralOracle
, takes polyhedral and ellipsoidal constraints on the uncertain parameters and will reformulate them using duality or generate cutting planes by solving LPs/SOCPs.
It all starts with the RobustModel
, which is essentially the same as a JuMP Model
. Like a Model
, it also accepts a solver
keyword argument. It can also take an additional cutsolver
keyword argument, which the oracle can use for cutting planes.
# Use default solver for the problem class
rm = RobustModel()
# Can also specify the solver
using Gurobi
rm = RobustModel(solver=GurobiSolver())
# That'd produce a lot of output when using
# cutting planes, so we can set a solver just for cuts
rm = RobustModel( solver=GurobiSolver(MIPGap=0.01),
cutsolver=GurobiSolver(OutputFlag=0))
JuMPeR adds the Uncertain
type to allow the user to model uncertain parameters. Uncertain
s behave much like JuMP's Variable
s - they have bounds and types, and can be combined with numbers, data, and inequalities to make expressions and constraints. They are defined with @defUnc
:
rm = RobustModel()
# Can define single uncertain parameterss
@defUnc(rm, u)
# Can define indexed uncertain parameters too
@defUnc(rm, v[4:10,-2:2])
# They can have bounds too
@defUnc(rm, 0 <= w[i=1:10] <= i^2)
# You can also have types: integer (Int) and binary (Bin)
# Support for these will depend on the oracle. With the default
# oracle, they will only work with cutting plane mode, as
# reformulation is not possible in general for these types.
idxset = [:Apple, :Banana, :Orange]
@defUnc(rm, fruits[idxset], Bin)
There are some minor restrictions on Uncertain
to be aware of:
u*u
is not allowed.
$$
\begin{alignat}{2}
\max_{\mathbf{x} \in X} \min_{\mathbf{c} \in U} & \quad \mathbf{c} \cdot \mathbf{x}
\end{alignat}
$$
rm = RobustModel()
@defVar(rm, x[1:n])
@defUnc(rm, c[1:n])
# Create auxiliary for the objective
@defVar(rm, t)
# Epigraph form
@setObjective(rm, Max, t)
@addConstraint(rm, t <= sum{c[i]*x[i],i=1:n})
# ... constraints x in X ...
# ... constraints c in U ...
solve(rm)
In JuMP we have only data and variables, with one type of expression and one type constraint (mostly). In JuMPeR we have three types of expression, each with are corresponding type of constraint:
AffExpr
)UAffExpr
)FullAffExpr
)Type 1 is just like in JuMP - these a deterministic constraints, and are JuMPeR doesn't do anything with them. JuMPeR treats Type 2 constraints as uncertainty set constraints - that is, they partially define the uncertainty set for this problem. They can be used by the oracles to perform reformulations or generate cutting planes. Type 3 constraints are uncertain constraints that must be feasible for all realizations of the uncertain parameters - these are what the oracles are dealing with to solve the problem. Here is an example of each, from the knapsack problem defined above:
w = rand(n)
σ = 0.2 * rand(n) .* w
knap = RobustModel()
@defVar(knap, x[1:n], Bin)
@defUnc(knap, 0 <= z[1:n] <= 1)
# Type 1 constraint: only on the variables
# "must take at least one item"
@addConstraint(knap, sum{x[i], i=1:n} >= 1)
# Type 2 constraint: only on the uncertain parameters
# "only Γ parameters can be at their upper bound"
@addConstraint(knap, sum{z[i], i=1:n} <= Γ)
# Type 3 constraint: both variables and uncertain parameters
# "the total uncertain weight of items is less than capacity"
@addConstraint(knap, sum{(w[i]+σ[i]*z[i])*x[i], i=1:n} <= C)
JuMP doesn't provide a convenient syntax to express "ellipsoidal constraints", i.e.
$$
\begin{alignat}{2}
\| \mathbf{x} \|_2 & \leq \Gamma
\end{alignat}
$$
but ellipsoidal uncertainty sets are popular. We address this by providing addEllipseConstraint
. This function takes 3 arguments: a RobustModel
, a vector of Uncertain
s or affine expressions of them, and a constant right-hand-side that the norm of this vector should be less than. For example:
# A trivial ellipse: 5 <= v <= 7
@defUnc(m, v)
addEllipseConstraint(m, [v], 2)
# An sphere centered at (1,2,3,4,5) with a radius
# of 1 in each dimension
@defUnc(m, u[1:5])
addEllipseConstraint(m, [(u-i) for i in 1:5], 1)
# A more complicated ellipse
@defUnc(m, w[1:5])
addEllipseConstraint(m, [3.0*w[1]-5, 1.0*w[5]-5, 2.0*w[4]-5], 2)
By default, JuMPeR will use duality to reformulate the uncertain problem into a deterministic robust counterpart. However there are a couple of options to customize the solve, not to mention any options you provide to the oracle (more on that later).
prefer_cuts
option.add_box
.report
option.solve(rm) # Will use default, which is reformulation
solve(rm, prefer_cuts=true) # Use cuts if possible
solve(rm, report=true) # Get some information about the
# solution process at the end
solve(rm, add_box=1e3) # Provide an upper and lower bound of
# 1e3 (or whatever) for all variables
# that don't otherwise have them.
Note that the solution methods available depends on the chosen oracle's capabilities. The default oracle supports both, but some uncertainty sets may only support one or the other.
JuMPeR's design is focussed around oracles. Oracles are responsible for taking RO problems, or parts of them, and transforming them into something solveable. That could be reformulating uncertain constraints into deterministic constraints, it could be generating cutting-planes, or something else entirely. Oracles are also intimately connected to uncertainty sets - for example, we can provide data to an oracle from which it can generate cutting-planes - an uncertainty set never needs to explictly constructed. Finally oracles are interchangeable - you can obtain oracles from others or create your own to allow others to explore the performance of different sets and implementations.
In this section we will describe how to use oracles other than the default oracle, how to make an oracle, and some design considerations for oracles.
JuMPeR currently comes with three oracles:
GeneralOracle
, the default oracle. It takes an explicit polyhedral/ellipsoidal representation of the uncertainty set and can generate a reformulation or use cutting-planes.GeneralGraphOracle
, a variation on GeneralOracle
. This oracle attempts to discover if the uncertain parameters actually belong to seperate, disjunct uncertainty sets. This allows it to generate smaller reformulations, and a seperator cut generator for each set if using cutting-planes.BertSimOracle
, implements the uncertainty set described in the 2004 paper The Price of Robustness by Bertsimas and Sim. Will generate cutting-planes efficiently using sorting instead of solving an LP. The GeneralOracle
should be used if a reformulation is desired.To set the oracle for all constraints, use the setDefaultOracle!
command, e.g.
n = 3
weight_low = [1.0, 3.0, 2.0]
weight_high = [3.0, 9.0, 6.0]
values = [5.0, 6.0, 4.0]
# Create the model...
m = RobustModel()
# ... and set all constraints to use the Bertsimas-Sim set
setDefaultOracle!(m, BertSimOracle(1))
# Setup our problem
@defVar(m, x[1:n], Bin)
@defUnc(m, weight_low[i] <= u[i=1:n] <= weight_high[i])
@setObjective(m, Max, sum{values[i] * x[i], i=1:n})
# Notice we haven't provided an explicit uncertainty set,
# apart from the ranges on the uncertain values?
# The BertSimOracle looks at the range, and treats the
# nominal value for each uncertain parameter as the
# midpoint of the range. The parameter we passed is the
# number of uncertain parameters that can deviate from
# this nominal value. In other words:
# |u - 4|/1 + |u - 6|/3 + |u - 4|/2 <= 1
# |u - 4|/1 <= 1
# |u - 6|/3 <= 1
# |u - 4|/2 <= 1
@addConstraint(m, sum{u[i]*x[i], i=1:n} <= 8)
solve(m, prefer_cuts=true)
To make an oracle, we first need to understand what happens when you call solve
on a RobustModel
:
Model
, referred to as the master, is created with the same variables as the original RobustModel
and with all the deterministic constraints.registerConstraint
is called for the oracle associated with that constraint. Any constraints that don't have a oracle explicitly provided use the default.setup
is called for each oracle, giving them time to do any general setup shared across constraints. For example, it may take the dual of the uncertainty set in order to more efficiently reformulate multiple constraints.generateReform
). It will return the number of constraints reformulated (which may be zero).We can now specific how to make an oracle. An oracle should be a Julia type that is a subtype of AbstractOracle
, i.e.
type MyNewOracle <: JuMPeR.AbstractOracle
some_internal_state
end
The oracle must implement four methods, regardless of its functionality (defined in src/oracle.jl
)
# registerConstraint
# Notifies the oracle that it is responsible for this constraint, and
# passes any preferences provided via the solveRobust() command.
function registerConstraint(ab::AbstractOracle, rm::Model, ind::Int, prefs)
# setup
# Gives oracle time to do any setup it needs to do. Called after all
# constraints have been registered. Examples of work that could be done here
# include transforming the uncertainty set and generating a cutting plane
# model. Will NOT be called multiple times.
function setup(ab::AbstractOracle, rm::Model, prefs)
# generateReform
# Called before the main loop, adds anything it wants to the model. Returns
# number of constraints reformulated. If reformulation not supported or
# desired, simply return 0
function generateReform(ab::AbstractOracle, master::Model, rm::Model, inds::Vector{Int})
# generateCut
# Called in the main loop every iteration/every time an integer solution is
# found. Returns a vector of constraints which are added to the problem by
# the main solve loop. Return an empty list if there are no constraints to add.
# The optional "active" argument will be called if the user wants to know
# the active scenarios at optimality. This is still experimental!
generateCut(ab::AbstractOracle, master::Model, rm::Model, inds::Vector{Int}, active=false)
Stay tuned, watch this space, etc.