Example #1
0
    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)