Description
Summary
I have a model that defines the value of a Pyomo Expression
for each time step as the value of the same expression in the previous time step plus the value of a decision variable. This is a common structure for models of energy systems with storage, e.g., the state of charge of a battery or the temperature of a water heater.
The Expression
seems to be defined OK, but if there are many time steps and I define a Constraint
that uses this expression for a late time step, it causes a RecursionError
during presolve. This happens when constraint_generator
calls generate_standard_repn(constraint_data.body)
. That sets off a recursive sequence that goes through the same four functions for each time step back to the start. If there are more than 250 time steps, this results in a 1000-layer call stack, which creates a RecursionError
. (You need 750 steps to reproduce this in Jupyter, which has a 3000 call recursion limit.)
Steps to reproduce the issue
Running the code below will generate the error RecursionError: maximum recursion depth exceeded
.
import pyomo.environ as pyo
from pyomo.repn import generate_standard_repn
m = pyo.ConcreteModel()
m.STEPS = pyo.Set(initialize=range(1000), ordered=True)
m.Rate = pyo.Var(within=pyo.NonNegativeReals)
m.Level = pyo.Expression(
m.STEPS,
rule=lambda m, t: (0.5 if t == 0 else m.Level[m.STEPS.prev(t)]) + m.Rate
)
m.Min_Level = pyo.Constraint(rule=lambda m: m.Level[m.STEPS.last()] >= 0.5)
m.Cost = pyo.Objective(rule=lambda m: m.Rate)
opt = pyo.SolverFactory("glpk")
opt.solve(m)
# direct path to the error (called during opt.solve(m)):
# repn = generate_standard_repn(m.Min_Level.body)
Error Message
$ python chain_test.py
Traceback (most recent call last):
File "chain_test.py", line 21, in <module>
opt.solve(m)
File ".../pyomo/opt/base/solvers.py", line 575, in solve
self._presolve(*args, **kwds)
File ".../pyomo/opt/solver/shellcmd.py", line 198, in _presolve
OptSolver._presolve(self, *args, **kwds)
File ".../pyomo/opt/base/solvers.py", line 672, in _presolve
self._convert_problem(args,
File ".../pyomo/opt/base/solvers.py", line 742, in _convert_problem
return convert_problem(args,
File ".../pyomo/opt/base/convert.py", line 105, in convert_problem
problem_files, symbol_map = converter.apply(*tmp, **tmpkw)
File ".../pyomo/solvers/plugins/converter/model.py", line 84, in apply
instance.write(
File ".../pyomo/core/base/block.py", line 1811, in write
(filename, smap) = problem_writer(self,
File ".../pyomo/repn/plugins/cpxlp.py", line 161, in __call__
symbol_map = self._print_model_LP(
File ".../pyomo/repn/plugins/cpxlp.py", line 610, in _print_model_LP
for constraint_data, repn in yield_all_constraints():
File ".../pyomo/repn/plugins/cpxlp.py", line 593, in constraint_generator
repn = generate_standard_repn(constraint_data.body)
File ".../pyomo/repn/standard_repn.py", line 372, in generate_standard_repn
return _generate_standard_repn(expr,
File ".../pyomo/repn/standard_repn.py", line 983, in _generate_standard_repn
ans = _collect_standard_repn(expr, 1, idMap, compute_values, verbose, quadratic)
File ".../pyomo/repn/standard_repn.py", line 950, in _collect_standard_repn
return fn(exp, multiplier, idMap, compute_values, verbose, quadratic)
File ".../pyomo/repn/standard_repn.py", line 862, in _collect_identity
return _collect_standard_repn(exp.expr, multiplier, idMap, compute_values, verbose, quadratic)
File ".../pyomo/repn/standard_repn.py", line 950, in _collect_standard_repn
return fn(exp, multiplier, idMap, compute_values, verbose, quadratic)
File ".../pyomo/repn/standard_repn.py", line 477, in _collect_sum
res_ = _collect_standard_repn(e_, multiplier, idMap,
File ".../pyomo/repn/standard_repn.py", line 950, in _collect_standard_repn
return fn(exp, multiplier, idMap, compute_values, verbose, quadratic)
File ".../pyomo/repn/standard_repn.py", line 862, in _collect_identity
return _collect_standard_repn(exp.expr, multiplier, idMap, compute_values, verbose, quadratic)
File ".../pyomo/repn/standard_repn.py", line 950, in _collect_standard_repn
return fn(exp, multiplier, idMap, compute_values, verbose, quadratic)
File ".../pyomo/repn/standard_repn.py", line 477, in _collect_sum
res_ = _collect_standard_repn(e_, multiplier, idMap,
... many repetitions of the previous four calls ...
File ".../pyomo/repn/standard_repn.py", line 950, in _collect_standard_repn
return fn(exp, multiplier, idMap, compute_values, verbose, quadratic)
File ".../pyomo/repn/standard_repn.py", line 477, in _collect_sum
res_ = _collect_standard_repn(e_, multiplier, idMap,
File ".../pyomo/repn/standard_repn.py", line 950, in _collect_standard_repn
return fn(exp, multiplier, idMap, compute_values, verbose, quadratic)
File ".../pyomo/repn/standard_repn.py", line 855, in _collect_identity
if exp._args_[0].__class__ in native_numeric_types:
RecursionError: maximum recursion depth exceeded
Information on your system
Pyomo version: 5.7.3
Python version: CPython 3.8.0
Operating system: macOS 10.14.6, Darwin 18.7.0
How Pyomo was installed (PyPI, conda, source): conda or source
Solver (if applicable): glpk
Additional information
As noted in a comment at the end of the code, you can also reproduce the error by calling generate_standard_repn
directly on the body of the constraint.
It is possible to workaround this by calculating the Expression
directly as a sum of the initial level plus all adjustments since then (without reference to prior levels), like this:
m.Level = pyo.Expression(
m.STEPS,
rule=lambda m, t: 0.5 + sum(m.Rate for t_ in m.STEPS if t_ < t)
)
But this is less clear and more error-prone than chaining levels from one step to the next. I had to introduce a dummy time-step variable and make the (possibly incorrect) assumption that members of the STEPS set are sorted.