Source code for docplex.mp.basic

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

from abc import abstractmethod, ABCMeta

from docplex.mp.operand import Operand
from docplex.mp.sttck import StaticTypeChecker


# noinspection PyUnusedLocal,PyPropertyAccess

class _AbstractModelObject(metaclass=ABCMeta):
    """
    Abstract API for all classes which have a "model" property.
    """

    @property
    @abstractmethod
    def model(self):  # pragma: no cover
        raise NotImplementedError

    def is_in_model(self, mdl):
        return self.model is mdl

    def get_linear_factory(self):
        return self.model._lfactory

    @property
    def lfactory(self):
        return self.model._lfactory

    @property
    def qfactory(self):
        return self.model._qfactory

    def _check_model_has_solution(self):
        self.model._check_has_solution()

    @property
    def error_handler(self):
        return self.logger

    @property
    def logger(self):
        return self.model.error_handler

    def fatal(self, msg, *args):
        self.logger.fatal(msg, args)

    def error(self, msg, *args):
        self.logger.error(msg, args)

    def warning(self, msg, *args):
        self.logger.warning(msg, args)


class _AbstractValuable(_AbstractModelObject):
    # abstract API for all objects which can be evaluated from a solution.

    __slots__ = ()

    def _round_if_discrete(self, raw_value):
        return self.model._round_element_value_if_necessary(self, raw_value)

    @abstractmethod
    def _raw_solution_value(self, s=None):
        # INTERNAL: compute raw solution value, no rounding, no checking
        raise NotImplementedError  # pragma: no cover

    @property
    def solution_value(self):
        self.model._check_has_solution()
        raw = self._raw_solution_value()
        return self._round_if_discrete(raw)

    @property
    def raw_solution_value(self):
        self.model._check_has_solution()
        return self._raw_solution_value()

    @property
    def sv(self):
        return self.solution_value

    @property
    def rsv(self):
        return self.raw_solution_value


class _SubscriptionMixin(object):
    __slots__ = ()

    # INTERNAL:
    # This class is absolutely not meant to be directly instantiated
    # but used as a mixin

    @classmethod
    def _new_empty_subscribers(cls):
        return []

    def notify_used(self, user):
        # INTERNAL
        self._subscribers.append(user)

    notify_subscribed = notify_used

    def notify_unsubscribed(self, subscriber):
        # 1 find index
        for s, sc in enumerate(self._subscribers):
            if sc is subscriber:
                del self._subscribers[s]
                break

    def clear_subscribers(self):
        self._subscribers = []

    def is_in_use(self):
        return bool(self._subscribers)

    @property
    def nb_subscribers(self):
        return len(self._subscribers)

    def is_shared(self):
        return self.nb_subscribers >= 2

    def is_used_by(self, obj):
        # lists are not optimal here, but we favor insertion: append is faster than set.add
        return any(obj is sc for sc in self.iter_subscribers())

    def notify_modified(self, event):
        for s in self._subscribers:
            s.notify_expr_modified(self, event)

    def iter_subscribers(self):
        return iter(self._subscribers)

    def notify_replaced(self, new_expr):
        for s in self._subscribers:
            s.notify_expr_replaced(self, new_expr)

    def grab_subscribers(self, other):
        # grab subscribers from another expression
        # typically when an expression is replaced by another.
        for s in other.iter_subscribers():
            self._subscribers.append(s)
        # delete all subscriptions on old
        other.clear_subscribers()


class _AbstractBendersAnnotated(_AbstractModelObject):
    # a maxin class to group all benders-related code.
    __slots__ = ()

    def set_benders_annotation(self, group):
        self.model.set_benders_annotation(self, group)

    def get_benders_annotation(self):
        return self.model.get_benders_annotation(self)


class _AbstractNamable(metaclass=ABCMeta):

    # abstract name API across all modeling objects.

    @property
    @abstractmethod
    def name(self):  # pragma: no cover
        raise NotImplemented

    @abstractmethod
    def _set_name(self, new_name):  # pragma: no cover
        raise NotImplementedError

    def check_name(self, new_name):
        pass

    def get_name(self):
        # deprecate
        return self.name

    def set_name(self, new_name):
        self.check_name(new_name)
        self._set_name(new_name)

    @property
    def safe_name(self):
        return self.name or ''

    def check_lp_name(self, qualifier, new_name, accept_empty, accept_none):
        return StaticTypeChecker.check_lp_name(logger=self, qualifier=qualifier, obj=self, new_name=new_name,
                                               accept_empty=accept_empty, accept_none=accept_none)

    def has_name(self):
        return self.name is not None

    def has_user_name(self):
        return self.has_name()


