diff --git a/pyomo/common/errors.py b/pyomo/common/errors.py index 744dbf76f59..76a635ba2ca 100644 --- a/pyomo/common/errors.py +++ b/pyomo/common/errors.py @@ -166,6 +166,17 @@ class InfeasibleConstraintException(PyomoException): pass +class IterationLimitError(PyomoException, RuntimeError): + """A subclass of :py:class:`RuntimeError`, raised by an iterative method + when the iteration limit is reached. + + TODO: solvers currently do not raise this exception, but probably + should (at least when non-normal termination conditions are mapped + to exceptions) + + """ + + class IntervalException(PyomoException, ValueError): """ Exception class used for errors in interval arithmetic. diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index e2da295d1c0..81bbd285dd2 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.common.errors import IterationLimitError from pyomo.core.expr.numvalue import native_numeric_types, value, is_fixed from pyomo.core.expr.calculus.derivatives import differentiate from pyomo.core.base.constraint import Constraint, _ConstraintData @@ -89,7 +90,7 @@ def calculate_variable_from_constraint( upper = constraint.ub if lower != upper: - raise ValueError("Constraint must be an equality constraint") + raise ValueError(f"Constraint '{constraint}' must be an equality constraint") if variable.value is None: # Note that we use "skip_validation=True" here as well, as the @@ -201,7 +202,10 @@ def calculate_variable_from_constraint( raise if type(expr_deriv) in native_numeric_types and expr_deriv == 0: - raise ValueError("Variable derivative == 0, cannot solve for variable") + raise ValueError( + f"Variable '{variable}' derivative == 0 in constraint " + f"'{constraint}', cannot solve for variable" + ) if expr_deriv is None: fp0 = differentiate(expr, wrt=variable, mode=diff_mode) @@ -209,9 +213,10 @@ def calculate_variable_from_constraint( fp0 = value(expr_deriv) if abs(value(fp0)) < 1e-12: - raise RuntimeError( - 'Initial value for variable results in a derivative value that is ' - 'very close to zero.\n\tPlease provide a different initial guess.' + raise ValueError( + f"Initial value for variable '{variable}' results in a derivative " + f"value for constraint '{constraint}' that is very close to zero.\n" + "\tPlease provide a different initial guess." ) iter_left = iterlim @@ -219,8 +224,9 @@ def calculate_variable_from_constraint( while abs(fk) > eps and iter_left: iter_left -= 1 if not iter_left: - raise RuntimeError( - "Iteration limit (%s) reached; remaining residual = %s" + raise IterationLimitError( + f"Iteration limit (%s) reached solving for variable '{variable}' " + f"using constraint '{constraint}'; remaining residual = %s" % (iterlim, value(expr)) ) @@ -235,8 +241,8 @@ def calculate_variable_from_constraint( # the line search is turned off) logger.error( "Newton's method encountered an error evaluating the " - "expression.\n\tPlease provide a different initial guess " - "or enable the linesearch if you have not." + f"expression for constraint '{constraint}'.\n\tPlease provide a " + "different initial guess or enable the linesearch if you have not." ) raise @@ -246,8 +252,11 @@ def calculate_variable_from_constraint( fpk = value(expr_deriv) if abs(fpk) < 1e-12: + # TODO: should this raise a ValueError or a new + # DerivativeError (subclassing ArithmeticError)? raise RuntimeError( - "Newton's method encountered a derivative that was too " + "Newton's method encountered a derivative of constraint " + f"'{constraint}' with respect to variable '{variable}' that was too " "close to zero.\n\tPlease provide a different initial guess " "or enable the linesearch if you have not." ) @@ -282,9 +291,10 @@ def calculate_variable_from_constraint( residual = value(expr, exception=False) if residual is None or type(residual) is complex: residual = "{function evaluation error}" - raise RuntimeError( - "Linesearch iteration limit reached; remaining " - "residual = %s." % (residual,) + raise IterationLimitError( + f"Linesearch iteration limit reached solving for " + f"variable '{variable}' using constraint '{constraint}'; " + f"remaining residual = {residual}." ) # # Re-set the variable value to trigger any warnings WRT the final diff --git a/pyomo/util/tests/test_calc_var_value.py b/pyomo/util/tests/test_calc_var_value.py index 64bcb82e019..91f23dd5a5d 100644 --- a/pyomo/util/tests/test_calc_var_value.py +++ b/pyomo/util/tests/test_calc_var_value.py @@ -14,6 +14,7 @@ import pyomo.common.unittest as unittest +from pyomo.common.errors import IterationLimitError from pyomo.common.log import LoggingIntercept from pyomo.environ import ( ConcreteModel, @@ -96,7 +97,7 @@ def test_initialize_value(self): m.lt = Constraint(expr=m.x <= m.y) with self.assertRaisesRegex( - ValueError, "Constraint must be an equality constraint" + ValueError, "Constraint 'lt' must be an equality constraint" ): calculate_variable_from_constraint(m.x, m.lt) @@ -152,7 +153,7 @@ def test_nonlinear(self): for mode in all_diff_modes: m.x.set_value(1.25) # set the initial value with self.assertRaisesRegex( - RuntimeError, r'Iteration limit \(10\) reached' + IterationLimitError, r'Iteration limit \(10\) reached' ): calculate_variable_from_constraint( m.x, m.d, iterlim=10, linesearch=False, diff_mode=mode @@ -162,7 +163,7 @@ def test_nonlinear(self): for mode in all_diff_modes: m.x.set_value(1.25) # set the initial value with self.assertRaisesRegex( - RuntimeError, "Linesearch iteration limit reached" + IterationLimitError, "Linesearch iteration limit reached" ): calculate_variable_from_constraint( m.x, m.d, iterlim=10, linesearch=True, diff_mode=mode @@ -172,9 +173,9 @@ def test_nonlinear(self): for mode in all_diff_modes: m.x = 0 with self.assertRaisesRegex( - RuntimeError, - "Initial value for variable results in a " - "derivative value that is very close to zero.", + ValueError, + "Initial value for variable 'x' results in a " + "derivative value for constraint 'c' that is very close to zero.", ): calculate_variable_from_constraint(m.x, m.c, diff_mode=mode) @@ -185,13 +186,15 @@ def test_nonlinear(self): # numeric differentiation should not be used to check if a # derivative is always zero with self.assertRaisesRegex( - RuntimeError, - "Initial value for variable results in a " - "derivative value that is very close to zero.", + ValueError, + "Initial value for variable 'y' results in a " + "derivative value for constraint 'c' that is very close to zero.", ): calculate_variable_from_constraint(m.y, m.c, diff_mode=mode) else: - with self.assertRaisesRegex(ValueError, "Variable derivative == 0"): + with self.assertRaisesRegex( + ValueError, "Variable 'y' derivative == 0 in constraint 'c'" + ): calculate_variable_from_constraint(m.y, m.c, diff_mode=mode) # should succeed with or without a linesearch @@ -224,8 +227,8 @@ def test_nonlinear(self): m.x.set_value(3.0) with self.assertRaisesRegex( RuntimeError, - "Newton's method encountered a derivative " - "that was too close to zero", + "Newton's method encountered a derivative of constraint 'f' " + "with respect to variable 'x' that was too close to zero", ): calculate_variable_from_constraint( m.x, m.f, linesearch=False, diff_mode=mode @@ -299,7 +302,8 @@ def f(m): # calculate_variable_from_constraint calculate_variable_from_constraint(m.x, m.c, linesearch=False) self.assertIn( - "Newton's method encountered an error evaluating the expression.", + "Newton's method encountered an error evaluating the expression for " + "constraint 'c'.", output.getvalue(), ) @@ -311,9 +315,9 @@ def f(m): m.c = Constraint(expr=m.x**0.5 == -1e-8) m.x = 1e-8 # 197.932807183 with self.assertRaisesRegex( - RuntimeError, - "Linesearch iteration limit reached; " - "remaining residual = {function evaluation error}", + IterationLimitError, + "Linesearch iteration limit reached solving for variable 'x' using " + "constraint 'c'; remaining residual = {function evaluation error}", ): calculate_variable_from_constraint(m.x, m.c, linesearch=True, alpha_min=0.5)