Skip to content
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from pyomo.common.dependencies import numpy as np, packaging
from pyomo.common.enums import IntEnum
from pyomo.common.modeling import unique_component_name
from pyomo.core.expr.numeric_expr import SumExpression
from pyomo.core.expr.numeric_expr import SumExpression, mutable_expression
from pyomo.core.expr import identify_variables
from pyomo.core.expr import SumExpression
from pyomo.core.util import target_list
Expand Down Expand Up @@ -648,8 +648,9 @@ def _get_bounds_list(self, var_list, obj):
bounds.append((v.bounds, v.is_integer()))
return bounds

def _needs_approximating(self, expr, approximate_quadratic):
repn = self._quadratic_repn_visitor.walk_expression(expr)
def _needs_approximating(self, expr, approximate_quadratic, repn=None):
if repn is None:
repn = self._quadratic_repn_visitor.walk_expression(expr)
if repn.nonlinear is None:
if repn.quadratic is None:
# Linear constraint. Always skip.
Expand All @@ -661,23 +662,51 @@ def _needs_approximating(self, expr, approximate_quadratic):
return ExprType.QUADRATIC, True
return ExprType.GENERAL, True

def _separate_linear_parts(self, repn):
var_map = self._quadratic_repn_visitor.var_map
Comment thread
michaelbynum marked this conversation as resolved.
Outdated
linear = 0
nonlinear = 0
if repn.nonlinear is not None:
nonlinear += repn.nonlinear
if repn.quadratic:
for (x1, x2), coef in repn.quadratic.items():
if repn.multiplier_flag(coef):
if x1 == x2:
nonlinear += coef * var_map[x1] ** 2
else:
nonlinear += coef * (var_map[x1] * var_map[x2])

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.

Does separating these two cases matter? I mean, obviously you make a different expression tree, but do we need it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think it matters to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm going to leave this just for consistency with QuadraticRepn.

if repn.linear:
for vid, coef in repn.linear.items():
if repn.multiplier_flag(coef):
linear += coef * var_map[vid]
if repn.constant_flag(repn.constant):
linear += repn.constant
if repn.multiplier_flag(repn.multiplier) != 1:
linear *= repn.multiplier
nonlinear *= repn.multiplier

@emma58 emma58 Jan 13, 2026

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.

@jsiirola, can this happen? I have something in the back of my mind telling me multiplier is sure to be 1 at this point?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I just copied the to_expression code from the QuadraticRepn class and modified it slightly...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

which is to say that I have no idea. I'll take a look, though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh, I can't read. This question was for @jsiirola. My bad.


return linear, nonlinear

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

let's not lump the quadratic part back into the nonlinear part. additively decompose will separate it again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I started to do this, but the min_dimension_to_additively_decompose option made it more complex than I thought was worthwhile.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm taking back what I said. I'm going to make this change. I was confused.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Okay, this is done, now.


def _approximate_expression(
self, expr, obj, trans_block, config, approximate_quadratic
):
repn = self._quadratic_repn_visitor.walk_expression(expr)
expr_type, needs_approximating = self._needs_approximating(
expr, approximate_quadratic
expr, approximate_quadratic, repn
)
if not needs_approximating:
return None, expr_type

linear_part, nonlinear_part = self._separate_linear_parts(repn)

# Additively decompose expr and work on the pieces
pwl_summands = []
pwl_summands = [linear_part]
for k, subexpr in enumerate(
_additively_decompose_expr(
expr, config.min_dimension_to_additively_decompose
nonlinear_part, config.min_dimension_to_additively_decompose
)
if config.additively_decompose
else (expr,)
else (nonlinear_part,)
):
# First check if this is a good idea
expr_vars = list(identify_variables(subexpr, include_fixed=False))
Expand Down
Loading