Summary
gdpopt.enumerate materializes the complete list of discrete solutions before it checks time_limit or iterlim. For GDPs with many disjunctions, this can exhaust memory or appear to ignore the configured time limit before any subproblem solve starts.
The issue is in the setup phase of GDP_Enumeration_Solver._solve_gdp(): it does
discrete_solns = list(
self._discrete_solution_iterator(...)
)
self.num_discrete_solns = len(discrete_solns)
for soln in discrete_solns:
if self.reached_time_limit(config) or self.reached_iteration_limit(config):
break
so the time-limit check happens only after the full Cartesian product has been built.
Steps to reproduce the issue
This solver-free example forces the time-limit predicate to be true before enumeration starts, then raises if the discrete-solution iterator is still called. It demonstrates the ordering issue without requiring a large model, GAMS, Gurobi, IPOPT, or another external solver.
$ python enumerate_time_limit_mwe.py
# enumerate_time_limit_mwe.py
import pyomo.environ as pyo
from pyomo.gdp import Disjunct, Disjunction
from pyomo.contrib.gdpopt.enumerate import GDP_Enumeration_Solver
print("has enumerate solver", pyo.SolverFactory("gdpopt.enumerate").available())
def fail_if_enumerated(self, *args, **kwargs):
raise RuntimeError("enumeration happened before the time-limit check")
# Simulate entering _solve_gdp after the time limit has already expired.
GDP_Enumeration_Solver.reached_time_limit = lambda self, config: True
GDP_Enumeration_Solver._discrete_solution_iterator = fail_if_enumerated
m = pyo.ConcreteModel()
m.x = pyo.Var(bounds=(0, 2))
m.d1 = Disjunct()
m.d2 = Disjunct()
m.d1.c = pyo.Constraint(expr=m.x <= 0.5)
m.d2.c = pyo.Constraint(expr=m.x >= 1.5)
m.disj = Disjunction(expr=[m.d1, m.d2])
m.obj = pyo.Objective(expr=m.x)
pyo.SolverFactory("gdpopt.enumerate").solve(m, time_limit=1, tee=False)
Error Message
The current behavior calls _discrete_solution_iterator() before returning on the already-expired time limit:
$ python enumerate_time_limit_mwe.py
has enumerate solver True
Traceback (most recent call last):
File "enumerate_time_limit_mwe.py", line 25, in <module>
pyo.SolverFactory("gdpopt.enumerate").solve(m, time_limit=1, tee=False)
File ".../pyomo/contrib/gdpopt/enumerate.py", line 71, in solve
return super().solve(model, **kwds)
File ".../pyomo/contrib/gdpopt/algorithm_base_class.py", line 128, in solve
self._solve_gdp(model, config)
File ".../pyomo/contrib/gdpopt/enumerate.py", line 128, in _solve_gdp
self._discrete_solution_iterator(
File "enumerate_time_limit_mwe.py", line 10, in fail_if_enumerated
raise RuntimeError("enumeration happened before the time-limit check")
RuntimeError: enumeration happened before the time-limit check
For a real downstream model with 32 active binary disjunction choices, this same eager list(...) construction attempts to materialize 2**32 discrete choices before the time limit can be honored, which can exhaust memory or make the machine unresponsive.
Information on your system
Pyomo version: 6.10.0
Python version: 3.12.13
Operating system: Linux WSL2, glibc 2.39
How Pyomo was installed (PyPI, conda, source): conda-forge package through Pixi
Solver (if applicable): not applicable for the MWE; downstream evidence used GAMS/DICOPT, GAMS/IPOPTH, and GAMS/Gurobi role solvers
Additional information
Expected behavior: gdpopt.enumerate should not need to build the entire discrete solution list before enforcing time_limit or iterlim. It should iterate lazily, or otherwise check limits while generating choices. This would avoid memory exhaustion and would make the documented time limit meaningful for large discrete spaces.
Downstream context from GDPlib:
In GDPlib's mod_hens model, issue SECQUOIA/gdplib#71 exposed this behavior. Before the model-side mitigation, the default formulation had 32 active exchanger disjunctions, 20 of which represented structurally fixed absent choices. gdpopt.enumerate appeared not to respect a 60-second time limit because it was materializing the full discrete solution list first. After pruning those fixed choices from the active GDP structure in SECQUOIA/gdplib#140, the same 60-second gdpopt.enumerate probe returned normally with maxTimeLimit at about 60.10 seconds and reported 12 disjunctions.
This is distinct from, but adjacent to, the LBB time-limit finalization issue reported in #3941.
Summary
gdpopt.enumeratematerializes the complete list of discrete solutions before it checkstime_limitoriterlim. For GDPs with many disjunctions, this can exhaust memory or appear to ignore the configured time limit before any subproblem solve starts.The issue is in the setup phase of
GDP_Enumeration_Solver._solve_gdp(): it doesso the time-limit check happens only after the full Cartesian product has been built.
Steps to reproduce the issue
This solver-free example forces the time-limit predicate to be true before enumeration starts, then raises if the discrete-solution iterator is still called. It demonstrates the ordering issue without requiring a large model, GAMS, Gurobi, IPOPT, or another external solver.
$ python enumerate_time_limit_mwe.pyError Message
The current behavior calls
_discrete_solution_iterator()before returning on the already-expired time limit:For a real downstream model with 32 active binary disjunction choices, this same eager
list(...)construction attempts to materialize2**32discrete choices before the time limit can be honored, which can exhaust memory or make the machine unresponsive.Information on your system
Pyomo version: 6.10.0
Python version: 3.12.13
Operating system: Linux WSL2, glibc 2.39
How Pyomo was installed (PyPI, conda, source): conda-forge package through Pixi
Solver (if applicable): not applicable for the MWE; downstream evidence used GAMS/DICOPT, GAMS/IPOPTH, and GAMS/Gurobi role solvers
Additional information
Expected behavior:
gdpopt.enumerateshould not need to build the entire discrete solution list before enforcingtime_limitoriterlim. It should iterate lazily, or otherwise check limits while generating choices. This would avoid memory exhaustion and would make the documented time limit meaningful for large discrete spaces.Downstream context from GDPlib:
mod_hensto fix benchmark issues SECQUOIA/gdplib#71In GDPlib's
mod_hensmodel, issue SECQUOIA/gdplib#71 exposed this behavior. Before the model-side mitigation, the default formulation had 32 active exchanger disjunctions, 20 of which represented structurally fixed absent choices.gdpopt.enumerateappeared not to respect a 60-second time limit because it was materializing the full discrete solution list first. After pruning those fixed choices from the active GDP structure in SECQUOIA/gdplib#140, the same 60-secondgdpopt.enumerateprobe returned normally withmaxTimeLimitat about 60.10 seconds and reported 12 disjunctions.This is distinct from, but adjacent to, the LBB time-limit finalization issue reported in #3941.