Skip to content

GDP Hull transform: add handling for cases of constraint functions not well-defined at the origin#3880

Open
sadavis1 wants to merge 33 commits intoPyomo:mainfrom
sadavis1:hull-fix
Open

GDP Hull transform: add handling for cases of constraint functions not well-defined at the origin#3880
sadavis1 wants to merge 33 commits intoPyomo:mainfrom
sadavis1:hull-fix

Conversation

@sadavis1
Copy link
Copy Markdown
Contributor

@sadavis1 sadavis1 commented Mar 23, 2026

Fixes # n/a

Summary/Motivation:

When applying the gdp.hull transformation to a model containing a constraint function whose evaluation is not well-defined when every variable is fixed at zero, the perspective functions used by the hull transformation are not well-defined either and lead to errors, especially when the mode is set to FurmanSawayaGrossmann (which is the useful one). This PR slightly alters the mathematical formulation to permit a nonzero base point which can be used in place of the origin, and adds a heuristic to try to magically find one by calling gurobi. When the zero point works, it uses that so Gurobi is never invoked.

Note: this is a minor merge conflict with #3874, if that is merged first I can rebase this

Changes proposed in this PR:

  • Alter mathematical formulation for gdp.hull transformation to include a base point
  • Add a heuristic to get a well-defined point. This is done at the pyomo level by solving a subproblem using a nonlinear solver, even though solvers usually have a way to do this sort of thing directly, because doing it directly requires setting many solver-specific options and in some cases triggers bugs in solvers (this happened for BARON during testing). This is done using a walker to generate constraints corresponding to each potentially ill-defined function evaluation.
  • Change the way LocalVars works to permit a category of variables that still need to be disaggregated now, but do not need to worry about global constraints and also do not need to be re-disaggregated in any parent disjunct if the GDP is nested ("generalized local vars"). This category is naturally occupied by local variables that have an offset (since the offset handling happens during disaggregation) but other variables may be placed in this category by the user by marking them LocalVars even though they appear on multiple disjuncts. Ordinary LocalVars occupying only one disjunct behave the same way as before.

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 88.23529% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.95%. Comparing base (ae0ae63) to head (24c4392).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
pyomo/gdp/plugins/hull.py 88.23% 22 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3880      +/-   ##
==========================================
+ Coverage   86.80%   89.95%   +3.15%     
==========================================
  Files         903      903              
  Lines      106604   106748     +144     
==========================================
+ Hits        92537    96027    +3490     
+ Misses      14067    10721    -3346     
Flag Coverage Δ
builders 29.19% <17.11%> (-0.03%) ⬇️
default 86.28% <88.23%> (?)
expensive 35.63% <17.11%> (?)
linux 87.42% <88.23%> (+1.03%) ⬆️
linux_other 87.42% <88.23%> (?)
oldsolvers 28.13% <17.11%> (-0.02%) ⬇️
osx 82.78% <88.23%> (?)
win 85.84% <88.23%> (?)
win_other 85.84% <88.23%> (?)

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 a review from emma58 March 24, 2026 18:48
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.

Thank you so much for this @sadavis1, some of this is quite unpleasant to think through! :P A lot of comments and questions, but overall this is looking good. @jsiirola, I left a couple questions for you in the comments too.

Comment thread pyomo/gdp/plugins/hull.py Outdated
Comment thread pyomo/gdp/plugins/hull.py Outdated
Comment thread pyomo/gdp/plugins/hull.py
Comment thread pyomo/gdp/plugins/hull.py
Comment thread pyomo/gdp/plugins/hull.py Outdated
Comment thread pyomo/gdp/plugins/hull.py Outdated
Comment thread pyomo/gdp/plugins/hull.py Outdated
Comment thread pyomo/gdp/plugins/hull.py Outdated
Comment thread pyomo/gdp/tests/test_hull.py Outdated
Comment thread pyomo/gdp/tests/test_hull.py Outdated
@emma58 emma58 requested a review from jsiirola March 24, 2026 22:14
@sadavis1
Copy link
Copy Markdown
Contributor Author

Updated for review comments

Copy link
Copy Markdown
Contributor

@bernalde bernalde left a comment

Choose a reason for hiding this comment

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

Please address the inline comments.

Comment thread pyomo/gdp/plugins/hull.py
local_vars[disj].add(var)
all_local_vars.add(var)
else:
vars_to_disaggregate[disjunct].add(var)
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.

Blocking: This branch uses the stale outer-loop disjunct variable instead of the current disj. For a variable marked LocalVars while appearing in multiple disjuncts, this only adds the variable to vars_to_disaggregate for the last active disjunct, leaving other disjuncts without a substitute. A simple two-disjunct model with x in both disjuncts and LocalVars entries for both currently fails during transformation with ValueError: No value for uninitialized ScalarVar object x.

Please use the current disjunct and add regression coverage for the generalized-local-vars case. The fix may need to ensure every disjunct in disjuncts_var_appears_in[var] gets a disaggregated variable once the variable is treated as generalized local.

Comment thread pyomo/gdp/plugins/hull.py
f"{e.__class__.__name__} (was prepared for success or a ValueError)."
)
raise
# Second, try making it well-defined by editing only the regular vars
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.

Blocking: _get_well_defined_point() mutates original model variables before calling the heuristic solver, but restores values/fixed states only on the successful path. If the solver is unavailable, errors, or no point is found, the user model is left changed. For example, with a fixed initialized variable, a failed hull transform due to missing gurobipy left x changed from value=5, fixed=True to value=0, fixed=False.

Please wrap the solve/search section in try/finally and restore orig_values and orig_fixed on all exits, including solver exceptions and the GDP_Error path.

self.assertEqual(m_pts[m.disjunction][m.a], 0)
self.assertEqual(m_pts[m.disjunction][m.x], 0)

@unittest.skipUnless(gurobi_available, "Gurobi is not available")
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.

Nonblocking: Most new domain-restriction coverage is skipped when Gurobi is unavailable, including the documented escape hatch for users who pass well_defined_points directly. Please add at least one non-Gurobi test that supplies a ComponentMap manually, for example transforming log(m.x - 1) with {m.disjunction: ComponentMap([(m.x, 2)])}.

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.

I don't think this is a concern--we have Gurobi in our testing infrastructure.

Comment thread pyomo/gdp/plugins/hull.py
""",
),
)
CONFIG.declare(
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.

Nonblocking: The class docstring says the transformation accepts keyword arguments but still lists only perspective_function, EPS, and targets. Since this PR adds public options, please document well_defined_points and well_defined_points_heuristic_solver there or point readers to the generated CONFIG documentation.

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.

5 participants