From 1ce65719da10cf032b9dd04a6ffd15c7570bd6d6 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 7 Jul 2023 17:09:10 -0600 Subject: [PATCH 1/3] add PyNumeroEvaluationError and use it in ASL interface instead of asserting return values --- pyomo/contrib/pynumero/asl.py | 16 +++++++++++----- pyomo/contrib/pynumero/exceptions.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 pyomo/contrib/pynumero/exceptions.py diff --git a/pyomo/contrib/pynumero/asl.py b/pyomo/contrib/pynumero/asl.py index 0cd25633da3..a28741fb230 100644 --- a/pyomo/contrib/pynumero/asl.py +++ b/pyomo/contrib/pynumero/asl.py @@ -11,6 +11,7 @@ from pyomo.common.fileutils import find_library from pyomo.common.dependencies import numpy as np +from pyomo.contrib.pynumero.exceptions import PyNumeroEvaluationError import ctypes import logging import os @@ -364,7 +365,8 @@ def eval_f(self, x): res = self.ASLib.EXTERNAL_AmplInterface_eval_f( self._obj, x, self._nx, ctypes.byref(sol) ) - assert res, "Error in AMPL evaluation" + if not res: + raise PyNumeroEvaluationError("Error in AMPL evaluation") return sol.value def eval_deriv_f(self, x, df): @@ -373,7 +375,8 @@ def eval_deriv_f(self, x, df): x.dtype == np.double ), "Error: array type. Function eval_deriv_f expects an array of type double" res = self.ASLib.EXTERNAL_AmplInterface_eval_deriv_f(self._obj, x, df, len(x)) - assert res, "Error in AMPL evaluation" + if not res: + raise PyNumeroEvaluationError("Error in AMPL evaluation") def struct_jac_g(self, irow, jcol): irow_p = irow.astype(np.intc, casting='safe', copy=False) @@ -409,7 +412,8 @@ def eval_jac_g(self, x, jac_g_values): res = self.ASLib.EXTERNAL_AmplInterface_eval_jac_g( self._obj, xeval, self._nx, jac_eval, self._nnz_jac_g ) - assert res, "Error in AMPL evaluation" + if not res: + raise PyNumeroEvaluationError("Error in AMPL evaluation") def eval_g(self, x, g): assert x.size == self._nx, "Error: Dimension mismatch." @@ -423,7 +427,8 @@ def eval_g(self, x, g): res = self.ASLib.EXTERNAL_AmplInterface_eval_g( self._obj, x, self._nx, g, self._ny ) - assert res, "Error in AMPL evaluation" + if not res: + raise PyNumeroEvaluationError("Error in AMPL evaluation") def eval_hes_lag(self, x, lam, hes_lag, obj_factor=1.0): assert x.size == self._nx, "Error: Dimension mismatch." @@ -453,7 +458,8 @@ def eval_hes_lag(self, x, lam, hes_lag, obj_factor=1.0): res = self.ASLib.EXTERNAL_AmplInterface_eval_hes_lag( self._obj, x, self._nx, lam, self._ny, hes_lag, self._nnz_hess ) - assert res, "Error in AMPL evaluation" + if not res: + raise PyNumeroEvaluationError("Error in AMPL evaluation") def finalize_solution(self, ampl_solve_status_num, msg, x, lam): b_msg = msg.encode('utf-8') diff --git a/pyomo/contrib/pynumero/exceptions.py b/pyomo/contrib/pynumero/exceptions.py new file mode 100644 index 00000000000..f73ef62887d --- /dev/null +++ b/pyomo/contrib/pynumero/exceptions.py @@ -0,0 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +class PyNumeroEvaluationError(ArithmeticError): + """An exception to be raised by PyNumero evaluation backends in the event + of a failed function evaluation. This should be caught by solver interfaces + and translated to the solver-specific evaluation error API. + + """ + pass From ec5c83b1e8c9389f737a10f2d7c6879bc86c50e2 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 7 Jul 2023 17:25:36 -0600 Subject: [PATCH 2/3] add blank line for black --- pyomo/contrib/pynumero/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/pynumero/exceptions.py b/pyomo/contrib/pynumero/exceptions.py index f73ef62887d..dc2167d75d2 100644 --- a/pyomo/contrib/pynumero/exceptions.py +++ b/pyomo/contrib/pynumero/exceptions.py @@ -9,10 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ + class PyNumeroEvaluationError(ArithmeticError): """An exception to be raised by PyNumero evaluation backends in the event of a failed function evaluation. This should be caught by solver interfaces and translated to the solver-specific evaluation error API. """ + pass From ef93dca636de8193e4a7f381bb52911018de1b23 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 12 Jul 2023 17:51:26 -0600 Subject: [PATCH 3/3] add tests catching PyNumeroEvaluationError --- .../pynumero/interfaces/tests/test_nlp.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py b/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py index b1a4e1c501b..38d44473a67 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py @@ -22,6 +22,7 @@ raise unittest.SkipTest("Pynumero needs scipy and numpy to run NLP tests") from pyomo.contrib.pynumero.asl import AmplInterface +from pyomo.contrib.pynumero.exceptions import PyNumeroEvaluationError if not AmplInterface.available(): raise unittest.SkipTest("Pynumero needs the ASL extension to run NLP tests") @@ -816,6 +817,57 @@ def test_util_maps(self): self.assertTrue(np.array_equal(expected_full_primals_lb, full_primals_lb)) +class TestExceptions(unittest.TestCase): + def _make_bad_model(self): + m = pyo.ConcreteModel() + m.I = pyo.Set(initialize=[1, 2, 3]) + m.x = pyo.Var(m.I, initialize=1) + + m.obj = pyo.Objective(expr=m.x[1] + m.x[2] / m.x[3]) + m.eq1 = pyo.Constraint(expr=m.x[1] == pyo.sqrt(m.x[2])) + return m + + def test_eval_error_in_constraint(self): + m = self._make_bad_model() + m.x[2] = -1 + nlp = PyomoNLP(m) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + residuals = nlp.evaluate_constraints() + + def test_eval_error_in_constraint_jacobian(self): + m = self._make_bad_model() + m.x[2] = -1 + nlp = PyomoNLP(m) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + jacobian = nlp.evaluate_jacobian() + + def test_eval_error_in_objective(self): + m = self._make_bad_model() + m.x[3] = 0 + nlp = PyomoNLP(m) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + objval = nlp.evaluate_objective() + + def test_eval_error_in_objective_gradient(self): + m = self._make_bad_model() + m.x[3] = 0 + nlp = PyomoNLP(m) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + gradient = nlp.evaluate_grad_objective() + + def test_eval_error_in_lagrangian_hessian(self): + m = self._make_bad_model() + m.x[3] = 0 + nlp = PyomoNLP(m) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + hessian = nlp.evaluate_hessian_lag() + + if __name__ == '__main__': TestAslNLP.setUpClass() t = TestAslNLP()