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

Convert implicit sets created by IndexedComponents to "anonymous" sets #3075

Merged
merged 24 commits into from
Feb 15, 2024

Conversation

jsiirola
Copy link
Member

@jsiirola jsiirola commented Jan 5, 2024

Fixes #45, fixes #2068.

Summary/Motivation:

Pyomo creates Set objects on the fly when constructing IndexedComponents indexed by anything other than a single Pyomo Set object. These objects get added to the Block using a fixed auto-generated names. This causes problems when deleting an indexed component and then trying to create it again, because the sets implicitly created by Pyomo are still on the Block and lead to errors about name conflicts.

This PR resolves this by adding support for "anonymous" (unnamed) Set objects. With this PR, he Set objects implicitly created during component declaration (including index_set and domain) will not be explicitly attached to the Block. However, they will have their parent_block() point tot he "owning" block, thereby ensuring that the anonymous sets obey the correct scoping rules when deepcopied / cloned. While this may seem like a departure to the convention that all modeling components are explicitly attached to Blocks, there is precedent in the case of Sets for global Set objects (e.g., Reals, Integers, etc).

This touches a lot of files due to changes in test baselines. If you want to see the diffs for everything except the test changes, see https://github.com/Pyomo/pyomo/compare/d5b53d5..0325346

Changes proposed in this PR:

  • rework how component Set-like arguments are processed and anonymous sets are tracked
  • update string representation for Set objects with a parent_block() but no _name (use the Set data as the name)
  • remove _implicit_subsets attribute (previously used for the implicit arguments to SetProduct) and replaced it with the general purpose _anonymous_sets (used by all anonymous sets)
  • some standardization in Component.construct() implementations
  • resolved some issues when using slices when set flattening is disabled
  • remove the dependence on cplex_direct in Benders tests

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

@jsiirola jsiirola changed the title Convert implicit sets created by IndexedCOmponents to "anonymous" sets Convert implicit sets created by IndexedComponents to "anonymous" sets Jan 7, 2024
Copy link

codecov bot commented Jan 8, 2024

Codecov Report

Attention: 11 lines in your changes are missing coverage. Please review.

Comparison is base (42fa578) 88.28% compared to head (6408d44) 88.28%.
Report is 36 commits behind head on main.

Files Patch % Lines
pyomo/dae/flatten.py 69.23% 4 Missing ⚠️
pyomo/core/base/global_set.py 66.66% 2 Missing ⚠️
pyomo/core/base/set.py 97.64% 2 Missing ⚠️
pyomo/core/base/logical_constraint.py 90.00% 1 Missing ⚠️
pyomo/mpec/complementarity.py 83.33% 1 Missing ⚠️
pyomo/network/arc.py 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3075      +/-   ##
==========================================
- Coverage   88.28%   88.28%   -0.01%     
==========================================
  Files         832      832              
  Lines       92307    92392      +85     
==========================================
+ Hits        81495    81566      +71     
- Misses      10812    10826      +14     
Flag Coverage Δ
linux 86.04% <94.35%> (-0.01%) ⬇️
osx 75.56% <94.35%> (+<0.01%) ⬆️
other 86.23% <94.35%> (-0.01%) ⬇️
win 83.45% <94.35%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@blnicho blnicho self-requested a review January 23, 2024 19:55
2 Var Declarations
x : Size=12, Index=x_index
x : Size=12, Index={Harlingen, Memphis, Ashland}*{NYC, LA, Chicago, Houston}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this change in the string representation to use the Set data as the name when there are only a few data elements but this output could quickly lose its human readability when the sets are large. What do you think about concatenating the data when the string representation exceeds a certain number of characters to guard against this case? The output could look something like:

Index={Harlingen, Memphis, Ashland, ...}*{NYC, LA, Chicago, Houston, ...}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been wanting to to that for a long time (and not just here, but in expressions, too), but that would require a significant redesign of the way we convert things to strings. I could hack something in here, but wonder if we should do it as a separate PR.

(The challenge is this is really part of converting expressions to strings, and strings are built from the leaf nodes up to the root node. So, you may not know that the result is "too long" until you get up the expression tree , but then need to go back down the tree and truncate things closer to the leaves)

@@ -642,7 +642,7 @@ def Reference(reference, ctype=NOTSET):
...
>>> m.r3 = Reference(m.b[:].x[:])
>>> m.r3.pprint()
r3 : Size=4, Index=r3_index, ReferenceTo=b[:].x[:]
r3 : Size=4, Index=ReferenceSet(b[:].x[:]), ReferenceTo=b[:].x[:]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the index for this Reference come back as a ReferenceSet instead of {1,2}*{3,4} like the first reference example?

Copy link
Member Author

@jsiirola jsiirola Feb 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually something that has not changed in this PR. In this case, the index for m.b[1].x is not the same as the index for m.b[2].x: While those sets are equivalent (both contain [3, 4]), they are not the same set. As a result, it is not proper to make the reference look like it is indexed by the Cartesian product of only two sets. The ReferenceSet is just a notation saying it is defined by the slice iterator (and not simple set operations).

The first example shows the Cartesian product because the block is defined by a Cartesian product. Similarly, there is a variant of this second example that would show the Cartesian product:

>>> from pyomo.environ import *
>>> m = ConcreteModel()
>>> m.I = Set(initialize=[3,4])
>>> @m.Block([1,2])
... def b(b,i):
...    b.x = Var(m.I, bounds=(i,None))
...
>>> m.r4 = Reference(m.b[:].x[:])
>>> m.r4.pprint()
r4 : Size=4, Index={1, 2}*I, ReferenceTo=b[:].x[:]
    Key    : Lower : Value : Upper : Fixed : Stale : Domain
    (1, 3) :     1 :  None :  None : False :  True :  Reals
    (1, 4) :     1 :  None :  None : False :  True :  Reals
    (2, 3) :     2 :  None :  None : False :  True :  Reals
    (2, 4) :     2 :  None :  None : False :  True :  Reals

"""
print(buf.getvalue())
self.maxDiff = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this line doing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically only for debugging (unittest truncates long diffs when something like assertEqual fails). Setting maxDiff = None will always show the full diff on failure. I usually take that out when I get the test to stop failing. That said, I have thought about putting that line into the base class defition in pyomo.common.unittest

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never knew this was an option. I think adding it to the base class would make a lot of sense. This seems like behavior we would always want when tests fail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thoroughly agree.

Copy link
Contributor

@emma58 emma58 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really exciting! :)

@mrmundt mrmundt merged commit d09c24d into Pyomo:main Feb 15, 2024
33 checks passed
@jsiirola jsiirola deleted the anonymous-sets branch February 21, 2024 05:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants