Skip to content

Initialization Module#3912

Open
michaelbynum wants to merge 85 commits into
Pyomo:mainfrom
michaelbynum:initialization
Open

Initialization Module#3912
michaelbynum wants to merge 85 commits into
Pyomo:mainfrom
michaelbynum:initialization

Conversation

@michaelbynum
Copy link
Copy Markdown
Contributor

@michaelbynum michaelbynum commented Apr 15, 2026

Fixes #3878

Summary/Motivation:

This PR adds a module to devel called initialization. The goal of the module is to provide methods to help initialize nonconvex nonlinear programming problems.

Changes proposed in this PR:

  • Add initialization module
  • Add an initialization method that uses a global optimization solver to find a feasible solution
  • Add an initialization method that builds a linear programming approximation using linear least squares
  • Add an initialization method that builds a piecewise linear approximation and iteratively refines it

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.

Comment thread doc/OnlineDocs/conf.py Outdated
Comment thread pyomo/devel/initialization/examples/__init__.py
Comment thread pyomo/devel/initialization/global_init.py Outdated
Comment thread pyomo/devel/initialization/lp_approx_init.py Outdated
Comment thread pyomo/devel/initialization/lp_approx_init.py Outdated
Comment thread pyomo/devel/initialization/pwl_init.py Outdated
Comment thread pyomo/devel/initialization/pwl_init.py Outdated
Comment thread pyomo/devel/initialization/utils.py Outdated
@blnicho blnicho requested a review from mrmundt May 27, 2026 17:28
@michaelbynum
Copy link
Copy Markdown
Contributor Author

Looks like tests are failing due to a cplex release yesterday...

Copy link
Copy Markdown
Member

@blnicho blnicho left a comment

Choose a reason for hiding this comment

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

@michaelbynum I have a few questions and suggestions but overall I think this looks great and I'm excited to try it out!

Comment thread doc/OnlineDocs/explanation/analysis/nlp_initialization/nlp_initialization.rst Outdated
`Design of Experiments`
`MPC`
`AOS`
`NLP Initialization`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this change was necessary. I'm pretty sure this is just an old comment block we were using to track the documentation reorganization effort.

Comment on lines +118 to +127
# in all cases, try to solve the nlp before doing extra work
res = nlp_solver.solve(
nlp, load_solutions=False, raise_exception_on_nonoptimal_result=False
)
logger.info(f'solved NLP: {res.solution_status}, {res.termination_condition}')

if res.solution_status == SolutionStatus.optimal:
res.solution_loader.load_vars()
logger.info('NLP solved without any initialization')
return res
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What do you think about adding an option to skip this initial NLP solve? I feel like this could add a lot of extra time for complex models and I can picture a scenario where a user has already tried and failed to solve it directly so they turn to this initialization tool and have to wait again for the NLP solver to fail (which could take a while even with the default solver settings).

Comment thread pyomo/devel/initialization/initialize.py
Comment thread pyomo/devel/initialization/initialize.py Outdated
Comment thread pyomo/devel/initialization/initialize.py
Comment on lines +83 to +87
lb = -1e6
else:
lb = v.lb
if v.ub is None:
ub = 1e6
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should these be using the default_bound instead of being hard-coded to 1e6?

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 this code should exist. We set the bounds to default_bound before we ever get to this function. I'm going to delete it. Good catch.

Copy link
Copy Markdown
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

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

This is really cool. Lots of comments / questions, but I don't think there was anything that should block merging.

@@ -0,0 +1 @@
The purpose of this module is to provide methods for initializing nonlinear programming models. No newline at end of file
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this readme necessary? Should it just have a pointer to the documentation?

constraints in the model, m. If variable bounds cannot be obtained,
we use default_bound.
"""
fbbt(m)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it worth only running FBBT on constraints that actually involve unbounded variables (instead of attempting to tighten all variables in the model)?

return opt


def initialize_nlp(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Design question: would it better to make this a context manager instead of a "global gateway for all initialization routines"? The problem with the "gateway function" is that:

  • it has to be updated every time we add a new strategy/method
  • the API must be the superset of all the options / arguments that any method takes (and then there is the question about warning about arguments that the user provided, but are ignored by the chosen method)

Instead, if you made this a context manager, it might look like:

with initialization_context(m):
    res = initialize_with_piecewise_linear_approximation(
        nlp=m,
        mip_solver="xpress",
        max_iter=5,
    )

And InitializationContext could be as simple as

@context_manager
def initialization_context(nlp):
    # get all variable bounds, domains, etc. to restore them later
    orig_var_data = [
        (v, v.lower, v.upper, v.domain, v.fixed, v.value) for v in get_vars(nlp)
    ]
    yield
    for v, lb, ub, domain, fixed, value in orig_var_data:
        v.setlb(lb)
        v.setub(ub)
        v.domain = domain
        if fixed:
            assert v.value == value
            assert v.fixed
        else:
            v.unfix()

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 know how to ensure users use the context manager. However, I agree. I'm going to just put this code into helper functions and ensure all the initialization functions call them.


# get all variable bounds, domains, etc. to restore them later
orig_vars = get_vars(nlp)
orig_var_data = ComponentMap(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't need to be a ComponentMap; a list is sufficient

Comment thread pyomo/devel/initialization/initialize.py
Comment on lines +89 to +96
elif lb is None:
ps = m.slacks.add()
ps.setlb(0)
con.set_value(body - ub - ps <= 0)
elif ub is None:
ns = m.slacks.add()
ns.setlb(0)
con.set_value(body - lb + ns >= 0)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Will you get an exception if the constraint is unbounded? Maybe catch that case above with

if lb == ub:
    if lb is not None: 
        ps = m.slacks.add()
        # ...
    else:
        # trivial constraint; no slacks needed
        pass

logger = logging.getLogger(__name__)


def _minimize_infeasibility(m):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is basically a reimplementation of the core.add_slack_variables transformation. Why not just use it?

_handlers[t] = _handle_node


class _PWLRefinementVisitor(StreamBasedExpressionVisitor):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I lost the thread on the logic here, but this seems to do way more expression manipulation than I would think is necessary. I would have thought this could have been done in one pass through the model where you substituted the nonlinear expressions with a new variable. If you kept a list of all the "new" constraints, you would have only had to rebuild the refined piecewise linearization of those expressions (no walker needed)?

Comment on lines +235 to +238
if val <= v.lb + 1e-6 + 1e-6 * abs(v.lb):
val += 1e-5
if val >= v.ub - 1e-6 - 1e-6 * abs(v.ub):
val -= 1e-5
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should these embedded constants be moved to either an argument or a global named constant?


if len(violations) == 0:
raise RuntimeError(
'Did not find any piecewise linear functions with variable values'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I do not understand this exception message. Can you clarify it?

TerminationCondition,
)
from pyomo.contrib.solver.common.base import Availability, SolverBase
import pytest
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
import pytest
from pyomo.common.unittest import pytest

michaelbynum and others added 2 commits May 29, 2026 18:02
…tialization.rst

Co-authored-by: Bethany Nicholson <blnicho@users.noreply.github.com>
Co-authored-by: Bethany Nicholson <blnicho@users.noreply.github.com>
@michaelbynum
Copy link
Copy Markdown
Contributor Author

Thanks for all the feedback! Working on this now.

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.

Piecewise Nonlinear => PWL nudge

4 participants