diff --git a/docs/src/manual/constraints.md b/docs/src/manual/constraints.md index 292fb419804..89a854e96a3 100644 --- a/docs/src/manual/constraints.md +++ b/docs/src/manual/constraints.md @@ -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) diff --git a/src/JuMP.jl b/src/JuMP.jl index 9a4e70ebe3d..70d3280995c 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -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 @@ -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 +MOI@0.10. + +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) @@ -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 @@ -500,37 +552,16 @@ 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}, @@ -538,6 +569,7 @@ function _moi_add_bridge( MOI.Bridges.add_bridge(bridge_opt, BridgeType{Float64}) return end + function _moi_add_bridge( caching_opt::MOIU.CachingOptimizer, BridgeType::Type{<:MOI.Bridges.AbstractBridge}, @@ -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 @@ -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) @@ -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 diff --git a/src/constraints.jl b/src/constraints.jl index 90ae38189b5..2175abbc7a0 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -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 """ @@ -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 diff --git a/src/copy.jl b/src/copy.jl index 5c709ab8e01..795a7c7b294 100644 --- a/src/copy.jl +++ b/src/copy.jl @@ -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. diff --git a/src/objective.jl b/src/objective.jl index a7143e127b2..0d5dc9a8b21 100644 --- a/src/objective.jl +++ b/src/objective.jl @@ -90,22 +90,26 @@ functions; the recommended way to set the objective is with the """ function set_objective_function end -function set_objective_function(model::Model, func::MOI.AbstractScalarFunction) - attr = MOI.ObjectiveFunction{typeof(func)}() - if !MOI.supports(backend(model), attr) - error( - "The solver does not support an objective function of type ", - typeof(func), - ".", - ) - end - MOI.set(model, attr, func) - # Nonlinear objectives override regular objectives, so if there was a - # nonlinear objective set, we must clear it. - if model.nlp_data !== nothing - model.nlp_data.nlobj = nothing +function set_objective_function( + model::Model, + f::F, +) where {F<:MOI.AbstractScalarFunction} + attr = MOI.ObjectiveFunction{F}() + if MOI.supports(backend(model), attr) + MOI.set(model, attr, f) + # Nonlinear objectives override regular objectives, so if there was a + # nonlinear objective set, we must clear it. + if model.nlp_data !== nothing + model.nlp_data.nlobj = nothing + end + return + else + _add_bridges_if_needed(model) + return set_objective_function(model, f) end - return + return error( + "The solver does not support an objective function of type $F.", + ) end function set_objective_function(model::Model, func::AbstractJuMPScalar) diff --git a/src/optimizer_interface.jl b/src/optimizer_interface.jl index 49ab948fcab..4cb546615e9 100644 --- a/src/optimizer_interface.jl +++ b/src/optimizer_interface.jl @@ -73,51 +73,58 @@ function MOIU.attach_optimizer(model::Model) end """ - set_optimizer(model::Model, optimizer_factory; - bridge_constraints::Bool=true) - + set_optimizer( + model::Model, + optimizer_factory; + bridge_constraints::Bool = false, + ) Creates an empty `MathOptInterface.AbstractOptimizer` instance by calling -`optimizer_factory()` and sets it as the optimizer of `model`. Specifically, -`optimizer_factory` must be callable with zero arguments and return an empty -`MathOptInterface.AbstractOptimizer`. +`MOI.instantiate(optimizer_factory)` and sets it as the optimizer of `model`. -If `bridge_constraints` is true, constraints that are not supported by the -optimizer are automatically bridged to equivalent supported constraints when -an appropriate transformation is defined in the `MathOptInterface.Bridges` -module or is defined in another module and is explicitly added. +If `bridge_constraints`, adds a `MOI.Bridges.LazyBridgeOptimizer` layer around +the constructed optimizer. -See [`set_optimizer_attributes`](@ref) and [`set_optimizer_attribute`](@ref) for setting -solver-specific parameters of the optimizer. +See [`set_optimizer_attributes`](@ref) and [`set_optimizer_attribute`](@ref) for +setting solver-specific parameters of the optimizer. ## Examples + ```julia model = Model() + set_optimizer(model, GLPK.Optimizer) + +set_optimizer(model, () -> Gurobi.Optimizer(); bridge_constraints = true) + +factory = optimizer_with_attributes(Gurobi.Optimizer, "OutputFlag" => 0) +set_optimizer(model, factory) ``` """ function set_optimizer( model::Model, optimizer_constructor; - bridge_constraints::Bool = true, + bridge_constraints::Bool = false, ) error_if_direct_mode(model, :set_optimizer) - if bridge_constraints - # We set `with_names=false` because the names are handled by the first - # caching optimizer. If `default_copy_to` without names is supported, - # no need for a second cache. + if length(model.bridge_types) > 0 + bridge_constraints = true # If the user added bridges, add them. + end + optimizer = if bridge_constraints optimizer = MOI.instantiate( - optimizer_constructor, + optimizer_constructor; with_bridge_type = Float64, - with_names = false, ) - for bridge_type in model.bridge_types - _moi_add_bridge(optimizer, bridge_type) - end + # Make sure to add the bridges in `model.bridge_types`! These may have + # been added when no optimizer was present. + _moi_add_bridge.(Ref(optimizer), model.bridge_types) + optimizer else - optimizer = MOI.instantiate(optimizer_constructor) + MOI.instantiate(optimizer_constructor) end - return MOIU.reset_optimizer(model, optimizer) + model.moi_backend = _CachingOptimizer(backend(model).model_cache, optimizer) + MOIU.reset_optimizer(model) + return end # Deprecation for JuMP v0.18 -> JuMP v0.19 transition diff --git a/src/variables.jl b/src/variables.jl index d142ac84cd1..f8b8554dcfd 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -455,17 +455,22 @@ See also [`LowerBoundRef`](@ref), [`has_lower_bound`](@ref), [`lower_bound`](@ref), [`delete_lower_bound`](@ref). """ function set_lower_bound(v::VariableRef, lower::Number) - return _moi_set_lower_bound(backend(owner_model(v)), v, lower) + model = owner_model(v) + return _moi_set_lower_bound(model, backend(model), v, lower) end -function _moi_set_lower_bound(backend, v::VariableRef, lower::Number) +function _moi_set_lower_bound(model, moi_backend, v::VariableRef, lower::Number) new_set = MOI.GreaterThan(convert(Float64, lower)) - if _moi_has_lower_bound(backend, v) + if _moi_has_lower_bound(moi_backend, v) cindex = _lower_bound_index(v) - MOI.set(backend, MOI.ConstraintSet(), cindex, new_set) + MOI.set(moi_backend, MOI.ConstraintSet(), cindex, new_set) else - @assert !_moi_is_fixed(backend, v) - MOI.add_constraint(backend, MOI.SingleVariable(index(v)), new_set) + @assert !_moi_is_fixed(moi_backend, v) + moi_add_constraint( + model, + MOI.SingleVariable(index(v)), + new_set, + ) end return end @@ -554,17 +559,22 @@ See also [`UpperBoundRef`](@ref), [`has_upper_bound`](@ref), [`upper_bound`](@ref), [`delete_upper_bound`](@ref). """ function set_upper_bound(v::VariableRef, upper::Number) - return _moi_set_upper_bound(backend(owner_model(v)), v, upper) + model = owner_model(v) + return _moi_set_upper_bound(model, backend(model), v, upper) end -function _moi_set_upper_bound(backend, v::VariableRef, upper::Number) +function _moi_set_upper_bound(model, moi_backend, v::VariableRef, upper::Number) new_set = MOI.LessThan(convert(Float64, upper)) - if _moi_has_upper_bound(backend, v) + if _moi_has_upper_bound(moi_backend, v) cindex = _upper_bound_index(v) - MOI.set(backend, MOI.ConstraintSet(), cindex, new_set) + MOI.set(moi_backend, MOI.ConstraintSet(), cindex, new_set) else - @assert !_moi_is_fixed(backend, v) - MOI.add_constraint(backend, MOI.SingleVariable(index(v)), new_set) + @assert !_moi_is_fixed(moi_backend, v) + moi_add_constraint( + model, + MOI.SingleVariable(index(v)), + new_set, + ) end return end @@ -657,17 +667,24 @@ See also [`FixRef`](@ref), [`is_fixed`](@ref), [`fix_value`](@ref), [`unfix`](@ref). """ function fix(variable::VariableRef, value::Number; force::Bool = false) - return _moi_fix(backend(owner_model(variable)), variable, value, force) + model = owner_model(variable) + return _moi_fix(model, backend(model), variable, value, force) end -function _moi_fix(backend, variable::VariableRef, value::Number, force::Bool) +function _moi_fix( + model, + moi_backend, + variable::VariableRef, + value::Number, + force::Bool, +) new_set = MOI.EqualTo(convert(Float64, value)) - if _moi_is_fixed(backend, variable) # Update existing fixing constraint. + if _moi_is_fixed(moi_backend, variable) # Update existing fixing constraint. c_index = _fix_index(variable) - MOI.set(backend, MOI.ConstraintSet(), c_index, new_set) + MOI.set(moi_backend, MOI.ConstraintSet(), c_index, new_set) else # Add a new fixing constraint. - if _moi_has_upper_bound(backend, variable) || - _moi_has_lower_bound(backend, variable) + if _moi_has_upper_bound(moi_backend, variable) || + _moi_has_lower_bound(moi_backend, variable) if !force error( "Unable to fix $(variable) to $(value) because it has " * @@ -676,15 +693,15 @@ function _moi_fix(backend, variable::VariableRef, value::Number, force::Bool) "delete existing bounds before fixing the variable.", ) end - if _moi_has_upper_bound(backend, variable) - MOI.delete(backend, _upper_bound_index(variable)) + if _moi_has_upper_bound(moi_backend, variable) + MOI.delete(moi_backend, _upper_bound_index(variable)) end - if _moi_has_lower_bound(backend, variable) - MOI.delete(backend, _lower_bound_index(variable)) + if _moi_has_lower_bound(moi_backend, variable) + MOI.delete(moi_backend, _lower_bound_index(variable)) end end - MOI.add_constraint( - backend, + moi_add_constraint( + model, MOI.SingleVariable(index(variable)), new_set, ) @@ -766,20 +783,21 @@ Add an integrality constraint on the variable `variable_ref`. See also [`IntegerRef`](@ref), [`is_integer`](@ref), [`unset_integer`](@ref). """ function set_integer(variable_ref::VariableRef) - return _moi_set_integer(backend(owner_model(variable_ref)), variable_ref) + model = owner_model(variable_ref) + return _moi_set_integer(model, backend(model), variable_ref) end -function _moi_set_integer(backend, variable_ref::VariableRef) - if _moi_is_integer(backend, variable_ref) +function _moi_set_integer(model, moi_backend, variable_ref::VariableRef) + if _moi_is_integer(moi_backend, variable_ref) return - elseif _moi_is_binary(backend, variable_ref) + elseif _moi_is_binary(moi_backend, variable_ref) error( "Cannot set the variable_ref $(variable_ref) to integer as it " * "is already binary.", ) end - MOI.add_constraint( - backend, + moi_add_constraint( + model, MOI.SingleVariable(index(variable_ref)), MOI.Integer(), ) @@ -843,20 +861,21 @@ Add a constraint on the variable `v` that it must take values in the set See also [`BinaryRef`](@ref), [`is_binary`](@ref), [`unset_binary`](@ref). """ function set_binary(variable_ref::VariableRef) - return _moi_set_binary(backend(owner_model(variable_ref)), variable_ref) + model = owner_model(variable_ref) + return _moi_set_binary(model, backend(model), variable_ref) end -function _moi_set_binary(backend, variable_ref) - if _moi_is_binary(backend, variable_ref) +function _moi_set_binary(model, moi_backend, variable_ref) + if _moi_is_binary(moi_backend, variable_ref) return - elseif _moi_is_integer(backend, variable_ref) + elseif _moi_is_integer(moi_backend, variable_ref) error( "Cannot set the variable_ref $(variable_ref) to binary as it " * "is already integer.", ) end - MOI.add_constraint( - backend, + moi_add_constraint( + model, MOI.SingleVariable(index(variable_ref)), MOI.ZeroOne(), ) @@ -969,52 +988,71 @@ Add a variable `v` to `Model m` and sets its name. function add_variable end function add_variable(model::Model, v::ScalarVariable, name::String = "") - return _moi_add_variable(backend(model), model, v, name) + return _moi_add_variable(model, backend(model), v, name) end -function _moi_add_variable(backend, model, v::ScalarVariable, name::String) - index = MOI.add_variable(backend) +function _moi_add_variable( + model::Model, + moi_backend::MOI.ModelLike, + v::ScalarVariable, + name::String, +) + index = MOI.add_variable(moi_backend) var_ref = VariableRef(model, index) - _moi_constrain_variable(backend, index, v.info) + _moi_constrain_variable(model, moi_backend, index, v.info) if !isempty(name) set_name(var_ref, name) end return var_ref end -function _moi_constrain_variable(backend::MOI.ModelLike, index, info) +function _moi_constrain_variable( + model::Model, + moi_backend::MOI.ModelLike, + index, + info, +) # We don't call the _moi* versions (e.g., _moi_set_lower_bound) because they # have extra checks that are not necessary for newly created variables. if info.has_lb - MOI.add_constraint( - backend, + moi_add_constraint( + model, MOI.SingleVariable(index), MOI.GreaterThan{Float64}(info.lower_bound), ) end if info.has_ub - MOI.add_constraint( - backend, + moi_add_constraint( + model, MOI.SingleVariable(index), MOI.LessThan{Float64}(info.upper_bound), ) end if info.has_fix - MOI.add_constraint( - backend, + moi_add_constraint( + model, MOI.SingleVariable(index), MOI.EqualTo{Float64}(info.fixed_value), ) end if info.binary - MOI.add_constraint(backend, MOI.SingleVariable(index), MOI.ZeroOne()) + moi_add_constraint( + model, + MOI.SingleVariable(index), + MOI.ZeroOne(), + ) end if info.integer - MOI.add_constraint(backend, MOI.SingleVariable(index), MOI.Integer()) + moi_add_constraint( + model, + MOI.SingleVariable(index), + MOI.Integer(), + ) end if info.has_start - MOI.set(backend, MOI.VariablePrimalStart(), index, Float64(info.start)) + MOI.set(moi_backend, MOI.VariablePrimalStart(), index, Float64(info.start)) end + return end """ @@ -1048,6 +1086,7 @@ function add_variable( name::String, ) var_index = _moi_add_constrained_variable( + model, backend(model), variable.scalar_variable, variable.set, @@ -1057,17 +1096,29 @@ function add_variable( end function _moi_add_constrained_variable( - backend::MOI.ModelLike, + model::Model, + moi_backend::MOI.ModelLike, scalar_variable::ScalarVariable, - set::MOI.AbstractScalarSet, + set::S, name::String, -) - var_index, con_index = MOI.add_constrained_variable(backend, set) - _moi_constrain_variable(backend, var_index, scalar_variable.info) - if !isempty(name) - MOI.set(backend, MOI.VariableName(), var_index, name) +) where {S<:MOI.AbstractScalarSet} + if MOI.supports_add_constrained_variable(moi_backend, S) + var_index, _ = MOI.add_constrained_variable(moi_backend, set) + _moi_constrain_variable(model, moi_backend, var_index, scalar_variable.info) + if !isempty(name) + MOI.set(moi_backend, MOI.VariableName(), var_index, name) + end + return var_index + elseif _add_bridges_if_needed(model) + return _moi_add_constrained_variable( + model, + backend(model), + scalar_variable, + set, + name, + ) end - return var_index + return error("Model does not support constrained variable in $(set).") end """ @@ -1111,6 +1162,7 @@ function add_variable( names, ) var_indices = _moi_add_constrained_variables( + model, backend(model), variable.scalar_variables, variable.set, @@ -1121,22 +1173,41 @@ function add_variable( end function _moi_add_constrained_variables( - backend::MOI.ModelLike, - scalar_variables::Vector{<:ScalarVariable}, - set::MOI.AbstractVectorSet, - names::Vector{String}, + ::Model, + moi_backend::MOI.ModelLike, + set::MOI.Reals, ) - if set isa MOI.Reals - var_indices = MOI.add_variables(backend, MOI.dimension(set)) - else - var_indices, con_index = MOI.add_constrained_variables(backend, set) + return MOI.add_variables(moi_backend, MOI.dimension(set)) +end + +function _moi_add_constrained_variables( + model::Model, + moi_backend::MOI.ModelLike, + set::S, +) where {S<:MOI.AbstractVectorSet} + if MOI.supports_add_constrained_variables(moi_backend, S) + var_indices, _ = MOI.add_constrained_variables(moi_backend, set) + return var_indices + elseif _add_bridges_if_needed(model) + return _moi_add_constrained_variables(model, backend(model), set) end + return error("Model does not support constrained variables in $(set).") +end + +function _moi_add_constrained_variables( + model::Model, + moi_backend::MOI.ModelLike, + scalar_variables::Vector{<:ScalarVariable}, + set::S, + names::Vector{String}, +) where {S<:MOI.AbstractVectorSet} + var_indices = _moi_add_constrained_variables(model, moi_backend, set) for (index, variable) in zip(var_indices, scalar_variables) - _moi_constrain_variable(backend, index, variable.info) + _moi_constrain_variable(model, moi_backend, index, variable.info) end for (var_index, name) in zip(var_indices, names) if !isempty(name) - MOI.set(backend, MOI.VariableName(), var_index, name) + MOI.set(moi_backend, MOI.VariableName(), var_index, name) end end return var_indices diff --git a/test/constraint.jl b/test/constraint.jl index 50d24afe1df..0ba87282c9f 100644 --- a/test/constraint.jl +++ b/test/constraint.jl @@ -876,7 +876,7 @@ function _test_shadow_price_util( ), ) JuMP.optimize!(model) - mock_optimizer = JuMP.backend(model).optimizer.model + mock_optimizer = JuMP.unsafe_backend(model) MOI.set(mock_optimizer, MOI.TerminationStatus(), MOI.OPTIMAL) MOI.set(mock_optimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) JuMP.optimize!(model) diff --git a/test/feasibility_checker.jl b/test/feasibility_checker.jl index cbe473efd5e..9bfac849391 100644 --- a/test/feasibility_checker.jl +++ b/test/feasibility_checker.jl @@ -109,7 +109,7 @@ function test_primal_solution() model = Model(() -> MOIU.MockOptimizer(MOIU.Model{Float64}())) @variable(model, x, Bin) optimize!(model) - mock = backend(model).optimizer.model + mock = unsafe_backend(model) MOI.set(mock, MOI.TerminationStatus(), MOI.OPTIMAL) MOI.set(mock, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) MOI.set(mock, MOI.VariablePrimal(), optimizer_index(x), 1.0) diff --git a/test/generate_and_solve.jl b/test/generate_and_solve.jl index 4538bd01d76..18e44f3da75 100644 --- a/test/generate_and_solve.jl +++ b/test/generate_and_solve.jl @@ -56,7 +56,7 @@ using JuMP ) JuMP.optimize!(m) - mockoptimizer = JuMP.backend(m).optimizer.model + mockoptimizer = JuMP.unsafe_backend(m) MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string") MOI.set(mockoptimizer, MOI.ObjectiveValue(), -1.0) @@ -204,7 +204,6 @@ using JuMP MOIU.Model{Float64}(), eval_objective_value = false, ), - caching_mode = MOIU.AUTOMATIC, ) @variable(m, x == 1.0, Int) @variable(m, y, Bin) @@ -233,7 +232,7 @@ using JuMP MOIU.attach_optimizer(m) - mockoptimizer = JuMP.backend(m).optimizer.model + mockoptimizer = JuMP.unsafe_backend(m) MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string") MOI.set(mockoptimizer, MOI.ObjectiveValue(), 1.0) @@ -314,7 +313,7 @@ using JuMP ) JuMP.optimize!(m) - mockoptimizer = JuMP.backend(m).optimizer.model + mockoptimizer = JuMP.unsafe_backend(m) MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string") MOI.set(mockoptimizer, MOI.ObjectiveValue(), -1.0) @@ -627,7 +626,7 @@ c2: x + y <= 1.0 ) JuMP.optimize!(m) - mock = JuMP.backend(m).optimizer.model + mock = JuMP.unsafe_backend(m) MOI.set(mock, MOI.TerminationStatus(), MOI.OPTIMAL) MOI.set(mock, MOI.ResultCount(), 2) diff --git a/test/lp_sensitivity.jl b/test/lp_sensitivity.jl index c7306ee2a67..7e992e1c7df 100644 --- a/test/lp_sensitivity.jl +++ b/test/lp_sensitivity.jl @@ -27,7 +27,7 @@ function test_lp_rhs_perturbation_range( ), ) JuMP.optimize!(model) - mock_optimizer = JuMP.backend(model).optimizer.model + mock_optimizer = JuMP.unsafe_backend(model) MOI.set(mock_optimizer, MOI.TerminationStatus(), MOI.OPTIMAL) JuMP.optimize!(model) @@ -188,7 +188,7 @@ function test_lp_objective_perturbation_range( ), ) JuMP.optimize!(model) - mock_optimizer = JuMP.backend(model).optimizer.model + mock_optimizer = JuMP.unsafe_backend(model) MOI.set(mock_optimizer, MOI.TerminationStatus(), MOI.OPTIMAL) MOI.set(mock_optimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT) diff --git a/test/model.jl b/test/model.jl index 4dc3c5abbcf..291f3e3fe80 100644 --- a/test/model.jl +++ b/test/model.jl @@ -203,11 +203,13 @@ end function test_bridges_automatic() # optimizer not supporting Interval - model = Model(() -> MOIU.MockOptimizer(SimpleLPModel{Float64}())) - @test JuMP.bridge_constraints(model) + model = Model( + () -> MOIU.MockOptimizer(SimpleLPModel{Float64}()); + bridge_constraints = true, + ) @test JuMP.backend(model) isa MOIU.CachingOptimizer @test JuMP.backend(model).optimizer isa MOI.Bridges.LazyBridgeOptimizer - @test JuMP.backend(model).optimizer.model isa MOIU.MockOptimizer + @test JuMP.unsafe_backend(model) isa MOIU.MockOptimizer @variable model x cref = @constraint model 0 <= x + 1 <= 1 @test cref isa JuMP.ConstraintRef{ @@ -228,12 +230,12 @@ function test_bridges_automatic_with_cache() SimpleLPModel{Float64}(), needs_allocate_load = true, ), + bridge_constraints = true, ) - @test JuMP.bridge_constraints(model) @test JuMP.backend(model) isa MOIU.CachingOptimizer @test JuMP.backend(model).optimizer isa MOI.Bridges.LazyBridgeOptimizer @test JuMP.backend(model).optimizer.model isa MOIU.CachingOptimizer - @test JuMP.backend(model).optimizer.model.optimizer isa MOIU.MockOptimizer + @test JuMP.unsafe_backend(model) isa MOIU.MockOptimizer @variable model x err = ErrorException( "There is no `optimizer_index` as the optimizer is not " * @@ -258,27 +260,10 @@ function test_bridges_automatic_with_cache() @test_throws err optimizer_index(cref) end -function test_bridges_automatic_disabled() - # Automatic bridging disabled with `bridge_constraints` keyword - model = Model( - () -> MOIU.MockOptimizer(SimpleLPModel{Float64}()), - bridge_constraints = false, - ) - @test !JuMP.bridge_constraints(model) - @test JuMP.backend(model) isa MOIU.CachingOptimizer - @test !(JuMP.backend(model).optimizer isa MOI.Bridges.LazyBridgeOptimizer) - @variable model x - err = ErrorException( - "Constraints of type MathOptInterface.ScalarAffineFunction{Float64}-in-MathOptInterface.Interval{Float64} are not supported by the solver, try using `bridge_constraints=true` in the `JuMP.Model` constructor if you believe the constraint can be reformulated to constraints supported by the solver.", - ) - @test_throws err @constraint model 0 <= x + 1 <= 1 -end - function test_bridges_direct() # No bridge automatically added in Direct mode optimizer = MOIU.MockOptimizer(SimpleLPModel{Float64}()) model = JuMP.direct_model(optimizer) - @test !JuMP.bridge_constraints(model) @variable model x err = ErrorException( "Constraints of type MathOptInterface.ScalarAffineFunction{Float64}-in-MathOptInterface.Interval{Float64} are not supported by the solver.", @@ -303,7 +288,7 @@ function mock_factory() end function test_bridges_add_before_con_model_optimizer() - model = Model(mock_factory) + model = Model(mock_factory; bridge_constraints = true) @variable(model, x) JuMP.add_bridge(model, NonnegativeBridge) c = @constraint(model, x in Nonnegative()) @@ -318,7 +303,7 @@ function test_bridges_add_before_con_set_optimizer() @variable(model, x) c = @constraint(model, x in Nonnegative()) JuMP.add_bridge(model, NonnegativeBridge) - set_optimizer(model, mock_factory) + set_optimizer(model, mock_factory; bridge_constraints = true) JuMP.optimize!(model) @test 1.0 == @inferred JuMP.value(x) @test 1.0 == @inferred JuMP.value(c) @@ -326,7 +311,7 @@ function test_bridges_add_before_con_set_optimizer() end function test_bridges_add_after_con_model_optimizer() - model = Model(mock_factory) + model = Model(mock_factory; bridge_constraints = true) @variable(model, x) flag = true try @@ -381,7 +366,7 @@ function test_bridges_add_bridgeable_con_set_optimizer() constraint = ScalarConstraint(x, Nonnegative()) bc = BridgeableConstraint(constraint, NonnegativeBridge) c = add_constraint(model, bc) - set_optimizer(model, mock_factory) + set_optimizer(model, mock_factory; bridge_constraints = true) JuMP.optimize!(model) @test 1.0 == @inferred JuMP.value(x) @test 1.0 == @inferred JuMP.value(c) @@ -389,19 +374,13 @@ function test_bridges_add_bridgeable_con_set_optimizer() end function test_bridge_graph_false() - model = Model(mock_factory, bridge_constraints = false) + model = Model(mock_factory) @variable(model, x) @test_throws( ErrorException( - "Cannot add bridge if `bridge_constraints` was set to `false` in " * - "the `Model` constructor.", - ), - add_bridge(model, NonnegativeBridge) - ) - @test_throws( - ErrorException( - "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)`.", ), print_bridge_graph(model) ) @@ -509,7 +488,7 @@ end function dummy_optimizer_hook(::JuMP.AbstractModel) end function copy_model_style_mode(use_copy_model, caching_mode, filter_mode) - model = Model(caching_mode = caching_mode) + model = Model() model.optimize_hook = dummy_optimizer_hook data = DummyExtensionData(model) model.ext[:dummy] = data @@ -596,12 +575,6 @@ end function test_copy_model_base_auto() return copy_model_style_mode(false, MOIU.AUTOMATIC, false) end -function test_copy_model_jump_manual() - return copy_model_style_mode(true, MOIU.MANUAL, false) -end -function test_copy_model_base_manual() - return copy_model_style_mode(false, MOIU.MANUAL, false) -end function test_copy_direct_mode() mock = MOIU.MockOptimizer(MOIU.Model{Float64}()) @@ -782,7 +755,7 @@ function test_copy_conflict() ) JuMP.optimize!(model) - mockoptimizer = JuMP.backend(model).optimizer.model + mockoptimizer = JuMP.unsafe_backend(model) MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.INFEASIBLE) MOI.set(mockoptimizer, MOI.ConflictStatus(), MOI.CONFLICT_FOUND) MOI.set( diff --git a/test/print.jl b/test/print.jl index 5f4d466d315..d3702b03799 100644 --- a/test/print.jl +++ b/test/print.jl @@ -902,7 +902,7 @@ end ) optimize!(model) - mockoptimizer = JuMP.backend(model).optimizer.model + mockoptimizer = JuMP.unsafe_backend(model) MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL) MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string") MOI.set(mockoptimizer, MOI.ObjectiveValue(), -1.0)