Skip to content

GDPopt enumerate materializes all discrete solutions before checking time_limit #3953

@bernalde

Description

@bernalde

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions