def parse(self, expression): """ If expression is in parser cache, return cached result, otherwise delegate to raw_parse. """ expression_no_whitespace = expression.replace(' ', '') cache_key = expression_no_whitespace if expression_no_whitespace in self.cache: return self.cache[cache_key] try: parsed = self.raw_parse(expression_no_whitespace) except ParseException: msg = "Invalid Input: Could not parse '{}' as a formula" raise UnableToParse(msg.format(expression)) self.cache[cache_key] = parsed return parsed
def eval_array(parse_result, metadata_dict): """ Takes in a list of evaluated expressions and returns it as a MathArray. May mutate metadata_dict. If passed a list of numpy arrays, generates a matrix/tensor/etc. Arguments: parse_result: A list containing each element of the array metadata_dict: A dictionary with key 'max_array_dim_used', whose value should be an integer. If the result of eval_array has higher dimension than 'max_array_dim_used', this value will be updated. Usage ===== Returns MathArray instances and updates metadata_dict['max_array_dim_used'] if needed: >>> metadata_dict = { 'max_array_dim_used': 0 } >>> MathExpression.eval_array([1, 2, 3], metadata_dict) MathArray([1, 2, 3]) >>> metadata_dict['max_array_dim_used'] 1 If metadata_dict['max_array_dim_used'] is larger than returned array value, then metadata_dict is not updated: >>> metadata_dict = { 'max_array_dim_used': 2 } >>> MathExpression.eval_array([1, 2, 3], metadata_dict) MathArray([1, 2, 3]) >>> metadata_dict['max_array_dim_used'] 2 >>> metadata_dict = { 'max_array_dim_used': 0 } >>> MathExpression.eval_array([ # doctest: +NORMALIZE_WHITESPACE ... [1 , 2], ... [3, 4] ... ], metadata_dict) MathArray([[1, 2], [3, 4]]) In practice, this is called recursively: >>> metadata_dict = { 'max_array_dim_used': 0 } >>> MathExpression.eval_array([ # doctest: +NORMALIZE_WHITESPACE ... MathExpression.eval_array([1, 2, 3], metadata_dict), ... MathExpression.eval_array([4, 5, 6], metadata_dict) ... ], metadata_dict) MathArray([[1, 2, 3], [4, 5, 6]]) >>> metadata_dict['max_array_dim_used'] 2 One complex entry will convert everything to complex: >>> metadata_dict = { 'max_array_dim_used': 0 } >>> MathExpression.eval_array([ # doctest: +NORMALIZE_WHITESPACE ... MathExpression.eval_array([1, 2j, 3], metadata_dict), ... MathExpression.eval_array([4, 5, 6], metadata_dict) ... ], metadata_dict) MathArray([[ 1.+0.j, 0.+2.j, 3.+0.j], [ 4.+0.j, 5.+0.j, 6.+0.j]]) We try to detect shape errors: >>> metadata_dict = { 'max_array_dim_used': 0 } >>> try: # doctest: +ELLIPSIS ... MathExpression.eval_array([ ... MathExpression.eval_array([1, 2, 3], metadata_dict), ... 4 ... ], metadata_dict) ... except UnableToParse as error: ... print(error) Unable to parse vector/matrix. If you're trying ... >>> metadata_dict = { 'max_array_dim_used': 0 } >>> try: # doctest: +ELLIPSIS ... MathExpression.eval_array([ ... 2.0, ... MathExpression.eval_array([1, 2, 3], metadata_dict), ... 4 ... ], metadata_dict) ... except UnableToParse as error: ... print(error) Unable to parse vector/matrix. If you're trying ... """ shape_message = ("Unable to parse vector/matrix. If you're trying to " "enter a matrix, this is most likely caused by an " "unequal number of elements in each row.") try: array = MathArray(parse_result) except ValueError: # This happens, for example, with np.array([1, 2, [3]]) # when using numpy version 1.6 raise UnableToParse(shape_message) if array.dtype == 'object': # This happens, for example, with np.array([[1], 2, 3]), # OR with with np.array([1, 2, [3]]) in recent versions of numpy raise UnableToParse(shape_message) if array.ndim > metadata_dict['max_array_dim_used']: metadata_dict['max_array_dim_used'] = array.ndim return array
def evaluator(formula, variables=DEFAULT_VARIABLES, functions=DEFAULT_FUNCTIONS, suffixes=DEFAULT_SUFFIXES, max_array_dim=None, allow_inf=False): """ Evaluate an expression; that is, take a string of math and return a float. Arguments ========= - formula (str): The formula to be evaluated Pass a scope consisting of variables, functions, and suffixes: - variables (dict): maps strings to variable values, defaults to DEFAULT_VARIABLES - functions (dict): maps strings to functions, defaults to DEFAULT_FUNCTIONS - suffixes (dict): maps strings to suffix values, defaults to DEFAULT_SUFFIXES Also: - max_array_dim: Maximum dimension of MathArrays - allow_inf: Whether to raise an error if the evaluator encounters an infinity NOTE: Everything is case sensitive (this is different to edX!) Usage ===== Evaluates the formula and records usage of functions/variables/suffixes: >>> result = evaluator("1+1", {}, {}, {}) >>> expected = ( 2.0 , EvalMetaData( ... variables_used=set(), ... functions_used=set(), ... suffixes_used=set(), ... max_array_dim_used=0 ... )) >>> result == expected True >>> result = evaluator("square(x) + 5k", ... variables={'x':5, 'y': 10}, ... functions={'square': lambda x: x**2, 'cube': lambda x: x**3}, ... suffixes={'%': 0.01, 'k': 1000 }) >>> expected = ( 5025.0 , EvalMetaData( ... variables_used=set(['x']), ... functions_used=set(['square']), ... suffixes_used=set(['k']), ... max_array_dim_used=0 ... )) >>> result == expected True Empty submissions evaluate to nan: >>> evaluator("")[0] nan Submissions that generate infinities will raise an error: >>> try: # doctest: +ELLIPSIS ... evaluator("inf", variables={'inf': float('inf')})[0] ... except CalcOverflowError as error: ... print(error) Numerical overflow occurred. Does your expression generate ... Unless you specify that infinity is ok: >>> evaluator("inf", variables={'inf': float('inf')}, allow_inf=True)[0] inf """ empty_usage = EvalMetaData(variables_used=set(), functions_used=set(), suffixes_used=set(), max_array_dim_used=0) if formula is None: # No need to go further. return float('nan'), empty_usage formula = formula.strip() if formula == "": # No need to go further. return float('nan'), empty_usage parsed = parse(formula) result, eval_metadata = parsed.eval(variables, functions, suffixes, allow_inf=allow_inf) # Were vectors/matrices/tensors used when they shouldn't have been? if max_array_dim is not None and eval_metadata.max_array_dim_used > max_array_dim: if max_array_dim == 0: msg = "Vector and matrix expressions have been forbidden in this entry." elif max_array_dim == 1: msg = "Matrix expressions have been forbidden in this entry." else: msg = "Tensor expressions have been forbidden in this entry." raise UnableToParse(msg) # Return the result of the evaluation, as well as the set of functions used return result, eval_metadata