Skip to content

Commit c9811bf

Browse files
authored
Merge pull request #2473 from jsiirola/nl-writer-cache-expressions
NL Writer, version 2
2 parents 49b43c3 + 59eb104 commit c9811bf

38 files changed

+3799
-488
lines changed

doc/OnlineDocs/conf.py

+26-14
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ def check_output(self, want, got, optionflags):
245245
doctest.OutputChecker = IgnoreResultOutputChecker
246246

247247
doctest_global_setup = '''
248+
import os, platform, sys
249+
on_github_actions = bool(os.environ.get('GITHUB_ACTIONS', ''))
250+
system_info = (
251+
sys.platform,
252+
platform.machine(),
253+
platform.python_implementation()
254+
)
255+
248256
from pyomo.common.dependencies import (
249257
attempt_import, numpy_available, scipy_available, pandas_available,
250258
yaml_available, networkx_available, matplotlib_available,
@@ -253,26 +261,30 @@ def check_output(self, want, got, optionflags):
253261
pint_available = attempt_import('pint', defer_check=False)[1]
254262
from pyomo.contrib.parmest.parmest import parmest_available
255263
256-
import pyomo.opt
264+
import pyomo.environ as _pe # (trigger all plugin registrations)
265+
import pyomo.opt as _opt
266+
257267
# Not using SolverFactory to check solver availability because
258268
# as of June 2020 there is no way to supress warnings when
259269
# solvers are not available
260-
ipopt_available = bool(pyomo.opt.check_available_solvers('ipopt'))
261-
sipopt_available = bool(pyomo.opt.check_available_solvers('ipopt_sens'))
262-
k_aug_available = bool(pyomo.opt.check_available_solvers('k_aug'))
263-
dot_sens_available = bool(pyomo.opt.check_available_solvers('dot_sens'))
264-
baron_available = bool(pyomo.opt.check_available_solvers('baron'))
265-
glpk_available = bool(pyomo.opt.check_available_solvers('glpk'))
266-
baron = pyomo.opt.SolverFactory('baron')
267-
gurobipy_available = bool(pyomo.opt.check_available_solvers('gurobi_direct'))
270+
ipopt_available = bool(_opt.check_available_solvers('ipopt'))
271+
sipopt_available = bool(_opt.check_available_solvers('ipopt_sens'))
272+
k_aug_available = bool(_opt.check_available_solvers('k_aug'))
273+
dot_sens_available = bool(_opt.check_available_solvers('dot_sens'))
274+
baron_available = bool(_opt.check_available_solvers('baron'))
275+
glpk_available = bool(_opt.check_available_solvers('glpk'))
276+
gurobipy_available = bool(_opt.check_available_solvers('gurobi_direct'))
277+
278+
baron = _opt.SolverFactory('baron')
279+
268280
if numpy_available and scipy_available:
269-
from pyomo.contrib.pynumero.asl import AmplInterface
270-
asl_available = AmplInterface.available()
281+
import pyomo.contrib.pynumero.asl as _asl
282+
asl_available = _asl.AmplInterface.available()
283+
import pyomo.contrib.pynumero.linalg.ma27 as _ma27
284+
ma27_available = _ma27.MA27Interface.available()
271285
from pyomo.contrib.pynumero.linalg.mumps_interface import mumps_available
272-
from pyomo.contrib.pynumero.linalg.ma27 import MA27Interface
273-
ma27_available = MA27Interface.available()
274286
else:
275287
asl_available = False
276-
mumps_available = False
277288
ma27_available = False
289+
mumps_available = False
278290
'''

doc/OnlineDocs/errors.rst

+79-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Common Warnings/Errors
1616
.. py:currentmodule:: pyomo.environ
1717
1818
19+
.. ===================================================================
20+
.. Extended descriptions for Pyomo warnings
21+
.. ===================================================================
22+
1923
Warnings
2024
--------
2125

@@ -51,6 +55,7 @@ Users can bypass all domain validation by setting the value using:
5155
0.75
5256

5357

58+
5459
.. _W1002:
5560

5661
W1002: Setting Var value outside the bounds
@@ -82,6 +87,72 @@ Users can bypass all domain validation by setting the value using:
8287
10
8388

8489

