Skip to content

Conversation

emma58
Copy link
Contributor

@emma58 emma58 commented Oct 6, 2025

Fixes #3522 (except for the request to have a persistent MINLP interface to Gurobi)

Summary/Motivation:

This adds a new solver interface gurobi_direct_minlp that supports sending MINLP problems to Gurobi via the Python API. currently this is implemented as a separate Gurobi direct interface in contrib.solver so that we don't have negative performance implications for gurobi_direct_v2 (and to punt for the moment on integrating the matrix-vector inferface with the general MINLP one).

Changes proposed in this PR:

  • Adds a walker that makes the (required, in Gurobi 12) conversion from general nonlinear constraints to a mixture of aux_var == general_nonlinear_expr equalities and linear/quadratic constraints involving the auxiliary and other variables.
  • Implements a pretty minimalistic writer that uses the above walker: The special thing here is that some Pyomo constraints will create more than one constraint in the gurobi model because of the transformation mentioned in the bullet point above.
  • Implements a new solver interface that inherits from GurobiDirect in contrib.solver: The only thing special here is the solve function which is again just unique because of the custom walker/writer explained above.
  • Gets the test coverage to 89%, which rounds to 90%... The only thing interesting here is that I added a method for converting general nonlinear Gurobi expression trees back to Pyomo expressions so that we can test them that way.

Outstanding TODOs, none of which should prevent merging unless we want them to:

  • Move all the tests that manually test Gurobi's expression tree onto the gurobi_nl_to_pyo_expr function, which will make them a lot more readable, and easier to maintain if every there is a change to the public API in gurobipy.
  • Add some tests for the sake of coverage--we're missing some cases

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.

emma58 and others added 30 commits January 7, 2025 13:48
… from Gurobi's perspective. Starting to write the gurobi walker modeled after the sympy one
…on visitor, no real tests to speak of yet though
…ngs like AbsExpression, we can't rely completely on the expression evaluation visitor, and I need to know the types of expressions. Starting to build out tests of the walker.
@emma58
Copy link
Contributor Author

emma58 commented Oct 15, 2025

@jsiirola @mrmundt If the tests pass, I really am done messing with this now, I think!

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.

A couple minor changes, and this should be good to go!

raise NoSolutionError()

iterator = zip(self._pyo_vars, self._grb_vars.x.tolist())
iterator = zip(self._pyo_vars, map(operator.attrgetter('x'), self._grb_vars))
Copy link
Member

Choose a reason for hiding this comment

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

Is fetching the values one at a time slower than getting them in a single vector? (Same question applies to get_primals, get_duals. and get_redued_costs below)

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 don't know? If it is, this is much harder to generalize because the Gurobi MINLP version doesn't have the gurobi vars as a vector--they're already a list.

gurobi_model, A, x, repn.rows, repn.columns, repn.objectives
gurobi_model,
A,
x.tolist(),
Copy link
Member

Choose a reason for hiding this comment

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

Why convert x to a list?

Copy link
Contributor Author

@emma58 emma58 Oct 17, 2025

Choose a reason for hiding this comment

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

Because of the thing you commented on above, actually. I'm making this look the same as Gurobi MINLP so that they can use an identical SolutionLoader. Gurobi MINLP doesn't have the luxury of declaring the variables as a vector (I don't think? Although if I can dynamically add to one, that's another option, maybe?), so they're a list at this point right now.

gurobi_model,
A,
x.tolist(),
list(map(operator.itemgetter(0), repn.rows)),
Copy link
Member

Choose a reason for hiding this comment

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

Why de-tupalize rows? aren't both values needed by the loader?

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 don't think so--the only time we need anything from this at all is to get the duals. And then you have to get the Pi attribute off each constraint. The difference is that was vectorized before too...

@blnicho blnicho moved this from Todo to Review In Progress in Pyomo 6.9.5 Release Oct 17, 2025
@@ -0,0 +1,78 @@
from pyomo.common.dependencies import attempt_import
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs copyright.

@@ -0,0 +1,226 @@
import math
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs copyright.

# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong copyright year.

# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong copyright year.

# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong copyright year.

@mrmundt
Copy link
Contributor

mrmundt commented Oct 17, 2025

I am not pushing the copyright changes yet - waiting for tests to finish since all the copyright stuff is NFC.

@mrmundt
Copy link
Contributor

mrmundt commented Oct 17, 2025

But I DO have the changes ready to push as soon as the tests finish.

x1 = visitor.var_map[id(m.x1)]
x2 = visitor.var_map[id(m.x2)]

# Also linear, whoot!
Copy link
Contributor

Choose a reason for hiding this comment

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

Not changing but this is an abomination of a word. Woot? Yes. Hoot? Yes. Whoo? Yes. But WHOOT???

@github-project-automation github-project-automation bot moved this from Review In Progress to Reviewer Approved in Pyomo 6.9.5 Release Oct 17, 2025
@mrmundt mrmundt merged commit 590ecfd into Pyomo:main Oct 17, 2025
1 check was pending
@github-project-automation github-project-automation bot moved this from Reviewer Approved to Done in Pyomo 6.9.5 Release Oct 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Support Gurobi nonlinear expressions in the direct/persistent solvers

4 participants