Skip to content

Commit a70451d

Browse files
authored
Merge pull request #3478 from tvignon-artelys/feature/support_for_knitro_python_package
Support for upcoming knitro python package
2 parents d1a507f + 87cef7b commit a70451d

File tree

5 files changed

+242
-2
lines changed

5 files changed

+242
-2
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2025
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
import logging
12+
from pathlib import Path
13+
14+
from pyomo.common import Executable
15+
from pyomo.common.collections import Bunch
16+
from pyomo.opt.base.formats import ProblemFormat, ResultsFormat
17+
from pyomo.opt.base.solvers import SolverFactory, OptSolver
18+
from pyomo.solvers.plugins.solvers.ASL import ASL
19+
20+
logger = logging.getLogger('pyomo.solvers')
21+
22+
23+
@SolverFactory.register(
24+
'knitroampl', doc='The Knitro solver for NLP/MINLP and their subclasses'
25+
)
26+
class KNITROAMPL(ASL):
27+
"""An interface to the Knitro optimizer that uses the AMPL Solver Library."""
28+
29+
def __init__(self, **kwds):
30+
"""Constructor"""
31+
executable = kwds.pop('executable', None)
32+
validate = kwds.pop('validate', True)
33+
kwds["type"] = "knitroampl"
34+
kwds["options"] = {'solver': "knitroampl"}
35+
OptSolver.__init__(self, **kwds)
36+
self._keepfiles = False
37+
self._results_file = None
38+
self._timer = ''
39+
self._user_executable = None
40+
# broadly useful for reporting, and in cases where
41+
# a solver plugin may not report execution time.
42+
self._last_solve_time = None
43+
self._define_signal_handlers = None
44+
self._version_timeout = 2
45+
46+
if executable == 'knitroampl':
47+
self.set_executable(name=None, validate=validate)
48+
elif executable is not None:
49+
self.set_executable(name=executable, validate=validate)
50+
#
51+
# Setup valid problem formats, and valid results for each problem format.
52+
# Also set the default problem and results formats.
53+
#
54+
self._valid_problem_formats = [ProblemFormat.nl]
55+
self._valid_result_formats = {ProblemFormat.nl: [ResultsFormat.sol]}
56+
self.set_problem_format(ProblemFormat.nl)
57+
#
58+
# Note: Undefined capabilities default to 'None'
59+
#
60+
self._capabilities = Bunch()
61+
self._capabilities.linear = True
62+
self._capabilities.integer = True
63+
self._capabilities.quadratic_objective = True
64+
self._capabilities.quadratic_constraint = True
65+
self._capabilities.sos1 = False
66+
self._capabilities.sos2 = False
67+
68+
def _default_executable(self):
69+
try:
70+
# If knitro Python package is available, use the executable it contains
71+
import knitro
72+
73+
package_knitroampl_path = (
74+
Path(knitro.__file__).resolve().parent / 'knitroampl' / 'knitroampl'
75+
)
76+
executable = Executable(str(package_knitroampl_path))
77+
except ModuleNotFoundError:
78+
# Otherwise, search usual path list
79+
executable = Executable('knitroampl')
80+
if not executable:
81+
logger.warning(
82+
"Could not locate the 'knitroampl' executable, "
83+
"which is required for solver %s" % self.name
84+
)
85+
self.enable = False
86+
return None
87+
return executable.path()

pyomo/solvers/plugins/solvers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@
3232
xpress_direct,
3333
xpress_persistent,
3434
SAS,
35+
KNITROAMPL,
3536
)
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2025
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
from pyomo.common import unittest
13+
from pyomo.environ import (
14+
ConcreteModel,
15+
Var,
16+
Objective,
17+
Constraint,
18+
Suffix,
19+
NonNegativeIntegers,
20+
NonNegativeReals,
21+
value,
22+
)
23+
from pyomo.opt import SolverFactory, TerminationCondition
24+
25+
knitroampl_available = SolverFactory('knitroampl').available(False)
26+
27+
28+
class TestKNITROAMPLInterface(unittest.TestCase):
29+
@unittest.skipIf(
30+
not knitroampl_available, "The 'knitroampl' command is not available"
31+
)
32+
def test_infeasible_lp(self):
33+
with SolverFactory('knitroampl') as opt:
34+
model = ConcreteModel()
35+
model.X = Var(within=NonNegativeReals)
36+
model.C1 = Constraint(expr=model.X == 1)
37+
model.C2 = Constraint(expr=model.X == 2)
38+
model.Obj = Objective(expr=model.X)
39+
40+
results = opt.solve(model)
41+
42+
self.assertEqual(
43+
results.solver.termination_condition, TerminationCondition.infeasible
44+
)
45+
46+
@unittest.skipIf(
47+
not knitroampl_available, "The 'knitroampl' command is not available"
48+
)
49+
def test_unbounded_lp(self):
50+
with SolverFactory('knitroampl') as opt:
51+
model = ConcreteModel()
52+
model.X = Var()
53+
model.Obj = Objective(expr=model.X)
54+
55+
results = opt.solve(model)
56+
57+
self.assertIn(
58+
results.solver.termination_condition,
59+
(
60+
TerminationCondition.unbounded,
61+
TerminationCondition.infeasibleOrUnbounded,
62+
),
63+
)
64+
65+
@unittest.skipIf(
66+
not knitroampl_available, "The 'knitroampl' command is not available"
67+
)
68+
def test_optimal_lp(self):
69+
with SolverFactory('knitroampl') as opt:
70+
model = ConcreteModel()
71+
model.X = Var(within=NonNegativeReals)
72+
model.C1 = Constraint(expr=model.X >= 2.5)
73+
model.Obj = Objective(expr=model.X)
74+
75+
results = opt.solve(model, load_solutions=True)
76+
77+
self.assertEqual(
78+
results.solver.termination_condition, TerminationCondition.optimal
79+
)
80+
self.assertAlmostEqual(value(model.X), 2.5)
81+
82+
@unittest.skipIf(
83+
not knitroampl_available, "The 'knitroampl' command is not available"
84+
)
85+
def test_get_duals_lp(self):
86+
with SolverFactory('knitroampl') as opt:
87+
model = ConcreteModel()
88+
model.X = Var(within=NonNegativeReals)
89+
model.Y = Var(within=NonNegativeReals)
90+
91+
model.C1 = Constraint(expr=2 * model.X + model.Y >= 8)
92+
model.C2 = Constraint(expr=model.X + 3 * model.Y >= 6)
93+
94+
model.Obj = Objective(expr=model.X + model.Y)
95+
96+
results = opt.solve(model, suffixes=['dual'], load_solutions=False)
97+
98+
model.dual = Suffix(direction=Suffix.IMPORT)
99+
model.solutions.load_from(results)
100+
101+
self.assertAlmostEqual(model.dual[model.C1], 0.4)
102+
self.assertAlmostEqual(model.dual[model.C2], 0.2)
103+
104+
@unittest.skipIf(
105+
not knitroampl_available, "The 'knitroampl' command is not available"
106+
)
107+
def test_infeasible_mip(self):
108+
with SolverFactory('knitroampl') as opt:
109+
model = ConcreteModel()
110+
model.X = Var(within=NonNegativeIntegers)
111+
model.C1 = Constraint(expr=model.X == 1)
112+
model.C2 = Constraint(expr=model.X == 2)
113+
model.Obj = Objective(expr=model.X)
114+
115+
results = opt.solve(model)
116+
117+
self.assertEqual(
118+
results.solver.termination_condition, TerminationCondition.infeasible
119+
)
120+
121+
@unittest.skipIf(
122+
not knitroampl_available, "The 'knitroampl' command is not available"
123+
)
124+
def test_optimal_mip(self):
125+
with SolverFactory('knitroampl') as opt:
126+
model = ConcreteModel()
127+
model.X = Var(within=NonNegativeIntegers)
128+
model.C1 = Constraint(expr=model.X >= 2.5)
129+
model.Obj = Objective(expr=model.X)
130+
131+
results = opt.solve(model, load_solutions=True)
132+
133+
self.assertEqual(
134+
results.solver.termination_condition, TerminationCondition.optimal
135+
)
136+
self.assertAlmostEqual(value(model.X), 3)
137+
138+
139+
if __name__ == "__main__":
140+
unittest.main()

pyomo/solvers/tests/mip/test4.soln

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
MILP solution:
2+
Objective value = 2
3+
x2 = 1

pyomo/solvers/tests/testcases.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,17 @@
285285
#
286286
# KNITROAMPL
287287
#
288-
# NO EXPECTED FAILURES
289-
#
288+
for prob in ('LP_trivial_constraints', 'LP_trivial_constraints_kernel'):
289+
ExpectedFailures['knitroampl', 'nl', prob] = (
290+
lambda v: True,
291+
'Knitro does not consider tight trivial constraints to have zero dual value',
292+
)
293+
294+
for prob in ('MILP_unbounded', 'MILP_unbounded_kernel'):
295+
ExpectedFailures['knitroampl', 'nl', prob] = (
296+
lambda v: v[:2] <= (14, 2),
297+
'Unbounded MILP detection not operational in Knitro, fixed in 15.0',
298+
)
290299

291300

292301
def generate_scenarios(arg=None):

0 commit comments

Comments
 (0)