#!/usr/bin/python
# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# http://www.apache.org/licenses/
# (c) Copyright IBM Corp. 2017
# ---------------------------------------------------------------------------
from docplex.mp.cplex_engine 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:`docplex.mp.model.Model.register_callback`
"""
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 `docplex.mp.Model`
"""
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:`docplex.mp.solution.SolveSolution` 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:`docplex.mp.solution.SolveSolution` 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
[docs]def print_called(prompt_msg=None):
""" A decorator function to be used on __call__() methods for derived callbacks.
Use this decorator function to decorate __call__() methods of custom callbacks.
Example:
::
class MyCallback(ConstraintCallbackMixin, LazyConstraintCallback):
@print_called('my custom callback called #{0}')
def __call__(self):
...
will print messages, before executing the callback code:
>>> "my custom callback called #1"
>>> "my custom callback called #2"
each time the callback is called
:param prompt_msg: A format string taking one argument (the number of calls)
:return:
As decorator, modifies the code of the __call_ method inplace.
"""
def cb_decorator(func):
prompt = prompt_msg or "* callback: {0} called: #{1}"
def wrapper(self, *args, **kwargs):
wrapper.count = wrapper.count + 1
print(prompt.format(wrapper.count))
res = func(self, *args, **kwargs)
return res
wrapper.count = 0
return wrapper
return cb_decorator