-
Notifications
You must be signed in to change notification settings - Fork 589
Add Xpress direct and persistent solver interfaces #3987
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
Open
XPRSc4v4
wants to merge
18
commits into
Pyomo:main
Choose a base branch
from
XPRSc4v4:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
d5d97a4
Merge branch 'main' of https://github.com/Pyomo/pyomo
XPRSc4v4 c9f9934
Merge remote-tracking branch 'upstream/main'
XPRSc4v4 2b56a8c
Merge branch 'main' of https://github.com/Pyomo/pyomo
XPRSc4v4 d0d5a73
Txp 8774 nlp (#3)
XPRSc4v4 732665f
Merge upstream/main: add SCIP solver, NLP log test precision fix
XPRSc4v4 38f73d3
Formatting last few additions that were left off
XPRSc4v4 fbe1a83
Import was happening unconditionally
mrmundt 5c09bed
Should import numpy from deps
mrmundt de8fab1
Remove hard numpy dep
mrmundt bc2fd1a
Merge branch 'main' of https://github.com/Pyomo/pyomo
XPRSc4v4 3e3e0ad
Merge branch 'main' of https://github.com/XPRSc4v4/pyomo
XPRSc4v4 e6f685d
Xpress Community License detection skip unsupported feature test
XPRSc4v4 b85b68e
Remove dataclass import
XPRSc4v4 93c1e74
Merge branch 'main' into main
mrmundt c79ca68
Merge branch 'main' of https://github.com/Pyomo/pyomo
XPRSc4v4 ca9d501
Merge branch 'main' of https://github.com/Pyomo/pyomo
XPRSc4v4 7b0c7e9
Addressing review comments:
XPRSc4v4 cd3fe7a
Merge branch 'main' of https://github.com/XPRSc4v4/pyomo
XPRSc4v4 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,4 +8,5 @@ Solver Interfaces | |
| cplex_persistent.rst | ||
| gurobi_direct.rst | ||
| gurobi_persistent.rst | ||
| xpress.rst | ||
| xpress_persistent.rst | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| Xpress (New Interface) | ||
| ====================== | ||
|
|
||
| .. currentmodule:: pyomo.contrib.solver.solvers.xpress | ||
|
|
||
| Pyomo provides two solver interfaces to the FICO Xpress solver: | ||
| :class:`XpressDirect` for one-shot solves, and :class:`XpressPersistent` | ||
| for workflows that solve a model repeatedly with small modifications | ||
| between solves. | ||
|
|
||
| Both interfaces support the complete range of problem classes that Xpress | ||
| handles: LP, MIP, QP, MIQP, NLP, MINLP, second-order cone programs, | ||
| and SOS Type 1 and 2 constraints. | ||
|
|
||
| Expression Walker | ||
| ----------------- | ||
|
|
||
| :class:`XpressDirect` uses a custom expression walker that translates the | ||
| full Pyomo expression tree (linear, quadratic, or nonlinear) directly | ||
| into an equivalent Xpress expression object, avoiding further intermediate | ||
| Python transformations, and handing off to the Xpress C library as directly | ||
| as possible. Quadratic terms arising from Cartesian-product expansions are | ||
| expanded on the C side. The result is a lean, single-path translation | ||
| with no additional overhead for more complex expression types. | ||
|
|
||
| :class:`XpressPersistent` takes a slightly different approach. | ||
| Pyomo's ``generate_standard_repn`` runs first: it decomposes each | ||
| constraint into its linear and quadratic parts and, crucially, provides | ||
| symbolic (non-evaluated) coefficients that are used to register the | ||
| mutable-parameter update helpers driving the targeted ``chgMCoef`` / | ||
| ``chgRHS`` / ``chgQRowCoeff`` calls between solves. If a nonlinear | ||
| subexpression remains after that decomposition, the same walker handles | ||
| it, producing an Xpress nonlinear expression. | ||
|
|
||
| XpressDirect | ||
| ------------ | ||
|
|
||
| :class:`XpressDirect` builds a fresh Xpress problem from the Pyomo model | ||
| on every call to :meth:`~XpressDirect.solve`. Use it for one-shot solves | ||
| or exploratory modeling. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from pyomo.contrib.solver.solvers.xpress import XpressDirect | ||
| import pyomo.environ as pyo | ||
|
|
||
| m = pyo.ConcreteModel() | ||
| m.x = pyo.Var(bounds=(0, 10)) | ||
| m.c = pyo.Constraint(expr=m.x >= 3) | ||
| m.obj = pyo.Objective(expr=m.x) | ||
|
|
||
| res = XpressDirect().solve(m) | ||
|
|
||
| XpressPersistent | ||
| ---------------- | ||
|
|
||
| :class:`XpressPersistent` keeps the Xpress problem in memory between | ||
| solves and uses Pyomo's model-change notification framework to apply | ||
| only the minimal set of solver API calls required to reflect each change. | ||
|
|
||
| Mutable :class:`~pyomo.environ.Param` components are tracked | ||
| automatically. Updating a parameter value before the next | ||
| :meth:`~XpressPersistent.solve` call triggers targeted coefficient or | ||
| bound updates (``chgMCoef``, ``chgRHS``, ``chgQRowCoeff``) rather than a | ||
| full model rebuild. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from pyomo.contrib.solver.solvers.xpress import XpressPersistent | ||
| import pyomo.environ as pyo | ||
|
|
||
| m = pyo.ConcreteModel() | ||
| m.cost = pyo.Param(mutable=True, initialize=2.0) | ||
| m.x = pyo.Var(bounds=(0, 10)) | ||
| m.c = pyo.Constraint(expr=m.x >= 3) | ||
| m.obj = pyo.Objective(expr=m.cost * m.x) | ||
|
|
||
| opt = XpressPersistent() | ||
| opt.solve(m) # full build | ||
| m.cost.set_value(5.0) | ||
| opt.solve(m) # incremental: only the objective coefficient is updated | ||
|
|
||
| Incremental operations | ||
| ^^^^^^^^^^^^^^^^^^^^^^ | ||
|
|
||
| Between solves the persistent interface supports: | ||
|
|
||
| - **LP/QP coefficient and bound updates** without row removal, using | ||
| the Xpress ``chgMCoef`` / ``chgRHS`` / ``chgQRowCoeff`` API. | ||
| - **NLP constraint updates** via row removal and re-insertion (required | ||
| when the nonlinear structure changes). | ||
| - **Variable fixing and unfixing** through bound updates only. No | ||
| constraints are removed or rebuilt as a result of fixing; Xpress | ||
| folds fixed variables natively during the solve. Fixing all integer | ||
| variables in a MINLP therefore reduces to a sequence of bound calls, | ||
| after which Xpress can treat the problem as continuous without any | ||
| structural modification to the Pyomo model. | ||
| - **Structural modifications**: add and remove constraints, variables, | ||
| SOS constraints, and sub-blocks. | ||
|
|
||
| Configuration | ||
| ------------- | ||
|
|
||
| Common configuration options (time limits, thread count, MIP gaps, | ||
| symbolic labels, raw solver options, etc.) are documented on | ||
| :class:`~pyomo.contrib.solver.common.config.BranchAndBoundConfig`, which | ||
| both interfaces accept as keyword arguments to :meth:`~XpressDirect.solve`. | ||
|
|
||
| Xpress-specific options: | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
| :widths: 25 75 | ||
|
|
||
| * - Option | ||
| - Description | ||
| * - ``warmstart`` | ||
| - Pass variable values as a MIP start hint (default ``True``). | ||
| * - ``pool_solutions`` | ||
| - Collect multiple MIP solutions during branch-and-bound. | ||
| ``N > 0``: keep a rolling window of the last ``N`` solutions | ||
| found. | ||
|
|
||
| Any Xpress control name accepted by ``prob.controls.<name>`` can be passed: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| res = opt.solve(m, | ||
| solver_options={ | ||
| 'outputlog': 0, # suppress solver output | ||
| 'maxnode': 500, # B&B node limit | ||
|
XPRSc4v4 marked this conversation as resolved.
|
||
| 'feastol': 1e-8, # primal feasibility tolerance | ||
| } | ||
| ) | ||
|
|
||
| Results | ||
|
XPRSc4v4 marked this conversation as resolved.
|
||
| ------- | ||
|
|
||
| Every :meth:`~XpressDirect.solve` call returns a | ||
| :class:`~pyomo.contrib.solver.common.results.Results` object: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| res = opt.solve(m) | ||
| print(res.termination_condition) # e.g. convergenceCriteriaSatisfied | ||
| print(res.solution_status) # e.g. optimal | ||
| print(res.incumbent_objective) # objective value at the best solution | ||
|
|
||
| See :class:`~pyomo.contrib.solver.common.results.Results` for the full set | ||
| of attributes. For NLP problems solved via Xpress SLP, | ||
| :attr:`~pyomo.contrib.solver.common.results.Results.solution_status` will | ||
| be ``feasible`` rather than ``optimal``, reflecting the local convergence | ||
| nature of the algorithm. | ||
|
|
||
| Solution Pool | ||
| ------------- | ||
|
|
||
| :class:`XpressDirect` and :class:`XpressPersistent` can collect multiple | ||
| feasible MIP solutions found during branch-and-bound via the | ||
| ``pool_solutions`` configuration option. Setting ``pool_solutions=N`` | ||
| (N > 0) keeps a rolling window of the last ``N`` solutions found: once | ||
| the window is full, the oldest entry is evicted on each new solution, | ||
| so the pool always contains the N most recently discovered feasible | ||
| solutions. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| res = opt.solve(m, pool_solutions=5) | ||
| loader = res.solution_loader | ||
| print(loader.get_number_of_solutions()) # up to 6 (incumbent + pool) | ||
|
|
||
| # Load the incumbent (solution 0) into the model | ||
| loader.solution(0).load_vars() | ||
|
|
||
| # Inspect pool entry 1 without modifying the model permanently | ||
| with loader.solution(1): | ||
| loader.load_vars() | ||
| print(m.x.value) | ||
| # After the with-block the active solution reverts to the incumbent | ||
|
|
||
| NLP and Nonlinear Expressions | ||
| ------------------------------ | ||
|
|
||
| All standard Pyomo nonlinear operators, trigonometric and hyperbolic | ||
| functions, and user-defined Python callback functions | ||
| (``pyo.ExternalFunction``) are supported. | ||
|
|
||
| ``pyo.floor`` and ``pyo.ceil`` are not currently supported and raise | ||
| :class:`~pyomo.contrib.solver.common.util.IncompatibleModelError`. | ||
| These operations must be reformulated by introducing an auxiliary integer | ||
| variable together with two linear inequality constraints that encode the | ||
| floor or ceil relationship. Adding an integer variable to a continuous | ||
| NLP produces a MINLP. | ||
|
|
||
| Testing | ||
| ------- | ||
|
|
||
| The interface ships with a test suite covering LP, MIP, QP, QCP, NLP, | ||
| MINLP, SOS, mutable parameter tracking, incremental structural updates, | ||
| and the solution pool. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # ____________________________________________________________________________________ | ||
| # | ||
| # Pyomo: Python Optimization Modeling Objects | ||
| # Copyright (c) 2008-2026 National Technology and Engineering Solutions of Sandia, LLC | ||
| # Under the terms of Contract DE-NA0003525 with National Technology and Engineering | ||
| # Solutions of Sandia, LLC, the U.S. Government retains certain rights in this | ||
| # software. This software is distributed under the 3-clause BSD License. | ||
| # ____________________________________________________________________________________ | ||
|
|
||
| from pyomo.contrib.solver.solvers.xpress.xpress_direct import XpressDirect | ||
| from pyomo.contrib.solver.solvers.xpress.xpress_persistent import XpressPersistent |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.