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__ = ['IndexedComponent', 'ActiveIndexedComponent']
12
13import inspect
14import logging
15import sys
16
17from pyomo.core.expr.expr_errors import TemplateExpressionError
18from pyomo.core.expr.numvalue import native_types, NumericNDArray
19from pyomo.core.base.indexed_component_slice import IndexedComponent_slice
20from pyomo.core.base.initializer import Initializer
21from pyomo.core.base.component import Component, ActiveComponent
22from pyomo.core.base.config import PyomoOptions
23from pyomo.core.base.global_set import UnindexedComponent_set
24from pyomo.common import DeveloperError
25from pyomo.common.dependencies import numpy as np, numpy_available
26from pyomo.common.deprecation import deprecated, deprecation_warning
27from pyomo.common.modeling import NOTSET
28
29from collections.abc import Sequence
30
31logger = logging.getLogger('pyomo.core')
32
33sequence_types = {tuple, list}
34def normalize_index(x):
35    """Normalize a component index.
36
37    This flattens nested sequences into a single tuple.  There is a
38    "global" flag (normalize_index.flatten) that will turn off index
39    flattening across Pyomo.
40
41    Scalar values will be returned unchanged.  Tuples with a single
42    value will be unpacked and returned as a single value.
43
44    Returns
45    -------
46    scalar or tuple
47
48    """
49    if x.__class__ in native_types:
50        return x
51    elif x.__class__ in sequence_types:
52        # Note that casting a tuple to a tuple is cheap (no copy, no
53        # new object)
54        x = tuple(x)
55    else:
56        x = (x,)
57
58    x_len = len(x)
59    i = 0
60    while i < x_len:
61        _xi_class = x[i].__class__
62        if _xi_class in native_types:
63            i += 1
64        elif _xi_class in sequence_types:
65            x_len += len(x[i]) - 1
66            # Note that casting a tuple to a tuple is cheap (no copy, no
67            # new object)
68            x = x[:i] + tuple(x[i]) + x[i + 1:]
69        elif issubclass(_xi_class, Sequence):
70            if issubclass(_xi_class, str):
71                # This is very difficult to get to: it would require a
72                # user creating a custom derived string type
73                native_types.add(_xi_class)
74                i += 1
75            else:
76                sequence_types.add(_xi_class)
77                x_len += len(x[i]) - 1
78                x = x[:i] + tuple(x[i]) + x[i + 1:]
79        else:
80            i += 1
81
82    if x_len == 1:
83        return x[0]
84    return x
85
86# Pyomo will normalize indices by default
87normalize_index.flatten = True
88
89
90class _NotFound(object):
91    pass
92class _NotSpecified(object):
93    pass
94
95#
96# Get the fully-qualified name for this index.  If there isn't anything
97# in the _data dict (and there shouldn't be), then add something, get
98# the name, and remove it.  This allows us to get the name of something
99# that we haven't added yet without changing the state of the constraint
100# object.
101#
102def _get_indexed_component_data_name(component, index):
103    """Returns the fully-qualified component name for an unconstructed index.
104
105    The ComponentData.name property assumes that the ComponentData has
106    already been assigned to the owning Component.  This is a problem
107    during the process of constructing a ComponentData instance, as we
108    may need to throw an exception before the ComponentData is added to
109    the owning component.  In those cases, we can use this function to
110    generate the fully-qualified name without (permanently) adding the
111    object to the Component.
112
113    """
114    if not component.is_indexed():
115        return component.name
116    elif index in component._data:
117        ans = component._data[index].name
118    else:
119        for i in range(5):
120            try:
121                component._data[index] = component._ComponentDataClass(
122                    *((None,)*i), component=component)
123                i = None
124                break
125            except:
126                pass
127        if i is not None:
128            # None of the generic positional arguments worked; raise an
129            # exception
130            component._data[index] = component._ComponentDataClass(
131                component=component)
132        try:
133            ans = component._data[index].name
134        except:
135            ans = component.name + '[{unknown index}]'
136        finally:
137            del component._data[index]
138    return ans
139
140_rule_returned_none_error = """%s '%s': rule returned None.
141
142%s rules must return either a valid expression, numeric value, or
143%s.Skip.  The most common cause of this error is forgetting to
144include the "return" statement at the end of your rule.
145"""
146
147def rule_result_substituter(result_map):
148    _map = result_map
149    _map_types = set(type(key) for key in result_map)
150
151    def rule_result_substituter_impl(rule, *args, **kwargs):
152        if rule.__class__ in _map_types:
153            #
154            # The argument is a trivial type and will be mapped
155            #
156            value = rule
157        else:
158            #
159            # Otherwise, the argument is a functor, so call it to
160            # generate the rule result.
161            #
162            value = rule( *args, **kwargs )
163        #
164        # Map the returned value:
165        #
166        if value.__class__ in _map_types and value in _map:
167            return _map[value]
168        return value
169
170    return rule_result_substituter_impl
171
172_map_rule_funcdef = \
173"""def wrapper_function%s:
174    args, varargs, kwds, local_env = inspect.getargvalues(
175        inspect.currentframe())
176    args = tuple(local_env[_] for _ in args) + (varargs or ())
177    return wrapping_fcn(rule, *args, **(kwds or {}))
178"""
179
180def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None):
181    """Wrap a rule with another function
182
183    This utility method provides a way to wrap a function (rule) with
184    another function while preserving the original function signature.
185    This is important for rules, as the :py:func:`Initializer`
186    argument processor relies on knowing the number of positional
187    arguments.
188
189    Parameters
190    ----------
191    rule: function
192        The original rule being wrapped
193    wrapping_fcn: function or Dict
194        The wrapping function.  The `wrapping_fcn` will be called with
195        ``(rule, *args, **kwargs)``.  For convenience, if a `dict` is
196        passed as the `wrapping_fcn`, then the result of
197        :py:func:`rule_result_substituter(wrapping_fcn)` is used as the
198        wrapping function.
199    positional_arg_map: iterable[int]
200        An iterable of indices of rule positional arguments to expose in
201        the wrapped function signature.  For example,
202        `positional_arg_map=(2, 0)` and `rule=fcn(a, b, c)` would produce a
203        wrapped function with a signature `wrapper_function(c, a)`
204
205    """
206    if isinstance(wrapping_fcn, dict):
207        wrapping_fcn = rule_result_substituter(wrapping_fcn)
208        if not inspect.isfunction(rule):
209            return wrapping_fcn(rule)
210    # Because some of our processing of initializer functions relies on
211    # knowing the number of positional arguments, we will go to extra
212    # effort here to preserve the original function signature.
213    rule_sig = inspect.signature(rule)
214    if positional_arg_map is not None:
215        param = list(rule_sig.parameters.values())
216        rule_sig = rule_sig.replace(
217            parameters=(param[i] for i in positional_arg_map))
218    _funcdef = _map_rule_funcdef % (str(rule_sig),)
219    # Create the wrapper in a temporary environment that mimics this
220    # function's environment.
221    _env = dict(globals())
222    _env.update(locals())
223    exec(_funcdef, _env)
224    return _env['wrapper_function']
225
226
227class IndexedComponent(Component):
228    """
229    This is the base class for all indexed modeling components.
230    This class stores a dictionary, self._data, that maps indices
231    to component data objects.  The object self._index defines valid
232    keys for this dictionary, and the dictionary keys may be a
233    strict subset.
234
235    The standard access and iteration methods iterate over the the
236    keys of self._data.  This class supports a concept of a default
237    component data value.  When enabled, the default does not
238    change the access and iteration methods.
239
240    IndexedComponent may be given a set over which indexing is restricted.
241    Alternatively, IndexedComponent may be indexed over Any
242    (pyomo.core.base.set_types.Any), in which case the IndexedComponent
243    behaves like a dictionary - any hashable object can be used as a key
244    and keys can be added on the fly.
245
246    Constructor arguments:
247        ctype       The class type for the derived subclass
248        doc         A text string describing this component
249
250    Private class attributes:
251        _data               A dictionary from the index set to
252                                component data objects
253        _index              The set of valid indices
254        _implicit_subsets   A temporary data element that stores
255                                sets that are transfered to the model
256    """
257
258    class Skip(object): pass
259
260    #
261    # If an index is supplied for which there is not a _data entry
262    # (specifically, in a get call), then this flag determines whether
263    # a check is performed to see if the input index is in the
264    # index set _index. This is extremely expensive, and so this flag
265    # is provided to disable that feature globally.
266    #
267    _DEFAULT_INDEX_CHECKING_ENABLED = True
268
269    def __init__(self, *args, **kwds):
270        from pyomo.core.base.set import process_setarg
271        #
272        kwds.pop('noruleinit', None)
273        Component.__init__(self, **kwds)
274        #
275        self._data = {}
276        #
277        if len(args) == 0 or (len(args) == 1 and
278                              args[0] is UnindexedComponent_set):
279            #
280            # If no indexing sets are provided, generate a dummy index
281            #
282            self._implicit_subsets = None
283            self._index = UnindexedComponent_set
284        elif len(args) == 1:
285            #
286            # If a single indexing set is provided, just process it.
287            #
288            self._implicit_subsets = None
289            self._index = process_setarg(args[0])
290        else:
291            #
292            # If multiple indexing sets are provided, process them all,
293            # and store the cross-product of these sets.  The individual
294            # sets need to stored in the Pyomo model, so the
295            # _implicit_subsets class data is used for this temporary
296            # storage.
297            #
298            # Example:  Pyomo allows things like
299            # "Param([1,2,3], range(100), initialize=0)".  This
300            # needs to create *3* sets: two SetOf components and then
301            # the SetProduct.  That means that the component needs to
302            # hold on to the implicit SetOf objects until the component
303            # is assigned to a model (where the implicit subsets can be
304            # "transferred" to the model).
305            #
306            tmp = [process_setarg(x) for x in args]
307            self._implicit_subsets = tmp
308            self._index = tmp[0].cross(*tmp[1:])
309
310    def __getstate__(self):
311        # Special processing of getstate so that we never copy the
312        # UnindexedComponent_set set
313        state = super(IndexedComponent, self).__getstate__()
314        if not self.is_indexed():
315            state['_index'] = None
316        return state
317
318    def __setstate__(self, state):
319        # Special processing of setstate so that we never copy the
320        # UnindexedComponent_set set
321        if state['_index'] is None:
322            state['_index'] = UnindexedComponent_set
323        super(IndexedComponent, self).__setstate__(state)
324
325    def to_dense_data(self):
326        """TODO"""
327        for idx in self._index:
328            if idx in self._data:
329                continue
330            try:
331                self._getitem_when_not_present(idx)
332            except KeyError:
333                # Rule could have returned Skip, which we will silently ignore
334                pass
335
336    def clear(self):
337        """Clear the data in this component"""
338        if self.is_indexed():
339            self._data = {}
340        else:
341            raise DeveloperError(
342                "Derived scalar component %s failed to define clear()."
343                % (self.__class__.__name__,))
344
345    def index_set(self):
346        """Return the index set"""
347        return self._index
348
349    def is_indexed(self):
350        """Return true if this component is indexed"""
351        return self._index is not UnindexedComponent_set
352
353    def is_reference(self):
354        """Return True if this component is a reference, where
355        "reference" is interpreted as any component that does not
356        own its own data.
357        """
358        return self._data is not None and type(self._data) is not dict
359
360    def dim(self):
361        """Return the dimension of the index"""
362        if not self.is_indexed():
363            return 0
364        return self._index.dimen
365
366    def __len__(self):
367        """
368        Return the number of component data objects stored by this
369        component.
370        """
371        return len(self._data)
372
373    def __contains__(self, idx):
374        """Return true if the index is in the dictionary"""
375        return idx in self._data
376
377    # The default implementation is for keys() and __iter__ to be
378    # synonyms.  The logic is implemented in keys() so that
379    # keys/values/items continue to work for components that implement
380    # other definitions for __iter__ (e.g., Set)
381    def __iter__(self):
382        """Return an iterator of the keys in the dictionary"""
383        return self.keys()
384
385    def keys(self):
386        """Iterate over the keys in the dictionary"""
387
388        if hasattr(self._index, 'isfinite') and not self._index.isfinite():
389            #
390            # If the index set is virtual (e.g., Any) then return the
391            # data iterator.  Note that since we cannot check the length
392            # of the underlying Set, there should be no warning if the
393            # user iterates over the set when the _data dict is empty.
394            #
395            return self._data.__iter__()
396        elif self.is_reference():
397            return self._data.__iter__()
398        elif len(self._data) == len(self._index):
399            #
400            # If the data is dense then return the index iterator.
401            #
402            return self._index.__iter__()
403        else:
404            if not self._data and self._index and PyomoOptions.paranoia_level:
405                logger.warning(
406"""Iterating over a Component (%s)
407defined by a non-empty concrete set before any data objects have
408actually been added to the Component.  The iterator will be empty.
409This is usually caused by Concrete models where you declare the
410component (e.g., a Var) and apply component-level operations (e.g.,
411x.fix(0)) before you use the component members (in something like a
412constraint).
413
414You can silence this warning by one of three ways:
415    1) Declare the component to be dense with the 'dense=True' option.
416       This will cause all data objects to be immediately created and
417       added to the Component.
418    2) Defer component-level iteration until after the component data
419       members have been added (through explicit use).
420    3) If you intend to iterate over a component that may be empty, test
421       if the component is empty first and avoid iteration in the case
422       where it is empty.
423""" % (self.name,) )
424
425            if not hasattr(self._index, 'isordered') or not self._index.isordered():
426                #
427                # If the index set is not ordered, then return the
428                # data iterator.  This is in an arbitrary order, which is
429                # fine because the data is unordered.
430                #
431                return self._data.__iter__()
432            else:
433                #
434                # Test each element of a sparse data with an ordered
435                # index set in order.  This is potentially *slow*: if
436                # the component is in fact very sparse, we could be
437                # iterating over a huge (dense) index in order to sort a
438                # small number of indices.  However, this provides a
439                # consistent ordering that the user expects.
440                #
441                def _sparse_iter_gen(self):
442                    for idx in self._index.__iter__():
443                        if idx in self._data:
444                            yield idx
445                return _sparse_iter_gen(self)
446
447    def values(self):
448        """Return an iterator of the component data objects in the dictionary"""
449        return (self[s] for s in self.keys())
450
451    def items(self):
452        """Return an iterator of (index,data) tuples from the dictionary"""
453        return((s, self[s]) for s in self.keys())
454
455    @deprecated('The iterkeys method is deprecated. Use dict.keys().',
456                version='6.0')
457    def iterkeys(self):
458        """Return a list of keys in the dictionary"""
459        return self.keys()
460
461    @deprecated('The itervalues method is deprecated. Use dict.values().',
462                version='6.0')
463    def itervalues(self):
464        """Return a list of the component data objects in the dictionary"""
465        return self.values()
466
467    @deprecated('The iteritems method is deprecated. Use dict.items().',
468                version='6.0')
469    def iteritems(self):
470        """Return a list (index,data) tuples from the dictionary"""
471        return self.items()
472
473    def __getitem__(self, index):
474        """
475        This method returns the data corresponding to the given index.
476        """
477        if self._constructed is False:
478            self._not_constructed_error(index)
479
480        try:
481            obj = self._data.get(index, _NotFound)
482        except TypeError:
483            try:
484                index = self._processUnhashableIndex(index)
485            except TypeError:
486                # This index is really unhashable.  Set a flag so that
487                # we can re-raise the original exception (not this one)
488                index = TypeError
489            if index is TypeError:
490                raise
491            if index.__class__ is IndexedComponent_slice:
492                return index
493            # The index could have contained constant but nonhashable
494            # objects (e.g., scalar immutable Params).
495            # _processUnhashableIndex will evaluate those constants, so
496            # if it made any changes to the index, we need to re-check
497            # the _data dict for membership.
498            try:
499                obj = self._data.get(index, _NotFound)
500            except TypeError:
501                obj = _NotFound
502
503        if obj is _NotFound:
504            # Not good: we have to defer this import to now
505            # due to circular imports (expr imports _VarData
506            # imports indexed_component, but we need expr
507            # here
508            from pyomo.core.expr import current as EXPR
509            if index.__class__ is EXPR.GetItemExpression:
510                return index
511            validated_index = self._validate_index(index)
512            if validated_index is not index:
513                index = validated_index
514                # _processUnhashableIndex could have found a slice, or
515                # _validate could have found an Ellipsis and returned a
516                # slicer
517                if index.__class__ is IndexedComponent_slice:
518                    return index
519                obj = self._data.get(index, _NotFound)
520            #
521            # Call the _getitem_when_not_present helper to retrieve/return
522            # the default value
523            #
524            if obj is _NotFound:
525                return self._getitem_when_not_present(index)
526
527        return obj
528
529    def __setitem__(self, index, val):
530        #
531        # Set the value: This relies on _setitem_when_not_present() to
532        # insert the correct ComponentData into the _data dictionary
533        # when it is not present and _setitem_impl to update an existing
534        # entry.
535        #
536        # Note: it is important that we check _constructed is False and not
537        # just evaluates to false: when constructing immutable Params,
538        # _constructed will be None during the construction process when
539        # setting the value is valid.
540        #
541        if self._constructed is False:
542            self._not_constructed_error(index)
543
544        try:
545            obj = self._data.get(index, _NotFound)
546        except TypeError:
547            obj = _NotFound
548            index = self._processUnhashableIndex(index)
549
550        if obj is _NotFound:
551            # If we didn't find the index in the data, then we need to
552            # validate it against the underlying set (as long as
553            # _processUnhashableIndex didn't return a slicer)
554            if index.__class__ is not IndexedComponent_slice:
555                index = self._validate_index(index)
556        else:
557            return self._setitem_impl(index, obj, val)
558        #
559        # Call the _setitem_impl helper to populate the _data
560        # dictionary and set the value
561        #
562        # Note that we need to RECHECK the class against
563        # IndexedComponent_slice, as _validate_index could have found
564        # an Ellipsis (which is hashable) and returned a slicer
565        #
566        if index.__class__ is IndexedComponent_slice:
567            # support "m.x[:,1] = 5" through a simple recursive call.
568            #
569            # Assert that this slice was just generated
570            assert len(index._call_stack) == 1
571            #
572            # Note that the slicer will only slice over *existing*
573            # entries, but they may not be in the data dictionary.  Make
574            # a copy of the slicer items *before* we start iterating
575            # over it in case the setter changes the _data dictionary.
576            for idx, obj in list(index.expanded_items()):
577                self._setitem_impl(idx, obj, val)
578        else:
579            obj = self._data.get(index, _NotFound)
580            if obj is _NotFound:
581                return self._setitem_when_not_present(index, val)
582            else:
583                return self._setitem_impl(index, obj, val)
584
585    def __delitem__(self, index):
586        if self._constructed is False:
587            self._not_constructed_error(index)
588
589        try:
590            obj = self._data.get(index, _NotFound)
591        except TypeError:
592            obj = _NotFound
593            index = self._processUnhashableIndex(index)
594
595        if obj is _NotFound:
596            if index.__class__ is not IndexedComponent_slice:
597                index = self._validate_index(index)
598
599        # this supports "del m.x[:,1]" through a simple recursive call
600        if index.__class__ is IndexedComponent_slice:
601            # Assert that this slice ws just generated
602            assert len(index._call_stack) == 1
603            # Make a copy of the slicer items *before* we start
604            # iterating over it (since we will be removing items!).
605            for idx in list(index.expanded_keys()):
606                del self[idx]
607        else:
608            # Handle the normal deletion operation
609            if self.is_indexed():
610                # Remove reference to this object
611                self._data[index]._component = None
612            del self._data[index]
613
614    def _pop_from_kwargs(self, name, kwargs, namelist, notset=None):
615        args = [arg for arg in (kwargs.pop(name, notset) for name in namelist)
616                if arg is not notset]
617        if len(args) == 1:
618            return args[0]
619        elif not args:
620            return notset
621        else:
622            argnames = "%s%s '%s='" % (
623                ', '.join("'%s='" % _ for _ in namelist[:-1]),
624                ',' if len(namelist) > 2 else '',
625                namelist[-1]
626            )
627            raise ValueError(
628                "Duplicate initialization: %s() only accepts one of %s" %
629                (name, argnames))
630
631    def _construct_from_rule_using_setitem(self):
632        if self._rule is None:
633            return
634        index = None
635        rule = self._rule
636        block = self.parent_block()
637        try:
638            if rule.constant() and self.is_indexed():
639                # A constant rule could return a dict-like thing or
640                # matrix that we would then want to process with
641                # Initializer().  If the rule actually returned a
642                # constant, then this is just a little overhead.
643                self._rule = rule = Initializer(
644                    rule(block, None),
645                    treat_sequences_as_mappings=False,
646                    arg_not_specified=NOTSET
647                )
648
649            if rule.contains_indices():
650                # The index is coming in externally; we need to validate it
651                for index in rule.indices():
652                    self[index] = rule(block, index)
653            elif not self.index_set().isfinite():
654                # If the index is not finite, then we cannot iterate
655                # over it.  Since the rule doesn't provide explicit
656                # indices, then there is nothing we can do (the
657                # assumption is that the user will trigger specific
658                # indices to be created at a later time).
659                pass
660            elif rule.constant():
661                # Slight optimization: if the initializer is known to be
662                # constant, then only call the rule once.
663                val = rule(block, None)
664                for index in self.index_set():
665                    self._setitem_when_not_present(index, val)
666            else:
667                for index in self.index_set():
668                    self._setitem_when_not_present(index, rule(block, index))
669        except:
670            err = sys.exc_info()[1]
671            logger.error(
672                "Rule failed for %s '%s' with index %s:\n%s: %s"
673                % (self.ctype.__name__,
674                   self.name,
675                   str(index),
676                   type(err).__name__,
677                   err))
678            raise
679
680    def _not_constructed_error(self, idx):
681        # Generate an error because the component is not constructed
682        if not self.is_indexed():
683            idx_str = ''
684        elif idx.__class__ is tuple:
685            idx_str = "[" + ",".join(str(i) for i in idx) + "]"
686        else:
687            idx_str = "[" + str(idx) + "]"
688        raise ValueError(
689            "Error retrieving component %s%s: The component has "
690            "not been constructed." % (self.name, idx_str,))
691
692    def _validate_index(self, idx):
693        if not IndexedComponent._DEFAULT_INDEX_CHECKING_ENABLED:
694            # Return whatever index was provided if the global flag dictates
695            # that we should bypass all index checking and domain validation
696            return idx
697
698        # This is only called through __{get,set,del}item__, which has
699        # already trapped unhashable objects.
700        validated_idx = self._index.get(idx, _NotFound)
701        if validated_idx is not _NotFound:
702            # If the index is in the underlying index set, then return it
703            #  Note: This check is potentially expensive (e.g., when the
704            # indexing set is a complex set operation)!
705            return validated_idx
706
707        if idx.__class__ is IndexedComponent_slice:
708            return idx
709
710        if normalize_index.flatten:
711            # Now we normalize the index and check again.  Usually,
712            # indices will be already be normalized, so we defer the
713            # "automatic" call to normalize_index until now for the
714            # sake of efficiency.
715            normalized_idx = normalize_index(idx)
716            if normalized_idx is not idx:
717                idx = normalized_idx
718                if idx in self._data:
719                    return idx
720                if idx in self._index:
721                    return idx
722        # There is the chance that the index contains an Ellipsis,
723        # so we should generate a slicer
724        if idx is Ellipsis or idx.__class__ is tuple and Ellipsis in idx:
725            return self._processUnhashableIndex(idx)
726        #
727        # Generate different errors, depending on the state of the index.
728        #
729        if not self.is_indexed():
730            raise KeyError(
731                "Cannot treat the scalar component '%s' "
732                "as an indexed component" % ( self.name, ))
733        #
734        # Raise an exception
735        #
736        raise KeyError(
737            "Index '%s' is not valid for indexed component '%s'"
738            % ( idx, self.name, ))
739
740    def _processUnhashableIndex(self, idx):
741        """Process a call to __getitem__ with unhashable elements
742
743        There are three basic ways to get here:
744          1) the index contains one or more slices or ellipsis
745          2) the index contains an unhashable type (e.g., a Pyomo
746             (Simple)Component
747          3) the index contains an IndexTemplate
748        """
749        from pyomo.core.expr import current as EXPR
750        #
751        # Iterate through the index and look for slices and constant
752        # components
753        #
754        fixed = {}
755        sliced = {}
756        ellipsis = None
757        _found_numeric = False
758        #
759        # Setup the slice template (in fixed)
760        #
761        if normalize_index.flatten:
762            idx = normalize_index(idx)
763        if idx.__class__ is not tuple:
764            idx = (idx,)
765
766        for i,val in enumerate(idx):
767            if type(val) is slice:
768                if val.start is not None or val.stop is not None:
769                    raise IndexError(
770                        "Indexed components can only be indexed with simple "
771                        "slices: start and stop values are not allowed.")
772                if val.step is not None:
773                    deprecation_warning(
774                        "The special wildcard slice (::0) is deprecated.  "
775                        "Please use an ellipsis (...) to indicate "
776                        "'0 or more' indices", version='4.4')
777                    val = Ellipsis
778                else:
779                    if ellipsis is None:
780                        sliced[i] = val
781                    else:
782                        sliced[i-len(idx)] = val
783                    continue
784
785            if val is Ellipsis:
786                if ellipsis is not None:
787                    raise IndexError(
788                        "Indexed components can only be indexed with simple "
789                        "slices: the Pyomo wildcard slice (Ellipsis; "
790                        "e.g., '...') can only appear once")
791                ellipsis = i
792                continue
793
794            if hasattr(val, 'is_expression_type'):
795                _num_val = val
796                # Attempt to retrieve the numeric value .. if this
797                # is a template expression generation, then it
798                # should raise a TemplateExpressionError
799                try:
800                    val = EXPR.evaluate_expression(val, constant=True)
801                    _found_numeric = True
802
803                except TemplateExpressionError:
804                    #
805                    # The index is a template expression, so return the
806                    # templatized expression.
807                    #
808                    from pyomo.core.expr import current as EXPR
809                    return EXPR.GetItemExpression((self,) + tuple(idx))
810
811                except EXPR.NonConstantExpressionError:
812                    #
813                    # The expression contains an unfixed variable
814                    #
815                    raise RuntimeError(
816"""Error retrieving the value of an indexed item %s:
817index %s is not a constant value.  This is likely not what you meant to
818do, as if you later change the fixed value of the object this lookup
819will not change.  If you understand the implications of using
820non-constant values, you can get the current value of the object using
821the value() function.""" % ( self.name, i ))
822
823                except EXPR.FixedExpressionError:
824                    #
825                    # The expression contains a fixed variable
826                    #
827                    raise RuntimeError(
828"""Error retrieving the value of an indexed item %s:
829index %s is a fixed but not constant value.  This is likely not what you
830meant to do, as if you later change the fixed value of the object this
831lookup will not change.  If you understand the implications of using
832fixed but not constant values, you can get the current value using the
833value() function.""" % ( self.name, i ))
834                #
835                # There are other ways we could get an exception such as
836                # evaluating a Param / Var that is not initialized.
837                # These exceptions will continue up the call stack.
838                #
839
840            # verify that the value is hashable
841            hash(val)
842            if ellipsis is None:
843                fixed[i] = val
844            else:
845                fixed[i - len(idx)] = val
846
847        if sliced or ellipsis is not None:
848            slice_dim = len(idx)
849            if ellipsis is not None:
850                slice_dim -= 1
851            if normalize_index.flatten:
852                set_dim = self.dim()
853            elif self._implicit_subsets is None:
854                # Scalar component.
855                set_dim = 0
856            else:
857                set_dim = len(self._implicit_subsets)
858
859            structurally_valid = False
860            if slice_dim == set_dim or set_dim is None:
861                structurally_valid = True
862            elif ellipsis is not None and slice_dim < set_dim:
863                structurally_valid = True
864            elif set_dim == 0 and idx == (slice(None),):
865                # If dim == 0 and idx is slice(None), the component was
866                # a scalar passed a single slice. Since scalar components
867                # can be accessed with a "1-dimensional" index of None,
868                # this behavior is allowed.
869                #
870                # Note that x[...] is caught above, as slice_dim will be
871                # 0 in that case
872                structurally_valid = True
873
874            if not structurally_valid:
875                raise IndexError(
876                    "Index %s contains an invalid number of entries for "
877                    "component %s. Expected %s, got %s."
878                    % (idx, self.name, set_dim, slice_dim))
879            return IndexedComponent_slice(self, fixed, sliced, ellipsis)
880        elif _found_numeric:
881            if len(idx) == 1:
882                return fixed[0]
883            else:
884                return tuple( fixed[i] for i in range(len(idx)) )
885        else:
886            raise DeveloperError(
887                "Unknown problem encountered when trying to retrieve "
888                "index for component %s" % (self.name,) )
889
890    def _getitem_when_not_present(self, index):
891        """Returns/initializes a value when the index is not in the _data dict.
892
893        Override this method if the component allows implicit member
894        construction.  For classes that do not support a 'default' (at
895        this point, everything except Param and Var), requesting
896        _getitem_when_not_present will generate a KeyError (just like a
897        normal dict).
898
899        Implementations may assume that the index has already been validated
900        and is a legitimate entry in the _data dict.
901
902        """
903        raise KeyError(index)
904
905    def _setitem_impl(self, index, obj, value):
906        """Perform the fundamental object value storage
907
908        Components that want to implement a nonstandard storage mechanism
909        should override this method.
910
911        Implementations may assume that the index has already been
912        validated and is a legitimate pre-existing entry in the _data
913        dict.
914
915        """
916        if value is IndexedComponent.Skip:
917            del self[index]
918            return None
919        else:
920            obj.set_value(value)
921        return obj
922
923    def _setitem_when_not_present(self, index, value=_NotSpecified):
924        """Perform the fundamental component item creation and storage.
925
926        Components that want to implement a nonstandard storage mechanism
927        should override this method.
928
929        Implementations may assume that the index has already been
930        validated and is a legitimate entry in the _data dict.
931        """
932        # If the value is "Skip" do not add anything
933        if value is IndexedComponent.Skip:
934            return None
935        #
936        # If we are a scalar, then idx will be None (_validate_index ensures
937        # this)
938        if index is None and not self.is_indexed():
939            obj = self._data[index] = self
940        else:
941            obj = self._data[index] = self._ComponentDataClass(component=self)
942        try:
943            if value is not _NotSpecified:
944                obj.set_value(value)
945        except:
946            self._data.pop(index, None)
947            raise
948        return obj
949
950    def set_value(self, value):
951        """Set the value of a scalar component."""
952        if self.is_indexed():
953            raise ValueError(
954                "Cannot set the value for the indexed component '%s' "
955                "without specifying an index value.\n"
956                "\tFor example, model.%s[i] = value"
957                % (self.name, self.name))
958        else:
959            raise DeveloperError(
960                "Derived component %s failed to define set_value() "
961                "for scalar instances."
962                % (self.__class__.__name__,))
963
964    def _pprint(self):
965        """Print component information."""
966        return ( [("Size", len(self)),
967                  ("Index", self._index if self.is_indexed() else None),
968                  ],
969                 self._data.items(),
970                 ( "Object",),
971                 lambda k, v: [ type(v) ]
972                 )
973
974    def id_index_map(self):
975        """
976        Return an dictionary id->index for
977        all ComponentData instances.
978        """
979        result = {}
980        for index, component_data in self.items():
981            result[id(component_data)] = index
982        return result
983
984
985class ActiveIndexedComponent(IndexedComponent, ActiveComponent):
986    """
987    This is the base class for all indexed modeling components
988    whose data members are subclasses of ActiveComponentData, e.g.,
989    can be activated or deactivated.
990
991    The activate and deactivate methods activate both the
992    component as well as all component data values.
993    """
994
995    def __init__(self, *args, **kwds):
996        IndexedComponent.__init__(self, *args, **kwds)
997        # Replicate the ActiveComponent.__init__() here.  We don't want
998        # to use super, because that will run afoul of certain
999        # assumptions for derived SimpleComponents' __init__()
1000        #
1001        # FIXME: eliminate multiple inheritance of SimpleComponents
1002        self._active = True
1003
1004    def activate(self):
1005        """Set the active attribute to True"""
1006        super(ActiveIndexedComponent, self).activate()
1007        if self.is_indexed():
1008            for component_data in self.values():
1009                component_data.activate()
1010
1011    def deactivate(self):
1012        """Set the active attribute to False"""
1013        super(ActiveIndexedComponent, self).deactivate()
1014        if self.is_indexed():
1015            for component_data in self.values():
1016                component_data.deactivate()
1017
1018
1019# Ideally, this would inherit from np.lib.mixins.NDArrayOperatorsMixin,
1020# but doing so overrides things like __contains__ in addition to the
1021# operators that we are interested in.
1022class IndexedComponent_NDArrayMixin(object):
1023    """Support using IndexedComponent with numpy.ndarray
1024
1025    This IndexedComponent mixin class adds support for implicitly using
1026    the IndexedComponent as a term in an expression with numpy ndarray
1027    objects.
1028
1029    """
1030
1031    def __array__(self, dtype=None):
1032        if not self.is_indexed():
1033            ans = NumericNDArray(shape=(1,), dtype=object)
1034            ans[0] = self
1035            return ans
1036
1037        _dim = self.dim()
1038        if _dim is None:
1039            raise TypeError(
1040                "Cannot convert a non-dimensioned Pyomo IndexedComponent "
1041                "(%s) into a numpy array" % (self,))
1042        bounds = self.index_set().bounds()
1043        if not isinstance(bounds[0], Sequence):
1044            bounds = ((bounds[0],), (bounds[1],))
1045        if any(b != 0 for b in bounds[0]):
1046            raise TypeError(
1047                "Cannot convert a Pyomo IndexedComponent "
1048                "(%s) with bounds [%s, %s] into a numpy array" % (
1049                    self, bounds[0], bounds[1]))
1050        shape = tuple(b+1 for b in bounds[1])
1051        ans = NumericNDArray(shape=shape, dtype=object)
1052        for k, v in self.items():
1053            ans[k] = v
1054        return ans
1055
1056    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
1057        return NumericNDArray.__array_ufunc__(
1058            None, ufunc, method, *inputs, **kwargs)
1059