diff --git a/pyomo/solvers/plugins/solvers/cuopt_direct.py b/pyomo/solvers/plugins/solvers/cuopt_direct.py index 87f16b861c9..4ccdc9e631d 100644 --- a/pyomo/solvers/plugins/solvers/cuopt_direct.py +++ b/pyomo/solvers/plugins/solvers/cuopt_direct.py @@ -249,16 +249,18 @@ def _postsolve(self): is_mip = solution.get_problem_category() # Termination Status - # 0 - CUOPT_TERIMINATION_STATUS_NO_TERMINATION - # 1 - CUOPT_TERIMINATION_STATUS_OPTIMAL - # 2 - CUOPT_TERIMINATION_STATUS_INFEASIBLE - # 3 - CUOPT_TERIMINATION_STATUS_UNBOUNDED - # 4 - CUOPT_TERIMINATION_STATUS_ITERATION_LIMIT - # 5 - CUOPT_TERIMINATION_STATUS_TIME_LIMIT - # 6 - CUOPT_TERIMINATION_STATUS_NUMERICAL_ERROR - # 7 - CUOPT_TERIMINATION_STATUS_PRIMAL_FEASIBLE - # 8 - CUOPT_TERIMINATION_STATUS_FEASIBLE_FOUND - # 9 - CUOPT_TERIMINATION_STATUS_CONCURRENT_LIMIT + # 0 - CUOPT_TERIMINATION_STATUS_NO_TERMINATION + # 1 - CUOPT_TERIMINATION_STATUS_OPTIMAL + # 2 - CUOPT_TERIMINATION_STATUS_INFEASIBLE + # 3 - CUOPT_TERIMINATION_STATUS_UNBOUNDED + # 4 - CUOPT_TERIMINATION_STATUS_ITERATION_LIMIT + # 5 - CUOPT_TERIMINATION_STATUS_TIME_LIMIT + # 6 - CUOPT_TERIMINATION_STATUS_NUMERICAL_ERROR + # 7 - CUOPT_TERIMINATION_STATUS_PRIMAL_FEASIBLE + # 8 - CUOPT_TERIMINATION_STATUS_FEASIBLE_FOUND + # 9 - CUOPT_TERIMINATION_STATUS_CONCURRENT_LIMIT + # 10 - CUOPT_TERIMINATION_STATUS_WORK_LIMIT + # 11 - CUOPT_TERIMINATION_STATUS_UNBOUNDED_OR_INFEASIBLE if status == 1: self.results.solver.status = SolverStatus.ok @@ -292,6 +294,12 @@ def _postsolve(self): self.results.solver.status = SolverStatus.ok self.results.solver.termination_condition = TerminationCondition.other soln.status = SolutionStatus.other + elif status == 11: + self.results.solver.status = SolverStatus.warning + self.results.solver.termination_condition = ( + TerminationCondition.infeasibleOrUnbounded + ) + soln.status = SolutionStatus.unsure else: self.results.solver.status = SolverStatus.error self.results.solver.termination_condition = TerminationCondition.error diff --git a/pyomo/solvers/tests/checks/test_cuopt_direct.py b/pyomo/solvers/tests/checks/test_cuopt_direct.py index aef0a924468..baa13c560e8 100644 --- a/pyomo/solvers/tests/checks/test_cuopt_direct.py +++ b/pyomo/solvers/tests/checks/test_cuopt_direct.py @@ -125,6 +125,23 @@ def test_infeasible_trivial_constraint(self): with pytest.raises(ValueError, match=r"Trivial constraint.*infeasible"): opt.solve(m, skip_trivial_constraints=True) + @unittest.skipIf(not cuopt_available, "The CuOpt solver is not available") + def test_unbounded_or_infeasible_status(self): + # An LP with no variable bounds and an unbounded objective triggers + # cuOpt's presolver to return UnboundedOrInfeasible (status 11), which + # the plugin maps to TerminationCondition.infeasibleOrUnbounded. + m = ConcreteModel() + m.x = Var() + m.y = Var() + m.obj = Objective(expr=m.x + m.y, sense=minimize) + + opt = SolverFactory('cuopt') + res = opt.solve(m, load_solutions=False) + + self.assertEqual(res.solver.termination_condition, "infeasibleOrUnbounded") + self.assertEqual(res.solver.status, "warning") + self.assertEqual(res.solution[0].status, "unsure") + @unittest.skipIf(not cuopt_available, "The CuOpt solver is not available") def test_nonlinear_constraint_rejected(self): m = ConcreteModel()