Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PEP] Redesign Pyomo Set component #326

Closed
16 of 18 tasks
jsiirola opened this issue Jan 30, 2018 · 7 comments · Fixed by #1111
Closed
16 of 18 tasks

[PEP] Redesign Pyomo Set component #326

jsiirola opened this issue Jan 30, 2018 · 7 comments · Fixed by #1111

Comments

@jsiirola
Copy link
Member

jsiirola commented Jan 30, 2018

Proposal

This PEP proposes a partial redesign of the Set component. The goal is to be API-compatible with the current Set components, while resolving a large number of bug reports. The draft redesign is currently available on jsiirola/set-rewrite

Motivation

We have started accumulating a number of documented issues with the current implementation of Set components. Known Set issues:

Discussion

The current Set design has a number of bugs and limitations. Some particularly problematic issues include:

  • Set does not follow the typical Pyomo Component class hierarchy.
  • IndexedSet members cannot by used in the same ways as scalar Set objects, as certain methods were implemented on the Set object and not the _SetData object
  • Set operators do not preserve flags like ordered or sorted
  • Users are responsible for knowing how to (efficiently, reliably) sort/order set members
  • Support for non-discrete sets is ad hoc, and there is no support for semi-continuous ranges

Range objects

This redesign first proposes adding full support for discrete and continuous ranges. This is done through four fundamental "range" objects:

  • NumericRange for representing a contiguous continuous (closed or open) or discrete (closed) range
  • NonNumericRange for representing a single discrete non-numeric value
  • AnyRange for representing the special "Any" range
  • RangeProduct for representing cross products of ranges

Note that range objects are NOT Set objects, although they implement a subset of a Set-like API:

  • __contains__: return True if a value is within the range
  • __eq__: return True if two ranges are equal
  • isdiscrete(): return True if the range is over discrete values
  • isfinite(): return True if the range is over a finite set of discrete values (isfinite() == True implies isdiscrete() == True)
  • isdisjoint(other): return True if the range is completely disjoint from the other range.
  • issubset(other): return True if the range is contained within (equal to or strict subset of) the other range.
  • range_difference(other_ranges): return a list of ranges resulting from subtracting all of the other_ranges from this range
  • range_intersection(other_ranges): return a list of ranges resulting from intersecting this range with all of the other_ranges.

Numeric ranges are defined with a start, end, and step values, along with closed (a 2-tuple of True/False). Continuous ranges are indicated by step == 0. start and end can both contain either a floating point value or None. start must be less than or equal to end. closed[0] and closed[1] indicate if start and end (respectively) are included in the range. Discrete ranges are indicated by step taking a non-zero signed integer. Discrete ranges must be closed, thus closed == (True, True). The discrete range is "grounded" by start (which must be an integer and cannot be None) and proceeding up to and including end (which may be None). (end - start) % step is guaranteed to be 0. If start == end, then step == 0 (by convention).

NonNumericRange and AnyRange are included to support Set operations between discrete or Any Sets and range-based Sets. NonNumericRange is a bit of a misnomer, as it does not contain a "range", but rather a single non-numeric value. Sets of non-numeric values are represented by lists of NonNumericRange objects.

RangeProduct represents the cross product of two ranges and is needed to provide complete support for Set operations (Set operations other than cross product are simple ranges and can be represented by lists of NumericRange and NonNumericRange objects).

Ranges must support "correct" set arithmetic (difference and intersection), or raise an exception. That is, operations like

NumericRange(start=0, end=20, step=2).range_difference([NumericRange(0, 18, 3)])

should yield

[NumericRange(2,20,6), NumericRange(4,16,6)]

The current draft implementation supports everything except unbounded continuous range minus unbounded discrete range (which raises an exception). The Any range is handled a bit like "infinity", in that

AnyRange().range_difference([AnyRange]) == []

and

AnyRange().range_difference([NumericRange(0,20,0)]) == [AnyRange]

It is possible that we may desire the latter case to raise an exception.

RangeSet objects

RangeSet has been significantly expanded over the previous RangeSet implementation.

RangeSet objects are now based around NumericRange objects, which includes support for non-finite ranges (both continuous and unbounded). Similarly, boutique ranges (like semi-continuous domains) can be represented, e.g.:

