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

Support validate / filter for IndexedSet components using the index #3338

Merged
merged 38 commits into from
Aug 20, 2024

Conversation

jsiirola
Copy link
Member

@jsiirola jsiirola commented Aug 10, 2024

Fixes #2655; supersedes #3304

Summary/Motivation:

IndexedSet components supported the validate and filter arguments, but those callbacks didn't provide the index. This prevented index-specific behavior, which is inconsistent to other indexed components. This PR builds on (and supersedes) #3304 to provide the index to the callbacks for both validate and filter. This implementation extends the standard Initializer() logic to support parameterized callbacks.

More history

We have historically supported filters on multidimensional scalar sets that look like this:

model.I = RangeSet(5)
def lower_triangular(m, i, j):
    return i<= j
model.II = Set(initialize=model.I*model.I, filter=lower_triangular)

This starts to break down if the Set itself is indexed:

model.I = RangeSet(5)
def lower_triangular(m, i, j, k):
    return i<= j and i <= k and j <= k
model.II = Set(model.I, initialize=model.I*model.I, filter=lower_triangular)

And can become ambiguous if the IndexedSet index or the individual SetData objects are jagged sets.

To remove the ambiguity, this PR moves to a new paradigm where the "extra data" is not expanded when passed to the function:

model.I = RangeSet(5)
def lower_triangular(m, val, k):
    i, j = val
    return i<= j and i <= k and j <= k
model.II = Set(model.I, initialize=model.I*model.I, filter=lower_triangular)

Changes proposed in this PR:

  • Add "Parameterized" initializer classes to the Initializer system
  • Convert IndexedSet to using Initializers to process the validate and filter arguments
  • Deprecate support of "expanded" value arguments to the validate and filter arguments when used on IndexedSet objects (TBD: discussion as to if we should also deprecate that API for scalar Set objects)

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.

Pedro L. Magalhães and others added 30 commits June 13, 2024 23:56
…s to the index. Original pyomo tests pass.
pyomo/core/base/initializer.py Outdated Show resolved Hide resolved
pyomo/core/base/set.py Outdated Show resolved Hide resolved
pyomo/core/base/set.py Outdated Show resolved Hide resolved
@jsiirola jsiirola requested a review from mrmundt August 12, 2024 15:29
Copy link

codecov bot commented Aug 12, 2024

Codecov Report

Attention: Patch coverage is 87.17949% with 15 lines in your changes missing coverage. Please review.

Project coverage is 88.53%. Comparing base (404fd6d) to head (c5a4fa4).

Files Patch % Lines
pyomo/core/base/initializer.py 71.79% 11 Missing ⚠️
pyomo/core/base/set.py 94.59% 4 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3338   +/-   ##
=======================================
  Coverage   88.53%   88.53%           
=======================================
  Files         868      868           
  Lines       98495    98550   +55     
=======================================
+ Hits        87199    87248   +49     
- Misses      11296    11302    +6     
Flag Coverage Δ
linux 86.05% <87.17%> (-0.01%) ⬇️
osx 75.63% <87.17%> (-0.01%) ⬇️
other 86.55% <87.17%> (+<0.01%) ⬆️
win 83.86% <87.17%> (+<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.

Comment on lines 2267 to 2286
if self._validate.__class__ is ParameterizedIndexedCallInitializer:
# TBD [JDS: 8/2024]: should we deprecate the "expanded
# tuple" version of the validate callback for scalar sets?
# It is widely used and we can (reasonably reliably) map to
# the expected behavior...
orig_fcn = self._validate._fcn
self._validate = ParameterizedScalarCallInitializer(
lambda m, v: orig_fcn(m, *v), True
)

if self._filter.__class__ is ParameterizedIndexedCallInitializer:
# TBD [JDS: 8/2024]: should we deprecate the "expanded
# tuple" version of the filter callback for scalar sets?
# It is widely used and we can (reasonably reliably) map to
# the expected behavior...
orig_fcn = self._filter._fcn
self._filter = ParameterizedScalarCallInitializer(
lambda m, v: orig_fcn(m, *v), True
)

Copy link
Member Author

@jsiirola jsiirola Aug 12, 2024

Choose a reason for hiding this comment

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

Note that this code (silently) preserves the old API for scalar Set objects. However, that old API is

  • difficult to reconcile with the "newer" API for IndexedSets
  • widely used in Pyomo models in the wild

I think I'd like to put in a deprecation warning here, but that would generate a LOT of noise (basically, it would generate deprecation warnings for every model that uses filter= or validate=).

Thoughts (@blnicho, @mrmundt, @emma58, @michaelbynum, @whart222)?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't mind disturbing users in the wild, but I want the deprecation warnings to be helpful / tell them how to change over their models. What is the new, preferred method for doing this? Either we detail that directly in the deprecation message or we make a new page on RTD that gives more detailed instructions.

Copy link
Member Author

Choose a reason for hiding this comment

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

We had a long discussion at the Dev call today and decided that we should start issuing deprecation warnings for ScalarSets now.

pyomo/core/base/initializer.py Outdated Show resolved Hide resolved
% (value, self.name)
)
yield value
flag = fcn(block, idx, value)
Copy link
Member

Choose a reason for hiding this comment

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

Should the order of idx and value be switched?

Comment on lines +1508 to +1515
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.7.4.dev0',
Copy link
Member

Choose a reason for hiding this comment

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

Same question here about the ordering of idx and value. The deprecation warning matches the argument ordering I was expecting but the call to fcn does not. Is there magic happening in the initializer function call logic to swap argument ordering?

Comment on lines +357 to +361
def __call__(self, parent, idx, *args):
if idx.__class__ is tuple:
return self._fcn(parent, *args, *idx)
else:
return self._fcn(parent, *args, idx)
Copy link
Member

Choose a reason for hiding this comment

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

I think I'm answering my own question by noticing that the argument ordering is indeed swapped here.

Copy link
Member

Choose a reason for hiding this comment

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

@jsiirola there isn't really anything for you to resolve in my comments/questions, I just wanted to document my initial confusion. I am curious why the argument ordering and subsequent reordering was necessary. I'm guessing it had to do with having more control over the format of the index being passed in.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah - this turned into a bit of a mess (and yes, it is confusing that they were reversed). The calls you are seeing here are all internal to the Initializer methods, so the user is never aware of them. In this case, because the "Parameterized" initializer was new, the old fcn API was just (block, index). Parameterized was adding "value", so it made sense for the extra arg to come at the end. This was further confused by the historical behavior where we would expand the value arg and pass it to the user-provided callback (leading things like the messy deprecation paths around set.py lines 1482-1520), and leaving things like this seemed to require less code gymnastics to implement the deprecation paths and make things compatible with the rest of the Initializer system.

@blnicho blnicho merged commit 084d057 into Pyomo:main Aug 20, 2024
32 checks passed
@jsiirola jsiirola deleted the valsetindex branch August 20, 2024 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

No access to the index when validating indexed set members
4 participants