Exact hull reformulation for quadratic constraints#3874
Exact hull reformulation for quadratic constraints#3874sergey-gusev94 wants to merge 50 commits intoPyomo:mainfrom
Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Make eigenvalue PSD/NSD tolerance configurable in hull exact quadratic reformulation
Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
…ic feature Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
doc: add `exact_hull_quadratic` to Hull class docstring
…unctions Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
…envalue_tolerance in permissive test Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
Add TestExactHullQuadratic tests and extract models into models.py
Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Clean up stale "treated as linear" language and inline imports in test_hull.py
Co-authored-by: sergey-gusev94 <101810399+sergey-gusev94@users.noreply.github.com>
Reorder hull CONFIG: declare `exact_hull_quadratic` before `eigenvalue_tolerance`
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
added exact hull reformulations
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3874 +/- ##
==========================================
+ Coverage 89.68% 89.70% +0.01%
==========================================
Files 908 908
Lines 106753 106906 +153
==========================================
+ Hits 95740 95895 +155
+ Misses 11013 11011 -2
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:
|
emma58
left a comment
There was a problem hiding this comment.
This looks pretty good, but I'm submitting a partial review because there's one major revision: See my notes below, but can you please rewrite this to use the QuadraticRepnVisitor rather than generate_standard_repn? You'll be able to get all the same data, though it will come in a slightly different structure. But it is much more robust to edge cases--I think generate_standard_repn is not long for this world, actually.
| [p.name for p in mutable_params], | ||
| ) | ||
|
|
||
| repn = generate_standard_repn(c.body) |
There was a problem hiding this comment.
Sorry, this isn't documented yet, but we are gradually replacing generate_standard_repn with more robust expression visitors. For your purposes, you should use the QuadraticRepnVisitor from pyomo.repn.quadratic.
You will also want to account for the assume_fixed_vars_permanent option when you do this. Right now, the easiest way to do that when you are not assuming they are permanent (the default) is to actually unfix them before you walk the expression and then fix them back to their old values after the fact. The same Hessian issue would happen if they are treated as params (the True option) but in this case it's actually okay because setting that option to True is promising to not unfix them or change their values in any way.
| """Transform a single Constraint on a Disjunct. | ||
|
|
||
| Applies the appropriate hull reformulation to each | ||
| ``ConstraintData`` in ``obj``. When ``exact_hull_quadratic`` is | ||
| enabled and the constraint body has polynomial degree 2, an exact | ||
| hull formulation is used instead of the perspective function. | ||
|
|
| NL = c.body.polynomial_degree() not in (0, 1) | ||
| polynomial_degree = c.body.polynomial_degree() |
There was a problem hiding this comment.
There are some known issues with polynomial_degree... I think that, since we're going to need it anyway, it probably makes sense to use the QuadraticRepnVisitor here to determine the nature of the Constraint body. That will be more robust to some strange edge cases.
| if mutable_params: | ||
| logger.warning( | ||
| "GDP(Hull): Constraint '%s' contains mutable parameters %s. " | ||
| "The exact hull reformulation evaluates these to their current " | ||
| "numeric values for the eigenvalue-based convexity check; the " | ||
| "transformed constraint will not update if these parameters " | ||
| "change later.", | ||
| c.getname(fully_qualified=True), | ||
| [p.name for p in mutable_params], | ||
| ) |
There was a problem hiding this comment.
I would lean towards putting this fact in the documentation rather than logging a warning, since there's nothing a user can do to prevent this warning other than actually going through and replacing mutable params with immutable ones. But we should definitely make this loud in the docs: It's a departure from the usual behavior of GDP transformations.
| all_vars = [] | ||
| var_ids_seen = set() | ||
| for var_i, var_j in repn.quadratic_vars: | ||
| if id(var_i) not in var_ids_seen: | ||
| all_vars.append(var_i) | ||
| var_ids_seen.add(id(var_i)) | ||
| if id(var_j) not in var_ids_seen: | ||
| all_vars.append(var_j) | ||
| var_ids_seen.add(id(var_j)) | ||
|
|
||
| n_vars = len(all_vars) | ||
| var_to_idx = {id(var): idx for idx, var in enumerate(all_vars)} |
There was a problem hiding this comment.
It looks like you're building several data structures you don't need here. You never use all_vars for anything, and the keys of var_to_index are the var_ids_seen set. So I propose replacing these lines with:
var_to_idx = {}
for quad_vars in repn.quadratic_vars:
for var_i in quad_vars:
if id(var_i) not in var_to_idx:
var_to_idx[id(var_i)] = len(var_to_idx)
n_vars = len(var_to_idx)
Fixes # .
Summary/Motivation:
This PR adds support for the exact hull reformulation for quadratic constraints in Generalized Disjunctive Programming (GDP).
The current
gdp.hulltransformation applies perspective-function based relaxations for nonlinear constraints. While effective for many nonlinear expressions, this approach can produce weaker relaxations for quadratically constrained disjunctions.This PR implements the exact hull reformulation for quadratic constraints described in:
Gusev, S., & Bernal Neira, D. E. (2025).
Exact Hull Reformulation for Quadratically Constrained Generalized Disjunctive Programs.
https://arxiv.org/abs/2508.16093
The implementation extends the existing hull transformation with an optional configuration flag (which is
Falseby default) that detects quadratic constraints and applies the exact hull formulation when appropriate.For convex quadratic constraints, the reformulation is expressed as a rotated second-order cone (SOC) representable constraint. For nonconvex quadratics and equality constraints, the general exact hull formulation is used.
Convexity is determined automatically through eigenvalue analysis of the Hessian matrix.
This provides a tighter relaxation for quadratic GDP models and maintains the quadratic structure of constraints in hull reformulation.
Changes proposed in this PR:
Extend the
gdp.hulltransformation with a new configuration option:exact_hull_quadratic(default:False)When enabled, quadratic constraints inside disjuncts are reformulated using the exact hull formulation instead of the standard perspective function.
Implement automatic convexity detection via eigenvalue decomposition of the quadratic Hessian matrix.
Apply different reformulations depending on structure:
Convex quadratic inequalities
Reformulated using a rotated second-order cone representation.
Nonconvex quadratics or equalities
Reformulated using the general exact hull formulation.
Add a configuration parameter:
eigenvalue_toleranceControls numerical tolerance when checking positive or negative semidefiniteness of the Hessian.
Extend the implementation in
pyomo.gdp.plugins.hullto support these reformulations.Add unit tests verifying:
The default behavior of
gdp.hullis unchanged unless the new option is enabled.Example usage:
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: