diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index 80df23f6bac..67ff1458e79 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -507,14 +507,39 @@ def process_logfile(self): for line in output.split("\n"): tokens = re.split('[ \t]+',line.strip()) - if len(tokens) > 3 and tokens[0] == "CPLEX" and tokens[1] == "Error": + if len(tokens) > 3 and ("CPLEX", "Error") in {tuple(tokens[0:2]), tuple(tokens[1:3])}: # IMPT: See below - cplex can generate an error line and then terminate fine, e.g., in CPLEX 12.1. # To handle these cases, we should be specifying some kind of termination criterion always # in the course of parsing a log file (we aren't doing so currently - just in some conditions). - results.solver.status=SolverStatus.error + if ( + results.solver.status == SolverStatus.ok + and results.solver.termination_condition + in { + TerminationCondition.optimal, + TerminationCondition.infeasible, + TerminationCondition.maxTimeLimit, + TerminationCondition.noSolution, + TerminationCondition.unbounded, + } + ): + # If we have already determined the termination condition, reduce it to a warning. + # This is to be consistent with the code in the rest of this method that downgrades an error to a + # warning upon determining these termination conditions. + results.solver.status = SolverStatus.warning + else: + results.solver.status = SolverStatus.error results.solver.error = " ".join(tokens) + + # Find the first token that starts with an integer, and strip non-integer characters for the return code + error_code_token = next((token for token in tokens if re.match(r'\d', token)), None) + if error_code_token: + results.solver.return_code = int(re.sub(r'[^\d]', '', error_code_token)) + else: + results.solver.return_code = None elif len(tokens) >= 3 and tokens[0] == "ILOG" and tokens[1] == "CPLEX": cplex_version = tokens[2].rstrip(',') + elif len(tokens) >= 3 and tokens[1] == "Version": + cplex_version = tokens[3] elif len(tokens) >= 3 and tokens[0] == "Variables": if results.problem.number_of_variables is None: # CPLEX 11.2 and subsequent versions have two Variables sections in the log file output. results.problem.number_of_variables = int(tokens[2]) @@ -529,9 +554,9 @@ def process_logfile(self): elif len(tokens) >= 3 and tokens[0] == "Nonzeros": if results.problem.number_of_nonzeros is None: # CPLEX 11.2 and subsequent has two Nonzeros sections. results.problem.number_of_nonzeros = int(tokens[2]) - elif len(tokens) >= 5 and tokens[4] == "MINIMIZE": + elif (len(tokens) >= 5 and tokens[4] == "MINIMIZE") or (len(tokens) >= 4 and tokens[3] == 'Minimize'): results.problem.sense = ProblemSense.minimize - elif len(tokens) >= 5 and tokens[4] == "MAXIMIZE": + elif (len(tokens) >= 5 and tokens[4] == "MAXIMIZE") or (len(tokens) >= 4 and tokens[3] == 'Maximize'): results.problem.sense = ProblemSense.maximize elif len(tokens) >= 4 and tokens[0] == "Solution" and tokens[1] == "time" and tokens[2] == "=": # technically, I'm not sure if this is CPLEX user time or user+system - CPLEX doesn't appear @@ -563,6 +588,9 @@ def process_logfile(self): results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxTimeLimit results.solver.termination_message = ' '.join(tokens) + elif len(tokens) >= 6 and tokens[0] == "MIP" and tuple(tokens[5:]) == ('no', 'integer', 'solution.'): + results.solver.termination_condition = TerminationCondition.noSolution + results.solver.termination_message = ' '.join(tokens) elif len(tokens) >= 10 and tokens[0] == "Current" and tokens[1] == "MIP" and tokens[2] == "best" and tokens[3] == "bound": self._best_bound = float(tokens[5]) self._gap = float(tokens[8].rstrip(',')) diff --git a/pyomo/solvers/plugins/solvers/cplex_direct.py b/pyomo/solvers/plugins/solvers/cplex_direct.py index 990f8f3590c..fe7b1ffb359 100644 --- a/pyomo/solvers/plugins/solvers/cplex_direct.py +++ b/pyomo/solvers/plugins/solvers/cplex_direct.py @@ -180,9 +180,15 @@ def _process_stream(arg): if not _is_numeric(option): raise opt_cmd.set(float(option)) - + + self._error_code = None t0 = time.time() - self._solver_model.solve() + + try: + self._solver_model.solve() + except self._cplex.exceptions.CplexSolverError as e: + self._error_code = e.args[2] # See cplex.exceptions.error_codes + t1 = time.time() self._wallclock_time = t1 - t0 finally: @@ -482,6 +488,7 @@ def _postsolve(self): cpxprob = self._solver_model status = cpxprob.solution.get_status() + rtn_codes = cpxprob.solution.status if cpxprob.get_problem_type() in [cpxprob.problem_type.MILP, cpxprob.problem_type.MIQP, @@ -499,38 +506,72 @@ def _postsolve(self): self.results.solver.name = ("CPLEX {0}".format(cpxprob.get_version())) self.results.solver.wallclock_time = self._wallclock_time - if status in [1, 101, 102]: + if status in { + rtn_codes.optimal, + rtn_codes.MIP_optimal, + rtn_codes.optimal_tolerance, + }: self.results.solver.status = SolverStatus.ok self.results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal - elif status in [2, 40, 118, 133, 134]: + elif status in { + rtn_codes.unbounded, + 40, + rtn_codes.MIP_unbounded, + rtn_codes.relaxation_unbounded, + 134, + }: self.results.solver.status = SolverStatus.warning self.results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.unbounded - elif status in [4, 119, 134]: + elif status in { + rtn_codes.infeasible_or_unbounded, + rtn_codes.MIP_infeasible_or_unbounded, + 134, + }: # Note: status of 4 means infeasible or unbounded # and 119 means MIP infeasible or unbounded self.results.solver.status = SolverStatus.warning self.results.solver.termination_condition = \ TerminationCondition.infeasibleOrUnbounded soln.status = SolutionStatus.unsure - elif status in [3, 103]: + elif status in {rtn_codes.infeasible, rtn_codes.MIP_infeasible}: self.results.solver.status = SolverStatus.warning self.results.solver.termination_condition = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible - elif status in [10]: + elif status in {rtn_codes.abort_iteration_limit}: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_condition = TerminationCondition.maxIterations soln.status = SolutionStatus.stoppedByLimit - elif status in [11, 25, 107, 131]: + elif status in { + rtn_codes.abort_time_limit, + rtn_codes.abort_dettime_limit, + rtn_codes.MIP_time_limit_feasible, + rtn_codes.MIP_dettime_limit_feasible, + }: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_condition = TerminationCondition.maxTimeLimit soln.status = SolutionStatus.stoppedByLimit + elif status in { + rtn_codes.MIP_time_limit_infeasible, + rtn_codes.MIP_dettime_limit_infeasible, + rtn_codes.node_limit_infeasible, + rtn_codes.mem_limit_infeasible, + rtn_codes.MIP_abort_infeasible, + } or self._error_code == self._cplex.exceptions.error_codes.CPXERR_NO_SOLN: + # CPLEX doesn't have a solution status for `noSolution` so we assume this from the combination of + # maxTimeLimit + infeasible (instead of a generic `TerminationCondition.error`). + self.results.solver.status = SolverStatus.warning + self.results.solver.termination_condition = TerminationCondition.noSolution + soln.status = SolutionStatus.unknown else: self.results.solver.status = SolverStatus.error self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error + self.results.solver.return_code = self._error_code + self.results.solver.termination_message = cpxprob.solution.get_status_string(status) + if cpxprob.objective.get_sense() == cpxprob.objective.sense.minimize: self.results.problem.sense = minimize elif cpxprob.objective.get_sense() == cpxprob.objective.sense.maximize: diff --git a/pyomo/solvers/tests/checks/test_CPLEXDirect.py b/pyomo/solvers/tests/checks/test_CPLEXDirect.py index 4e88405f4d4..157e3413147 100644 --- a/pyomo/solvers/tests/checks/test_CPLEXDirect.py +++ b/pyomo/solvers/tests/checks/test_CPLEXDirect.py @@ -8,10 +8,14 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import sys +from itertools import product +from random import random, seed + import pyutilib.th as unittest -from pyomo.opt import * + from pyomo.environ import * -import sys +from pyomo.opt import * try: import cplex @@ -113,6 +117,103 @@ def test_infeasible_mip(self): self.assertEqual(results.solver.termination_condition, TerminationCondition.infeasible) + @unittest.skipIf(not cplexpy_available, + "The 'cplex' python bindings are not available") + def test_no_solution_mip(self): + def build_mtz_tsp_model(nodes, links, distances): + # Taken from examples/pyomo/callbacks/tsp.py + model = ConcreteModel() + + model.POINTS = Set(initialize=nodes, ordered=True) + model.POINTS_LESS_FIRST = Set(initialize=nodes[1:], ordered=True) + model.LINKS = Set(initialize=links, ordered=True) + model.LINKS_LESS_FIRST = Set( + initialize=[ + (i, j) for (i, j) in links if i in nodes[1:] and j in nodes[1:] + ], + ordered=True, + ) + + model.N = len(nodes) + model.d = Param(model.LINKS, initialize=distances) + + model.Z = Var(model.LINKS, domain=Binary) + model.FLOW = Var( + model.POINTS_LESS_FIRST, + domain=NonNegativeReals, + bounds=(0, model.N - 1), + ) + + model.InDegrees = Constraint( + model.POINTS, + rule=lambda m, i: sum( + model.Z[i, j] for (i_, j) in model.LINKS if i == i_ + ) + == 1, + ) + model.OutDegrees = Constraint( + model.POINTS, + rule=lambda m, i: sum( + model.Z[j, i] for (j, i_) in model.LINKS if i == i_ + ) + == 1, + ) + + model.FlowCon = Constraint( + model.LINKS_LESS_FIRST, + rule=lambda m, i, j: model.FLOW[i] - model.FLOW[j] + m.N * m.Z[i, j] + <= m.N - 1, + ) + + model.tour_length = Objective( + expr=sum_product(model.d, model.Z), sense=minimize + ) + return model + + with SolverFactory("cplex", solver_io="python") as opt: + # Set the `options` such that CPLEX cannot determine the problem as infeasible within the time allowed + opt.options["dettimelimit"] = 1 + opt.options["lpmethod"] = 1 + opt.options["threads"] = 1 + + opt.options["mip_limits_nodes"] = 0 + opt.options["mip_limits_eachcutlimit"] = 0 + opt.options["mip_limits_cutsfactor"] = 0 + opt.options["mip_limits_auxrootthreads"] = -1 + + opt.options["preprocessing_presolve"] = 0 + opt.options["preprocessing_reduce"] = 0 + opt.options["preprocessing_relax"] = 0 + + opt.options["mip_strategy_heuristicfreq"] = -1 + opt.options["mip_strategy_presolvenode"] = -1 + opt.options["mip_strategy_probe"] = -1 + + opt.options["mip_cuts_mircut"] = -1 + opt.options["mip_cuts_implied"] = -1 + opt.options["mip_cuts_gomory"] = -1 + opt.options["mip_cuts_flowcovers"] = -1 + opt.options["mip_cuts_pathcut"] = -1 + opt.options["mip_cuts_liftproj"] = -1 + opt.options["mip_cuts_zerohalfcut"] = -1 + opt.options["mip_cuts_cliques"] = -1 + opt.options["mip_cuts_covers"] = -1 + + nodes = list(range(15)) + links = list((i, j) for i, j in product(nodes, nodes) if i != j) + + seed(0) + distances = {link: random() for link in links} + + model = build_mtz_tsp_model(nodes, links, distances) + + results = opt.solve(model) + + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.noSolution + ) + @unittest.skipIf(not cplexpy_available, "The 'cplex' python bindings are not available") def test_unbounded_mip(self): diff --git a/pyomo/solvers/tests/checks/test_cplex.py b/pyomo/solvers/tests/checks/test_cplex.py index a0959fee9f4..4dff2d17738 100644 --- a/pyomo/solvers/tests/checks/test_cplex.py +++ b/pyomo/solvers/tests/checks/test_cplex.py @@ -16,7 +16,8 @@ import pyomo.kernel as pmo from pyomo.core import Binary, ConcreteModel, Constraint, Objective, Var, Integers, RangeSet, minimize, quicksum, Suffix from pyomo.environ import * -from pyomo.opt import ProblemFormat, convert_problem, SolverFactory, BranchDirection +from pyomo.opt import (BranchDirection, ProblemFormat, SolverFactory, + SolverStatus, TerminationCondition, convert_problem) from pyomo.solvers.plugins.solvers.CPLEX import CPLEXSHELL, MockCPLEX, _validate_file_name @@ -299,5 +300,84 @@ def get_mock_model_with_priorities(self): return m +class TestCPLEXSHELLProcessLogfile(unittest.TestCase): + def setUp(self): + solver = MockCPLEX() + solver._log_file = pyutilib.services.TempfileManager.create_tempfile( + suffix=".log" + ) + self.solver = solver + + def test_log_file_shows_no_solution(self): + log_file_text = """ +MIP - Time limit exceeded, no integer solution. +Current MIP best bound = 0.0000000000e+00 (gap is infinite) +Solution time = 0.00 sec. Iterations = 0 Nodes = 0 +Deterministic time = 0.00 ticks (0.20 ticks/sec) + +CPLEX> CPLEX Error 1217: No solution exists. +No file written. +CPLEX>""" + with open(self.solver._log_file, "w") as f: + f.write(log_file_text) + + results = CPLEXSHELL.process_logfile(self.solver) + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.noSolution + ) + self.assertEqual( + results.solver.termination_message, + "MIP - Time limit exceeded, no integer solution.", + ) + self.assertEqual(results.solver.return_code, 1217) + + def test_log_file_shows_infeasible(self): + log_file_text = """ +MIP - Integer infeasible. +Current MIP best bound = 0.0000000000e+00 (gap is infinite) +Solution time = 0.00 sec. Iterations = 0 Nodes = 0 +Deterministic time = 0.00 ticks (0.20 ticks/sec) + +CPLEX> CPLEX Error 1217: No solution exists. +No file written. +CPLEX>""" + with open(self.solver._log_file, "w") as f: + f.write(log_file_text) + + results = CPLEXSHELL.process_logfile(self.solver) + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.infeasible + ) + self.assertEqual( + results.solver.termination_message, "MIP - Integer infeasible." + ) + self.assertEqual(results.solver.return_code, 1217) + + def test_log_file_shows_presolve_infeasible(self): + log_file_text = """ +Infeasibility row 'c_e_x18_': 0 = -1. +Presolve time = 0.00 sec. (0.00 ticks) +Presolve - Infeasible. +Solution time = 0.00 sec. +Deterministic time = 0.00 ticks (0.61 ticks/sec) +CPLEX> CPLEX Error 1217: No solution exists. +No file written. +CPLEX>""" + + with open(self.solver._log_file, "w") as f: + f.write(log_file_text) + + results = CPLEXSHELL.process_logfile(self.solver) + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.infeasible + ) + self.assertEqual( + results.solver.termination_message, "Presolve - Infeasible." + ) + self.assertEqual(results.solver.return_code, 1217) + if __name__ == "__main__": unittest.main()