90+
91+
.. _W1003:
92+
93+
W1003: Unexpected RecursionError walking an expression tree
94+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
95+
96+
Pyomo leverages a recursive walker (the
97+
:py:class:`~pyomo.core.expr.visitor.StreamBasedExpressionVisitor`) to
98+
traverse (walk) expression trees. For most expressions, this recursive
99+
walker is the most efficient. However, Python has a relatively shallow
100+
recursion limit (generally, 1000 frames). The recursive walker is
101+
designed to monitor the stack depth and cleanly switch to a nonrecursive
102+
walker before hitting the stack limit. However, there are two (rare)
103+
cases where the Python stack limit can still generate a
104+
:py:exc:`RecursionError` exception:
105+
106+
#. Starting the walker with fewer than
107+
:py:data:`pyomo.core.expr.visitor.RECURSION_LIMIT` available frames.
108+
#. Callbacks that require more than 2 *
109+
:py:data:`pyomo.core.expr.visitor.RECURSION_LIMIT` frames.
110+
111+
The (default) recursive walker will catch the exception and restart the
112+
walker from the beginning in non-recursive mode, issuing this warning.
113+
The caution is that any partial work done by the walker before the
114+
exception was raised will be lost, potentially leaving the walker in an
115+
inconsistent state. Users can avoid this by
116+
117+
- avoiding recursive callbacks
118+
- restructuring the system design to avoid triggering the walker with
119+
few available stack frames
120+
- directly calling the
121+
:py:meth:`~pyomo.core.expr.visitor.StreamBasedExpressionVisitor.walk_expression_nonrecursive()`
122+
walker method
123+
124+
.. doctest::
125+
:skipif: (on_github_actions and system_info[0].startswith('win')) \
126+
or system_info[2] == 'PyPy'
127+
128+
>>> import sys
129+
>>> import pyomo.core.expr.visitor as visitor
130+
>>> from pyomo.core.tests.unit.test_visitor import fill_stack
131+
>>> expression_depth = visitor.StreamBasedExpressionVisitor(
132+
... exitNode=lambda node, data: max(data) + 1 if data else 1)
133+
>>> m = pyo.ConcreteModel()
134+
>>> m.x = pyo.Var()
135+
>>> @m.Expression(range(35))
136+
... def e(m, i):
137+
... return m.e[i-1] if i else m.x
138+
>>> expression_depth.walk_expression(m.e[34])
139+
36
140+
>>> fill_stack(sys.getrecursionlimit() - visitor.get_stack_depth() - 30,
141+
... expression_depth.walk_expression,
142+
... m.e[34])
143+
WARNING (W1003): Unexpected RecursionError walking an expression tree.
144+
See also https://pyomo.readthedocs.io/en/stable/errors.html#w1003
145+
36
146+
>>> fill_stack(sys.getrecursionlimit() - visitor.get_stack_depth() - 30,
147+
... expression_depth.walk_expression_nonrecursive,
148+
... m.e[34])
149+
36
150+
151+
152+
.. ===================================================================
153+
.. Extended descriptions for Pyomo errors
154+
.. ===================================================================
155+
85156
Errors
86157
------
87158

@@ -110,7 +181,12 @@ this warning (and an exception from the converter):
110181
See also https://pyomo.readthedocs.io/en/stable/errors.html#e2001
111182

112183

113-
Exceptions
114-
----------
115184

116-
.. _X101:
185+
.. ===================================================================
186+
.. Extended descriptions for Pyomo exceptions
187+
.. ===================================================================
188+
189+
.. Exceptions
190+
.. ----------
191+
192+
.. .. _X101:

pyomo/common/tests/test_timing.py

+79-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import gc
1616
from io import StringIO
17+
import logging
1718
import sys
1819
import time
1920

@@ -23,6 +24,7 @@
2324
TicTocTimer, HierarchicalTimer,
2425
)
2526
from pyomo.environ import ConcreteModel, RangeSet, Var, TransformationFactory
27+
from pyomo.core.base.var import _VarData
2628

2729
class _pseudo_component(Var):
2830
def getname(*args, **kwds):
@@ -40,9 +42,24 @@ def tearDown(self):
4042

