Source code for pyomo.core.base.set

#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2024
#  National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

from __future__ import annotations
import inspect
import itertools
import logging
import math
import sys
import weakref

from collections.abc import Iterator
from functools import partial
from typing import Union, Type, Any as typingAny

from pyomo.common.autoslots import AutoSlots
from pyomo.common.collections import ComponentSet
from pyomo.common.deprecation import deprecated, deprecation_warning, RenamedClass
from pyomo.common.errors import DeveloperError, PyomoException
from pyomo.common.log import is_debug_set
from pyomo.common.modeling import NOTSET
from pyomo.common.pyomo_typing import overload
from pyomo.common.sorting import sorted_robust
from pyomo.common.timing import ConstructionTimer

from pyomo.core.expr.numvalue import (
    native_types,
    native_numeric_types,
    as_numeric,
    value,
    is_constant,
)
from pyomo.core.base.disable_methods import disable_methods
from pyomo.core.base.initializer import (
    CountedCallInitializer,
    IndexedCallInitializer,
    Initializer,
    InitializerBase,
    ParameterizedIndexedCallInitializer,
    ParameterizedInitializer,
    ParameterizedScalarCallInitializer,
)
from pyomo.core.base.range import (
    NumericRange,
    NonNumericRange,
    AnyRange,
    RangeProduct,
    RangeDifferenceError,
)
from pyomo.core.base.component import (
    ComponentBase,
    Component,
    ComponentData,
    ModelComponentFactory,
)
from pyomo.core.base.indexed_component import (
    IndexedComponent,
    UnindexedComponent_set,
    normalize_index,
    rule_wrapper,
)
from pyomo.core.base.global_set import (
    GlobalSets,
    GlobalSetBase,
    UnindexedComponent_index,
)

from collections.abc import Sequence
from operator import itemgetter

logger = logging.getLogger('pyomo.core')

_inf = float('inf')

FLATTEN_CROSS_PRODUCT = True

"""Set objects

Pyomo `Set` objects are designed to be "API-compatible" with Python
`set` objects.  However, not all Set objects implement the full `set`
API (e.g., only finite discrete Sets support `add()`).

All Sets implement one of the following APIs:

1. `class SetData(ComponentData)`
   *(base class for all AML Sets)*

2. `class _FiniteSetMixin(object)`
   *(pure virtual interface, adds support for discrete/iterable sets)*

4. `class _OrderedSetMixin(object)`
   *(pure virtual interface, adds support for ordered Sets)*

This is a bit of a change from python set objects.  First, the
lowest-level (non-abstract) Data object supports infinite sets; that is,
sets that contain an infinite number of values (this includes both
bounded continuous ranges as well as unbounded discrete ranges).  As
there are an infinite number of values, iteration is *not*
supported. The base class also implements all Python set operations.
Note that `SetData` does *not* implement `len()`, as Python requires
`len()` to return a positive integer.

Finite sets add iteration and support for `len()`.  In addition, they
support access to members through three methods: `data()` returns the
members as a tuple (in the internal storage order), and may not be
deterministic.  `ordered_data()` returns the members, and is guaranteed
to be in a deterministic order (in the case of insertion order sets, up
to the determinism of the script that populated the set).  Finally,
`sorted_data()` returns the members in a sorted order (guaranteed
deterministic, up to the implementation of < and ==).

..TODO: should these three members all return generators?  This would
further change the implementation of `data()`, but would allow consumers
to potentially access the members in a more efficient manner.

Ordered sets add support for `ord()` and `__getitem__`, as well as the
`first`, `last`, `next` and `prev` methods for stepping over set
members.

Note that the base APIs are all declared (and to the extent possible,
implemented) through Mixin classes.
"""


