Skip to content
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

Support for upcoming knitro python package #3478

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions pyomo/solvers/plugins/solvers/ASL.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
# ___________________________________________________________________________


from pathlib import Path
import os
import subprocess

from pyomo.common import Executable
from pyomo.common.errors import ApplicationError
from pyomo.common.collections import Bunch
from pyomo.common.tempfiles import TempfileManager
from pyomo.common.fileutils import find_executable

from pyomo.opt.base import ProblemFormat, ResultsFormat
from pyomo.opt.base.solvers import _extract_version, SolverFactory
Expand Down
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-2024
# 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)
elif executable is not None:
self.set_executable(name=executable, validate=validate)
#
# 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 = (
Path(knitro.__file__).resolve().parent / 'knitroampl' / 'knitroampl'
)
executable = Executable(str(package_knitroampl_path))
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,
)
141 changes: 141 additions & 0 deletions pyomo/solvers/tests/checks/test_KNITROAMPL.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# 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,
Integers,
NonNegativeIntegers,
NonNegativeReals,
value,
)
from pyomo.opt import SolverFactory, TerminationCondition, SolutionStatus

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