Skip to content

Commit

Permalink
WIP: bridge backend of Model if needed
Browse files Browse the repository at this point in the history
  • Loading branch information
odow committed May 21, 2021
1 parent c27e4b9 commit e5e0918
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 272 deletions.
2 changes: 1 addition & 1 deletion docs/src/manual/constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ DocTestSetup = quote
@constraint(model, con, x <= 1);
@objective(model, Max, -2x);
optimize!(model);
mock = backend(model).optimizer.model;
mock = unsafe_backend(model);
MOI.set(mock, MOI.TerminationStatus(), MOI.OPTIMAL)
MOI.set(mock, MOI.DualStatus(), MOI.FEASIBLE_POINT)
MOI.set(mock, MOI.ConstraintDual(), optimizer_index(con), -2.0)
Expand Down
203 changes: 120 additions & 83 deletions src/JuMP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -236,59 +236,61 @@ mutable struct Model <: AbstractModel
end

"""
Model(; caching_mode::MOIU.CachingOptimizerMode=MOIU.AUTOMATIC)
Model(optimizer_factory = nothing; bridge_constraints::Bool = false,)
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`. Use [`set_optimizer`](@ref) to set the optimizer before
calling [`optimize!`](@ref).
"""
function Model(;
caching_mode::MOIU.CachingOptimizerMode = MOIU.AUTOMATIC,
solver = nothing,
)
if solver !== nothing
error(
"The solver= keyword is no longer available in JuMP 0.19 and " *
"later. See the JuMP documentation " *
"(https://jump.dev/JuMP.jl/latest/) for latest syntax.",
)
end
universal_fallback = MOIU.UniversalFallback(MOIU.Model{Float64}())
caching_opt = MOIU.CachingOptimizer(universal_fallback, caching_mode)
return direct_model(caching_opt)
end
Construct a JuMP model with a `MOI.Utilities.CachingOptimizer` backend.
"""
Model(optimizer_factory;
caching_mode::MOIU.CachingOptimizerMode=MOIU.AUTOMATIC,
bridge_constraints::Bool=true)
See [`set_optimizer`](@ref) for a description of the arguments.
## Examples
Return a new JuMP model with the provided optimizer and bridge settings. This
function is equivalent to:
Create a model with no backing optimizer. Call [`set_optimizer`](@ref) later to
add an optimizer prior to calling [`optimize!`](@ref).
```julia
model = Model()
set_optimizer(model, optimizer_factory,
bridge_constraints=bridge_constraints)
return model
model = Model()
```
See [`set_optimizer`](@ref) for the description of the `optimizer_factory` and
`bridge_constraints` arguments.
## Examples
Pass a `.Optimizer` object from a supported package:
```julia
model = Model(GLPK.Optimizer)
```
The following creates a model with the optimizer set to `Ipopt`:
Use [`optimizer_with-attributes`](@ref) to initialize the model with the
provided attributes:
```julia
model = Model(Ipopt.Optimizer)
model = Model(
optimizer_with_attributes(GLPK.Optimizer, "loglevel" => 0),
)
```
Create an anonymous function to pass positional arguments to the optimizer:
```julia
model = Model() do
AmplNLWriter.Optimizer(Bonmin_jll.amplexe)
end
```
Pass `bridge_constraints = true` to intialize the model with bridges. Normally,
bridges will be added only if necessary. Adding them here can have performance
benefits if you know that your model will use the bridges.
```julia
model = Model(SCS.Optimizer; bridge_constraints = true)
```
"""
function Model(optimizer_factory; bridge_constraints::Bool = true, kwargs...)
model = Model(; kwargs...)
set_optimizer(
model,
optimizer_factory,
bridge_constraints = bridge_constraints,
function Model(optimizer_factory = nothing; bridge_constraints::Bool = false)
model = direct_model(
MOI.Utilities.CachingOptimizer(
MOIU.UniversalFallback(MOIU.Model{Float64}()),
MOI.Utilities.AUTOMATIC,
),
)
if optimizer_factory !== nothing
set_optimizer(
model,
optimizer_factory;
bridge_constraints = bridge_constraints,
)
end
return model
end

Expand Down Expand Up @@ -432,6 +434,66 @@ end
unsafe_backend(model::MOIB.LazyBridgeOptimizer) = unsafe_backend(model.model)
unsafe_backend(model::MOI.ModelLike) = model

_needs_bridges(::MOI.ModelLike) = true
_needs_bridges(::MOI.Bridges.LazyBridgeOptimizer) = false
_needs_bridges(::Nothing) = false

"""
TODO(odow): replace with `MOI.Utilities.CachingOptimizer` when updating to
[email protected].
This function works around an unnecessary `@assert MOI.is_valid(model_cache)`
check when creating a CachingOptimizer.
"""
function _CachingOptimizer(
model_cache::A,
optimizer::B,
) where {A<:MOI.ModelLike,B<:MOI.AbstractOptimizer}
return MOI.Utilities.CachingOptimizer{B,A}(
optimizer,
model_cache,
MOI.Utilities.EMPTY_OPTIMIZER,
MOI.Utilities.AUTOMATIC,
MOI.Utilities.IndexMap(),
MOI.Utilities.IndexMap(),
)
end

function _add_bridges_if_needed(
model::Model,
backend::MOI.Utilities.CachingOptimizer,
)
if !_needs_bridges(backend.optimizer)
return false
end
optimizer = backend.optimizer
if !MOI.Utilities.supports_default_copy_to(backend.optimizer, false)
optimizer = Utilities.CachingOptimizer(
Utilities.UniversalFallback(Utilities.Model{Float64}()),
backend.optimizer,
)
end
bridge = MOI.Bridges.full_bridge_optimizer(optimizer, Float64)
# We don't have to worry about adding `model.bridge_types` here, because
# calling `add_bridge` will force a `LazyBridgeOptimizer` backend.
model.moi_backend = _CachingOptimizer(backend.model_cache, bridge)
MOIU.reset_optimizer(model)
return true
end

_add_bridges_if_needed(::Model, ::MOI.ModelLike) = false

"""
_add_bridges_if_needed(model::Model)
Add a `MOI.Bridges.LazyBridgeOptimizer` to the backend of `model` if one does
not already exist. Returns `true` if a new `MOI.Bridges.LazyBridgeOptimizer` is
added, and `false` otherwise.
"""
function _add_bridges_if_needed(model::Model)
return _add_bridges_if_needed(model, backend(model))
end

"""
moi_mode(model::MOI.ModelLike)
Expand All @@ -458,16 +520,6 @@ function mode(model::Model)
return moi_mode(backend(model))
end

"""
moi_bridge_constraints(model::MOI.ModelLike)
Return `true` if `model` will bridge constraints.
"""
moi_bridge_constraints(model::MOI.ModelLike) = false
function moi_bridge_constraints(model::MOIU.CachingOptimizer)
return model.optimizer isa MOI.Bridges.LazyBridgeOptimizer
end

# Internal function.
function _try_get_solver_name(model_like)
try
Expand Down Expand Up @@ -500,44 +552,24 @@ function solver_name(model::Model)
end
end

"""
bridge_constraints(model::Model)
# No optimizer is attached, the bridge will be added when one is attached
_moi_add_bridge(::Nothing, ::Type{<:MOI.Bridges.AbstractBridge}) = nothing

When in direct mode, return `false`.
When in manual or automatic mode, return a `Bool` indicating whether the
optimizer is set and unsupported constraints are automatically bridged
to equivalent supported constraints when an appropriate transformation is
available.
"""
function bridge_constraints(model::Model)
# The type of `backend(model)` is not type-stable, so we use a function
# barrier (`moi_bridge_constraints`) to improve performance.
return moi_bridge_constraints(backend(model))
end

function _moi_add_bridge(
model::Nothing,
BridgeType::Type{<:MOI.Bridges.AbstractBridge},
)
# No optimizer is attached, the bridge will be added when one is attached
return
end
function _moi_add_bridge(
model::MOI.ModelLike,
BridgeType::Type{<:MOI.Bridges.AbstractBridge},
)
function _moi_add_bridge(::MOI.ModelLike, ::Type{<:MOI.Bridges.AbstractBridge})
return error(
"Cannot add bridge if `bridge_constraints` was set to `false` in the",
" `Model` constructor.",
"`In order to add a bridge, you must pass `bridge_constraints=true` " *
"to `Model`, i.e., `Model(optimizer; bridge_constraints = true)`.",
)
end

function _moi_add_bridge(
bridge_opt::MOI.Bridges.LazyBridgeOptimizer,
BridgeType::Type{<:MOI.Bridges.AbstractBridge},
)
MOI.Bridges.add_bridge(bridge_opt, BridgeType{Float64})
return
end

function _moi_add_bridge(
caching_opt::MOIU.CachingOptimizer,
BridgeType::Type{<:MOI.Bridges.AbstractBridge},
Expand All @@ -547,8 +579,10 @@ function _moi_add_bridge(
end

"""
add_bridge(model::Model,
BridgeType::Type{<:MOI.Bridges.AbstractBridge})
add_bridge(
model::Model,
BridgeType::Type{<:MOI.Bridges.AbstractBridge},
)
Add `BridgeType` to the list of bridges that can be used to transform
unsupported constraints into an equivalent formulation using only constraints
Expand All @@ -559,6 +593,8 @@ function add_bridge(
BridgeType::Type{<:MOI.Bridges.AbstractBridge},
)
push!(model.bridge_types, BridgeType)
# Make sure we force a `LazyBridgeOptimizer` backend!
_add_bridges_if_needed(model)
# The type of `backend(model)` is not type-stable, so we use a function
# barrier (`_moi_add_bridge`) to improve performance.
_moi_add_bridge(JuMP.backend(model), BridgeType)
Expand Down Expand Up @@ -606,8 +642,9 @@ end

function _moi_print_bridge_graph(::IO, ::MOI.ModelLike)
return error(
"Cannot print bridge graph if `bridge_constraints` was set to " *
"`false` in the `Model` constructor.",
"`In order to print the bridge graph, you must pass " *
"`bridge_constraints=true` to `Model`, i.e., " *
"`Model(optimizer; bridge_constraints = true)`.",
)
end

Expand Down
58 changes: 33 additions & 25 deletions src/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -513,33 +513,37 @@ function check_belongs_to_model(con::VectorConstraint, model)
end

function moi_add_constraint(
model::MOI.ModelLike,
f::MOI.AbstractFunction,
s::MOI.AbstractSet,
)
if !MOI.supports_constraint(model, typeof(f), typeof(s))
if moi_mode(model) == DIRECT
bridge_message = "."
elseif moi_bridge_constraints(model)
error(
sprint(
io -> MOI.Bridges.debug(
model.optimizer,
typeof(f),
typeof(s);
io = io,
),
),
)
else
bridge_message = ", try using `bridge_constraints=true` in the `JuMP.Model` constructor if you believe the constraint can be reformulated to constraints supported by the solver."
end
model::JuMP.Model,
f::F,
s::S,
) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet}
return moi_add_constraint(model, backend(model), f, s)
end