[docs] def process_setarg(arg): if isinstance(arg, SetData): if ( getattr(arg, '_parent', None) is not None or getattr(arg, '_anonymous_sets', None) is GlobalSetBase or arg.parent_component()._parent is not None ): return arg, None _anonymous = ComponentSet((arg,)) if getattr(arg, '_anonymous_sets', None) is not None: _anonymous.update(arg._anonymous_sets) return arg, _anonymous elif isinstance(arg, ComponentBase): if isinstance(arg, IndexedComponent) and arg.is_indexed(): raise TypeError( "Cannot apply a Set operator to an " "indexed %s component (%s)" % (arg.ctype.__name__, arg.name) ) if isinstance(arg, Component): raise TypeError( "Cannot apply a Set operator to a non-Set " "%s component (%s)" % (arg.__class__.__name__, arg.name) ) if isinstance(arg, ComponentData): raise TypeError( "Cannot apply a Set operator to a non-Set " "component data (%s)" % (arg.name,) ) # DEPRECATED: This functionality has never been documented, # and I don't know of a use of it in the wild. if hasattr(arg, 'set_options'): deprecation_warning( "The set_options set attribute is deprecated. " "Please explicitly construct complex sets", version='5.7.3', ) # If the argument has a set_options attribute, then use # it to initialize a set args = arg.set_options args.setdefault('initialize', arg) args.setdefault('ordered', type(arg) not in Set._UnorderedInitializers) ans = Set(**args) _init = args['initialize'] if not ( inspect.isgenerator(_init) or inspect.isfunction(_init) or ( isinstance(_init, ComponentData) and not _init.parent_component().is_constructed() ) ): ans.construct() return process_setarg(ans) # TBD: should lists/tuples be copied into Sets, or # should we preserve the reference using SetOf? # Historical behavior is to *copy* into a Set. # # ans.append(Set(initialize=arg, # ordered=type(arg) in {tuple, list})) # ans.construct() # # But this causes problems, especially because Set()'s constructor # needs to know if the object is ordered (Set defaults to ordered, # and will toss a warning if the underlying data source is not # ordered)). While we could add checks where we create the Set # (like here and in the __r*__ operators) and pass in a reasonable # value for ordered, it is starting to make more sense to use SetOf # (which has that logic). Alternatively, we could use SetOf to # create the Set: # _defer_construct = False if not hasattr(arg, '__contains__'): if inspect.isgenerator(arg): _ordered = True _defer_construct = True elif inspect.isfunction(arg): _ordered = True _defer_construct = True else: raise TypeError( "Cannot create a Set from data that does not support " "__contains__. Expected set-like object supporting " "collections.abc.Collection interface, but received '%s'." % (type(arg).__name__,) ) elif arg.__class__ is type: # This catches the (deprecated) RealSet API. return process_setarg(arg()) else: arg = SetOf(arg) _ordered = arg.isordered() ans = Set(initialize=arg, ordered=_ordered) # # Because the resulting set will be attached to the model (at least # for the time being), we will NOT construct it here unless the data # is already determined (either statically provided, or through an # already-constructed component). # if not _defer_construct: ans.construct() # # Or we can do the simple thing and just use SetOf: # # ans = SetOf(arg) _anonymous = ComponentSet((ans,)) if getattr(ans, '_anonymous_sets', None) is not None: _anonymous.update(_anonymous_sets) return ans, _anonymous
[docs] @deprecated( 'The set_options decorator is deprecated; create Sets from ' 'functions explicitly by passing the function to the Set ' 'constructor using the "initialize=" keyword argument.', version='5.7', ) def set_options(**kwds): """ This is a decorator for set initializer functions. This decorator allows an arbitrary dictionary of values to passed through to the set constructor. Examples -------- .. code:: @set_options(dimen=3) def B_index(model): return [(i,i+1,i*i) for i in model.A] @set_options(domain=Integers) def B_index(model): return range(10) """ def decorator(func): func.set_options = kwds return func return decorator
[docs] def simple_set_rule(rule): """ This is a decorator that translates None into Set.End. This supports a simpler syntax in set rules, though these can be more difficult to debug when errors occur. Examples -------- .. code:: @simple_set_rule def A_rule(model, i, j): ... """ return rule_wrapper(rule, {None: Set.End})
[docs] class UnknownSetDimen(object): pass
[docs] class SetInitializer(InitializerBase): """An Initializer wrapper for returning Set objects This initializer wraps another Initializer and converts the return value to a proper Pyomo Set. If the initializer is None, then Any is returned. This initializer can be 'intersected' with another initializer to return the SetIntersect of the Sets returned by the initializers. """ __slots__ = ('_set', 'verified')
[docs] def __init__(self, init, allow_generators=True): self.verified = False if init is None: self._set = None else: self._set = Initializer( init, allow_generators=allow_generators, treat_sequences_as_mappings=False, )
def intersect(self, other): if self._set is None: if type(other) is SetInitializer: self._set = other._set else: self._set = other elif type(other) is SetInitializer: if other._set is not None: self._set = SetIntersectInitializer(self._set, other._set) else: self._set = SetIntersectInitializer(self._set, other) def __call__(self, parent, idx, obj): if self._set is None: return Any _ans, _anonymous = process_setarg(self._set(parent, idx)) if _anonymous: pc = obj.parent_component() if getattr(pc, '_anonymous_sets', None) is None: pc._anonymous_sets = _anonymous else: pc._anonymous_sets.update(_anonymous) for _set in _anonymous: _set._parent = pc._parent if pc._constructed: for _set in _anonymous: _set.construct() return _ans
[docs] def constant(self): return self._set is None or self._set.constant()
[docs] def contains_indices(self): return self._set is not None and self._set.contains_indices()
[docs] def indices(self): if self._set is not None: return self._set.indices() else: super(SetInitializer, self).indices()
def setdefault(self, val): if self._set is None: self._set = Initializer(val)
[docs] class SetIntersectInitializer(InitializerBase): """An Initializer that returns the intersection of two SetInitializers Users will typically not create a SetIntersectInitializer directly. Instead, SetInitializer.intersect() may return a SetInitializer that contains a SetIntersectInitializer instance. """ __slots__ = ('_A', '_B')
[docs] def __init__(self, setA, setB): self._A = setA self._B = setB
def __call__(self, parent, idx): return SetIntersection(self._A(parent, idx), self._B(parent, idx))
[docs] def constant(self): return self._A.constant() and self._B.constant()
[docs] def contains_indices(self): return self._A.contains_indices() or self._B.contains_indices()
[docs] def indices(self): if self._A.contains_indices(): if self._B.contains_indices(): if set(self._A.indices()) != set(self._B.indices()): raise ValueError( "SetIntersectInitializer contains two " "sub-initializers with inconsistent external indices" ) return self._A.indices() else: # It is OK (and desirable) for this to raise the exception # if B does not contain external indices return self._B.indices()
[docs] class BoundsInitializer(InitializerBase): """An Initializer wrapper that converts bounds information to a RangeSet The BoundsInitializer wraps another initializer that is expected to return valid arguments to the RangeSet constructor. Nominally, this would be bounds information in the form of (lower bound, upper bound), but could also be a single scalar or a 3-tuple. Calling this initializer will return a RangeSet object. BoundsInitializer objects can be intersected with other SetInitializer objects using the SetInitializer.intersect() method. """ __slots__ = ('_init', 'default_step')
[docs] def __init__(self, init, default_step=0): self._init = Initializer(init, treat_sequences_as_mappings=False) self.default_step = default_step
def __call__(self, parent, idx): val = self._init(parent, idx) if not isinstance(val, Sequence): val = (1, val, self.default_step) else: val = tuple(val) if len(val) == 2: val += (self.default_step,) elif len(val) == 1: val = (1, val[0], self.default_step) elif len(val) == 0: val = (None, None, self.default_step) ans = RangeSet(*val) # We don't need to construct here, as the RangeSet will # automatically construct itself if it can # ans.construct() return ans
[docs] def constant(self): return self._init.constant()
def setdefault(self, val): # This is a real range set... there is no default to set pass
[docs] class TuplizeError(PyomoException): pass
[docs] class TuplizeValuesInitializer(InitializerBase): """An initializer wrapper that will "tuplize" a sequence This initializer takes the result of another initializer, and if it is a sequence that does not already contain tuples, will convert it to a sequence of tuples, each of length 'dimen' before returning it. """ __slots__ = ('_init', '_dimen') def __new__(cls, *args): if args == (None,): return None else: return super(TuplizeValuesInitializer, cls).__new__(cls)
[docs] def __init__(self, _init): self._init = _init self._dimen = UnknownSetDimen
def __call__(self, parent, index): _val = self._init(parent, index) if self._dimen in {1, None, UnknownSetDimen}: return _val elif _val is Set.Skip: return _val elif _val is None: return _val if not isinstance(_val, Sequence): _val = tuple(_val) if not _val or isinstance(_val[0], tuple): return _val return self._tuplize(_val, parent, index)
[docs] def constant(self): return self._init.constant()
[docs] def contains_indices(self): return self._init.contains_indices()
[docs] def indices(self): return self._init.indices()
def _tuplize(self, _val, parent, index): d = self._dimen if len(_val) % d: raise TuplizeError( "Cannot tuplize list data for set %%s%%s because its " "length %s is not a multiple of dimen=%s" % (len(_val), d) ) return (tuple(_val[i : i + d]) for i in range(0, len(_val), d))
class _NotFound(object): "Internal type flag used to indicate if an object is not found in a set" pass
[docs] class SetData(ComponentData): """The base for all Pyomo objects that can be used as a component indexing set. Derived versions of this class can be used as the Index for any IndexedComponent (including IndexedSet).""" __slots__ = () def __contains__(self, value): try: ans = self.get(value, _NotFound) except TypeError: # In Python 3.x, Sets are unhashable if isinstance(value, SetData): ans = _NotFound else: raise if ans is _NotFound: if isinstance(value, SetData): deprecation_warning( "Testing for set subsets with 'a in b' is deprecated. " "Use 'a.issubset(b)'.", version='5.7', ) return value.issubset(self) else: return False return True def get(self, value, default=None): raise DeveloperError( "Derived set class (%s) failed to " "implement get()" % (type(self).__name__,) )
[docs] def isdiscrete(self): """Returns True if this set admits only discrete members""" return False
[docs] def isfinite(self): """Returns True if this is a finite discrete (iterable) Set""" return False
[docs] def isordered(self): """Returns True if this is an ordered finite discrete (iterable) Set""" return False
def subsets(self, expand_all_set_operators=None): return iter((self,)) def __iter__(self) -> Iterator[typingAny]: """Iterate over the set members Raises AttributeError for non-finite sets. This must be declared for non-finite sets because scalar sets inherit from IndexedComponent, which provides an iterator (over the underlying indexing set). """ raise TypeError( "'%s' object is not iterable (non-finite Set '%s' " "is not iterable)" % (self.__class__.__name__, self.name) ) def __eq__(self, other): if self is other: return True # Special case: non-finite range sets that only contain finite # ranges (or no ranges). We will re-generate non-finite sets to # make sure we get an accurate "finiteness" flag. if hasattr(other, 'isfinite'): if not other.parent_component().is_constructed(): return False other_isfinite = other.isfinite() if not other_isfinite: try: other = RangeSet(ranges=list(other.ranges())) other_isfinite = other.isfinite() except TypeError: pass elif hasattr(other, '__contains__'): # we assume that everything that does not implement # isfinite() is a discrete set. other_isfinite = True try: # For efficiency, if the other is not a Set, we will try # converting it to a Python set() for efficient lookup. other = set(other) except: pass else: return False if not self.isfinite(): try: self = RangeSet(ranges=list(self.ranges())) except TypeError: pass if self.isfinite(): if not other_isfinite: return False if len(self) != len(other): return False for x in self: if x not in other: return False return True elif other_isfinite: return False return self.issubset(other) and other.issubset(self) def __ne__(self, other): return not self.__eq__(other) def __str__(self): raise DeveloperError( "Derived set class (%s) failed to " "implement __str__" % (type(self).__name__,) ) @property def dimen(self): raise DeveloperError( "Derived set class (%s) failed to " "implement dimen" % (type(self).__name__,) ) @property def domain(self): raise DeveloperError( "Derived set class (%s) failed to " "implement domain" % (type(self).__name__,) ) def ranges(self): raise DeveloperError( "Derived set class (%s) failed to " "implement ranges" % (type(self).__name__,) ) def bounds(self): try: _bnds = [ (r.start, r.end) if r.step >= 0 else (r.end, r.start) for r in self.ranges() ] except AttributeError: return None, None if len(_bnds) == 1: lb, ub = _bnds[0] elif not _bnds: return None, None else: lb = min(_bnds, key=itemgetter(0))[0] ub = max(_bnds, key=itemgetter(1))[1] if lb == -_inf: lb = None elif int(lb) == lb: lb = int(lb) if ub == _inf: ub = None elif int(ub) == ub: ub = int(ub) return lb, ub
[docs] def get_interval(self): """Return the interval for this Set as (start, end, step) Returns the effective interval for this Set as a (start, end, step) tuple. Start and End are the same as returned by `bounds()`. Step is 0 for continuous ranges, a positive value for regular discrete sets (e.g., 1 for Integers), or `None` for Sets that do not have a regular interval (e.g., semicontinuous sets, mixed type sets, sets with dimen != 1, etc). """ if self.dimen != 1: return self.bounds() + (None,) if self.isdiscrete(): return self._get_discrete_interval() else: return self._get_continuous_interval()
def _get_discrete_interval(self): # # Note: I'd like to use set() for ranges, since we will be # randomly removing elements from the list; however, since we # do it by enumerating over ranges, using set() would make this # routine nondeterministic. Not a huge issue for the result, # but problemmatic for code coverage. ranges = list(self.ranges()) if len(ranges) == 1: try: start, end, c = ranges[0].normalize_bounds() except AttributeError: # Catching Any, NonNumericRange, etc... return self.bounds() + (None,) return ( None if start == -_inf else start, None if end == _inf else end, abs(ranges[0].step), ) try: step = min(abs(r.step) for r in ranges if r.step != 0) except ValueError: # If all the ranges are single points, we will just # brute-force it: sort the values and ensure that the step # is consistent. Note that we know at this point ranges # only contains NumericRange objects (or at least that they # all have a `step` attribute). vals = sorted(self) if len(vals) < 2: return (vals[0], vals[0], 0) step = vals[1] - vals[0] for i in range(2, len(vals)): if step != vals[i] - vals[i - 1]: return self.bounds() + (None,) return (vals[0], vals[-1], step) except AttributeError: # Catching Any, NonNumericRange, RangeProduct, etc... return self.bounds() + (None,) nRanges = len(ranges) r = ranges.pop() _rlen = len(ranges) ref = r.start if r.step >= 0: start, end = r.start, r.end else: end, start = r.start, r.end if r.step % step: return self.bounds() + (None,) # Catch misaligned ranges for r in ranges: if (r.start - ref) % step: return self.bounds() + (None,) if r.step % step: return self.bounds() + (None,) # This loop terminates when we have a complete pass that doesn't # remove any ranges from the ranges list. while nRanges > _rlen: nRanges = _rlen for i, r in enumerate(ranges): if r.step > 0: rstart, rend = r.start, r.end else: rend, rstart = r.start, r.end if not r.step or abs(r.step) == step: if start <= rend + step and rstart <= end + step: ranges[i] = None if start > rstart: start = rstart if end < rend: end = rend else: # The range has a step bigger than the base # interval we are building. For us to absorb # it, it has to be contained within the current # interval +/- step. if start <= rstart + step and end >= rend - step: ranges[i] = None if start > rstart: start = rstart if end < rend: end = rend ranges = list(_ for _ in ranges if _ is not None) _rlen = len(ranges) if ranges: return self.bounds() + (None,) # Note: while unbounded NumericRanges are -inf..inf, Pyomo # Sets are None..None return (None if start == -_inf else start, None if end == _inf else end, step) def _get_continuous_interval(self): # Note: this method assumes that at least one range is continuous. # # Note: I'd like to use set() for ranges, since we will be # randomly removing elements from the list; however, since we # do it by enumerating over ranges, using set() would make this # routine nondeterministic. Not a hoge issue for the result, # but problemmatic for code coverage. # # Note: We do not need to trap non-NumericRange objects: # RangeProduct and AnyRange will be caught by the dimen test in # get_interval(), and NonNumericRange objects are cleanly # handled as if they were regular discrete ranges. ranges = [] discrete = [] # Pull out the discrete intervals (for checking later), and # copy the continuous ranges (so we can later make them # closed, if applicable) for r in self.ranges(): if r.isdiscrete(): discrete.append(r) else: ranges.append(NumericRange(r.start, r.end, r.step, r.closed)) if len(ranges) == 1 and not discrete: r = ranges[0] return ( None if r.start == -_inf else r.start, None if r.end == _inf else r.end, abs(r.step), ) # There is a particular edge case where we could get 2 disjoint # continuous ranges that are joined by a discrete range... When # we encounter an open range, check to see if the endpoint is # in the discrete set, and if so, convert it to a closed range. for r in ranges: if not r.closed[0]: for d in discrete: if r.start in d: r.closed = (True, r.closed[1]) break if not r.closed[1]: for d in discrete: if r.end in d: r.closed = (r.closed[0], True) break nRanges = len(ranges) r = ranges.pop() interval = NumericRange(r.start, r.end, r.step, r.closed) _rlen = len(ranges) # This loop terminates when we have a complete pass that doesn't # remove any ranges from the ranges list. while _rlen and nRanges > _rlen: nRanges = _rlen for i, r in enumerate(ranges): if interval.isdisjoint(r): continue # r and interval overlap: merge r into interval ranges[i] = None if r.start < interval.start: interval.start = r.start interval.closed = (r.closed[0], interval.closed[1]) elif not interval.closed[0] and r.start == interval.start: interval.closed = (r.closed[0], interval.closed[1]) if r.end > interval.end: interval.end = r.end interval.closed = (interval.closed[0], r.closed[1]) elif not interval.closed[1] and r.end == interval.end: interval.closed = (interval.closed[0], r.closed[1]) ranges = list(_ for _ in ranges if _ is not None) _rlen = len(ranges) if ranges: # The continuous ranges are disjoint return self.bounds() + (None,) for r in discrete: if not r.issubset(interval): # The discrete range extends outside the continuous # interval return self.bounds() + (None,) start = interval.start if start == -_inf: start = None end = interval.end if end == _inf: end = None return (start, end, interval.step) @property @deprecated("The 'virtual' attribute is no longer supported", version='5.7') def virtual(self): return isinstance(self, (_AnySet, SetOperator, InfiniteRangeSetData)) @virtual.setter def virtual(self, value): if value != self.virtual: raise ValueError( "Attempting to set the (deprecated) 'virtual' attribute on %s " "to an invalid value (%s)" % (self.name, value) ) @property @deprecated( "The 'concrete' attribute is no longer supported. " "Use isdiscrete() or isfinite()", version='5.7', ) def concrete(self): return self.isfinite() @concrete.setter def concrete(self, value): if value != self.concrete: raise ValueError( "Attempting to set the (deprecated) 'concrete' attribute on %s " "to an invalid value (%s)" % (self.name, value) ) @property @deprecated( "The 'ordered' attribute is no longer supported. Use isordered()", version='5.7', ) def ordered(self): return self.isordered() @property @deprecated("'filter' is no longer a public attribute.", version='5.7') def filter(self): return None
[docs] @deprecated( "check_values() is deprecated: Sets only contain valid members", version='5.7' ) def check_values(self): """ Verify that the values in this set are valid. """ return True
[docs] def isdisjoint(self, other): """Test if this Set is disjoint from `other` Parameters ---------- other : ``Set`` or ``iterable`` The Set or iterable object to compare this Set against Returns ------- bool : True if this set is disjoint from `other` """ if hasattr(other, 'isfinite'): other_isfinite = other.isfinite() elif hasattr(other, '__contains__'): # we assume that everything that does not implement # isfinite() is a discrete set. other_isfinite = True try: # For efficiency, if the other is not a Set, we will try # converting it to a Python set() for efficient lookup. other = set(other) except: pass else: # Raise an exception consistent with Python's set.isdisjoint() raise TypeError("'%s' object is not iterable" % (type(other).__name__,)) if self.isfinite(): for x in self: if x in other: return False return True elif other_isfinite: for x in other: if x in self: return False return True else: all(r.isdisjoint(s) for r in self.ranges() for s in other.ranges())
[docs] def issubset(self, other): """Test if this Set is a subset of `other` Parameters ---------- other : ``Set`` or ``iterable`` The Set or iterable object to compare this Set against Returns ------- bool : True if this set is a subset of `other` """ # Special case: non-finite range sets that only contain finite # ranges (or no ranges). We will re-generate non-finite sets to # make sure we get an accurate "finiteness" flag. if hasattr(other, 'isfinite'): other_isfinite = other.isfinite() if not other_isfinite: try: other = RangeSet(ranges=list(other.ranges())) other_isfinite = other.isfinite() except TypeError: pass elif hasattr(other, '__contains__'): # we assume that everything that does not implement # isfinite() is a discrete set. other_isfinite = True try: # For efficiency, if the other is not a Set, we will try # converting it to a Python set() for efficient lookup. other = set(other) except: pass else: # Raise an exception consistent with Python's set.issubset() raise TypeError("'%s' object is not iterable" % (type(other).__name__,)) if not self.isfinite(): try: self = RangeSet(ranges=list(self.ranges())) except TypeError: pass if self.isfinite(): for x in self: if x not in other: return False return True elif other_isfinite: return False else: for r in self.ranges(): try: if r.range_difference(other.ranges()): return False except RangeDifferenceError: # This only occurs when subtracting an infinite # discrete set from an infinite continuous set, so r # (and hence self) cannot be a subset of other return False return True
[docs] def issuperset(self, other): """Test if this Set is a superset of `other` Parameters ---------- other : ``Set`` or ``iterable`` The Set or iterable object to compare this Set against Returns ------- bool : True if this set is a superset of `other` """ # Special case: non-finite range sets that only contain finite # ranges (or no ranges). We will re-generate non-finite sets to # make sure we get an accurate "finiteness" flag. if hasattr(other, 'isfinite'): other_isfinite = other.isfinite() if not other_isfinite: try: other = RangeSet(ranges=list(other.ranges())) other_isfinite = other.isfinite() except TypeError: pass elif hasattr(other, '__contains__'): # we assume that everything that does not implement # isfinite() is a discrete set. other_isfinite = True try: # For efficiency, if the other is not a Set, we will try # converting it to a Python set() for efficient lookup. other = set(other) except: pass else: # Raise an exception consistent with Python's set.issuperset() raise TypeError("'%s' object is not iterable" % (type(other).__name__,)) if other_isfinite: for x in other: # Other may contain elements that are not representable # in self. Trap that error (a TypeError due to hashing) # and return False try: if x not in self: return False except TypeError: return False return True if not self.isfinite(): try: self = RangeSet(ranges=list(self.ranges())) except TypeError: pass if self.isfinite(): return False else: return other.issubset(self)
[docs] def union(self, *args): """ Return the union of this set with one or more sets. """ tmp = self for arg in args: tmp = SetUnion(tmp, arg) return tmp
[docs] def intersection(self, *args): """ Return the intersection of this set with one or more sets """ tmp = self for arg in args: tmp = SetIntersection(tmp, arg) return tmp
[docs] def difference(self, *args): """ Return the difference between this set with one or more sets """ tmp = self for arg in args: tmp = SetDifference(tmp, arg) return tmp
[docs] def symmetric_difference(self, other): """ Return the symmetric difference of this set with another set """ return SetSymmetricDifference(self, other)
[docs] def cross(self, *args): """ Return the cross-product between this set and one or more sets """ return SetProduct(self, *args)
# <= is equivalent to issubset # >= is equivalent to issuperset # | is equivalent to union # & is equivalent to intersection # - is equivalent to difference # ^ is equivalent to symmetric_difference # * is equivalent to cross __le__ = issubset __ge__ = issuperset __or__ = union __and__ = intersection __sub__ = difference __xor__ = symmetric_difference __mul__ = cross def __ror__(self, other): # See the discussion of Set vs SetOf in process_setarg above return SetUnion(other, self) def __rand__(self, other): # See the discussion of Set vs SetOf in process_setarg above return SetIntersection(other, self) def __rsub__(self, other): # See the discussion of Set vs SetOf in process_setarg above return SetDifference(other, self) def __rxor__(self, other): # See the discussion of Set vs SetOf in process_setarg above return SetSymmetricDifference(other, self) def __rmul__(self, other): # See the discussion of Set vs SetOf in process_setarg above return SetProduct(other, self) def __lt__(self, other): """ Return True if the set is a strict subset of 'other' """ return self <= other and not self == other def __gt__(self, other): """ Return True if the set is a strict superset of 'other' """ return self >= other and not self == other
class _SetData(metaclass=RenamedClass): __renamed__new_class__ = SetData __renamed__version__ = '6.7.2' class _SetDataBase(metaclass=RenamedClass): __renamed__new_class__ = SetData __renamed__version__ = '6.7.2' class _FiniteSetMixin(object): __slots__ = () def __len__(self): raise DeveloperError( "Derived finite set class (%s) failed to " "implement __len__" % (type(self).__name__,) ) def _iter_impl(self): raise DeveloperError( "Derived finite set class (%s) failed to " "implement _iter_impl" % (type(self).__name__,) ) def __iter__(self): """Iterate over the finite set Note: derived classes should NOT reimplement this method, and should instead overload _iter_impl. The expression template system relies on being able to replace this method for all Sets during template generation. """ return self._iter_impl() def __reversed__(self): return reversed(self.data()) def sorted_iter(self): return iter(sorted_robust(self)) def ordered_iter(self): return self.sorted_iter() def isdiscrete(self): """Returns True if this set admits only discrete members""" return True def isfinite(self): """Returns True if this is a finite discrete (iterable) Set""" return True def data(self): return tuple(self) @property @deprecated( "The 'value' attribute is deprecated. Use .data() to " "retrieve the values in a finite set.", version='5.7', ) def value(self): return set(self) @property @deprecated( "The 'value_list' attribute is deprecated. Use " ".ordered_data() to retrieve the values from a finite set " "in a deterministic order.", version='5.7', ) def value_list(self): return list(self.ordered_data()) def sorted_data(self): return tuple(sorted_robust(self.data())) def ordered_data(self): return self.sorted_data() def bounds(self): try: lb = min(self, default=None) ub = max(self, default=None) except: lb = ub = None # Python2/3 consistency: We will follow the Python3 convention # and not assume numeric/nonnumeric types are comparable. If a # set is mixed non-numeric type, then we will report the bounds # as None. if type(lb) is not type(ub) and ( type(lb) not in native_numeric_types or type(ub) not in native_numeric_types ): return None, None else: return lb, ub def ranges(self): # This is way inefficient, but should always work: the ranges in a # Finite set is the list of scalars for i in self: if i.__class__ in native_numeric_types: yield NumericRange(i, i, 0) elif i.__class__ in native_types: yield NonNumericRange(i) else: # Because of things like SetOf, self could contain types # we have never seen before. try: as_numeric(i) yield NumericRange(i, i, 0) except: yield NonNumericRange(i)
[docs] class FiniteSetData(_FiniteSetMixin, SetData): """A general unordered iterable Set""" __slots__ = ('_values', '_domain', '_dimen')
[docs] def __init__(self, component): SetData.__init__(self, component=component) # Derived classes (like OrderedSetData) may want to change the # storage if not hasattr(self, '_values'): self._values = set() self._domain = Any self._dimen = UnknownSetDimen
[docs] def get(self, value, default=None): """ Return True if the set contains a given value. This method will raise TypeError for unhashable types. """ if normalize_index.flatten: value = normalize_index(value) if value in self._values: return value return default
def _iter_impl(self): return iter(self._values) def __reversed__(self): try: return reversed(self._values) except: return reversed(self.data()) def __len__(self): """ Return the number of elements in the set. """ return len(self._values) def __str__(self): if self.parent_component()._name is not None: return self.name if not self.parent_component()._constructed: return type(self).__name__ return "{" + (', '.join(str(_) for _ in self)) + "}" @property def dimen(self): if self._dimen is UnknownSetDimen: # Special case: abstract Sets with constant dimen # initializers have a known dimen before construction _comp = self.parent_component() if not _comp._constructed and _comp._init_dimen.constant(): return _comp._init_dimen.val return self._dimen @property def domain(self): return self._domain @property @deprecated("'filter' is no longer a public attribute.", version='5.7') def filter(self): return self._filter def add(self, *values): N = len(self) self.update(values) return len(self) - N def _update_impl(self, values): self._values.update(values) def remove(self, val): self._values.remove(val) def discard(self, val): self._values.discard(val) def clear(self): self._values.clear() def set_value(self, val): self.clear() self.update(val) def _initialize(self, val): try: # We want to explicitly call the update() on *this class* to # bypass potential double logging of the use of unordered # data with ordered Sets FiniteSetData.update(self, val) except TypeError as e: if 'not iterable' in str(e): logger.error( "Initializer for Set %s returned non-iterable object " "of type %s." % ( self.name, (val if val.__class__ is type else type(val).__name__), ) ) raise def update(self, values): # Special case: set operations that are not first attached # to the model must be constructed. if isinstance(values, SetOperator): values.construct() # It is important that val_iter is an actual iterator val_iter = iter(values) if self._dimen is not None: if normalize_index.flatten: val_iter = self._cb_normalized_dimen_verifier(self._dimen, val_iter) else: val_iter = self._cb_raw_dimen_verifier(self._dimen, val_iter) elif normalize_index.flatten: val_iter = map(normalize_index, val_iter) else: val_iter = self._cb_check_set_end(val_iter) if self._domain is not Any: val_iter = self._cb_domain_verifier(self._domain, val_iter) comp = self.parent_component() if comp._filter is not None: val_iter = self._cb_validate_filter('filter', val_iter) if comp._validate is not None: val_iter = self._cb_validate_filter('validate', val_iter) # We wrap this check in a try-except because some values # (like lists) are not hashable and can raise exceptions. try: self._update_impl(val_iter) except Set._SetEndException: pass def pop(self): return self._values.pop() def _cb_domain_verifier(self, domain, val_iter): for value in val_iter: if value not in domain: raise ValueError( "Cannot add value %s to Set %s.\n" "\tThe value is not in the domain %s" % (value, self.name, self._domain) ) yield value def _cb_check_set_end(self, val_iter): for value in val_iter: if value is Set.End: return yield value def _cb_validate_filter(self, mode, val_iter): fail_false = mode == 'validate' comp = self.parent_component() fcn = getattr(comp, '_' + mode) block = comp.parent_block() idx = self.index() for value in val_iter: try: flag = fcn(block, idx, value) if flag: yield value continue except Exception as e: flag = None exc = e if isinstance(value, tuple): vstar = value else: vstar = (value,) # First: try the old format: *values and no index if fcn.__class__ is ParameterizedIndexedCallInitializer: try: flag = fcn(block, (), *vstar) if flag: self._filter_validate_scalar_api_deprecation(mode, warning=True) yield value continue except TypeError: pass except Exception as e: exc = e # Now try *values and index try: flag = fcn(block, idx, *value) if flag: deprecation_warning( f"{self.__class__.__name__} {self.name}: '{mode}=' " "callback signature matched (block, *value, *index). " "Please update the callback to match the signature " "(block, value, *index).", version='6.8.0', ) if fcn.__class__ is not ParameterizedInitializer: orig_fcn = fcn._fcn fcn._fcn = lambda m, v, *i: orig_fcn(m, *v, *i) yield value continue except TypeError: pass except Exception as e: exc = e if flag is not None: if fail_false: raise ValueError( "The value=%s violates the validation rule of Set %s" % (value, self.name) ) continue logger.error( "Exception raised while validating element '%s' " "for Set %s" % (value, self.name) ) raise exc from None def _filter_validate_scalar_api_deprecation(self, mode, warning): comp = self.parent_component() fcn = getattr(comp, '_' + mode) if warning: deprecation_warning( f"{self.__class__.__name__} {self.name}: '{mode}=' " "callback signature matched (block, *value). " "Please update the callback to match the signature " f"(block, value{', *index' if comp.is_indexed() else ''}).", version='6.8.0', ) orig_fcn = fcn._fcn fcn = ParameterizedScalarCallInitializer(lambda m, v: orig_fcn(m, *v), True) setattr(comp, '_' + mode, fcn) def _cb_normalized_dimen_verifier(self, dimen, val_iter): for value in val_iter: if value.__class__ in native_types: if dimen == 1: yield value continue normalized_value = value else: normalized_value = normalize_index(value) # Note: normalize_index() will never return a 1-tuple if normalized_value.__class__ is tuple: if dimen == len(normalized_value): yield normalized_value[0] if dimen == 1 else normalized_value continue _d = len(normalized_value) if normalized_value.__class__ is tuple else 1 if _d == dimen: yield normalized_value elif dimen is UnknownSetDimen: # The first thing added to a Set with unknown dimension # sets its dimension self._dimen = dimen = _d yield normalized_value else: raise ValueError( "The value=%s has dimension %s and is not " "valid for Set %s which has dimen=%s" % (value, _d, self.name, self._dimen) ) def _cb_raw_dimen_verifier(self, dimen, val_iter): for value in val_iter: if isinstance(value, Sequence): if dimen == len(value): yield value continue elif dimen == 1: yield value continue _d = len(value) if isinstance(value, Sequence) else 1 if dimen is UnknownSetDimen: # The first thing added to a Set with unknown dimension # sets its dimension self._dimen = dimen = _d yield value else: raise ValueError( "The value=%s has dimension %s and is not " "valid for Set %s which has dimen=%s" % (value, _d, self.name, self._dimen) )
class _FiniteSetData(metaclass=RenamedClass): __renamed__new_class__ = FiniteSetData __renamed__version__ = '6.7.2' class _ScalarOrderedSetMixin(object): # This mixin is required because scalar ordered sets implement # __getitem__() as an alias of at() __slots__ = () def values(self): """Return an iterator of the component data objects in the dictionary""" if list(self.keys()): yield self def items(self): """Return an iterator of (index,data) tuples from the dictionary""" _keys = list(self.keys()) if _keys: yield _keys[0], self class _OrderedSetMixin(object): __slots__ = () _valid_getitem_keys = {None, (None,), Ellipsis} def at(self, index): raise DeveloperError( "Derived ordered set class (%s) failed to " "implement at" % (type(self).__name__,) ) def ord(self, val): raise DeveloperError( "Derived ordered set class (%s) failed to " "implement ord" % (type(self).__name__,) ) def __getitem__(self, key): # If key looks like the valid key for UnindexedComponent_set, or # is an Ellipsis/slice (because someone is generating a # component slice), then treat this like a regular Scalar # component and defer to the IndexedComponent implementation. # In any other case, defer to the deprecated OrderedScalarSet # functionality if not self.is_indexed() and ( key in self._valid_getitem_keys or type(key) is slice ): return super().__getitem__(key) deprecation_warning( "Using __getitem__ to return a set value from its (ordered) " "position is deprecated. Please use at()", version='6.1', remove_in='7.0', ) return self.at(key) @deprecated( "card() was incorrectly added to the Set API. Please use at()", version='6.1.2', remove_in='6.2', ) def card(self, index): return self.at(index) def isordered(self): """Returns True if this is an ordered finite discrete (iterable) Set""" return True def ordered_data(self): return self.data() def ordered_iter(self): return iter(self) def first(self): try: return next(iter(self)) except StopIteration: raise IndexError(f"{self.name} index out of range") from None def last(self): try: return next(reversed(self)) except StopIteration: raise IndexError(f"{self.name} index out of range") from None def next(self, item, step=1): """ Return the next item in the set. The default behavior is to return the very next element. The `step` option can specify how many steps are taken to get the next element. If the search item is not in the Set, or the next element is beyond the end of the set, then an IndexError is raised. """ position = self.ord(item) + step if position < 1: raise IndexError("Cannot advance before the beginning of the Set") if position > len(self): raise IndexError("Cannot advance past the end of the Set") return self.at(position) def nextw(self, item, step=1): """ Return the next item in the set with wrapping if necessary. The default behavior is to return the very next element. The `step` option can specify how many steps are taken to get the next element. If the next element is past the end of the Set, the search wraps back to the beginning of the Set. If the search item is not in the Set an IndexError is raised. """ position = self.ord(item) return self.at((position + step - 1) % len(self) + 1) def prev(self, item, step=1): """Return the previous item in the set. The default behavior is to return the immediately previous element. The `step` option can specify how many steps are taken to get the previous element. If the search item is not in the Set, or the previous element is before the beginning of the set, then an IndexError is raised. """ return self.next(item, -step) def prevw(self, item, step=1): """Return the previous item in the set with wrapping if necessary. The default behavior is to return the immediately previouselement. The `step` option can specify how many steps are taken to get the previous element. If the previous element is past the end of the Set, the search wraps back to the end of the Set. If the search item is not in the Set an IndexError is raised. """ return self.nextw(item, -step) def _to_0_based_index(self, item): # Efficiency note: unlike older Set implementations, this # implementation does not guarantee that the index is valid (it # could be outside of abs(i) <= len(self)). try: _item = int(item) if item != _item: raise IndexError() except: raise IndexError( f"Set '{self.name}' positional indices must be integers, " f"not {type(item).__name__}" ) from None if _item >= 1: return _item - 1 elif _item < 0: _item += len(self) if _item < 0: raise IndexError(f"{self.name} index out of range") return _item else: raise IndexError( "Accessing Pyomo Sets by position is 1-based: valid Set positional " "index values are [1 .. len(Set)] or [-1 .. -len(Set)]" )
[docs] class OrderedSetData(_OrderedSetMixin, FiniteSetData): """ This class defines the base class for an ordered set of concrete data. In older Pyomo terms, this defines a "concrete" ordered set - that is, a set that "owns" the list of set members. While this class actually implements a set ordered by insertion order, we make the "official" InsertionOrderSetData an empty derivative class, so that issubclass(SortedSetData, InsertionOrderSetData) == False Constructor Arguments: component The Set object that owns this data. Public Class Attributes: """ __slots__ = ('_ordered_values',)
[docs] def __init__(self, component): self._values = {} self._ordered_values = None FiniteSetData.__init__(self, component=component)
def _iter_impl(self): """ Return an iterator for the set. """ return iter(self._values) def __reversed__(self): return reversed(self._values) def _update_impl(self, values): for val in values: # Note that we reset _ordered_values within the loop because # of an old example where the initializer rule makes # reference to values previously inserted into the Set # (which triggered the creation of the _ordered_values) self._ordered_values = None self._values[val] = None def remove(self, val): self._values.pop(val) self._ordered_values = None def discard(self, val): try: self.remove(val) except KeyError: pass def clear(self): self._values.clear() self._ordered_values = None def pop(self): try: ans = self.last() except IndexError: # Map the exception for iterating over an empty dict to a # KeyError for consistency with set().pop() raise KeyError('pop from an empty set') from None self.discard(ans) return ans
[docs] def at(self, index): """ Return the specified member of the set. The public Set API is 1-based, even though the internal _lookup and _values are (pythonically) 0-based. """ if self._ordered_values is None: self._rebuild_ordered_values() i = self._to_0_based_index(index) try: return self._ordered_values[i] except IndexError: raise IndexError(f"{self.name} index out of range") from None
[docs] def ord(self, item): """ Return the position index of the input value. Note that Pyomo Set objects have positions starting at 1 (not 0). If the search item is not in the Set, then an IndexError is raised. """ # The bulk of single-value set members are stored as scalars. # However, we are now being more careful about matching tuples # when they are actually put as Set members. So, we will look # for the exact thing that the user sent us and then fall back # on the scalar. if self._ordered_values is None: self._rebuild_ordered_values() try: return self._values[item] + 1 except KeyError: if item.__class__ is not tuple or len(item) > 1: raise ValueError("%s.ord(x): x not in %s" % (self.name, self.name)) try: return self._values[item[0]] + 1 except KeyError: raise ValueError("%s.ord(x): x not in %s" % (self.name, self.name))
def _rebuild_ordered_values(self): _set = self._values self._ordered_values = list(_set) for i, v in enumerate(self._ordered_values): _set[v] = i
class _OrderedSetData(metaclass=RenamedClass): __renamed__new_class__ = OrderedSetData __renamed__version__ = '6.7.2'
[docs] class InsertionOrderSetData(OrderedSetData): """ This class defines the data for a ordered set where the items are ordered in insertion order (similar to Python's OrderedSet. Constructor Arguments: component The Set object that owns this data. Public Class Attributes: """ __slots__ = () def _initialize(self, val): if type(val) in Set._UnorderedInitializers: logger.warning( "Initializing ordered Set %s with " "a fundamentally unordered data source (type: %s). " "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (self.name, type(val).__name__) ) super()._initialize(val) def set_value(self, val): if type(val) in Set._UnorderedInitializers: logger.warning( "Calling set_value() on an insertion order Set with " "a fundamentally unordered data source (type: %s). " "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (type(val).__name__,) ) self.clear() super().update(val) def update(self, values): if type(values) in Set._UnorderedInitializers: logger.warning( "Calling update() on an insertion order Set with " "a fundamentally unordered data source (type: %s). " "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (type(values).__name__,) ) super().update(values)
class _InsertionOrderSetData(metaclass=RenamedClass): __renamed__new_class__ = InsertionOrderSetData __renamed__version__ = '6.7.2' class _SortedSetMixin(object): """""" __slots__ = () def ordered_iter(self): return iter(self) def sorted_iter(self): return iter(self)
[docs] class SortedSetData(_SortedSetMixin, OrderedSetData): """ This class defines the data for a sorted set. Constructor Arguments: component The Set object that owns this data. Public Class Attributes: """ __slots__ = () def _iter_impl(self): """ Return an iterator for the set. """ if self._ordered_values is None: self._rebuild_ordered_values() return iter(self._ordered_values) def __reversed__(self): if self._ordered_values is None: self._rebuild_ordered_values() return reversed(self._ordered_values) def _update_impl(self, values): for val in values: # Note that we reset _ordered_values within the loop because # of an old example where the initializer rule makes # reference to values previously inserted into the Set # (which triggered the creation of the _ordered_values) self._ordered_values = None self._values[val] = None # Note: removing data does not affect the sorted flag # def remove(self, val): # def discard(self, val): def sorted_data(self): return self.data() def _rebuild_ordered_values(self): _set = self._values self._ordered_values = list(self.parent_component()._sort_fcn(_set)) for i, v in enumerate(self._ordered_values): _set[v] = i
class _SortedSetData(metaclass=RenamedClass): __renamed__new_class__ = SortedSetData __renamed__version__ = '6.7.2' ############################################################################ _SET_API = (('__contains__', 'test membership in'), 'get', 'ranges', 'bounds') _FINITESET_API = _SET_API + ( ('__iter__', 'iterate over'), '__reversed__', '__len__', 'data', 'sorted_data', 'ordered_data', ) _ORDEREDSET_API = _FINITESET_API + ('at', 'ord') _SETDATA_API = ('set_value', 'add', 'remove', 'discard', 'clear', 'update', 'pop')
[docs] @ModelComponentFactory.register("Set data that is used to define a model instance.") class Set(IndexedComponent): """A component used to index other Pyomo components. This class provides a Pyomo component that is API-compatible with Python `set` objects, with additional features, including: 1. Member validation and filtering. The user can declare domains and provide callback functions to validate set members and to filter (ignore) potential members. 2. Set expressions. Operations on Set objects (&,|,*,-,^) produce Set expressions that preserve their references to the original Set objects so that updating the argument Sets implicitly updates the Set operator instance. 3. Support for set operations with RangeSet instances (both finite and non-finite ranges). Parameters ---------- initialize : initializer(iterable), optional The initial values to store in the Set when it is constructed. Values passed to ``initialize`` may be overridden by ``data`` passed to the :py:meth:`construct` method. dimen : initializer(int), optional Specify the Set's arity (the required tuple length for all members of the Set), or None if no arity is enforced ordered : bool or Set.InsertionOrder or Set.SortedOrder or function Specifies whether the set is ordered. Possible values are: ====================== ===================================== ``False`` Unordered ``True`` Ordered by insertion order ``Set.InsertionOrder`` Ordered by insertion order [default] ``Set.SortedOrder`` Ordered by sort order ``<function>`` Ordered with this comparison function ====================== ===================================== within : initialiser(set), optional A set that defines the valid values that can be contained in this set. If the latter is indexed, the former can be indexed or non-indexed, in which case it applies to all indices. domain : initializer(set), optional A set that defines the valid values that can be contained in this set bounds : initializer(tuple), optional A tuple that specifies the bounds for valid Set values (accepts 1-, 2-, or 3-tuple RangeSet arguments) filter : initializer(rule), optional A rule for determining membership in this set. This has the functional form: ``f: Block, *data -> bool`` and returns True if the data belongs in the set. Set will quietly ignore any values where `filter` returns False. validate : initializer(rule), optional A rule for validating membership in this set. This has the functional form: ``f: Block, *data -> bool`` and returns True if the data belongs in the set. Set will raise a ``ValueError`` for any values where `validate` returns False. name : str, optional The name of the set doc : str, optional A text string describing this component Notes ----- .. note:: ``domain=``, ``within=``, and ``bounds=`` all provide restrictions on the valid set values. If more than one is specified, Set values will be restricted to the intersection of ``domain``, ``within``, and ``bounds``. """ class _SetEndException(Exception): pass class _SetEndType(type): def __hash__(self): raise Set._SetEndException() class End(metaclass=_SetEndType): pass class InsertionOrder(object): pass class SortedOrder(object): pass _ValidOrderedArguments = {True, False, InsertionOrder, SortedOrder} _UnorderedInitializers = {set} @overload def __new__(cls: Type[Set], *args, **kwds) -> Union[SetData, IndexedSet]: ... @overload def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: ... def __new__(cls, *args, **kwds): if cls is not Set: return super(Set, cls).__new__(cls) # TBD: Should ordered be allowed to vary across an IndexedSet? # # Many things are easier by forcing it to be consistent across # the set (namely, the _ComponentDataClass is constant). # However, it is a bit off that 'ordered' it the only arg NOT # processed by Initializer. We can mock up a SortedSetData # sort function that preserves Insertion Order (lambda x: x), but # the unsorted is harder (it would effectively be insertion # order, but ordered() may not be deterministic based on how the # set was populated - and we could not issue a warning?) # # JDS [5/2019]: Until someone demands otherwise, I think we # should leave it constant across an IndexedSet ordered = kwds.get('ordered', Set.InsertionOrder) if ordered is True: ordered = Set.InsertionOrder if ordered not in Set._ValidOrderedArguments: if inspect.isfunction(ordered): ordered = Set.SortedOrder else: # We want the list to be deterministic, but not # alphabetical, so we first sort by type and then # convert evetything to string. Note that we have to # convert *types* to string early, as the default # ordering of types is random: so InsertionOrder and # SortedOrder would occasionally swap places. raise TypeError( "Set 'ordered' argument is not valid (must be one of {%s})" % ( ', '.join( str(_) for _ in sorted_robust( 'Set.' + x.__name__ if isinstance(x, type) else x for x in Set._ValidOrderedArguments.union( {'<function>'} ) ) ) ) ) if not args or (args[0] is UnindexedComponent_set and len(args) == 1): if ordered is Set.InsertionOrder: return super(Set, cls).__new__(AbstractOrderedScalarSet) elif ordered is Set.SortedOrder: return super(Set, cls).__new__(AbstractSortedScalarSet) else: return super(Set, cls).__new__(AbstractFiniteScalarSet) else: newObj = super(Set, cls).__new__(IndexedSet) if ordered is Set.InsertionOrder: newObj._ComponentDataClass = InsertionOrderSetData elif ordered is Set.SortedOrder: newObj._ComponentDataClass = SortedSetData else: newObj._ComponentDataClass = FiniteSetData return newObj @overload def __init__( self, *indexes, initialize=None, dimen=UnknownSetDimen, ordered=InsertionOrder, within=None, domain=None, bounds=None, filter=None, validate=None, name=None, doc=None, ): ...
[docs] def __init__(self, *args, **kwds): kwds.setdefault('ctype', Set) # The ordered flag was processed by __new__, but if this is a # sorted set, then we need to set the sorting function _ordered = kwds.pop('ordered', None) if _ordered and _ordered is not Set.InsertionOrder and _ordered is not True: if inspect.isfunction(_ordered): self._sort_fcn = _ordered else: self._sort_fcn = sorted_robust # 'domain', 'within', and 'bounds' are synonyms, in that they # restrict the set of valid set values. If more than one is # specified, we will restrict the Set values to the intersection # of the individual arguments self._init_domain = SetInitializer(None) _domain = kwds.pop('domain', None) if _domain is not None: self._init_domain.intersect(SetInitializer(_domain)) _within = kwds.pop('within', None) if _within is not None: self._init_domain.intersect(SetInitializer(_within)) _bounds = kwds.pop('bounds', None) if _bounds is not None: self._init_domain.intersect(BoundsInitializer(_bounds)) self._init_dimen = Initializer( kwds.pop('dimen', UnknownSetDimen), arg_not_specified=NOTSET ) self._init_values = TuplizeValuesInitializer( Initializer( kwds.pop('initialize', None), treat_sequences_as_mappings=False, allow_generators=True, ) ) self._validate = Initializer(kwds.pop('validate', None), additional_args=1) self._filter = Initializer(kwds.pop('filter', None), additional_args=1) if 'virtual' in kwds: deprecation_warning( "Pyomo Sets ignore the 'virtual' keyword argument", logger='pyomo.core.base', version='5.6.7', ) kwds.pop('virtual') IndexedComponent.__init__(self, *args, **kwds) # HACK to make the "counted call" syntax work. We wait until # after the base class is set up so that is_indexed() is # reliable. if ( self._init_values is not None and self._init_values._init.__class__ is IndexedCallInitializer ): self._init_values._init = CountedCallInitializer( self, self._init_values._init ) if not self.is_indexed(): # HACK: the DAT parser needs to know the domain of a set in # order to correctly parse the data stream. if self._init_domain.constant(): self._domain = self._init_domain(self.parent_block(), None, self) if self._init_dimen.constant(): self._dimen = self._init_dimen(self.parent_block(), None) if self._filter.__class__ is ParameterizedIndexedCallInitializer: self._filter_validate_scalar_api_deprecation('filter', warning=False) if self._validate.__class__ is ParameterizedIndexedCallInitializer: self._filter_validate_scalar_api_deprecation('validate', warning=False)
[docs] @deprecated( "check_values() is deprecated: Sets only contain valid members", version='5.7' ) def check_values(self): """ Verify that the values in this set are valid. """ return True
[docs] def construct(self, data=None): if self._constructed: return self._constructed = True timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug("Constructing Set, name=%s, from data=%r" % (self, data)) if self._anonymous_sets is not None: for _set in self._anonymous_sets: _set.construct() if data is not None: # Data supplied to construct() should override data provided # to the constructor tmp_init, self._init_values = self._init_values, TuplizeValuesInitializer( Initializer(data, treat_sequences_as_mappings=False) ) try: if self._init_values is None: if not self.is_indexed(): # This ensures backwards compatibility by causing all # scalar sets (including set operators) to be # initialized (and potentially empty) after construct(). self._getitem_when_not_present(None) elif self._init_values.contains_indices(): # The index is coming in externally; we need to validate it for index in self._init_values.indices(): IndexedComponent.__getitem__(self, index) else: # Bypass the index validation and create the member directly for index in self.index_set(): self._getitem_when_not_present(index) finally: # Restore the original initializer (if overridden by data argument) if data is not None: self._init_values = tmp_init timer.report()
# # This method must be defined on subclasses of # IndexedComponent that support implicit definition # def _getitem_when_not_present(self, index): """Returns the default component data value.""" # Because we allow sets within an IndexedSet to have different # dimen, we have moved the tuplization logic from PyomoModel # into Set (because we cannot know the dimen of a SetData until # we are actually constructing that index). This also means # that we need to potentially communicate the dimen to the # (wrapped) value initializer. So, we will get the dimen first, # then get the values. Only then will we know that this index # will actually be constructed (and not Skipped). _block = self.parent_block() # Note: _init_dimen and _init_domain are guaranteed to be non-None _d = self._init_dimen(_block, index) if not normalize_index.flatten and _d is not UnknownSetDimen and _d is not None: logger.warning( "Ignoring non-None dimen (%s) for set %s%s " "(normalize_index.flatten is False, so dimen " "verification is not available)." % (_d, self.name, ("[%s]" % (index,) if self.is_indexed() else "")) ) _d = None domain = self._init_domain(_block, index, self) if domain is not None: domain.parent_component().construct() if _d is UnknownSetDimen and domain is not None and domain.dimen is not None: _d = domain.dimen if index is None and not self.is_indexed(): obj = self._data[index] = self else: obj = self._data[index] = self._ComponentDataClass(component=self) obj._index = index obj._domain = domain if _d is not UnknownSetDimen: obj._dimen = _d if self._init_values is not None: # record the user-provided dimen in the initializer self._init_values._dimen = _d try: _values = self._init_values(_block, index) except TuplizeError as e: raise ValueError( str(e) % (self._name, "[%s]" % index if self.is_indexed() else "") ) if _values is Set.Skip: del self._data[index] return elif _values is None: raise ValueError( "Set rule or initializer returned None instead of Set.Skip" ) obj._initialize(_values) return obj @staticmethod def _pprint_members(x): if x.isfinite(): return '{' + str(x.ordered_data())[1:-1] + "}" else: ans = ' | '.join(str(_) for _ in x.ranges()) if ' | ' in ans: return "(" + ans + ")" if ans: return ans else: return "[]" @staticmethod def _pprint_dimen(x): d = x.dimen if d is UnknownSetDimen: return "--" return d @staticmethod def _pprint_domain(x): if x._domain is x and isinstance(x, SetOperator): return x._expression_str() else: return x._domain def _pprint(self): """ Return data that will be printed for this component. """ # # Eventually, we might want to support a 'verbose' flag to # pprint() that will suppress some of the very long (less # informative) output # # if verbose: # def members(x): # return '{' + str(x.ordered_data())[1:-1] + "}" # else: # MAX_MEMBERES=10 # def members(x): # ans = x.ordered_data() # if len(x) > MAX_MEMBERES: # return '{' + str(ans[:MAX_MEMBERES])[1:-1] + ', ...}' # else: # return '{' + str(ans)[1:-1] + "}" # TBD: In the current design, we force all SetData within an # indexed Set to have the same isordered value, so we will only # print it once in the header. Is this a good design? try: _ordered = self.isordered() _refClass = type(self) except: _ordered = issubclass(self._ComponentDataClass, _OrderedSetMixin) _refClass = self._ComponentDataClass if _ordered: # This is a bit of an anachronism. Historically Pyomo # reported "Insertion" for Set.InsertionOrder, "Sorted" for # Set.SortedOrder, and "{user}" for everything else. # However, we do not preserve that flag any more, so we # will infer it from the class hierarchy if issubclass(_refClass, _SortedSetMixin): if self.parent_component()._sort_fcn is sorted_robust: _ordered = "Sorted" else: _ordered = "{user}" elif issubclass(_refClass, InsertionOrderSetData): _ordered = "Insertion" return ( [ ("Size", len(self._data)), ("Index", self._index_set if self.is_indexed() else None), ("Ordered", _ordered), ], self._data.items(), ("Dimen", "Domain", "Size", "Members"), lambda k, v: [ Set._pprint_dimen(v), Set._pprint_domain(v), len(v) if v.isfinite() else 'Inf', Set._pprint_members(v), ], )
[docs] class IndexedSet(Set):
[docs] def data(self): "Return a dict containing the data() of each Set in this IndexedSet" return {k: v.data() for k, v in self.items()}
@overload def __getitem__(self, index) -> SetData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore
[docs] class FiniteScalarSet(FiniteSetData, Set):
[docs] def __init__(self, **kwds): FiniteSetData.__init__(self, component=self) Set.__init__(self, **kwds) self._index = UnindexedComponent_index
[docs] class FiniteSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = FiniteScalarSet __renamed__version__ = '6.0'
[docs] class OrderedScalarSet(_ScalarOrderedSetMixin, InsertionOrderSetData, Set):
[docs] def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag kwds.setdefault('ordered', Set.InsertionOrder) InsertionOrderSetData.__init__(self, component=self) Set.__init__(self, **kwds)
[docs] class OrderedSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = OrderedScalarSet __renamed__version__ = '6.0'
[docs] class SortedScalarSet(_ScalarOrderedSetMixin, SortedSetData, Set):
[docs] def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag kwds.setdefault('ordered', Set.SortedOrder) SortedSetData.__init__(self, component=self) Set.__init__(self, **kwds) self._index = UnindexedComponent_index
[docs] class SortedSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = SortedScalarSet __renamed__version__ = '6.0'
[docs] @disable_methods(_FINITESET_API + _SETDATA_API) class AbstractFiniteScalarSet(FiniteScalarSet): pass
[docs] class AbstractFiniteSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractFiniteScalarSet __renamed__version__ = '6.0'
[docs] @disable_methods(_ORDEREDSET_API + _SETDATA_API) class AbstractOrderedScalarSet(OrderedScalarSet): pass
[docs] class AbstractOrderedSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractOrderedScalarSet __renamed__version__ = '6.0'
[docs] @disable_methods(_ORDEREDSET_API + _SETDATA_API) class AbstractSortedScalarSet(SortedScalarSet): pass
[docs] class AbstractSortedSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractSortedScalarSet __renamed__version__ = '6.0'
############################################################################
[docs] class SetOf(SetData, Component): """""" def __new__(cls, *args, **kwds): if cls is not SetOf: return super(SetOf, cls).__new__(cls) (reference,) = args if isinstance(reference, (SetData, GlobalSetBase)): if reference.isfinite(): if reference.isordered(): return super(SetOf, cls).__new__(OrderedSetOf) else: return super(SetOf, cls).__new__(FiniteSetOf) else: return super(SetOf, cls).__new__(InfiniteSetOf) if isinstance(reference, Sequence): return super(SetOf, cls).__new__(OrderedSetOf) else: return super(SetOf, cls).__new__(FiniteSetOf)
[docs] def __init__(self, reference, **kwds): SetData.__init__(self, component=self) kwds.setdefault('ctype', SetOf) Component.__init__(self, **kwds) self._ref = reference self.construct()
def __str__(self): if self._name is not None: return self.name return str(self._ref)
[docs] def construct(self, data=None): if self._constructed: return self._constructed = True timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug("Constructing SetOf, name=%s, from data=%r" % (self, data)) timer.report()
@property def dimen(self): if isinstance(self._ref, SetData): return self._ref.dimen _iter = iter(self) try: x = next(_iter) if type(x) is tuple: ans = len(x) else: ans = 1 except: return 0 for x in _iter: _this = len(x) if type(x) is tuple else 1 if _this != ans: return None return ans @property def domain(self): return self def _pprint(self): """ Return data that will be printed for this component. """ return ( [("Dimen", self.dimen), ("Size", len(self)), ("Bounds", self.bounds())], {None: self}.items(), ("Ordered", "Members"), lambda k, v: [v.isordered(), str(v._ref)], )
[docs] class InfiniteSetOf(SetOf): def ranges(self): # InfiniteSetOf references are assumed to implement the Set API return self._ref.ranges()
[docs] class FiniteSetOf(_FiniteSetMixin, SetOf): def get(self, value, default=None): # Note that the efficiency of this depends on the reference object # # The bulk of single-value set members were stored as scalars. # Check that first. if value.__class__ is tuple and len(value) == 1: if value[0] in self._ref: return value[0] if value in self._ref: return value return default def __len__(self): return len(self._ref) def _iter_impl(self): return iter(self._ref) def __reversed__(self): try: return reversed(self._ref) except: return reversed(self.data())
[docs] class UnorderedSetOf(metaclass=RenamedClass): __renamed__new_class__ = FiniteSetOf __renamed__version__ = '6.2'
[docs] class OrderedSetOf(_ScalarOrderedSetMixin, _OrderedSetMixin, FiniteSetOf): def at(self, index): i = self._to_0_based_index(index) try: return self._ref[i] except IndexError: raise IndexError(f"{self.name} index out of range") from None def ord(self, item): # The bulk of single-value set members are stored as scalars. # However, we are now being more careful about matching tuples # when they are actually put as Set members. So, we will look # for the exact thing that the user sent us and then fall back # on the scalar. try: return self._ref.index(item) + 1 except ValueError: if item.__class__ is not tuple or len(item) > 1: raise return self._ref.index(item[0]) + 1
############################################################################
[docs] class InfiniteRangeSetData(SetData): """Data class for a infinite set. This Set implements an interface to an *infinite set* defined by one or more NumericRange objects. As there are an infinite number of members, Infinite Range Sets are not iterable. """ __slots__ = ('_ranges',)
[docs] def __init__(self, component): SetData.__init__(self, component=component) self._ranges = None
def get(self, value, default=None): # The bulk of single-value set members were stored as scalars. # Check that first. if value.__class__ is tuple and len(value) == 1: v = value[0] if any(v in r for r in self._ranges): return v if any(value in r for r in self._ranges): return value return default
[docs] def isdiscrete(self): """Returns True if this set admits only discrete members""" return all(r.isdiscrete() for r in self.ranges())
@property def dimen(self): return 1 @property def domain(self): return Reals def clear(self): self._ranges = () def ranges(self): return iter(self._ranges)
class _InfiniteRangeSetData(metaclass=RenamedClass): __renamed__new_class__ = InfiniteRangeSetData __renamed__version__ = '6.7.2'
[docs] class FiniteRangeSetData( _SortedSetMixin, _OrderedSetMixin, _FiniteSetMixin, InfiniteRangeSetData ): __slots__ = () @staticmethod def _range_gen(r): start, end = (r.start, r.end) if r.step > 0 else (r.end, r.start) step = abs(r.step) n = start i = 0 if start == end: yield start else: while n <= end: yield n i += 1 n = start + i * step def _iter_impl(self): # If there is only a single underlying range, then we will # iterate over it nIters = len(self._ranges) - 1 if not nIters: yield from FiniteRangeSetData._range_gen(self._ranges[0]) return # The trick here is that we need to remove any duplicates from # the multiple ranges. We will set up iterators for each range, # pull the first element from each iterator, sort and yield the # lowest value. iters = [] for r in self._ranges: # Note: there should always be at least 1 member in each # NumericRange i = FiniteRangeSetData._range_gen(r) iters.append([next(i), i]) iters.sort(reverse=True, key=lambda x: x[0]) n = None while iters: if n != iters[-1][0]: n = iters[-1][0] yield n try: iters[-1][0] = next(iters[-1][1]) if nIters and iters[-2][0] < iters[-1][0]: iters.sort(reverse=True) except StopIteration: iters.pop() nIters -= 1 def __len__(self): if len(self._ranges) == 1: # If there is only one range, then this set's range is equal # to the range's length r = self._ranges[0] if r.start == r.end: return 1 else: return int((r.end - r.start) // r.step) + 1 else: return sum(1 for _ in self) def at(self, index): assert int(index) == index idx = self._to_0_based_index(index) if len(self._ranges) == 1: r = self._ranges[0] ans = r.start + (idx) * r.step if ans <= r.end: return ans else: for ans in self: if not idx: return ans idx -= 1 raise IndexError(f"{self.name} index out of range") def ord(self, item): if len(self._ranges) == 1: r = self._ranges[0] i = float(item - r.start) / r.step if ( item >= r.start and item <= r.end and abs(i - math.floor(i + 0.5)) < r._EPS ): return int(math.floor(i + 0.5)) + 1 else: ans = 1 for val in self: if val == item: return ans ans += 1 raise ValueError( "Cannot identify position of %s in Set %s: item not in Set" % (item, self.name) ) # We must redefine ranges(), bounds(), and domain so that we get the # InfiniteRangeSetData version and not the one from # _FiniteSetMixin. bounds = InfiniteRangeSetData.bounds ranges = InfiniteRangeSetData.ranges domain = InfiniteRangeSetData.domain
class _FiniteRangeSetData(metaclass=RenamedClass): __renamed__new_class__ = FiniteRangeSetData __renamed__version__ = '6.7.2'
[docs] @ModelComponentFactory.register( "A sequence of numeric values. RangeSet(start,end,step) is a sequence " "starting a value 'start', and increasing in values by 'step' until a " "value greater than or equal to 'end' is reached." ) class RangeSet(Component): """A set object that represents a set of numeric values `RangeSet` objects are based around `NumericRange` objects, which include support for non-finite ranges (both continuous and unbounded). Similarly, boutique ranges (like semi-continuous domains) can be represented, e.g.: .. doctest:: >>> from pyomo.core.base.range import NumericRange >>> from pyomo.environ import RangeSet >>> print(RangeSet(ranges=(NumericRange(0,0,0), NumericRange(1,100,0)))) ([0] | [1..100]) The `RangeSet` object continues to support the notation for specifying discrete ranges using "[first=1], last, [step=1]" values: .. doctest:: >>> r = RangeSet(3) >>> print(r) [1:3] >>> print(list(r)) [1, 2, 3] >>> r = RangeSet(2, 5) >>> print(r) [2:5] >>> print(list(r)) [2, 3, 4, 5] >>> r = RangeSet(2, 5, 2) >>> print(r) [2:4:2] >>> print(list(r)) [2, 4] >>> r = RangeSet(2.5, 4, 0.5) >>> print(r) ([2.5] | [3.0] | [3.5] | [4.0]) >>> print(list(r)) [2.5, 3.0, 3.5, 4.0] By implementing RangeSet using NumericRanges, the global Sets (like `Reals`, `Integers`, `PositiveReals`, etc.) are trivial instances of a RangeSet and support all Set operations. Parameters ---------- *args: int | float | None The range defined by ([start=1], end, [step=1]). If only a single positional parameter, `end` is supplied, then the RangeSet will be the integers starting at 1 up through and including end. Providing two positional arguments, `x` and `y`, will result in a range starting at x up to and including y, incrementing by 1. Providing a 3-tuple enables the specification of a step other than 1. finite: bool, optional This sets if this range is finite (discrete and bounded) or infinite ranges: iterable, optional The list of range objects that compose this RangeSet bounds: tuple, optional The lower and upper bounds of values that are admissible in this RangeSet filter: function, optional Function (rule) that returns True if the specified value is in the RangeSet or False if it is not. validate: function, optional Data validation function (rule). The function will be called for every data member of the set, and if it returns False, a ValueError will be raised. name: str, optional Name for this component. doc: str, optional Text describing this component. """ def __new__(cls, *args, **kwds): if cls is not RangeSet: return super(RangeSet, cls).__new__(cls) finite = kwds.pop('finite', None) if finite is None: if 'ranges' in kwds: if any(not r.isfinite() for r in kwds['ranges']): finite = False for i, _ in enumerate(args): if type(_) not in native_types: # Strange nosetest coverage issue: if the logic is # negated and the continue is in the "else", that # line is not caught as being covered. if ( not isinstance(_, ComponentData) or not _.parent_component().is_constructed() ): continue else: # "Peek" at constructed components to try and # infer if this component will be Infinite _ = value(_) if i < 2: if _ in {None, _inf, -_inf}: finite = False break elif _ == 0 and args[0] is not args[1]: finite = False if finite is None: # Assume "undetermined" RangeSets will be finite. If a # user wants them to be infinite, they can always # specify finite=False finite = True if finite: return super(RangeSet, cls).__new__(AbstractFiniteScalarRangeSet) else: return super(RangeSet, cls).__new__(AbstractInfiniteScalarRangeSet) # `start`, `end`, `step` in `*args` are positional-only that cannot be filled with keywords. # But positional-only params syntax are not supported before python 3.8. # To emphasize they are positional-only, an underscore is added before their name. @overload def __init__( self, _end, *, finite=None, ranges=(), bounds=None, filter=None, validate=None, name=None, doc=None, ): ... @overload def __init__( self, _start, _end, _step=1, *, finite=None, ranges=(), bounds=None, filter=None, validate=None, name=None, doc=None, ): ... @overload def __init__( self, *, finite=None, ranges=(), bounds=None, filter=None, validate=None, name=None, doc=None, ): ...
[docs] def __init__(self, *args, **kwds): # Finite was processed by __new__ kwds.setdefault('ctype', RangeSet) if len(args) > 3: raise ValueError( "RangeSet expects 3 or fewer positional " "arguments (received %s)" % (len(args),) ) kwds.pop('finite', None) self._init_data = (args, kwds.pop('ranges', ())) self._validate = Initializer(kwds.pop('validate', None), additional_args=1) self._filter = Initializer(kwds.pop('filter', None), additional_args=1) self._init_bounds = kwds.pop('bounds', None) if self._init_bounds is not None: self._init_bounds = BoundsInitializer(self._init_bounds) Component.__init__(self, **kwds) # Shortcut: if all the relevant construction information is # simple (hard-coded) values, then it is safe to go ahead and # construct the set. # # NOTE: We will need to revisit this if we ever allow passing # data into the construct method (which would override the # hard-coded values here). # # NOTE: We will NOT automatically construct RangeSet objects # that reference mutable data so that we can generate a more # meaningful warning message about a RangeSet defined by mutable # data. try: if all( type(_) in native_types or (_.parent_component().is_constructed() and is_constant(_)) for _ in args ): self.construct() except AttributeError: pass
def __str__(self): # Named components should return their name e.g., Reals if self._name is not None: return self.name # Unconstructed floating components return their type if not self._constructed: return type(self).__name__ # Floating, unnamed constructed components return their ranges() ans = ' | '.join(str(_) for _ in self.ranges()) if ' | ' in ans: return "(" + ans + ")" if ans: return ans else: return "[]"
[docs] def construct(self, data=None): if self._constructed: return timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug("Constructing RangeSet, name=%s, from data=%r" % (self, data)) # Note: we cannot set the constructed flag until after we have # generated the debug message: the debug message needs the name, # which in turn may need ranges(), which has not been # constructed. self._constructed = True if data is not None: raise ValueError( "RangeSet.construct() does not support the data= argument.\n" "Initialization data (range endpoints) can only be supplied " "as numbers, constants, or Params to the RangeSet() " "declaration" ) args, ranges = self._init_data nonconstant_data_warning = any(not is_constant(arg) for arg in args) args = tuple(value(arg) for arg in args) if type(ranges) is not tuple: ranges = tuple(ranges) if len(args) == 1: # This is a bit of a hack for backwards compatibility with # the old RangeSet implementation, where we did less # validation of the RangeSet arguments, and allowed the # creation of 0-length RangeSets if args[0] != 0: # No need to check for floating point - it will # automatically be truncated ranges = ranges + (NumericRange(1, args[0], 1),) elif len(args) == 2: # This is a bit of a hack for backwards compatibility with # the old RangeSet implementation, where we did less # validation of the RangeSet arguments, and allowed the # creation of 0-length RangeSets if None in args or args[1] - args[0] != -1: args = (args[0], args[1], 1) if len(args) == 3: # Discrete ranges anchored by a floating point value or # incremented by a floating point value cannot be handled by # the NumericRange object. We will just discretize this # range (mostly for backwards compatibility) start, end, step = args if step: if start is None: start, end = end, start step *= -1 if start is None: # Backwards compatibility: assume unbounded RangeSet # is grounded at 0 ranges += ( NumericRange(0, None, step), NumericRange(0, None, -step), ) elif int(step) != step: if end is None: raise ValueError( "RangeSet does not support unbounded ranges " "with a non-integer step (got [%s:%s:%s])" % (start, end, step) ) if (end >= start) ^ (step > 0): raise ValueError( "RangeSet: start, end ordering incompatible with " "step direction (got [%s:%s:%s])" % (start, end, step) ) n = start i = 0 while (step > 0 and n <= end) or (step < 0 and n >= end): ranges += (NumericRange(n, n, 0),) i += 1 n = start + step * i else: ranges += (NumericRange(start, end, step),) else: ranges += (NumericRange(*args),) for r in ranges: if not isinstance(r, NumericRange): raise TypeError( "RangeSet 'ranges' argument must be an " "iterable of NumericRange objects" ) if not r.isfinite() and self.isfinite(): raise ValueError( "Constructing a finite RangeSet over a non-finite " "range (%s). Either correct the range data or " "specify 'finite=False' when declaring the RangeSet" % (r,) ) _block = self.parent_block() if self._init_bounds is not None: bnds = self._init_bounds(_block, None) tmp = [] for r in ranges: tmp.extend(r.range_intersection(bnds.ranges())) ranges = tuple(tmp) self._ranges = ranges if self._filter is not None: if not self.isfinite(): raise ValueError( "The 'filter' keyword argument is not valid for " "non-finite RangeSet component (%s)" % (self.name,) ) _filter = self._filter # If this is a finite set, then we can go ahead and filter # all the ranges. This allows pprint and len to be correct, # without special handling new_ranges = [] old_ranges = list(self.ranges()) old_ranges.reverse() while old_ranges: r = old_ranges.pop() for i, val in enumerate(FiniteRangeSetData._range_gen(r)): if not _filter(_block, (), val): split_r = r.range_difference((NumericRange(val, val, 0),)) if len(split_r) == 2: new_ranges.append(split_r[0]) old_ranges.append(split_r[1]) elif len(split_r) == 1: if i == 0: old_ranges.append(split_r[0]) else: new_ranges.append(split_r[0]) i = None break if i is not None: new_ranges.append(r) self._ranges = new_ranges if self._validate is not None: if not self.isfinite(): raise ValueError( "The 'validate' keyword argument is not valid for " "non-finite RangeSet component (%s)" % (self.name,) ) try: for val in self: if not self._validate(_block, None, val): raise ValueError( "The value=%s violates the validation rule of " "Set %s" % (val, self.name) ) except: logger.error( "Exception raised while validating element '%s' " "for Set %s" % (val, self.name) ) raise # Defer the warning about non-constant args until after the # component has been constructed, so that the conversion of the # component to a rational string will work (anonymous RangeSets # will report their ranges, which aren't present until # construction is over) if nonconstant_data_warning: logger.warning( "Constructing RangeSet '%s' from non-constant data (e.g., " "Var or mutable Param). The linkage between this RangeSet " "and the original source data will be broken, so updating " "the data value in the future will not be reflected in this " "RangeSet. To suppress this warning, explicitly convert " "the source data to a constant type (e.g., float, int, or " "immutable Param)" % (self,) ) timer.report()
# # Until the time that we support indexed RangeSet objects, we will # mock up some of the IndexedComponent API for consistency with the # previous (<=5.6.7) implementation. # def dim(self): return 0 def index_set(self): return UnindexedComponent_set def _pprint(self): """ Return data that will be printed for this component. """ return ( [ ("Dimen", self.dimen), ("Size", len(self) if self.isfinite() else 'Inf'), ("Bounds", self.bounds()), ], {None: self}.items(), ("Finite", "Members"), lambda k, v: [ v.isfinite(), # isinstance(v, _FiniteSetMixin), ', '.join(str(r) for r in self.ranges()) or '[]', ], )
[docs] class InfiniteScalarRangeSet(InfiniteRangeSetData, RangeSet):
[docs] def __init__(self, *args, **kwds): InfiniteRangeSetData.__init__(self, component=self) RangeSet.__init__(self, *args, **kwds) self._index = UnindexedComponent_index
# We want the RangeSet.__str__ to override the one in _FiniteSetMixin __str__ = RangeSet.__str__
[docs] class InfiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__new_class__ = InfiniteScalarRangeSet __renamed__version__ = '6.0'
[docs] class FiniteScalarRangeSet(_ScalarOrderedSetMixin, FiniteRangeSetData, RangeSet):
[docs] def __init__(self, *args, **kwds): FiniteRangeSetData.__init__(self, component=self) RangeSet.__init__(self, *args, **kwds) self._index = UnindexedComponent_index
# We want the RangeSet.__str__ to override the one in _FiniteSetMixin __str__ = RangeSet.__str__
[docs] class FiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__new_class__ = FiniteScalarRangeSet __renamed__version__ = '6.0'
[docs] @disable_methods(_SET_API) class AbstractInfiniteScalarRangeSet(InfiniteScalarRangeSet): pass
[docs] class AbstractInfiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractInfiniteScalarRangeSet __renamed__version__ = '6.0'
[docs] @disable_methods(_ORDEREDSET_API) class AbstractFiniteScalarRangeSet(FiniteScalarRangeSet): pass
[docs] class AbstractFiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractFiniteScalarRangeSet __renamed__version__ = '6.0'
############################################################################ # Set Operators ############################################################################
[docs] class SetOperator(SetData, Set): __slots__ = ('_sets',)
[docs] def __init__(self, *args, **kwds): SetData.__init__(self, component=self) Set.__init__(self, **kwds) self._sets, _anonymous = zip(*(process_setarg(_set) for _set in args)) _anonymous = tuple(filter(None, _anonymous)) if _anonymous: self._anonymous_sets = ComponentSet() for _set in _anonymous: self._anonymous_sets.update(_set) # We will immediately construct all set operators if the operands # are all themselves constructed. if all(_.parent_component()._constructed for _ in self._sets): self.construct()
[docs] def construct(self, data=None): if self._constructed: return self._constructed = True timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug( "Constructing SetOperator, name=%s, from data=%r" % (self, data) ) if self._anonymous_sets is not None: for _set in self._anonymous_sets: _set.construct() # This ensures backwards compatibility by causing all scalar # sets (including set operators) to be initialized (and # potentially empty) after construct(). self._getitem_when_not_present(None) if data: deprecation_warning( "Providing construction data to SetOperator objects is " "deprecated. This data is ignored and in a future version " "will not be allowed", version='5.7', ) fail = len(data) > 1 or None not in data if not fail: _data = data[None] if len(_data) != len(self): fail = True else: for v in _data: if v not in self: fail = True break if fail: raise ValueError( "Constructing SetOperator %s with incompatible data " "(data=%s}" % (self, data) ) timer.report()
def __len__(self): """Return the length of this Set Because Set objects (and therefore SetOperator objects) are a subclass of IndexedComponent, we need to override the definition of len() to return the length of the Set and not the Component. Failing to do so would result in scalar infinite set operators to return a length of "1". Python requires len() to return a nonnegatie integer. Instead of returning `float('inf')` here and allowing Python to raise the OverflowError, we will raise it directly here where we can provide a more informative error message. """ raise OverflowError( "The length of a non-finite Set is Inf; however, Python " "requires len() to return a non-negative integer value. Check " "isfinite() before calling len() for possibly infinite Sets" ) def __str__(self): if self._name is not None: return self.name return self._expression_str() def _expression_str(self): _args = [] for arg in self._sets: arg_str = str(arg) if ' ' in arg_str and isinstance(arg, SetOperator): arg_str = "(" + arg_str + ")" _args.append(arg_str) return self._operator.join(_args)
[docs] def isdiscrete(self): """Returns True if this set admits only discrete members""" return all(r.isdiscrete() for r in self.ranges())
def subsets(self, expand_all_set_operators=None): if not isinstance(self, SetProduct): if expand_all_set_operators is None: logger.warning( """ Extracting subsets for Set %s, which is a SetOperator other than a SetProduct. Returning this set and not descending into the set operands. To descend into this operator, specify 'subsets(expand_all_set_operators=True)' or to suppress this warning, specify 'subsets(expand_all_set_operators=False)'""" % (self.name,) ) yield self return elif not expand_all_set_operators: yield self return for s in self._sets: yield from s.subsets(expand_all_set_operators=expand_all_set_operators) @property @deprecated( "SetProduct.set_tuple is deprecated. " "Use SetProduct.subsets() to get the operator arguments.", version='5.7', ) def set_tuple(self): # Despite its name, in the old SetProduct, set_tuple held a list return list(self.subsets()) @property def domain(self): return self._domain @property def _domain(self): # We hijack the _domain attribute of SetOperator so that pprint # prints out the expression as the Set's "domain". Doing this # as a property prevents the circular reference return self @_domain.setter def _domain(self, val): if val is not Any: raise ValueError( "Setting the domain of a Set Operator is not allowed: %s" % val ) @staticmethod def _checkArgs(*sets): ans = [] for s in sets: if isinstance(s, SetData): ans.append((s.isordered(), s.isfinite())) elif type(s) in {tuple, list}: ans.append((True, True)) else: ans.append((False, True)) return ans
############################################################################
[docs] class SetUnion(SetOperator): __slots__ = tuple() _operator = " | " def __new__(cls, *args): if cls != SetUnion: return super(SetUnion, cls).__new__(cls) set0, set1 = SetOperator._checkArgs(*args) if set0[0] and set1[0]: cls = SetUnion_OrderedSet elif set0[1] and set1[1]: cls = SetUnion_FiniteSet else: cls = SetUnion_InfiniteSet return cls.__new__(cls) def ranges(self): return itertools.chain(*tuple(s.ranges() for s in self._sets)) @property def dimen(self): d0 = self._sets[0].dimen d1 = self._sets[1].dimen if d0 is None or d1 is None: return None if d0 is UnknownSetDimen or d1 is UnknownSetDimen: return UnknownSetDimen if d0 == d1: return d0 else: return None
[docs] class SetUnion_InfiniteSet(SetUnion): __slots__ = tuple() def get(self, val, default=None): # return any(val in s for s in self._sets) for s in self._sets: v = s.get(val, default) if v is not default: return v return default
[docs] class SetUnion_FiniteSet(_FiniteSetMixin, SetUnion_InfiniteSet): __slots__ = tuple() def _iter_impl(self): set0 = self._sets[0] return itertools.chain(set0, (_ for _ in self._sets[1] if _ not in set0)) def __len__(self): """ Return the number of elements in the set. """ # There is no easy way to tell how many duplicates there are in # the second set. Our only choice is to count them. We will # try and be a little efficient by using len() for the first # set, though. set0, set1 = self._sets return len(set0) + sum(1 for s in set1 if s not in set0)
[docs] class SetUnion_OrderedSet(_ScalarOrderedSetMixin, _OrderedSetMixin, SetUnion_FiniteSet): __slots__ = tuple() def at(self, index): idx = self._to_0_based_index(index) set0_len = len(self._sets[0]) if idx < set0_len: return self._sets[0].at(idx + 1) else: idx -= set0_len - 1 set1_iter = iter(self._sets[1]) try: while idx: val = next(set1_iter) if val not in self._sets[0]: idx -= 1 except StopIteration: raise IndexError(f"{self.name} index out of range") from None return val
[docs] def ord(self, item): """ Return the position index of the input value. Note that Pyomo Set objects have positions starting at 1 (not 0). If the search item is not in the Set, then an IndexError is raised. """ if item in self._sets[0]: return self._sets[0].ord(item) if item not in self._sets[1]: raise IndexError( "Cannot identify position of %s in Set %s: item not in Set" % (item, self.name) ) idx = len(self._sets[0]) _iter = iter(self._sets[1]) while True: val = next(_iter) if val == item: break elif val not in self._sets[0]: idx += 1 return idx + 1
############################################################################
[docs] class SetIntersection(SetOperator): __slots__ = tuple() _operator = " & " def __new__(cls, *args): if cls != SetIntersection: return super(SetIntersection, cls).__new__(cls) set0, set1 = SetOperator._checkArgs(*args) if set0[0] or set1[0]: cls = SetIntersection_OrderedSet elif set0[1] or set1[1]: cls = SetIntersection_FiniteSet else: cls = SetIntersection_InfiniteSet return cls.__new__(cls)
[docs] def construct(self, data=None): super(SetIntersection, self).construct(data) if not self.isfinite(): _finite = True for r in self.ranges(): if not r.isfinite(): _finite = False break if _finite: self.__class__ = SetIntersection_OrderedSet
def ranges(self): for a in self._sets[0].ranges(): yield from a.range_intersection(self._sets[1].ranges()) @property def dimen(self): d1 = self._sets[0].dimen d2 = self._sets[1].dimen if d1 is None: return d2 elif d2 is None: return d1 elif d1 == d2: return d1 elif d1 is UnknownSetDimen or d2 is UnknownSetDimen: return UnknownSetDimen else: return 0
[docs] class SetIntersection_InfiniteSet(SetIntersection): __slots__ = tuple() def get(self, val, default=None): # return all(val in s for s in self._sets) for s in self._sets: v = s.get(val, default) if v is default: return default return v
[docs] class SetIntersection_FiniteSet(_FiniteSetMixin, SetIntersection_InfiniteSet): __slots__ = tuple() def _iter_impl(self): set0, set1 = self._sets if not set0.isordered(): if set1.isordered(): set0, set1 = set1, set0 elif not set0.isfinite(): if set1.isfinite(): set0, set1 = set1, set0 else: # The odd case of a finite continuous range # intersected with an infinite discrete range... ranges = [] for r0 in set0.ranges(): ranges.extend(r0.range_intersection(set1.ranges())) # Note that the RangeSet is automatically # constructed, as it has no non-native positional # parameters. return iter(RangeSet(ranges=ranges)) return (s for s in set0 if s in set1) def __len__(self): """ Return the number of elements in the set. """ return sum(1 for _ in self)
[docs] class SetIntersection_OrderedSet( _ScalarOrderedSetMixin, _OrderedSetMixin, SetIntersection_FiniteSet ): __slots__ = tuple() def at(self, index): idx = self._to_0_based_index(index) _iter = iter(self) try: while idx: next(_iter) idx -= 1 return next(_iter) except StopIteration: raise IndexError(f"{self.name} index out of range") from None
[docs] def ord(self, item): """ Return the position index of the input value. Note that Pyomo Set objects have positions starting at 1 (not 0). If the search item is not in the Set, then an IndexError is raised. """ if item not in self._sets[0] or item not in self._sets[1]: raise IndexError( "Cannot identify position of %s in Set %s: item not in Set" % (item, self.name) ) idx = 0 _iter = iter(self) while next(_iter) != item: idx += 1 return idx + 1
############################################################################
[docs] class SetDifference(SetOperator): __slots__ = tuple() _operator = " - " def __new__(cls, *args): if cls != SetDifference: return super(SetDifference, cls).__new__(cls) set0, set1 = SetOperator._checkArgs(*args) if set0[0]: cls = SetDifference_OrderedSet elif set0[1]: cls = SetDifference_FiniteSet else: cls = SetDifference_InfiniteSet return cls.__new__(cls) def ranges(self): for a in self._sets[0].ranges(): yield from a.range_difference(self._sets[1].ranges()) @property def dimen(self): return self._sets[0].dimen
[docs] class SetDifference_InfiniteSet(SetDifference): __slots__ = tuple() def get(self, val, default=None): # return val in self._sets[0] and not val in self._sets[1] v_l = self._sets[0].get(val, default) if v_l is default: return default v_r = self._sets[1].get(val, default) if v_r is default: return v_l return default
[docs] class SetDifference_FiniteSet(_FiniteSetMixin, SetDifference_InfiniteSet): __slots__ = tuple() def _iter_impl(self): set0, set1 = self._sets return (_ for _ in set0 if _ not in set1) def __len__(self): """ Return the number of elements in the set. """ return sum(1 for _ in self)
[docs] class SetDifference_OrderedSet( _ScalarOrderedSetMixin, _OrderedSetMixin, SetDifference_FiniteSet ): __slots__ = tuple() def at(self, index): idx = self._to_0_based_index(index) _iter = iter(self) try: while idx: next(_iter) idx -= 1 return next(_iter) except StopIteration: raise IndexError(f"{self.name} index out of range") from None
[docs] def ord(self, item): """ Return the position index of the input value. Note that Pyomo Set objects have positions starting at 1 (not 0). If the search item is not in the Set, then an IndexError is raised. """ if item not in self: raise IndexError( "Cannot identify position of %s in Set %s: item not in Set" % (item, self.name) ) idx = 0 _iter = iter(self) while next(_iter) != item: idx += 1 return idx + 1
############################################################################
[docs] class SetSymmetricDifference(SetOperator): __slots__ = tuple() _operator = " ^ " def __new__(cls, *args): if cls != SetSymmetricDifference: return super(SetSymmetricDifference, cls).__new__(cls) set0, set1 = SetOperator._checkArgs(*args) if set0[0] and set1[0]: cls = SetSymmetricDifference_OrderedSet elif set0[1] and set1[1]: cls = SetSymmetricDifference_FiniteSet else: cls = SetSymmetricDifference_InfiniteSet return cls.__new__(cls) def ranges(self): # Note: the following loop implements for (a,b), (b,a) assert len(self._sets) == 2 for set_a, set_b in (self._sets, reversed(self._sets)): for a_r in set_a.ranges(): yield from a_r.range_difference(set_b.ranges()) @property def dimen(self): d0 = self._sets[0].dimen d1 = self._sets[1].dimen if d0 is None or d1 is None: return None if d0 is UnknownSetDimen or d1 is UnknownSetDimen: return UnknownSetDimen if d0 == d1: return d0 else: return None
[docs] class SetSymmetricDifference_InfiniteSet(SetSymmetricDifference): __slots__ = tuple() def get(self, val, default=None): # return (val in self._sets[0]) ^ (val in self._sets[1]) v_l = self._sets[0].get(val, default) v_r = self._sets[1].get(val, default) if v_l is default: return v_r if v_r is default: return v_l return default
[docs] class SetSymmetricDifference_FiniteSet( _FiniteSetMixin, SetSymmetricDifference_InfiniteSet ): __slots__ = tuple() def _iter_impl(self): set0, set1 = self._sets return itertools.chain( (_ for _ in set0 if _ not in set1), (_ for _ in set1 if _ not in set0) ) def __len__(self): """ Return the number of elements in the set. """ return sum(1 for _ in self)
[docs] class SetSymmetricDifference_OrderedSet( _ScalarOrderedSetMixin, _OrderedSetMixin, SetSymmetricDifference_FiniteSet ): __slots__ = tuple() def at(self, index): idx = self._to_0_based_index(index) _iter = iter(self) try: while idx: next(_iter) idx -= 1 return next(_iter) except StopIteration: raise IndexError(f"{self.name} index out of range") from None
[docs] def ord(self, item): """ Return the position index of the input value. Note that Pyomo Set objects have positions starting at 1 (not 0). If the search item is not in the Set, then an IndexError is raised. """ if item not in self: raise IndexError( "Cannot identify position of %s in Set %s: item not in Set" % (item, self.name) ) idx = 0 _iter = iter(self) while next(_iter) != item: idx += 1 return idx + 1
############################################################################
[docs] class SetProduct(SetOperator): __slots__ = tuple() _operator = "*" def __new__(cls, *args): if cls != SetProduct: return super(SetProduct, cls).__new__(cls) _sets = SetOperator._checkArgs(*args) if all(_[0] for _ in _sets): cls = SetProduct_OrderedSet elif all(_[1] for _ in _sets): cls = SetProduct_FiniteSet else: cls = SetProduct_InfiniteSet return cls.__new__(cls) def ranges(self): yield RangeProduct(list(list(_.ranges()) for _ in self.subsets(False))) def bounds(self): lb, ub = zip(*map(lambda x: x.bounds(), self.subsets(False))) return lb, ub @property def dimen(self): if not (FLATTEN_CROSS_PRODUCT and normalize_index.flatten): return len(self._sets) # By convention, "None" trumps UnknownSetDimen. That is, a set # product is "non-dimentioned" if any term is non-dimentioned, # even if we do not yet know the dimentionality of another term. ans = 0 _unknown = False for s in self._sets: s_dim = s.dimen if s_dim is None: return None elif s_dim is UnknownSetDimen: _unknown = True else: ans += s_dim return UnknownSetDimen if _unknown else ans def _flatten_product(self, val): """Flatten any nested set product terms (due to nested products) Note that because this is called in a recursive context, this method is assured that there is no more than a single level of nested tuples (so this only needs to check the top-level terms) """ for i in range(len(val) - 1, -1, -1): if val[i].__class__ is tuple: val = val[:i] + val[i] + val[i + 1 :] return val
[docs] class SetProduct_InfiniteSet(SetProduct): __slots__ = tuple() def get(self, val, default=None): # return self._find_val(val) is not None v = self._find_val(val) if v is None: return default if normalize_index.flatten: return self._flatten_product(v[0]) return v[0] def _find_val(self, val): """Locate a value in this SetProduct Locate a value in this SetProduct. Returns None if the value is not found, otherwise returns a (value, cutpoints) tuple. Value is the value that was searched for, possibly normalized. Cutpoints is the set of indices that specify how to split the value into the corresponding subsets such that subset[i] = cutpoints[i:i+1]. Cutpoints is None if the value is trivially split with a single index for each subset. Returns ------- val: tuple cutpoints: list """ # Support for ambiguous cross products: if val matches the # number of subsets, we will start by checking each value # against the corresponding subset. Failure is not sufficient # to determine the val is not in this set. if hasattr(val, '__len__') and len(val) == len(self._sets): if all(v in self._sets[i] for i, v in enumerate(val)): return val, None # If we are not normalizing indices, then if the above did not # match, we will NOT attempt to guess how to split the indices if not normalize_index.flatten: return None val = normalize_index(val) if val.__class__ is tuple: v_len = len(val) else: val = (val,) v_len = 1 # Get the dimentionality of all the component sets setDims = list(s.dimen for s in self._sets) # For this search, if a subset has an unknown dimension, assume # it is "None". for i, d in enumerate(setDims): if d is UnknownSetDimen: setDims[i] = None # Find the starting index for each subset (based on dimentionality) index = [None] * len(setDims) lastIndex = 0 for i, dim in enumerate(setDims): index[i] = lastIndex if dim is None: firstNonDimSet = i break lastIndex += dim # We can also check for this subset member immediately. # Non-membership is sufficient to return "not found" if lastIndex > v_len: return None elif val[index[i] : lastIndex] not in self._sets[i]: return None # The end of the last subset is always the length of the val index.append(v_len) # If there were no non-dimentioned sets, then we have checked # each subset, found a match, and can reach a verdict: if None not in setDims: if lastIndex == v_len: return val, index else: return None # If a subset is non-dimentioned, then we will have broken out # of the forward loop early. Start at the end and work # backwards. lastIndex = index[-1] for iEnd, dim in enumerate(reversed(setDims)): i = len(setDims) - (iEnd + 1) if dim is None: lastNonDimSet = i break lastIndex -= dim index[i] = lastIndex # We can also check for this subset member immediately. # Non-membership is sufficient to return "not found" if val[index[i] : index[i + 1]] not in self._sets[i]: return None if firstNonDimSet == lastNonDimSet: # We have inferred the subpart of val that must be in the # (single) non-dimentioned subset. Check membership and # return the final verdict. if ( val[index[firstNonDimSet] : index[firstNonDimSet + 1]] in self._sets[firstNonDimSet] ): return val, index else: return None # There were multiple subsets with dimen==None. The only thing # we can do at this point is to search for any possible # combination that works subsets = self._sets[firstNonDimSet : lastNonDimSet + 1] _val = val[index[firstNonDimSet] : index[lastNonDimSet + 1]] for cuts in self._cutPointGenerator(subsets, len(_val)): if all(_val[cuts[i] : cuts[i + 1]] in s for i, s in enumerate(subsets)): offset = index[firstNonDimSet] for i in range(1, len(subsets)): index[firstNonDimSet + i] = offset + cuts[i] return val, index return None @staticmethod def _cutPointGenerator(subsets, val_len): """Generate the sequence of cut points for a series of subsets. This generator produces the valid set of cut points for separating a list of length val_len into chunks that are valid for the specified subsets. In this method, the first and last subsets must have dimen==None. The return value is a list with length one greater that then number of subsets. Value slices (for membership tests) are determined by cuts[i]:cuts[i+1] in subsets[i] """ setDims = list(_.dimen for _ in subsets) cutIters = [None] * (len(subsets) + 1) cutPoints = [0] * (len(subsets) + 1) i = 1 cutIters[i] = iter(range(val_len + 1)) cutPoints[-1] = val_len while i > 0: try: cutPoints[i] = next(cutIters[i]) if i < len(subsets) - 1: if setDims[i] is not None: cutIters[i + 1] = iter((cutPoints[i] + setDims[i],)) else: cutIters[i + 1] = iter(range(cutPoints[i], val_len + 1)) i += 1 elif cutPoints[i] > val_len: i -= 1 else: yield cutPoints except StopIteration: i -= 1
[docs] class SetProduct_FiniteSet(_FiniteSetMixin, SetProduct_InfiniteSet): __slots__ = tuple() def _iter_impl(self): _iter = itertools.product(*self._sets) # Note: if all the member sets are simple 1-d sets, then there # is no need to call flatten_product. if ( FLATTEN_CROSS_PRODUCT and normalize_index.flatten and self.dimen != len(self._sets) ): return (self._flatten_product(_) for _ in _iter) return _iter def __len__(self): """ Return the number of elements in the set. """ ans = 1 for s in self._sets: ans *= max(0, len(s)) return ans
[docs] class SetProduct_OrderedSet( _ScalarOrderedSetMixin, _OrderedSetMixin, SetProduct_FiniteSet ): __slots__ = tuple() def at(self, index): _idx = self._to_0_based_index(index) _ord = list(len(_) for _ in self._sets) i = len(_ord) while i: i -= 1 _ord[i], _idx = _idx % _ord[i], _idx // _ord[i] if _idx: raise IndexError(f"{self.name} index out of range") ans = tuple(s.at(i + 1) for s, i in zip(self._sets, _ord)) if FLATTEN_CROSS_PRODUCT and normalize_index.flatten and self.dimen != len(ans): return self._flatten_product(ans) return ans
[docs] def ord(self, item): """ Return the position index of the input value. Note that Pyomo Set objects have positions starting at 1 (not 0). If the search item is not in the Set, then an IndexError is raised. """ found = self._find_val(item) if found is None: raise IndexError( "Cannot identify position of %s in Set %s: item not in Set" % (item, self.name) ) val, cutPoints = found if cutPoints is not None: val = tuple( val[cutPoints[i] : cutPoints[i + 1]] for i in range(len(self._sets)) ) _idx = tuple(s.ord(val[i]) - 1 for i, s in enumerate(self._sets)) _len = list(len(_) for _ in self._sets) _len.append(1) ans = 0 for pos, n in zip(_idx, _len[1:]): ans += pos ans *= n return ans + 1
############################################################################ class _AnySet(SetData, Set): def __init__(self, **kwds): SetData.__init__(self, component=self) # There is a chicken-and-egg game here: the SetInitializer uses # Any as part of the processing of the domain/within/bounds # domain restrictions. However, Any has not been declared when # constructing Any, so we need to bypass that logic. This # works, but requires us to declare a special domain setter to # accept (and ignore) this value. kwds.setdefault('domain', self) Set.__init__(self, **kwds) self.construct() def get(self, val, default=None): return val if val is not Ellipsis else default def ranges(self): yield AnyRange() def bounds(self): return (None, None) # We need to implement this to override the clear() from IndexedComponent def clear(self): return # We need to implement this to override __len__ from IndexedComponent def __len__(self): raise TypeError("object of type 'Any' has no len()") @property def dimen(self): return None @property def domain(self): return Any def __str__(self): if self._name is not None: return self.name return type(self).__name__ class _AnyWithNoneSet(_AnySet): # Note that we put the deprecation warning on contains() and not on # the class because we will always create a global instance for # backwards compatibility with the Book. @deprecated( "The AnyWithNone set is deprecated. Use Any, which includes None", version='5.7', ) def get(self, val, default=None): return super(_AnyWithNoneSet, self).get(val, default) class _EmptySet(_FiniteSetMixin, SetData, Set): def __init__(self, **kwds): SetData.__init__(self, component=self) Set.__init__(self, **kwds) self.construct() def get(self, val, default=None): return default # We need to implement this to override clear from IndexedComponent def clear(self): pass # We need to implement this to override __len__ from IndexedComponent def __len__(self): return 0 def _iter_impl(self): return iter(tuple()) @property def dimen(self): return 0 @property def domain(self): return EmptySet def __str__(self): if self._name is not None: return self.name return type(self).__name__ ############################################################################
[docs] def DeclareGlobalSet(obj, caller_globals=None): """Declare a copy of a set as a global set in the calling module This takes a Set object and declares a duplicate of it as a GlobalSet object in the global namespace of the caller's module using the local name of the passed set. GlobalSet objects are pseudo-singletons, in that copy.deepcopy (and Model.clone()) will not duplcicate them, and when you pickle and restore objects containing GlobalSets will still refer to the same object. The declared GlobalSet object will be an instance of the original Set type. """ obj.construct() assert obj.parent_component() is obj assert obj.parent_block() is None # Build the global set before registering its name so that we don't # run afoul of the logic in GlobalSet.__new__ _name = obj.local_name if _name in GlobalSets and obj is not GlobalSets[_name]: raise RuntimeError("Duplicate Global Set declaration, %s" % (_name,)) # Push this object into the caller's module namespace # Stack: 0: DeclareGlobalSet() # 1: the caller if caller_globals is None: caller_globals = inspect.currentframe().f_back.f_globals if _name in caller_globals and obj is not caller_globals[_name]: raise RuntimeError("Refusing to overwrite global object, %s" % (_name,)) if _name in GlobalSets: _set = caller_globals[_name] = GlobalSets[_name] return _set # Handle duplicate registrations before defining the GlobalSet # object to avoid inconsistent MRO order. class GlobalSet(GlobalSetBase, obj.__class__): __doc__ = """%s References to this object will not be duplicated by deepcopy and be maintained/restored by pickle. """ % ( obj.doc, ) # Note: a simple docstring does not appear to be picked up (at # least in Python 2.7), so we will explicitly set the __doc__ # attribute. __slots__ = ('_bounds', '_interval') global_name = None def __new__(cls, *args, **kwds): """Hijack __new__ to mock up old RealSet el al. interface In the original Set implementation (Pyomo<=5.6.7), the global sets were instances of their own virtual set classes (RealSet, IntegerSet, BooleanSet), and one could create new instances of those sets with modified bounds. Since the GlobalSet mechanism also declares new classes for every GlobalSet, we can mock up the old behavior through how we handle __new__(). """ if ( cls is GlobalSet and GlobalSet.global_name and issubclass(GlobalSet, RangeSet) ): deprecation_warning( "The use of RealSet, IntegerSet, BinarySet and " "BooleanSet as Pyomo Set class generators is " "deprecated. Please either use one of the pre-declared " "global Sets (e.g., Reals, NonNegativeReals, Integers, " "PositiveIntegers, Binary), or create a custom RangeSet.", version='5.7.1', ) # Note: we will completely ignore any positional # arguments. In this situation, these could be the # parent_block and any indices; e.g., # Var(m.I, within=RealSet) base_set = GlobalSets[GlobalSet.global_name] bounds = kwds.pop('bounds', None) range_init = SetInitializer(base_set) if bounds is not None: range_init.intersect(BoundsInitializer(bounds)) name = name_kwd = kwds.pop('name', None) cls_name = kwds.pop('class_name', None) if name is None: if cls_name is None: name = base_set.name else: name = cls_name tmp = Set() ans = RangeSet( ranges=list(range_init(None, None, tmp).ranges()), name=name ) ans._anonymous_sets = tmp._anonymous_sets if name_kwd is None and (cls_name is not None or bounds is not None): ans._name += str(ans.bounds()) else: ans = super(GlobalSet, cls).__new__(cls, *args, **kwds) if kwds: raise RuntimeError("Unexpected keyword arguments: %s" % (kwds,)) return ans # # Global sets are assumed to be constant sets. For performance, # we will precompute and cache the Set bounds() and interval # def bounds(self): return self._bounds def get_interval(self): return self._interval _set = GlobalSet() # TODO: Can GlobalSets be a proper Block? GlobalSets[_name] = caller_globals[_name] = _set GlobalSet.global_name = _name _set.__class__.__setstate__(_set, obj.__getstate__()) _set._component = weakref.ref(_set) _set.construct() # Cache the set bounds / interval _set._bounds = obj.bounds() _set._interval = obj.get_interval() # Now that the set is constructed, override the _anonymous_sets to # mark the set as a global set (used by process_setarg) _set._anonymous_sets = GlobalSetBase return _set
DeclareGlobalSet( _AnySet(name='Any', doc="A global Pyomo Set that admits any value"), globals() ) DeclareGlobalSet( _AnyWithNoneSet(name='AnyWithNone', doc="A global Pyomo Set that admits any value"), globals(), ) DeclareGlobalSet( _EmptySet(name='EmptySet', doc="A global Pyomo Set that contains no members"), globals(), ) DeclareGlobalSet( RangeSet( name='Reals', doc='A global Pyomo Set that admits any real (floating point) value', ranges=(NumericRange(None, None, 0),), ), globals(), ) DeclareGlobalSet( RangeSet( name='NonNegativeReals', doc='A global Pyomo Set admitting any real value in [0, +inf]', ranges=(NumericRange(0, None, 0),), ), globals(), ) DeclareGlobalSet( RangeSet( name='NonPositiveReals', doc='A global Pyomo Set admitting any real value in [-inf, 0]', ranges=(NumericRange(None, 0, 0),), ), globals(), ) DeclareGlobalSet( RangeSet( name='NegativeReals', doc='A global Pyomo Set admitting any real value in [-inf, 0)', ranges=(NumericRange(None, 0, 0, (True, False)),), ), globals(), ) DeclareGlobalSet( RangeSet( name='PositiveReals', doc='A global Pyomo Set admitting any real value in (0, +inf]', ranges=(NumericRange(0, None, 0, (False, True)),), ), globals(), ) DeclareGlobalSet( RangeSet( name='Integers', doc='A global Pyomo Set admitting any integer value', ranges=(NumericRange(0, None, 1), NumericRange(0, None, -1)), ), globals(), ) DeclareGlobalSet( RangeSet( name='NonNegativeIntegers', doc='A global Pyomo Set admitting any integer value in [0, +inf]', ranges=(NumericRange(0, None, 1),), ), globals(), ) DeclareGlobalSet( RangeSet( name='NonPositiveIntegers', doc='A global Pyomo Set admitting any integer value in [-inf, 0]', ranges=(NumericRange(0, None, -1),), ), globals(), ) DeclareGlobalSet( RangeSet( name='NegativeIntegers', doc='A global Pyomo Set admitting any integer value in [-inf, -1]', ranges=(NumericRange(-1, None, -1),), ), globals(), ) DeclareGlobalSet( RangeSet( name='PositiveIntegers', doc='A global Pyomo Set admitting any integer value in [1, +inf]', ranges=(NumericRange(1, None, 1),), ), globals(), ) DeclareGlobalSet( RangeSet( name='Binary', doc='A global Pyomo Set admitting the integers {0, 1}', ranges=(NumericRange(0, 1, 1),), ), globals(), ) # TODO: Convert Boolean from an alias for Binary to a proper Boolean Set # admitting {True, False}) DeclareGlobalSet( RangeSet( name='Boolean', doc='A global Pyomo Set admitting the integers {0, 1}', ranges=(NumericRange(0, 1, 1),), ), globals(), ) DeclareGlobalSet( RangeSet( name='PercentFraction', doc='A global Pyomo Set admitting any real value in [0, 1]', ranges=(NumericRange(0, 1, 0),), ), globals(), ) DeclareGlobalSet( RangeSet( name='UnitInterval', doc='A global Pyomo Set admitting any real value in [0, 1]', ranges=(NumericRange(0, 1, 0),), ), globals(), ) # DeclareGlobalSet(Set( # initialize=[None], # name='UnindexedComponent_set', # doc='A global Pyomo Set for unindexed (scalar) IndexedComponent objects', # ), globals()) real_global_set_ids = set( id(_) for _ in ( Reals, NonNegativeReals, NonPositiveReals, NegativeReals, PositiveReals, PercentFraction, UnitInterval, ) ) integer_global_set_ids = set( id(_) for _ in ( Integers, NonNegativeIntegers, NonPositiveIntegers, NegativeIntegers, PositiveIntegers, Binary, ) ) RealSet = Reals.__class__ IntegerSet = Integers.__class__ BinarySet = Binary.__class__ BooleanSet = Boolean.__class__ # # Backwards compatibility: declare the RealInterval and IntegerInterval # classes (leveraging the new global RangeSet objects) #
[docs] @deprecated( "RealInterval has been deprecated. Please use RangeSet(lower, upper, 0)", version='5.7', ) class RealInterval(RealSet): def __new__(cls, **kwds): kwds.setdefault('class_name', 'RealInterval') return super(RealInterval, cls).__new__(RealSet, **kwds)
[docs] @deprecated( "IntegerInterval has been deprecated. Please use RangeSet(lower, upper, 1)", version='5.7', ) class IntegerInterval(IntegerSet): def __new__(cls, **kwds): kwds.setdefault('class_name', 'IntegerInterval') return super(IntegerInterval, cls).__new__(IntegerSet, **kwds)