4143
def test_raw_construction_timer(self):
4244
a = ConstructionTimer(None)
43-
self.assertIn(
44-
"ConstructionTimer object for NoneType (unknown); ",
45-
str(a))
45+
self.assertRegex(
46+
str(a),
47+
r"ConstructionTimer object for NoneType \(unknown\); "
48+
r"[0-9\.]+ elapsed seconds")
49+
v = Var()
50+
v.construct()
51+
a = ConstructionTimer(_VarData(v))
52+
self.assertRegex(
53+
str(a),
54+
r"ConstructionTimer object for Var ScalarVar\[NOTSET\]; "
55+
r"[0-9\.]+ elapsed seconds")
56+
57+
def test_raw_transformation_timer(self):
58+
a = TransformationTimer(None)
59+
self.assertRegex(
60+
str(a),
61+
r"TransformationTimer object for NoneType; "
62+
r"[0-9\.]+ elapsed seconds")
4663

4764
v = _pseudo_component()
4865
a = ConstructionTimer(v)
@@ -223,6 +240,65 @@ def test_TicTocTimer_context_manager(self):
223240
self.assertGreater(abs_time, SLEEP*3 - RES/10)
224241
self.assertAlmostEqual(timer.toc(None), abs_time - exclude, delta=RES)
225242

243+
def test_TicTocTimer_logger(self):
244+
# test specifying a logger to the timer constructor disables all
245+
# output to stdout
246+
timer = TicTocTimer(logger=logging.getLogger(__name__))
247+
with capture_output() as OUT:
248+
with LoggingIntercept(level=logging.INFO) as LOG:
249+
timer.toc("msg1")
250+
self.assertRegex(LOG.getvalue(), r"^\[[^\]]+\] msg1$")
251+
with LoggingIntercept(level=logging.INFO) as LOG:
252+
timer.toc("msg2", level=logging.DEBUG)
253+
self.assertEqual(LOG.getvalue(), "")
254+
self.assertEqual(OUT.getvalue(), "")
255+
256+
with capture_output() as OUT:
257+
with LoggingIntercept(level=logging.DEBUG) as LOG:
258+
timer.toc("msg1")
259+
timer.toc("msg2", level=logging.DEBUG)
260+
self.assertEqual(OUT.getvalue(), "")
261+
self.assertRegex(LOG.getvalue(), r"^\[[^\]]+\] msg1\n\[[^\]]+\] msg2$")
262+
263+
# test specifying a logger to toc() for a general timer disables
264+
# output to stdout
265+
timer = TicTocTimer()
266+
with capture_output() as OUT:
267+
with LoggingIntercept(level=logging.INFO) as LOG:
268+
timer.toc("msg1", logger=logging.getLogger(__name__))
269+
self.assertRegex(LOG.getvalue(), r"^\[[^\]]+\] msg1$")
270+
self.assertEqual(OUT.getvalue(), "")
271+
272+
def test_TicTocTimer_deprecated(self):
273+
timer = TicTocTimer()
274+
with LoggingIntercept() as LOG, capture_output() as out:
275+
timer.tic("msg", None, None)
276+
self.assertEqual(out.getvalue(), "")
277+
self.assertRegex(
278+
LOG.getvalue().replace('\n', ' ').strip(),
279+
r"DEPRECATED: tic\(\): 'ostream' and 'logger' should be specified "
280+
r"as keyword arguments( +\([^\)]+\)){2}")
281+
282+
with LoggingIntercept() as LOG, capture_output() as out:
283+
timer.toc("msg", True, None, None)
284+
self.assertEqual(out.getvalue(), "")
285+
self.assertRegex(
286+
LOG.getvalue().replace('\n', ' ').strip(),
287+
r"DEPRECATED: toc\(\): 'delta', 'ostream', and 'logger' should be "
288+
r"specified as keyword arguments( +\([^\)]+\)){2}")
289+
290+
timer = TicTocTimer()
291+
with LoggingIntercept() as LOG, capture_output() as out:
292+
timer.tic("msg %s, %s", None, None)
293+
self.assertIn('msg None, None', out.getvalue())
294+
self.assertEqual(LOG.getvalue(), "")
295+
296+
with LoggingIntercept() as LOG, capture_output() as out:
297+
timer.toc("msg %s, %s, %s", True, None, None)
298+
self.assertIn('msg True, None, None', out.getvalue())
299+
self.assertEqual(LOG.getvalue(), "")
300+
301+
226302
def test_HierarchicalTimer(self):
227303
RES = 0.01 # resolution (seconds)
228304

0 commit comments

Comments
 (0)