Source code for docplex.mp.solution

# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# http://www.apache.org/licenses/
# (c) Copyright IBM Corp. 2015, 2022
# --------------------------------------------------------------------------

import sys
import copy

try:
    from cplex._internal._subinterfaces import CutType
except:
    CutType = list

try:  # pragma: no cover
    from itertools import zip_longest as izip_longest
except ImportError:  # pragma: no cover
    # noinspection PyUnresolvedReferences
    from itertools import izip_longest

from io import StringIO
from docplex.mp.constants import CplexScope, BasisStatus, WriteLevel
from docplex.mp.utils import is_iterable, is_number, is_string, is_indexable, str_maxed, normalize_basename
from docplex.mp.utils import make_output_path2, _var_match_function
from docplex.mp.dvar import Var
from docplex.mp.error_handler import docplex_fatal, handle_error

from docplex.mp.solmst import SolutionMSTPrinter
from docplex.mp.soljson import SolutionJSONPrinter
from docplex.mp.solsol import SolutionSolPrinter

from collections import defaultdict


# noinspection PyAttributeOutsideInit
[docs]class SolveSolution(object): """ The :class:`SolveSolution` class holds the result of a solve. """ # a symbolic value for no objective ? NO_OBJECTIVE_VALUE = -1e+75 @staticmethod def _is_discrete_value(v): return v == int(v) def __init__(self, model, var_value_map=None, obj=None, blended_obj_by_priority=None, name=None, solved_by=None, keep_zeros=True): """ SolveSolution(model, var_value_map, obj, name) Creates a new solution object, associated to a a model. Args: model: The model to which the solution is associated. This model cannot be changed. obj: The value of the objective in the solution. A value of None means the objective is not defined at the time the solution is created, and will be set later. blended_obj_by_priority: For multi-objective models: the value of sub-problems' objectives (each sub-problem groups objectives having same priority). var_value_map: a Python dictionary containing associations of variables to values. name: a name for the solution. The default is None, in which case the solution is named after the model name. :return: A solution object. """ assert model is not None assert solved_by is None or is_string(solved_by) assert obj is None or is_number(obj) or is_indexable(obj) assert blended_obj_by_priority is None or is_indexable(blended_obj_by_priority) self._model = model self._name = name self._problem_objective_expr = model.objective_expr if model.has_objective() else None self._objective = self.NO_OBJECTIVE_VALUE if obj is None else obj self._blended_objective_by_priority = [self.NO_OBJECTIVE_VALUE] if blended_obj_by_priority is None else \ blended_obj_by_priority self._solved_by = solved_by self._var_value_map = {} # attributes self._sensitivity = {} self._cuts = None self._reduced_costs = None self._dual_values = None self._slack_values = None self._infeasibilities = {} self._basis_statuses = None self._solve_status = None self._keep_zeros = keep_zeros self._solve_details = None if var_value_map is not None: self._store_var_value_map(var_value_map, keep_zeros=keep_zeros) @property def _checker(self): return self._model._checker @staticmethod def make_engine_solution(model, var_value_map, obj, blended_obj_by_priority, solved_by, solve_details, job_solve_status=None): # INTERNAL # noinspection PyArgumentEqualDefault sol = SolveSolution(model, var_value_map=None, obj=obj, blended_obj_by_priority=blended_obj_by_priority, solved_by=solved_by, keep_zeros=False) if solve_details is not None: sol._solve_details = copy.copy(solve_details) if model.round_solution: # only for models which specify round_solution roundfn = sol._model._round_function for dvar, value in var_value_map.items(): if value and dvar.is_discrete() and value != int(value): var_value_map[dvar] = roundfn(value) # do trust engines... sol._var_value_map = var_value_map if job_solve_status is not None: sol._set_solve_status(job_solve_status) return sol def _get_var_by_name(self, varname): return self._model.get_var_by_name(varname) def as_mip_start(self, write_level=WriteLevel.DiscreteVars, complete_vars=False, eps_zero=1e-6): write_level_ = WriteLevel.parse(write_level) filter_discrete = write_level_.filter_nondiscrete() filter_zeros = write_level_.filter_zeros() mdl = self.model mipstart_dict = {} def generate_completed_var_values(): for dv_ in mdl.generate_user_variables(): yield dv_, self._get_var_value(dv_) if not complete_vars: vv_iter = self.iter_var_values() else: vv_iter = generate_completed_var_values() for dv, dvv in vv_iter: if filter_discrete and not dv.is_discrete(): continue if filter_zeros and abs(dvv) <= eps_zero: continue mipstart_dict[dv] = dvv mipstart = SolveSolution(self.model, name=self.name, var_value_map=None, obj=self.objective_value, solved_by=self.solved_by, keep_zeros=True) mipstart._var_value_map = mipstart_dict return mipstart
[docs] def clear(self): """ Clears all solve result data. All data related to the model are left unchanged. """ self._var_value_map = {} self._objective = self.NO_OBJECTIVE_VALUE self._reduced_costs = None self._dual_values = None self._slack_values = None self._infeasibilities = {} self._solve_status = None self._cuts = None self._sensitivity = {}
[docs] def is_empty(self): """ Checks whether the solution is empty. Returns: Boolean: True if the solution is empty; in other words, the solution has no defined objective and no variable value. """ return not self.has_objective() and not self._var_value_map
@property def problem_name(self): return self._model.name @property def solved_by(self): ''' Returns a string indicating how the solution was produced. - If the solution was created by a program, this field returns None. - If the solution originated from a local CPLEX solve, this method returns the string 'cplex_local'. - If the solution originated from a DOcplexcloud solve, this method returns 'cplex_cloud'. Returns: A string, or None. ''' return self._solved_by def set_name(self, solution_name): self._checker.typecheck_string(solution_name, accept_empty=False, accept_none=True, caller='SolveSolution.set_name(): ') self._name = solution_name @property def name(self): """ This property allows to get/set a name on the solution. In some cases , it might be interesting to build different solutions for the same model, in this case, use the name property to distinguish them. """ return self._name @name.setter def name(self, sol_name): self.set_name(sol_name) def _resolve_var(self, var_key, do_raise): # INTERNAL: accepts either strings or variable objects # returns a variable or None if isinstance(var_key, Var): return var_key elif is_string(var_key): var = self._get_var_by_name(var_key) # var might be None here if the name is unknown if var is not None: return var # var is None hereafter elif do_raise: self.model.fatal("No variable with named {0}", var_key) else: self.model.warning("No variable with named {0}", var_key) return None else: # pragma: no cover self.model.fatal("Expecting variable or name, got: {0!r}", var_key) def _typecheck_var_key_value(self, var_key, value, caller): # INTERNAL self._checker.typecheck_num(value, caller=caller) if not is_string(var_key) and not isinstance(var_key, Var): self.model.fatal("{0} expects either Var or string, got: {1!r}", caller, var_key)
[docs] def add_var_value(self, var_key, value): """ Adds a new (variable, value) pair to this solution. Args: var_key: A decision variable (:class:`docplex.mp.dvar.Var`) or a variable name (string). value (number): The value of the variable in the solution. """ self._typecheck_var_key_value(var_key, value, caller="Solution.add_var_value") self._set_var_key_value(var_key, value, keep_zero=self._keep_zeros)
def __setitem__(self, var_key, value): # always keep zero, no warnings, no checks self._set_var_key_value(var_key, value, keep_zero=self._keep_zeros) def set_var_key_value(self, var_key, value, keep_zero): # INTERNAL self._typecheck_var_key_value(var_key, value, caller="Solution.add_var_value") self._set_var_key_value(var_key, value, keep_zero) def _set_var_key_value(self, var_key, value, keep_zero): # INTERNAL: no checks done. dvar = self._resolve_var(var_key, do_raise=False) if dvar is not None: if value or keep_zero: # either value is nonzero or we keep all, store. self._set_var_value_internal(dvar, value) elif self.contains(dvar): # value is 0 and we dont keep zeros: zap the variable, if del self._var_value_map[dvar] def _set_var_value_internal(self, var, value): self._var_value_map[var] = value def _set_var_value(self, var, value): # INTERNAL self._set_var_value_internal(var, value)
[docs] def update(self, var_values_iterable): """ Updates the solution from a dictionary. Keys can be either strings, interpreted as variable names, or variables; values are the new values for the variable. This method returns nothing, only performs a side effect on the solution object. :param var_values_iterable: a dictionary of keys, values. """ keep_zeros = self._keep_zeros for k, v in var_values_iterable.items(): self._set_var_key_value(k, v, keep_zeros)
@property def model(self): """ This property returns the model associated with the solution. """ return self._model @property def solve_details(self): """ This property returns the solve_details associated with the solution,if any. Note: This property returns an instance of solve details if the solution is the result of a solve operation. If the solution has been created by API, this property returns None See Also: :class:`docplex.mp.sdetails.SolveDetails` Returns: an instance of SolveDetails, or None. """ return self._solve_details # @property # def error_handler(self): # return self.__model.error_handler
[docs] def get_objective_value(self): """ Gets the objective value (or list of objectives value) as defined in the solution. When the objective value has not been defined, a special value `NO_SOLUTION` is returned. To check whether the objective has been set, use :func:`has_objective`. Returns: float or list(float): The value of the objective (or list of values for multi-objective) as defined by the solution. """ return self._objective
[docs] def set_objective_value(self, obj): """ Sets the objective value (or list of values for multi-objective) of the solution. Args: obj (float or list(float)): The value of the objective (or list of values for multi-objective) in the solution. """ self._objective = obj
[docs] def get_blended_objective_value_by_priority(self): """ Gets the blended objective value (or list of blended objectives value) by priority level as defined in the solution. When the objective value has not been defined, a special value `NO_SOLUTION` is returned. To check whether the objective has been set, use :func:`has_objective`. Returns: float or list(float): The value of the objective (or list of values for multi-objective) as defined by the solution. """ return self._blended_objective_by_priority
@property def blended_objective_values(self): return self._blended_objective_by_priority
[docs] def has_objective(self): """ Checks whether or not the objective has been set. Returns: Boolean: True if the solution defines an objective value. """ return self._objective != self.NO_OBJECTIVE_VALUE
@property def objective_value(self): """ This property is used to get the objective value of the solution. In case of multi-objective this property returns the value for the first objective When the objective value has not been defined, a special value `NO_SOLUTION` is returned. To check whether the objective has been set, use :func:`has_objective`. """ try: return self._objective[0] except TypeError: return self._objective @objective_value.setter def objective_value(self, new_objvalue): self.set_objective_value(new_objvalue) @property def multi_objective_values(self): """ This property is used to get the list of objective values of the solution. In case of single objective this property returns the value for the objective as a singleton list When the objective value has not been defined, a special value `NO_SOLUTION` is returned. To check whether the objective has been set, use :func:`has_objective`. """ self_obj = self._objective return self_obj if is_indexable(self_obj) else [self_obj] @property def solve_status(self): return self._solve_status def _set_solve_status(self, new_status): # INTERNAL self._solve_status = new_status def _store_var_value_map(self, key_value_map, keep_zeros=False): # INTERNAL for e, val in key_value_map.items(): # need to check var_keys and values self.set_var_key_value(var_key=e, value=val, keep_zero=keep_zeros) def store_infeasibilities(self, infeasibilities): assert isinstance(infeasibilities, dict) self._infeasibilities = infeasibilities @staticmethod def _resolve_attribute_index_map(attr_idx_map, obj_mapper): return {obj_mapper(idx): attr_val for idx, attr_val in attr_idx_map.items() if attr_val and obj_mapper(idx) is not None} @classmethod def _resolve_attribute_list(cls, attr_list, obj_mapper): # attr list is a list of length N and obj_mapper maps indices to objs return {obj_mapper(idx): attr_val for idx, attr_val in enumerate(attr_list)} def store_attribute_lists(self, mdl, slacks): def linct_mapper(idx): return mdl.get_constraint_by_index(idx) resolved_linear_slacks = self._resolve_attribute_list(slacks, linct_mapper) self._slack_values = defaultdict(dict) self._slack_values[CplexScope.LINEAR_CT_SCOPE] = resolved_linear_slacks
[docs] def iter_var_values(self): """Iterates over the (variable, value) pairs in the solution. Returns: iterator: A dict-style iterator which returns a two-component tuple (variable, value) for all variables mentioned in the solution. """ return self._var_value_map.items()
[docs] def iter_variables(self): """Iterates over all variables mentioned in the solution. Returns: iterator: An iterator object over all variables mentioned in the solution. """ return self._var_value_map.keys()
[docs] def contains(self, dvar): """ Checks whether or not a decision variable is mentioned in the solution. This predicate can also be used in the form `var in solution`, because the :func:`__contains_` method has been redefined for this purpose. Args: dvar (:class:`docplex.mp.dvar.Var`): The variable to check. Returns: Boolean: True if the variable is mentioned in the solution. """ return dvar in self._var_value_map
def __contains__(self, dvar): return self.contains(dvar)
[docs] def get_value(self, arg): """ Gets the value of a variable or an expression in a solution. If the variable is not mentioned in the solution, the method returns 0 and does not raise an exception. Note that this method can also be used as :func:`solution[arg]` because the :func:`__getitem__` method has been overloaded. Args: arg: A decision variable (:class:`docplex.mp.dvar.Var`), a variable name (a string), or an expression. Returns: float: The value of the variable in the solution. """ if is_string(arg): var = self._get_var_by_name(arg) if var is None: self.model.fatal("No variable with name: {0}", arg) else: return self._get_var_value(var) elif isinstance(arg, Var): return self._get_var_value(arg) else: try: v = arg._raw_solution_value(self) return v except AttributeError: self._model.fatal("Expecting variable, variable name or expression, {0!r} was passed", arg)
def get_var_value(self, dvar): self._checker.typecheck_var(dvar) return self._get_var_value(dvar) def _get_var_value(self, dvar): # INTERNAL return self._var_value_map.get(dvar, 0)
[docs] def get_value_list(self, dvars): """ Gets the value of a sequence of variables in a solution. If a variable is not mentioned in the solution, the method assumes a 0 value. Args: dvars: an ordered sequence of decision variables. Returns: list: A list of float values, in the same order as the variable sequence. """ checker = self._checker checker.check_ordered_sequence(arg=dvars, caller='SolveSolution.get_values() expects ordered sequence of variables') dvar_seq = checker.typecheck_var_seq(dvars) return self._get_values(dvar_seq)
[docs] def get_values(self, var_seq): """ Same as get_value_list """ return self.get_value_list(var_seq)
def _get_values(self, dvars): # internal: no checks are done. self_value_map = self._var_value_map return [self_value_map.get(dv, 0) for dv in dvars] def _get_all_values(self): # internal: no checks are done. self_value_map = self._var_value_map m = self._model return [self_value_map.get(dv, 0) for dv in m.iter_variables()] @staticmethod def _accept_value(value, accept_zeros: bool, precision: float = 1e-6): # INTERNAL if not value: # accepting zero values is controlled by the accept_zeros flag. return accept_zeros else: return abs(value) >= precision
[docs] def get_value_dict(self, var_dict, keep_zeros=True, precision=1e-6): """ Converts a dictionary of variables to a dictionary of solutions Assuming `var_dict` is a dictionary of variables (for example, as returned by `Model.integer_var_dict()`, returns a dictionary with the same keys and as values the solution values of the variables. :param var_dict: a dictionary of decision variables. :param keep_zeros: an optional flag to keep zero values (default is True) :param precision: an optional precision, used to filter small non-zero values. The default is 1e-6. :return: A dictionary from variable keys to solution values (floats). """ # assume var_dict is a key-> variable dictionary assert precision >= 0 # if precision -> abs(dvv) >= prec else dvv value_dict = {} for key, dvar in var_dict.items(): dvar_value = self._get_var_value(dvar) if self._accept_value(dvar_value, keep_zeros, precision=precision): value_dict[key] = dvar_value return value_dict
[docs] def get_value_df(self, var_dict, value_column_name=None, key_column_names=None): """ Returns values of a dicitonary of variables, as a pandas dataframe. If pandas is not present, returns a dicitonary of columns. :param var_dict: the dicitonary of variables, as created by Model.xx_var_dict :param value_column_name: an optional string to name the value column. Default is 'value' :param key_column_names: an optional list of strings to name th ekeys of the dicitonary. If not present, keys are named 'k1', 'k2', ... :return: a pandas DataFrame, if pandas is present. """ keys = list(var_dict.keys()) values = self.get_values((dv for dv in var_dict.values())) if isinstance(keys[0], tuple): keys = list(zip(*keys)) knames = None if key_column_names: if len(key_column_names) == len(keys): knames = key_column_names if not knames: knames = ['key_%d' % k for k in range(1, len(keys) + 1)] kd = {kn: ks for kn, ks in zip(knames, keys)} else: kn = key_column_names or 'key' kd = {kn: keys} value_col_name = value_column_name or 'value' kd[value_col_name] = values try: import pandas as pd return pd.DataFrame(kd) except ImportError: self.model.warning("pandas module not found, returning a dict instead of DataFrame") return kd
# def __len__(self): # return len(self.__var_value_map) @property def number_of_var_values(self): """ This property returns the number of variable/value pairs stored in this solution. """ return len(self._var_value_map) @property def size(self): """ This property returns the number of variable/value pairs stored in this solution. """ return len(self._var_value_map) def __getitem__(self, arg): return self.get_value(arg)
[docs] def get_status(self, ct): """ Returns the status of a linear constraint in the solution. Returns 1 if the constraint is satisfied, else returns 0. This is particularly useful when using the status variable of constraints. :param ct: A linear constraint :return: a number (1 or 0) """ self._checker.typecheck_linear_constraint(ct) return self._get_status(ct)
def _get_status(self, ct): # INTERNAL ct_status_var = ct._get_status_var() if ct_status_var: return self._var_value_map.get(ct_status_var, 0) elif ct.is_added(): # a posted constraint is true if there is a solution return 1 else: return 1 if ct.is_satisfied(self) else 0 def find_unsatisfied_constraints(self, m, tolerance=1e-6): unsats = [] for ct in m.iter_constraints(): if not ct.is_satisfied(self, tolerance): unsats.append(ct) return unsats def number_of_var_diffs(self, other_sol, precision=1e-6, match="auto"): target_model = other_sol.model var_match_fn = _var_match_function(self.model, target_model, match) nb_diffs = 0 for dv, dvv in self.iter_var_values(): other_dv = var_match_fn(dv, target_model) if other_dv: other_dvv = other_sol[other_dv] if abs(dvv - other_dvv) >= precision: nb_diffs += 1 return nb_diffs def restore(self, target_model, abs_tolerance=1e-6, rel_tolerance=1e-4, restore_all=False, match="auto"): # restores the solution in its model, adding ranges. find_matching_var = _var_match_function(self.model, target_model, match) lfactory = target_model._lfactory restore_ranges = [] for dvar, val in self.iter_var_values(): if not dvar.is_generated() or restore_all: dvar2 = find_matching_var(dvar, target_model) if dvar2 is not None: rel_prec = abs(val) * rel_tolerance used_prec = max(abs_tolerance, rel_prec) rlb = max(dvar2.lb, val - used_prec) rub = min(dvar2.ub, val + used_prec) if rlb >= rub + 1e-6: target_model.fatal("restore solution fails on empty domain, var: {)}, lb={1} > ub={2}", dvar2, rlb, rub) restore_ranges.append(lfactory.new_range_constraint(rlb, dvar2, rub)) else: print("could not find matching var for {0}".format(dvar)) target_model.info("restored {0} variable values using range constraints".format(len(restore_ranges))) return target_model.add(restore_ranges) def find_invalid_domain_variables(self, m, tolerance=1e-6): invalid_domain_vars = [] for dv in m.iter_variables(): dvv = self.get_var_value(dv) if not dv.accepts_value(dvv, tolerance=tolerance): invalid_domain_vars.append(dv) return invalid_domain_vars
[docs] def is_valid_solution(self, tolerance=1e-6, silent=True): """ Returns True if the solution is feasible. This method checks that solution values for variables are compatible for their types and bounds. It also checks that all constraints are satisfied, within the tolerance. :param tolerance: a float number used to check satisfaction; default is 1e-6. :param silent: optional flag. If False, prints which variable (or constraint) causes the solution to be invalid. default is True. :return: True if the solution is valid, within the tolerance value. *New in version 2.13* """ m = self.model verbose = not silent invalid_domain_vars = self.find_invalid_domain_variables(m, tolerance) if verbose and invalid_domain_vars: m.warning("invalid domain vars: {0}".format(len(invalid_domain_vars))) for v, invd_var in enumerate(invalid_domain_vars, start=1): dvv = self.get_var_value(invd_var) m.warning("{0} - invalid value {1} for variable {2}({5}), [{3}, {4}]".format(v, dvv, invd_var.lp_name, invd_var.lb, invd_var.ub, invd_var.cplex_typecode)) unsat_cts = self.find_unsatisfied_constraints(m, tolerance) if verbose and unsat_cts: m.info("unsatisfied constraints[{0}]".format(len(unsat_cts))) for u, uct in enumerate(unsat_cts, start=1): if uct.is_logical(): # TODO: compute a measure of violation on logical cts s_violated = '' else: uctv = uct._compute_violation(self, tolerance) s_violated = f', violated: {uctv:0.3g}' s_uct = uct.to_readable_string() ctx = uct.index + 1 m.warning("{0} - unsatisfied constraint[#{1}]: {2}{3}".format(u, ctx, s_uct, s_violated)) return not (invalid_domain_vars or unsat_cts)
is_feasible_solution = is_valid_solution def equals(self, other, check_models=False, obj_precision=1e-3, var_precision=1e-6, assume_equal_indices=True): from itertools import dropwhile if check_models and (self.model is not other.model): return False if is_iterable(self.objective_value) and is_iterable(other.objective_value): if len(self.objective_value) == len(other.objective_value): for self_obj_val, other_obj_val in zip(self.objective_value, other.objective_value): if abs(self_obj_val - other_obj_val) >= obj_precision: return False else: # Different number of objectives return False elif not is_iterable(self.objective_value) and not is_iterable(other.objective_value): if abs(self.objective_value - other.objective_value) >= obj_precision: return False else: # One solution is for multi-objective, and not the other return False # noinspection PyPep8 this_triplets = [(dv.index, dv.name, svalue) for dv, svalue in dropwhile(lambda dvv: not dvv[1], self.iter_var_values())] other_triplets = [(dv.index, dv.name, svalue) for dv, svalue in dropwhile(lambda dvv: not dvv[1], other.iter_var_values())] # noinspection PyArgumentList res = True for this_triple, other_triple in zip(this_triplets, other_triplets): this_index, this_name, this_val = this_triple other_index, other_name, other_val = other_triple if (assume_equal_indices and (other_index != this_index)) \ or this_name != other_name \ or abs(this_val - other_val) >= var_precision: res = False break return res def ensure_reduced_costs(self, model, engine): if self._reduced_costs is None: self._reduced_costs = engine.get_all_reduced_costs(model) def ensure_cuts(self, model, engine): if self._cuts is None: self._cuts = engine.get_all_cuts(model) def ensure_dual_values(self, model, engine): if self._dual_values is None: self._dual_values = engine.get_all_dual_values(model) def ensure_slack_values(self, model, engine): if self._slack_values is None: self._slack_values = engine.get_all_slack_values(model) def ensure_basis_statuses(self, model, engine): if self._basis_statuses is None: # returns a tuple of two lists self._basis_statuses = engine.get_basis(model) def has_basis(self): m = self.model self.ensure_basis_statuses(m, m.get_engine()) return self._has_basis() def _has_basis(self): try: return len(self._basis_statuses[0]) > 0 except TypeError: return False
[docs] def get_sensitivity(self, dvars): """ Returns the sensitivity values for a variable iterable. Note: The model must be solved successfully before calling this method. :param dvars: a sequence of variables. :return: a list of tuples, in the same order as the variable sequence. Each tuple contains 3 tuples: the lower lower_bounds, the upper_bounds and the objective For example [((-1e+20, 2.5), (-3.0, 5.0), (-1e+20, -2.0), (0.0, 4.0)), ((-1e+20, 2.5), (-3.0, 5.0), (-1e+20, -2.0), (0.0, 4.0))] """ ret = [None for d in dvars] idx = {d : i for i,d in enumerate(dvars)} todo = [] for d in dvars: if d in self._sensitivity.keys(): sensitivity = self._sensitivity[d] i = idx[d] ret[i] = sensitivity else: todo.append(d) if len(todo) != 0: m = self.model values = m.get_engine().get_sensitivity(todo) for d,v in zip(todo, values): i = idx[d] ret[i] = v return ret
[docs] def get_num_cuts(self, cut_type): """ Returns the number of cuts for a specific type. :param cut_type: a cut type. :return: the number of cuts associated to this type of cut. 0 if CPLEX is not present """ cut_type_instance = CutType() if cut_type in cut_type_instance: cuts = self.get_cuts() name = cut_type_instance[cut_type] return cuts[name] return 0
[docs] def get_cuts(self): """ Returns the number of cuts under the form of a dict(type -> number). :return: the number of cuts under the form of a dict(type -> number). Empty dict if CPLEX is not present. """ m = self.model self.ensure_cuts(m, m.get_engine()) return self._cuts
[docs] def get_reduced_costs(self, dvars): """ Returns the reduced costs for a variable iterable. Note: the model must a pure LP: no integer or binary variable, no piecewise, no SOS. The model must also be solved successfully before calling this method. :param dvars: a sequence of variables. :return: a list of float numbers, in the same order as the variable sequence. """ m = self.model self.ensure_reduced_costs(m, m.get_engine()) rcs = self._reduced_costs assert rcs is not None return [rcs.get(dv, 0) for dv in dvars]
[docs] def get_dual_values(self, lcts): """ Returns the dual values of a sequence of linear constraints. Note: the model must a pure LP: no integer or binary variable, no piecewise, no SOS. The model must also be solved successfully before calling this method. :param lcts: a sequence of linear constraints. :return: a sequence of float numbers """ duals = self._dual_values assert duals is not None return [duals.get(lc, 0) for lc in lcts]
[docs] def get_slacks(self, cts): """ Return the slack values for a sequence of constraints. Slack values are available for linear, quadratic and indicator constraints. The model must be solved successfully before calling this method. :param cts: a sequence of constraints. :return: a list of float values, in the same order as the constraints. """ all_slacks = self._slack_values assert all_slacks is not None # first get cplex_scope, then fetch the slack: two indirections return [all_slacks[ct.cplex_scope].get(ct, 0) for ct in cts]
[docs] def slack_value(self, ct, error='raise'): """ Return the slack value for a constraint. Slack values are available for linear, quadratic and indicator constraints. The model must be solved successfully before calling this method. :param ct: a constraint. :return: the float value. """ all_slacks = self._slack_values slack = 0 if all_slacks is None: handle_error(logger=self.model, error=error, msg="Solution contains no slack data") else: slack = all_slacks[ct.cplex_scope].get(ct, 0) return slack
def get_var_basis_statuses(self, dvars): assert self._basis_statuses is not None all_var_basis_statuses = self._basis_statuses[0] return [BasisStatus.parse(all_var_basis_statuses.get(dv, -1)) for dv in dvars] def get_linearct_basis_statuses(self, linear_cts): assert self._basis_statuses is not None all_linearct_basis_statuses = self._basis_statuses[1] return [BasisStatus.parse(all_linearct_basis_statuses.get(lct, -1)) for lct in linear_cts] def get_infeasibility(self, ct): return self._infeasibilities.get(ct, 0) def display_attributes(self): pass def display(self, print_zeros=True, header_fmt="solution for: {0:s}", objective_fmt="{0}: {1:.{prec}f}", value_fmt="{varname:s} = {value:.{prec}f}", iter_vars=None, **kwargs): print_generated = kwargs.get("print_generated", False) problem_name = self.problem_name if header_fmt and problem_name: print(header_fmt.format(problem_name)) if self._problem_objective_expr is not None and objective_fmt and self.has_objective(): obj_prec = self.model.objective_expr.float_precision print(objective_fmt.format('objective', self._objective, prec=obj_prec)) if self.solve_status is not None: print("status: %s(%d)" %(self.solve_status.name,self.solve_status.value)) if self.solve_details is not None and len(self.solve_details.quality_metrics) != 0: for k,v in self.solve_details.quality_metrics.items(): if abs(v) > 1e-16: if isinstance(v,int): print("%s: %d" %(k,v)) else: print("%s: %f16" % (k, v)) if iter_vars is None: iter_vars = self.iter_variables() print_counter = 0 for dvar in iter_vars: if print_generated or not dvar.is_generated(): var_value = self._get_var_value(dvar) if print_zeros or var_value: print_counter += 1 varname = dvar.lp_name # if type(value_fmt) != type(varname): # # infamous mix of str and unicode. Should happen only # # in py2. Let's convert things # if isinstance(value_fmt, str): # # noinspection PyUnresolvedReferences # value_fmt = value_fmt.decode('utf-8') # else: # value_fmt = value_fmt.encode('utf-8') output = value_fmt.format(varname=varname, value=var_value, prec=dvar.float_precision, counter=print_counter) try: print(output) except UnicodeEncodeError: encoding = 'ascii' if hasattr(sys.stdout, 'encoding') and sys.stdout.encoding: encoding = sys.stdout.encoding print(output.encode(encoding, errors='backslashreplace')) def to_string(self, print_zeros=True): oss = StringIO() self.to_stringio(oss, print_zeros=print_zeros) return oss.getvalue() def to_stringio(self, oss, print_zeros=True): problem_name = self.problem_name if problem_name: oss.write("solution for: %s\n" % problem_name) if self._problem_objective_expr is not None and self.has_objective(): oss.write("objective: %g\n" % self._objective) if self.solve_status is not None: oss.write("status: %s(%d)\n" %(self.solve_status.name,self.solve_status.value)) if self.solve_details is not None and len(self.solve_details.quality_metrics) != 0: for k,v in self.solve_details.quality_metrics.items(): if abs(v) > 1e-16: if isinstance(v,int): oss.write("%s: %d\n" %(k,v)) else: oss.write("%s: %f16\n" % (k, v)) value_fmt = "{var:s}={value:.{prec}f}" for dvar, val in self.iter_var_values(): if not dvar.is_generated(): var_value = self._get_var_value(dvar) if print_zeros or var_value != 0: oss.write(value_fmt.format(var=str(dvar), value=var_value, prec=dvar.float_precision)) oss.write("\n") def __str__(self): return self.to_string() def __repr__(self): if self.has_objective(): s_obj = "obj={0:g}".format(self.objective_value) else: s_obj = "obj=N/A" s_values = ",".join(["{0!s}:{1:g}".format(var, val) for var, val in self.iter_var_values()]) r = "docplex.mp.solution.SolveSolution({0},values={{{1}}})".format(s_obj, s_values) return str_maxed(r, maxlen=72) def __iter__(self): # INTERNAL: this is necessary to prevent solution from being an iterable. # as it follows getitem protocol, it can mistakenly be interpreted as an iterable raise TypeError def __as_df__(self, name_key='name', value_key='value'): return self.as_df(name_key, value_key)
[docs] def as_df(self, name_key='name', value_key='value'): """ Converts the solution to a pandas dataframe with two columns: variable name and values :param name_key: column name for variable names. Default is 'name' :param value_key: cilumn name for values., Default is 'value'. :return: a pandas dataframe, if pandas is present. *New in version 2.15* """ assert name_key assert value_key assert name_key != value_key try: import pandas as pd except ImportError: raise ImportError('Cannot convert solution to pandas.DataFrame if pandas is not available') names = [] values = [] for dv, dvv in self.iter_var_values(): names.append(dv.to_string()) values.append(dvv) name_value_dict = {name_key: names, value_key: values} return pd.DataFrame(name_value_dict)
[docs] def print_mst(self, outs=None, **kwargs): """ Writes the solution in MST format in an output stream (default is sys.out) """ if outs is None: outs = sys.stdout self.export(outs, format='mst', **kwargs)
def _export_as_string(self, format_spec, **kwargs): # INTERNAL printer = self._new_printer(format_spec) return printer.print_to_string(self, **kwargs) def export_as_mst_string(self, write_level=WriteLevel.Auto, **kwargs): kwargs['write_level'] = WriteLevel.parse(write_level) return self._export_as_string(format_spec='mst', **kwargs)
[docs] def export_as_mst(self, path=None, basename=None, write_level=WriteLevel.Auto, **kwargs): """ Exports a solution to a file in CPLEX mst format. Args: basename: Controls the basename with which the solution is printed. Accepts None, a plain string, or a string format. If None, the model's name is used. If passed a plain string, the string is used in place of the model's name. If passed a string format (either with %s or {0}), this format is used to format the model name to produce the basename of the written file. path: A path to write the file, expects a string path or None. Can be a directory, in which case the basename that was computed with the basename argument is appended to the directory to produce the file. If given a full path, the path is directly used to write the file, and the basename argument is not used. If passed None, the output directory will be ``tempfile.gettempdir()``. write_level: an enumerated value which controls which variables are printed. The default is WriteLevel.Auto, which prints the values of all discrete variables. This parameter also accepts the number values of the corresponding CPLEX parameters (1 for AllVars, 2 for DiscreteVars, 3 for NonZeroVars, 4 for NonZeroDiscreteVars) Returns: The full path of the file, when successful, else None Examples: Assuming the solution has the name "prob": ``sol.export_as_mst()`` will write file prob.mst in a temporary directory. ``sol.export_as_mst(write_level=WriteLevel.ALlvars)`` will write file prob.mst in a temporary directory, and will print all variables in the problem. ``sol.export_as_mst(path="c:/temp/myprob1.mst")`` will write file "c:/temp/myprob1.mst". ``sol.export_as_mst(basename="my_%s_mipstart", path ="z:/home/")`` will write "z:/home/my_prob_mipstart.mst". Note: The complete description of MST format is found here: https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/CPLEX/FileFormats/topics/MST.html See Also: :class:`docplex.mp.constants.WriteLevel` """ kwargs2 = kwargs.copy() kwargs2['write_level'] = WriteLevel.parse(write_level) return self._export(format_spec='mst', path=path, basename=basename, **kwargs2)
[docs] def export_as_sol(self, path=None, basename=None, **kwargs): """ Exports a solution to a file in CPLEX SOL format. SOL format is valid for all types of solutions, LP or MIP, but cannot be used for warm starts. Arguments are identical to the method :func:`export_as_mst` Note: The complete description of SOL format is found here: https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/CPLEX/FileFormats/topics/SOL.html See Also: :func:`docplex.mp.model.SolveSolution.export_as_mst` """ return self._export(format_spec='sol', path=path, basename=basename, **kwargs)
def _export(self, format_spec, path=None, basename=None, **kwargs): # INTERNAL printer = self._new_printer(format_spec) return self._static_export(exported=self, basename=self.problem_name, printer=printer, path=path, basename_fmt=basename, **kwargs ) @classmethod def _static_export(cls, exported, basename, printer, path, basename_fmt, **kwargs): sol_basename = normalize_basename(basename, force_lowercase=True) mst_path = make_output_path2(actual_name=sol_basename, extension=printer.extension(), path=path, basename_fmt=basename_fmt) if mst_path: printer.print_to_stream(exported, mst_path, **kwargs) return mst_path # noinspection PyPep8 @classmethod def _new_printer(cls, format_spec): printers = {'json': SolutionJSONPrinter, 'xml': SolutionMSTPrinter, 'mst': SolutionMSTPrinter, 'sol': SolutionSolPrinter } printer_type = printers.get(format_spec.lower()) if not printer_type: raise ValueError("format key must be one of {}".format(printers.keys())) return printer_type()
[docs] def export(self, file_or_filename, format="json", **kwargs): """ Export this solution. Args: file_or_filename: If ``file_or_filename`` is a string, this argument contains the filename to write to. If this is a file object, this argument contains the file object to write to. format: A string, the name of format to use. Possible values are: - "json" - "mst": the MST cplex format for MIP starts - "xml": same as MST kwargs: additional kwargs passed to the actual exporter """ printer = self._new_printer(format) if isinstance(file_or_filename, str): fp = open(file_or_filename, "w") close_fp = True else: fp = file_or_filename close_fp = False try: printer.print_to_stream(self, fp, **kwargs) finally: if close_fp: fp.close()
[docs] def export_as_json_string(self, **kwargs): """ Returns the solution as a string in JSON format. :return: a string. *New in version 2.10* """ return self._export_as_string(format_spec='json', **kwargs)
def export_as_sol_string(self, **kwargs): return self._export_as_string(format_spec='sol', **kwargs)
[docs] def check_as_mip_start(self, strong_check=False): """Checks that this solution is a valid MIP start. To be valid, it must have: * at least one discrete variable (integer or binary), and * the values for decision variables should be consistent with the type. Returns: Boolean: True if this solution is a valid MIP start. """ count_values = 0 count_errors = 0 m = self.model for dv, dvv in self.iter_var_values(): if dv.is_discrete() and not dv.is_generated(): count_values += 1 if not dv.accepts_value(dvv): # pragma: no cover count_errors += 1 m.warning("Solution value {1} is outside the domain of variable {0!r}: {1}, type: {2!s}", dv, dvv, dv.vartype.short_name) if count_values == 0: docplex_fatal("MIP start contains no discrete variable") # pragma: no cover return not count_errors if strong_check else True
def as_dict(self, keep_zeros=False): var_value_dict = {} # INTERNAL: return a dictionary of variable: variable_value for dvar, dval in self.iter_var_values(): if keep_zeros or dval: var_value_dict[dvar] = dval return var_value_dict def as_name_dict(self, keep_zeros=False, error='ignore'): # return a dictionary of variable_name: variable_value def var_name_or_lp_name(dvar_): return dvar_.name or dvar_.lp_name return self._as_dict(var_name_or_lp_name, keep_zeros, error) def as_index_dict(self, keep_zeros=False, error='ignore'): # return a dictionary of var index: variable_value # invalid indices are ignored def var_valid_index(dvar_): var_idx = dvar_.index return var_idx if var_idx >= 0 else None return self._as_dict(var_valid_index, keep_zeros, error) def _as_dict(self, var_to_key_fn, keep_zeros=False, error='ignore'): # INTERNAL key_value_dict = {} for dvar, dval in self.iter_var_values(): if keep_zeros or dval: dvar_key = var_to_key_fn(dvar) if dvar_key is not None: key_value_dict[dvar_key] = dval else: msg = ("Invalid variable key in solution.as_dict, variable: {0}, transformer: {1}" .format(dvar, var_to_key_fn.__name__)) handle_error(logger=self.model, error=error, msg=msg) return key_value_dict
[docs] def kpi_value_by_name(self, name, match_case=False): ''' Returns the solution value of a KPI from its name. Args: name (string): The string to be matched. match_case (boolean): If True, looks for a case-exact match, else ignores case. Default is False. Returns: The value of the KPI, evaluated in the solution. Note: This method raises an error when the string does not match any KPI in the model. See: :func: `docplex.mp.model.kpi_by_name` ''' kpi = self.model.kpi_by_name(name, try_match=True, match_case=match_case) return kpi._raw_solution_value(self)
[docs] @classmethod def from_file(cls, filename, mdl): """ Builds solution(s) from a SOL file. Assumes `filename` is in CPLEX SOL format, reference: https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/CPLEX/FileFormats/topics/SOL.html Returns: a list of solution objects, read from the file, or None, if an error occured. """ from docplex.mp.sol_xml_reader import read_sol_file sols = read_sol_file(filename, mdl) return sols
[docs]class SolutionPool(object): """SolutionPool() Solutions pools as returned by `Model.populate()` This class is not to be instantiated by users, only used after returned by Model.populate. Instances of this class can be used like lists. They are fully iterable, and accessible by index. See Also: :func:`docplex.mp.model.Model.populate` """ def __init__(self, sols, num_replaced=0): self._solutions = tuple(sols) self._num_replaced = num_replaced def __iter__(self): """ Returns an iterator on pool solutions. """ return iter(self._solutions) def __len__(self): """ Returns the number of solutions in the pool. """ return self.size @property def size(self): """ Returns the number of solutions in the pool. :return: """ return len(self._solutions) def __getitem__(self, item): return self._solutions[item] def __str__(self): return 'SolutionPool[{0}](mean={1:.3f})'.format(len(self), self.mean_objective_value) def __repr__(self): return "docplex.mp.SolutionPool[{0}]".format(len(self)) @property def num_replaced(self): return self._num_replaced @property def mean_objective_value(self): """ This property returns the mean objective value in the pool. """ return self.stats[1]
[docs] def describe_objectives(self): """ Prints statistical information about poolobjective values. Relies on the `stats` property. """ nb_solutions, obj_mean, obj_sd, obj_min, obj_med, obj_max = self.stats print("count = {0}".format(nb_solutions)) print("mean = {0}".format(obj_mean)) print("std = {0}".format(obj_sd)) print("min = {0}".format(obj_min)) print("med = {0}".format(obj_med)) print("max = {0}".format(obj_max))
@property def stats(self): """ Returns statistics about pool objective values. :return: a tuple of floats containing (in this order: - number of solutions (same as len() - mean objective value - standard deviation - minimum objective value - median objective value - maximum objective value Note: if pool is empty returns dummy values, only the first value (len of 0) is valid. """ from math import sqrt nb_solutions = len(self) obj_min = 1e+75 obj_max = -1e+75 if not nb_solutions: # dummy values return 0, 0, 0, obj_min, obj_min, obj_max objs = [] obj_sum1 = 0 obj_sum2 = 0 for ps in self._solutions: obj = ps.objective_value objs.append(obj) if obj < obj_min: obj_min = obj if obj > obj_max: obj_max = obj obj_sum1 += obj obj_sum2 += obj * obj obj_med = sorted(objs)[nb_solutions // 2] obj_mean = obj_sum1 / nb_solutions variance = (obj_sum2 / nb_solutions) - (obj_mean ** 2) obj_sd = sqrt(variance) return nb_solutions, obj_mean, obj_sd, obj_min, obj_med, obj_max
[docs] def export_as_sol(self, path=None, basename=None, **kwargs): """ Exports the solution pool as a SOL file. Args: basename: Controls the basename with which the solution is printed. Accepts None, a plain string, or a string format. If None, the model name is used. If passed a plain string, the string is used in place of the model's name. If passed a string format (either with %s or {0}), this format is used to format the model name to produce the basename of the written file. path: A path to write the file, expects a string path or None. Can be a directory, in which case the basename that was computed with the basename argument is appended to the directory to produce the file. If given a full path, the path is directly used to write the file, and the basename argument is not used. If passed None, the output directory will be ``tempfile.gettempdir()``. :return: The path to which the solutions from the pool are written, or None if an error occured. """ printer = SolutionSolPrinter() return SolveSolution._static_export(exported=self._solutions, basename="pool", printer=printer, basename_fmt=basename, path=path, **kwargs)