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..dc2167d75d2 --- /dev/null +++ b/pyomo/contrib/pynumero/exceptions.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# 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 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()