Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve calculate_variable_from_constraint error messages #2914

Merged
merged 6 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pyomo/common/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
36 changes: 23 additions & 13 deletions pyomo/util/calc_var_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -201,26 +202,31 @@ 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)
else:
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
fk = residual_1 - upper
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))
)

Expand All @@ -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

Expand All @@ -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."
)
Expand Down Expand Up @@ -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
Expand Down
36 changes: 20 additions & 16 deletions pyomo/util/tests/test_calc_var_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
)

Expand All @@ -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)

Expand Down