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__ = ['Expression', '_ExpressionData']
12
13import sys
14import logging
15from weakref import ref as weakref_ref
16
17from pyomo.common.log import is_debug_set
18from pyomo.common.deprecation import deprecated, RenamedClass
19from pyomo.common.modeling import NOTSET
20from pyomo.common.formatting import tabular_writer
21from pyomo.common.timing import ConstructionTimer
22
23from pyomo.core.base.component import ComponentData, ModelComponentFactory
24from pyomo.core.base.indexed_component import (
25    IndexedComponent,
26    UnindexedComponent_set, )
27from pyomo.core.base.misc import apply_indexed_rule
28from pyomo.core.base.numvalue import (NumericValue,
29                                      as_numeric)
30from pyomo.core.base.initializer import Initializer
31
32logger = logging.getLogger('pyomo.core')
33
34
35class _ExpressionData(NumericValue):
36    """
37    An object that defines a named expression.
38
39    Public Class Attributes
40        expr       The expression owned by this data.
41    """
42
43    __slots__ = ()
44
45    #
46    # Interface
47    #
48
49    def __call__(self, exception=True):
50        """Compute the value of this expression."""
51        if self.expr is None:
52            return None
53        return self.expr(exception=exception)
54
55    def is_named_expression_type(self):
56        """A boolean indicating whether this in a named expression."""
57        return True
58
59    def is_expression_type(self):
60        """A boolean indicating whether this in an expression."""
61        return True
62
63    def arg(self, index):
64        if index < 0 or index >= 1:
65            raise KeyError("Invalid index for expression argument: %d" % index)
66        return self.expr
67
68    @property
69    def _args_(self):
70        return (self.expr,)
71
72    @property
73    def args(self):
74        return (self.expr,)
75
76    def nargs(self):
77        return 1
78
79    def _precedence(self):
80        return 0
81
82    def _associativity(self):
83        return 0
84
85    def _to_string(self, values, verbose, smap, compute_values):
86        if verbose:
87            return "%s{%s}" % (str(self), values[0])
88        if self.expr is None:
89            return "%s{None}" % str(self)
90        return values[0]
91
92    def clone(self):
93        """Return a clone of this expression (no-op)."""
94        return self
95
96    def _apply_operation(self, result):
97        # This "expression" is a no-op wrapper, so just return the inner
98        # result
99        return result[0]
100
101    def polynomial_degree(self):
102        """A tuple of subexpressions involved in this expressions operation."""
103        return self.expr.polynomial_degree()
104
105    def _compute_polynomial_degree(self, result):
106        return result[0]
107
108    def _is_fixed(self, values):
109        return values[0]
110
111    #
112    # Abstract Interface
113    #
114
115    @property
116    def expr(self):
117        """Return expression on this expression."""
118        raise NotImplementedError
119
120    def set_value(self, expr):
121        """Set the expression on this expression."""
122        raise NotImplementedError
123
124    def is_constant(self):
125        """A boolean indicating whether this expression is constant."""
126        raise NotImplementedError
127
128    def is_fixed(self):
129        """A boolean indicating whether this expression is fixed."""
130        raise NotImplementedError
131
132    # _ExpressionData should never return False because
133    # they can store subexpressions that contain variables
134    def is_potentially_variable(self):
135        return True
136
137
138class _GeneralExpressionDataImpl(_ExpressionData):
139    """
140    An object that defines an expression that is never cloned
141
142    Constructor Arguments
143        expr        The Pyomo expression stored in this expression.
144        component   The Expression object that owns this data.
145
146    Public Class Attributes
147        expr       The expression owned by this data.
148    """
149
150    __pickle_slots__ = ('_expr',)
151
152    __slots__ = ()
153
154    def __init__(self, expr=None):
155        self._expr = as_numeric(expr) if (expr is not None) else None
156
157    def create_node_with_local_data(self, values):
158        """
159        Construct a simple expression after constructing the
160        contained expression.
161
162        This class provides a consistent interface for constructing a
163        node, which is used in tree visitor scripts.
164        """
165        obj = ScalarExpression()
166        obj.construct()
167        obj.expr = values[0]
168        return obj
169
170    def __getstate__(self):
171        state = super(_GeneralExpressionDataImpl, self).__getstate__()
172        for i in _GeneralExpressionDataImpl.__pickle_slots__:
173            state[i] = getattr(self, i)
174        return state
175
176    # Note: because NONE of the slots on this class need to be edited,
177    #       we don't need to implement a specialized __setstate__
178    #       method.
179
180    #
181    # Abstract Interface
182    #
183
184    @property
185    def expr(self):
186        """Return expression on this expression."""
187        return self._expr
188    @expr.setter
189    def expr(self, expr):
190        self.set_value(expr)
191
192    # for backwards compatibility reasons
193    @property
194    @deprecated("The .value property getter on _GeneralExpressionDataImpl "
195                "is deprecated. Use the .expr property getter instead",
196                version='4.3.11323')
197    def value(self):
198        return self._expr
199
200    @value.setter
201    @deprecated("The .value property setter on _GeneralExpressionDataImpl "
202                "is deprecated. Use the set_value(expr) method instead",
203                version='4.3.11323')
204    def value(self, expr):
205        self.set_value(expr)
206
207    def set_value(self, expr):
208        """Set the expression on this expression."""
209        self._expr = as_numeric(expr) if (expr is not None) else None
210
211    def is_constant(self):
212        """A boolean indicating whether this expression is constant."""
213        # The underlying expression can always be changed
214        # so this should never evaluate as constant
215        return False
216
217    def is_fixed(self):
218        """A boolean indicating whether this expression is fixed."""
219        return self._expr.is_fixed()
220
221class _GeneralExpressionData(_GeneralExpressionDataImpl,
222                             ComponentData):
223    """
224    An object that defines an expression that is never cloned
225
226    Constructor Arguments
227        expr        The Pyomo expression stored in this expression.
228        component   The Expression object that owns this data.
229
230    Public Class Attributes
231        expr        The expression owned by this data.
232
233    Private class attributes:
234        _component  The expression component.
235    """
236
237    __slots__ = _GeneralExpressionDataImpl.__pickle_slots__
238
239    def __init__(self, expr=None, component=None):
240        _GeneralExpressionDataImpl.__init__(self, expr)
241        # Inlining ComponentData.__init__
242        self._component = weakref_ref(component) if (component is not None) \
243                          else None
244
245
246@ModelComponentFactory.register(
247    "Named expressions that can be used in other expressions.")
248class Expression(IndexedComponent):
249    """
250    A shared expression container, which may be defined over a index.
251
252    Constructor Arguments:
253        initialize  A Pyomo expression or dictionary of expressions
254                        used to initialize this object.
255        expr        A synonym for initialize.
256        rule        A rule function used to initialize this object.
257    """
258
259    _ComponentDataClass = _GeneralExpressionData
260    # This seems like a copy-paste error, and should be renamed/removed
261    NoConstraint = IndexedComponent.Skip
262
263    def __new__(cls, *args, **kwds):
264        if cls != Expression:
265            return super(Expression, cls).__new__(cls)
266        if not args or (args[0] is UnindexedComponent_set and len(args)==1):
267            return ScalarExpression.__new__(ScalarExpression)
268        else:
269            return IndexedExpression.__new__(IndexedExpression)
270
271    def __init__(self, *args, **kwds):
272        _init = tuple(
273            arg for arg in
274            (kwds.pop(_arg, None) for _arg in ('rule', 'expr', 'initialize'))
275            if arg is not None
276        )
277        if len(_init) == 1:
278            _init = _init[0]
279        elif not _init:
280            _init = None
281        else:
282            raise ValueError(
283                "Duplicate initialization: Expression() only "
284                "accepts one of 'rule=', 'expr=', and 'initialize='")
285
286        kwds.setdefault('ctype', Expression)
287        IndexedComponent.__init__(self, *args, **kwds)
288
289        # Historically, Expression objects were dense (but None):
290        # setting arg_not_specified causes Initializer to recognize
291        # _init==None as a constant initializer returning None
292        self._rule = Initializer(_init, arg_not_specified=NOTSET)
293
294    def _pprint(self):
295        return (
296            [('Size', len(self)),
297             ('Index', None if (not self.is_indexed())
298                  else self._index)
299             ],
300            self.items(),
301            ("Expression",),
302            lambda k,v: \
303               ["Undefined" if v.expr is None else v.expr]
304            )
305
306    def display(self, prefix="", ostream=None):
307        """TODO"""
308        if not self.active:
309            return
310        if ostream is None:
311            ostream = sys.stdout
312        tab="    "
313        ostream.write(prefix+self.local_name+" : ")
314        ostream.write("Size="+str(len(self)))
315
316        ostream.write("\n")
317        tabular_writer(
318            ostream,
319            prefix+tab,
320            ((k,v) for k,v in self._data.items()),
321            ( "Value", ),
322            lambda k, v: \
323               ["Undefined" if v.expr is None else v()])
324
325    #
326    # A utility to extract all index-value pairs defining this
327    # expression, returned as a dictionary. useful in many contexts,
328    # in which key iteration and repeated __getitem__ calls are too
329    # expensive to extract the contents of an expression.
330    #
331    def extract_values(self):
332        return {key:expression_data.expr
333                for key, expression_data in self.items()}
334
335    #
336    # takes as input a (index, value) dictionary for updating this
337    # Expression.  if check=True, then both the index and value are
338    # checked through the __getitem__ method of this class.
339    #
340    def store_values(self, new_values):
341
342        if (self.is_indexed() is False) and \
343           (not None in new_values):
344            raise KeyError(
345                "Cannot store value for scalar Expression"
346                "="+self.name+"; no value with index "
347                "None in input new values map.")
348
349        for index, new_value in new_values.items():
350            self._data[index].set_value(new_value)
351
352    def _getitem_when_not_present(self, idx):
353        if self._rule is None:
354            _init = None
355            # TBD: Is this desired behavior?  I can see implicitly setting
356            # an Expression if it was not originally defined, but I am less
357            # convinced that implicitly creating an Expression (like what
358            # works with a Var) makes sense.  [JDS 25 Nov 17]
359            #raise KeyError(idx)
360        else:
361            _init = self._rule(self.parent_block(), idx)
362        obj = self._setitem_when_not_present(idx, _init)
363        #if obj is None:
364        #    raise KeyError(idx)
365        return obj
366
367    def construct(self, data=None):
368        """ Apply the rule to construct values in this set """
369        if self._constructed:
370            return
371        self._constructed = True
372
373        timer = ConstructionTimer(self)
374        if is_debug_set(logger):
375            logger.debug(
376                "Constructing Expression, name=%s, from data=%s"
377                % (self.name, str(data)))
378
379        try:
380            # We do not (currently) accept data for constructing Constraints
381            assert data is None
382            self._construct_from_rule_using_setitem()
383        finally:
384            timer.report()
385
386
387class ScalarExpression(_GeneralExpressionData, Expression):
388
389    def __init__(self, *args, **kwds):
390        _GeneralExpressionData.__init__(self, expr=None, component=self)
391        Expression.__init__(self, *args, **kwds)
392
393    #
394    # Since this class derives from Component and
395    # Component.__getstate__ just packs up the entire __dict__ into
396    # the state dict, we do not need to define the __getstate__ or
397    # __setstate__ methods.  We just defer to the super() get/set
398    # state.  Since all of our get/set state methods rely on super()
399    # to traverse the MRO, this will automatically pick up both the
400    # Component and Data base classes.
401    #
402
403    #
404    # Override abstract interface methods to first check for
405    # construction
406    #
407
408    @property
409    def expr(self):
410        """Return expression on this expression."""
411        if self._constructed:
412            return _GeneralExpressionData.expr.fget(self)
413        raise ValueError(
414            "Accessing the expression of expression '%s' "
415            "before the Expression has been constructed (there "
416            "is currently no value to return)."
417            % (self.name))
418    @expr.setter
419    def expr(self, expr):
420        """Set the expression on this expression."""
421        self.set_value(expr)
422
423    # for backwards compatibility reasons
424    @property
425    @deprecated("The .value property getter on ScalarExpression "
426                "is deprecated. Use the .expr property getter instead",
427                version='4.3.11323')
428    def value(self):
429        return self.expr
430
431    @value.setter
432    @deprecated("The .value property setter on ScalarExpression "
433                "is deprecated. Use the set_value(expr) method instead",
434                version='4.3.11323')
435    def value(self, expr):
436        self.set_value(expr)
437
438    def clear(self):
439        self._data = {}
440
441    def set_value(self, expr):
442        """Set the expression on this expression."""
443        if self._constructed:
444            return _GeneralExpressionData.set_value(self, expr)
445        raise ValueError(
446            "Setting the expression of expression '%s' "
447            "before the Expression has been constructed (there "
448            "is currently no object to set)."
449            % (self.name))
450
451    def is_constant(self):
452        """A boolean indicating whether this expression is constant."""
453        if self._constructed:
454            return _GeneralExpressionData.is_constant(self)
455        raise ValueError(
456            "Accessing the is_constant flag of expression '%s' "
457            "before the Expression has been constructed (there "
458            "is currently no value to return)."
459            % (self.name))
460
461    def is_fixed(self):
462        """A boolean indicating whether this expression is fixed."""
463        if self._constructed:
464            return _GeneralExpressionData.is_fixed(self)
465        raise ValueError(
466            "Accessing the is_fixed flag of expression '%s' "
467            "before the Expression has been constructed (there "
468            "is currently no value to return)."
469            % (self.name))
470
471    #
472    # Leaving this method for backward compatibility reasons.
473    # (probably should be removed)
474    #
475    def add(self, index, expr):
476        """Add an expression with a given index."""
477        if index is not None:
478            raise KeyError(
479                "ScalarExpression object '%s' does not accept "
480                "index values other than None. Invalid value: %s"
481                % (self.name, index))
482        if (type(expr) is tuple) and \
483           (expr == Expression.Skip):
484            raise ValueError(
485                "Expression.Skip can not be assigned "
486                "to an Expression that is not indexed: %s"
487                % (self.name))
488        self.set_value(expr)
489        return self
490
491
492class SimpleExpression(metaclass=RenamedClass):
493    __renamed__new_class__ = ScalarExpression
494    __renamed__version__ = '6.0'
495
496
497class IndexedExpression(Expression):
498
499    #
500    # Leaving this method for backward compatibility reasons
501    # Note: It allows adding members outside of self._index.
502    #       This has always been the case. Not sure there is
503    #       any reason to maintain a reference to a separate
504    #       index set if we allow this.
505    #
506    def add(self, index, expr):
507        """Add an expression with a given index."""
508        if (type(expr) is tuple) and \
509           (expr == Expression.Skip):
510            return None
511        cdata = _GeneralExpressionData(expr, component=self)
512        self._data[index] = cdata
513        return cdata
514
515