From a85a7f71df6c0bc889b2d5cbe40926b3663d375e Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 13:34:58 -0400 Subject: [PATCH] Rename variables; get rid of OPS --- common/lib/calc/calc.py | 170 ++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 83 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 5d0aeb3fd1..f862b41542 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -11,16 +11,15 @@ import operator import re import numpy -import numbers import scipy.constants -from pyparsing import Word, nums, Literal -from pyparsing import ZeroOrMore, MatchFirst -from pyparsing import Optional, Forward -from pyparsing import CaselessLiteral -from pyparsing import NoMatch, stringEnd, Suppress, Combine +from pyparsing import (Word, nums, Literal, + ZeroOrMore, MatchFirst, + Optional, Forward, + CaselessLiteral, + NoMatch, stringEnd, Suppress, Combine) -default_functions = {'sin': numpy.sin, +DEFAULT_FUNCTIONS = {'sin': numpy.sin, 'cos': numpy.cos, 'tan': numpy.tan, 'sqrt': numpy.sqrt, @@ -34,7 +33,7 @@ default_functions = {'sin': numpy.sin, 'fact': math.factorial, 'factorial': math.factorial } -default_variables = {'j': numpy.complex(0, 1), +DEFAULT_VARIABLES = {'j': numpy.complex(0, 1), 'e': numpy.e, 'pi': numpy.pi, 'k': scipy.constants.k, @@ -43,22 +42,15 @@ default_variables = {'j': numpy.complex(0, 1), 'q': scipy.constants.e } - -ops = {"^": operator.pow, - "*": operator.mul, - "/": operator.truediv, - "+": operator.add, - "-": operator.sub, -} # We eliminated extreme ones, since they're rarely used, and potentially # confusing. They may also conflict with variables if we ever allow e.g. # 5R instead of 5*R -suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, +SUFFIXES = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24, 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24} -log = logging.getLogger("mitx.courseware.capa") +LOG = logging.getLogger("mitx.courseware.capa") class UndefinedVariable(Exception): @@ -73,13 +65,12 @@ class UndefinedVariable(Exception): # raise self -general_whitespace = re.compile('[^\\w]+') - - def check_variables(string, variables): """ Confirm the only variables in string are defined. + Otherwise, raise an UndefinedVariable containing all bad variables. + Pyparsing uses a left-to-right parser, which makes the more elegant approach pretty hopeless. @@ -88,19 +79,22 @@ def check_variables(string, variables): undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) varnames = varnames | undefined_variable """ - possible_variables = re.split(general_whitespace, string) # List of all alnums in string + general_whitespace = re.compile('[^\\w]+') + # List of all alnums in string + possible_variables = re.split(general_whitespace, string) bad_variables = list() - for v in possible_variables: - if len(v) == 0: + for var in possible_variables: + if len(var) == 0: continue - if v[0] <= '9' and '0' <= v: # Skip things that begin with numbers + if var[0] <= '9' and '0' <= var: # Skip things that begin with numbers continue - if v not in variables: - bad_variables.append(v) + if var not in variables: + bad_variables.append(var) if len(bad_variables) > 0: raise UndefinedVariable(' '.join(bad_variables)) -def lower_dict(d): + +def lower_dict(input_dict): """ takes each key in the dict and makes it lowercase, still mapping to the same value. @@ -109,7 +103,8 @@ def lower_dict(d): variables that have the same lowercase representation. It would be hard to tell which is used in the final dict and which isn't. """ - return dict([(k.lower(), d[k]) for k in d]) + return dict([(k.lower(), input_dict[k]) for k in input_dict]) + # The following few functions define parse actions, which are run on lists of # results from each parse component. They convert the strings and (previously @@ -119,32 +114,37 @@ def super_float(text): """ Like float, but with si extensions. 1k goes to 1000 """ - if text[-1] in suffixes: - return float(text[:-1]) * suffixes[text[-1]] + if text[-1] in SUFFIXES: + return float(text[:-1]) * SUFFIXES[text[-1]] else: return float(text) -def number_parse_action(x): + +def number_parse_action(parse_result): """ Create a float out of its string parts e.g. [ '7', '.', '13' ] -> [ 7.13 ] Calls super_float above """ - return [super_float("".join(x))] + return super_float("".join(parse_result)) -def exp_parse_action(x): + +def exp_parse_action(parse_result): """ Take a list of numbers and exponentiate them, right to left e.g. [ 3, 2, 3 ] (which is 3^2^3 = 3^(2^3)) -> 6561 """ - x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ - x.reverse() - x = reduce(lambda a, b: b ** a, x) - return x + # pyparsing.ParseResults doesn't play well with reverse() + parse_result = parse_result.asList() + parse_result.reverse() + # the result of an exponentiation is called a power + power = reduce(lambda a, b: b ** a, parse_result) + return power -def parallel(x): + +def parallel(parse_result): """ Compute numbers according to the parallel resistors operator @@ -154,15 +154,17 @@ def parallel(x): Return NaN if there is a zero among the inputs """ - x = list(x) - if len(x) == 1: - return x[0] - if 0 in x: + # convert from pyparsing.ParseResults, which doesn't support '0 in parse_result' + parse_result = parse_result.asList() + if len(parse_result) == 1: + return parse_result[0] + if 0 in parse_result: return float('nan') - x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || - return 1. / sum(x) + reciprocals = [1. / e for e in parse_result] + return 1. / sum(reciprocals) -def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 + +def sum_parse_action(parse_result): """ Add the inputs @@ -171,29 +173,35 @@ def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 Allow a leading + or - """ total = 0.0 - op = ops['+'] - for e in x: - if e in set('+-'): - op = ops[e] + current_op = operator.add + for token in parse_result: + if token is '+': + current_op = operator.add + elif token is '-': + current_op = operator.sub else: - total = op(total, e) + total = current_op(total, token) return total -def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 + +def prod_parse_action(parse_result): """ Multiply the inputs [ 1, '*', 2, '/', 3 ] => 0.66 """ prod = 1.0 - op = ops['*'] - for e in x: - if e in set('*/'): - op = ops[e] + current_op = operator.mul + for token in parse_result: + if token is '*': + current_op = operator.mul + elif token is '/': + current_op = operator.truediv else: - prod = op(prod, e) + prod = current_op(prod, token) return prod + def evaluator(variables, functions, string, cs=False): """ Evaluate an expression. Variables are passed as a dictionary @@ -202,20 +210,12 @@ def evaluator(variables, functions, string, cs=False): cs: Case sensitive """ - # log.debug("variables: {0}".format(variables)) - # log.debug("functions: {0}".format(functions)) - # log.debug("string: {0}".format(string)) - - all_variables = copy.copy(default_variables) - all_functions = copy.copy(default_functions) - - def func_parse_action(x): - return [all_functions[x[0]](x[1])] - - if not cs: - all_variables = lower_dict(all_variables) - all_functions = lower_dict(all_functions) + # LOG.debug("variables: {0}".format(variables)) + # LOG.debug("functions: {0}".format(functions)) + # LOG.debug("string: {0}".format(string)) + all_variables = copy.copy(DEFAULT_VARIABLES) + all_functions = copy.copy(DEFAULT_FUNCTIONS) all_variables.update(variables) all_functions.update(functions) @@ -234,7 +234,7 @@ def evaluator(variables, functions, string, cs=False): return float('nan') # SI suffixes and percent - number_suffix = MatchFirst([Literal(k) for k in suffixes.keys()]) + number_suffix = MatchFirst([Literal(k) for k in SUFFIXES.keys()]) plus_minus = Literal('+') | Literal('-') times_div = Literal('*') | Literal('/') @@ -249,11 +249,10 @@ def evaluator(variables, functions, string, cs=False): number = (inner_number + Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) + Optional(number_suffix)) - number = number.setParseAction(number_parse_action) # Convert to number + number.setParseAction(number_parse_action) # Convert to number # Predefine recursive variables expr = Forward() - factor = Forward() # Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution. # Special case for no variables because of how we understand PyParsing is put together @@ -261,9 +260,10 @@ def evaluator(variables, functions, string, cs=False): # We sort the list so that var names (like "e2") match before # mathematical constants (like "e"). This is kind of a hack. all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True) - literal_all_vars = [CasedLiteral(k) for k in all_variables_keys] - varnames = MatchFirst(literal_all_vars) - varnames.setParseAction(lambda x: [all_variables[k] for k in x]) + varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys]) + varnames.setParseAction( + lambda x: [all_variables[k] for k in x] + ) else: # all_variables includes DEFAULT_VARIABLES, which isn't empty # this is unreachable. Get rid of it? @@ -273,7 +273,9 @@ def evaluator(variables, functions, string, cs=False): if len(all_functions) > 0: funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()]) function = funcnames + Suppress("(") + expr + Suppress(")") - function.setParseAction(func_parse_action) + function.setParseAction( + lambda x: [all_functions[x[0]](x[1])] + ) else: # see note above (this is unreachable) function = NoMatch() @@ -281,11 +283,13 @@ def evaluator(variables, functions, string, cs=False): atom = number | function | varnames | Suppress("(") + expr + Suppress(")") # Do the following in the correct order to preserve order of operation - factor << (atom + ZeroOrMore("^" + atom)).setParseAction(exp_parse_action) # 7^6 - paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k - paritem = paritem.setParseAction(parallel) - term = paritem + ZeroOrMore(times_div + paritem) # 7 * 5 / 4 - 3 - term = term.setParseAction(prod_parse_action) - expr << Optional(plus_minus) + term + ZeroOrMore(plus_minus + term) # -5 + 4 - 3 - expr = expr.setParseAction(sum_parse_action) + pow_term = atom + ZeroOrMore(Suppress("^") + atom) + pow_term.setParseAction(exp_parse_action) # 7^6 + par_term = pow_term + ZeroOrMore(Suppress('||') + pow_term) # 5k || 4k + par_term.setParseAction(parallel) + prod_term = par_term + ZeroOrMore(times_div + par_term) # 7 * 5 / 4 - 3 + prod_term.setParseAction(prod_parse_action) + sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3 + sum_term.setParseAction(sum_parse_action) + expr << sum_term # finish the recursion return (expr + stringEnd).parseString(string)[0]