Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get attributes set by optimizer without calling optimize #3684

Closed
rezabayani opened this issue Dec 14, 2023 · 18 comments · Fixed by #3719
Closed

Get attributes set by optimizer without calling optimize #3684

rezabayani opened this issue Dec 14, 2023 · 18 comments · Fixed by #3719

Comments

@rezabayani
Copy link

rezabayani commented Dec 14, 2023

I tried running an older code (first written around 2 years ago) with newer JuMP and Gurobi versions. When I try to access some of the model attributes such as Gurobi.VariableAttribute("LB") or Gurobi.ConstraintAttribute("RHS"), I get the error OptimizeNotCalled(). This worked fine previously.

model = direct_model(Gurobi.Optimizer())
@variable(model, x >= 1.5)
@constraint(model, c, 2x + 1 <= 10)
@objective(model, Min, 1-x)

get_optimizer_attribute(model, Gurobi.ModelAttribute("IsMIP")) # ERROR: OptimizeNotCalled()
MOI.get.(model, Gurobi.VariableAttribute("LB"), x) # ERROR: OptimizeNotCalled()
MOI.get.(model, Gurobi.ConstraintAttribute("RHS"), c) # ERROR: OptimizeNotCalled()

Right now, I need to run optimize! and then it works as intended. Is there a workaround for this issue?

My current installed versions are:
JuMP version = "1.16.0"
Gurobi version = "1.2.0"

My old code versions are:
JuMP version = "0.7.3"
Gurobi version = "0.10.3"

@odow
Copy link
Member

odow commented Dec 14, 2023

Double-checked that I can reproduce:

julia> model = direct_model(Gurobi.Optimizer())
A JuMP Model
Feasibility problem with:
Variables: 0
Model mode: DIRECT
Solver name: Gurobi

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

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

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

julia> get_optimizer_attribute(model, Gurobi.ModelAttribute("IsMIP")) # ERROR: OptimizeNotCalled()
ERROR: OptimizeNotCalled()
Stacktrace:
 [1] _moi_get_result(model::Gurobi.Optimizer, args::Gurobi.ModelAttribute)
   @ JuMP ~/.julia/packages/JuMP/R53zo/src/optimizer_interface.jl:652
 [2] get(model::Model, attr::Gurobi.ModelAttribute)
   @ JuMP ~/.julia/packages/JuMP/R53zo/src/optimizer_interface.jl:683
 [3] get_attribute
   @ ~/.julia/packages/JuMP/R53zo/src/optimizer_interface.jl:793 [inlined]
 [4] get_optimizer_attribute(model::Model, attr::Gurobi.ModelAttribute)
   @ JuMP ~/.julia/packages/JuMP/R53zo/src/optimizer_interface.jl:152
 [5] top-level scope
   @ REPL[13]:1

julia> MOI.get.(model, Gurobi.VariableAttribute("LB"), x) # ERROR: OptimizeNotCalled()
ERROR: OptimizeNotCalled()
Stacktrace:
 [1] _moi_get_result(::Gurobi.Optimizer, ::Gurobi.VariableAttribute, ::Vararg{Any})
   @ JuMP ~/.julia/packages/JuMP/R53zo/src/optimizer_interface.jl:652
 [2] get(model::Model, attr::Gurobi.VariableAttribute, v::VariableRef)
   @ JuMP ~/.julia/packages/JuMP/R53zo/src/optimizer_interface.jl:703
 [3] _broadcast_getindex_evalf
   @ ./broadcast.jl:683 [inlined]
 [4] _broadcast_getindex
   @ ./broadcast.jl:656 [inlined]
 [5] getindex
   @ ./broadcast.jl:610 [inlined]
 [6] copy
   @ ./broadcast.jl:888 [inlined]
 [7] materialize(bc::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{0}, Nothing, typeof(MathOptInterface.get), Tuple{Base.RefValue{Model}, Base.RefValue{Gurobi.VariableAttribute}, Base.RefValue{VariableRef}}})
   @ Base.Broadcast ./broadcast.jl:873
 [8] top-level scope
   @ REPL[14]:1

julia> MOI.get.(model, Gurobi.ConstraintAttribute("RHS"), c)
ERROR: OptimizeNotCalled()

I expected this to work, so marking this as a bug. I'll take a look.

@odow odow added the Type: Bug label Dec 14, 2023
@odow
Copy link
Member

odow commented Dec 14, 2023

This was caused by jump-dev/Gurobi.jl#425.

We don't have a way to say which attributes were set by the solver and require optimize!, and which are defined by the optimizer without needing optimize!.

See also jump-dev/MathOptInterface.jl#2345

@odow
Copy link
Member

odow commented Dec 14, 2023

At the JuMP level, we can't just pass through (even in direct-mode), because then we'd stop throwing errors for MOI.VariablePrimal etc.

Transferring to MOI because it probably needs a fix there.

@odow odow transferred this issue from jump-dev/JuMP.jl Dec 14, 2023
@odow
Copy link
Member

odow commented Dec 15, 2023

So you can force this with:

julia> using JuMP, Gurobi

julia> model = direct_model(Gurobi.Optimizer())
A JuMP Model
Feasibility problem with:
Variables: 0
Model mode: DIRECT
Solver name: Gurobi

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

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

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

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

julia> MOI.get.(backend(model), Gurobi.VariableAttribute("LB"), index.(x))
1.5

julia> MOI.get.(backend(model), Gurobi.ConstraintAttribute("RHS"), index.(c))
9.0

I wonder if JuMP needs a get_attribute(model, args...; ignore_optimize_not_called = true) method.

@odow odow changed the title Querying Gurobi attributes returns OptimizeNotCalled() error Attributes set by optimizer without calling optimize Dec 15, 2023
@odow odow changed the title Attributes set by optimizer without calling optimize Get attributes set by optimizer without calling optimize Dec 15, 2023
@odow
Copy link
Member

odow commented Dec 15, 2023

The change in Gurobi was part of v1.0.0, so it was part of a breaking release and so technically okay. I've thought of a few options but nothing seems like an obviously good idea.

Maybe we just need to update https://github.com/jump-dev/Gurobi.jl#accessing-gurobi-specific-attributes

@rezabayani
Copy link
Author

So you can force this with:
julia> MOI.get(backend(model), Gurobi.ModelAttribute("IsMIP"))
0
julia> MOI.get.(backend(model), Gurobi.VariableAttribute("LB"), index.(x))
1.5
julia> MOI.get.(backend(model), Gurobi.ConstraintAttribute("RHS"), index.(c))
9.0

Thanks, Oscar, this is what I was looking for.

@odow
Copy link
Member

odow commented Dec 15, 2023

I've updated the Gurobi docs: jump-dev/Gurobi.jl#539. Not sure what we should do here.

@blegat
Copy link
Member

blegat commented Dec 16, 2023

We don't have a way to say which attributes were set by the solver and require optimize!, and which are defined by the optimizer without needing optimize!.

Can you elaborate? Can't we say that the ones of this issue don't need optimize ?

@odow
Copy link
Member

odow commented Dec 16, 2023

We know. But MOI doesn't have a MOI.is_set_by_optimizer_but_doesnt_need_optimize function.

@blegat
Copy link
Member

blegat commented Dec 16, 2023

I see, maybe is_set_by_optimize is really CachingOptimizer should forward it to the optimizer and we shouldn't have done the JuMP's check that prevents getting it when the status is OPTIMIZE_NOT_CALLED. The right approach for that was to have a custom error defined in MOI that is thrown by MOI wrappers when appropriate.

@odow
Copy link
Member

odow commented Dec 18, 2023

Agreed we could change for MOI 2.0. Anything non-breaking in the near term?

Maybe we do need

I wonder if JuMP needs a get_attribute(model, args...; ignore_optimize_not_called = true) method.

@blegat
Copy link
Member

blegat commented Dec 19, 2023

Agreed we could change for MOI 2.0

Does that need any change at the MOI level ? Also, it doesn't seem breaking to me. Only current JuMP code that error will instead work.

@odow
Copy link
Member

odow commented Dec 19, 2023

I was thinking it would be breaking to change from is_set_by_optimize to the new method.

@blegat
Copy link
Member

blegat commented Dec 20, 2023

I would be the same method. Renaming it can be in MOI 2.0

@odow
Copy link
Member

odow commented Dec 20, 2023

I don't really understand what you're suggesting. That JuMP should just stop throwing the error and forward to the solver? But then it might start returning incorrect results.

@blegat
Copy link
Member

blegat commented Dec 21, 2023

That JuMP should just stop throwing the error and forward to the solver? But then it might start returning incorrect results.

Yes but that's not breaking because it's already the case if the MOI wrapper is used without JuMP. This JuMP errors just makes existing bugs be more rare (because they don't occur when the solver is used from JuMP) instead of shedding light on them with MOI tests and then fixing them.

See also our discussing in #2709 (comment)

@odow
Copy link
Member

odow commented Dec 21, 2023

Yes but that's not breaking because it's already the case if the MOI wrapper is used without JuMP

It is, because we decided that at the JuMP level we should prevent this entire class of bugs and throw and error. But using the MOI wrapper is for experts, and they can use it with care. Let me find the discussion. I remember writing it all up somewhere...

@odow
Copy link
Member

odow commented Dec 21, 2023

See #1739, #1766 but this isn't what I'm thinking of.

I think we decided that:

  • At the solver level, some solvers store solutions and allow querying them even if you have modified (some of) the model.
  • We don't want to enforce an OptimizeNotCalled error at the MOI level because experts may want the low-level solver behavior
  • At the JuMP level, accidentally querying old solutions or without calling optimize! seems like a very common source of bugs, particularly for new users
  • To support users, JuMP added a readable error message

I regard changing the error message to the solver-default as "breaking" because it is likely to lead to confusion for users. And people may be relying on errors being thrown. Silently starting to return incorrect solutions (even if the user calls value without calling optimize! in error) seems like a bad direction to move in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
3 participants