RangeSet(ranges=(NumericRange(0,0,0), NumericRange(1,100,0)))

The RangeSet object continues to support the old notation for specifying discrete ranges:

RangeSet(3)          # [1, 2, 3]
RangeSet(2,5)        # [2, 3, 4, 5]
RangeSet(2,5,2)      # [2, 4]
RangeSet(2.5,4,0.5)  # [2.5, 3, 3.5, 4]

By implementing RangeSet using NumericRanges, the global Sets (like Reals, Integers, PositiveReals, etc.) are now trivial instances of a RangeSet and support all Set operations. A consequence of this is that it may impact the implementation of Kernel. Kernel currently relies on class inheritance to determine domains (i.e., isinstance of RealSet, IntegerSet, BooleanSet, etc). As this new approach to the global sets makes use of the RangeSet object for domains, this would pose two problems for Kernel:

  1. You cannot determine domain through inheritance.
    • As an alternative, Set provides isdiscrete() to determine if the set is completely discrete. Continuous real ranges can be verified be extracting the ranges() from the RangeSet and verifying that they are all not isdiscrete() and that they are not isdisjoint.
    • Alternatively, you could verify continuous real ranges by computing Reals - domain and seeing if the resulting Set only contained unbounded ranges (all(None in r.bounds() for r in (Reals - domain).ranges()))
  2. The Kernel would not be independent of the AML, as the global domain sets are instances of RangeSet, which is a derivative of the Set component.

Given @ghackebeil's comment below, it is possible that Kernel could operate completely on Set APIs (isdiscrete() and bounds()) and not have to explicitly import anything from the AML. We could also mitigate compatibility concerns by extending the definition of DeclareGlobalSet to also support adding additional base classes (like an abstract RealSet class). The abstract base classes could be defined in Kernel, thereby avoiding a circular import between Kernel and AML.

