Source code for

# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# (c) Copyright IBM Corp. 2017
# ---------------------------------------------------------------------------

from import CplexEngine

[docs]class ModelCallbackMixin(object): """ This mixin class is intended as a bridge between DOcplex expression and constraints and CPLEX callback API. It is not intended to be instantiated directly, but to be inherited from in custom callbacks , jointly with a CPLEX callback type. For example, to define a custom BranchCallback in Docplex, define a new class which inherits both from ModelCallbackMixin and the legacy callback class BranchCallback. Note: - `ModelCallbackMixin` should be first in inheritance order, - the constructor of the custom callback class must take an `env` parameter to comply with the CPLEX API - the constructor of the custom callback must call two __init__() methods: - one for the cplex callback class, taking an `env` parameter - one for the mixin class. Example: class MyBranch(ModelCallbackMixin, cplex.callbacks.BranchCallback): def __init__(self, env): cplex.callbacks.BranchCallback.__init__(self, env) ModelCallbackMixin.__init__(self) A custom callback must be registered with a `Model` class using Model.register_callback; this method assumes the custom callback has a model setter property to connect the model to the callback. See Also: :func:`` """ def __init__(self): self._model = None @property def model(self): """ This property is used to get the model associated with the mixin. An exception is raised if no model has been associated with the mixin. :return: an instance of `` """ if not self._model: raise ValueError('No model has been attached to the callback.') # pragma: no cover return self._model
[docs] def index_to_var(self, var_index): """ This method converts a variable index to a Var object. A model must have been associated withthe mixin, otherwise an error is raised. :param var_index: A valid variable index, that is a positive integer. :return: A Docplex variable with this index, or None. """ assert var_index >= 0 dv = self.model.get_var_by_index(var_index) return dv
[docs] @staticmethod def linear_ct_to_cplex(linear_ct): """ Converst a DOcplex linear constraint to CPLEX Python data :param linear_ct: a DOcplex linear constraint. :return: a 3-tuple containing elements representing the constraint in CPLEX-Python - a list of two lists, indices and coefficients , representing the linear part - a floating point number , the "right hand side" or rhs - a one-letter string (possible values are: 'L', 'E', 'G') representing the sense of the constraint. Example: Assuming variable X has index 1, the constraint (2X <= 7) will be converted to ct = 2 * X <= 7 linear_ct_cplex(ct) >>> [[1], [2.0]], 7.0, 'L' """ cpx_lhs = CplexEngine.linear_ct_to_cplex(linear_ct=linear_ct) cpx_rhs = linear_ct.cplex_num_rhs() cpx_sense = linear_ct.sense.cplex_code return cpx_lhs, cpx_sense, cpx_rhs
[docs] def make_solution_from_vars(self, dvars): """ Creates an intermediate solution from a list of variables. :param dvars: a list of DOcplex variables. :return: a :class:`` object. """ if dvars: indices = [v._index for v in dvars] # this calls the Cplex callback method get_values, which crashes if called with empty list # noinspection PyUnresolvedReferences var_values = super(ModelCallbackMixin, self).get_values(indices) # noinspection PyArgumentList var_value_dict = {v: val for v, val in zip(dvars, var_values)} else: # pragma: no cover var_value_dict = {} return self.model.new_solution(var_value_dict)
[docs] def make_complete_solution(self): """ Creates and returns an intermediate solution with all variables. Values are taken from the `get_values()` method of the callback :return: a :class:`` object. """ all_vars = list(self.model.iter_variables()) return self.make_solution_from_vars(all_vars)
# compatibility make_solution = make_complete_solution
[docs]class ConstraintCallbackMixin(ModelCallbackMixin): def __init__(self): ModelCallbackMixin.__init__(self) self._ct_vars = None self.cts = [] self._vars = [] def register_constraints(self, cts): self.cts.extend(cts) self._ct_vars = None def register_constraint(self, ct): self.register_constraints([ct])
[docs] def register_watched_var(self, dvar): """ Register one variable. Registered variables will be part of the intermediate solutions. """ self._vars.append(dvar)
[docs] def register_watched_vars(self, dvars): """ Register an iterable of variables. Registered variables will be part of the intermediate solutions. """ self._vars.extend(dvars)
@staticmethod def _collect_constraint_variables(cts): # collect variables as a set var_set = set(v for c in cts for v in c.iter_variables()) # convert to list var_list = list(var_set) var_list.sort(key=lambda dv: dv._index) return var_list def _get_or_collect_vars(self): # INTERNAL if self._ct_vars is None: self._ct_vars = self._collect_constraint_variables(self.cts) return self._ct_vars @property def watched_vars(self): return self._get_or_collect_vars() + self._vars
[docs] def make_solution_from_watched(self): """ Creates and returns a DOcplex solution instance from watched items. This method should be called when CPLEX has a new incumbent solution. It builds an intermediate solution from the watched variables and variables mentioned in the registered constraints.. To build a soluton from all variables, use `make_complete_solution()` :return: An instance of SolveSolution. """ return self.make_solution_from_vars(self.watched_vars)
[docs] def get_cpx_unsatisfied_cts(self, cts, sol, tolerance=1e-6): """ returns the subset of unsatisfied constraints in a given solution. This is used in custom lazy constraints or user cut callbacks. :param cts: a list of constraints among which to look for unsatisfied :param sol: A solution object :param tolerance: amn optional numerical value used to determine whether a constraint is satisfied or not. Defaut is 1e-6. :return: a list of tuples (ct, lhs, sense, lhs) where: ct is an unsatisfied constraint lhs is the left-hand size, as expected by the cplex callback sense is the constraint sense, as expected by the cplex callback rhs is the rith-and side (a number), as expected by the cplex callback """ unsatisfied = [] for ct in cts: if not ct.is_satisfied(sol, tolerance): # use mixin API to convert to cplex lingo cpx_lhs, cpx_sense, cpx_rhs = self.linear_ct_to_cplex(ct) unsatisfied.append( (ct, cpx_lhs, cpx_sense, cpx_rhs) ) return unsatisfied