function moi_add_constraint(
model::JuMP.Model,
moi_backend::MOI.ModelLike,
f::F,
s::S,
) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet}
if MOI.supports_constraint(moi_backend, F, S)
# Our backend supports the constraint. Go ahead and add it.
return MOI.add_constraint(moi_backend, f, s)
elseif _add_bridges_if_needed(model)
# In here, we added some bridges. Call again with the new backend.
return moi_add_constraint(model, f, s)
end
# Our backend doesn't support the constraint, and even after we added
# bridges it still didn't!
if moi_mode(moi_backend) == DIRECT
error("Constraints of type $F-in-$S are not supported by the solver.")
else
error(
"Constraints of type $(typeof(f))-in-$(typeof(s)) are not supported by the solver" *
bridge_message,
sprint() do io
return MOI.Bridges.debug(moi_backend.optimizer, F, S; io = io)
end,
)
end
return MOI.add_constraint(model, f, s)
end

"""
Expand All @@ -555,7 +559,11 @@ function add_constraint(
# The type of backend(model) is unknown so we directly redirect to another
# function.
check_belongs_to_model(con, model)
cindex = moi_add_constraint(backend(model), moi_function(con), moi_set(con))
cindex = moi_add_constraint(
model,
moi_function(con),
moi_set(con),
)
cshape = shape(con)
if !(cshape isa ScalarShape) && !(cshape isa VectorShape)
model.shapes[cindex] = cshape
Expand Down
3 changes: 1 addition & 2 deletions src/copy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,7 @@ function copy_model(
"able to copy the constructed model.",
)
end
caching_mode = backend(model).mode
new_model = Model(caching_mode = caching_mode)
new_model = Model()

# At JuMP's level, filter_constraints should work with JuMP.ConstraintRef,
# whereas MOI.copy_to's filter_constraints works with MOI.ConstraintIndex.
Expand Down
Loading

0 comments on commit e5e0918

Please sign in to comment.