From 1184be9cb3b040a9d9be24c4be5e1ee0efdd6a81 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 31 Jul 2023 15:51:58 -0600 Subject: [PATCH 01/21] Centralize the definition of nan, sum_like_expression --- pyomo/repn/linear.py | 10 ++++------ pyomo/repn/plugins/nl_writer.py | 4 +++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index a963a5b9216..3b6a7ecffb1 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -42,21 +42,19 @@ import pyomo.core.kernel as kernel from pyomo.repn.util import ( ExprType, + InvalidNumber, apply_node_operation, complex_number_error, - InvalidNumber, + nan, + sum_like_expression_types, ) logger = logging.getLogger(__name__) -nan = float("nan") - _CONSTANT = ExprType.CONSTANT _LINEAR = ExprType.LINEAR _GENERAL = ExprType.GENERAL -_SumLikeExpression = {SumExpression, LinearExpression, NPV_SumExpression} - def _merge_dict(dest_dict, mult, src_dict): if mult == 1: @@ -879,7 +877,7 @@ def beforeChild(self, node, child, child_idx): def enterNode(self, node): # SumExpression are potentially large nary operators. Directly # populate the result - if node.__class__ in _SumLikeExpression: + if node.__class__ in sum_like_expression_types: return node.args, self.Result() else: return node.args, [] diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index c7d10883461..b122b272c0b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -77,6 +77,8 @@ complex_number_error, initialize_var_map_from_column_order, ordered_active_constraints, + nan, + sum_like_expression_types, ) from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env @@ -2448,7 +2450,7 @@ def beforeChild(self, node, child, child_idx): def enterNode(self, node): # SumExpression are potentially large nary operators. Directly # populate the result - if node.__class__ is SumExpression: + if node.__class__ in sum_like_expression_types: data = AMPLRepn(0, {}, None) data.nonlinear = [] return node.args, data From 9b462dce4587885a0f579318b14588f932d390b3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 31 Jul 2023 15:55:25 -0600 Subject: [PATCH 02/21] Centralize processing of fixed values --- pyomo/repn/linear.py | 112 ++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 70 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 3b6a7ecffb1..f0b92ccc922 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -554,12 +554,7 @@ def _before_var(visitor, child): _id = id(child) if _id not in visitor.var_map: if child.fixed: - ans = child() - if ans is None or ans != ans: - ans = InvalidNumber(nan) - elif ans.__class__ in _complex_types: - ans = complex_number_error(ans, visitor, child) - return False, (_CONSTANT, ans) + return False, (_CONSTANT, visitor._eval_fixed(child)) visitor.var_map[_id] = child visitor.var_order[_id] = len(visitor.var_order) ans = visitor.Result() @@ -568,31 +563,13 @@ def _before_var(visitor, child): def _before_param(visitor, child): - ans = child() - if ans is None or ans != ans: - ans = InvalidNumber(nan) - elif ans.__class__ in _complex_types: - ans = complex_number_error(ans, visitor, child) - return False, (_CONSTANT, ans) + return False, (_CONSTANT, visitor._eval_fixed(child)) def _before_npv(visitor, child): - # TBD: It might be more efficient to cache the value of NPV - # expressions to avoid duplicate evaluations. However, current - # examples do not benefit from this cache. - # - # _id = id(child) - # if _id in visitor.value_cache: - # child = visitor.value_cache[_id] - # else: - # child = visitor.value_cache[_id] = child() - # return False, (_CONSTANT, child) try: return False, (_CONSTANT, visitor._eval_expr(child)) - except: - # If there was an exception evaluating the subexpression, then - # we need to descend into it (in case there is something like 0 * - # nan that we need to map to 0) + except (ValueError, ArithmeticError): return True, None @@ -603,39 +580,29 @@ def _before_monomial(visitor, child): # arg1, arg2 = child._args_ if arg1.__class__ not in native_types: - # TBD: It might be more efficient to cache the value of NPV - # expressions to avoid duplicate evaluations. However, current - # examples do not benefit from this cache. - # - # _id = id(arg1) - # if _id in visitor.value_cache: - # arg1 = visitor.value_cache[_id] - # else: - # arg1 = visitor.value_cache[_id] = arg1() try: arg1 = visitor._eval_expr(arg1) - except: - # If there was an exception evaluating the subexpression, - # then we need to descend into it (in case there is something - # like 0 * nan that we need to map to 0) + except (ValueError, ArithmeticError): return True, None # Trap multiplication by 0 and nan. if not arg1: - if arg2.fixed and arg2.value != arg2.value: - deprecation_warning( - f"Encountered {arg1}*{str(arg2.value)} in expression tree. " - "Mapping the NaN result to 0 for compatibility " - "with the lp_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.6.0', - ) + if arg2.fixed: + arg2 = visitor._eval_fixed(arg2) + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{str(arg2.value)} in expression " + "tree. Mapping the NaN result to 0 for compatibility " + "with the lp_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.6.0', + ) return False, (_CONSTANT, arg1) _id = id(arg2) if _id not in visitor.var_map: if arg2.fixed: - return False, (_CONSTANT, arg1 * visitor._eval_expr(arg2)) + return False, (_CONSTANT, arg1 * visitor._eval_fixed(arg2)) visitor.var_map[_id] = arg2 visitor.var_order[_id] = len(visitor.var_order) ans = visitor.Result() @@ -656,26 +623,27 @@ def _before_linear(visitor, child): if arg1.__class__ not in native_types: try: arg1 = visitor._eval_expr(arg1) - except: - # If there was an exception evaluating the - # subexpression, then we need to descend into it (in - # case there is something like 0 * nan that we need - # to map to 0) + except (ValueError, ArithmeticError): return True, None + + # Trap multiplication by 0 and nan. if not arg1: - if arg2.fixed and arg2.value != arg2.value: - deprecation_warning( - f"Encountered {arg1}*{str(arg2.value)} in expression tree. " - "Mapping the NaN result to 0 for compatibility " - "with the lp_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.6.0', - ) + if arg2.fixed: + arg2 = visitor._eval_fixed(arg2) + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{str(arg2.value)} in expression " + "tree. Mapping the NaN result to 0 for compatibility " + "with the lp_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.6.0', + ) continue + _id = id(arg2) if _id not in var_map: if arg2.fixed: - const += arg1 * visitor._eval_expr(arg2) + const += arg1 * visitor._eval_fixed(arg2) continue var_map[_id] = arg2 var_order[_id] = next_i @@ -685,17 +653,13 @@ def _before_linear(visitor, child): linear[_id] += arg1 else: linear[_id] = arg1 - elif arg.__class__ not in native_numeric_types: + elif arg.__class__ in native_numeric_types: + const += arg + else: try: const += visitor._eval_expr(arg) - except: - # If there was an exception evaluating the - # subexpression, then we need to descend into it (in - # case there is something like 0 * nan that we need to - # map to 0) + except (ValueError, ArithmeticError): return True, None - else: - const += arg if linear: ans.constant = const return False, (_LINEAR, ans) @@ -855,6 +819,14 @@ def __init__(self, subexpression_cache, var_map, var_order): self.var_order = var_order self._eval_expr_visitor = _EvaluationVisitor(True) + def _eval_fixed(self, obj): + ans = obj() + if ans is None or ans != ans: + ans = InvalidNumber(nan) + elif ans.__class__ in _complex_types: + ans = complex_number_error(ans, self, obj) + return ans + def _eval_expr(self, expr): ans = self._eval_expr_visitor.dfs_postorder_stack(expr) if ans.__class__ not in native_types: From d50e285ce4fdcb5a3478815cfd494df2c25a2727 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 31 Jul 2023 15:58:27 -0600 Subject: [PATCH 03/21] Port NaN handling from LP writer to NL writer --- pyomo/repn/plugins/nl_writer.py | 122 +++++++++++++++++--------------- 1 file changed, 64 insertions(+), 58 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index b122b272c0b..797e335208c 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -11,7 +11,7 @@ import logging import os -from collections import deque +from collections import deque, defaultdict from operator import itemgetter, attrgetter, setitem from pyomo.common.backports import nullcontext @@ -72,6 +72,7 @@ ExprType, FileDeterminism, FileDeterminism_to_SortComponents, + InvalidNumber, apply_node_operation, categorize_valid_components, complex_number_error, @@ -2255,22 +2256,9 @@ def _before_param(visitor, child): def _before_npv(visitor, child): - # TBD: It might be more efficient to cache the value of NPV - # expressions to avoid duplicate evaluations. However, current - # examples do not benefit from this cache. - # - # _id = id(child) - # if _id in visitor.value_cache: - # child = visitor.value_cache[_id] - # else: - # child = visitor.value_cache[_id] = child() - # return False, (_CONSTANT, child) try: return False, (_CONSTANT, visitor._eval_expr(child)) - except: - # If there was an exception evaluating the subexpression, then - # we need to descend into it (in case there is something like 0 * - # nan that we need to map to 0) + except (ValueError, ArithmeticError): return True, None @@ -2281,42 +2269,29 @@ def _before_monomial(visitor, child): # arg1, arg2 = child._args_ if arg1.__class__ not in native_types: - # TBD: It might be more efficient to cache the value of NPV - # expressions to avoid duplicate evaluations. However, current - # examples do not benefit from this cache. - # - # _id = id(arg1) - # if _id in visitor.value_cache: - # arg1 = visitor.value_cache[_id] - # else: - # arg1 = visitor.value_cache[_id] = arg1() try: arg1 = visitor._eval_expr(arg1) - except: - # If there was an exception evaluating the subexpression, - # then we need to descend into it (in case there is something - # like 0 * nan that we need to map to 0) + except (ValueError, ArithmeticError): return True, None - if arg2.fixed: - arg2 = arg2.value - _prod = arg1 * arg2 - if not (arg1 and arg2) and _prod: - deprecation_warning( - f"Encountered {arg1}*{arg2} in expression tree. " - "Mapping the NaN result to 0 for compatibility " - "with the nl_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.4.3', - ) - _prod = 0 - return False, (_CONSTANT, _prod) - - # Trap multiplication by 0. + # Trap multiplication by 0 and nan. if not arg1: - return False, (_CONSTANT, 0) + if arg2.fixed: + arg2 = visitor._eval_fixed(arg2) + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{arg2} in expression tree. " + "Mapping the NaN result to 0 for compatibility " + "with the nl_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.4.3', + ) + return False, (_CONSTANT, arg1) + _id = id(arg2) if _id not in visitor.var_map: + if arg2.fixed: + return False, (_CONSTANT, arg1 * visitor._eval_fixed(arg2)) visitor.var_map[_id] = arg2 return False, (_MONOMIAL, _id, arg1) @@ -2330,23 +2305,46 @@ def _before_linear(visitor, child): linear = {} for arg in child.args: if arg.__class__ is MonomialTermExpression: - c, v = arg.args - if c.__class__ not in native_types: - c = visitor._eval_expr(c) - if v.fixed: - const += c * v.value - elif c: - _id = id(v) - if _id not in var_map: - var_map[_id] = v - if _id in linear: - linear[_id] += c - else: - linear[_id] = c + arg1, arg2 = arg._args_ + if arg1.__class__ not in native_types: + try: + arg1 = visitor._eval_expr(arg1) + except (ValueError, ArithmeticError): + return True, None + + # Trap multiplication by 0 and nan. + if not arg1: + if arg2.fixed: + arg2 = visitor._eval_fixed(arg2) + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{str(arg2.value)} in expression " + "tree. Mapping the NaN result to 0 for compatibility " + "with the nl_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.6.0', + ) + continue + + _id = id(arg2) + if _id not in var_map: + if arg2.fixed: + const += arg1 * visitor._eval_fixed(arg2) + continue + var_map[_id] = arg2 + linear[_id] = arg1 + elif _id in linear: + linear[_id] += arg1 + else: + linear[_id] = arg1 elif arg.__class__ in native_types: const += arg else: - const += visitor._eval_expr(arg) + try: + const += visitor._eval_expr(arg) + except (ValueError, ArithmeticError): + return True, None + if linear: return False, (_GENERAL, AMPLRepn(const, linear, None)) else: @@ -2424,6 +2422,14 @@ def __init__( self.encountered_string_arguments = False self._eval_expr_visitor = _EvaluationVisitor(True) + def _eval_fixed(self, obj): + ans = obj() + if ans is None or ans != ans: + ans = InvalidNumber(nan) + elif ans.__class__ in _complex_types: + ans = complex_number_error(ans, self, obj) + return ans + def _eval_expr(self, expr): ans = self._eval_expr_visitor.dfs_postorder_stack(expr) if ans.__class__ not in native_types: From a8c83132d953cdfe5b93d9590a740177dedd1e32 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 31 Jul 2023 15:58:51 -0600 Subject: [PATCH 04/21] Switch to using defaultdict in NL writer --- pyomo/repn/plugins/nl_writer.py | 91 +++++++++++++++++---------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 797e335208c..742aa1a7858 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2369,10 +2369,55 @@ def _before_general_expression(visitor, child): return True, None +def _register_new_before_child_handler(visitor, child): + handlers = _before_child_handlers + child_type = child.__class__ + if child_type in native_numeric_types: + if isinstance(child_type, complex): + _complex_types.add(child_type) + dispatcher[child_type] = _before_complex + else: + dispatcher[child_type] = _before_native + elif issubclass(child_type, str): + handlers[child_type] = _before_string + elif child_type in native_types: + handlers[child_type] = _before_native + elif not child.is_expression_type(): + if child.is_potentially_variable(): + handlers[child_type] = _before_var + else: + handlers[child_type] = _before_param + elif not child.is_potentially_variable(): + handlers[child_type] = _before_npv + # If we descend into the named expression (because of an + # evaluation error), then on the way back out, we will use + # the potentially variable handler to process the result. + pv_base_type = child.potentially_variable_base_class() + if pv_base_type not in handlers: + try: + child.__class__ = pv_base_type + _register_new_before_child_handler(visitor, child) + finally: + child.__class__ = child_type + if pv_base_type in _operator_handles: + _operator_handles[child_type] = _operator_handles[pv_base_type] + elif id(child) in visitor.subexpression_cache or issubclass( + child_type, _GeneralExpressionData + ): + handlers[child_type] = _before_named_expression + _operator_handles[child_type] = handle_named_expression_node + else: + handlers[child_type] = _before_general_expression + return handlers[child_type](visitor, child) + + +_before_child_handlers = defaultdict(lambda: _register_new_before_child_handler) + _complex_types = set((complex,)) # Register an initial set of known expression types with the "before # child" expression handler lookup table. -_before_child_handlers = {_type: _before_native for _type in native_numeric_types} +for _type in native_numeric_types: + _before_child_handlers[_type] = _before_native _before_child_handlers[complex] = _before_complex for _type in native_types: if issubclass(_type, str): @@ -2447,10 +2492,6 @@ def initializeWalker(self, expr): return True, expr def beforeChild(self, node, child, child_idx): - try: - return _before_child_handlers[child.__class__](self, child) - except KeyError: - self._register_new_before_child_processor(child) return _before_child_handlers[child.__class__](self, child) def enterNode(self, node): @@ -2533,43 +2574,3 @@ def finalizeResult(self, result): # self.active_expression_source = None return ans - - def _register_new_before_child_processor(self, child): - handlers = _before_child_handlers - child_type = child.__class__ - if child_type in native_numeric_types: - if isinstance(child_type, complex): - _complex_types.add(child_type) - dispatcher[child_type] = _before_complex - else: - dispatcher[child_type] = _before_native - elif issubclass(child_type, str): - handlers[child_type] = _before_string - elif child_type in native_types: - handlers[child_type] = _before_native - elif not child.is_expression_type(): - if child.is_potentially_variable(): - handlers[child_type] = _before_var - else: - handlers[child_type] = _before_param - elif not child.is_potentially_variable(): - handlers[child_type] = _before_npv - # If we descend into the named expression (because of an - # evaluation error), then on the way back out, we will use - # the potentially variable handler to process the result. - pv_base_type = child.potentially_variable_base_class() - if pv_base_type not in handlers: - try: - child.__class__ = pv_base_type - _register_new_before_child_processor(self, child) - finally: - child.__class__ = child_type - if pv_base_type in _operator_handles: - _operator_handles[child_type] = _operator_handles[pv_base_type] - elif id(child) in self.subexpression_cache or issubclass( - child_type, _GeneralExpressionData - ): - handlers[child_type] = _before_named_expression - _operator_handles[child_type] = handle_named_expression_node - else: - handlers[child_type] = _before_general_expression From 0274aa01697c68d120e19807728dbb53950d6ec1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 31 Jul 2023 15:59:35 -0600 Subject: [PATCH 05/21] Improve testing of NaN in NL, LP writers --- pyomo/repn/tests/ampl/test_nlv2.py | 68 +++++++++++++++++++++++++- pyomo/repn/tests/test_linear.py | 76 +++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 629f2a88dd2..c13a2509f83 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -25,6 +25,7 @@ from pyomo.core.expr import Expr_if, inequality, LinearExpression from pyomo.core.base.expression import ScalarExpression from pyomo.environ import ( + Any, ConcreteModel, Objective, Param, @@ -64,6 +65,15 @@ def __init__(self, symbolic=False): True, ) + def __enter__(self): + assert nl_writer.AMPLRepn.ActiveVisitor is None + nl_writer.AMPLRepn.ActiveVisitor = self.visitor + return self + + def __exit__(self, exc_type, exc_value, tb): + assert nl_writer.AMPLRepn.ActiveVisitor is self.visitor + nl_writer.AMPLRepn.ActiveVisitor = None + class Test_AMPLRepnVisitor(unittest.TestCase): def test_divide(self): @@ -454,9 +464,10 @@ def test_errors_unary_func(self): def test_errors_propagate_nan(self): m = ConcreteModel() - m.p = Param(mutable=True, initialize=0) + m.p = Param(mutable=True, initialize=0, domain=Any) m.x = Var() m.y = Var() + m.z = Var() m.y.fix(1) expr = m.y**2 * m.x**2 * (((3 * m.x) / m.p) * m.x) / m.y @@ -476,6 +487,61 @@ def test_errors_propagate_nan(self): self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) + m.y.fix(None) + expr = log(m.y) + 3 + repn = info.visitor.walk_expression((expr, None, None)) + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + + expr = 3 * m.y + repn = info.visitor.walk_expression((expr, None, None)) + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + + m.p.value = None + expr = 5 * (m.p * m.x + 2 * m.z) + repn = info.visitor.walk_expression((expr, None, None)) + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {id(m.z): 10}) + self.assertEqual(repn.nonlinear, None) + + expr = m.y * m.x + repn = info.visitor.walk_expression((expr, None, None)) + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + + m.z = Var([1, 2, 3, 4], initialize=lambda m, i: i - 1) + m.z[1].fix(None) + expr = m.z[1] - ((m.z[2] * m.z[3]) * m.z[4]) + with INFO() as info: + repn = info.visitor.walk_expression((expr, None, None)) + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear[0], 'o16\no2\no2\nv%s\nv%s\nv%s\n') + self.assertEqual(repn.nonlinear[1], [id(m.z[2]), id(m.z[3]), id(m.z[4])]) + + m.z[3].fix(float('nan')) + with INFO() as info: + repn = info.visitor.walk_expression((expr, None, None)) + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + def test_linearexpression_npv(self): m = ConcreteModel() m.x = Var(initialize=4) diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 5e8df940efc..2ab75a028b8 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -18,7 +18,7 @@ from pyomo.repn.linear import LinearRepn, LinearRepnVisitor from pyomo.repn.util import InvalidNumber -from pyomo.environ import ConcreteModel, Param, Var, Expression, ExternalFunction, cos +from pyomo.environ import Any, ConcreteModel, Param, Var, Expression, ExternalFunction, cos, log nan = float('nan') @@ -1312,6 +1312,80 @@ def test_external(self): self.assertEqual(repn.linear, {}) self.assertIs(repn.nonlinear, e) + def test_errors_propagate_nan(self): + m = ConcreteModel() + m.p = Param(mutable=True, initialize=0, domain=Any) + m.x = Var() + m.y = Var() + m.z = Var() + m.y.fix(1) + + expr = m.y + m.x + m.z + ((3 * m.x) / m.p) / m.y + cfg = VisitorConfig() + with LoggingIntercept() as LOG: + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual( + LOG.getvalue(), + "Exception encountered evaluating expression 'div(3, 0)'\n" + "\tmessage: division by zero\n" + "\texpression: 3/p\n", + ) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 1) + self.assertEqual(len(repn.linear), 2) + self.assertEqual(repn.linear[id(m.z)], 1) + self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertEqual(repn.nonlinear, None) + + m.y.fix(None) + expr = log(m.y) + 3 + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + + expr = 3 * m.y + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + + m.p.value = None + expr = 5 * (m.p * m.x + 2 * m.z) + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(len(repn.linear), 2) + self.assertEqual(repn.linear[id(m.z)], 10) + self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertEqual(repn.nonlinear, None) + + expr = m.y * m.x + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(len(repn.linear), 1) + self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertEqual(repn.nonlinear, None) + + m.z = Var([1, 2, 3, 4], initialize=lambda m, i: i - 1) + m.z[1].fix(None) + expr = m.z[1] - ((m.z[2] * m.z[3]) * m.z[4]) + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertIsNotNone(repn.nonlinear) + + m.z[3].fix(float('nan')) + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertIsNotNone(repn.nonlinear) + def test_type_registrations(self): m = ConcreteModel() From 2afea93c5833185b11e67614c35a86d4952d52b9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 31 Jul 2023 15:51:58 -0600 Subject: [PATCH 06/21] Centralize the definition of nan, sum_like_expression --- pyomo/repn/linear.py | 10 ++++------ pyomo/repn/plugins/nl_writer.py | 4 +++- pyomo/repn/util.py | 6 ++++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index a963a5b9216..3b6a7ecffb1 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -42,21 +42,19 @@ import pyomo.core.kernel as kernel from pyomo.repn.util import ( ExprType, + InvalidNumber, apply_node_operation, complex_number_error, - InvalidNumber, + nan, + sum_like_expression_types, ) logger = logging.getLogger(__name__) -nan = float("nan") - _CONSTANT = ExprType.CONSTANT _LINEAR = ExprType.LINEAR _GENERAL = ExprType.GENERAL -_SumLikeExpression = {SumExpression, LinearExpression, NPV_SumExpression} - def _merge_dict(dest_dict, mult, src_dict): if mult == 1: @@ -879,7 +877,7 @@ def beforeChild(self, node, child, child_idx): def enterNode(self, node): # SumExpression are potentially large nary operators. Directly # populate the result - if node.__class__ in _SumLikeExpression: + if node.__class__ in sum_like_expression_types: return node.args, self.Result() else: return node.args, [] diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index c7d10883461..b122b272c0b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -77,6 +77,8 @@ complex_number_error, initialize_var_map_from_column_order, ordered_active_constraints, + nan, + sum_like_expression_types, ) from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env @@ -2448,7 +2450,7 @@ def beforeChild(self, node, child, child_idx): def enterNode(self, node): # SumExpression are potentially large nary operators. Directly # populate the result - if node.__class__ is SumExpression: + if node.__class__ in sum_like_expression_types: data = AMPLRepn(0, {}, None) data.nonlinear = [] return node.args, data diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 4f855b53433..a576573741d 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -29,12 +29,18 @@ ) from pyomo.core.base.component import ActiveComponent from pyomo.core.expr.numvalue import native_numeric_types, is_fixed, value +import pyomo.core.expr as EXPR import pyomo.core.kernel as kernel logger = logging.getLogger('pyomo.core') valid_expr_ctypes_minlp = {Var, Param, Expression, Objective} valid_active_ctypes_minlp = {Block, Constraint, Objective, Suffix} +sum_like_expression_types = { + EXPR.SumExpression, + EXPR.LinearExpression, + EXPR.NPV_SumExpression, +} HALT_ON_EVALUATION_ERROR = False nan = float('nan') From 19f4f606ba0c2c30df4ada72f28686ae07eb7041 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 31 Jul 2023 17:19:50 -0600 Subject: [PATCH 07/21] Avoid double-nesting InvalidNumber instances --- pyomo/repn/linear.py | 14 ++++++++++---- pyomo/repn/plugins/nl_writer.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index f0b92ccc922..e3aa779e5dc 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -821,7 +821,10 @@ def __init__(self, subexpression_cache, var_map, var_order): def _eval_fixed(self, obj): ans = obj() - if ans is None or ans != ans: + if ans.__class__ not in native_numeric_types: + if ans.__class__ is not InvalidNumber: + ans = InvalidNumber(nan if ans is None else ans) + elif ans != ans: ans = InvalidNumber(nan) elif ans.__class__ in _complex_types: ans = complex_number_error(ans, self, obj) @@ -831,9 +834,12 @@ def _eval_expr(self, expr): ans = self._eval_expr_visitor.dfs_postorder_stack(expr) if ans.__class__ not in native_types: ans = value(ans) - if ans != ans: - return InvalidNumber(ans) - if ans.__class__ in _complex_types: + if ans.__class__ not in native_numeric_types: + if ans.__class__ is not InvalidNumber: + ans = InvalidNumber(nan if ans is None else ans) + elif ans != ans: + ans = InvalidNumber(nan) + elif ans.__class__ in _complex_types: return complex_number_error(ans, self, expr) return ans diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 742aa1a7858..2bb7822bffe 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2469,7 +2469,10 @@ def __init__( def _eval_fixed(self, obj): ans = obj() - if ans is None or ans != ans: + if ans.__class__ not in native_numeric_types: + if ans.__class__ is not InvalidNumber: + ans = InvalidNumber(nan if ans is None else ans) + elif ans != ans: ans = InvalidNumber(nan) elif ans.__class__ in _complex_types: ans = complex_number_error(ans, self, obj) @@ -2479,7 +2482,12 @@ def _eval_expr(self, expr): ans = self._eval_expr_visitor.dfs_postorder_stack(expr) if ans.__class__ not in native_types: ans = value(ans) - if ans.__class__ in _complex_types: + if ans.__class__ not in native_numeric_types: + if ans.__class__ is not InvalidNumber: + ans = InvalidNumber(nan if ans is None else ans) + elif ans != ans: + ans = InvalidNumber(nan) + elif ans.__class__ in _complex_types: return complex_number_error(ans, self, expr) return ans From 2e6d325022815359dc2f8c54b66a7038dcd7859f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 31 Jul 2023 17:20:25 -0600 Subject: [PATCH 08/21] Apply black --- pyomo/repn/tests/test_linear.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 2ab75a028b8..7f34707edeb 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -18,7 +18,16 @@ from pyomo.repn.linear import LinearRepn, LinearRepnVisitor from pyomo.repn.util import InvalidNumber -from pyomo.environ import Any, ConcreteModel, Param, Var, Expression, ExternalFunction, cos, log +from pyomo.environ import ( + Any, + ConcreteModel, + Param, + Var, + Expression, + ExternalFunction, + cos, + log, +) nan = float('nan') From a285af2419a1f894f6243f482fc994c3120f16f8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 31 Jul 2023 19:11:01 -0600 Subject: [PATCH 09/21] Remove some automatic registration; fix undefined symbol --- pyomo/repn/linear.py | 10 ---------- pyomo/repn/plugins/nl_writer.py | 22 ++-------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index e3aa779e5dc..f7e41911b23 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -768,18 +768,8 @@ def _register_new_before_child_dispatcher(visitor, child): # complex number types _complex_types = set((complex,)) -# Register an initial set of known expression types with the "before -# child" expression handler lookup table. -for _type in native_numeric_types: - _before_child_dispatcher[_type] = _before_native # We do not support writing complex numbers out _before_child_dispatcher[complex] = _before_complex -# general operators -for _type in _exit_node_handlers: - _before_child_dispatcher[_type] = _before_general_expression -# override for named subexpressions -for _type in _named_subexpression_types: - _before_child_dispatcher[_type] = _before_named_expression # Special handling for expr_if and external functions: will be handled # as terminal nodes from the point of view of the visitor _before_child_dispatcher[Expr_ifExpression] = _before_expr_if diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 2bb7822bffe..1d7a3bf80ca 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2375,9 +2375,9 @@ def _register_new_before_child_handler(visitor, child): if child_type in native_numeric_types: if isinstance(child_type, complex): _complex_types.add(child_type) - dispatcher[child_type] = _before_complex + handlers[child_type] = _before_complex else: - dispatcher[child_type] = _before_native + handlers[child_type] = _before_native elif issubclass(child_type, str): handlers[child_type] = _before_string elif child_type in native_types: @@ -2414,28 +2414,10 @@ def _register_new_before_child_handler(visitor, child): _before_child_handlers = defaultdict(lambda: _register_new_before_child_handler) _complex_types = set((complex,)) -# Register an initial set of known expression types with the "before -# child" expression handler lookup table. -for _type in native_numeric_types: - _before_child_handlers[_type] = _before_native _before_child_handlers[complex] = _before_complex for _type in native_types: if issubclass(_type, str): _before_child_handlers[_type] = _before_string -# general operators -for _type in _operator_handles: - _before_child_handlers[_type] = _before_general_expression -# named subexpressions -for _type in ( - _GeneralExpressionData, - ScalarExpression, - kernel.expression.expression, - kernel.expression.noclone, - _GeneralObjectiveData, - ScalarObjective, - kernel.objective.objective, -): - _before_child_handlers[_type] = _before_named_expression # Special linear / summation expressions _before_child_handlers[MonomialTermExpression] = _before_monomial _before_child_handlers[LinearExpression] = _before_linear From dab0366ba722a78af954ff92b4f5b79bf6a6350d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 21 Aug 2023 09:09:13 -0600 Subject: [PATCH 10/21] Reduce repeated code (leverage _eval_fixed) --- pyomo/repn/plugins/nl_writer.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 1d7a3bf80ca..0b606ffa477 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2236,23 +2236,13 @@ def _before_var(visitor, child): _id = id(child) if _id not in visitor.var_map: if child.fixed: - ans = child() - if ans is None or ans != ans: - ans = InvalidNumber(nan) - elif ans.__class__ in _complex_types: - ans = complex_number_error(ans, self, node) - return False, (_CONSTANT, ans) + return False, (_CONSTANT, visitor._eval_fixed(child)) visitor.var_map[_id] = child return False, (_MONOMIAL, _id, 1) def _before_param(visitor, child): - ans = child() - if ans is None or ans != ans: - ans = InvalidNumber(nan) - elif ans.__class__ in _complex_types: - ans = complex_number_error(ans, self, child) - return False, (_CONSTANT, ans) + return False, (_CONSTANT, visitor._eval_fixed(child)) def _before_npv(visitor, child): From fc954df7ad34dbb974cf800e7a60d694504ae81a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 21 Aug 2023 10:57:22 -0600 Subject: [PATCH 11/21] Simplify InvalidNumber logic; save causes from all InvalidNumber arguments --- pyomo/repn/util.py | 127 +++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index a576573741d..51658cd12cd 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -12,6 +12,7 @@ import enum import itertools import logging +import operator import sys from pyomo.common.collections import Sequence, ComponentMap @@ -99,44 +100,60 @@ def _missing_(cls, value): class InvalidNumber(object): - def __init__(self, value): + def __init__(self, value, cause=""): self.value = value - - def duplicate(self, new_value): - return InvalidNumber(new_value) - - def merge(self, other, new_value): - return InvalidNumber(new_value) + if cause: + if cause.__class__ is list: + self.causes = list(cause) + else: + self.causes = [cause] + else: + self.causes = [] + + def _parse_args(self, *args): + causes = [] + real_args = [] + for arg in args: + if obj.__class__ is InvalidNumber: + causes.extend(arg.causes) + real_args.append(arg.value) + else: + real_args.append(arg) + return real_args, causes + + def _cmp(self, op, other): + args, causes = self._parse_args(self, other) + try: + return op(*args) + except TypeError: + # TypeError will be raised when comparing incompatible types + # (e.g., int <= None) + return False + + def _op(self, op, *args): + args, causes = self._parse_args(self, other) + try: + return InvalidNumber(op(*args), causes) + except (TypeError, ArithmeticError): + # TypeError will be raised when operating on incompatible + # types (e.g., int + None); ArithmeticError can be raised by + # invalid operations (like divide by zero) + return self.value def __eq__(self, other): - if other.__class__ is InvalidNumber: - return self.value == other.value - else: - return self.value == other + return self._cmp(operator.eq, other) def __lt__(self, other): - if other.__class__ is InvalidNumber: - return self.value < other.value - else: - return self.value < other + return self._cmp(operator.lt, other) def __gt__(self, other): - if other.__class__ is InvalidNumber: - return self.value > other.value - else: - return self.value > other + return self._cmp(operator.gt, other) def __le__(self, other): - if other.__class__ is InvalidNumber: - return self.value <= other.value - else: - return self.value <= other + return self._cmp(operator.le, other) def __ge__(self, other): - if other.__class__ is InvalidNumber: - return self.value >= other.value - else: - return self.value >= other + return self._cmp(operator.ge, other) def __str__(self): return f'InvalidNumber({self.value})' @@ -158,85 +175,69 @@ def __format__(self, format_spec): # raise InvalidValueError(f'Cannot emit {str(self)} in compiled representation') def __neg__(self): - return self.duplicate(-self.value) + return self._op(operator.neg, self) def __abs__(self): - return self.duplicate(abs(self.value)) - + return self._op(operator.abs, self) + def __add__(self, other): - if other.__class__ is InvalidNumber: - return self.merge(other, self.value + other.value) - else: - return self.duplicate(self.value + other) + return self._op(operator.add, self, other) def __sub__(self, other): - if other.__class__ is InvalidNumber: - return self.merge(other, self.value - other.value) - else: - return self.duplicate(self.value - other) + return self._op(operator.sub, self, other) def __mul__(self, other): - if other.__class__ is InvalidNumber: - return self.merge(other, self.value * other.value) - else: - return self.duplicate(self.value * other) + return self._op(operator.mul, self, other) def __truediv__(self, other): - if other.__class__ is InvalidNumber: - return self.merge(other, self.value / other.value) - else: - return self.duplicate(self.value / other) + return self._op(operator.truediv, self, other) def __pow__(self, other): - if other.__class__ is InvalidNumber: - return self.merge(other, self.value**other.value) - else: - return self.duplicate(self.value**other) + return self._op(operator.pow, self, other) def __radd__(self, other): - return self.duplicate(other + self.value) + return self._op(operator.add, other, self) def __rsub__(self, other): - return self.duplicate(other - self.value) + return self._op(operator.sub, other, self) def __rmul__(self, other): - return self.duplicate(other * self.value) + return self._op(operator.mul, other, self) def __rtruediv__(self, other): - return self.duplicate(other / self.value) + return self._op(operator.truediv, other, self) def __rpow__(self, other): - return self.duplicate(other**self.value) + return self._op(operator.pow, other, self) def apply_node_operation(node, args): try: ans = node._apply_operation(args) if ans != ans and ans.__class__ is not InvalidNumber: - ans = InvalidNumber(ans) + ans = InvalidNumber(ans, "Evaluating '{node}' returned NaN") return ans except: + exc_msg = str(sys.exc_info()[1]) logger.warning( "Exception encountered evaluating expression " "'%s(%s)'\n\tmessage: %s\n\texpression: %s" - % (node.name, ", ".join(map(str, args)), str(sys.exc_info()[1]), node) + % (node.name, ", ".join(map(str, args)), exc_msg, node) ) if HALT_ON_EVALUATION_ERROR: raise - return InvalidNumber(nan) + return InvalidNumber(nan, exc_msg) def complex_number_error(value, visitor, expr, node=""): msg = f'Pyomo {visitor.__class__.__name__} does not support complex numbers' - logger.warning( - ' '.join(filter(None, ("Complex number returned from expression", node))) - + f"\n\tmessage: {msg}\n\texpression: {expr}" - ) + cause = ' '.join(filter(None, ("Complex number returned from expression", node))) + logger.warning(f"{cause}\n\tmessage: {msg}\n\texpression: {expr}") if HALT_ON_EVALUATION_ERROR: raise InvalidValueError( f'Pyomo {visitor.__class__.__name__} does not support complex numbers' ) - return InvalidNumber(value) + return InvalidNumber(value, cause) def categorize_valid_components( From 994e5123599375caa5af35115c50ac572f287e0c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 21 Aug 2023 11:15:20 -0600 Subject: [PATCH 12/21] Improve handling / documentation of non-numeric values in NL/LP writers --- pyomo/repn/linear.py | 66 ++++++++++++++++++++++++++------- pyomo/repn/plugins/nl_writer.py | 66 ++++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 28 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index f7e41911b23..f25f606c0e8 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -810,26 +810,64 @@ def __init__(self, subexpression_cache, var_map, var_order): self._eval_expr_visitor = _EvaluationVisitor(True) def _eval_fixed(self, obj): - ans = obj() + ans = obj.value if ans.__class__ not in native_numeric_types: - if ans.__class__ is not InvalidNumber: - ans = InvalidNumber(nan if ans is None else ans) - elif ans != ans: - ans = InvalidNumber(nan) - elif ans.__class__ in _complex_types: - ans = complex_number_error(ans, self, obj) + # None can be returned from uninitialized Var/Param objects + if ans is None: + return InvalidNumber( + None, f"'{obj}' contains a nonnumeric value '{ans}'" + ) + if ans.__class__ is InvalidNumber: + return ans + else: + # It is possible to get other non-numeric types. Most + # common are bool and 1-element numpy.array(). We will + # attempt to convert the value to a float before + # proceeding. + # + # TODO: we should check bool and warn/error (while bool is + # convertible to float in Python, they have very + # different semantic meanings in Pyomo). + try: + ans = float(ans) + except: + return InvalidNumber( + ans, f"'{obj}' contains a nonnumeric value '{ans}'" + ) + if ans != ans: + return InvalidNumber(nan, f"'{obj}' contains a nonnumeric value '{ans}'") + if ans.__class__ in _complex_types: + return complex_number_error(ans, self, obj) return ans def _eval_expr(self, expr): ans = self._eval_expr_visitor.dfs_postorder_stack(expr) - if ans.__class__ not in native_types: - ans = value(ans) if ans.__class__ not in native_numeric_types: - if ans.__class__ is not InvalidNumber: - ans = InvalidNumber(nan if ans is None else ans) - elif ans != ans: - ans = InvalidNumber(nan) - elif ans.__class__ in _complex_types: + # None can be returned from uninitialized Expression objects + if ans is None: + return InvalidNumber( + ans, f"'{expr}' evaluated to nonnumeric value '{ans}'" + ) + if ans.__class__ is InvalidNumber: + return ans + else: + # It is possible to get other non-numeric types. Most + # common are bool and 1-element numpy.array(). We will + # attempt to convert the value to a float before + # proceeding. + # + # TODO: we should check bool and warn/error (while bool is + # convertible to float in Python, they have very + # different semantic meanings in Pyomo). + try: + ans = float(ans) + except: + return InvalidNumber( + ans, f"'{expr}' evaluated to nonnumeric value '{ans}'" + ) + if ans != ans: + return InvalidNumber(ans, f"'{expr}' evaluated to nonnumeric value '{ans}'") + if ans.__class__ in _complex_types: return complex_number_error(ans, self, expr) return ans diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 0b606ffa477..56913a79a05 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2440,26 +2440,64 @@ def __init__( self._eval_expr_visitor = _EvaluationVisitor(True) def _eval_fixed(self, obj): - ans = obj() + ans = obj.value if ans.__class__ not in native_numeric_types: - if ans.__class__ is not InvalidNumber: - ans = InvalidNumber(nan if ans is None else ans) - elif ans != ans: - ans = InvalidNumber(nan) - elif ans.__class__ in _complex_types: - ans = complex_number_error(ans, self, obj) + # None can be returned from uninitialized Var/Param objects + if ans is None: + return InvalidNumber( + None, f"'{obj}' contains a nonnumeric value '{ans}'" + ) + if ans.__class__ is InvalidNumber: + return ans + else: + # It is possible to get other non-numeric types. Most + # common are bool and 1-element numpy.array(). We will + # attempt to convert the value to a float before + # proceeding. + # + # TODO: we should check bool and warn/error (while bool is + # convertible to float in Python, they have very + # different semantic meanings in Pyomo). + try: + ans = float(ans) + except: + return InvalidNumber( + ans, f"'{obj}' contains a nonnumeric value '{ans}'" + ) + if ans != ans: + return InvalidNumber(nan, f"'{obj}' contains a nonnumeric value '{ans}'") + if ans.__class__ in _complex_types: + return complex_number_error(ans, self, obj) return ans def _eval_expr(self, expr): ans = self._eval_expr_visitor.dfs_postorder_stack(expr) - if ans.__class__ not in native_types: - ans = value(ans) if ans.__class__ not in native_numeric_types: - if ans.__class__ is not InvalidNumber: - ans = InvalidNumber(nan if ans is None else ans) - elif ans != ans: - ans = InvalidNumber(nan) - elif ans.__class__ in _complex_types: + # None can be returned from uninitialized Expression objects + if ans is None: + return InvalidNumber( + ans, f"'{expr}' evaluated to nonnumeric value '{ans}'" + ) + if ans.__class__ is InvalidNumber: + return ans + else: + # It is possible to get other non-numeric types. Most + # common are bool and 1-element numpy.array(). We will + # attempt to convert the value to a float before + # proceeding. + # + # TODO: we should check bool and warn/error (while bool is + # convertible to float in Python, they have very + # different semantic meanings in Pyomo). + try: + ans = float(ans) + except: + return InvalidNumber( + ans, f"'{expr}' evaluated to nonnumeric value '{ans}'" + ) + if ans != ans: + return InvalidNumber(ans, f"'{expr}' evaluated to nonnumeric value '{ans}'") + if ans.__class__ in _complex_types: return complex_number_error(ans, self, expr) return ans From 4f4c3753da3b964c6bfd1e810ff00e0c8453d9bf Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 21 Aug 2023 11:21:15 -0600 Subject: [PATCH 13/21] Update tests to reflect InvalidNumber(None) --- pyomo/repn/tests/ampl/test_nlv2.py | 15 ++++++++------- pyomo/repn/tests/test_linear.py | 18 +++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index c13a2509f83..a3b2e262e6d 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -18,6 +18,7 @@ import pyomo.repn.util as repn_util import pyomo.repn.plugins.nl_writer as nl_writer +from pyomo.repn.util import InvalidNumber from pyomo.repn.tests.nl_diff import nl_diff from pyomo.common.log import LoggingIntercept @@ -500,7 +501,7 @@ def test_errors_propagate_nan(self): repn = info.visitor.walk_expression((expr, None, None)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertEqual(repn.const, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -509,16 +510,16 @@ def test_errors_propagate_nan(self): repn = info.visitor.walk_expression((expr, None, None)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') - self.assertEqual(repn.linear, {id(m.z): 10}) + self.assertEqual(repn.const, 0) + self.assertEqual(repn.linear, {id(m.z): 10, id(m.x): InvalidNumber(None)}) self.assertEqual(repn.nonlinear, None) expr = m.y * m.x repn = info.visitor.walk_expression((expr, None, None)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') - self.assertEqual(repn.linear, {}) + self.assertEqual(repn.const, 0) + self.assertEqual(repn.linear, {id(m.x): InvalidNumber(None)}) self.assertEqual(repn.nonlinear, None) m.z = Var([1, 2, 3, 4], initialize=lambda m, i: i - 1) @@ -528,7 +529,7 @@ def test_errors_propagate_nan(self): repn = info.visitor.walk_expression((expr, None, None)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertEqual(repn.const, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear[0], 'o16\no2\no2\nv%s\nv%s\nv%s\n') self.assertEqual(repn.nonlinear[1], [id(m.z[2]), id(m.z[3]), id(m.z[4])]) @@ -538,7 +539,7 @@ def test_errors_propagate_nan(self): repn = info.visitor.walk_expression((expr, None, None)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertEqual(repn.const, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 7f34707edeb..cda1bce5acd 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -134,7 +134,7 @@ def test_scalars(self): self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.constant, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -188,7 +188,7 @@ def test_scalars(self): self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.constant, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -408,7 +408,7 @@ def test_monomial(self): self.assertEqual(cfg.var_order, {id(m.x): 0}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) - self.assertStructuredAlmostEqual(repn.linear, {id(m.x): InvalidNumber(nan)}) + self.assertStructuredAlmostEqual(repn.linear, {id(m.x): InvalidNumber(None)}) self.assertEqual(repn.nonlinear, None) m.p.set_value(4) @@ -494,7 +494,7 @@ def test_monomial(self): self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.constant, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -1357,7 +1357,7 @@ def test_errors_propagate_nan(self): expr = 3 * m.y repn = LinearRepnVisitor(*cfg).walk_expression(expr) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.constant, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -1368,7 +1368,7 @@ def test_errors_propagate_nan(self): self.assertEqual(repn.constant, 0) self.assertEqual(len(repn.linear), 2) self.assertEqual(repn.linear[id(m.z)], 10) - self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertEqual(repn.linear[id(m.x)], InvalidNumber(None)) self.assertEqual(repn.nonlinear, None) expr = m.y * m.x @@ -1376,7 +1376,7 @@ def test_errors_propagate_nan(self): self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(len(repn.linear), 1) - self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertEqual(repn.linear[id(m.x)], InvalidNumber(None)) self.assertEqual(repn.nonlinear, None) m.z = Var([1, 2, 3, 4], initialize=lambda m, i: i - 1) @@ -1384,14 +1384,14 @@ def test_errors_propagate_nan(self): expr = m.z[1] - ((m.z[2] * m.z[3]) * m.z[4]) repn = LinearRepnVisitor(*cfg).walk_expression(expr) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.constant, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertIsNotNone(repn.nonlinear) m.z[3].fix(float('nan')) repn = LinearRepnVisitor(*cfg).walk_expression(expr) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.constant, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertIsNotNone(repn.nonlinear) From 1eb64d9a3835acfa873927da7e8b22acfa248fde Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 21 Aug 2023 11:21:58 -0600 Subject: [PATCH 14/21] LP: Improve handling of InvalidNumbers in Expr_if --- pyomo/repn/linear.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index f25f606c0e8..5b91bee6b99 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -436,7 +436,7 @@ def _handle_expr_if_const(visitor, node, arg1, arg2, arg3): _type, _test = arg1 assert _type is _CONSTANT if _test: - if _test != _test: + if _test != _test or test.__class__ is InvalidNumber: # nan return _handle_expr_if_nonlinear(visitor, node, arg1, arg2, arg3) return arg2 @@ -680,17 +680,26 @@ def _before_named_expression(visitor, child): def _before_expr_if(visitor, child): + # We want a _before_expr_if to catch constant test expressions so + # that we ONLY evaluate the appropriate branch. test, t, f = child.args if is_fixed(test): try: - test = test() + test = visitor._eval_expr(test) except: return True, None - subexpr = LinearRepnVisitor( + if test.__class__ is InvalidNumber: + return True, None + subexpr = visitor.__class__( visitor.subexpression_cache, visitor.var_map, visitor.var_order ).walk_expression(t if test else f) if subexpr.nonlinear is not None: return False, (_GENERAL, subexpr) + # This test is not needed for LINEAR, but is for the derived + # QUADRATIC. As this is the ONLY _before_* specialization + # needed for Quadratic, we will handle it here. + elif hasattr(subexpr, 'quadratic') and subexpr.quadratic: + return False, (ExprType._QUADRATIC, subexpr) elif subexpr.linear: return False, (_LINEAR, subexpr) else: From a4518ff6befd5e7c6cd3d8b01a6891221fe9d369 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 21 Aug 2023 11:22:33 -0600 Subject: [PATCH 15/21] Apply black --- pyomo/repn/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 51658cd12cd..9e062fb85a3 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -179,7 +179,7 @@ def __neg__(self): def __abs__(self): return self._op(operator.abs, self) - + def __add__(self, other): return self._op(operator.add, self, other) From 185199b499793157e29ca995b0458e2a23fcf7b9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 21 Aug 2023 13:13:26 -0600 Subject: [PATCH 16/21] Fixing local variable names / return types --- pyomo/repn/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 9e062fb85a3..da95024c701 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -114,7 +114,7 @@ def _parse_args(self, *args): causes = [] real_args = [] for arg in args: - if obj.__class__ is InvalidNumber: + if arg.__class__ is InvalidNumber: causes.extend(arg.causes) real_args.append(arg.value) else: @@ -131,14 +131,14 @@ def _cmp(self, op, other): return False def _op(self, op, *args): - args, causes = self._parse_args(self, other) + args, causes = self._parse_args(*args) try: return InvalidNumber(op(*args), causes) except (TypeError, ArithmeticError): # TypeError will be raised when operating on incompatible # types (e.g., int + None); ArithmeticError can be raised by # invalid operations (like divide by zero) - return self.value + return InvalidNumber(self.value, causes) def __eq__(self, other): return self._cmp(operator.eq, other) From d0923755bb7924d745ddca7f5bed725734523812 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 22 Aug 2023 09:45:07 -0600 Subject: [PATCH 17/21] Correct deprecation version number --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 56913a79a05..b94cec257f5 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2312,7 +2312,7 @@ def _before_linear(visitor, child): "tree. Mapping the NaN result to 0 for compatibility " "with the nl_v1 writer. In the future, this NaN " "will be preserved/emitted to comply with IEEE-754.", - version='6.6.0', + version='6.4.3', ) continue From 3eb560222e6dfb146d9587d78b0a3c91e9288db6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 22 Aug 2023 17:14:27 -0600 Subject: [PATCH 18/21] InvalidNumber should be a PyomoObject --- pyomo/repn/util.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index da95024c701..73d7c1cc921 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -18,6 +18,7 @@ from pyomo.common.collections import Sequence, ComponentMap from pyomo.common.deprecation import deprecation_warning from pyomo.common.errors import DeveloperError, InvalidValueError +from pyomo.core.pyomoobject import PyomoObject from pyomo.core.base import ( Var, Param, @@ -99,18 +100,16 @@ def _missing_(cls, value): return super()._missing_(value) -class InvalidNumber(object): +class InvalidNumber(PyomoObject): def __init__(self, value, cause=""): self.value = value - if cause: - if cause.__class__ is list: - self.causes = list(cause) - else: - self.causes = [cause] + if cause.__class__ is list: + self.causes = list(cause) else: - self.causes = [] + self.causes = [cause] - def _parse_args(self, *args): + @staticmethod + def parse_args(*args): causes = [] real_args = [] for arg in args: @@ -122,7 +121,7 @@ def _parse_args(self, *args): return real_args, causes def _cmp(self, op, other): - args, causes = self._parse_args(self, other) + args, causes = InvalidNumber.parse_args(self, other) try: return op(*args) except TypeError: @@ -131,7 +130,7 @@ def _cmp(self, op, other): return False def _op(self, op, *args): - args, causes = self._parse_args(*args) + args, causes = InvalidNumber.parse_args(*args) try: return InvalidNumber(op(*args), causes) except (TypeError, ArithmeticError): From 52ec95dc22d408c54830b919f9d3fecd3b7a7d73 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 22 Aug 2023 17:16:44 -0600 Subject: [PATCH 19/21] Update Expr_if processing; improve InvalidNumber handling --- pyomo/repn/linear.py | 69 +++++++++-------- pyomo/repn/tests/test_linear.py | 132 ++++++++++++++++++++++++++++++-- 2 files changed, 161 insertions(+), 40 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 5b91bee6b99..9e938102136 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -436,7 +436,7 @@ def _handle_expr_if_const(visitor, node, arg1, arg2, arg3): _type, _test = arg1 assert _type is _CONSTANT if _test: - if _test != _test or test.__class__ is InvalidNumber: + if _test != _test or _test.__class__ is InvalidNumber: # nan return _handle_expr_if_nonlinear(visitor, node, arg1, arg2, arg3) return arg2 @@ -473,7 +473,17 @@ def _handle_expr_if_nonlinear(visitor, node, arg1, arg2, arg3): def _handle_equality_const(visitor, node, arg1, arg2): - return _CONSTANT, arg1[1] == arg2[1] + # It is exceptionally likely that if we get here, one of the + # arguments is an InvalidNumber + args, causes = InvalidNumber.parse_args(arg1[1], arg2[1]) + try: + ans = args[0] == args[1] + except: + ans = False + causes.append(str(sys.exc_info()[1])) + if causes: + ans = InvalidNumber(ans, causes) + return _CONSTANT, ans def _handle_equality_general(visitor, node, arg1, arg2): @@ -493,7 +503,17 @@ def _handle_equality_general(visitor, node, arg1, arg2): def _handle_inequality_const(visitor, node, arg1, arg2): - return _CONSTANT, arg1[1] <= arg2[1] + # It is exceptionally likely that if we get here, one of the + # arguments is an InvalidNumber + args, causes = InvalidNumber.parse_args(arg1[1], arg2[1]) + try: + ans = args[0] <= args[1] + except: + ans = False + causes.append(str(sys.exc_info()[1])) + if causes: + ans = InvalidNumber(ans, causes) + return _CONSTANT, ans def _handle_inequality_general(visitor, node, arg1, arg2): @@ -515,7 +535,17 @@ def _handle_inequality_general(visitor, node, arg1, arg2): def _handle_ranged_const(visitor, node, arg1, arg2, arg3): - return _CONSTANT, arg1[1] <= arg2[1] <= arg3[1] + # It is exceptionally likely that if we get here, one of the + # arguments is an InvalidNumber + args, causes = InvalidNumber.parse_args(arg1[1], arg2[1], arg3[1]) + try: + ans = args[0] <= args[1] <= args[2] + except: + ans = False + causes.append(str(sys.exc_info()[1])) + if causes: + ans = InvalidNumber(ans, causes) + return _CONSTANT, ans def _handle_ranged_general(visitor, node, arg1, arg2, arg3): @@ -679,34 +709,6 @@ def _before_named_expression(visitor, child): return True, None -def _before_expr_if(visitor, child): - # We want a _before_expr_if to catch constant test expressions so - # that we ONLY evaluate the appropriate branch. - test, t, f = child.args - if is_fixed(test): - try: - test = visitor._eval_expr(test) - except: - return True, None - if test.__class__ is InvalidNumber: - return True, None - subexpr = visitor.__class__( - visitor.subexpression_cache, visitor.var_map, visitor.var_order - ).walk_expression(t if test else f) - if subexpr.nonlinear is not None: - return False, (_GENERAL, subexpr) - # This test is not needed for LINEAR, but is for the derived - # QUADRATIC. As this is the ONLY _before_* specialization - # needed for Quadratic, we will handle it here. - elif hasattr(subexpr, 'quadratic') and subexpr.quadratic: - return False, (ExprType._QUADRATIC, subexpr) - elif subexpr.linear: - return False, (_LINEAR, subexpr) - else: - return False, (_CONSTANT, subexpr.constant) - return True, None - - def _before_external(visitor, child): ans = visitor.Result() if all(is_fixed(arg) for arg in child.args): @@ -779,9 +781,8 @@ def _register_new_before_child_dispatcher(visitor, child): # We do not support writing complex numbers out _before_child_dispatcher[complex] = _before_complex -# Special handling for expr_if and external functions: will be handled +# Special handling for external functions: will be handled # as terminal nodes from the point of view of the visitor -_before_child_dispatcher[Expr_ifExpression] = _before_expr_if _before_child_dispatcher[ExternalFunctionExpression] = _before_external # Special linear / summation expressions _before_child_dispatcher[MonomialTermExpression] = _before_monomial diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index cda1bce5acd..8a6b94d4621 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -9,12 +9,15 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.log import LoggingIntercept import pyomo.common.unittest as unittest +from pyomo.common.log import LoggingIntercept +from pyomo.common.dependencies import numpy, numpy_available + from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.core.expr.numeric_expr import LinearExpression, MonomialTermExpression from pyomo.core.expr import Expr_if, inequality, LinearExpression, NPV_SumExpression +import pyomo.repn.linear as linear from pyomo.repn.linear import LinearRepn, LinearRepnVisitor from pyomo.repn.util import InvalidNumber @@ -285,6 +288,28 @@ def test_npv(self): self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) + m.p = None + + cfg = VisitorConfig() + repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, InvalidNumber(None)) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + + cfg = VisitorConfig() + repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, InvalidNumber(None)) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + def test_monomial(self): m = ConcreteModel() m.x = Var() @@ -844,6 +869,32 @@ def test_named_expr(self): self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) + m.e = None + + cfg = VisitorConfig() + repn = LinearRepnVisitor(*cfg).walk_expression(m.e) + self.assertEqual( + cfg.subexpr, {id(m.e): (linear._CONSTANT, InvalidNumber(None))} + ) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, InvalidNumber(None)) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + + cfg = VisitorConfig() + repn = LinearRepnVisitor(*cfg).walk_expression(2 * m.e) + self.assertEqual( + cfg.subexpr, {id(m.e): (linear._CONSTANT, InvalidNumber(None))} + ) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, InvalidNumber(None)) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + def test_pow_expr(self): m = ConcreteModel() m.x = Var() @@ -1157,7 +1208,11 @@ def test_expr_if(self): self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {}) - assertExpressionsEqual(self, repn.nonlinear, m.x**2) + assertExpressionsEqual( + self, + repn.nonlinear, + Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2) + ) cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(f) @@ -1167,7 +1222,11 @@ def test_expr_if(self): self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {}) - assertExpressionsEqual(self, repn.nonlinear, m.x**2) + assertExpressionsEqual( + self, + repn.nonlinear, + Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2) + ) cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(g) @@ -1177,7 +1236,11 @@ def test_expr_if(self): self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {}) - assertExpressionsEqual(self, repn.nonlinear, m.x**2) + assertExpressionsEqual( + self, + repn.nonlinear, + Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2) + ) m.y.unfix() @@ -1219,6 +1282,37 @@ def test_expr_if(self): Expr_if(IF=inequality(3, m.y, 5), THEN=m.x, ELSE=m.x**2), ) + h = Expr_if(1/m.y >= 1, m.x, m.x**2) + + cfg = VisitorConfig() + repn = LinearRepnVisitor(*cfg).walk_expression(h) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.x): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + assertExpressionsEqual( + self, + repn.nonlinear, + Expr_if(IF=1/m.y >= 1, THEN=m.x, ELSE=m.x**2), + ) + + m.y.fix(0) + cfg = VisitorConfig() + repn = LinearRepnVisitor(*cfg).walk_expression(h) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + assertExpressionsEqual( + self, + repn.nonlinear, + Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2), + ) + def test_division(self): m = ConcreteModel() m.x = Var() @@ -1401,8 +1495,6 @@ def test_type_registrations(self): cfg = VisitorConfig() visitor = LinearRepnVisitor(*cfg) - import pyomo.repn.linear as linear - _orig_dispatcher = linear._before_child_dispatcher linear._before_child_dispatcher = bcd = {} try: @@ -1490,3 +1582,31 @@ def test_to_expression(self): expr.linear[id(m.x)] = 0 expr.linear[id(m.y)] = 0 assertExpressionsEqual(self, expr.to_expression(visitor), LinearExpression()) + + @unittest.skipUnless(numpy_available, "Test requires numpy") + def test_nonnumeric(self): + m = ConcreteModel() + m.p = Param(mutable=True, initialize=numpy.array([3]), domain=Any) + m.e = Expression() + + cfg = VisitorConfig() + repn = LinearRepnVisitor(*cfg).walk_expression(m.p) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 3) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + + m.p = numpy.array([3,4]) + + cfg = VisitorConfig() + repn = LinearRepnVisitor(*cfg).walk_expression(m.p) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(str(repn.constant), 'InvalidNumber([3 4])') + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) From e9a260c9659d8995f37466b1e20d65c44c044ff9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 22 Aug 2023 17:17:20 -0600 Subject: [PATCH 20/21] Add before_child handler for non-numeric (invalid) types --- pyomo/repn/linear.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 9e938102136..c4c1a1291bf 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -576,6 +576,12 @@ def _before_native(visitor, child): return False, (_CONSTANT, child) +def _before_invalid(visitor, child): + return False, ( + _CONSTANT, InvalidNumber(child, "'{child}' is not a valid numeric type") + ) + + def _before_complex(visitor, child): return False, (_CONSTANT, complex_number_error(child, visitor, child)) @@ -734,6 +740,8 @@ def _register_new_before_child_dispatcher(visitor, child): dispatcher[child_type] = _before_complex else: dispatcher[child_type] = _before_native + elif child_type in native_types: + dispatcher[child_type] = _before_invalid elif not child.is_expression_type(): if child.is_potentially_variable(): dispatcher[child_type] = _before_var From 4025b8bc8687ed12abcc92fd992b9068aa3f3c18 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 22 Aug 2023 17:17:52 -0600 Subject: [PATCH 21/21] Apply black --- pyomo/repn/linear.py | 3 ++- pyomo/repn/tests/test_linear.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index c4c1a1291bf..bae480f22d0 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -578,7 +578,8 @@ def _before_native(visitor, child): def _before_invalid(visitor, child): return False, ( - _CONSTANT, InvalidNumber(child, "'{child}' is not a valid numeric type") + _CONSTANT, + InvalidNumber(child, "'{child}' is not a valid numeric type"), ) diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 8a6b94d4621..04b36d67058 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1211,7 +1211,7 @@ def test_expr_if(self): assertExpressionsEqual( self, repn.nonlinear, - Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2) + Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2), ) cfg = VisitorConfig() @@ -1225,7 +1225,7 @@ def test_expr_if(self): assertExpressionsEqual( self, repn.nonlinear, - Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2) + Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2), ) cfg = VisitorConfig() @@ -1239,7 +1239,7 @@ def test_expr_if(self): assertExpressionsEqual( self, repn.nonlinear, - Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2) + Expr_if(IF=InvalidNumber(False), THEN=m.x, ELSE=m.x**2), ) m.y.unfix() @@ -1282,7 +1282,7 @@ def test_expr_if(self): Expr_if(IF=inequality(3, m.y, 5), THEN=m.x, ELSE=m.x**2), ) - h = Expr_if(1/m.y >= 1, m.x, m.x**2) + h = Expr_if(1 / m.y >= 1, m.x, m.x**2) cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(h) @@ -1293,9 +1293,7 @@ def test_expr_if(self): self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {}) assertExpressionsEqual( - self, - repn.nonlinear, - Expr_if(IF=1/m.y >= 1, THEN=m.x, ELSE=m.x**2), + self, repn.nonlinear, Expr_if(IF=1 / m.y >= 1, THEN=m.x, ELSE=m.x**2) ) m.y.fix(0) @@ -1599,7 +1597,7 @@ def test_nonnumeric(self): self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) - m.p = numpy.array([3,4]) + m.p = numpy.array([3, 4]) cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(m.p)