1#  ___________________________________________________________________________
2#
3#  Pyomo: Python Optimization Modeling Objects
4#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
5#  Under the terms of Contract DE-NA0003525 with National Technology and
6#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
7#  rights in this software.
8#  This software is distributed under the 3-clause BSD License.
9#  ___________________________________________________________________________
10
11__all__ = ('Objective',
12           'simple_objective_rule',
13           '_ObjectiveData',
14           'minimize',
15           'maximize',
16           'simple_objectivelist_rule',
17           'ObjectiveList')
18
19import sys
20import logging
21from weakref import ref as weakref_ref
22
23from pyomo.common.log import is_debug_set
24from pyomo.common.deprecation import deprecated, RenamedClass
25from pyomo.common.formatting import tabular_writer
26from pyomo.common.timing import ConstructionTimer
27from pyomo.core.expr.numvalue import value
28from pyomo.core.base.component import (
29    ActiveComponentData, ModelComponentFactory,
30)
31from pyomo.core.base.indexed_component import (
32    ActiveIndexedComponent, UnindexedComponent_set, rule_wrapper,
33    _get_indexed_component_data_name,
34)
35from pyomo.core.base.expression import (_ExpressionData,
36                                        _GeneralExpressionDataImpl)
37from pyomo.core.base.misc import apply_indexed_rule
38from pyomo.core.base.set import Set
39from pyomo.core.base.initializer import (
40    Initializer, IndexedCallInitializer, CountedCallInitializer,
41)
42from pyomo.core.base import minimize, maximize
43
44logger = logging.getLogger('pyomo.core')
45
46_rule_returned_none_error = """Objective '%s': rule returned None.
47
48Objective rules must return either a valid expression, numeric value, or
49Objective.Skip.  The most common cause of this error is forgetting to
50include the "return" statement at the end of your rule.
51"""
52
53def simple_objective_rule(rule):
54    """
55    This is a decorator that translates None into Objective.Skip.
56    This supports a simpler syntax in objective rules, though these
57    can be more difficult to debug when errors occur.
58
59    Example use:
60
61    @simple_objective_rule
62    def O_rule(model, i, j):
63        ...
64
65    model.o = Objective(rule=simple_objective_rule(...))
66    """
67    return rule_wrapper(rule, {None: Objective.Skip})
68
69def simple_objectivelist_rule(rule):
70    """
71    This is a decorator that translates None into ObjectiveList.End.
72    This supports a simpler syntax in objective rules, though these
73    can be more difficult to debug when errors occur.
74
75    Example use:
76
77    @simple_objectivelist_rule
78    def O_rule(model, i, j):
79        ...
80
81    model.o = ObjectiveList(expr=simple_objectivelist_rule(...))
82    """
83    return rule_wrapper(rule, {None: ObjectiveList.End})
84
85#
86# This class is a pure interface
87#
88
89class _ObjectiveData(_ExpressionData):
90    """
91    This class defines the data for a single objective.
92
93    Public class attributes:
94        expr            The Pyomo expression for this objective
95        sense           The direction for this objective.
96    """
97
98    __slots__ = ()
99
100    #
101    # Interface
102    #
103
104    def is_minimizing(self):
105        """Return True if this is a minimization objective."""
106        return self.sense == minimize
107
108    #
109    # Abstract Interface
110    #
111
112    @property
113    def sense(self):
114        """Access sense (direction) of this objective."""
115        raise NotImplementedError
116
117    def set_sense(self, sense):
118        """Set the sense (direction) of this objective."""
119        raise NotImplementedError
120
121class _GeneralObjectiveData(_GeneralExpressionDataImpl,
122                            _ObjectiveData,
123                            ActiveComponentData):
124    """
125    This class defines the data for a single objective.
126
127    Note that this is a subclass of NumericValue to allow
128    objectives to be used as part of expressions.
129
130    Constructor arguments:
131        expr            The Pyomo expression stored in this objective.
132        sense           The direction for this objective.
133        component       The Objective object that owns this data.
134
135    Public class attributes:
136        expr            The Pyomo expression for this objective
137        active          A boolean that is true if this objective is active
138                            in the model.
139        sense           The direction for this objective.
140
141    Private class attributes:
142        _component      The objective component.
143        _active         A boolean that indicates whether this data is active
144    """
145
146    __pickle_slots__ = ("_sense",)
147    __slots__ = __pickle_slots__ + _GeneralExpressionDataImpl.__pickle_slots__
148
149    def __init__(self, expr=None, sense=minimize, component=None):
150        _GeneralExpressionDataImpl.__init__(self, expr)
151        # Inlining ActiveComponentData.__init__
152        self._component = weakref_ref(component) if (component is not None) \
153                          else None
154        self._active = True
155        self._sense = sense
156
157        if (self._sense != minimize) and \
158           (self._sense != maximize):
159            raise ValueError("Objective sense must be set to one of "
160                             "'minimize' (%s) or 'maximize' (%s). Invalid "
161                             "value: %s'" % (minimize, maximize, sense))
162
163    def __getstate__(self):
164        """
165        This method must be defined because this class uses slots.
166        """
167        state = _GeneralExpressionDataImpl.__getstate__(self)
168        for i in _GeneralObjectiveData.__pickle_slots__:
169            state[i] = getattr(self,i)
170        return state
171
172    # Note: because NONE of the slots on this class need to be edited,
173    #       we don't need to implement a specialized __setstate__
174    #       method.
175
176    def set_value(self, expr):
177        if expr is None:
178            raise ValueError(_rule_returned_none_error % (self.name,))
179        return super().set_value(expr)
180
181    #
182    # Abstract Interface
183    #
184
185    @property
186    def sense(self):
187        """Access sense (direction) of this objective."""
188        return self._sense
189    @sense.setter
190    def sense(self, sense):
191        """Set the sense (direction) of this objective."""
192        self.set_sense(sense)
193
194    def set_sense(self, sense):
195        """Set the sense (direction) of this objective."""
196        if sense in {minimize, maximize}:
197            self._sense = sense
198        else:
199            raise ValueError("Objective sense must be set to one of "
200                             "'minimize' (%s) or 'maximize' (%s). Invalid "
201                             "value: %s'" % (minimize, maximize, sense))
202
203@ModelComponentFactory.register("Expressions that are minimized or maximized.")
204class Objective(ActiveIndexedComponent):
205    """
206    This modeling component defines an objective expression.
207
208    Note that this is a subclass of NumericValue to allow
209    objectives to be used as part of expressions.
210
211    Constructor arguments:
212        expr
213            A Pyomo expression for this objective
214        rule
215            A function that is used to construct objective expressions
216        sense
217            Indicate whether minimizing (the default) or maximizing
218        doc
219            A text string describing this component
220        name
221            A name for this component
222
223    Public class attributes:
224        doc
225            A text string describing this component
226        name
227            A name for this component
228        active
229            A boolean that is true if this component will be used to construct
230            a model instance
231        rule
232            The rule used to initialize the objective(s)
233        sense
234            The objective sense
235
236    Private class attributes:
237        _constructed
238            A boolean that is true if this component has been constructed
239        _data
240            A dictionary from the index set to component data objects
241        _index
242            The set of valid indices
243        _implicit_subsets
244            A tuple of set objects that represents the index set
245        _model
246            A weakref to the model that owns this component
247        _parent
248            A weakref to the parent block that owns this component
249        _type
250            The class type for the derived subclass
251    """
252
253    _ComponentDataClass = _GeneralObjectiveData
254    NoObjective = ActiveIndexedComponent.Skip
255
256    def __new__(cls, *args, **kwds):
257        if cls != Objective:
258            return super(Objective, cls).__new__(cls)
259        if not args or (args[0] is UnindexedComponent_set and len(args)==1):
260            return ScalarObjective.__new__(ScalarObjective)
261        else:
262            return IndexedObjective.__new__(IndexedObjective)
263
264    def __init__(self, *args, **kwargs):
265        _sense = kwargs.pop('sense', minimize)
266        _init = tuple( _arg for _arg in (
267            kwargs.pop('rule', None), kwargs.pop('expr', None)
268        ) if _arg is not None )
269        if len(_init) == 1:
270            _init = _init[0]
271        elif not _init:
272            _init = None
273        else:
274            raise ValueError("Duplicate initialization: Objective() only "
275                             "accepts one of 'rule=' and 'expr='")
276
277        kwargs.setdefault('ctype', Objective)
278        ActiveIndexedComponent.__init__(self, *args, **kwargs)
279
280        self.rule = Initializer(_init)
281        self._init_sense = Initializer(_sense)
282
283    def construct(self, data=None):
284        """
285        Construct the expression(s) for this objective.
286        """
287        if self._constructed:
288            return
289        self._constructed = True
290
291        timer = ConstructionTimer(self)
292        if is_debug_set(logger):
293            logger.debug("Constructing objective %s" % (self.name))
294
295        rule = self.rule
296        try:
297            # We do not (currently) accept data for constructing Objectives
298            index = None
299            assert data is None
300
301            if rule is None:
302                # If there is no rule, then we are immediately done.
303                return
304
305            if rule.constant() and self.is_indexed():
306                raise IndexError(
307                    "Objective '%s': Cannot initialize multiple indices "
308                    "of an objective with a single expression" %
309                    (self.name,) )
310
311            block = self.parent_block()
312            if rule.contains_indices():
313                # The index is coming in externally; we need to validate it
314                for index in rule.indices():
315                    ans = self.__setitem__(index, rule(block, index))
316                    if ans is not None:
317                        self[index].set_sense(self._init_sense(block, index))
318            elif not self.index_set().isfinite():
319                # If the index is not finite, then we cannot iterate
320                # over it.  Since the rule doesn't provide explicit
321                # indices, then there is nothing we can do (the
322                # assumption is that the user will trigger specific
323                # indices to be created at a later time).
324                pass
325            else:
326                # Bypass the index validation and create the member directly
327                for index in self.index_set():
328                    ans = self._setitem_when_not_present(
329                        index, rule(block, index))
330                    if ans is not None:
331                        ans.set_sense(self._init_sense(block, index))
332        except Exception:
333            err = sys.exc_info()[1]
334            logger.error(
335                "Rule failed when generating expression for "
336                "Objective %s with index %s:\n%s: %s"
337                % (self.name,
338                   str(index),
339                   type(err).__name__,
340                   err))
341            raise
342        finally:
343            timer.report()
344
345    def _getitem_when_not_present(self, index):
346        if self.rule is None:
347            raise KeyError(index)
348        obj = self._setitem_when_not_present(
349            index, self.rule(self.parent_block(), index))
350        if obj is None:
351            raise KeyError(index)
352        else:
353            obj.set_sense(self._init_sense(block, index))
354        return obj
355
356    def _pprint(self):
357        """
358        Return data that will be printed for this component.
359        """
360        return (
361            [("Size", len(self)),
362             ("Index", self._index if self.is_indexed() else None),
363             ("Active", self.active)
364             ],
365            self._data.items(),
366            ( "Active","Sense","Expression"),
367            lambda k, v: [ v.active,
368                           ("minimize" if (v.sense == minimize) else "maximize"),
369                           v.expr
370                           ]
371            )
372
373    def display(self, prefix="", ostream=None):
374        """Provide a verbose display of this object"""
375        if not self.active:
376            return
377        tab = "    "
378        if ostream is None:
379            ostream = sys.stdout
380        ostream.write(prefix+self.local_name+" : ")
381        ostream.write(", ".join("%s=%s" % (k,v) for k,v in [
382                    ("Size", len(self)),
383                    ("Index", self._index if self.is_indexed() else None),
384                    ("Active", self.active),
385                    ] ))
386
387        ostream.write("\n")
388        tabular_writer( ostream, prefix+tab,
389                        ((k,v) for k,v in self._data.items() if v.active),
390                        ( "Active","Value" ),
391                        lambda k, v: [ v.active, value(v), ] )
392
393
394class ScalarObjective(_GeneralObjectiveData, Objective):
395    """
396    ScalarObjective is the implementation representing a single,
397    non-indexed objective.
398    """
399
400    def __init__(self, *args, **kwd):
401        _GeneralObjectiveData.__init__(self, expr=None, component=self)
402        Objective.__init__(self, *args, **kwd)
403
404    #
405    # Since this class derives from Component and
406    # Component.__getstate__ just packs up the entire __dict__ into
407    # the state dict, we do not need to define the __getstate__ or
408    # __setstate__ methods.  We just defer to the super() get/set
409    # state.  Since all of our get/set state methods rely on super()
410    # to traverse the MRO, this will automatically pick up both the
411    # Component and Data base classes.
412    #
413
414    #
415    # Override abstract interface methods to first check for
416    # construction
417    #
418
419    @property
420    def expr(self):
421        """Access the expression of this objective."""
422        if self._constructed:
423            if len(self._data) == 0:
424                raise ValueError(
425                    "Accessing the expression of ScalarObjective "
426                    "'%s' before the Objective has been assigned "
427                    "a sense or expression. There is currently "
428                    "nothing to access." % (self.name))
429            return _GeneralObjectiveData.expr.fget(self)
430        raise ValueError(
431            "Accessing the expression of objective '%s' "
432            "before the Objective has been constructed (there "
433            "is currently no value to return)."
434            % (self.name))
435    @expr.setter
436    def expr(self, expr):
437        """Set the expression of this objective."""
438        self.set_value(expr)
439
440    # for backwards compatibility reasons
441    @property
442    @deprecated("The .value property getter on ScalarObjective is deprecated. "
443                "Use the .expr property getter instead", version='4.3.11323')
444    def value(self):
445        return self.expr
446
447    @value.setter
448    @deprecated("The .value property setter on ScalarObjective is deprecated. "
449                "Use the set_value(expr) method instead", version='4.3.11323')
450    def value(self, expr):
451        self.set_value(expr)
452
453    @property
454    def sense(self):
455        """Access sense (direction) of this objective."""
456        if self._constructed:
457            if len(self._data) == 0:
458                raise ValueError(
459                    "Accessing the sense of ScalarObjective "
460                    "'%s' before the Objective has been assigned "
461                    "a sense or expression. There is currently "
462                    "nothing to access." % (self.name))
463            return _GeneralObjectiveData.sense.fget(self)
464        raise ValueError(
465            "Accessing the sense of objective '%s' "
466            "before the Objective has been constructed (there "
467            "is currently no value to return)."
468            % (self.name))
469    @sense.setter
470    def sense(self, sense):
471        """Set the sense (direction) of this objective."""
472        self.set_sense(sense)
473
474    #
475    # Singleton objectives are strange in that we want them to be
476    # both be constructed but have len() == 0 when not initialized with
477    # anything (at least according to the unit tests that are
478    # currently in place). So during initialization only, we will
479    # treat them as "indexed" objects where things like
480    # Objective.Skip are managed. But after that they will behave
481    # like _ObjectiveData objects where set_value does not handle
482    # Objective.Skip but expects a valid expression or None
483    #
484
485    def clear(self):
486        self._data = {}
487
488    def set_value(self, expr):
489        """Set the expression of this objective."""
490        if not self._constructed:
491            raise ValueError(
492                "Setting the value of objective '%s' "
493                "before the Objective has been constructed (there "
494                "is currently no object to set)."
495                % (self.name))
496        if not self._data:
497            self._data[None] = self
498        return super().set_value(expr)
499
500    def set_sense(self, sense):
501        """Set the sense (direction) of this objective."""
502        if self._constructed:
503            if len(self._data) == 0:
504                self._data[None] = self
505            return _GeneralObjectiveData.set_sense(self, sense)
506        raise ValueError(
507            "Setting the sense of objective '%s' "
508            "before the Objective has been constructed (there "
509            "is currently no object to set)."
510            % (self.name))
511
512    #
513    # Leaving this method for backward compatibility reasons.
514    # (probably should be removed)
515    #
516    def add(self, index, expr):
517        """Add an expression with a given index."""
518        if index is not None:
519            raise ValueError(
520                "ScalarObjective object '%s' does not accept "
521                "index values other than None. Invalid value: %s"
522                % (self.name, index))
523        self.set_value(expr)
524        return self
525
526
527class SimpleObjective(metaclass=RenamedClass):
528    __renamed__new_class__ = ScalarObjective
529    __renamed__version__ = '6.0'
530
531
532class IndexedObjective(Objective):
533
534    #
535    # Leaving this method for backward compatibility reasons
536    #
537    # Note: Beginning after Pyomo 5.2 this method will now validate that
538    # the index is in the underlying index set (through 5.2 the index
539    # was not checked).
540    #
541    def add(self, index, expr):
542        """Add an objective with a given index."""
543        return self.__setitem__(index, expr)
544
545
546@ModelComponentFactory.register("A list of objective expressions.")
547class ObjectiveList(IndexedObjective):
548    """
549    An objective component that represents a list of objectives.
550    Objectives can be indexed by their index, but when they are added
551    an index value is not specified.
552    """
553
554    class End(object): pass
555
556    def __init__(self, **kwargs):
557        """Constructor"""
558        if 'expr' in kwargs:
559            raise ValueError(
560                "ObjectiveList does not accept the 'expr' keyword")
561        _rule = kwargs.pop('rule', None)
562
563        args = (Set(dimen=1),)
564        super().__init__(*args, **kwargs)
565
566        self.rule = Initializer(_rule, allow_generators=True)
567        # HACK to make the "counted call" syntax work.  We wait until
568        # after the base class is set up so that is_indexed() is
569        # reliable.
570        if self.rule is not None and type(self.rule) is IndexedCallInitializer:
571            self.rule = CountedCallInitializer(self, self.rule)
572
573    def construct(self, data=None):
574        """
575        Construct the expression(s) for this objective.
576        """
577        if self._constructed:
578            return
579        self._constructed=True
580
581        if is_debug_set(logger):
582            logger.debug("Constructing objective list %s"
583                         % (self.name))
584
585        self.index_set().construct()
586
587        if self.rule is not None:
588            _rule = self.rule(self.parent_block(), ())
589            for cc in iter(_rule):
590                if cc is ObjectiveList.End:
591                    break
592                if cc is Objective.Skip:
593                    continue
594                self.add(cc, sense=self._init_sense)
595
596    def add(self, expr, sense=minimize):
597        """Add an objective to the list."""
598        next_idx = len(self._index) + 1
599        self._index.add(next_idx)
600        ans = self.__setitem__(next_idx, expr)
601        if ans is not None:
602            if sense not in {minimize, maximize}:
603                sense = sense(self.parent_block(), next_idx)
604            ans.set_sense(sense)
605        return ans
606
607