Skip to content

Commit

Permalink
Merge b4b4f17 into f4130f9
Browse files Browse the repository at this point in the history
  • Loading branch information
blegat committed Aug 2, 2018
2 parents f4130f9 + b4b4f17 commit 9b6d000
Show file tree
Hide file tree
Showing 6 changed files with 351 additions and 143 deletions.
88 changes: 86 additions & 2 deletions docs/src/solvers.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,91 @@
Interacting with solvers
========================

TODO: Describe the connection between JuMP and solvers. Automatic vs. Manual
mode. CachingOptimizer. How to set/change solvers. How to set parameters (solver
A JuMP model keeps a [MathOptInterface (MOI)](https://github.com/JuliaOpt/MathOptInterface.jl)
*backend* of type `MOI.ModelLike` internally that stores the optimization
problem and acts as the optimization solver. We call it an MOI *backend* and
not optimizer as it can also be a wrapper around an optimization file format
such as MPS that writes the JuMP model in a file. JuMP can be viewed as a
lightweight user-friendly layer on top of the MOI backend:

* JuMP does not maintain any copy of the model outside this MOI backend.
* JuMP variable (resp. constraint) references are simple structures containing
both a reference to the JuMP model and the MOI index of the variable (resp.
constraint).
* JuMP gives the constraints to the MOI backend in the form provided by the user
without doing any automatic reformulation.
* variables additions, constraints additions/modifications and objective
modifications are directly applied to the MOI backend thus expecting the
backend to support such modifications.

While this allows JuMP to be a thin wrapper on top of the solver API, as
mentioned in the last point above, this seems rather demanding on the solver.
Indeed, while some solvers support incremental building of the model and
modifications before and after solve, other solvers only support the model being
copied at once before solve. Moreover it seems to require all solvers to
implement all possible reformulations independently which seems both very
ambitious and might generate a lot of duplicated code.

These apparent limitations are in fact addressed at the MOI level in a manner
that is completely transparent to JuMP. While the MOI API may seem very
demanding, it allows MOI models to be a succession of lightweight MOI layers
that fill the gap between JuMP requirements and the solver capabilities.

JuMP models can be created in three different modes: Automatic, Manual and
Direct.

## Automatic and Manual modes

In Automatic and Manual modes, two MOI layers are automatically applied to the
optimizer:

* `CachingOptimizer`: maintains a cache of the model so that when the optimizer
does not support an incremental change to the model, the optimizer's internal
model can be discarded and restored from the cache just before optimization.
The `CachingOptimizer` has two different modes: Automatic and Manual
corresponding to the two JuMP modes with the same names.
* `LazyBridgeOptimizer` (this can be disabled using the `bridge_constraints`
keyword argument to [`Model`](@ref) constructor): when a constraint added is
not supported by the optimizer, it tries transform the constraint into an
equivalent form, possibly adding new variables and constraints that are
supported by the optimizer. The applied transformations are selected among
known recipes which are called bridges. A few default bridges are defined in
MOI but new ones can be defined and added to the `LazyBridgeOptimizer` used by
JuMP.

See the [MOI documentation](http://www.juliaopt.org/MathOptInterface.jl/stable/)
for more details on these two MOI layers.

To attach an optimizer to a JuMP model, JuMP needs to create a new empty
optimizer instance. New optimizer instances can be obtained using an
`OptimizerFactory` that can be created using the [`with_optimizer`](@ref)
function:
```@docs
with_optimizer
```

The factory can be provided either at model construction time or at
[`JuMP.optimize`](@ref) time:
```@docs
JuMP.optimize
```

New JuMP models are created using the [`Model`](@ref) constructor:
```@docs
Model()
Model(::JuMP.OptimizerFactory)
```

TODO: how to control the caching optimizer states

## Direct mode

JuMP models can be created in Direct mode using the [`JuMP.direct_model`](@ref)
function.
```@docs
JuMP.direct_model
```

TODO: How to set parameters (solver
specific and generic). Status codes. Accessing the result.
How to accurately measure the solve time.
186 changes: 144 additions & 42 deletions src/JuMP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ using .Derivatives
export
# Objects
Model, VariableRef, Norm, AffExpr, QuadExpr,
with_optimizer,
# LinearConstraint, QuadConstraint, SDConstraint,
NonlinearConstraint,
ConstraintRef,
Expand Down Expand Up @@ -77,6 +78,56 @@ const MOIBIN = MOICON{MOI.SingleVariable,MOI.ZeroOne}

@MOIU.model JuMPMOIModel (ZeroOne, Integer) (EqualTo, GreaterThan, LessThan, Interval) (Zeros, Nonnegatives, Nonpositives, SecondOrderCone, RotatedSecondOrderCone, GeometricMeanCone, PositiveSemidefiniteConeTriangle, PositiveSemidefiniteConeSquare, RootDetConeTriangle, RootDetConeSquare, LogDetConeTriangle, LogDetConeSquare) () (SingleVariable,) (ScalarAffineFunction,ScalarQuadraticFunction) (VectorOfVariables,) (VectorAffineFunction,)

"""
OptimizerFactory
User-friendly closure that creates new MOI models. New `OptimizerFactory`s are
created with [`with_optimizer`](@ref) and new models are created from the
optimizer factory `optimizer_factory` with `optimizer_factory()`.
## Examples
The following construct an optimizer factory and then use it to create two
independent `IpoptOptimizer`s:
```julia
optimizer_factory = with_optimizer(IpoptOptimizer, print_level=0)
optimizer1 = optimizer_factory()
optimizer2 = optimizer_factory()
```
"""
struct OptimizerFactory
# The constructor can be
# * `Function`: a function, or
# * `DataType`: a type, or
# * `UnionAll`: a type with missing parameters.
constructor::Union{Function, DataType, UnionAll}
args::Tuple
kwargs # type changes from Julia v0.6 to v0.7 so we leave it untyped for now
end

"""
with_optimizer(constructor::Type, args...; kwargs...)
Return an `OptimizerFactory` that creates optimizers using the constructor
`constructor` with positional arguments `args` and keyword arguments `kwargs`.
## Examples
The following returns an optimizer factory that creates `IpoptOptimizer`s using
the constructor call `IpoptOptimizer(print_level=0)`:
```julia
with_optimizer(IpoptOptimizer, print_level=0)
```
"""
function with_optimizer(constructor::Type, args...; kwargs...)
return OptimizerFactory(constructor, args, kwargs)
end

function (optimizer_factory::OptimizerFactory)()
return optimizer_factory.constructor(optimizer_factory.args...;
optimizer_factory.kwargs...)
end

###############################################################################
# Model

Expand Down Expand Up @@ -123,50 +174,101 @@ mutable struct Model <: AbstractModel
# Enable extensions to attach arbitrary information to a JuMP model by
# using an extension-specific symbol as a key.
ext::Dict{Symbol, Any}
end

# Default constructor.
function Model(;
mode::ModelMode=Automatic,
backend=nothing,
optimizer=nothing,
bridge_constraints=true)
model = new()
model.variabletolowerbound = Dict{MOIVAR, MOILB}()
model.variabletoupperbound = Dict{MOIVAR, MOIUB}()
model.variabletofix = Dict{MOIVAR, MOIFIX}()
model.variabletointegrality = Dict{MOIVAR, MOIINT}()
model.variabletozeroone = Dict{MOIVAR, MOIBIN}()
model.customnames = VariableRef[]
if backend != nothing
# TODO: It would make more sense to not force users to specify
# Direct mode if they also provide a backend.
@assert mode == Direct
@assert optimizer === nothing
@assert MOI.isempty(backend)
model.moibackend = backend
else
@assert mode != Direct
universal_fallback = MOIU.UniversalFallback(JuMPMOIModel{Float64}())
caching_mode = (mode == Automatic) ? MOIU.Automatic : MOIU.Manual
caching_opt = MOIU.CachingOptimizer(universal_fallback,
caching_mode)
if bridge_constraints
model.moibackend = MOI.Bridges.fullbridgeoptimizer(caching_opt,
Float64)
else
model.moibackend = caching_opt
end
if optimizer !== nothing
MOIU.resetoptimizer!(model, optimizer)
end
end
model.optimizehook = nothing
model.nlpdata = nothing
model.objdict = Dict{Symbol, Any}()
model.operator_counter = 0
model.ext = Dict{Symbol, Any}()
return model
"""
Model(moibackend::MOI.ModelLike)
Return a new JuMP model with MOI backend `moibackend`. This constructor is a
low-level constructor used by [`Model()`](@ref),
[`Model(::OptimizerFactory)`](@ref) and [`direct_model`](@ref).
"""
function Model(moibackend::MOI.ModelLike)
@assert MOI.isempty(moibackend)
return Model(Dict{MOIVAR, MOILB}(),
Dict{MOIVAR, MOIUB}(),
Dict{MOIVAR, MOIFIX}(),
Dict{MOIVAR, MOIINT}(),
Dict{MOIVAR, MOIBIN}(),
VariableRef[],
moibackend,
nothing,
nothing,
Dict{Symbol, Any}(),
0,
Dict{Symbol, Any}())
end

"""
Model(; caching_mode::MOIU.CachingOptimizerMode=MOIU.Automatic,
bridge_constraints::Bool=true)
Return a new JuMP model without any optimizer; the model is stored the model in
a cache. The mode of the `CachingOptimizer` storing this cache is
`caching_mode`. The optimizer can be set later in the [`JuMP.optimize`](@ref)
call. If `bridge_constraints` is true, constraints that are not supported by the
optimizer are automatically bridged to equivalent supported constraints when
an appropriate is defined in the `MathOptInterface.Bridges` module or is
defined in another module and is explicitely added.
"""
function Model(; caching_mode::MOIU.CachingOptimizerMode=MOIU.Automatic,
bridge_constraints::Bool=true)
universal_fallback = MOIU.UniversalFallback(JuMPMOIModel{Float64}())
caching_opt = MOIU.CachingOptimizer(universal_fallback,
caching_mode)
if bridge_constraints
backend = MOI.Bridges.fullbridgeoptimizer(caching_opt,
Float64)
else
backend = caching_opt
end
return Model(backend)
end

"""
Model(optimizer_factory::OptimizerFactory;
caching_mode::MOIU.CachingOptimizerMode=MOIU.Automatic,
bridge_constraints::Bool=true)
Return a new JuMP model using the optimizer factory `optimizer_factory` to
create the optimizer. The optimizer factory can be created by the
[`with_optimizer`](@ref) function.
## Examples
The following creates a model using the optimizer
`IpoptOptimizer(print_level=0)`:
```julia
model = JuMP.Model(with_optimizer(IpoptOptimizer, print_level=0))
```
"""
function Model(optimizer_factory::OptimizerFactory; kwargs...)
model = Model(; kwargs...)
optimizer = optimizer_factory()
MOIU.resetoptimizer!(model, optimizer)
return model
end

"""
direct_model(backend::MOI.ModelLike)
Return a new JuMP model using `backend` to store the model and solve it. As
opposed to the [`Model`](@ref) constructor, no cache of the model is stored
outside of `backend` and no bridges are automatically applied to `backend`.
The absence of cache reduces the memory footprint but it is important to bear
in mind the following implications of creating models using this *direct* mode:
* When `backend` does not support an operation such as adding
variables/constraints after solver or modifying constraints, an error is
thrown. With models created using the [`Model`](@ref) constructor, such
situations can be dealt with by storing the modifications in a cache and
loading them into the optimizer when `JuMP.optimize` is called.
* No constraint bridging is supported by default.
* The optimizer used cannot be changed the model is constructed.
* The model created cannot be copied.
"""
function direct_model(backend::MOI.ModelLike)
return Model(backend)
end

# In Automatic and Manual mode, `model.moibackend` is either directly the
Expand Down
25 changes: 23 additions & 2 deletions src/optimizerinterface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,36 @@ function MOIU.attachoptimizer!(model::Model)
end


function optimize(model::Model;
ignore_optimize_hook=(model.optimizehook===nothing))
"""
function optimize(model::Model,
optimizer_factory::Union{Nothing, OptimizerFactory} = nothing;
ignore_optimize_hook=(model.optimizehook===nothing))
Optimize the model. If `optimizer_factory` is not `nothing`, it first set the
optimizer to a new one created using the optimizer factory.
"""
function optimize(model::Model,
optimizer_factory::Union{Nothing, OptimizerFactory} = nothing;
ignore_optimize_hook=(model.optimizehook===nothing))
# The NLPData is not kept in sync, so re-set it here.
# TODO: Consider how to handle incremental solves.
if model.nlpdata !== nothing
MOI.set!(model, MOI.NLPBlock(), create_nlp_block_data(model))
empty!(model.nlpdata.nlconstr_duals)
end

if optimizer_factory !== nothing
if mode(model) == Direct
error("An optimizer factory cannot be provided at the `optimize` call in Direct mode.")
end
if MOIU.state(caching_optimizer(model)) != MOIU.NoOptimizer
error("An optimizer factory cannot both be provided in the `Model` constructor and at the `optimize` call.")
end
optimizer = optimizer_factory()
MOIU.resetoptimizer!(model, optimizer)
MOIU.attachoptimizer!(model)
end

# If the user or an extension has provided an optimize hook, call
# that instead of solving the model ourselves
if !ignore_optimize_hook
Expand Down
Loading

0 comments on commit 9b6d000

Please sign in to comment.