jump-dev / JuMP.jl

Modeling language for Mathematical Optimization (linear, mixed-integer, conic, semidefinite, nonlinear)
http://jump.dev/JuMP.jl/
Other
2.22k stars 393 forks source link

DNM: Add unsafe_(g|s)et_backend_attribute #3685

Closed odow closed 6 months ago

odow commented 7 months ago

Closes https://github.com/jump-dev/JuMP.jl/issues/3684

The issue is asking for something like this to work, which skips the safety checks like https://github.com/jump-dev/JuMP.jl/blob/4be967cc9ecad56218929914308b7d4fbcd79678/src/optimizer_interface.jl#L740-L748

using JuMP, Gurobi
model = Model(Gurobi.Optimizer)
@variable(model, x >= 1.5)
@constraint(model, c, 2x + 1 <= 10)
@objective(model, Min, 1-x)
unsafe_get_backend_attribute(model, Gurobi.ModelAttribute("IsMIP"))
unsafe_get_backend_attribute.(model, Gurobi.VariableAttribute("LB"), x)
unsafe_get_backend_attribute(model, Gurobi.ConstraintAttribute("RHS"), c)

The other option is something like:

    get_attribute(model::GenericModel, attr::MOI.AbstractModelAttribute; force_backend::Bool = false)
    get_attribute(x::GenericVariableRef, attr::MOI.AbstractVariableAttribute; force_backend::Bool = false)
    get_attribute(cr::ConstraintRef, attr::MOI.AbstractConstraintAttribute; force_backend::Bool = false)

TODO

Waiting for consensus on

a) whether we should add this b) naming

codecov[bot] commented 7 months ago

Codecov Report

All modified and coverable lines are covered by tests :white_check_mark:

Project coverage is 98.34%. Comparing base (4be967c) to head (5e7228e).

Additional details and impacted files ```diff @@ Coverage Diff @@ ## master #3685 +/- ## ======================================= Coverage 98.33% 98.34% ======================================= Files 43 43 Lines 5696 5728 +32 ======================================= + Hits 5601 5633 +32 Misses 95 95 ```

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

odow commented 6 months ago

@blegat says consider instead unsafe_index which returns the MOI.VariableIndex of the variable/constraint in the unsafe_backend.

odow commented 6 months ago

To elaborate on unsafe_index, developer call suggests

using JuMP, Gurobi
model = Model(Gurobi.Optimizer)
@variable(model, x >= 1.5)
@constraint(model, c, 2x + 1 <= 10)
@objective(model, Min, 1 - x)
grb = unsafe_backend(model)
MOI.get(grb, Gurobi.ModelAttribute("IsMIP"))
MOI.get.(grb, Gurobi.VariableAttribute("LB"), unsafe_index.(x))
MOI.get(grb, Gurobi.ConstraintAttribute("RHS"), unsafe_index(c))

to complement

using JuMP, Gurobi
model = direct_model(Gurobi.Optimizer())
@variable(model, x >= 1.5)
@constraint(model, c, 2x + 1 <= 10)
@objective(model, Min, 1 - x)
grb = backend(model)
MOI.get(grb, Gurobi.ModelAttribute("IsMIP"))
MOI.get.(grb, Gurobi.VariableAttribute("LB"), index.(x))
MOI.get(grb, Gurobi.ConstraintAttribute("RHS"), index(c))
odow commented 6 months ago

So we already have optimizer_index, which is exactly this.

julia> using JuMP, Gurobi

julia> model = Model(Gurobi.Optimizer)
A JuMP Model
Feasibility problem with:
Variables: 0
Model mode: AUTOMATIC
CachingOptimizer state: EMPTY_OPTIMIZER
Solver name: Gurobi

julia> @variable(model, x >= 1.5)
x

julia> @constraint(model, c, 2x + 1 <= 10)
c : 2 x ≤ 9

julia> @constraint(model, d, 0 <= 2x + 1 <= 10)
d : 2 x ∈ [-1, 9]

julia> @objective(model, Min, 1 - x)
-x + 1

