Skip to content

Constraints on chained expressions cause RecursionError in generate_standard_repn #1956

Open
@mfripp

Description

@mfripp

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions