diff --git a/pyomo/contrib/solver/solvers/knitro/base.py b/pyomo/contrib/solver/solvers/knitro/base.py index bb5a15c5f65..003ee8cab2e 100644 --- a/pyomo/contrib/solver/solvers/knitro/base.py +++ b/pyomo/contrib/solver/solvers/knitro/base.py @@ -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 @@ -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 diff --git a/pyomo/contrib/solver/tests/solvers/test_knitro_direct.py b/pyomo/contrib/solver/tests/solvers/test_knitro_direct.py index 0d6908f8e24..c8dcd5e555a 100644 --- a/pyomo/contrib/solver/tests/solvers/test_knitro_direct.py +++ b/pyomo/contrib/solver/tests/solvers/test_knitro_direct.py @@ -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() @@ -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):