Source code for pyomo.core.base.objective

#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2022
#  National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

__all__ = ('Objective',
           'simple_objective_rule',
           '_ObjectiveData',
           'minimize',
           'maximize',
           'simple_objectivelist_rule',
           'ObjectiveList')

import sys
import logging
from weakref import ref as weakref_ref
from pyomo.common.pyomo_typing import overload

from pyomo.common.deprecation import RenamedClass
from pyomo.common.log import is_debug_set
from pyomo.common.modeling import NOTSET
from pyomo.common.formatting import tabular_writer
from pyomo.common.timing import ConstructionTimer

from pyomo.core.expr.numvalue import value
from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory
from pyomo.core.base.global_set import UnindexedComponent_index
from pyomo.core.base.indexed_component import (
    ActiveIndexedComponent, UnindexedComponent_set, rule_wrapper,
)
from pyomo.core.base.expression import (_ExpressionData,
                                        _GeneralExpressionDataImpl)
from pyomo.core.base.set import Set
from pyomo.core.base.initializer import (
    Initializer, IndexedCallInitializer, CountedCallInitializer,
)
from pyomo.core.base import minimize, maximize

logger = logging.getLogger('pyomo.core')

_rule_returned_none_error = """Objective '%s': rule returned None.

Objective rules must return either a valid expression, numeric value, or
Objective.Skip.  The most common cause of this error is forgetting to
include the "return" statement at the end of your rule.
"""

def simple_objective_rule(rule):
    """
    This is a decorator that translates None into Objective.Skip.
    This supports a simpler syntax in objective rules, though these
    can be more difficult to debug when errors occur.

    Example use:

    @simple_objective_rule
    def O_rule(model, i, j):
        ...

    model.o = Objective(rule=simple_objective_rule(...))
    """
    return rule_wrapper(rule, {None: Objective.Skip})

def simple_objectivelist_rule(rule):
    """
    This is a decorator that translates None into ObjectiveList.End.
    This supports a simpler syntax in objective rules, though these
    can be more difficult to debug when errors occur.

    Example use:

    @simple_objectivelist_rule
    def O_rule(model, i, j):
        ...

    model.o = ObjectiveList(expr=simple_objectivelist_rule(...))
    """
    return rule_wrapper(rule, {None: ObjectiveList.End})

#
# This class is a pure interface
#

class _ObjectiveData(_ExpressionData):
    """
    This class defines the data for a single objective.

    Public class attributes:
        expr            The Pyomo expression for this objective
        sense           The direction for this objective.
    """

    __slots__ = ()

    #
    # Interface
    #

    def is_minimizing(self):
        """Return True if this is a minimization objective."""
        return self.sense == minimize

    #
    # Abstract Interface
    #

    @property
    def sense(self):
        """Access sense (direction) of this objective."""
        raise NotImplementedError

    def set_sense(self, sense):
        """Set the sense (direction) of this objective."""
        raise NotImplementedError

class _GeneralObjectiveData(_GeneralExpressionDataImpl,
                            _ObjectiveData,
                            ActiveComponentData):
    """
    This class defines the data for a single objective.

    Note that this is a subclass of NumericValue to allow
    objectives to be used as part of expressions.

    Constructor arguments:
        expr            The Pyomo expression stored in this objective.
        sense           The direction for this objective.
        component       The Objective object that owns this data.

    Public class attributes:
        expr            The Pyomo expression for this objective
        active          A boolean that is true if this objective is active
                            in the model.
        sense           The direction for this objective.

    Private class attributes:
        _component      The objective component.
        _active         A boolean that indicates whether this data is active
    """

    __slots__ = ("_sense", "_expr")

    def __init__(self, expr=None, sense=minimize, component=None):
        _GeneralExpressionDataImpl.__init__(self, expr)
        # Inlining ActiveComponentData.__init__
        self._component = weakref_ref(component) if (component is not None) \
                          else None
        self._index = NOTSET
        self._active = True
        self._sense = sense

        if (self._sense != minimize) and \
           (self._sense != maximize):
            raise ValueError("Objective sense must be set to one of "
                             "'minimize' (%s) or 'maximize' (%s). Invalid "
                             "value: %s'" % (minimize, maximize, sense))

    def set_value(self, expr):
        if expr is None:
            raise ValueError(_rule_returned_none_error % (self.name,))
        return super().set_value(expr)

    #
    # Abstract Interface
    #

    @property
    def sense(self):
        """Access sense (direction) of this objective."""
        return self._sense
    @sense.setter
    def sense(self, sense):
        """Set the sense (direction) of this objective."""
        self.set_sense(sense)

    def set_sense(self, sense):
        """Set the sense (direction) of this objective."""
        if sense in {minimize, maximize}:
            self._sense = sense
        else:
            raise ValueError("Objective sense must be set to one of "
                             "'minimize' (%s) or 'maximize' (%s). Invalid "
                             "value: %s'" % (minimize, maximize, sense))

