Source code for docplex.mp.sdetails

# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# http://www.apache.org/licenses/
# (c) Copyright IBM Corp. 2015, 2016
# --------------------------------------------------------------------------
from io import StringIO
from docplex.mp.utils import is_almost_equal
from docplex.mp.constants import int_probtype_to_string
from math import isnan


[docs]class SolveDetails(object): """ The :class:`SolveDetails` class contains the details of a solve. This class should never be instantiated. You get an instance of this class from the model by calling the property :data:`docplex.mp.model.Model.solve_details`. """ _unknown_label = "*unknown*" NOT_A_NUM = float('nan') _NO_GAP = NOT_A_NUM # value used when no gap is available _NO_BEST_BOUND = NOT_A_NUM def __init__(self, time=0, dettime=0, status_code=-1, status_string=None, problem_type=None, ncolumns=0, nonzeros=0, miprelgap=None, best_bound=None, n_iterations=0, n_nodes_processed=0): self._time = max(time, 0) self._dettime = max(dettime, 0) self._solve_status_code = status_code self._solve_status = status_string or self._unknown_label self._problem_type = problem_type or self._unknown_label # string self._ncolumns = ncolumns self._linear_nonzeros = nonzeros # -- self._miprelgap = self._NO_GAP if miprelgap is None else miprelgap self._best_bound = self._NO_BEST_BOUND if best_bound is None else best_bound # -- progress self._n_iterations = n_iterations self._n_nodes_processed = n_nodes_processed self._quality_metrics = {} def equals(self, other, relative=1e-3, absolute=1e-5, compare_times=True): if not isinstance(other, SolveDetails): return False if compare_times and not is_almost_equal(self.time, other.time, relative, absolute): return False elif self.status_code != other.status_code: return False elif self.problem_type != other.problem_type: return False elif self.columns != other.columns: return False elif self.nb_linear_nonzeros != other.nb_linear_nonzeros: return False elif not is_almost_equal(self.mip_relative_gap, other.mip_relative_gap, relative, absolute): return False else: return True def as_worker_dict(self): # INTERNAL # Converts the solve details to a dictionary for python worker... # using "legacy' keys from drop-solve worker_dict = {"MODEL_DETAIL_TYPE": self._problem_type, "MODEL_DETAIL_NONZEROS": self._linear_nonzeros } if not isnan(self._miprelgap): worker_dict["PROGRESS_GAP"] = self._miprelgap if not isnan(self._best_bound): worker_dict['PROGRESS_BEST_OBJECTIVE'] = self._best_bound return worker_dict @staticmethod def to_plain_str(arg_s): return arg_s # in py3 do nothing. # --- # list of fields to be retrieved from the details # as tuples: (<detail_attribute_name>, <json_attribute_name>, <type_conversion_fn>, <default_value>) # example: # _time denotes solve time, a float value, default is 0, to be found in json["cplex.time"] # --- _json_fields = (("_time", "cplex.time", float, 0), ("_solve_status_code", "cplex.status", int, -1), ("_solve_status", "cplex.statusstring", lambda s: SolveDetails.to_plain_str(s), ""), ("_problem_type", "cplex.problemtype", lambda p: int_probtype_to_string(p), ""), ("_ncolumns", "cplex.columns", int, 0), ("_linear_nonzeros", "MODEL_DETAIL_NON_ZEROS", int, 0), ("_miprelgap", "cplex.miprelgap", float, _NO_GAP), ('_best_bound', 'PROGRESS_BEST_OBJECTIVE', float, _NO_BEST_BOUND), ("_md5", "cplex.model.md5", str, ""), ('_n_iterations', 'cplex.itcount', int, 0), ('_n_nodes_processed', 'cplex.nodes.processed', int, 0) ) @staticmethod def from_json(json_details, all_json_fields=_json_fields): if not json_details: return SolveDetails.make_dummy() # for k,v in json_details.items(): # print("{0}: {1!s}".format(k, v)) # print("# -------------------------") details = SolveDetails() for attr_name, field_name, field_conv_fn, field_default in all_json_fields: field_val = json_details.get(field_name, field_default) if field_conv_fn is not None: field_val = field_conv_fn(field_val) # conversion setattr(details, attr_name, field_val) return details @staticmethod def make_dummy(): dummy_details = SolveDetails(status_string="dummy") return dummy_details @staticmethod def make_fake_details(time, feasible): if feasible: status_code = 1 status = "OPTIMAL" else: status_code = 3 status = "infeasible" details = SolveDetails(time=time, status_code=status_code, status_string=status, problem_type=None) return details @property def time(self): """ This property returns the solve time in seconds. """ return self._time @property def status_code(self): """ This property returns the CPLEX status code as a number. Possible values for the status code are described in the CPLEX documentation at: https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/refcallablelibrary/macros/Solution_status_codes.html :return: an integer number (a CPLEX status code) """ return self._solve_status_code # status string # CPX_STAT_ABORT_DETTIME_LIM = 25 # CPX_STAT_ABORT_DUAL_OBJ_LIM = 22 # CPX_STAT_ABORT_IT_LIM = 10 # CPX_STAT_ABORT_OBJ_LIM = 12 # CPX_STAT_ABORT_PRIM_OBJ_LIM = 21 # CPX_STAT_ABORT_TIME_LIM = 11 # CPX_STAT_ABORT_USER = 13 # CPX_STAT_CONFLICT_ABORT_CONTRADICTION = 32 # CPX_STAT_CONFLICT_ABORT_DETTIME_LIM = 39 # CPX_STAT_CONFLICT_ABORT_IT_LIM = 34 # CPX_STAT_CONFLICT_ABORT_MEM_LIM = 37 # CPX_STAT_CONFLICT_ABORT_NODE_LIM = 35 # CPX_STAT_CONFLICT_ABORT_OBJ_LIM = 36 # CPX_STAT_CONFLICT_ABORT_TIME_LIM = 33 # CPX_STAT_CONFLICT_ABORT_USER = 38 # CPX_STAT_CONFLICT_FEASIBLE = 30 # CPX_STAT_CONFLICT_MINIMAL = 31 # CPX_STAT_FEASIBLE = 23 # CPX_STAT_FEASIBLE_RELAXED_INF = 16 # CPX_STAT_FEASIBLE_RELAXED_QUAD = 18 # CPX_STAT_FEASIBLE_RELAXED_SUM = 14 # CPX_STAT_FIRSTORDER = 24 # CPX_STAT_INFEASIBLE = 3 # CPX_STAT_INForUNBD = 4 # CPX_STAT_NUM_BEST = 6 # CPX_STAT_OPTIMAL = 1 # CPX_STAT_OPTIMAL_FACE_UNBOUNDED = 20 # CPX_STAT_OPTIMAL_INFEAS = 5 # CPX_STAT_OPTIMAL_RELAXED_INF = 17 # CPX_STAT_OPTIMAL_RELAXED_QUAD = 19 # CPX_STAT_OPTIMAL_RELAXED_SUM = 15 # CPX_STAT_UNBOUNDED = 2 @property def status(self): """ This property returns the solve status as a string. This string is normally the value returned by the CPLEX callable library method CPXXgetstatstring, see https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/refcallablelibrary/cpxapi/getstatstring.html Example: * Returns "optimal" when the solution has been proven optimal. * Returns "feasible" for a feasible, but not optimal, solution. * Returns "MIP_time_limit_feasible" for a MIP solution obtained before a time limit. Note: In certain cases, status may return a string that is not directly passed from CPLEX * If an exception occurs during the CPLEX solve phase, status contains the text of the exception. * If solve fails because of a promotional version limitation, the following message is returned "Promotional version. Problem size limits exceeded., CPLEX code=1016." The corresponding status code is 1016. """ return self._solve_status @property def problem_type(self): """ This property returns the problem type as a string. """ return self._problem_type @property def columns(self): """ This property returns the number of columns. """ return self._ncolumns @property def nb_linear_nonzeros(self): """ This property returns the number of linear non-zeros in the matrix solved. """ return self._linear_nonzeros @property def mip_relative_gap(self): """ This property returns the MIP relative gap. Note: * This property returns NaN when the problem is not a MIP. * This property returns 1e+20 for multi-objective MIP problems. * The gap is returned as a floating-point value, not as a percentage. """ return self._miprelgap gap = mip_relative_gap @property def best_bound(self): """ This property returns the MIP best bound at the end of the solve. Note: * This property returns NaN when the problem is not a MIP. * This property returns 1e+20 for multi-objective MIP problems. """ return self._best_bound @property def nb_iterations(self): """ This property returns the number of iterations at the end of the solve. Note: - The nature of the iterations depend on the algorithm usd to solve the model. - For multi-objective models, this property returns a tuple of numbers, one for each objective solved. """ return self._n_iterations @property def nb_nodes_processed(self): """ This property returns the number of nodes processed at the end of solve. Note: for multi-objective problems, this property returns a tuple of numbers, one for each objective solved. """ return self._n_nodes_processed @property def dettime(self): """ This property returns the solve deterministic time in ticks. """ return self._dettime deterministic_time = dettime def __repr__(self): return "docplex.mp.SolveDetails(time={0:g},status={1!r})" \ .format(self._time, self._solve_status) def print_information(self): print("status = {0}".format(self._solve_status)) print("time = {0:g} s.".format(self._time)) print("problem = {0}".format(self._problem_type)) print("columns = {0:d}".format(self._ncolumns)) print("iterations={0:d}".format(self._n_iterations)) # print("nonzeros= {0}".format(self._linear_nonzeros)) if self._miprelgap >= 0: print("gap = {0:g}%".format(100 * self._miprelgap)) def to_string(self): oss = StringIO() oss.write("status = {0}\n".format(self._solve_status)) oss.write("time = {0:g} s.\n".format(self._time)) oss.write("problem = {0}\n".format(self._problem_type)) if self._miprelgap >= 0: oss.write("gap = {0:g}%\n".format(100.0 * self._miprelgap)) return oss.getvalue() def __str__(self): return self.to_string() _limit_statuses = frozenset({104, # solution limit 107, 108 # time limit })
[docs] def has_hit_limit(self): """ Checks if the solve details indicate that the solve has hit a limit. Returns: Boolean: True if the solve details indicate that the solve has hit a limit. """ return self._solve_status_code in self._limit_statuses
def notify_hit_limit(self, logger): if self.has_hit_limit(): logger.info("solve: {0}".format(self.status)) @property def quality_metrics(self): return self._quality_metrics
# cplex.miprelgap: 0 # cplex.quality.int.CPX_MAX_QCSLACK: -1 # MODEL_DETAIL_CONSTRAINTS: 78 # cplex.quality.int.CPX_MAX_SCALED_PRIMAL_RESIDUAL: 66 # cplex.quality.double.CPX_SUM_QCSLACK: 0 # cplex.semiintegers: 0 # cplex.solution.type: 3 # cplex.statusstring: integer optimal solution # cplex.status: 101 # cplex.quality.double.CPX_SUM_PRIMAL_RESIDUAL: 2.10942374678779742680490016937255859375E-15 # cplex.quality.int.CPX_MAX_INDSLACK_INFEAS: -1 # cplex.numquad: 0 # cplex.quality.double.CPX_MAX_QCSLACK_INFEAS: 0 # cplex.quality.int.CPX_MAX_SLACK: 3 # cplex.quality.double.CPX_MAX_SCALED_X: 5.99999999999999911182158029987476766109466552734375 # cplex.quality.int.CPX_MAX_QCPRIMAL_RESIDUAL: -1 # cplex.quality.double.CPX_SUM_SCALED_SLACK: 10.608333333333337833437326480634510517120361328125 # MODEL_DETAIL_CONTINUOUS_VARS: 26 # cplex.nodes.processed: 0 # cplex.quality.double.CPX_MAX_SCALED_PRIMAL_INFEAS: 1.1102230246251565404236316680908203125E-15 # cplex.model.md5: B019A9CEBA436F0A65EAD04B9378517E # MODEL_DETAIL_TYPE: MILP # cplex.quality.double.CPX_MAX_PRIMAL_RESIDUAL: 4.44089209850062616169452667236328125E-16 # PROGRESS_CURRENT_OBJECTIVE: 144.266750000000001818989403545856475830078125 # cplex.quality.double.CPX_MAX_X: 5.99999999999999911182158029987476766109466552734375 # cplex.infeasible: false # MODEL_DETAIL_NON_ZEROS: 189 # MODEL_DETAIL_BOOLEAN_VARS: 40 # MODEL_DETAIL_LINEAR_CONSTRAINTS: 78 # cplex.quality.double.CPX_SUM_SCALED_PRIMAL_RESIDUAL: 2.10942374678779742680490016937255859375E-15 # cplex.qpnonzeros: 0 # cplex.quality.int.CPX_MAX_PRIMAL_INFEAS: -62 # MODEL_DETAIL_INTEGER_VARS: 0 # cplex.quality.double.CPX_MAX_INDSLACK_INFEAS: 0 # cplex.quality.double.CPX_SUM_PRIMAL_INFEAS: 4.55191440096314181573688983917236328125E-15 # cplex.quality.double.CPX_SUM_SCALED_PRIMAL_INFEAS: 4.55191440096314181573688983917236328125E-15 # cplex.solution.dfeas: true # cplex.quality.int.CPX_MAX_SCALED_PRIMAL_INFEAS: -62 # cplex.quality.double.CPX_MAX_QCSLACK: 0 # cplex.quality.double.CPX_SUM_X: 85.18333333333333712289459072053432464599609375 # cplex.quality.double.CPX_MAX_QCPRIMAL_RESIDUAL: 0 # cplex.quality.double.CPX_MAX_SLACK: 1.875 # cplex.indicatorconstraints: 0 # cplex.nodes.left: 0 # cplex.quality.double.CPX_SUM_QCPRIMAL_RESIDUAL: 0 # cplex.quality.double.CPX_MAX_SCALED_PRIMAL_RESIDUAL: 4.44089209850062616169452667236328125E-16 # PROGRESS_GAP: 4.930898076512417 # cplex.quality.double.CPX_SUM_QCSLACK_INFEAS: 0 # cplex.quality.int.CPX_MAX_QCSLACK_INFEAS: -1 # cplex.quality.double.CPX_SUM_INDSLACK_INFEAS: 0 # cplex.quality.double.CPX_MAX_PRIMAL_INFEAS: 1.1102230246251565404236316680908203125E-15 # cplex.quality.double.CPX_SUM_SCALED_X: 85.18333333333333712289459072053432464599609375 # cplex.quality.int.CPX_MAX_X: 1 # cplex.objective.sense: -1 # cplex.quality.int.CPX_MAX_PRIMAL_RESIDUAL: 66 # cplex.quality.double.CPX_MAX_INT_INFEAS: 7.7715611723760957829654216766357421875E-16 # cplex.solution.method: 12 # cplex.parameters.md5: B08B4BE748EC31D8D31294F0EBE7F26D # cplex.time: 0.03053903579711914 # cplex.quality.int.CPX_MAX_SCALED_X: 1 # cplex.quality.double.CPX_SUM_INT_INFEAS: 1.5543122344752191565930843353271484375E-15 # cplex.columns: 66 # cplex.sosconstraints: 0 # cplex.mipitcount: 5 # cplex.quality.int.CPX_MAX_INT_INFEAS: 14 # cplex.problemtype: 1 # cplex.quality.double.CPX_MAX_SCALED_SLACK: 1.875 # cplex.solution.pfeas: true # cplex.semicontinuous: 0 # cplex.quality.double.CPX_SUM_SLACK: 10.608333333333337833437326480634510517120361328125 # MODEL_DETAIL_QUADRATIC_CONSTRAINTS: 0 # cplex.mipabsgap: 0 # cplex.quality.int.CPX_MAX_SCALED_SLACK: 3 # cplex.dettime: 0.2523078918457031 # PROGRESS_BEST_OBJECTIVE: 144.266750000000001818989403545856475830078125