[Includes #3960] More flexible LinearStandardFormCompiler#3949
[Includes #3960] More flexible LinearStandardFormCompiler#3949bknueven wants to merge 14 commits into
LinearStandardFormCompiler#3949Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3949 +/- ##
==========================================
+ Coverage 90.11% 90.13% +0.01%
==========================================
Files 905 905
Lines 107502 107579 +77
==========================================
+ Hits 96878 96964 +86
+ Misses 10624 10615 -9
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
jsiirola
left a comment
There was a problem hiding this comment.
I like this approach. I am still digging through the review, but in the interests of getting things moving one big initial question: have you quantified how this change impacts the standard_form performance?
| ), | ||
| ) | ||
| CONFIG.declare( | ||
| 'extra_valid_ctypes', |
There was a problem hiding this comment.
Potentially rename this ignore_ctypes? Should this be promoted to an ADVANCED_OPTION?
There was a problem hiding this comment.
I think that name is better. Should any other options be promoted to an ADVANCED_OPTION?
michaelbynum
left a comment
There was a problem hiding this comment.
This is a clever way to enable using the standard form compiler with solvers that support nonlinear expressions. Nice work.
Question - are you actually using kernel, or were you just trying to get tests to pass?
| offset, linear_index, linear_data, lb, ub = ( | ||
| template_visitor.expand_expression(obj, obj.template_expr()) | ||
| ) | ||
| except InvalidExpressionError: |
There was a problem hiding this comment.
Is it possible to get an InvalidExpressionError for some reason other than a nonlinear expression?
There was a problem hiding this comment.
I think I am not terribly concerned about this. If this error is raised for some other reason, then the solver interface who is handling the nonlinear constraints will have to deal with this.
There was a problem hiding this comment.
I did check with template constraints on a small NLP, and this is in fact the exception we get on main:
ERROR: Error compiling expanded template expressions for
TemplateScalarConstraint 'c' LinearTemplateRepn does not support expressions
containing general nonlinear terms.
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
File ~/Software/pyomo/pyomo/repn/linear_template.py:380, in LinearTemplateRepnVisitor.expand_expression(self, obj, template_info)
379 try:
--> 380 body, lb, ub = self.expanded_templates[id(template_info)]
381 except KeyError:
KeyError: 6408477952
During handling of the above exception, another exception occurred:
InvalidExpressionError Traceback (most recent call last)
Cell In[22], line 1
----> 1 repn = LinearStandardFormCompiler().write(m)
File ~/Software/pyomo/pyomo/repn/plugins/standard_form.py:268, in LinearStandardFormCompiler.write(self, model, ostream, **options)
264 # Pause the GC, as the walker that generates the compiled LP
265 # representation generates (and disposes of) a large number of
266 # small objects.
267 with PauseGC():
--> 268 return _LinearStandardFormCompiler_impl(config).write(model)
File ~/Software/pyomo/pyomo/repn/plugins/standard_form.py:432, in _LinearStandardFormCompiler_impl.write(self, model)
428 last_parent = con._component
430 if hasattr(con, 'template_expr'):
431 offset, linear_index, linear_data, lb, ub = (
--> 432 template_visitor.expand_expression(con, con.template_expr())
433 )
434 N = len(linear_data)
435 else:
436 # Note: lb and ub could be a number, expression, or None.
437 # Non-fixed expressions will raise an InvalidConstraintError.
File ~/Software/pyomo/pyomo/repn/linear_template.py:383, in LinearTemplateRepnVisitor.expand_expression(self, obj, template_info)
381 except KeyError:
382 try:
--> 383 body, lb, ub = self._generate_expanded_template(obj, template_info)
384 except:
385 # Provide some context clues for what component failed
386 # compilation
387 msg = sys.exc_info()[1]
File ~/Software/pyomo/pyomo/repn/linear_template.py:448, in LinearTemplateRepnVisitor._generate_expanded_template(self, obj, template_info)
446 env = self.env
447 if body is not None:
--> 448 body = self.walk_expression(body).compile(
449 self.env, smap, self.expr_cache, args
450 )
451 if lb is not None:
452 lb = self.walk_expression(lb).compile(
453 self.env, smap, self.expr_cache, args
454 )
File ~/Software/pyomo/pyomo/core/expr/visitor.py:274, in StreamBasedExpressionVisitor.walk_expression(self, expr)
271 root = expr
273 try:
--> 274 result = self._process_node(root, RECURSION_LIMIT)
275 _nonrecursive = None
276 except RevertToNonrecursive:
File ~/Software/pyomo/pyomo/core/expr/visitor.py:440, in StreamBasedExpressionVisitor._process_node_bex(self, node, recursion_limit)
436 args.__exit__(None, None, None)
438 # We are done with this node. Call exitNode to compute
439 # any result
--> 440 return self.exitNode(node, data)
File ~/Software/pyomo/pyomo/repn/linear.py:868, in LinearRepnVisitor.exitNode(self, node, data)
864 return data.walker_exitNode()
865 #
866 # General expressions...
867 #
--> 868 return self.exit_node_dispatcher[(node.__class__, *map(itemgetter(0), data))](
869 self, node, *data
870 )
File ~/Software/pyomo/pyomo/repn/linear.py:305, in _handle_product_nonlinear(visitor, node, arg1, arg2)
303 ans = visitor.Result()
304 if not visitor.expand_nonlinear_products:
--> 305 ans.nonlinear = to_expression(visitor, arg1) * to_expression(visitor, arg2)
306 return _GENERAL, ans
307 #
308 # We are multiplying (and expanding) m(A + Bx + C(x)) * m(A + Bx + C(x))
File ~/Software/pyomo/pyomo/repn/linear.py:239, in to_expression(visitor, arg)
237 return arg[1]
238 else:
--> 239 return arg[1].to_expression(visitor)
File ~/Software/pyomo/pyomo/repn/linear_template.py:104, in LinearTemplateRepn.to_expression(self, visitor)
99 def to_expression(self, visitor):
100 # to_expression() is only used by the underlying
101 # LinearRepnVisitor to generate nonlinear expressions. We are
102 # explicitly disallowing nonlinear expressions here, so we are
103 # going to bail now:
--> 104 raise InvalidExpressionError(
105 "LinearTemplateRepn does not support expressions containing "
106 "general nonlinear terms."
107 )
InvalidExpressionError: LinearTemplateRepn does not support expressions containing general nonlinear terms.| offset, linear_index, linear_data, lb, ub = ( | ||
| template_visitor.expand_expression(con, con.template_expr()) | ||
| ) | ||
| except InvalidExpressionError: |
There was a problem hiding this comment.
Same question here: Is there a way to get an InvalidExpressionError for a reason other than a nonlinear expression?
| linear_index = map(var_recorder.var_order.__getitem__, repn.linear) | ||
| linear_data = repn.linear.values() | ||
|
|
||
| # Normalize ±inf to None: both kernel constraints and AML |
There was a problem hiding this comment.
I don't normally see symbols like the +/- symbol here (which show up in several places). Will this cause problems? In other words, do we have any reason to limit ourselves to ASCII (I believe these characters are UTF-8)?
There was a problem hiding this comment.
We generally don't have a problem with UTF-8 characters in files (several others already have them). However, the file needs to start with the following line so that editors and interpreters parse it correctly (without complaint).
# -*- coding: utf-8 -*-
| constraint is not misclassified as a range constraint, and a fully | ||
| unbounded constraint is skipped rather than emitted as a range row. | ||
| """ | ||
| import pyomo.kernel as pmo |
There was a problem hiding this comment.
Do we want new tests for kernel?
| mk = pmo.block() | ||
| mk.x = pmo.variable() | ||
| # lb=-inf (unbounded below) → should become a pure ≤ row, not a range row | ||
| mk.c_ub = pmo.constraint(ub=2.0, body=mk.x) | ||
| # ub=+inf (unbounded above) → should become a pure ≥ row, not a range row | ||
| mk.c_lb = pmo.constraint(lb=-3.0, body=mk.x) | ||
| # Explicit finite range → should still be a range row | ||
| mk.c_rng = pmo.constraint((-1.0, mk.x, 4.0)) |
There was a problem hiding this comment.
I was surprised that I didn't see any inf bounds here given the name of the test and the docstring for the function. This is not necessarily a bad test, but I don't see how it tests inf bounds.
| when ``allow_nonlinear=False`` (the default). When | ||
| ``allow_nonlinear=True``, holds the list of objectives with nonlinear | ||
| terms that were omitted from the compiled matrices (may be empty). | ||
|
|
There was a problem hiding this comment.
Should this class also contain a list of the active ctypes that it found but ignored (when extra_valid_ctypes is used)?
There was a problem hiding this comment.
This can always be added later if we think it is a good idea. It is really just for convenience anyway.
| except AttributeError: | ||
| # Note that this only works for the AML, as kernel does not | ||
| # provide a parent_component() | ||
| _iter = (None, var) |
There was a problem hiding this comment.
I don't understand how this worked previously. Wouldn't an error get raised below because there was no tuple to unpack?
|
I don't see anything that could impact performance unless |
I have not done this. My hope would be that with the defaults there should not be a huge performance impact, but obviously there's more branches in this version. |
|
OK - this PR does two things:
I really like the first, and can see the value in the second. However, combining them in a single PR is making the review (more) complicated. In particular, I am wary of the extra branching and logic in the tight loops for processing constraints. What would you think of the following:
|
| if lb == -float('inf'): | ||
| lb = None | ||
| if ub == float('inf'): | ||
| ub = None |
There was a problem hiding this comment.
What's the path for getting +/-inf here?
Thanks for having a look @jsiirola. I will create a separate PR for just the ranged linear constraints change. If for some reason that doesn't move expeditiously I'll return to this PR and remove the ranged linear constraint changes here. |
LinearStandardFormCompilerLinearStandardFormCompiler
|
Closing -- replaced by #3960 and #3963. @jsiirola @michaelbynum I have attempted to address you initial comments in those PRs. |
Fixes # N/A
Summary/Motivation:
LinearStandardFormCompileris a great tool for rapidly expanding a Pyomo model into the form need for (mixed-integer) linear programming solvers. Many of these solvers also handle ranged (linear) constraints, quadratic constraints, SOS constraints, quadratic objectives, and even sometimes general nonlinear constraints and objectives. This PR would augment theLinearStandardFormCompilertwo significant ways:allow_nonlinearandextra_valid_ctypes).LinearStandardFormInfowhen the new optionkeep_range_constraints=True.See the usage proof of concept on #3879, which includes these changes.
Changes proposed in this PR:
keep_range_constraintstoLinearStandardFormCompilerand compile range constraints appropriately (84afd9a, e330102, efc1c74)LinearStandardFormCompilerto return non-linear constraints / objectives it encounters instead of raising an error (optionallow_nonlinearaa9ce77)ctypes, to be handled by the caller. Useful for SOS constraints (36953d1).TemplateVarRecorderto correctly handlepyomo.kernelvariables (6a2c69a)This PR was created with the help of GitHub Copilot CLI.
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: