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

Parse noSolution status from CPLEXShell and CPLEXDirect solvers correctly #1313

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions pyomo/solvers/plugins/solvers/CPLEX.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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
Expand Down Expand Up @@ -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(','))
Expand Down
57 changes: 49 additions & 8 deletions pyomo/solvers/plugins/solvers/cplex_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down
105 changes: 103 additions & 2 deletions pyomo/solvers/tests/checks/test_CPLEXDirect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
82 changes: 81 additions & 1 deletion pyomo/solvers/tests/checks/test_cplex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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()