`empty!` for JuMP.Model or JuMP.direct_model?

I did not found a way of emptying a model (created with either JuMP.model or JuMP.direct_model).

There is a MathOptInterface.empty!, but it seems I need to call it over backend(model) and after I do that, I cannot add a variable with the same name again because I did not delete the variables in the JuMP wrapper (I just desynched the internal ModelLike object from its JuMP wrapper).

Is this by design, or just nobody implemented an empty! method fro JuMP models? I have many methods that take a JuMP.Model object and the surrounding code expect the changes to be done to that object, but if the object enters a state that is not able to reset it to its initial state then I will have to redesign a lot of things.

I tried deleting all variables and constraints, but even if all variables are deleted, the model ‘remembers’ an array of variables with name x and does not allow defining it again. I cannot use anonymous variables, or allow this name to change, without breaking about the same amount of code (that queries the model for that set of variables).

It sounds like what you really want is to be passed an optimizer constructor.

i.e.,

# instead of
function foo(model)
    # do stuff with model
    empty!(model)
    # more stuff with model
end

# you want
function foo(optimizer)
    model = Model(optimizer)
    # do stuff with model
    model = Model(optimizer)
    # more stuff with model
end

This breaks as much code, I will need to return the model, and (to my knowledge) the default optimizers (GLPK.Optimizer, CPLEX.Optimizer) are non-pre-configured factories, and I pass a model exactly because its optimizer is already configured. It would be better to just pass an anonymous method that takes no arguments and just return a pre-configured model.

Is this something that should stay this way (i.e., is a design decision)? It was not done because it is hard (it would need to break other things to get this to work) or just because there was no demand for it? Could I could open a PR to add an empty! method?

My reasoning for why this is a interesting feature to have follows:

  1. The MathOptInterface already provides an empty! method for the backend.
  2. ‘Tainting’ an object in a way it cannot be reset makes some code hard to write, specially taking in account the following limitations.
  3. The direct_model does not support copy, and both the direct_model and the Model do not support deepcopy so you cannot make a safe copy of the object and taint this safe copy instead of the original (that is referred in other places).
  4. The Julia semantics of bindings does not allow something that is common in other languages (like C++), that is attributing a new value/object to a pointer/reference, effectively resetting the object (unless it has a const field), otherwise I could do empty!(backend(model)); model = direct_model(backend(model)) inside a method and update the references to it outside the method (keeping the optimizer configuration).

Have you considered using optimizier_with_attributes to pass around a pre-configured solver that is not attached to a specific model?

It hasn’t been done for a few reasons:

  • There are work-arounds, like passing optimizer_with_attributes
  • There are complexities: direct vs non-direct, general code complexity and maintenance
  • Little demand for it.

MOI.empty! has been there since the start, when we were still exploring the space. I’d be in favor of removing it now that we have OptimizerWithAttributes. But that is unnecessary code churn. A lot of the tests, for example, call MOI.empty! to start the test from a clean slate, but they should really be passed an OptimizerWithAttributes object that they can use to instantiate a new model.

It might be useful if you could elaborate on your use-case. What do you mean by “tainting,” and why would resetting a model help with this?

I had forgot about the optimizer_with_attributes, but it is not much different from having a anonymous (or not) function that builds and empty configured model being passed around. And I have already said why this would break code to me: (1) sometimes the model I pass is not empty, it is already partially constructed (the process I am reproducing build a model, fix some variables, solves part of it relaxed, solve it unrelaxed, remove some variables based on that, add new variables and solve the relaxation iteratively while adding variables, then delete more variables and finally gives a final model do be solved); (2) this spreads my decision of using Model or direct_model to all code, instead of keeping it centralized (but this is minor); (3) related to (1) there is the problem that I cannot store a reference to the model being built, because any new method could need to create a new model to follow with the building, so I need to change all my passing around of a model in a way that now every method always returns a model besides the values it already returned.

