Source code for

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

from import Operand
from import docplex_fatal, DOcplexException

from import is_number, is_string, is_function, str_maxed
from import _AbstractNamable, _AbstractValuable

[docs]class KPI(_AbstractNamable, _AbstractValuable): """ Base class for key performance indicators (KPIs). Each KPI has a unique name. A KPI is attached to a model instance and can compute a numerical value, using the :func:`compute` method. The `compute` method takes an optional solution argument; if passed a valid SolveSolution object, this solution is used to evaluate the KPI, else compute() will attempt to access th emodel's solution. If the model has no attached solution, then an exception is raised by `compute`. """ def __init__(self, name=None): self._name = name def _set_name(self, new_name): self._name = new_name @property def name(self): return self._name @name.setter def name(self, new_name): self.set_name(new_name)
[docs] def get_model(self): """ Returns: The model instance on which the KPI is defined. :rtype: :class:`` """ raise NotImplementedError # pragma: no cover
@property def model(self): return self.get_model() def compute(self, s=None): raise NotImplementedError # pragma: no cover # def _get_solution_value(self, s=None): # pragma: no cover # # to be removed # return self._raw_solution_value(s) def _raw_solution_value(self, s=None): # pragma: no cover return self.compute(s) def _ensure_solution(self, s, do_raise=True): # INTERNAL if s is not None: return s else: ms = self.get_model()._solution if ms is not None: return ms elif do_raise: self.get_model().fatal("KPI.compute() requires a solution, but model is not solved and no solution was passed") else: return None def check_name(self, name_arg): self.model._checker.typecheck_string(name_arg, accept_none=False, accept_empty=False, caller="")
[docs] def is_decision_expression(self): """ returns True if the KPI is based on a decision expression or variable. """ raise NotImplementedError # pragma: no cover
def copy(self, new_model, var_map): raise NotImplementedError # pragma: no cover def clone(self): raise NotImplementedError # pragma: no cover @staticmethod def new_kpi(model, kpi_arg, kpi_name): # static factory method to build a new concrete instance of KPI if isinstance(kpi_arg, KPI): if not kpi_name: return kpi_arg else: cloned = kpi_arg.clone() = kpi_name return cloned elif is_function(kpi_arg): return FunctionalKPI(kpi_arg, model, kpi_name) else: # try a linear expr conversion try: expr = model._lfactory._to_expr(kpi_arg) return DecisionKPI(expr, kpi_name) except DOcplexException: model.fatal("Cannot interpret this as a KPI: {0!r}. expecting expression, variable or function", kpi_arg) def notify_removed(self): pass
[docs]class DecisionKPI(KPI): """ Specialized class of Key Performance Indicator, based on expressions. This subclass is built from a decision variable or a linear expression. The :func:`compute` method evaluate the value of the KPI in a solution. This solution can either be passed to the `compute` method, or using th emodel's solution. In the latter case, the model must have been solved with a solution. """ def __init__(self, kpi_op, name=None): expr = None if is_number(kpi_op): expr = self.get_model().linear_expr(arg=kpi_op) elif isinstance(kpi_op, Operand): expr = kpi_op expr.notify_used(self) # kpi is a subscriber if hasattr(kpi_op, 'name'): name = name or getattr(kpi_op, 'name') else: self.get_model().fatal('cannot interpret this as kpi: {0!r}, expecting number or operand', kpi_op) super().__init__(name) self._expr = expr def notify_expr_modified(self, expr, event): # do nothing pass def notify_removed(self): self._expr.notify_unsubscribed(self)
[docs] def get_model(self): return self._expr.model
[docs] def compute(self, s=None): """ Redefinition of the abstract `compute(s=None)` method. Returns: float: The value of the decision expression in the solution. Note: Evaluating a KPI requires a solution object. This solution can either be passed explicitly in the `s` argument, otherwise the model solution is used. In the latter case, th emodel must have been solved with a solution, otherwise an exception is raised. Raises: Evaluating a KPI raises an exception if no `s` solution has been passed and the underlying model has not been solved with a solution. See Also: :class:`` """ es = self._ensure_solution(s, do_raise=True) return self._expr._raw_solution_value(es)
[docs] def is_decision_expression(self): return True
def to_expr(self): return self._expr as_expression = to_expr def to_linear_expr(self): return self._expr.to_linear_expr() def copy(self, new_model, var_map): expr_copy = self._expr.copy(new_model, var_map) return DecisionKPI(kpi_op=expr_copy, def clone(self): return DecisionKPI(self._expr, def __repr__(self): return "{0}(name={1},expr={2!s})".format(self.__class__.__name__,, str_maxed(self._expr, maxlen=64))
[docs]class FunctionalKPI(KPI): # Functional KPIs store a function that takes a model to compute a number # Functional KPIs do not require a successful solve. def __init__(self, fn, model, name): KPI.__init__(self, name) self._function = fn self._model = model
[docs] def get_model(self): return self._model
def compute(self, s=None): es = self._ensure_solution(s) return self._function(self._model, es)
[docs] def is_decision_expression(self): return False
def copy(self, new_model, var_map): return FunctionalKPI(fn=self._function, model=new_model, def clone(self): return FunctionalKPI(fn=self._function, model=self._model, def to_expr(self): docplex_fatal("This KPI cannot be used as an expression: {0!r}".format(self))