julia> grb = unsafe_backend(model)
    sense  : minimize
    number of variables             = 0
    number of linear constraints    = 0
    number of quadratic constraints = 0
    number of sos constraints       = 0
    number of non-zero coeffs       = 0
    number of non-zero qp objective terms  = 0
    number of non-zero qp constraint terms = 0

julia> optimizer_index.(x)
ERROR: There is no `optimizer_index` as the optimizer is not synchronized with the cached model. Call `MOIU.attach_optimizer(model)` to synchronize it.
Stacktrace:
 [1] error(::String, ::String, ::String)
   @ Base ./error.jl:44
 [2] _moi_optimizer_index(model::MathOptInterface.Utilities.CachingOptimizer{…}, index::MathOptInterface.VariableIndex)
   @ JuMP ~/.julia/packages/JuMP/kSaGf/src/optimizer_interface.jl:1103
 [3] optimizer_index(x::VariableRef)
   @ JuMP ~/.julia/packages/JuMP/kSaGf/src/optimizer_interface.jl:1145
 [4] _broadcast_getindex_evalf
   @ ./broadcast.jl:709 [inlined]
 [5] _broadcast_getindex
   @ ./broadcast.jl:682 [inlined]
 [6] getindex
   @ ./broadcast.jl:636 [inlined]
 [7] copy
   @ ./broadcast.jl:918 [inlined]
 [8] materialize(bc::Base.Broadcast.Broadcasted{…})
   @ Base.Broadcast ./broadcast.jl:903
 [9] top-level scope
   @ REPL[49]:1
Some type information was truncated. Use `show(err)` to see complete types.

julia> MOIU.attach_optimizer(model)

julia> optimizer_index.(x)
MOI.VariableIndex(1)

julia> MOI.get(grb, Gurobi.ModelAttribute("IsMIP"))
0

julia> MOI.get.(grb, Gurobi.VariableAttribute("LB"), optimizer_index.(x))
1.5

julia> MOI.get(grb, Gurobi.ConstraintAttribute("RHS"), optimizer_index(c))
9.0

julia> MOI.get(grb, Gurobi.ConstraintAttribute("RHS"), optimizer_index(d))
ERROR: There is no `optimizer_index` for MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.Interval{Float64}} constraints because they are bridged.
Stacktrace:
 [1] error(::String, ::String)
   @ Base ./error.jl:44
 [2] _moi_optimizer_index(model::MathOptInterface.Bridges.LazyBridgeOptimizer{…}, index::MathOptInterface.ConstraintIndex{…})
   @ JuMP ~/.julia/packages/JuMP/kSaGf/src/optimizer_interface.jl:1121
 [3] _moi_optimizer_index(model::MathOptInterface.Utilities.CachingOptimizer{…}, index::MathOptInterface.ConstraintIndex{…})
   @ JuMP ~/.julia/packages/JuMP/kSaGf/src/optimizer_interface.jl:1110
 [4] optimizer_index(x::ConstraintRef{Model, MathOptInterface.ConstraintIndex{…}, ScalarShape})
   @ JuMP ~/.julia/packages/JuMP/kSaGf/src/optimizer_interface.jl:1145
 [5] top-level scope
   @ REPL[55]:1
Some type information was truncated. Use `show(err)` to see complete types.
odow commented 6 months ago

So maybe this just needs to be better documented in Gurobi.jl

odow commented 6 months ago

@blegat I think we could just close this.

There are plenty of work-arounds, and the Gurobi.jl README now suggests direct-mode.

blegat commented 6 months ago

Maybe mention optimizer_index in the docstring of unsafe_index and vice versa ?

blegat commented 6 months ago

Actually, there is subtle difference. In DIRECT model, optimizer_index does not go through any bridge or caching optimizer layer while unsafe_backend does. So if you call direct_model(bridge_layer(optimizer)), optimizer_index does not actually give the index of the optimizer. Same if you do direct_model(bridge_layer(caching_layer(optimizer)). I suggest we fix that by ensuring that optimizer_index returns the index corresponding to unsafe_backend (it's a bug-fix IMO). We can keep the same name because optimizer_index is actually not unsafe. Then we can document in each other docstring that they can be combined.

odow commented 6 months ago

Closing in favor of #3719