[docs]@ModelComponentFactory.register("Expressions that are minimized or maximized.") class Objective(ActiveIndexedComponent): """ This modeling component defines an objective expression. Note that this is a subclass of NumericValue to allow objectives to be used as part of expressions. Constructor arguments: expr A Pyomo expression for this objective rule A function that is used to construct objective expressions sense Indicate whether minimizing (the default) or maximizing name A name for this component doc A text string describing this component Public class attributes: doc A text string describing this component name A name for this component active A boolean that is true if this component will be used to construct a model instance rule The rule used to initialize the objective(s) sense The objective sense Private class attributes: _constructed A boolean that is true if this component has been constructed _data A dictionary from the index set to component data objects _index The set of valid indices _implicit_subsets A tuple of set objects that represents the index set _model A weakref to the model that owns this component _parent A weakref to the parent block that owns this component _type The class type for the derived subclass """ _ComponentDataClass = _GeneralObjectiveData NoObjective = ActiveIndexedComponent.Skip def __new__(cls, *args, **kwds): if cls != Objective: return super(Objective, cls).__new__(cls) if not args or (args[0] is UnindexedComponent_set and len(args)==1): return ScalarObjective.__new__(ScalarObjective) else: return IndexedObjective.__new__(IndexedObjective) @overload def __init__(self, *indexes, expr=None, rule=None, sense=minimize, name=None, doc=None): ... def __init__(self, *args, **kwargs): _sense = kwargs.pop('sense', minimize) _init = tuple( _arg for _arg in ( kwargs.pop('rule', None), kwargs.pop('expr', None) ) if _arg is not None ) if len(_init) == 1: _init = _init[0] elif not _init: _init = None else: raise ValueError("Duplicate initialization: Objective() only " "accepts one of 'rule=' and 'expr='") kwargs.setdefault('ctype', Objective) ActiveIndexedComponent.__init__(self, *args, **kwargs) self.rule = Initializer(_init) self._init_sense = Initializer(_sense)
[docs] def construct(self, data=None): """ Construct the expression(s) for this objective. """ if self._constructed: return self._constructed = True timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug("Constructing objective %s" % (self.name)) rule = self.rule try: # We do not (currently) accept data for constructing Objectives index = None assert data is None if rule is None: # If there is no rule, then we are immediately done. return if rule.constant() and self.is_indexed(): raise IndexError( "Objective '%s': Cannot initialize multiple indices " "of an objective with a single expression" % (self.name,) ) block = self.parent_block() if rule.contains_indices(): # The index is coming in externally; we need to validate it for index in rule.indices(): ans = self.__setitem__(index, rule(block, index)) if ans is not None: self[index].set_sense(self._init_sense(block, index)) elif not self.index_set().isfinite(): # If the index is not finite, then we cannot iterate # over it. Since the rule doesn't provide explicit # indices, then there is nothing we can do (the # assumption is that the user will trigger specific # indices to be created at a later time). pass else: # Bypass the index validation and create the member directly for index in self.index_set(): ans = self._setitem_when_not_present( index, rule(block, index)) if ans is not None: ans.set_sense(self._init_sense(block, index)) except Exception: err = sys.exc_info()[1] logger.error( "Rule failed when generating expression for " "Objective %s with index %s:\n%s: %s" % (self.name, str(index), type(err).__name__, err)) raise finally: timer.report()
def _getitem_when_not_present(self, index): if self.rule is None: raise KeyError(index) block = self.parent_block() obj = self._setitem_when_not_present( index, self.rule(block, index)) if obj is None: raise KeyError(index) obj.set_sense(self._init_sense(block, index)) return obj def _pprint(self): """ Return data that will be printed for this component. """ return ( [("Size", len(self)), ("Index", self._index_set if self.is_indexed() else None), ("Active", self.active) ], self._data.items(), ( "Active","Sense","Expression"), lambda k, v: [ v.active, ("minimize" if (v.sense == minimize) else "maximize"), v.expr ] )
[docs] def display(self, prefix="", ostream=None): """Provide a verbose display of this object""" if not self.active: return tab = " " if ostream is None: ostream = sys.stdout ostream.write(prefix+self.local_name+" : ") ostream.write(", ".join("%s=%s" % (k,v) for k,v in [ ("Size", len(self)), ("Index", self._index_set if self.is_indexed() else None), ("Active", self.active), ] )) ostream.write("\n") tabular_writer( ostream, prefix+tab, ((k,v) for k,v in self._data.items() if v.active), ( "Active","Value" ), lambda k, v: [ v.active, value(v), ] )
class ScalarObjective(_GeneralObjectiveData, Objective): """ ScalarObjective is the implementation representing a single, non-indexed objective. """ def __init__(self, *args, **kwd): _GeneralObjectiveData.__init__(self, expr=None, component=self) Objective.__init__(self, *args, **kwd) self._index = UnindexedComponent_index # # Override abstract interface methods to first check for # construction # @property def expr(self): """Access the expression of this objective.""" if self._constructed: if len(self._data) == 0: raise ValueError( "Accessing the expression of ScalarObjective " "'%s' before the Objective has been assigned " "a sense or expression. There is currently " "nothing to access." % (self.name)) return _GeneralObjectiveData.expr.fget(self) raise ValueError( "Accessing the expression of objective '%s' " "before the Objective has been constructed (there " "is currently no value to return)." % (self.name)) @expr.setter def expr(self, expr): """Set the expression of this objective.""" self.set_value(expr) @property def sense(self): """Access sense (direction) of this objective.""" if self._constructed: if len(self._data) == 0: raise ValueError( "Accessing the sense of ScalarObjective " "'%s' before the Objective has been assigned " "a sense or expression. There is currently " "nothing to access." % (self.name)) return _GeneralObjectiveData.sense.fget(self) raise ValueError( "Accessing the sense of objective '%s' " "before the Objective has been constructed (there " "is currently no value to return)." % (self.name)) @sense.setter def sense(self, sense): """Set the sense (direction) of this objective.""" self.set_sense(sense) # # Singleton objectives are strange in that we want them to be # both be constructed but have len() == 0 when not initialized with # anything (at least according to the unit tests that are # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Objective.Skip are managed. But after that they will behave # like _ObjectiveData objects where set_value does not handle # Objective.Skip but expects a valid expression or None # def clear(self): self._data = {} def set_value(self, expr): """Set the expression of this objective.""" if not self._constructed: raise ValueError( "Setting the value of objective '%s' " "before the Objective has been constructed (there " "is currently no object to set)." % (self.name)) if not self._data: self._data[None] = self return super().set_value(expr) def set_sense(self, sense): """Set the sense (direction) of this objective.""" if self._constructed: if len(self._data) == 0: self._data[None] = self return _GeneralObjectiveData.set_sense(self, sense) raise ValueError( "Setting the sense of objective '%s' " "before the Objective has been constructed (there " "is currently no object to set)." % (self.name)) # # Leaving this method for backward compatibility reasons. # (probably should be removed) # def add(self, index, expr): """Add an expression with a given index.""" if index is not None: raise ValueError( "ScalarObjective object '%s' does not accept " "index values other than None. Invalid value: %s" % (self.name, index)) self.set_value(expr) return self class SimpleObjective(metaclass=RenamedClass): __renamed__new_class__ = ScalarObjective __renamed__version__ = '6.0' class IndexedObjective(Objective): # # Leaving this method for backward compatibility reasons # # Note: Beginning after Pyomo 5.2 this method will now validate that # the index is in the underlying index set (through 5.2 the index # was not checked). # def add(self, index, expr): """Add an objective with a given index.""" return self.__setitem__(index, expr) @ModelComponentFactory.register("A list of objective expressions.") class ObjectiveList(IndexedObjective): """ An objective component that represents a list of objectives. Objectives can be indexed by their index, but when they are added an index value is not specified. """ class End(object): pass def __init__(self, **kwargs): """Constructor""" if 'expr' in kwargs: raise ValueError( "ObjectiveList does not accept the 'expr' keyword") _rule = kwargs.pop('rule', None) self._starting_index = kwargs.pop('starting_index', 1) args = (Set(dimen=1),) super().__init__(*args, **kwargs) self.rule = Initializer(_rule, allow_generators=True) # HACK to make the "counted call" syntax work. We wait until # after the base class is set up so that is_indexed() is # reliable. if self.rule is not None and type(self.rule) is IndexedCallInitializer: self.rule = CountedCallInitializer( self, self.rule, self._starting_index ) def construct(self, data=None): """ Construct the expression(s) for this objective. """ if self._constructed: return self._constructed=True if is_debug_set(logger): logger.debug("Constructing objective list %s" % (self.name)) self.index_set().construct() if self.rule is not None: _rule = self.rule(self.parent_block(), ()) for cc in iter(_rule): if cc is ObjectiveList.End: break if cc is Objective.Skip: continue self.add(cc, sense=self._init_sense) def add(self, expr, sense=minimize): """Add an objective to the list.""" next_idx = len(self._index_set) + self._starting_index self._index_set.add(next_idx) ans = self.__setitem__(next_idx, expr) if ans is not None: if sense not in {minimize, maximize}: sense = sense(self.parent_block(), next_idx) ans.set_sense(sense) return ans