[docs]class ModelObject(_AbstractModelObject): # base for all model objects __array_priority__ = 100 __slots__ = ('_model',) def __init__(self, model): self._model = model @property def model(self): return self._model def repr_str(self): # INTERNAL try: return self.to_string(use_space=False) except (TypeError, AttributeError): return str(self) def zero_expr(self): # INTERNAL return self._model._lfactory.new_zero_expr() def _unsupported_binary_operation(self, lhs, op, rhs): self.fatal("Unsupported operation: {0!s} {1:s} {2!s}", lhs, op, rhs) def __str__(self): return self.to_string(use_space=self._model.str_use_space)
# def to_string(self): # raise NotImplementedError
[docs]class ModelingObjectBase(ModelObject, _AbstractNamable): """ModelingObjectBase() Parent class for all modeling objects (variables and constraints). This class is not intended to be instantiated directly. """ __array_priority__ = 100 __slots__ = ('_name',) # noinspection PyMissingConstructor def __init__(self, model, name=None): self._name = name self._model = model @property def name(self): """ This property is used to get or set the name of the modeling object. """ return self._name @name.setter def name(self, new_name): self.set_name(new_name) def _set_name(self, name): self._name = name
[docs] def has_name(self): """ Checks whether the object has a name. Returns: True if the object has a name. """ return super().has_name()
[docs] def has_user_name(self): """ Checks whether the object has a valid name given by the user. Returns: True if the object has a valid name given by the user. """ return self.has_name()
@property def model(self): """ This property returns the :class:`docplex.mp.model.Model` to which the object belongs. """ return super().model
[docs]class IndexableObject(ModelingObjectBase): __slots__ = ("_index",) @staticmethod def is_valid_index(idx): # INTERNAL: This is where the valid index check is performed return idx >= 0 _invalid_index = -2 # noinspection PyMissingConstructor def __init__(self, model, name=None, index=_invalid_index): # ModelingObjectBase.__init__(self, model, name) self._model = model self._name = name self._index = index
[docs] def is_generated(self): """ Checks whether this object has been generated by another modeling object. If so, the origin object is stored in the ``_origin`` attribute. Returns: True if the objects has been generated. """ return self.origin is not None
@property def origin(self): return self.model.get_obj_origin(self) @origin.setter def origin(self, origin): self.model.set_obj_origin(self, origin) def __hash__(self): return id(self) @property def model(self): return self._model @property def index(self): return self._index @property def index1(self): raw = self._index return raw if raw == self._invalid_index else raw + 1 def _set_index(self, idx): self._index = idx def has_valid_index(self): return self._index >= 0 def _set_invalid_index(self): self._index = self._invalid_index @property def safe_index(self): if not self.has_valid_index(): self.fatal("Modeling object {0!s} has invalid index: {1:d}", self, self._index) # pragma: no cover return self._index @property def container(self): return self.model.get_var_container(self) @container.setter def container(self, ctn): self._model.set_var_container(self, ctn) @property @abstractmethod def cplex_scope(self) -> int: return -1 # crash def get_scope(self): try: cpx_scope = self.cplex_scope return self.model._get_obj_scope(cpx_scope, error='ignore') except AttributeError: return None @property def scope(self): return self.get_scope()
[docs]class Expr(ModelObject, Operand, _AbstractValuable): """Expr() Parent class for all expression classes. """ __slots__ = () @property def name(self): return None def clone(self): # pragma: no cover raise NotImplementedError # pragma: no cover def iter_variables(self): # internal raise NotImplementedError # pragma: no cover def copy(self, target_model, var_mapping): # internal raise NotImplementedError # pragma: no cover
[docs] def number_of_variables(self): """ Returns: integer: The number of variables in the expression. """ return sum(1 for _ in self.iter_variables()) # pragma: no cover
[docs] def contains_var(self, dvar): """ Checks whether a variable is present in the expression. :param: dvar (:class:`docplex.mp.dvar.Var`): A decision variable. Returns: Boolean: True if the variable is present in the expression, else False. """ return any(dvar is v for v in self.iter_variables())
def to_string(self, nb_digits=None, use_space=False): from io import StringIO oss = StringIO() if nb_digits is None: nb_digits = self.model.float_precision self.to_stringio(oss, nb_digits=nb_digits, use_space=use_space) return oss.getvalue() def to_readable_string(self): return self.to_string(use_space=True)[:self.model.readable_str_len] def to_stringio(self, oss, nb_digits, use_space, var_namer=lambda v: v.name): raise NotImplementedError # pragma: no cover def _num_to_stringio(self, oss, num, ndigits=None, print_sign=False, force_plus=False, use_space=False): k = num if print_sign: if k < 0: sign = u'-' k = -k elif k > 0 and force_plus: # force a plus sign = u'+' else: sign = None if use_space: oss.write(u' ') if sign: oss.write(sign) if use_space: oss.write(u' ') # INTERNAL ndigits = ndigits or self.model.float_precision try: if k == int(k): oss.write(u'%d' % k) else: # use second arg as nb digits: oss.write(u"{0:.{1}f}".format(k, ndigits)) except ValueError: # possibly a nan oss.write('?') # def __pos__(self): # # + e is identical to e # return self def is_discrete(self): raise NotImplementedError # pragma: no cover
[docs] def is_quad_expr(self): """ Returns True if the expression is quadratic """ return False
def get_linear_part(self): return self # should be not implemented... def is_zero(self): return False constant = property(Operand.get_constant) @property def float_precision(self): return 0 if self.is_discrete() else self.model.float_precision def __pow__(self, power): # INTERNAL if 0 == power: return 1 elif 1 == power: return self elif 2 == power: return self.square() else: self.model.unsupported_power_error(self, power) def square(self): # redefine for each class of expression return None # pragma: no cover def __gt__(self, e): """ The strict > operator is not supported """ self.model.unsupported_relational_operator_error(self, ">", e) def __lt__(self, e): """ The strict < operator is not supported """ self.model.unsupported_relational_operator_error(self, "<", e)
# ---