Skip to content

Conversation

@michaelbynum
Copy link
Contributor

@michaelbynum michaelbynum commented Aug 12, 2025

Summary/Motivation:

This PR refactors the gurobi interfaces to achieve two goals:

  • reduce duplicate code
  • use the new contrib.observer package

Changes proposed in this PR:

  • create a directory for gurobi now that there are 4 files
  • create a base class for all of the interfaces that use gurobipy, making the direct and persistent interfaces more consistent (e.g., tc_map, postsolve, solve, mipstart)
  • modify the persistent interface to use the contrib.observer package
  • add mipstart functionality to gurobi direct and gurobi minlp
  • add a minimum gurobipy version to the direct interface (addMConstr was not added until version 9)
  • simplified some of the persistent interface

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

@codecov
Copy link

codecov bot commented Aug 13, 2025

Codecov Report

❌ Patch coverage is 90.82840% with 93 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.20%. Comparing base (13facca) to head (2ab061a).

Files with missing lines Patch % Lines
...contrib/solver/solvers/gurobi/gurobi_persistent.py 89.26% 83 Missing ⚠️
...ontrib/solver/solvers/gurobi/gurobi_direct_base.py 94.44% 8 Missing ⚠️
...omo/contrib/solver/solvers/gurobi/gurobi_direct.py 97.05% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3698      +/-   ##
==========================================
- Coverage   89.40%   86.20%   -3.21%     
==========================================
  Files         909      911       +2     
  Lines      105541   105552      +11     
==========================================
- Hits        94364    90996    -3368     
- Misses      11177    14556    +3379     
Flag Coverage Δ
builders 29.14% <22.09%> (+0.03%) ⬆️
default 86.04% <90.82%> (?)
expensive 35.79% <22.09%> (?)
linux ?
linux_other ?
osx ?
win ?
win_other ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@michaelbynum michaelbynum mentioned this pull request Nov 12, 2025
@michaelbynum michaelbynum changed the title [Depends on #3695] Observer gurobi refactor Observer gurobi refactor Nov 12, 2025
@michaelbynum michaelbynum requested a review from emma58 November 12, 2025 17:07
Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments and questions. I haven't gotten through the persistent interface yet. I have lots of design questions there -- it might be helpful to discuss offline / at a dev call.

Comment on lines +1 to +3
from .gurobi_direct import GurobiDirect
from .gurobi_persistent import GurobiPersistent
from .gurobi_direct_minlp import GurobiDirectMINLP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have had issues with relative imports; it would probably be best to convert these to absolute imports...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I think this has to be a relative import. This is in the __init__.py file.

GurobiDirectBase._gurobipy_env = None

@staticmethod
def env():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should provide a mechanism for users to construct their own Env and pass it to us. This is necessary for thing like working with a trial license.

Suggested change
def env():
def env(env=None):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

Comment on lines +56 to +57
self._gurobi_vars = None
self._pyomo_vars = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these being stored on the solver interface? Direct is supposed to be non-persistent, and adding this seems to violate that principle.

This seems to only be needed for _pyomo_gurobi_var_iter(), which is in turn only needed by _mipstart. I propose that both the attributes and _pyomo_gurobi)var_iter() be removed, and instead _mipstart be updated to accept the SolutionLoader as an argument. As the SolutionLoader is responsible for remembering the mapping between Pyomo and Gurobi, that feels like the natural place to query to get the mapping within _mipstart.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that suggestion. Will do.

Copy link
Contributor

@emma58 emma58 Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this going to work for callbacks too? They have the same issue, that they need this state (and actually more) to work... So I think there may be a bigger design question lurking here: For example, they need the gurobipy model to be somewhere they can get to: In persistent, that is currently also sitting on the solver interface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I was originally thinking that this kind of state could be added in a contextmanager kind of style, and removed before solve is over, regardless of what happens. But that certainly might not be the right answer.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent points, @emma58. I think this is worth further discussion.

)


def _load_suboptimal_mip_solution(solver_model, var_map, vars_to_load, solution_number):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this (and other loaders) be updated so that instead of var_map, it accepts an iterable of (pyomo_var, gurobi_var) tuples? That way interfaces that do not generate the mapping can still make use of this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have to iterate over it twice...

Comment on lines +93 to +98
original_solution_number = solver_model.getParamInfo('SolutionNumber')[2]
solver_model.setParam('SolutionNumber', solution_number)
gurobi_vars_to_load = [var_map[v] for v in vars_to_load]
vals = solver_model.getAttr("Xn", gurobi_vars_to_load)
res = ComponentMap(zip(vars_to_load, vals))
solver_model.setParam('SolutionNumber', original_solution_number)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These implementations all seem to be geared around "partial loads", and are actually quite inefficient for "full loads". As I would expect that the vast majority of users / use cases will actually be doing a full load, I would suggest redesigning these methods so that full loads are efficient, and partial loads are efficient if the number of objects to load is small.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What makes you say this implementation is inefficient?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least in the case of the direct interface, we already have the list of gurobi vars and pyomo vars in the order in which they were given to Gurobi, so if you are getting everything you could get away with:

return ComponentMap(zip(pyomo_vars, solver_model.getAttr('Xn')))

and avoid iterating over things twice, and forming an extra list, and performing N COmponentMap lookups (plus unknown lookups within Gurobi).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me do some profiling and see if this even matters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good model to use in profiling is @emma58's 400M var LP.

Comment on lines +57 to +58
def invalidate(self):
self._valid = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't the Direct interface also need to worry about invalidating the last loader before the next solve? Does this really need to be a specialization for the persistent interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direct interface solution loader should still be valid after subsequent solves, I think???

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

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

5 participants