Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
9ab1283
Remove deprecation warnings (#1)
XPRSc4v4 Dec 4, 2025
13ee044
Merge branch 'main' into main
XPRSc4v4 Dec 5, 2025
d694eb2
Merge branch 'main' of https://github.com/Pyomo/pyomo
XPRSc4v4 Dec 17, 2025
3675141
Merge branch 'main' of https://github.com/XPRSc4v4/pyomo
XPRSc4v4 Dec 17, 2025
1e1cd59
Removed very old version handling + added workflow test for 9.3
XPRSc4v4 Dec 17, 2025
f78123b
Workflow fix
XPRSc4v4 Dec 19, 2025
427f306
Merge branch 'main' into main
blnicho Jan 13, 2026
0236606
Merge branch 'main' into main
blnicho Jan 19, 2026
8878a96
Merge branch 'main' into main
blnicho Jan 22, 2026
1225942
Change GHA logic for testing old Xpress version
blnicho Jan 23, 2026
d0d9b41
Add 'intel' channel to conda configuration
blnicho Jan 23, 2026
ca192fe
Adjust which GHA jobs test older Xpress version
blnicho Jan 23, 2026
b36bace
Merge branch 'main' into main
mrmundt Jan 27, 2026
72a1258
Adjust which GHA jobs test old Xpress version
blnicho Feb 3, 2026
91b8daf
Merge branch 'main' into main
blnicho Feb 3, 2026
b69ebec
add warm start to KNITRO direct.
eminyouskn Feb 4, 2026
3a7ba32
enhace test case
eminyouskn Feb 4, 2026
55aa870
enhace warm_start
eminyouskn Feb 4, 2026
eb2c952
enhace tests.
eminyouskn Feb 4, 2026
3fca4fd
fix test.
eminyouskn Feb 4, 2026
ab8b870
black
eminyouskn Feb 4, 2026
6c569a7
Merge branch 'main' into knitro-warm-start
eminyouskn Feb 4, 2026
d9a3300
Merge branch 'main' into main
jsiirola Feb 4, 2026
2121e59
Revert changes to GHA workflow
blnicho Feb 4, 2026
98efd0f
Merge branch 'main' into knitro-warm-start
eminyouskn Feb 10, 2026
35575a6
Merge remote-tracking branch 'main' into knitro-warm-start
eminyouskn Feb 10, 2026
58d8928
Update solver tests for gurobiasl 13.0.0
jsiirola Feb 17, 2026
cce2e7b
Resolve degeneracy in solver test
jsiirola Feb 17, 2026
5077e32
NFC: Fix typo in test_solvers.py
blnicho Feb 17, 2026
2e496df
Merge branch 'main' into knitro-warm-start
eminyouskn Feb 17, 2026
080c96a
Rework pytest solver/writer marker handling; remove unised markers
jsiirola Feb 17, 2026
51be8cf
Add solver marks to tests
jsiirola Feb 17, 2026
c79b6da
NFC: apply black
jsiirola Feb 17, 2026
8d9be0f
Mark parameterized solver tests in contrib.solver
jsiirola Feb 17, 2026
a6ae027
Fix decorator typo
jsiirola Feb 18, 2026
4b52e95
Fix test typo
jsiirola Feb 18, 2026
1a01cc7
NFC: update pymumps license URL
jsiirola Feb 18, 2026
9da35cb
Merge pull request #3850 from jsiirola/gurobiasl-test
jsiirola Feb 18, 2026
f2aa109
Merge branch 'main' into url-pymumps
jsiirola Feb 18, 2026
7a74cf6
Merge branch 'main' into pytest-solver-test-filter
jsiirola Feb 18, 2026
5852a20
Merge pull request #3853 from jsiirola/url-pymumps
mrmundt Feb 18, 2026
def8d98
Merge branch 'main' into pytest-solver-test-filter
jsiirola Feb 18, 2026
cbb765f
Rework to remove pytest_runtest_setup callback
jsiirola Feb 18, 2026
45b6a5f
NFC: fix typo
jsiirola Feb 18, 2026
c7ff52c
Minor typo, fix language in contribution guide
mrmundt Feb 18, 2026
606b5bc
Clarify the differenence between id and vender
mrmundt Feb 18, 2026
38d0bb1
The pedants are arguing again
mrmundt Feb 18, 2026
c2ce299
NFC: fix another typo
jsiirola Feb 18, 2026
869e669
Merge branch 'main' into main
blnicho Feb 18, 2026
35424f2
Merge pull request #3854 from jsiirola/pytest-solver-test-filter
jsiirola Feb 18, 2026
837b614
Merge pull request #3794 from XPRSc4v4/main
blnicho Feb 18, 2026
199461d
add warm start to KNITRO direct.
eminyouskn Feb 4, 2026
44bfa4d
enhace test case
eminyouskn Feb 4, 2026
e7464c6
enhace warm_start
eminyouskn Feb 4, 2026
2fc2e19
enhace tests.
eminyouskn Feb 4, 2026
5fc9e92
fix test.
eminyouskn Feb 4, 2026
eed0223
black
eminyouskn Feb 4, 2026
7e887c6
Merge branch 'knitro-warm-start' of https://github.com/eminyouskn/pyo…
eminyouskn Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 81 additions & 54 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,76 +10,103 @@
import pytest

_implicit_markers = {'default'}
_extended_implicit_markers = _implicit_markers.union({'solver'})
_category_markers = {'solver', 'writer'}
_extended_implicit_markers = _implicit_markers.union(_category_markers)


def pytest_collection_modifyitems(items):
"""
This method will mark any unmarked tests with the implicit marker ('default')
def pytest_configure(config):
# If the user specified "--solver" or "--writer", then add that
# logic to the marker expression
markexpr = config.option.markexpr
for cat in _category_markers:
opt = config.getoption('--' + cat)
if opt:
if markexpr:
markexpr = f"({markexpr}) and "
markexpr += f"{cat}(id='{opt}')"
# If the user didn't specify a marker expression, then we will
# select all "default" tests.
if not markexpr:
markexpr = 'default'
config.option.markexpr = markexpr

"""
for item in items:
try:
next(item.iter_markers())
except StopIteration:
for marker in _implicit_markers:
item.add_marker(getattr(pytest.mark, marker))

def pytest_itemcollected(item):
"""Standardize all Pyomo test markers.

This callback ensures that all unmarked tests, along with all tests
that are only marked by category markers (e.g., "solver" or
"writer"), are also marked with the default (implicit) markers
(currently just "default").

About category markers
----------------------

We have historically supported "category markers"::

@pytest.mark.solver("highs")

Unfortunately, pytest doesn't allow for building marker
expressions (e.g., for "-m") based on the marker.args. We will
map the positional argument (for pytest.mark.solver and
pytest.mark.writer) to the keyword argument "id". This will allow
querying against specific solver interfaces in marker expressions
with::

solver(id='highs')

We will take this opportunity to also set a keyword argument for
the solver/writer "vendor" (defined as the id up to the first
underscore). This will allow running "all Gurobi tests"
(including, e.g., lp, direct, and persistent) with::

-m solver(vendor='gurobi')

As with all pytest markers, these can be combined into more complex
"marker expressions" using ``and``, ``or``, ``not``, and ``()``.

def pytest_runtest_setup(item):
"""
This method overrides pytest's default behavior for marked tests.

The logic below follows this flow:
1) Did the user ask for a specific solver using the '--solver' flag?
If so: Add skip statements to any test NOT labeled with the
requested solver category.
2) Did the user ask for a specific marker using the '-m' flag?
If so: Return to pytest's default behavior.
3) If the user requested no specific solver or marker, look at each
test for the following:
a) If unmarked, run the test
b) If marked with implicit_markers, run the test
c) If marked "solver" and NOT any explicit marker, run the test
OTHERWISE: Skip the test.
In other words - we want to run unmarked, implicit, and solver tests as
the default mode; but if solver tests are also marked with an explicit
category (e.g., "expensive"), we will skip them.
"""
solvernames = [mark.args[0] for mark in item.iter_markers(name="solver")]
solveroption = item.config.getoption("--solver")
markeroption = item.config.getoption("-m")
item_markers = set(mark.name for mark in item.iter_markers())
if solveroption:
if solveroption not in solvernames:
pytest.skip("SKIPPED: Test not marked {!r}".format(solveroption))
return
elif markeroption:
markers = list(item.iter_markers())
if not markers:
# No markers; add the implicit (default) markers
for marker in _implicit_markers:
item.add_marker(getattr(pytest.mark, marker))
return
elif item_markers:
if not _implicit_markers.issubset(item_markers) and not item_markers.issubset(
_extended_implicit_markers
):
pytest.skip('SKIPPED: Only running default, solver, and unmarked tests.')

marker_set = {mark.name for mark in markers}
# If the item is only marked by extended implicit markers (e.g.,
# solver and/or writer), then make sure it is also marked by all
# implicit markers (i.e., "default")
if marker_set.issubset(_extended_implicit_markers):
for marker in _implicit_markers - marker_set:
item.add_marker(getattr(pytest.mark, marker))

# Map any "category" markers (solver or writer) positional arguments
# to the id keyword, and ensure the 'vendor' keyword is populated
for mark in markers:
if mark.name not in _category_markers:
continue
if mark.args:
(_id,) = mark.args
mark.kwargs['id'] = _id
if 'vendor' not in mark.kwargs:
mark.kwargs['vendor'] = mark.kwargs['id'].split("_")[0]


def pytest_addoption(parser):
"""
Add another parser option to specify suite of solver tests to run
Add parser options as shorthand for running tests marked by specific
solvers or writers.
"""
parser.addoption(
"--solver",
action="store",
metavar="SOLVER",
help="Run tests matching the requested SOLVER.",
)


def pytest_configure(config):
"""
Register additional solver marker, as applicable.
This stops pytest from printing a warning about unregistered solver options.
"""
config.addinivalue_line(
"markers", "solver(name): mark test to run the named solver"
parser.addoption(
"--writer",
action="store",
metavar="WRITER",
help="Run tests matching the requested WRITER.",
)
8 changes: 6 additions & 2 deletions doc/OnlineDocs/contribution_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,12 @@ Markers are declared in ``pyproject.toml``. Some commonly used markers are:

- ``expensive``: tests that take a long time to run
- ``mpi``: tests that require MPI
- ``solver(name)``: dynamic marker to label a test for a specific solver,
e.g., ``@pytest.mark.solver("gurobi")``
- ``solver(id='name')``: tests for a specific solver,
e.g., ``@pytest.mark.solver("name")``
- ``solver(vendor='name')``: tests for a set of solvers (matching up to the
first underscore), e.g., ``solver(vendor="gurobi")`` will run tests marked
with ``solver("gurobi")``, ``solver("gurobi_direct")``, and
``solver("gurobi_persistent")``

More details about Pyomo-defined default test behavior can be found in
the `conftest.py file <https://github.com/Pyomo/pyomo/blob/main/conftest.py>`_.
Expand Down
2 changes: 1 addition & 1 deletion doc/OnlineDocs/getting_started/solvers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ the license requirements for their desired solver.
* - PyMUMPS
- ``pip install pymumps``
- ``conda install ‑c conda‑forge pymumps``
- `License <https://github.com/PyMumps/pymumps/blob/master/COPYING>`__
- `License <https://github.com/PyMumps/pymumps/blob/master/LICENSE.txt>`__
`Docs <https://github.com/pymumps/pymumps>`__
* - SCIP
- N/A
Expand Down
7 changes: 7 additions & 0 deletions pyomo/contrib/solver/solvers/knitro/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ def _restore_var_values(self) -> None:
var.set_value(self._saved_var_values[id(var)])
StaleFlagManager.mark_all_as_stale()

def _warm_start(self) -> None:
variables = []
for var in self._get_vars():
if var.value is not None:
variables.append(var)
self._engine.set_initial_values(variables)

@abstractmethod
def _presolve(
self, model: BlockData, config: KnitroConfig, timer: HierarchicalTimer
Expand Down
12 changes: 12 additions & 0 deletions pyomo/contrib/solver/solvers/knitro/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ def __init__(
visibility=visibility,
)

self.use_start: bool = self.declare(
"use_start",
ConfigValue(
domain=Bool,
default=False,
doc=(
"If True, KNITRO solver will use the the current values "
"of variables as starting points for the optimization."
),
),
)

self.rebuild_model_on_remove_var: bool = self.declare(
"rebuild_model_on_remove_var",
ConfigValue(
Expand Down
5 changes: 5 additions & 0 deletions pyomo/contrib/solver/solvers/knitro/direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ def _solve(self, config: KnitroConfig, timer: HierarchicalTimer) -> None:
self._engine.set_options(**config.solver_options)
timer.stop("load_options")

if config.use_start:
timer.start("warm_start")
self._warm_start()
timer.stop("warm_start")

timer.start("solve")
self._engine.solve()
timer.stop("solve")
5 changes: 5 additions & 0 deletions pyomo/contrib/solver/solvers/knitro/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ def get_idxs(
idx_map = self.maps[item_type]
return [idx_map[id(item)] for item in items]

def set_initial_values(self, variables: Iterable[VarData]) -> None:
values = [value(var) for var in variables]
idxs = self.get_idxs(VarData, variables)
self.execute(knitro.KN_set_var_primal_init_values, idxs, values)

def get_values(
self,
item_type: type[ItemType],
Expand Down
1 change: 1 addition & 0 deletions pyomo/contrib/solver/tests/solvers/test_gurobi_minlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@


@unittest.skipUnless(gurobi_direct.available(), "needs Gurobi Direct MINLP interface")
@unittest.pytest.mark.solver("gurobi_direct_minlp")
class TestGurobiMINLP(unittest.TestCase):
def test_gurobi_minlp_sincosexp(self):
m = ConcreteModel(name="test")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def get_visitor(self):


@unittest.skipUnless(gurobipy_available, "gurobipy is not available")
@unittest.pytest.mark.solver("gurobi_direct_minlp")
class TestGurobiMINLPWalker(CommonTest):
def _get_nl_expr_tree(self, visitor, expr):
# This is a bit hacky, but the only way that I know to get the expression tree
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def make_model():


@unittest.skipUnless(gurobipy_available, "Gurobipy 12 is not available")
@unittest.pytest.mark.solver("gurobi_direct_minlp")
class TestGurobiMINLPWriter(CommonTest):
def test_small_model(self):
grb_model = gurobipy.Model()
Expand Down
3 changes: 3 additions & 0 deletions pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def rule3(model):
return model


@unittest.pytest.mark.solver("gurobi_persistent")
class TestGurobiPersistentSimpleLPUpdates(unittest.TestCase):
def setUp(self):
self.m = pyo.ConcreteModel()
Expand Down Expand Up @@ -183,6 +184,7 @@ def test_lp(self):
self.assertAlmostEqual(y, self.m.y.value)


@unittest.pytest.mark.solver("gurobi_persistent")
class TestGurobiPersistent(unittest.TestCase):
def test_nonconvex_qcp_objective_bound_1(self):
# the goal of this test is to ensure we can get an objective bound
Expand Down Expand Up @@ -493,6 +495,7 @@ def test_zero_time_limit(self):
self.assertIsNone(res.incumbent_objective)


@unittest.pytest.mark.solver("gurobi_persistent")
class TestManualMode(unittest.TestCase):
def setUp(self):
opt = GurobiPersistent()
Expand Down
3 changes: 3 additions & 0 deletions pyomo/contrib/solver/tests/solvers/test_gurobi_warm_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def check_optimal_soln(self, m):
self.assertEqual(value(m.x[i]), x[i])

@unittest.skipUnless(gurobi_direct.available(), "needs Gurobi Direct interface")
@unittest.pytest.mark.solver("gurobi_direct")
def test_gurobi_direct_warm_start(self):
m = self.make_model()

Expand All @@ -82,6 +83,7 @@ def test_gurobi_direct_warm_start(self):
@unittest.skipUnless(
gurobi_direct_minlp.available(), "needs Gurobi Direct MINLP interface"
)
@unittest.pytest.mark.solver("gurobi_direct_minlp")
def test_gurobi_minlp_warmstart(self):
m = self.make_model()

Expand All @@ -97,6 +99,7 @@ def test_gurobi_minlp_warmstart(self):
@unittest.skipUnless(
gurobi_persistent.available(), "needs Gurobi persistent interface"
)
@unittest.pytest.mark.solver("gurobi_persistent")
def test_gurobi_persistent_warmstart(self):
m = self.make_model()

Expand Down
1 change: 1 addition & 0 deletions pyomo/contrib/solver/tests/solvers/test_highs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
raise unittest.SkipTest


@unittest.pytest.mark.solver("highs")
class TestBugs(unittest.TestCase):
def test_mutable_params_with_remove_cons(self):
m = pyo.ConcreteModel()
Expand Down
5 changes: 5 additions & 0 deletions pyomo/contrib/solver/tests/solvers/test_ipopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def windows_tee_buffer(size=1 << 20):
tee._pipe_buffersize = old


@unittest.pytest.mark.solver("ipopt")
class TestIpoptSolverConfig(unittest.TestCase):
def test_default_instantiation(self):
config = ipopt.IpoptConfig()
Expand Down Expand Up @@ -87,6 +88,7 @@ def test_custom_instantiation(self):
self.assertFalse(config.executable.available())


@unittest.pytest.mark.solver("ipopt")
class TestIpoptSolutionLoader(unittest.TestCase):
def test_get_reduced_costs_error(self):
loader = ipopt.IpoptSolutionLoader(
Expand All @@ -105,6 +107,7 @@ def test_get_duals_error(self):
loader.get_duals()


@unittest.pytest.mark.solver("ipopt")
class TestIpoptInterface(unittest.TestCase):
@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available")
def test_command_line_options(self):
Expand Down Expand Up @@ -1877,6 +1880,7 @@ def test_bad_executable(self):


@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available")
@unittest.pytest.mark.solver("ipopt")
class TestIpopt(unittest.TestCase):
def create_model(self):
model = pyo.ConcreteModel()
Expand Down Expand Up @@ -2091,6 +2095,7 @@ def test_load_suffixes_infeasible_model(self):


@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available")
@unittest.pytest.mark.solver("ipopt")
class TestLegacyIpopt(unittest.TestCase):
def create_model(self):
model = pyo.ConcreteModel()
Expand Down
Loading
Loading