@odow The message above should clarify why this work-around is not very useful to me, it also helps to clarify my use case. Now, what I mean by tainting is the following: an object enters a state that cannot be reset to an original ‘empty’ state, i.e., if the object reach some state, other states I could reach from the original empty state become unreacheable; I lose the capacity of doing anything I want with the object as I modify (taint) it. This sometimes is solved by being able to create copies of the object before making such modifications, but this is not available here, nor is attributing a new value to such object in a way that is visible to code that already stored a reference of it. The specific tainting is: I created a variable with some name (that will be queried by code that stored a reference of the model before it had such name), I cannot have a method that takes this model, solve it partially, and use the partial information to re-build it (lets say the same method is used to build and re-build it, but it takes different parameters that affect how large/hard the model will be, the second time the method will error because there is already a vector of variables with that name, even if deleted all variables of that name before passing the model again to that building method, I ‘need’ to create a new object because the old one is tainted with no way of cleaning).

sometimes the model I pass is not empty, it is already partially constructed

How does being able to empty the model help in this case?

I need to change all my passing around of a model in a way that now every method always returns a model besides the values it already returned.

This seems like a reasonable solution.

I created a variable with some name

Create anonymous variables instead. Creating variables with specific names seems fragile. If you’re relying on being able to look up variables with model[:my_specifc_name], you can instead go

x = @variable(model)
model.ext[:my_specific_name] = x

It might be useful if you can point to specific code. I still don’t understand the problem that being able to call empty! resolves.

How does being able to empty the model help in this case?

It helps because I may need to use the half constructed model, but
depending on the method calculations, in some cases the model will be
expanded, and in others it may be necessary or easier rebuild it from
scratch (while outside code keep references to that model).

This seems like a reasonable solution.

JuMP having an empty! method also seems a reasonable solution to me.
In fact, implementing an empty! method and using it just in my code,
as seems like the maintenance cost of it is too high for JuMP, is also
a reasonable solution. I could study JuMP internals, implement this
solution with little effort and little breakage of my code, and as
JuMP is below version 1.0, the API can change as much as the internals
so the breakage with JuMP is the same.

Creating variables with specific names seems fragile.

… “seems fragile”? It is or it is not. Why it would be fragile? Why
JuMP provides such feature if it is not a good idea do access the
variables this way (or is some way of accessing them, or with some
purpose, more fragile than other)? The workaround would be to create a
model wrapper structure and put the anonymous variables in a vector
there, and why this would be less fragile than using a feature that
JuMP already provides and that is widely used? One of my labs
colleagues did exactly that because he did not know about accessing
variables by name, and the first thing he did when discovered it was
to remove from his code all those useless wrappers and code keeping
them synced with the JuMP model.

It might be useful if you can point to specific code. I still don’t understand the problem that being able to call empty! resolves.

Sorry, but I will give up there. I already put a lot of effort in this
thread, my code is not a MWE and I would need to take some time to
reduce it to one, and it is basically what I already described with
prose. I am tired, and seems clear to me that solving the problem by
myself would have been more productive than asking questions and
proposing modifications in JuMP.

We discussed this offline. I was wrong about the complexity of implementing it ourselves.

Here’s the function I think you’re after:

function empty!(model::Model)
    MOI.empty!(model.backend)
    empty!(model.shapes)
    empty!(model.bridge_types)
    model.nlp_data = nothing
    empty!(model.obj_dict)
    empty!(model.ext)
    return model
end

Feel free to make a PR with tests.

1 Like

Thanks, @odow. I will work on this PR and the one related to MathOptInterface#776 today.

I apologize for how rude I was in my last post. After my last contribution to JuMP (vector deletion) I did not expect this resistance to something that (in my sight) was much simpler and a nice feature to have; so I ended up frustrated that I did spend time explaining a feature which seemed nobody else was interested or even understood the purpose, instead of just implementing it and not wasting mine and JuMP developers’ time.

1 Like

I’m sorry you’ve had multiple frustrating experiences with JuMP. It’s definitely something we don’t want to have happen. JuMP wouldn’t exist without volunteers like yourself.

If you make the PR, I’ll work to make sure it gets merged expeditiously.

2 Likes

Just for the people visiting this thread in the future, this feature was implemented by this PR and will probably will be available in the first JuMP version after 0.21.1.

1 Like