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