Skip to content

Commit

Permalink
remove EvaluationError (#752)
Browse files Browse the repository at this point in the history
Remove the evaluable.EvaluationError exception type, which presented
debugging difficulties by shielding the actual point of failure from
PDB. In the new situation, the evaluation stack is logged at error level
prior to reraising the original exception.
  • Loading branch information
gertjanvanzwieten committed Dec 15, 2022
2 parents eedfa7f + b76a1d1 commit 1506235
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 29 deletions.
53 changes: 29 additions & 24 deletions nutils/evaluable.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ def eval(self, **evalargs):
except KeyboardInterrupt:
raise
except Exception as e:
raise EvaluationError(self, values) from e
log.error(self._format_stack(values, e))
raise
else:
return values[-1]

Expand All @@ -398,7 +399,8 @@ def eval_withtimes(self, times, **evalargs):
except KeyboardInterrupt:
raise
except Exception as e:
raise EvaluationError(self, values) from e
log.error(self._format_stack(values, e))
raise
else:
return values[-1]

Expand All @@ -422,22 +424,29 @@ def eval(**args):
log.info('total time: {:.0f}ms\n'.format(tottime/1e6) + '\n'.join('{:4.0f} {} ({} calls, avg {:.3f} per call)'.format(t / 1e6, k, n, t / (1e6*n))
for k, t, n in sorted(aggstats, reverse=True, key=lambda item: item[1]) if n))

def _stack(self, values):
lines = [' %0 = EVALARGS']
for (op, indices), v in zip(self.serialized, values):
lines[-1] += ' --> ' + type(v).__name__
def _iter_stack(self):
yield '%0 = EVALARGS'
for i, (op, indices) in enumerate(self.serialized, start=1):
s = [f'%{i} = {op}']
if indices:
try:
sig = inspect.signature(op.evalf)
except ValueError:
s.extend(f'%{i}' for i in indices)
else:
s.extend(f'{param}=%{i}' for param, i in zip(sig.parameters, indices))
yield ' '.join(s)

def _format_stack(self, values, e):
lines = [f'evaluation failed in step {len(values)}/{len(self.dependencies)+1}']
stack = self._iter_stack()
for v, op in zip(values, stack): # NOTE values must come first to avoid popping next item from stack
s = f'{type(v).__name__}'
if numeric.isarray(v):
lines[-1] += '({})'.format(','.join(map(str, v.shape)))
try:
code = op.evalf.__code__
offset = 1 if getattr(op.evalf, '__self__', None) is not None else 0
names = code.co_varnames[offset:code.co_argcount]
names += tuple('{}[{}]'.format(code.co_varnames[code.co_argcount], n) for n in range(len(indices) - len(names)))
args = map(' {}=%{}'.format, names, indices)
except:
args = map(' %{}'.format, indices)
lines.append(' %{} = {}:{}'.format(len(lines), op, ','.join(args)))
return lines
s += f'<{v.dtype.kind}:{",".join(str(n) for n in v.shape)}>'
lines.append(f'{op} --> {s}')
lines.append(f'{next(stack)} --> {e}')
return '\n '.join(lines)

@property
@replace(depthfirst=True, recursive=True)
Expand Down Expand Up @@ -516,11 +525,6 @@ def _combine_loop_concatenates(self, outer_exclude):
return self


class EvaluationError(Exception):
def __init__(self, f, values):
super().__init__('evaluation failed in step {}/{}\n'.format(len(values), len(f.dependencies)) + '\n'.join(f._stack(values)))


class EVALARGS(Evaluable):
def __init__(self):
super().__init__(args=())
Expand Down Expand Up @@ -2733,7 +2737,8 @@ def __init__(self, points: asarray, expect: asarray):

@staticmethod
def evalf(points, expect):
assert numpy.equal(points, expect).all(), 'illegal point set'
if points.shape != expect.shape or not numpy.equal(points, expect).all():
raise ValueError('points do not correspond to original sample')
return numpy.eye(len(points))


Expand Down Expand Up @@ -4051,7 +4056,7 @@ def __init__(self, name: types.strictstr, length: asindex):
def __str__(self):
try:
length = self.length.__index__()
except EvaluationError:
except:
length = '?'
return 'LoopIndex({}, length={})'.format(self._name, length)

Expand Down
24 changes: 24 additions & 0 deletions tests/test_evaluable.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import unittest
import functools
import operator
import logging


@parametrize
Expand Down Expand Up @@ -1250,3 +1251,26 @@ def test_too_few_axes(self):
def test_unequal_naxes(self):
with self.assertRaises(ValueError):
evaluable.unalign(evaluable.zeros((2, 3)), evaluable.zeros((2, 3, 4)))


class log_error(TestCase):

class Fail(evaluable.Array):
def __init__(self, arg1, arg2):
super().__init__(args=(arg1, arg2), shape=(), dtype=int)
@staticmethod
def evalf(arg1, arg2):
raise RuntimeError('operation failed intentially.')

def test(self):
a1 = evaluable.asarray(1.)
a2 = evaluable.asarray([2.,3.])
with self.assertLogs('nutils', logging.ERROR) as cm, self.assertRaises(RuntimeError):
self.Fail(a1+evaluable.Sum(a2), a1).eval()
self.assertEqual(cm.output[0], '''ERROR:nutils:evaluation failed in step 5/5
%0 = EVALARGS --> dict
%1 = nutils.evaluable.Constant<f:> --> ndarray<f:>
%2 = nutils.evaluable.Constant<f:2> --> ndarray<f:2>
%3 = nutils.evaluable.Sum<f:> arr=%2 --> float64
%4 = nutils.evaluable.Add<f:> %1 %3 --> float64
%5 = tests.test_evaluable.Fail<i:> arg1=%4 arg2=%1 --> operation failed intentially.''')
4 changes: 2 additions & 2 deletions tests/test_finitecell.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ def test_trimmed_boundary(self):
gauss1 = trimmed.sample('gauss', 1)
leftbasis = self.domain0[:1].basis('std', degree=1)
self.assertTrue(numpy.any(gauss1.eval(leftbasis)))
with self.assertRaises(evaluable.EvaluationError):
with self.assertRaises(ValueError):
gauss1.eval(function.opposite(leftbasis))
rightbasis = self.domain0[1:].basis('std', degree=1)
self.assertTrue(numpy.any(gauss1.eval(function.opposite(rightbasis))))
with self.assertRaises(evaluable.EvaluationError):
with self.assertRaises(ValueError):
gauss1.eval(rightbasis)


Expand Down
4 changes: 2 additions & 2 deletions tests/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ class Test(function.Custom):
def evalf():
return numpy.array([1, 2, 3])

with self.assertRaises(evaluable.EvaluationError):
with self.assertRaises(ValueError):
Test((), (), int).eval()

def test_pointwise_singleton_expansion(self):
Expand Down Expand Up @@ -670,7 +670,7 @@ def test_values(self):
self.assertAllAlmostEqual(diff, 0)

def test_pointset(self):
with self.assertRaises(evaluable.EvaluationError):
with self.assertRaises(ValueError):
self.domain.integrate(self.f_sampled, ischeme='uniform2')


Expand Down
2 changes: 1 addition & 1 deletion tests/test_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ def test_asfunction(self):
func = self.geom[0]**2 - self.geom[1]**2
values = self.gauss2.eval(func)
sampled = self.gauss2.asfunction(values)
with self.assertRaises(evaluable.EvaluationError):
with self.assertRaises(ValueError):
self.bezier2.eval(sampled)
self.assertAllEqual(self.gauss2.eval(sampled), values)
arg = function.Argument('dofs', [2, 3])
Expand Down

0 comments on commit 1506235

Please sign in to comment.