Skip to content

Bugfixes and testing of linear templates#3781

Open
jsiirola wants to merge 44 commits intoPyomo:mainfrom
jsiirola:linear-template-testing
Open

Bugfixes and testing of linear templates#3781
jsiirola wants to merge 44 commits intoPyomo:mainfrom
jsiirola:linear-template-testing

Conversation

@jsiirola
Copy link
Copy Markdown
Member

Fixes # .

Summary/Motivation:

This PR is primarily adding more comprehensive testing of templatizing (and compiling) linear models. As part of flusing out the tests, this resolves a number of issues / bugs in the templatizer, the linear template compiler, and in general Pyomo:

General Pyomo changes

  • Rework Param.extract_values to return defaultdict for Params with default values
  • Provide a specialized InvalidConstraintError exception for constraints that cannot be emitted to the solvers
  • Use SetOf when iterating over IndexedComponent objects indexed by non-finite Sets
  • SetOf.dimen should return UnknownSetDimen (not 0) if the underlying reference has no data

Updates to the templatizer

  • Leverage attempt_import to break circular dependencies in template_expr
  • Catch UnknownSetDimen when iterating over template generators and treat like jagged sets
  • Expand (do not templatize) expressions with sum() that do not sum over generators
  • Expand (do not templatize) expressions with sum() whose generator doesn't create a template index
  • Expand (do not templatize) expressions with sum() that appear to iterate over things other than Pyomo Set objects

Updates to the linear template compiler

  • remove check_duplicates and remove_fixed_vars options (this is not more efficiently implementable through sparse matrix operations0
  • rework expression builders to be more testable and (slightly) more efficient
  • Support IndexedComponent.Skip
  • Resolve issue compiling constraints indexed by non-finite Sets

Changes proposed in this PR:

  • (see above)

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.

- iterate over the underlying reference when guessing the dimen
  This prevents recursion when templatizing.
- inconsistent dimen is reported as None
@codecov
Copy link
Copy Markdown

codecov bot commented Nov 11, 2025

Codecov Report

❌ Patch coverage is 96.77419% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.03%. Comparing base (6744ca0) to head (9de6ce3).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
pyomo/core/base/constraint.py 85.71% 1 Missing ⚠️
pyomo/core/base/param.py 91.66% 1 Missing ⚠️
pyomo/repn/linear_template.py 98.70% 1 Missing ⚠️
pyomo/repn/plugins/lp_writer.py 50.00% 1 Missing ⚠️
pyomo/repn/plugins/standard_form.py 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3781      +/-   ##
==========================================
+ Coverage   89.92%   90.03%   +0.10%     
==========================================
  Files         901      902       +1     
  Lines      106285   106496     +211     
==========================================
+ Hits        95580    95882     +302     
+ Misses      10705    10614      -91     
Flag Coverage Δ
builders 29.21% <16.77%> (+<0.01%) ⬆️
default 86.32% <96.77%> (?)
expensive 35.66% <22.58%> (?)
linux 87.48% <96.77%> (-1.94%) ⬇️
linux_other 87.48% <96.77%> (+0.10%) ⬆️
oldsolvers 28.11% <16.12%> (+<0.01%) ⬆️
osx 82.81% <96.77%> (+0.10%) ⬆️
win 85.90% <96.77%> (+0.10%) ⬆️
win_other 85.90% <96.77%> (+0.10%) ⬆️

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.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@blnicho blnicho requested review from blnicho and emma58 November 11, 2025 19:51
@blnicho blnicho moved this from Todo to Review In Progress in Pyomo 6.10 Nov 11, 2025
Copy link
Copy Markdown
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.

I kind of ran out of brainpower in the crux, so I'll look at linear_template.py again in the morning, but here are a few comments from elsewhere. I'm very exited that it's tested!!

logger = logging.getLogger(__name__)


def _validate_generator(generator):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Naming question: Should this be _check_generator_templatizable or something of the like? It's not really invalid... Just impossible.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't know. Naming is hard. This isn't checking that the generator is templatizable - that has already happened (when we call next(generator) in sum_template and don't get a TemplateExpressionError). So at this point, we already have a template expression and we are looking into the state of the generator to check that the internal state of the generator is "valid" (i.e., it doesn't have any local variables that are not IndexTemplate objects). So we really are validating that the generator is (in a) "correct" (state).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, okay. Then maybe your name makes sense!

Comment on lines +1085 to +1088
if type(d) is not int:
# This covers None (jagged set) and UnknownSetDimen. In
# both cases, we will not attempt to unpack the Set and just
# assume a single index template.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this safe for UnknownSetDimen? Or actually either of them?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because will it catch if someone is trying to templatize an expression where they unpacked something multidimensional? Or is the idea that that will die then, so just keep going for now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What is happening here is that we don't know how many scalars (i.e., IndexTemplates) to unpack the set item into. So we won't try and unpack anything (and hope that the user won't either). In this case we will return a single IndexTemplate that is the placeholder for the arbitrary tuple that would be returned by the Set. As long as the user doesn't try and unpack that tuple (and they shouldn't without first looking at the length of the tuple - which will generate the TemplateExpressionError that disables templatization).

Here's an example:

m.I = Set(dimen=None, initialize=[(1,), (2, 3), (4, 5, 6)])
m.x = Var(m.I)
m.c = Constraint(expr=sum(m.x[i] for i in m.I) <= 0)

Here, i is a tuple of some "random" length. So we want to templatize this as

sum(x[_1] for _1 in I)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK, I see!

# it is not clear what to import to get to the built-in "generator"
# type. We will just create a generator and query its __class__
generator_validators = {
(_ for _ in ()).__class__: _validate_generator,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wow, that one took me a minute, even with the comment! How strange that it's hard to import...

Comment on lines +1140 to +1143
niters = -len(self.cache)
expr = next(generator)
final_cache = len(self.cache)
return TemplateSumExpression((expr,), self.npop_cache(final_cache - init_cache))
niters += len(self.cache)
if niters:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is just me not understanding the old code either, but how does next(generator) change the state of self.cache?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is the core trick in Templatization of sum expressions: next(generator) calls the generator within the sum to return the first term. That in turn calls iter(...) on the thing (usually a Set) being summed over. For Set objects, that hits the overriden __iter__ implementation (see _set_iterator_template_generator, managed by _template_iter_context) that adds to the context cache)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ohhhhh. This would be a nice thing to write down in developer docs someday! :)

Copy link
Copy Markdown
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.

OK, please don't get hit by a bus, but I kind of understand this, and I reallllly like it!

Comment on lines +97 to +100
raise InvalidConstraintError(
"LinearTemplateRepn does not support constraints containing "
"general nonlinear terms."
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it possible to give the name or some pointer to the offending Constraint in this error?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants