Skip to content

Clip and scale calculate_variable_from_constraint#3993

Open
dallan-keylogic wants to merge 4 commits into
Pyomo:mainfrom
dallan-keylogic:clip_calculate_variable
Open

Clip and scale calculate_variable_from_constraint#3993
dallan-keylogic wants to merge 4 commits into
Pyomo:mainfrom
dallan-keylogic:clip_calculate_variable

Conversation

@dallan-keylogic

@dallan-keylogic dallan-keylogic commented Jul 13, 2026

Copy link
Copy Markdown

Fixes # #3985, part of #3785, and point 4 of #3571

Summary/Motivation:

Projects like PrOMMiS are increasingly relying on IDAES's BlockTriangularizationInitializer, which in turn relies on solve_strongly_connected_components. However, in models that need significant amounts of scaling, the initialization raises an error that the constraint residuals are not small. That's because BlockTriangularizationInitializer takes constraint scaling into account but solve_strongly_connected_components does not.

This PR begins addressing this issue by adding an option to scale the variable and constraint in calculate_variable_from_constraint. Additionally, it adds an option to clip variable values that fall outside their bounds back inside those bounds, because we have a ton of log spam from Pyomo warning about variables getting set to values barely outside their bounds.

Changes proposed in this PR:

  • Add option to allow scaling of variable and constraint in `calculate_variable_from_constraint
  • Add option to clip variables back to their bounds

AI-Use Disclosure

  • AI tools were NOT used during the preparation of this PR

or

  • AI tools contributed to the development of this PR

    • AI tools generated documentation (including the PR description/comments, code comments, and/or Sphinx documentation)
    • AI tools generated tests (baselines, examples, and/or code)
    • AI tools generated code (apart from tests)

    Review process (select ONE):

    • Rewritten: All AI-generated content was rewritten by me before being committed.
    • Reviewed/verified: I retained AI-generated content and verified it before committing. Verification included (as applicable):
      • Ran the code and fixed issues
      • Added and ran tests
      • Checked correctness/logic of code and tests
      • Checked for alignment with the contribution guide
      • Considered security implications
    • As-is: AI-generated content was commited directly to the repository

Notes for reviewers (optional): I set the default values of these new options to False in order to avoid anything breaking from using the new options. However, I think users will usually want to set those options to True. On the IDAES side, I can set those options in the configuration of BlockTriangularizationInitializer, but consider what the default behavior in Pyomo should be.

Additionally, when clipping the values back to their bounds, I check the residual to see whether to accept the clipping or not. If the clipped value does not satisfy the convergence condition, it gets set to the unclipped value. What should the default behavior be?

Finally, I noticed the line search done here doesn't use the Armijo sufficient decrease condition. Is that intended behavior?

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.

@dallan-keylogic dallan-keylogic changed the title Clip calculate variable Clip and scale calculate_variable_from_constraint Jul 13, 2026
#
# Re-set the variable value to trigger any warnings WRT the final
# variable state
variable.set_value(variable.value)

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.

We could just add skip_validation=True here to silence the warnings. And potentially expose this option in the user-facing function. Not saying this is necessarily better than clipping, just want to make sure it's considered.

We could also transform calculate_variable_from_constraint into a bound-constrained solver by "clipping" the variable when taking the Newton step, as done by interior point methods.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'd rather have the warnings if we end up setting the variable value to something outside its bounds.

I would be okay with converting it to a bound-constrained solver. For the initial version of this PR, I wanted to keep the changes minor as to not break any existing workflow. I wonder whether turning on bound clipping would cause some of the CubicComplementarityVLE examples to break in IDAES---we've seen examples where earlier blocks converge to the wrong root, causing later blocks to have no feasible solution that satisfies the variable bounds, even though a solution exists if the entire VLE were solved on a single block. I think the moral of that story is "You need a better initialization method for VLE".

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.

Maybe skip_validation if it violates the bound by more than the tolerance?

I guess my problem with clipping is that you're just trading some bound infeasibility for some equality infeasibility. I would want to make sure that we didn't make the constraint infeasible (by more than tolerance) after the clipping. But I'm also now realizing that, since these are single-variable problems, clipping is not so different than respecting bounds during the solve iterations.

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.

we've seen examples where earlier blocks converge to the wrong root, causing later blocks to have no feasible solution that satisfies the variable bounds, even though a solution exists if the entire VLE were solved on a single block

I would love to see an example of this. I have some ideas related to bound propagation among blocks (e.g., #3573) that I would like to test on a real problem.

@dallan-keylogic dallan-keylogic Jul 13, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Maybe skip_validation if it violates the bound by more than the tolerance?

I guess my problem with clipping is that you're just trading some bound infeasibility for some equality infeasibility. I would want to make sure that we didn't make the constraint infeasible (by more than tolerance) after the clipping.

Right now, if clipping the variable causes the constraint to become infeasible, it reverts to the unclipped value and logs a warning.

But I'm also now realizing that, since these are single-variable problems, clipping is not so different than respecting bounds during the solve iterations.

With any sort of nonlinear solution method, there's always the risk that changing the intermediate iterations causes the problem to converge to either a different solution or a stationary point for the constraint residual. In general, though, I would guess that clipping as part of the line search would give a better chance at converging to a solution that satisfies the bound (if such a solution exists).

There are two possible implementations:

  1. Do the thing same as IPOPT: if a candidate step violates a bound, set alpha = 0.5 * alpha and repeat.
  2. Just jump directly to the bound. If taking a step at the bound also results in bound violation, then we've found a local minimizer and terminate.

I think I prefer option (2), because we don't have to worry about how to determine we've reached a bound---we just jump straight to the bound. With option (1), we could end up spending a long time taking tiny line search steps before the sufficient decrease condition is violated.

Edit: Another thing to keep in mind is consistency between the three methods used in the function. The first shortcut method assumes the constraint is of the form x == y, the second assumes it's of the form y == m*x + b. Neither one of those methods checks bounds, and I'd prefer not to slow them down by adding checks.

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.

In general, though, I would guess that clipping as part of the line search would give a better chance at converging to a solution that satisfies the bound (if such a solution exists).

Good point. I don't have a strong preference on the implementation if it is demonstrated to work, as long as this is opt-in behavior (for now).

Comment on lines +125 to +128
scale_problem: `bool`
Whether to take variable and constraint scaling into account
when calculating if the constraint residual tolerance is
satisfied or if the initial derivative is zero.

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.

If this is just scaling tolerances, I'm not sure it belongs in this function. The caller can always use the scaling factor to compute a new tolerance if they want. I also don't love calling SuffixFinder within this function.

@dallan-keylogic dallan-keylogic Jul 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

If this is just scaling tolerances, I'm not sure it belongs in this function.

It also applies scaling factors when calculating if a problem has a derivative of effectively zero.

The caller can always use the scaling factor to compute a new tolerance if they want.

That's true, but that means I'm going to move the scaling here into solve_strongly_connected_components.

I also don't love calling SuffixFinder within this function.

Is the concern that it adds too much overhead to each function call?

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.

2 participants