Skip to content
59 changes: 22 additions & 37 deletions pyomo/contrib/solver/solvers/knitro/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
NoReducedCostsError,
NoSolutionError,
)
from pyomo.contrib.solver.solvers.knitro.api import knitro
from pyomo.contrib.solver.solvers.knitro.config import KnitroConfig
from pyomo.contrib.solver.solvers.knitro.engine import Engine
from pyomo.contrib.solver.solvers.knitro.package import PackageChecker
Expand Down Expand Up @@ -204,57 +203,43 @@ def _get_items(self, item_type: type[ItemType]) -> Sequence[ItemType]:

@staticmethod
def _get_solution_status(status: int) -> SolutionStatus:
if (
status == knitro.KN_RC_OPTIMAL
or status == knitro.KN_RC_OPTIMAL_OR_SATISFACTORY
or status == knitro.KN_RC_NEAR_OPT
):
"""
Map KNITRO status codes to Pyomo SolutionStatus values.

See https://www.artelys.com/app/docs/knitro/3_referenceManual/returnCodes.html
"""
if status in {0, -100}:
return SolutionStatus.optimal
elif status == knitro.KN_RC_FEAS_NO_IMPROVE:
elif -101 >= status >= -199 or -400 >= status >= -409:
return SolutionStatus.feasible
elif (
status == knitro.KN_RC_INFEASIBLE
or status == knitro.KN_RC_INFEAS_CON_BOUNDS
or status == knitro.KN_RC_INFEAS_VAR_BOUNDS
or status == knitro.KN_RC_INFEAS_NO_IMPROVE
):
elif status in {-200, -204, -205, -206}:
return SolutionStatus.infeasible
else:
return SolutionStatus.noSolution

@staticmethod
def _get_termination_condition(status: int) -> TerminationCondition:
if (
status == knitro.KN_RC_OPTIMAL
or status == knitro.KN_RC_OPTIMAL_OR_SATISFACTORY
or status == knitro.KN_RC_NEAR_OPT
):
"""
Map KNITRO status codes to Pyomo TerminationCondition values.

See https://www.artelys.com/app/docs/knitro/3_referenceManual/returnCodes.html
"""
if status in {0, -100}:
return TerminationCondition.convergenceCriteriaSatisfied
elif status == knitro.KN_RC_INFEAS_NO_IMPROVE:
elif status == -202:
return TerminationCondition.locallyInfeasible
elif (
status == knitro.KN_RC_INFEASIBLE
or status == knitro.KN_RC_INFEAS_CON_BOUNDS
or status == knitro.KN_RC_INFEAS_VAR_BOUNDS
):
elif status in {-200, -204, -205}:
return TerminationCondition.provenInfeasible
elif (
status == knitro.KN_RC_UNBOUNDED_OR_INFEAS
or status == knitro.KN_RC_UNBOUNDED
):
elif status in {-300, -301}:
return TerminationCondition.infeasibleOrUnbounded
elif (
status == knitro.KN_RC_ITER_LIMIT_FEAS
or status == knitro.KN_RC_ITER_LIMIT_INFEAS
):
elif status in {-400, -410}:
return TerminationCondition.iterationLimit
elif (
status == knitro.KN_RC_TIME_LIMIT_FEAS
or status == knitro.KN_RC_TIME_LIMIT_INFEAS
):
elif status in {-401, -411}:
return TerminationCondition.maxTimeLimit
elif status == knitro.KN_RC_USER_TERMINATION:
elif status == -500:
return TerminationCondition.interrupted
elif -500 > status >= -599:
return TerminationCondition.error
else:
return TerminationCondition.unknown

Expand Down
51 changes: 50 additions & 1 deletion pyomo/contrib/solver/tests/solvers/test_knitro_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
from contextlib import redirect_stdout

import pyomo.common.unittest as unittest
import pyomo.environ as pyo

from pyomo.contrib.solver.common.results import SolutionStatus, TerminationCondition
from pyomo.contrib.solver.solvers.knitro.config import KnitroConfig
from pyomo.contrib.solver.solvers.knitro.direct import KnitroDirectSolver
import pyomo.environ as pyo

avail = KnitroDirectSolver().available()

Expand Down Expand Up @@ -84,6 +85,54 @@ def test_available_cache(self):
self.assertTrue(opt._available_cache)
self.assertIsNotNone(opt._available_cache)

def test_solution_status_mapping(self):
opt = KnitroDirectSolver()
for opt_status in [0, -100]:
status = opt._get_solution_status(opt_status)
self.assertEqual(status, SolutionStatus.optimal)

for opt_status in [*range(-101, -103, -1), *range(-400, -406, -1)]:
status = opt._get_solution_status(opt_status)
self.assertEqual(status, SolutionStatus.feasible)

for opt_status in [-200, -204, -205, -206]:
status = opt._get_solution_status(opt_status)
self.assertEqual(status, SolutionStatus.infeasible)

for opt_status in [-501, -99999, -1]:
status = opt._get_solution_status(opt_status)
self.assertEqual(status, SolutionStatus.noSolution)

def test_termination_condition_mapping(self):
opt = KnitroDirectSolver()
for opt_status in [0, -100]:
term_cond = opt._get_termination_condition(opt_status)
self.assertEqual(
term_cond, TerminationCondition.convergenceCriteriaSatisfied
)
term_cond = opt._get_termination_condition(-202)
self.assertEqual(term_cond, TerminationCondition.locallyInfeasible)
for opt_status in [-200, -204, -205]:
term_cond = opt._get_termination_condition(opt_status)
self.assertEqual(term_cond, TerminationCondition.provenInfeasible)
for opt_status in [-300, -301]:
term_cond = opt._get_termination_condition(opt_status)
self.assertEqual(term_cond, TerminationCondition.infeasibleOrUnbounded)
for opt_status in [-400, -410]:
term_cond = opt._get_termination_condition(opt_status)
self.assertEqual(term_cond, TerminationCondition.iterationLimit)
for opt_status in [-401, -411]:
term_cond = opt._get_termination_condition(opt_status)
self.assertEqual(term_cond, TerminationCondition.maxTimeLimit)
term_cond = opt._get_termination_condition(-500)
self.assertEqual(term_cond, TerminationCondition.interrupted)
for opt_status in [-501, -550, -599]:
term_cond = opt._get_termination_condition(opt_status)
self.assertEqual(term_cond, TerminationCondition.error)
for opt_status in [-600, -99999, -1]:
term_cond = opt._get_termination_condition(opt_status)
self.assertEqual(term_cond, TerminationCondition.unknown)


@unittest.skipIf(not avail, "KNITRO solver is not available")
class TestKnitroDirectSolver(unittest.TestCase):
Expand Down
Loading