# --------------------------------------------------------------------------
# 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)