[JuMP + Gurobi Bug?] value of variable in solved model changes after deleting another variable

> using JuMP, Gurobi
> model = Model(with_optimizer(Gurobi.Optimizer));
> @variable(model, x[1:3], Bin);
> @constraint(model, sum(all_variables(model)) <= 2);
> @objective(model, Max, sum(model[:x] .* [10, 5, 2]));
> optimize!(model);
> value.(x)
3-element Array{Float64,1}:
 1.0
 1.0
 0.0
> is_valid.(model, x)
3-element BitArray{1}:
 true
 true
 true
> delete(model, x[2]);
> is_valid.(model, x)
3-element BitArray{1}:
  true
 false
  true
> value(x[1])
1.0
> value(x[3])
1.0

In other words, if I delete a variable from an already solved model (x[2] in this case), I change the value of another (x[3] from zero to one).

If I use GLPK as the solver then the behaviour is the one I expected, this is, the variables I did not delete keep their values unchanged.

I do not know if querying the values of variables from a solved-and-then-modified model is undefined behaviour, or one of the implementations (GLPK layer or Gurobi layer) is correct and the other is not. So I cannot be sure if this is a bug.

If someone could point me to some official statement on which should happen in such cases, or a developer confirm this is a bug, I would be thankful. I can work around the problem, but makes the code a little uglier and harder to maintain (I cannot use GLPK for my real use case). If it is a bug I would prefer to contribute to fixing it in the source if possible.

I am using:
[4076af6c] JuMP v0.20.0
[2e9cd046] Gurobi v0.7.2

If you need any more info just ask.

This is because of Gurobi’s lazy updating semantics. Essentially, when you call delete, it caches the deletion so that calls to value return the old solution. In this case, JuMP updates the column index of x[3], but Gurobi doesn’t invalidate the stored solution or modify it because of. the lazy update.

In general, querying the solution of a solved-then-modified model is undefined behavior. This should be clarified in the JuMP docs. (A pull request would be most helpful.) I think this had been discussed before; I’ll try to dredge up the discussion.

We can’t support querying of solved-then-modified models because this would require the wrappers to cache the entire solution. On the flip-side, enforcing all solvers to discard the solution is probably an equally large amount of code. Hence the undefined nature.

Edit: here’s the last time I came across this, no wonder you didn’t find it :slight_smile:, and all the more reason for JuMP docs.

Thanks for the fast answer @odow.

I suppose I may deep copy the model, and pay some run time cost, or make my code a little uglier and work around that. I called deepcopy and it seem to work, it has some limitation? it is not guaranteed to be implemented for every solver? (i.e., JuMP model cannot guarantee that it can implement deepcopy?)

I am not sure I would be the best person to make such pull request, I am learning of this now. But if you think its the best you can throw me the git link and some general idea what you want. I will give it a try.

In general, you should not call deepcopy on a model. That is because it copies the pointer to the Gurobi C model, rather than copying the underlying C model. Search past discourse issues for a full explanation.

The work-around you should use is to cache the solution yourself:

model = Model()
@variable(model, x[1:3])
optimize!(model)
x_val = Dict(
    xi => value(xi) for xi in x
)

delete!(model, x[2])
delete!(x_val, x[2])

@show x_val[x[3]]

The link to edit the docs is here: https://github.com/JuliaOpt/JuMP.jl/blob/master/docs/src/solutions.md#obtaining-solutions
Let me know if you need any help, or JuMP on Gitter: https://gitter.im/JuliaOpt/JuMP-dev

Oh… (I have now read the previous discourse posts.)

In this case, would not be better to not implement deepcopy at all but
just copy? Seems counter-intuitive that deepcopy is shallow (maybe it
is deep for the Julia structure, but semantically it is not really
deep).
Other options would be: allow copy while there is not a pointer yet,
and fail if there is; or, I don’t know, create a new pointer? I
suppose a new Gurobi C model can be created as the first one was, no?

If you think some of these solutions is reasonable, I can give them a try too.

Copying models is supported: https://github.com/JuliaOpt/JuMP.jl/blob/04735d2648c5fbcf388b38f7f7d860fb474b772b/src/copy.jl#L38-L139
but it looks like it needs to be added to the JuMP docs (an oversight).

deepcopying is not supported, and will not be supported, because of the issues with the underlying solver.

Given your original post, it’s not obvious why you need to copy the model? Isn’t the work-around of caching the solution sufficient?

Oh, no, I can work around it. I am thinking of people that will end up
with the same problem in the future. If there is no plan to support
deepcopy why do not specialize deepcopy to show an error message and
abort?

As it is just a stub, I can write it and make a pull request for it
too. I suppose that the better is to specialize it for the concrete
class and not the AbstractModel, as someone may want to implement a
model that supports deepcopy.

I will submit a change to the documentation soon. I just don’t know if
it is not kinda long considering the size of the issue. Feel free to
say to me rewrite it.

why do not specialize deepcopy to show an error message and
abort?

Sure! Make a pull request implementing Base.deepcopy(::AbstractModel)

I just don’t know if
it is not kinda long considering the size of the issue.

It just needs to be

!!! warn
    Accessing the solution of a model after modification (e.g., adding, 
    deleting, or modifying a constraint) is undefined behavior.