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__ = ('value', 'is_constant', 'is_fixed', 'is_variable_type',
12           'is_potentially_variable', 'NumericValue', 'ZeroConstant',
13           'native_numeric_types', 'native_types', 'nonpyomo_leaf_types',
14           'polynomial_degree')
15
16import sys
17import logging
18
19from pyomo.common.dependencies import numpy as np, numpy_available
20from pyomo.common.deprecation import deprecated
21from pyomo.core.expr.expr_common import (
22    _add, _sub, _mul, _div, _pow,
23    _neg, _abs, _radd,
24    _rsub, _rmul, _rdiv, _rpow,
25    _iadd, _isub, _imul, _idiv,
26    _ipow, _lt, _le, _eq
27)
28# TODO: update imports of these objects to pull from numeric_types
29from pyomo.common.numeric_types import (
30    nonpyomo_leaf_types, native_types, native_numeric_types,
31    native_integer_types, native_boolean_types, native_logical_types,
32    RegisterNumericType, RegisterIntegerType, RegisterBooleanType,
33    pyomo_constant_types,
34)
35from pyomo.core.pyomoobject import PyomoObject
36from pyomo.core.expr.expr_errors import TemplateExpressionError
37
38logger = logging.getLogger('pyomo.core')
39
40
41def _generate_sum_expression(etype, _self, _other):
42    raise RuntimeError("incomplete import of Pyomo expression system")  #pragma: no cover
43def _generate_mul_expression(etype, _self, _other):
44    raise RuntimeError("incomplete import of Pyomo expression system")  #pragma: no cover
45def _generate_other_expression(etype, _self, _other):
46    raise RuntimeError("incomplete import of Pyomo expression system")  #pragma: no cover
47def _generate_relational_expression(etype, lhs, rhs):
48    raise RuntimeError("incomplete import of Pyomo expression system")  #pragma: no cover
49
50##------------------------------------------------------------------------
51##
52## Standard types of expressions
53##
54##------------------------------------------------------------------------
55
56class NonNumericValue(object):
57    """An object that contains a non-numeric value
58
59    Constructor Arguments:
60        value           The initial value.
61    """
62    __slots__ = ('value',)
63
64    def __init__(self, value):
65        self.value = value
66
67    def __str__(self):
68        return str(self.value)
69
70    def __getstate__(self):
71        state = {}
72        state['value'] = getattr(self,'value')
73        return state
74
75    def __setstate__(self, state):
76        setattr(self, 'value', state['value'])
77
78nonpyomo_leaf_types.add(NonNumericValue)
79
80
81def value(obj, exception=True):
82    """
83    A utility function that returns the value of a Pyomo object or
84    expression.
85
86    Args:
87        obj: The argument to evaluate. If it is None, a
88            string, or any other primative numeric type,
89            then this function simply returns the argument.
90            Otherwise, if the argument is a NumericValue
91            then the __call__ method is executed.
92        exception (bool): If :const:`True`, then an exception should
93            be raised when instances of NumericValue fail to
94            evaluate due to one or more objects not being
95            initialized to a numeric value (e.g, one or more
96            variables in an algebraic expression having the
97            value None). If :const:`False`, then the function
98            returns :const:`None` when an exception occurs.
99            Default is True.
100
101    Returns: A numeric value or None.
102    """
103    if obj.__class__ in native_types:
104        return obj
105    if obj.__class__ in pyomo_constant_types:
106        #
107        # I'm commenting this out for now, but I think we should never expect
108        # to see a numeric constant with value None.
109        #
110        #if exception and obj.value is None:
111        #    raise ValueError(
112        #        "No value for uninitialized NumericConstant object %s"
113        #        % (obj.name,))
114        return obj.value
115    #
116    # Test if we have a duck types for Pyomo expressions
117    #
118    try:
119        obj.is_expression_type()
120    except AttributeError:
121        #
122        # If not, then try to coerce this into a numeric constant.  If that
123        # works, then return the object
124        #
125        try:
126            check_if_numeric_type_and_cache(obj)
127            return obj
128        except:
129            raise TypeError(
130                "Cannot evaluate object with unknown type: %s" %
131                (type(obj).__name__,))
132    #
133    # Evaluate the expression object
134    #
135    if exception:
136        #
137        # Here, we try to catch the exception
138        #
139
140        try:
141            tmp = obj(exception=True)
142            if tmp is None:
143                raise ValueError(
144                    "No value for uninitialized NumericValue object %s"
145                    % (obj.name,))
146            return tmp
147        except TemplateExpressionError:
148            # Template expressions work by catching this error type. So
149            # we should defer this error handling and not log an error
150            # message.
151            raise
152        except:
153            logger.error(
154                "evaluating object as numeric value: %s\n    (object: %s)\n%s"
155                % (obj, type(obj), sys.exc_info()[1]))
156            raise
157    else:
158        #
159        # Here, we do not try to catch the exception
160        #
161        return obj(exception=False)
162
163
164def is_constant(obj):
165    """
166    A utility function that returns a boolean that indicates
167    whether the object is a constant.
168    """
169    # This method is rarely, if ever, called.  Plus, since the
170    # expression generation (and constraint generation) system converts
171    # everything to NumericValues, it is better (i.e., faster) to assume
172    # that the obj is a NumericValue
173    #
174    # JDS: NB: I am not sure why we allow str to be a constant, but
175    # since we have historically done so, we check for type membership
176    # in native_types and not in native_numeric_types.
177    #
178    if obj.__class__ in native_types:
179        return True
180    try:
181        return obj.is_constant()
182    except AttributeError:
183        pass
184    try:
185        # Now we need to confirm that we have an unknown numeric type
186        check_if_numeric_type_and_cache(obj)
187        # As this branch is only hit for previously unknown (to Pyomo)
188        # types that behave reasonably like numbers, we know they *must*
189        # be constant.
190        return True
191    except:
192        raise TypeError(
193            "Cannot assess properties of object with unknown type: %s"
194            % (type(obj).__name__,))
195
196def is_fixed(obj):
197    """
198    A utility function that returns a boolean that indicates
199    whether the input object's value is fixed.
200    """
201    # JDS: NB: I am not sure why we allow str to be a constant, but
202    # since we have historically done so, we check for type membership
203    # in native_types and not in native_numeric_types.
204    #
205    if obj.__class__ in native_types:
206        return True
207    try:
208        return obj.is_fixed()
209    except AttributeError:
210        pass
211    try:
212        # Now we need to confirm that we have an unknown numeric type
213        check_if_numeric_type_and_cache(obj)
214        # As this branch is only hit for previously unknown (to Pyomo)
215        # types that behave reasonably like numbers, we know they *must*
216        # be fixed.
217        return True
218    except:
219        raise TypeError(
220            "Cannot assess properties of object with unknown type: %s"
221            % (type(obj).__name__,))
222
223def is_variable_type(obj):
224    """
225    A utility function that returns a boolean indicating
226    whether the input object is a variable.
227    """
228    if obj.__class__ in native_types:
229        return False
230    try:
231        return obj.is_variable_type()
232    except AttributeError:
233        return False
234
235def is_potentially_variable(obj):
236    """
237    A utility function that returns a boolean indicating
238    whether the input object can reference variables.
239    """
240    if obj.__class__ in native_types:
241        return False
242    try:
243        return obj.is_potentially_variable()
244    except AttributeError:
245        return False
246
247def is_numeric_data(obj):
248    """
249    A utility function that returns a boolean indicating
250    whether the input object is numeric and not potentially
251    variable.
252    """
253    if obj.__class__ in native_numeric_types:
254        return True
255    elif obj.__class__ in native_types:
256        # this likely means it is a string
257        return False
258    try:
259        # Test if this is an expression object that
260        # is not potentially variable
261        return not obj.is_potentially_variable()
262    except AttributeError:
263        pass
264    try:
265        # Now we need to confirm that we have an unknown numeric type
266        check_if_numeric_type_and_cache(obj)
267        # As this branch is only hit for previously unknown (to Pyomo)
268        # types that behave reasonably like numbers, we know they *must*
269        # be numeric data (unless an exception is raised).
270        return True
271    except:
272        pass
273    return False
274
275def polynomial_degree(obj):
276    """
277    A utility function that returns an integer
278    that indicates the polynomial degree for an
279    object. boolean indicating
280    """
281    if obj.__class__ in native_numeric_types:
282        return 0
283    elif obj.__class__ in native_types:
284        raise TypeError(
285            "Cannot evaluate the polynomial degree of a non-numeric type: %s"
286            % (type(obj).__name__,))
287    try:
288        return obj.polynomial_degree()
289    except AttributeError:
290        pass
291    try:
292        # Now we need to confirm that we have an unknown numeric type
293        check_if_numeric_type_and_cache(obj)
294        # As this branch is only hit for previously unknown (to Pyomo)
295        # types that behave reasonably like numbers, we know they *must*
296        # be a numeric constant.
297        return 0
298    except:
299        raise TypeError(
300            "Cannot assess properties of object with unknown type: %s"
301            % (type(obj).__name__,))
302
303#
304# It is very common to have only a few constants in a model, but those
305# constants get repeated many times.  KnownConstants lets us re-use /
306# share constants we have seen before.
307#
308# Note:
309#   For now, all constants are coerced to floats.  This avoids integer
310#   division in Python 2.x.  (At least some of the time.)
311#
312#   When we eliminate support for Python 2.x, we will not need this
313#   coercion.  The main difference in the following code is that we will
314#   need to index KnownConstants by both the class type and value, since
315#   INT, FLOAT and LONG values sometimes hash the same.
316#
317_KnownConstants = {}
318
319def as_numeric(obj):
320    """
321    A function that creates a NumericConstant object that
322    wraps Python numeric values.
323
324    This function also manages a cache of constants.
325
326    NOTE:  This function is only intended for use when
327        data is added to a component.
328
329    Args:
330        obj: The numeric value that may be wrapped.
331
332    Raises: TypeError if the object is in native_types and not in
333        native_numeric_types
334
335    Returns: A NumericConstant object or the original object.
336    """
337    if obj.__class__ in native_numeric_types:
338        val = _KnownConstants.get(obj, None)
339        if val is not None:
340            return val
341        #
342        # Coerce the value to a float, if possible
343        #
344        try:
345            obj = float(obj)
346        except:
347            pass
348        #
349        # Create the numeric constant.  This really
350        # should be the only place in the code
351        # where these objects are constructed.
352        #
353        retval = NumericConstant(obj)
354        #
355        # Cache the numeric constants.  We used a bounded cache size
356        # to avoid unexpectedly large lists of constants.  There are
357        # typically a small number of constants that need to be cached.
358        #
359        # NOTE:  A LFU policy might be more sensible here, but that
360        # requires a more complex cache.  It's not clear that that
361        # is worth the extra cost.
362        #
363        if len(_KnownConstants) < 1024:
364            _KnownConstants[obj] = retval
365            return retval
366        #
367        return retval
368    #
369    # Ignore objects that are duck types to work with Pyomo expressions
370    #
371    try:
372        obj.is_expression_type()
373        return obj
374    except AttributeError:
375        pass
376    #
377    # Test if the object looks like a number.  If so, register that type with a
378    # warning.
379    #
380    try:
381        return check_if_numeric_type_and_cache(obj)
382    except:
383        pass
384    #
385    # Generate errors
386    #
387    if obj.__class__ in native_types:
388        raise TypeError("Cannot treat the value '%s' as a constant" % str(obj))
389    raise TypeError(
390        "Cannot treat the value '%s' as a constant because it has unknown "
391        "type '%s'" % (str(obj), type(obj).__name__))
392
393
394def check_if_numeric_type_and_cache(obj):
395    """Test if the argument is a numeric type by checking if we can add
396    zero to it.  If that works, then we cache the value and return a
397    NumericConstant object.
398
399    """
400    obj_class = obj.__class__
401    if obj_class is (obj + 0).__class__:
402        #
403        # Coerce the value to a float, if possible
404        #
405        try:
406            obj = float(obj)
407        except:
408            pass
409        #
410        # obj may (or may not) be hashable, so we need this try
411        # block so that things proceed normally for non-hashable
412        # "numeric" types
413        #
414        retval = NumericConstant(obj)
415        try:
416            #
417            # Create the numeric constant and add to the
418            # list of known constants.
419            #
420            # Note: we don't worry about the size of the
421            # cache here, since we need to confirm that the
422            # object is hashable.
423            #
424            _KnownConstants[obj] = retval
425            #
426            # If we get here, this is a reasonably well-behaving
427            # numeric type: add it to the native numeric types
428            # so that future lookups will be faster.
429            #
430            native_numeric_types.add(obj_class)
431            native_types.add(obj_class)
432            nonpyomo_leaf_types.add(obj_class)
433            #
434            # Generate a warning, since Pyomo's management of third-party
435            # numeric types is more robust when registering explicitly.
436            #
437            logger.warning(
438                """Dynamically registering the following numeric type:
439    %s
440Dynamic registration is supported for convenience, but there are known
441limitations to this approach.  We recommend explicitly registering
442numeric types using the following functions:
443    RegisterNumericType(), RegisterIntegerType(), RegisterBooleanType()."""
444                % (obj_class.__name__,))
445        except:
446            pass
447        return retval
448
449
450class NumericValue(PyomoObject):
451    """
452    This is the base class for numeric values used in Pyomo.
453    """
454
455    __slots__ = ()
456
457    # This is required because we define __eq__
458    __hash__ = None
459
460    def __getstate__(self):
461        """
462        Prepare a picklable state of this instance for pickling.
463
464        Nominally, __getstate__() should execute the following::
465
466            state = super(Class, self).__getstate__()
467            for i in Class.__slots__:
468                state[i] = getattr(self,i)
469            return state
470
471        However, in this case, the (nominal) parent class is 'object',
472        and object does not implement __getstate__.  So, we will
473        check to make sure that there is a base __getstate__() to
474        call.  You might think that there is nothing to check, but
475        multiple inheritance could mean that another class got stuck
476        between this class and "object" in the MRO.
477
478        Further, since there are actually no slots defined here, the
479        real question is to either return an empty dict or the
480        parent's dict.
481        """
482        _base = super(NumericValue, self)
483        if hasattr(_base, '__getstate__'):
484            return _base.__getstate__()
485        else:
486            return {}
487
488    def __setstate__(self, state):
489        """
490        Restore a pickled state into this instance
491
492        Our model for setstate is for derived classes to modify
493        the state dictionary as control passes up the inheritance
494        hierarchy (using super() calls).  All assignment of state ->
495        object attributes is handled at the last class before 'object',
496        which may -- or may not (thanks to MRO) -- be here.
497        """
498        _base = super(NumericValue, self)
499        if hasattr(_base, '__setstate__'):
500            return _base.__setstate__(state)
501        else:
502            for key, val in state.items():
503                # Note: per the Python data model docs, we explicitly
504                # set the attribute using object.__setattr__() instead
505                # of setting self.__dict__[key] = val.
506                object.__setattr__(self, key, val)
507
508    def getname(self, fully_qualified=False, name_buffer=None):
509        """
510        If this is a component, return the component's name on the owning
511        block; otherwise return the value converted to a string
512        """
513        _base = super(NumericValue, self)
514        if hasattr(_base,'getname'):
515            return _base.getname(fully_qualified, name_buffer)
516        else:
517            return str(type(self))
518
519    @property
520    def name(self):
521        return self.getname(fully_qualified=True)
522
523    @property
524    def local_name(self):
525        return self.getname(fully_qualified=False)
526
527    @deprecated("The cname() method has been renamed to getname().",
528                version='5.0')
529    def cname(self, *args, **kwds):
530        return self.getname(*args, **kwds)
531
532    def is_numeric_type(self):
533        """Return True if this class is a Pyomo numeric object"""
534        return True
535
536    def is_constant(self):
537        """Return True if this numeric value is a constant value"""
538        return False
539
540    def is_fixed(self):
541        """Return True if this is a non-constant value that has been fixed"""
542        return False
543
544    def is_potentially_variable(self):
545        """Return True if variables can appear in this expression"""
546        return False
547
548    def is_relational(self):
549        """
550        Return True if this numeric value represents a relational expression.
551        """
552        return False
553
554    def is_indexed(self):
555        """Return True if this numeric value is an indexed object"""
556        return False
557
558    def polynomial_degree(self):
559        """
560        Return the polynomial degree of the expression.
561
562        Returns:
563            :const:`None`
564        """
565        return self._compute_polynomial_degree(None)
566
567    def _compute_polynomial_degree(self, values):
568        """
569        Compute the polynomial degree of this expression given
570        the degree values of its children.
571
572        Args:
573            values (list): A list of values that indicate the degree
574                of the children expression.
575
576        Returns:
577            :const:`None`
578        """
579        return None
580
581    def __float__(self):
582        """
583        Coerce the value to a floating point
584
585        Raises:
586            TypeError
587        """
588        raise TypeError(
589"""Implicit conversion of Pyomo NumericValue type `%s' to a float is
590disabled. This error is often the result of using Pyomo components as
591arguments to one of the Python built-in math module functions when
592defining expressions. Avoid this error by using Pyomo-provided math
593functions.""" % (self.name,))
594
595    def __int__(self):
596        """
597        Coerce the value to an integer
598
599        Raises:
600            TypeError
601        """
602        raise TypeError(
603"""Implicit conversion of Pyomo NumericValue type `%s' to an integer is
604disabled. This error is often the result of using Pyomo components as
605arguments to one of the Python built-in math module functions when
606defining expressions. Avoid this error by using Pyomo-provided math
607functions.""" % (self.name,))
608
609    def __lt__(self,other):
610        """
611        Less than operator
612
613        This method is called when Python processes statements of the form::
614
615            self < other
616            other > self
617        """
618        return _generate_relational_expression(_lt, self, other)
619
620    def __gt__(self,other):
621        """
622        Greater than operator
623
624        This method is called when Python processes statements of the form::
625
626            self > other
627            other < self
628        """
629        return _generate_relational_expression(_lt, other, self)
630
631    def __le__(self,other):
632        """
633        Less than or equal operator
634
635        This method is called when Python processes statements of the form::
636
637            self <= other
638            other >= self
639        """
640        return _generate_relational_expression(_le, self, other)
641
642    def __ge__(self,other):
643        """
644        Greater than or equal operator
645
646        This method is called when Python processes statements of the form::
647
648            self >= other
649            other <= self
650        """
651        return _generate_relational_expression(_le, other, self)
652
653    def __eq__(self,other):
654        """
655        Equal to operator
656
657        This method is called when Python processes the statement::
658
659            self == other
660        """
661        return _generate_relational_expression(_eq, self, other)
662
663    def __add__(self,other):
664        """
665        Binary addition
666
667        This method is called when Python processes the statement::
668
669            self + other
670        """
671        return _generate_sum_expression(_add,self,other)
672
673    def __sub__(self,other):
674        """
675        Binary subtraction
676
677        This method is called when Python processes the statement::
678
679            self - other
680        """
681        return _generate_sum_expression(_sub,self,other)
682
683    def __mul__(self,other):
684        """
685        Binary multiplication
686
687        This method is called when Python processes the statement::
688
689            self * other
690        """
691        return _generate_mul_expression(_mul,self,other)
692
693    def __div__(self,other):
694        """
695        Binary division
696
697        This method is called when Python processes the statement::
698
699            self / other
700        """
701        return _generate_mul_expression(_div,self,other)
702
703    def __truediv__(self,other):
704        """
705        Binary division (when __future__.division is in effect)
706
707        This method is called when Python processes the statement::
708
709            self / other
710        """
711        return _generate_mul_expression(_div,self,other)
712
713    def __pow__(self,other):
714        """
715        Binary power
716
717        This method is called when Python processes the statement::
718
719            self ** other
720        """
721        return _generate_other_expression(_pow,self,other)
722
723    def __radd__(self,other):
724        """
725        Binary addition
726
727        This method is called when Python processes the statement::
728
729            other + self
730        """
731        return _generate_sum_expression(_radd,self,other)
732
733    def __rsub__(self,other):
734        """
735        Binary subtraction
736
737        This method is called when Python processes the statement::
738
739            other - self
740        """
741        return _generate_sum_expression(_rsub,self,other)
742
743    def __rmul__(self,other):
744        """
745        Binary multiplication
746
747        This method is called when Python processes the statement::
748
749            other * self
750
751        when other is not a :class:`NumericValue <pyomo.core.expr.numvalue.NumericValue>` object.
752        """
753        return _generate_mul_expression(_rmul,self,other)
754
755    def __rdiv__(self,other):
756        """Binary division
757
758        This method is called when Python processes the statement::
759
760            other / self
761        """
762        return _generate_mul_expression(_rdiv,self,other)
763
764    def __rtruediv__(self,other):
765        """
766        Binary division (when __future__.division is in effect)
767
768        This method is called when Python processes the statement::
769
770            other / self
771        """
772        return _generate_mul_expression(_rdiv,self,other)
773
774    def __rpow__(self,other):
775        """
776        Binary power
777
778        This method is called when Python processes the statement::
779
780            other ** self
781        """
782        return _generate_other_expression(_rpow,self,other)
783
784    def __iadd__(self,other):
785        """
786        Binary addition
787
788        This method is called when Python processes the statement::
789
790            self += other
791        """
792        return _generate_sum_expression(_iadd,self,other)
793
794    def __isub__(self,other):
795        """
796        Binary subtraction
797
798        This method is called when Python processes the statement::
799
800            self -= other
801        """
802        return _generate_sum_expression(_isub,self,other)
803
804    def __imul__(self,other):
805        """
806        Binary multiplication
807
808        This method is called when Python processes the statement::
809
810            self *= other
811        """
812        return _generate_mul_expression(_imul,self,other)
813
814    def __idiv__(self,other):
815        """
816        Binary division
817
818        This method is called when Python processes the statement::
819
820            self /= other
821        """
822        return _generate_mul_expression(_idiv,self,other)
823
824    def __itruediv__(self,other):
825        """
826        Binary division (when __future__.division is in effect)
827
828        This method is called when Python processes the statement::
829
830            self /= other
831        """
832        return _generate_mul_expression(_idiv,self,other)
833
834    def __ipow__(self,other):
835        """
836        Binary power
837
838        This method is called when Python processes the statement::
839
840            self **= other
841        """
842        return _generate_other_expression(_ipow,self,other)
843
844    def __neg__(self):
845        """
846        Negation
847
848        This method is called when Python processes the statement::
849
850            - self
851        """
852        return _generate_sum_expression(_neg, self, None)
853
854    def __pos__(self):
855        """
856        Positive expression
857
858        This method is called when Python processes the statement::
859
860            + self
861        """
862        return self
863
864    def __abs__(self):
865        """ Absolute value
866
867        This method is called when Python processes the statement::
868
869            abs(self)
870        """
871        return _generate_other_expression(_abs,self, None)
872
873    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
874        return NumericNDArray.__array_ufunc__(
875            None, ufunc, method, *inputs, **kwargs)
876
877    def to_string(self, verbose=None, labeler=None, smap=None,
878                  compute_values=False):
879        """
880        Return a string representation of the expression tree.
881
882        Args:
883            verbose (bool): If :const:`True`, then the the string
884                representation consists of nested functions.  Otherwise,
885                the string representation is an algebraic equation.
886                Defaults to :const:`False`.
887            labeler: An object that generates string labels for
888                variables in the expression tree.  Defaults to :const:`None`.
889
890        Returns:
891            A string representation for the expression tree.
892        """
893        if compute_values:
894            try:
895                return str(self())
896            except:
897                pass
898        if not self.is_constant():
899            if smap:
900                return smap.getSymbol(self, labeler)
901            elif labeler is not None:
902                return labeler(self)
903        return self.__str__()
904
905
906class NumericConstant(NumericValue):
907    """An object that contains a constant numeric value.
908
909    Constructor Arguments:
910        value           The initial value.
911    """
912
913    __slots__ = ('value',)
914
915    def __init__(self, value):
916        self.value = value
917
918    def __getstate__(self):
919        state = super(NumericConstant, self).__getstate__()
920        for i in NumericConstant.__slots__:
921            state[i] = getattr(self,i)
922        return state
923
924    def is_constant(self):
925        return True
926
927    def is_fixed(self):
928        return True
929
930    def _compute_polynomial_degree(self, result):
931        return 0
932
933    def __str__(self):
934        return str(self.value)
935
936    def __nonzero__(self):
937        """Return True if the value is defined and non-zero"""
938        if self.value:
939            return True
940        if self.value is None:
941            raise ValueError("Numeric Constant: value is undefined")
942        return False
943
944    __bool__ = __nonzero__
945
946    def __call__(self, exception=True):
947        """Return the constant value"""
948        return self.value
949
950    def pprint(self, ostream=None, verbose=False):
951        if ostream is None:         #pragma:nocover
952            ostream = sys.stdout
953        ostream.write(str(self))
954
955
956pyomo_constant_types.add(NumericConstant)
957
958# We use as_numeric() so that the constant is also in the cache
959ZeroConstant = as_numeric(0)
960
961#
962# Note: the "if numpy_available" in the class definition also ensures
963# that the numpy types are registered if numpy is in fact available
964#
965class NumericNDArray(np.ndarray if numpy_available else object):
966    """An ndarray subclass that stores Pyomo numeric expressions"""
967
968    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
969        if method == '__call__':
970            # Convert all incoming types to ndarray (to prevent recursion)
971            args = [np.asarray(i) for i in inputs]
972            # Set the return type to be an 'object'.  This prevents the
973            # logical operators from casting the result to a bool.  This
974            # requires numpy >= 1.6
975            kwargs['dtype'] = object
976
977        # Delegate to the base ufunc, but return an instance of this
978        # class so that additional operators hit this method.
979        ans = getattr(ufunc, method)(*args, **kwargs)
980        if isinstance(ans, np.ndarray):
981            if ans.size == 1:
982                return ans[0]
983            return ans.view(NumericNDArray)
984        else:
985            return ans
986