diff --git a/doc/OnlineDocs/howto/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst similarity index 100% rename from doc/OnlineDocs/howto/contribution_guide.rst rename to doc/OnlineDocs/contribution_guide.rst diff --git a/doc/OnlineDocs/howto/index.rst b/doc/OnlineDocs/howto/index.rst index 1dd04dd056c..9f700bff4e8 100644 --- a/doc/OnlineDocs/howto/index.rst +++ b/doc/OnlineDocs/howto/index.rst @@ -9,4 +9,4 @@ How-To Guides solver_recipes abstract_models/index.rst debugging - contribution_guide + ../contribution_guide diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index e496824188c..0d7ccc41592 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -92,7 +92,7 @@ Contributing to Pyomo --------------------- Interested in contributing code or documentation to the project? Check out our -:doc:`Contribution Guide ` +:doc:`Contribution Guide ` Related Packages ---------------- diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index 154b52124d3..14589244135 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -17,9 +17,9 @@ deprecation_message = ( "Pyomo.DoE has been refactored. The current interface utilizes Experiment " "objects that label unknown parameters, experiment inputs, experiment outputs " - "and measurement error. This avoids string-based naming which is fragile. For " - "instructions to use the new interface, please see the Pyomo.DoE under the contributed " - "packages documentation at `https://pyomo.readthedocs.io/en/latest/contributed_packages/doe/doe.html`" + "and measurement error. This avoids fragile string-based naming. For " + "instructions on using the new interface, please see the Pyomo.DoE documentation " + "`https://pyomo.readthedocs.io/en/latest/explanation/analysis/doe/doe.html`" ) diff --git a/pyomo/repn/ampl.py b/pyomo/repn/ampl.py index c28785e050b..1e142fc16a4 100644 --- a/pyomo/repn/ampl.py +++ b/pyomo/repn/ampl.py @@ -170,6 +170,10 @@ def _strip_template_comments(vars_, base_): vars_[k] = '\n'.join(v_lines) +def _inv2str(val): + return f"{val._str() if hasattr(val, '_str') else val}" + + # The "standard" text mode template is the debugging template with the # comments removed class TextNLTemplate(TextNLDebugTemplate): @@ -542,7 +546,7 @@ def handle_product_node(visitor, node, arg1, arg2): _prod = mult * arg2[1] if _prod: deprecation_warning( - f"Encountered {mult}*{str(arg2[1])} in expression tree. " + f"Encountered {mult}*{_inv2str(arg2[1])} 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.", @@ -571,7 +575,7 @@ def handle_product_node(visitor, node, arg1, arg2): _prod = mult * arg2[1] if _prod: deprecation_warning( - f"Encountered {str(mult)}*{arg2[1]} in expression tree. " + f"Encountered {_inv2str(mult)}*{arg2[1]} 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.", @@ -979,7 +983,7 @@ def _before_monomial(visitor, child): arg2 = visitor.fixed_vars[_id] if arg2 != arg2: deprecation_warning( - f"Encountered {arg1}*{arg2} in expression tree. " + f"Encountered {arg1}*{_inv2str(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.", @@ -1019,7 +1023,7 @@ def _before_linear(visitor, child): arg2 = visitor.check_constant(arg2.value, arg2) if arg2 != arg2: deprecation_warning( - f"Encountered {arg1}*{str(arg2.value)} in expression " + f"Encountered {arg1}*{_inv2str(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.", diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index e5aece86556..90dc58eabc0 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -66,6 +66,10 @@ _GENERAL = ExprType.GENERAL +def _inv2str(val): + return f"{val._str() if hasattr(val, '_str') else val}" + + def _merge_dict(dest_dict, mult, src_dict): if mult == 1: for vid, coef in src_dict.items(): @@ -211,8 +215,10 @@ def _handle_product_constant_constant(visitor, node, arg1, arg2): ans = arg1[1] * arg2[1] if ans != ans: if not arg1[1] or not arg2[1]: + a = _inv2str(arg1[1]) + b = _inv2str(arg2[1]) deprecation_warning( - f"Encountered {str(arg1[1])}*{str(arg2[1])} in expression tree. " + f"Encountered {a}*{b} 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.", @@ -580,7 +586,7 @@ def _before_monomial(visitor, child): arg2 = visitor.check_constant(arg2.value, arg2) if arg2 != arg2: deprecation_warning( - f"Encountered {arg1}*{str(arg2.value)} in expression " + f"Encountered {arg1}*{_inv2str(arg2)} 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.", @@ -613,7 +619,7 @@ def _before_linear(visitor, child): arg2 = visitor.check_constant(arg2.value, arg2) if arg2 != arg2: deprecation_warning( - f"Encountered {arg1}*{str(arg2.value)} in expression " + f"Encountered {arg1}*{_inv2str(arg2)} 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.", @@ -799,7 +805,7 @@ def finalizeResult(self, result): c != c for c in ans.linear.values() ): deprecation_warning( - f"Encountered {str(mult)}*nan in expression tree. " + f"Encountered {mult}*nan 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.", diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 35030caeb52..43327dd9ae7 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -48,7 +48,7 @@ ) import pyomo.environ as pyo -_invalid_1j = r'InvalidNumber\((\([-+0-9.e]+\+)?1j\)?\)' +nan = float('nan') class INFO(object): @@ -171,7 +171,7 @@ def test_errors_divide_by_0(self): ) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -186,7 +186,7 @@ def test_errors_divide_by_0(self): ) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -201,7 +201,7 @@ def test_errors_divide_by_0(self): ) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -216,7 +216,7 @@ def test_errors_divide_by_0(self): ) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -231,7 +231,7 @@ def test_errors_divide_by_0(self): ) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -424,7 +424,7 @@ def test_errors_negative_frac_pow(self): ) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertRegex(str(repn.const), _invalid_1j) + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(1j)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -440,7 +440,7 @@ def test_errors_negative_frac_pow(self): ) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertRegex(str(repn.const), _invalid_1j) + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(1j)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -460,7 +460,7 @@ def test_errors_unary_func(self): ) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -484,7 +484,7 @@ def test_errors_propagate_nan(self): ) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -494,7 +494,7 @@ def test_errors_propagate_nan(self): repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) - self.assertEqual(str(repn.const), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.const, InvalidNumber(nan)) 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 04e1bdb6584..0f027099c89 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -162,7 +162,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.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -216,7 +216,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.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -227,7 +227,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(1j)') + self.assertEqual(repn.constant, InvalidNumber(1j)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -266,7 +266,7 @@ def test_npv(self): self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -501,7 +501,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.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -511,7 +511,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.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -521,7 +521,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.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -564,7 +564,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.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -778,7 +778,8 @@ def test_linear(self): with LoggingIntercept() as LOG: repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertIn( - "DEPRECATED: Encountered 0*nan in expression tree.", LOG.getvalue() + "DEPRECATED: Encountered 0*InvalidNumber(nan) in expression tree.", + LOG.getvalue(), ) self.assertEqual(cfg.subexpr, {}) @@ -1460,9 +1461,8 @@ def test_errors_propagate_nan(self): "\texpression: (x + 1)/p\n", ) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') - self.assertEqual(len(repn.linear), 1) - self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) + self.assertStructuredAlmostEqual(repn.linear, {id(m.x): InvalidNumber(nan)}) self.assertEqual(repn.nonlinear, None) expr = m.y + m.x + m.z + ((3 * m.x) / m.p) / m.y @@ -1477,16 +1477,16 @@ def test_errors_propagate_nan(self): ) 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.assertStructuredAlmostEqual( + repn.linear, {id(m.z): 1, 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.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) @@ -1644,7 +1644,9 @@ def test_nonnumeric(self): self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(array([3, 4]))') + self.assertStructuredAlmostEqual( + repn.constant, InvalidNumber(numpy.array([3, 4])) + ) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index e0fea0fb45c..fc9d86f966f 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -242,7 +242,7 @@ def test_apply_operation(self): pyomo.repn.util.HALT_ON_EVALUATION_ERROR = False with LoggingIntercept() as LOG: val = apply_node_operation(div, [1, 0]) - self.assertEqual(str(val), "InvalidNumber(nan)") + self.assertStructuredAlmostEqual(val, InvalidNumber(float('nan'))) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(1, 0)'\n" @@ -293,7 +293,7 @@ class Visitor(object): pyomo.repn.util.HALT_ON_EVALUATION_ERROR = False with LoggingIntercept() as LOG: val = complex_number_error(1j, visitor, exp) - self.assertEqual(str(val), "InvalidNumber(1j)") + self.assertEqual(val, InvalidNumber(1j)) self.assertEqual( LOG.getvalue(), "Complex number returned from expression\n" diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 380d68e6be7..c3c1563455c 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -159,19 +159,37 @@ def _op(self, op, *args): return InvalidNumber(self.value, causes) def __eq__(self, other): - return self._cmp(operator.eq, other) + ans = self._cmp(operator.eq, other) + try: + return bool(ans) + except ValueError: + # ValueError can be raised by numpy.ndarray when two arrays + # are returned. In that case, ndarray returns a new ndarray + # of bool values. We will fall back on using `all` to + # reduce it to a single bool. + try: + return all(ans) + except: + pass + raise def __lt__(self, other): - return self._cmp(operator.lt, other) + # Note that as < is ambiguous for arrays, we will attempt to + # cast the result to bool and if it was an array, allow the + # exception to propagate + return bool(self._cmp(operator.lt, other)) def __gt__(self, other): - return self._cmp(operator.gt, other) + # See the comment in __lt__() on the use of bool() + return bool(self._cmp(operator.gt, other)) def __le__(self, other): - return self._cmp(operator.le, other) + # See the comment in __lt__() on the use of bool() + return bool(self._cmp(operator.le, other)) def __ge__(self, other): - return self._cmp(operator.ge, other) + # See the comment in __lt__() on the use of bool() + return bool(self._cmp(operator.ge, other)) def _error(self, msg): causes = list(filter(None, self.causes)) @@ -181,14 +199,21 @@ def _error(self, msg): raise InvalidValueError(msg) def __str__(self): - # We will support simple conversion of InvalidNumber to strings - # (for reporting purposes) + # We want attempts to convert InvalidNumber to a string + # representation to raise a InvalidValueError, unless we are in + # the middle of processing an exception. In that case, it is + # very likely that an exception handler is generating an error + # message. We will play nice and return a reasonable string. + if sys.exc_info()[1] is None: + self._error(f'Cannot emit {self._str()} in compiled representation') + else: + return self._str() + + def _str(self): return f'InvalidNumber({self.value!r})' def __repr__(self): - # We want attempts to convert InvalidNumber to a string - # representation to raise a InvalidValueError. - return self._error(f'Cannot emit {str(self)} in compiled representation') + return str(self) def __format__(self, format_spec): # FIXME: We want to move to where converting InvalidNumber to