SetData 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 _SetDataBase(ComponentData) (pure virtual interface)
    • (no methods or data; it is a purely 'abstract' class for backwards compatibility)
  2. class _SetData(_SetDataBase) (base class for all AML Sets)
    • __contains__: [abstract] tests for set membership
    • __eq__: tests for set equivalence
    • __ne__: tests for set equivalence
    • __str__: [abstract] returns a string representation of the set (name or values)
    • dimen: [abstract] read-only property that returns the dimentionality of all set members (or None if the members are not required to have a common dimentionality.
    • isfinite(): [NEW] True if the set contains a countable number of members
    • isordered(): [NEW] True if the set storage has a (deterministic) ordering
    • ranges(): [NEW][abstract] generator yielding range objects that define the set
    • isdisjoint(other): True if self and other are disjoint sets
    • issubset(other): True if self is a subset of (<=) other. From python's set
    • issuperset(other): True if self is a superset of (>=) other. From python's set
    • virtual [DEPRECATED] False for ass Set objects except SetOperator derivatives
    • concrete [DEPRECATED] returns isfinite()
    • union(*others): Returns a SetOperator that represents the union of self and other sets
    • intersection(*others): Returns a SetOperator that represents the intersection of self and other sets
    • difference(*others): Returns a SetOperator that represents the difference of self and other sets
    • symmetric_difference(*others): Returns a SetOperator that represents the symmetric difference of self and other sets
    • cross(*others): Returns a SetOperator that represents the cross product of self and other sets
    • __le__ = issubset
    • __ge__ = issuperset
    • __or__ = union
    • __and__ = intersection
    • __sub__ = difference
    • __xor__ = symmetric_difference
    • __mul__ = cross
    • __ror__, __rand__, __rsub__, __rxor__, __rmul__
    • __lt__: Returns True if self is a strict subset of other
    • __gt__: Returns True if self is a strict superset of other
  3. class _FiniteSetMixin(object) (pure virtual interface, adds support for discrete/iterable sets)
    • __len__: [abstract] Returns the number of elements in the Set
    • __iter__: [abstract] Returns an iterator over the Set members
    • __reversed__: Returns a reversed iterator over the Set members
    • data(): [MODIFIED] Return all Set members as a tuple. NOTE: previous implementation returned a set() or list. This implementation proposes always returning a tuple in the same order as __iter__
    • ordered_data() [NEW] return the set members in a deterministic order (the internal order if isordered(), otherwise sorted using sorted_robust)
    • sorted_data() [NEW] return the set members sorted (using the internal sorting for sorted sets, otherwise the results of sorted_robust())
    • bounds(): returns a tuple representing the current bounds of the Set (based on it's current members/definition). Note that incomparable or mixed-type sets will return (None,None)
  4. class _FiniteSetData(_FiniteSetMixin, _SetData) (data class for finite discrete sets; equivalent to a python set)
    • add(): Add an element to this finite set
    • remove(): Remove an element from the set, raise an exception if the element is not found
    • discard(): Remove an element from the set, silently return if the item is not found
    • clear(): Remove all elements from this set
    • set_value(): Set the members of the set, discarding any previously-stored elements
    • update(): [NEW] Update the set by adding members from the passed iterable
    • pop(): [NEW] Remove one (random) element from the set
  5. _OrderedSetMixin(object) (pure virtual interface, adds support for ordered Sets)
    • first(): The first element of the Set
    • last(): The last element of the Set
    • next(x): The element after x, raising an exception after last()
    • nextw(x): The element after x, returning first() after last()
    • prev(x): The element before x, raising an exception after first()
    • prevw(x): The element before x, returning last() after first()
    • __getitem__: [abstract] Retrieve the nth element (1-based)
    • ord(x): [abstract] Return the (1-based) index of x
  6. class _OrderedSetData(_OrderedSetMixin, _FiniteSetData) (data class for ordered finite discrete sets)

This is a bit of a change from the current 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 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. (No changes from the previous API)

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

Behavioral changes / implementation notes

  • Sets preserve the same determinism properties of the current Set implementation: iterating over a Set with isordered() == False will not produce a deterministic ordering. However, if the client wants to ensure deterministic ordering, the Set API now provides ordered_data() and sorted_data(). ordered_data() is guaranteed to produce a deterministic ordering for all sets and sorted_data() will produce a sorted ordering (either using the Set's internal sorting order or the results of sorted_robust() as necessary). Using these accessors is preferable to relying on the client to perform the sorting as the Set methods will only perform additional sorting if it is necessary. That is, tuple(__iter__) == ordered_data() for Sets where isordered() == True, without performing any additional sorting.

    • Unless otherwise specified, Sets default to being ordered using insertion order.
  • Casting a Set to a string has been extended. Standard components (and old Set objects) return their fully-qualified names when cast to a string (through __str__()). If the object had not yet been attached to a Block (and didn't have a name specified when constructed), then the name of the Component class is returned. This PEP proposes changing the behavior for Set-like objects: when the object is not assigned to a Block, then __str__() returns the contents of the set (for set-like objects), the range representation (for range objects) or a string representation of the operation (for _SetOperation objects). The intent of this change is that Sets (and Set-like things) that the user explicitly attached to a Block will return the name that the user gave them. However, implicitly-created objects (like _SetOperator objects and implicit Sets) will be "expanded". For example, note the Var Index= field:

>>> m = ConcreteModel()
>>> m.I = Set(initialize=[1,2])
>>> m.x = Var(m.I * [3,4])
>>> m.pprint()
1 Set Declarations
    I : Dim=0, Size=1, Ordered=True, Sorted=False
        Key  : Dimen : Domain : Members
        None :     1 :    Any : {1, 2}

1 Var Declarations
    x : Size=4, Index=I*{3, 4}
        Key    : Lower : Value : Upper : Fixed : Stale : Domain
        (1, 3) :  None :  None :  None : False :  True :  Reals
        (1, 4) :  None :  None :  None : False :  True :  Reals
        (2, 3) :  None :  None :  None : False :  True :  Reals
        (2, 4) :  None :  None :  None : False :  True :  Reals

2 Declarations: I x
  • TODO Should the behavior of implicit indices be changed? Currently, implicitly declared indices (e.g., Var([1,2])) are created as first-class Set objects with Set(initialize=[1,2]). This is consistent with the previous implementation. However, this approach has several drawbacks:

    • The representation of the set appears to change to the user. For example, print(m.I | [3,4]) will give I | {3, 4}.
    • This causes unresolvable circular dependencies between Set and IndexedComponent files: IndexedComponent needs access to a method that can process the positional arguments and return Set(-like) objects for any implicit sets, however, Set is derived from IndexedComponent.
      The advantage of the current approach is that users are less likely to be "bitten" by reference counted objects, e.g.:
      a = [1,2,3]
      m.x = Var(a)
      a.append(4)
      m.y = Var(a)
      m.x[4] # == VALID!

    The current reference implementation for the PEP preserves previous behavior and creates implicit Sets using Set(). The leading alternative is to use SetOf(). We could mitigate the concerns for multiple implicit Sets referring to the same underlying object through a global (weakref) registry of implicitly-declares SetOf() objects, and issuing a warning any time a duplicate is encountered.

  • Set operators now preserve the "finiteness" and "orderedness" of the underlying Sets (as appropriate). This allows all Set operations to be performed on infinite Sets. In particular, Var(Any, m.I) is now valid, as is Var(Integers, m.I). IndexedComponent constructors should test isfinite() before iterating over the component's index_set() during construction.

  • The Set() __init__() and construct() make use of a new general system for processing arguments, based around the general functions Initializer() and SetInitializer(). This centralizes much of the argument processing (e.g., handling of scalars, dictionaries, lists, functions/rules, generators, etc). My intent is to eventually propagate this to all IndexedComponents (in a later PEP). A preliminary implementation for Var is promising, with little/no impact on construction time.

  • While the new implementation is far more robust and consistent compared to the current implementation, it comes at a significant increases in the number of classes. For example, there are now 3 "Simple" Set classes (FiniteSimpleSet, OrderedSimpleSet, and SortedSimpleSet). Further, SetOf requires 2 implementations (UnorderedSetOf and OrderedSetOf), and each Set operator requires 4 classes (e.g., SetUnion, SetUnion_InfiniteSet, SetUnion_FiniteSet, and SetUnion_OrderedSet)

  • This implementation provides a new approach for declaring "global" Sets (like Reals and Integers) that will correctly survive pickling and deepcopying (resolving open issues around using is for domain checks on cloned models).

  • TODO we should determine if implicit sets (e.g., created by set operations or implicit casting of iterable objects into sets) should be added to the Model. The current behavior of Block.add_component() is to add these implicit sets to the model with hard-coded names. This has caused issues for users, especially when deleting and re-adding components (see del_component issue when deleting constraints #45). Not adding these Sets to the model would resolve that issue, although we would need to be careful how/when the implicit sets get constructed (part would have to be handled by the Set construct() method.

  • How should we handle type casting in Sets?

    • Should 1 in Reals == True (Python thinks so: 1 in {1.0} == True)
    • Should 1. in Integers == True? (Python thinks so: 1.0 in {1} == True)
    • How should we handle item insertion? Should we cast the value into the domain type? Currently we don't; e.g.,
      >>> m.I = Set(domain=Integers)
      >>> m.I.add(1.0)
      >>> print(list(m.I))
      [1.0]
      >>> 1 in m.I
      True
      >>> 1.0 in m.I
      True
    • What about mixed-type non-disjoint domains (e.g., RangeSet(10) | RangeSet(5,20,0)?
  • How should we handle 1-tuples? Conceptually, we could think of all indices as tuples (and the primary role for Set objects is as index domains). That said, for efficiency reasons, we store 1-dimensional indices as bare values (that is, we unpack 1-tuples to be proper scalars). The original argument was that the scalars are more memory efficient and are faster to generate and manipulate. However, they also cause us to do strange things, like trap for non-tuples when firing rules, e.g.:

    if type(idx) is tuple:
        rule(self.parent, *idx)
    else:
        rule(self.parent, idx)

    It gets more confusing when we start doing set operations like SetProduct with non-dimensional sets (i.e., with dimen=None, so we have to calculate / determine / guess how to subdivide longer tuples into component indices). In addition, there is overlap in the meaning of dimen:

    Set(initialize=[1,2,3]).dimen == 1
    Set(initialize=[(1,), (2,), (3,)]).dimen == 1

    The draft implementation performs index normalization on set members, including unpacking all 1-tuples to scalars. This makes the internal storage more consistent and simplifies member lookup, guaranting that a Set will never contain a 1-tuple. However, this index normalization can be turned off (enabling both the storage of 1-tuples and nested tuples) by setting:

    pyomo.core.base.indexed_component.normalize_index.flatten = False
  • Change: The previous Set implementation only applies the filter function to the data provided at construction time (from initialize or the external data source). This PEP proposes a more consistent behavior and applies the filter any time data is added to the Set (even after construction).

Specific topics requesting discussion / comment

  1. Should we map all 1-tuples to scalars for improved consistency?
    • This PEP is proposing YES.
  2. Should we cast incoming Set members to the domain type?
    • This PEP is proposing NO.
  3. Should we change implicit sets from instances of Set to instances of SetOf?
    • This PEP is proposing NO.
  4. Should implicitly-created Sets (either through Set operators or implicitly casted from iterables) be added to the Model?
    • This PEP proposes NO.
  5. Should we allow RangeSet objects with floating point steps?
    • This PEP is proposing YES (but not for unbounded ranges)
  6. Should we deprecate virtual and concrete?
    • This PEP proposes YES.
  7. Should Set's data(), ordered_data(), and sorted_data() return generators?
    • This PEP is proposing NO.
@jsiirola
Copy link
Member Author

Note that the rewrite is currently underway on set-rewrite on the jsiirola/pyomo fork.

@blnicho
Copy link
Member

blnicho commented Jun 12, 2019

My thoughts:

  1. Should we map all 1-tuples to scalars for improved consistency? yes
  2. Should we cast incoming Set members to the domain type? no
  3. Should we change implicit sets from instances of Set to instances of SetOf? yes
  4. Should implicitly-created Sets (either through Set operators or implicitly casted from iterables) be added to the Model? no
  5. Should we allow RangeSet objects with floating point steps? yes
  6. Should we deprecate is_virtual and is_concrete? yes
  7. Should Set's data(), ordered_data(), and sorted_data() return generators? no

In other words, I agree with what the PEP is proposing for these 7 items.

Questions:

  1. You make several references to "continuous sets" in this PEP and I'm a little concerned about users getting confused between what you refer to as a "continuous set" and the Pyomo.DAE ContinuousSet. Do you have any thoughts on how we might mitigate that confusion? Do you envision the Pyomo.DAE ContinuousSet going away in favor of these new "continuous sets"?
  2. Something that has always been a major issue for me is being able to easily identify the individual indexing sets of any component. Right now I rely on checking the dimension of a component and then using that to determine if I check the component's _index or _implicit_subsets attribute. Will this PEP modify the behavior of those two attributes on IndexedComponents? If I have a Var indexed by a ContinuousSet and an implicitly created set, will I still be able to verify that the component is indexed by a ContinuousSet and the ordering of the indexing sets?

@ghackebeil
Copy link
Member

  1. Should we map all 1-tuples to scalars for improved consistency?
    -- I think if the user wants a set of one-tuples, you should let me have one.
  2. Should we cast incoming Set members to the domain type?
    -- I didn't even know Sets had a domain type. Does that add any real value?
  3. Should we change implicit sets from instances of Set to instances of SetOf?
    -- No opinion.
  4. Should implicitly-created Sets (either through Set operators or implicitly casted from iterables) be added to the Model?
    -- No.
  5. Should we allow RangeSet objects with floating point steps?
    -- No opinion.
  6. Should we deprecate is_virtual and is_concrete?
    -- No opinion.
  7. Should Set's data(), ordered_data(), and sorted_data() return generators?
    -- No opinion.

The only functionality I use from the domain objects in Kernel is checking if a domain is discrete and extracting its bounds, then I throw the domain away and store those two pieces of information directly onto a variable when it is created. I could easily implement some Kernel-only domain objects that capture this, if you wanted move those original domain objects back into base under this new design.

@jsiirola
Copy link
Member Author

I have updated the PEP to include the following:

  • @blnicho: There is no intent in this PEP to change/deprecate/remove ContinuousSet. I have scrubbed the PEP and there are now no references to "continuous sets". There are "continuous ranges", and that nuance is somewhat unavoidable.
  • @blnicho: there is a method SetProduct.flatten_cross_product that will return the expanded list of member sets in the cross product. This is useful when cross products are nested, e.g.:
    m.I = Set()
    m.J = Set()
    m.K = m.I * m.J
    m.L = m.K * m.I * m.K
    list(m.L.flatten_cross_product()) == [m.I, m.J, m.I, m.I, m.J]
  • @ghackebeil: the more I worked with some of out outstanding issues and tickets, the more it appears that we should extract 1-tuples by default for consistency. I have added a toggle to remove this behavior by setting pyomo.core.base.indexed_component.normalize_index.flatten = False. This disables all index manipulation, including recasting Sequence indices to tuples, flattening nested tuples, and unpacking 1-tuples.

@carldlaird
Copy link
Member

After discussion on the PEP, the PMC agreed that we should change the answer to #3 to NO. This will keep implicit sets using the Set object instead of the SetOf object. The major concern was potential performance issues regarding the use of SetOf (checking containment of a list scales poorly compared with the default behavior of Set today).

@whart222
Copy link
Member

whart222 commented Aug 13, 2019

Recommendations after reviewing the PEP:

  • Rename is_() methods to is()
  • Treat ranges with noninteger values as a special case. I think the proposal here makes sense.
  • Clarify the relationship between Set and Kernel in the documentation above.
  • Deprecate with an error for is_virtual() and is_concrete()
  • Are the new sets deterministic? If they are unordered, are they in a fixed, reproducible order? This isn’t clear from the documentation, but this feature can be exploited to speed up Pyomo.
  • When describing the SetData objects, please include a brief summary of the class. That helps interpret the API.
  • The documentation should clarify that the Set() API is intended to be compliant with Python set().
  • WRT the TODO, I recommend using generators. This is a performance issue for large sets.
  • WRT the TODO for Set([1,2]). I had always imagined that Pyomo was creating a copy of the data passed into Set(). Does this change the logic for this question?
  • WRT the TODO for adding implicit sets to models. If we can get away without this, then that’s great. I think we added these to the model because we had to.
  • WRT casting, I think if the user specifies a domain then that indicates that they want to do error checking. At least with the old model. With the new model, what does it mean to specify Set(domain=Integer)? Does this constraint add()? Or is this just a way of declaring the values currently in Set()?
  • Yeah, I think we should unpack 1-tuples.
  • I agree that filter should be applied always

@jsiirola
Copy link
Member Author

@whart222: The majority of your comments (indicated by checked boxes) have been implemented / addressed. As for the others:

  • I recommend using generators.
    • I think so, too. However, changing data() to return a generator will break backwards compatibility. That said, the only part of Pyomo that uses the .data() is the DataPortal. Anyone else that uses data() will get an error if they attempt to use the result as a set or list. The concern will be if a user assumed it was a list-like thing and attempted to iterate over it twice (the second attempt will be an empty iterator). One alternative would be to leave data() as a set/list/tuple (for backwards compatibility) and only make ordered_data() and sorted_data() return generators.
  • I had always imagined that Pyomo was creating a copy of the data passed into Set().
    • That is still the behavior. There was a discussion as part of this PEP as to whether we should make implicit Sets (e.g., the "set-like object" created with Var([1,2,3])) SetOf objects instead of the current behavior of creating Set objects, but the consensus appears to be to NOT make that change.
  • ...adding implicit sets to models. If we can get away without this, then that’s great
    • I agree, and I think we can do it.
  • I think if the user specifies a domain then that indicates that they want to do error checking
    • Agreed. The incoming members are checked to verify that they are in the domain, and if not, then an exception is raised. However, the issue is that Python treats integers the same as "floating point integers" (that is, 1 == 1.0 and 1 in {1.0} == True). Because of this, checking if an int is in the Reals set passes, as does checking if a floating point integer is in Integers passes. So, the question is if it is a problem that a Set with a Reals domain can contain integers or visa-versa:
    >>> from pyomo.environ import *
    >>> from pyomo.core.base.set import *
    >>> m = ConcreteModel()
    >>> m.I = Set(domain=Integers)
    >>> m.I.add(2.0)
    True
    >>> m.J = Set(domain=Reals)
    >>> m.J.add(3)
    True
    >>> m.pprint()
    2 Set Declarations
        I : Size=1, Index=None, Ordered=Insertion
            Key  : Dimen : Domain   : Size : Members
            None :     1 : Integers :    1 :  {2.0,}
        J : Size=1, Index=None, Ordered=Insertion
            Key  : Dimen : Domain : Size : Members
            None :     1 :  Reals :    1 :    {3,}
    
    2 Declarations: I J

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants