Skip to content

Support for upcoming knitro python package #3478

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

Merged
Merged
87 changes: 87 additions & 0 deletions pyomo/solvers/plugins/solvers/KNITROAMPL.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2025
# 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.
# ___________________________________________________________________________
import logging
from pathlib import Path

from pyomo.common import Executable
from pyomo.common.collections import Bunch
from pyomo.opt.base.formats import ProblemFormat, ResultsFormat
from pyomo.opt.base.solvers import SolverFactory, OptSolver
from pyomo.solvers.plugins.solvers.ASL import ASL

logger = logging.getLogger('pyomo.solvers')


@SolverFactory.register(
'knitroampl', doc='The Knitro solver for NLP/MINLP and their subclasses'
)
class KNITROAMPL(ASL):
"""An interface to the Knitro optimizer that uses the AMPL Solver Library."""

def __init__(self, **kwds):
"""Constructor"""
executable = kwds.pop('executable', None)
validate = kwds.pop('validate', True)
kwds["type"] = "knitroampl"
kwds["options"] = {'solver': "knitroampl"}
OptSolver.__init__(self, **kwds)
self._keepfiles = False
self._results_file = None
self._timer = ''
self._user_executable = None
# broadly useful for reporting, and in cases where
# a solver plugin may not report execution time.
self._last_solve_time = None
self._define_signal_handlers = None
self._version_timeout = 2

if executable == 'knitroampl':
self.set_executable(name=None, validate=validate)

Check warning on line 47 in pyomo/solvers/plugins/solvers/KNITROAMPL.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/KNITROAMPL.py#L47

Added line #L47 was not covered by tests
elif executable is not None:
self.set_executable(name=executable, validate=validate)

Check warning on line 49 in pyomo/solvers/plugins/solvers/KNITROAMPL.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/KNITROAMPL.py#L49

Added line #L49 was not covered by tests
#
# Setup valid problem formats, and valid results for each problem format.
# Also set the default problem and results formats.
#
self._valid_problem_formats = [ProblemFormat.nl]
self._valid_result_formats = {ProblemFormat.nl: [ResultsFormat.sol]}
self.set_problem_format(ProblemFormat.nl)
#
# Note: Undefined capabilities default to 'None'
#
self._capabilities = Bunch()
self._capabilities.linear = True
self._capabilities.integer = True
self._capabilities.quadratic_objective = True
self._capabilities.quadratic_constraint = True
self._capabilities.sos1 = False
self._capabilities.sos2 = False

def _default_executable(self):
try:
# If knitro Python package is available, use the executable it contains
import knitro

package_knitroampl_path = (

Check warning on line 73 in pyomo/solvers/plugins/solvers/KNITROAMPL.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/KNITROAMPL.py#L73

Added line #L73 was not covered by tests
Path(knitro.__file__).resolve().parent / 'knitroampl' / 'knitroampl'
)
executable = Executable(str(package_knitroampl_path))

Check warning on line 76 in pyomo/solvers/plugins/solvers/KNITROAMPL.py

View check run for this annotation

Codecov / codecov/patch

pyomo/solvers/plugins/solvers/KNITROAMPL.py#L76

Added line #L76 was not covered by tests
except ModuleNotFoundError:
# Otherwise, search usual path list
executable = Executable('knitroampl')
if not executable:
logger.warning(
"Could not locate the 'knitroampl' executable, "
"which is required for solver %s" % self.name
)
self.enable = False
return None
return executable.path()
1 change: 1 addition & 0 deletions pyomo/solvers/plugins/solvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@
xpress_direct,
xpress_persistent,
SAS,
KNITROAMPL,
)
140 changes: 140 additions & 0 deletions pyomo/solvers/tests/checks/test_KNITROAMPL.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2025
# 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.common import unittest
from pyomo.environ import (
ConcreteModel,
Var,
Objective,
Constraint,
Suffix,
NonNegativeIntegers,
NonNegativeReals,
value,
)
from pyomo.opt import SolverFactory, TerminationCondition

knitroampl_available = SolverFactory('knitroampl').available(False)


class TestKNITROAMPLInterface(unittest.TestCase):
@unittest.skipIf(
not knitroampl_available, "The 'knitroampl' command is not available"
)
def test_infeasible_lp(self):
with SolverFactory('knitroampl') as opt:
model = ConcreteModel()
model.X = Var(within=NonNegativeReals)
model.C1 = Constraint(expr=model.X == 1)
model.C2 = Constraint(expr=model.X == 2)
model.Obj = Objective(expr=model.X)

results = opt.solve(model)

self.assertEqual(
results.solver.termination_condition, TerminationCondition.infeasible
)

@unittest.skipIf(
not knitroampl_available, "The 'knitroampl' command is not available"
)
def test_unbounded_lp(self):
with SolverFactory('knitroampl') as opt:
model = ConcreteModel()
model.X = Var()
model.Obj = Objective(expr=model.X)

results = opt.solve(model)

self.assertIn(
results.solver.termination_condition,
(
TerminationCondition.unbounded,
TerminationCondition.infeasibleOrUnbounded,
),
)

@unittest.skipIf(
not knitroampl_available, "The 'knitroampl' command is not available"
)
def test_optimal_lp(self):
with SolverFactory('knitroampl') as opt:
model = ConcreteModel()
model.X = Var(within=NonNegativeReals)
model.C1 = Constraint(expr=model.X >= 2.5)
model.Obj = Objective(expr=model.X)

results = opt.solve(model, load_solutions=True)

self.assertEqual(
results.solver.termination_condition, TerminationCondition.optimal
)
self.assertAlmostEqual(value(model.X), 2.5)

@unittest.skipIf(
not knitroampl_available, "The 'knitroampl' command is not available"
)
def test_get_duals_lp(self):
with SolverFactory('knitroampl') as opt:
model = ConcreteModel()
model.X = Var(within=NonNegativeReals)
model.Y = Var(within=NonNegativeReals)

model.C1 = Constraint(expr=2 * model.X + model.Y >= 8)
model.C2 = Constraint(expr=model.X + 3 * model.Y >= 6)

model.Obj = Objective(expr=model.X + model.Y)

results = opt.solve(model, suffixes=['dual'], load_solutions=False)

model.dual = Suffix(direction=Suffix.IMPORT)
model.solutions.load_from(results)

self.assertAlmostEqual(model.dual[model.C1], 0.4)
self.assertAlmostEqual(model.dual[model.C2], 0.2)

@unittest.skipIf(
not knitroampl_available, "The 'knitroampl' command is not available"
)
def test_infeasible_mip(self):
with SolverFactory('knitroampl') as opt:
model = ConcreteModel()
model.X = Var(within=NonNegativeIntegers)
model.C1 = Constraint(expr=model.X == 1)
model.C2 = Constraint(expr=model.X == 2)
model.Obj = Objective(expr=model.X)

results = opt.solve(model)

self.assertEqual(
results.solver.termination_condition, TerminationCondition.infeasible
)

@unittest.skipIf(
not knitroampl_available, "The 'knitroampl' command is not available"
)
def test_optimal_mip(self):
with SolverFactory('knitroampl') as opt:
model = ConcreteModel()
model.X = Var(within=NonNegativeIntegers)
model.C1 = Constraint(expr=model.X >= 2.5)
model.Obj = Objective(expr=model.X)

results = opt.solve(model, load_solutions=True)

self.assertEqual(
results.solver.termination_condition, TerminationCondition.optimal
)
self.assertAlmostEqual(value(model.X), 3)


if __name__ == "__main__":
unittest.main()
3 changes: 3 additions & 0 deletions pyomo/solvers/tests/mip/test4.soln
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MILP solution:
Objective value = 2
x2 = 1
13 changes: 11 additions & 2 deletions pyomo/solvers/tests/testcases.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,17 @@
#
# KNITROAMPL
#
# NO EXPECTED FAILURES
#
for prob in ('LP_trivial_constraints', 'LP_trivial_constraints_kernel'):
ExpectedFailures['knitroampl', 'nl', prob] = (
lambda v: True,
'Knitro does not consider tight trivial constraints to have zero dual value',
)

for prob in ('MILP_unbounded', 'MILP_unbounded_kernel'):
ExpectedFailures['knitroampl', 'nl', prob] = (
lambda v: v[:2] <= (14, 2),
'Unbounded MILP detection not operational in Knitro, fixed in 15.0',
)


def generate_scenarios(arg=None):
Expand Down
Loading