def eval(self, variables, functions, suffixes, allow_inf=False): """ Numerically evaluate a MathExpression's tree, returning a tuple of the numeric result and evaluation metadata. Also recasts some errors as CalcExceptions (which are student-facing). Arguments: variables (dict): maps variable names to values functions (dict): maps function names to values suffixes (dict): maps suffix names to values allow_inf (bool): If true, any node evaluating to inf will throw a CalcOverflowError See class-level docstring for example usage. """ self.check_scope(variables, functions, suffixes) # metadata_dict['max_array_dim_used'] is updated by eval_array metadata_dict = {'max_array_dim_used': 0} actions = { 'number': lambda parse_result: self.eval_number(parse_result, suffixes), 'variable': lambda parse_result: self.eval_variable(parse_result, variables), 'arguments': lambda tokens: tokens, 'function': lambda parse_result: self.eval_function(parse_result, functions), 'array': lambda parse_result: self.eval_array(parse_result, metadata_dict), 'power': self.eval_power, 'negation': self.eval_negation, 'parallel': self.eval_parallel, 'product': self.eval_product, 'sum': self.eval_sum, 'parentheses': lambda tokens: tokens[0] # just get the unique child } # Find the value of the entire tree # Catch math errors that may arise try: result = self.eval_node(self.tree, actions, allow_inf) # set metadata after metadata_dict has been mutated metadata = EvalMetaData(variables_used=self.variables_used, functions_used=self.functions_used, suffixes_used=self.suffixes_used, max_array_dim_used=metadata_dict['max_array_dim_used']) except OverflowError: raise CalcOverflowError("Numerical overflow occurred. " "Does your input generate very large numbers?") except ZeroDivisionError: raise CalcZeroDivisionError("Division by zero occurred. " "Check your input's denominators.") return result, metadata
def eval_node(node, actions, allow_inf): """ Recursively evaluates a node, calling itself on the node's children. Delegates to one of the provided actions, passing evaluated child nodes as arguments. """ if not isinstance(node, ParseResults): # We have a leaf, do not recurse. Return it directly. # Entry is either a (python) number or a string. return cast_np_numeric_as_builtin(node) node_name = node.getName() if node_name not in actions: # pragma: no cover raise ValueError(u"Unknown branch name '{}'".format(node_name)) evaluated_children = [ MathExpression.eval_node(child, actions, allow_inf) for child in node ] # Check for nan if any( np.isnan(item) for item in evaluated_children if isinstance(item, float)): return float('nan') # Compute the result of this node action = actions[node_name] result = action(evaluated_children) # All actions convert the input to a number, array, or list. # (Only self.actions['arguments'] returns a list.) as_list = result if isinstance(result, list) else [result] # Check if there were any infinities or nan if not allow_inf and any(np.any(np.isinf(r)) for r in as_list): raise CalcOverflowError( "Numerical overflow occurred. Does your expression " "generate very large numbers?") if any(np.any(np.isnan(r)) for r in as_list): return float('nan') return cast_np_numeric_as_builtin(result, map_across_lists=True)
def eval_function(parse_result, functions): """ Evaluates a function Arguments: parse_result: ['funcname', arglist] Usage ===== Instantiate a parser and some functions: >>> import numpy as np >>> functions = {"sin": np.sin, "cos": np.cos} Single variable functions work: >>> MathExpression.eval_function(['sin', [0]], functions) 0.0 >>> MathExpression.eval_function(['cos', [0]], functions) 1.0 So do multivariable functions: >>> def h(x, y): return x + y >>> MathExpression.eval_function(['h', [1, 2]], {"h": h}) 3 Validation: ============================== By default, eval_function inspects its function's arguments to first validate that the correct number of arguments are passed: >>> def h(x, y): return x + y >>> try: ... MathExpression.eval_function(['h', [1, 2, 3]], {"h": h}) ... except ArgumentError as error: ... print(error) Wrong number of arguments passed to h(...): Expected 2 inputs, but received 3. However, if the function to be evaluated has a truthy 'validated' property, we assume it does its own validation and we do not check the number of arguments. >>> from mitxgraders.exceptions import StudentFacingError >>> def g(*args): ... if len(args) != 2: ... raise StudentFacingError('I need two inputs!') ... return args[0]*args[1] >>> g.validated = True >>> try: ... MathExpression.eval_function(['g', [1]], {"g": g}) ... except StudentFacingError as error: ... print(error) I need two inputs! """ # Obtain the function and arguments name, args = parse_result func = functions[name] # If function does not do its own validation, try and validate here. if not getattr(func, 'validated', False): MathExpression.validate_function_call(func, name, args) # Try to call the function try: return func(*args) except StudentFacingError: raise except ZeroDivisionError: # It would be really nice to tell student the symbolic argument as part of this message, # but making symbolic argument available would require some nontrivial restructing msg = ("There was an error evaluating {name}(...). " "Its input does not seem to be in its domain.").format( name=name) raise CalcZeroDivisionError(msg) except OverflowError: msg = ("There was an error evaluating {name}(...). " "(Numerical overflow).").format(name=name) raise CalcOverflowError(msg) except Exception: # pylint: disable=W0703 # Don't know what this is, or how you want to deal with it # Call it a domain issue. msg = ("There was an error evaluating {name}(...). " "Its input does not seem to be in its domain.").format( name=name) raise FunctionEvalError(msg)