From 74866a38b00c2ce5a593dc509dcb23f22febaaf0 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 12:55:11 -0400 Subject: [PATCH 01/14] Move parseActions and statics out of evaluator() --- common/lib/calc/calc.py | 144 ++++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 2ee82e2fb4..0ab02e413b 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -37,16 +37,33 @@ 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, + '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") class UndefinedVariable(Exception): - def raiseself(self): - ''' Helper so we can use inside of a lambda ''' - raise self + pass + # unused for now + # def raiseself(self): + # ''' Helper so we can use inside of a lambda ''' + # raise self -general_whitespace = re.compile('[^\w]+') +general_whitespace = re.compile('[^\\w]+') def check_variables(string, variables): @@ -65,13 +82,61 @@ def check_variables(string, variables): for v in possible_variables: if len(v) == 0: continue - if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers + if v[0] <= '9' and '0' <= v: # Skip things that begin with numbers continue if v not in variables: bad_variables.append(v) if len(bad_variables) > 0: raise UndefinedVariable(' '.join(bad_variables)) +def lower_dict(d): + return dict([(k.lower(), d[k]) for k in d]) + +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]] + else: + return float(text) + +def number_parse_action(x): # [ '7' ] -> [ 7 ] + return [super_float("".join(x))] + +def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512 + x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ + x.reverse() + x = reduce(lambda a, b: b ** a, x) + return x + +def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 + # convert from pyparsing.ParseResults, which doesn't support '0 in x' + x = list(x) + if len(x) == 1: + return x[0] + if 0 in x: + return float('nan') + x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || + return 1. / sum(x) + +def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 + total = 0.0 + op = ops['+'] + for e in x: + if e in set('+-'): + op = ops[e] + else: + total = op(total, e) + return total + +def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 + prod = 1.0 + op = ops['*'] + for e in x: + if e in set('*/'): + op = ops[e] + else: + prod = op(prod, e) + return prod def evaluator(variables, functions, string, cs=False): ''' @@ -86,12 +151,12 @@ def evaluator(variables, functions, string, cs=False): # log.debug("functions: {0}".format(functions)) # log.debug("string: {0}".format(string)) - def lower_dict(d): - return dict([(k.lower(), d[k]) for k in d]) - 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) @@ -113,69 +178,6 @@ def evaluator(variables, functions, string, cs=False): if string.strip() == "": return float('nan') - 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, - '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} - - 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]] - else: - return float(text) - - def number_parse_action(x): # [ '7' ] -> [ 7 ] - return [super_float("".join(x))] - - def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512 - x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ - x.reverse() - x = reduce(lambda a, b: b ** a, x) - return x - - def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 - # convert from pyparsing.ParseResults, which doesn't support '0 in x' - x = list(x) - if len(x) == 1: - return x[0] - if 0 in x: - return float('nan') - x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || - return 1. / sum(x) - - def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 - total = 0.0 - op = ops['+'] - for e in x: - if e in set('+-'): - op = ops[e] - else: - total = op(total, e) - return total - - def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 - prod = 1.0 - op = ops['*'] - for e in x: - if e in set('*/'): - op = ops[e] - else: - prod = op(prod, e) - return prod - - def func_parse_action(x): - return [all_functions[x[0]](x[1])] - # SI suffixes and percent number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) (dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") From ed45c505a39cf3a8aa094ee6c64591da1c604773 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 12:55:51 -0400 Subject: [PATCH 02/14] Simpler pyparsing --- common/lib/calc/calc.py | 48 +++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 0ab02e413b..64053d6ca5 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -8,11 +8,11 @@ import numpy import numbers import scipy.constants -from pyparsing import Word, alphas, nums, oneOf, Literal -from pyparsing import ZeroOrMore, OneOrMore, StringStart -from pyparsing import StringEnd, Optional, Forward -from pyparsing import CaselessLiteral, Group, StringEnd -from pyparsing import NoMatch, stringEnd, alphanums +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 default_functions = {'sin': numpy.sin, 'cos': numpy.cos, @@ -179,17 +179,19 @@ def evaluator(variables, functions, string, cs=False): return float('nan') # SI suffixes and percent - number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) - (dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") + number_suffix = MatchFirst([Literal(k) for k in suffixes.keys()]) + plus_minus = Literal('+') | Literal('-') + times_div = Literal('*') | Literal('/') number_part = Word(nums) # 0.33 or 7 or .34 or 16. inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part) + inner_number = Combine(inner_number) # by default pyparsing allows spaces between tokens--this prevents that # 0.33k or -17 - number = (Optional(minus | plus) + inner_number - + Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part) + number = (inner_number + + Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) + Optional(number_suffix)) number = number.setParseAction(number_parse_action) # Convert to number @@ -197,40 +199,34 @@ def evaluator(variables, functions, string, cs=False): expr = Forward() factor = Forward() - def sreduce(f, l): - ''' Same as reduce, but handle len 1 and len 0 lists sensibly ''' - if len(l) == 0: - return NoMatch() - if len(l) == 1: - return l[0] - return reduce(f, l) - # 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 if len(all_variables) > 0: # 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) - varnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_variables_keys)) - varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x)) + 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]) else: varnames = NoMatch() # Same thing for functions. if len(all_functions) > 0: - funcnames = sreduce(lambda x, y: x | y, - map(lambda x: CasedLiteral(x), all_functions.keys())) - function = funcnames + lpar.suppress() + expr + rpar.suppress() + funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()]) + function = funcnames + Suppress("(") + expr + Suppress(")") function.setParseAction(func_parse_action) else: function = NoMatch() - atom = number | function | varnames | lpar + expr + rpar - factor << (atom + ZeroOrMore(exp + atom)).setParseAction(exp_parse_action) # 7^6 + 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 = 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 << Optional(plus_minus) + term + ZeroOrMore(plus_minus + term) # -5 + 4 - 3 expr = expr.setParseAction(sum_parse_action) return (expr + stringEnd).parseString(string)[0] From 72d149caae1c5cd3909b59e850d94cb8ffc95c59 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 13:25:48 -0400 Subject: [PATCH 03/14] Add docstrings and comments --- common/lib/calc/calc.py | 81 +++++++++++++++++++++++---- common/lib/capa/capa/responsetypes.py | 1 + 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 64053d6ca5..5d0aeb3fd1 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -1,3 +1,9 @@ +""" +Parser and evaluator for FormulaResponse and NumericalResponse + +Uses pyparsing to parse. Main function as of now is evaluator(). +""" + import copy import logging import math @@ -56,6 +62,10 @@ log = logging.getLogger("mitx.courseware.capa") class UndefinedVariable(Exception): + """ + Used to indicate the student input of a variable, which was unused by the + instructor. + """ pass # unused for now # def raiseself(self): @@ -67,7 +77,8 @@ general_whitespace = re.compile('[^\\w]+') def check_variables(string, variables): - '''Confirm the only variables in string are defined. + """ + Confirm the only variables in string are defined. Pyparsing uses a left-to-right parser, which makes the more elegant approach pretty hopeless. @@ -76,7 +87,7 @@ def check_variables(string, variables): undefined_variable = achar + Word(alphanums) 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 bad_variables = list() for v in possible_variables: @@ -90,26 +101,59 @@ def check_variables(string, variables): raise UndefinedVariable(' '.join(bad_variables)) def lower_dict(d): + """ + takes each key in the dict and makes it lowercase, still mapping to the + same value. + + keep in mind that it is possible (but not useful?) to define different + 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]) +# The following few functions define parse actions, which are run on lists of +# results from each parse component. They convert the strings and (previously +# calculated) numbers into the number that component represents. + def super_float(text): - ''' Like float, but with si extensions. 1k goes to 1000''' + """ + Like float, but with si extensions. 1k goes to 1000 + """ if text[-1] in suffixes: return float(text[:-1]) * suffixes[text[-1]] else: return float(text) -def number_parse_action(x): # [ '7' ] -> [ 7 ] +def number_parse_action(x): + """ + Create a float out of its string parts + + e.g. [ '7', '.', '13' ] -> [ 7.13 ] + Calls super_float above + """ return [super_float("".join(x))] -def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512 +def exp_parse_action(x): + """ + 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 -def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 - # convert from pyparsing.ParseResults, which doesn't support '0 in x' +def parallel(x): + """ + Compute numbers according to the parallel resistors operator + + BTW it is commutative. Its formula is given by + out = 1 / (1/in1 + 1/in2 + ...) + e.g. [ 1, 2 ] => 2/3 + + Return NaN if there is a zero among the inputs + """ x = list(x) if len(x) == 1: return x[0] @@ -119,6 +163,13 @@ def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 return 1. / sum(x) def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 + """ + Add the inputs + + [ 1, '+', 2, '-', 3 ] -> 0 + + Allow a leading + or - + """ total = 0.0 op = ops['+'] for e in x: @@ -129,6 +180,11 @@ def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 return total def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 + """ + Multiply the inputs + + [ 1, '*', 2, '/', 3 ] => 0.66 + """ prod = 1.0 op = ops['*'] for e in x: @@ -139,14 +195,13 @@ def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 return prod def evaluator(variables, functions, string, cs=False): - ''' + """ Evaluate an expression. Variables are passed as a dictionary from string to value. Unary functions are passed as a dictionary from string to function. Variables must be floats. cs: Case sensitive - TODO: Fix it so we can pass integers and complex numbers in variables dict - ''' + """ # log.debug("variables: {0}".format(variables)) # log.debug("functions: {0}".format(functions)) # log.debug("string: {0}".format(string)) @@ -187,7 +242,8 @@ def evaluator(variables, functions, string, cs=False): # 0.33 or 7 or .34 or 16. inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part) - inner_number = Combine(inner_number) # by default pyparsing allows spaces between tokens--this prevents that + # by default pyparsing allows spaces between tokens--Combine prevents that + inner_number = Combine(inner_number) # 0.33k or -17 number = (inner_number @@ -209,6 +265,8 @@ def evaluator(variables, functions, string, cs=False): varnames = MatchFirst(literal_all_vars) 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? varnames = NoMatch() # Same thing for functions. @@ -217,6 +275,7 @@ def evaluator(variables, functions, string, cs=False): function = funcnames + Suppress("(") + expr + Suppress(")") function.setParseAction(func_parse_action) else: + # see note above (this is unreachable) function = NoMatch() atom = number | function | varnames | Suppress("(") + expr + Suppress(")") diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0fa50079de..314d01e7e8 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1717,6 +1717,7 @@ class FormulaResponse(LoncapaResponse): student_variables = dict() # ranges give numerical ranges for testing for var in ranges: + # TODO: allow specified ranges (i.e. integers and complex numbers) for random variables value = random.uniform(*ranges[var]) instructor_variables[str(var)] = value student_variables[str(var)] = value From a85a7f71df6c0bc889b2d5cbe40926b3663d375e Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 13:34:58 -0400 Subject: [PATCH 04/14] 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] From 83f1f9c2fc78442c77376457094ba674bca59c49 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 5 Jun 2013 15:50:35 -0400 Subject: [PATCH 05/14] Set numpy so it does not print out warnings on student input --- common/lib/calc/calc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index f862b41542..cc3a883221 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -13,6 +13,10 @@ import re import numpy import scipy.constants +# have numpy raise errors on functions outside its domain +# See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html +numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise' + from pyparsing import (Word, nums, Literal, ZeroOrMore, MatchFirst, Optional, Forward, From a746a9ad1511e5d02cea41a63250d3409d7868a9 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 10 Jun 2013 11:02:19 -0400 Subject: [PATCH 06/14] Get rid of unused code --- common/lib/calc/calc.py | 44 +++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index cc3a883221..349810d4c9 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -21,7 +21,7 @@ from pyparsing import (Word, nums, Literal, ZeroOrMore, MatchFirst, Optional, Forward, CaselessLiteral, - NoMatch, stringEnd, Suppress, Combine) + stringEnd, Suppress, Combine) DEFAULT_FUNCTIONS = {'sin': numpy.sin, 'cos': numpy.cos, @@ -258,31 +258,27 @@ def evaluator(variables, functions, string, cs=False): # Predefine recursive variables expr = 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 - if len(all_variables) > 0: - # 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) - 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? - varnames = NoMatch() + # Handle variables passed in. + # E.g. if we have {'R':0.5}, we make the substitution. + # 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) + varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys]) + varnames.setParseAction( + lambda x: [all_variables[k] for k in x] + ) + + # if all_variables were empty, then pyparsing wants + # varnames = NoMatch() + # this is not the case, as all_variables contains the defaults # Same thing for functions. - if len(all_functions) > 0: - funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()]) - function = funcnames + Suppress("(") + expr + Suppress(")") - function.setParseAction( - lambda x: [all_functions[x[0]](x[1])] - ) - else: - # see note above (this is unreachable) - function = NoMatch() + all_functions_keys = sorted(all_functions.keys(), key=len, reverse=True) + funcnames = MatchFirst([CasedLiteral(k) for k in all_functions_keys]) + function = funcnames + Suppress("(") + expr + Suppress(")") + function.setParseAction( + lambda x: [all_functions[x[0]](x[1])] + ) atom = number | function | varnames | Suppress("(") + expr + Suppress(")") From 58e98d13cc5df9f0ff2994719fcf152219ada507 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 10 Jun 2013 11:34:59 -0400 Subject: [PATCH 07/14] Make Jenkins test the calc module --- jenkins/test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/jenkins/test.sh b/jenkins/test.sh index 35be3a0121..127bf4fa1d 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -77,6 +77,7 @@ rake test_cms || TESTS_FAILED=1 rake test_lms || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1 +rake test_common/lib/calc || TESTS_FAILED=1 # Run the javascript unit tests rake phantomjs_jasmine_lms || TESTS_FAILED=1 From ed90ed9a345aac35cbdc878ddc484475923c08fd Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 5 Jun 2013 15:36:49 -0400 Subject: [PATCH 08/14] Added tests for new math functions --- common/lib/calc/tests/test_calc.py | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py index cfa1b7525d..e29c6776a9 100644 --- a/common/lib/calc/tests/test_calc.py +++ b/common/lib/calc/tests/test_calc.py @@ -194,6 +194,105 @@ class EvaluatorTest(unittest.TestCase): arctan_angles = arcsin_angles self.assert_function_values('arctan', arctan_inputs, arctan_angles) + def test_reciprocal_trig_functions(self): + """ + Test the reciprocal trig functions provided in calc.py + + which are: sec, csc, cot, arcsec, arccsc, arccot + """ + angles = ['-pi/4', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] + sec_values = [1.414, 1.155, 1.236, -1.414, 1.414, 0.498+0.591j] + csc_values = [-1.414, 2, 1.701, -1.414, 1.414, 0.622-0.304j] + cot_values = [-1, 1.732, 1.376, 1, 1, 0.218-0.868j] + + self.assert_function_values('sec', angles, sec_values) + self.assert_function_values('csc', angles, csc_values) + self.assert_function_values('cot', angles, cot_values) + + arcsec_inputs = ['1.1547', '1.2361', '2', '-2', '-1.4142', '0.4983+0.5911*j'] + arcsec_angles = [0.524, 0.628, 1.047, 2.094, 2.356, 1 + 1j] + self.assert_function_values('arcsec', arcsec_inputs, arcsec_angles) + + arccsc_inputs = ['-1.1547', '-1.4142', '2', '1.7013', '1.1547', '0.6215-0.3039*j'] + arccsc_angles = [-1.047, -0.785, 0.524, 0.628, 1.047, 1 + 1j] + self.assert_function_values('arccsc', arccsc_inputs, arccsc_angles) + + # Has the same range as arccsc + arccot_inputs = ['-0.5774', '-1', '1.7321', '1.3764', '0.5774', '(0.2176-0.868*j)'] + arccot_angles = arccsc_angles + self.assert_function_values('arccot', arccot_inputs, arccot_angles) + + def test_hyperbolic_functions(self): + """ + Test the hyperbolic functions + + which are: sinh, cosh, tanh, sech, csch, coth + """ + inputs = ['0', '0.5', '1', '2', '1+j'] + neg_inputs = ['0', '-0.5', '-1', '-2', '-1-j'] + negate = lambda x: [-k for k in x] + + # sinh is odd + sinh_vals = [0, 0.521, 1.175, 3.627, 0.635 + 1.298j] + self.assert_function_values('sinh', inputs, sinh_vals) + self.assert_function_values('sinh', neg_inputs, negate(sinh_vals)) + + # cosh is even - do not negate + cosh_vals = [1, 1.128, 1.543, 3.762, 0.834 + 0.989j] + self.assert_function_values('cosh', inputs, cosh_vals) + self.assert_function_values('cosh', neg_inputs, cosh_vals) + + # tanh is odd + tanh_vals = [0, 0.462, 0.762, 0.964, 1.084 + 0.272j] + self.assert_function_values('tanh', inputs, tanh_vals) + self.assert_function_values('tanh', neg_inputs, negate(tanh_vals)) + + # sech is even - do not negate + sech_vals = [1, 0.887, 0.648, 0.266, 0.498 - 0.591j] + self.assert_function_values('sech', inputs, sech_vals) + self.assert_function_values('sech', neg_inputs, sech_vals) + + # the following functions do not have 0 in their domain + inputs = inputs[1:] + neg_inputs = neg_inputs[1:] + + # csch is odd + csch_vals = [1.919, 0.851, 0.276, 0.304 - 0.622j] + self.assert_function_values('csch', inputs, csch_vals) + self.assert_function_values('csch', neg_inputs, negate(csch_vals)) + + # coth is odd + coth_vals = [2.164, 1.313, 1.037, 0.868 - 0.218j] + self.assert_function_values('coth', inputs, coth_vals) + self.assert_function_values('coth', neg_inputs, negate(coth_vals)) + + def test_hyperbolic_inverses(self): + """ + Test the inverse hyperbolic functions + + which are of the form arc[X]h + """ + results = [0, 0.5, 1, 2, 1+1j] + + sinh_vals = ['0', '0.5211', '1.1752', '3.6269', '0.635+1.2985*j'] + self.assert_function_values('arcsinh', sinh_vals, results) + + cosh_vals = ['1', '1.1276', '1.5431', '3.7622', '0.8337+0.9889*j'] + self.assert_function_values('arccosh', cosh_vals, results) + + tanh_vals = ['0', '0.4621', '0.7616', '0.964', '1.0839+0.2718*j'] + self.assert_function_values('arctanh', tanh_vals, results) + + sech_vals = ['1.0', '0.8868', '0.6481', '0.2658', '0.4983-0.5911*j'] + self.assert_function_values('arcsech', sech_vals, results) + + results = results[1:] + csch_vals = ['1.919', '0.8509', '0.2757', '0.3039-0.6215*j'] + self.assert_function_values('arccsch', csch_vals, results) + + coth_vals = ['2.164', '1.313', '1.0373', '0.868-0.2176*j'] + self.assert_function_values('arccoth', coth_vals, results) + def test_other_functions(self): """ Test the non-trig functions provided in calc.py From 944e3390e0f4f63b90e87b65e529115c8d8b26e0 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 3 Jun 2013 17:20:52 -0400 Subject: [PATCH 09/14] Add support for various math functions in calc.py. --- common/lib/calc/calc.py | 22 ++++++- common/lib/calc/calcfunctions.py | 99 ++++++++++++++++++++++++++++++ common/lib/calc/tests/test_calc.py | 8 +-- 3 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 common/lib/calc/calcfunctions.py diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 349810d4c9..d3874639bc 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -12,6 +12,7 @@ import re import numpy import scipy.constants +import calcfunctions # have numpy raise errors on functions outside its domain # See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html @@ -26,16 +27,35 @@ from pyparsing import (Word, nums, Literal, DEFAULT_FUNCTIONS = {'sin': numpy.sin, 'cos': numpy.cos, 'tan': numpy.tan, + 'sec': calcfunctions.sec, + 'csc': calcfunctions.csc, + 'cot': calcfunctions.cot, 'sqrt': numpy.sqrt, 'log10': numpy.log10, 'log2': numpy.log2, 'ln': numpy.log, + 'exp': numpy.exp, 'arccos': numpy.arccos, 'arcsin': numpy.arcsin, 'arctan': numpy.arctan, + 'arcsec': calcfunctions.arcsec, + 'arccsc': calcfunctions.arccsc, + 'arccot': calcfunctions.arccot, 'abs': numpy.abs, 'fact': math.factorial, - 'factorial': math.factorial + 'factorial': math.factorial, + 'sinh': numpy.sinh, + 'cosh': numpy.cosh, + 'tanh': numpy.tanh, + 'sech': calcfunctions.sech, + 'csch': calcfunctions.csch, + 'coth': calcfunctions.coth, + 'arcsinh': numpy.arcsinh, + 'arccosh': numpy.arccosh, + 'arctanh': numpy.arctanh, + 'arcsech': calcfunctions.arcsech, + 'arccsch': calcfunctions.arccsch, + 'arccoth': calcfunctions.arccoth } DEFAULT_VARIABLES = {'j': numpy.complex(0, 1), 'e': numpy.e, diff --git a/common/lib/calc/calcfunctions.py b/common/lib/calc/calcfunctions.py new file mode 100644 index 0000000000..d0ac4f7a3d --- /dev/null +++ b/common/lib/calc/calcfunctions.py @@ -0,0 +1,99 @@ +""" +Provide the mathematical functions that numpy doesn't. + +Specifically, the secant/cosecant/cotangents and their inverses and +hyperbolic counterparts +""" +import numpy + + +# Normal Trig +def sec(arg): + """ + Secant + """ + return 1 / numpy.cos(arg) + + +def csc(arg): + """ + Cosecant + """ + return 1 / numpy.sin(arg) + + +def cot(arg): + """ + Cotangent + """ + return 1 / numpy.tan(arg) + + +# Inverse Trig +# http://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Relationships_among_the_inverse_trigonometric_functions +def arcsec(val): + """ + Inverse secant + """ + return numpy.arccos(1. / val) + + +def arccsc(val): + """ + Inverse cosecant + """ + return numpy.arcsin(1. / val) + + +def arccot(val): + """ + Inverse cotangent + """ + if numpy.real(val) < 0: + return -numpy.pi / 2 - numpy.arctan(val) + else: + return numpy.pi / 2 - numpy.arctan(val) + + +# Hyperbolic Trig +def sech(arg): + """ + Hyperbolic secant + """ + return 1 / numpy.cosh(arg) + + +def csch(arg): + """ + Hyperbolic cosecant + """ + return 1 / numpy.sinh(arg) + + +def coth(arg): + """ + Hyperbolic cotangent + """ + return 1 / numpy.tanh(arg) + + +# And their inverses +def arcsech(val): + """ + Inverse hyperbolic secant + """ + return numpy.arccosh(1. / val) + + +def arccsch(val): + """ + Inverse hyperbolic cosecant + """ + return numpy.arcsinh(1. / val) + + +def arccoth(val): + """ + Inverse hyperbolic cotangent + """ + return numpy.arctanh(1. / val) diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py index e29c6776a9..13cd9e9471 100644 --- a/common/lib/calc/tests/test_calc.py +++ b/common/lib/calc/tests/test_calc.py @@ -201,9 +201,9 @@ class EvaluatorTest(unittest.TestCase): which are: sec, csc, cot, arcsec, arccsc, arccot """ angles = ['-pi/4', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] - sec_values = [1.414, 1.155, 1.236, -1.414, 1.414, 0.498+0.591j] - csc_values = [-1.414, 2, 1.701, -1.414, 1.414, 0.622-0.304j] - cot_values = [-1, 1.732, 1.376, 1, 1, 0.218-0.868j] + sec_values = [1.414, 1.155, 1.236, -1.414, 1.414, 0.498 + 0.591j] + csc_values = [-1.414, 2, 1.701, -1.414, 1.414, 0.622 - 0.304j] + cot_values = [-1, 1.732, 1.376, 1, 1, 0.218 - 0.868j] self.assert_function_values('sec', angles, sec_values) self.assert_function_values('csc', angles, csc_values) @@ -272,7 +272,7 @@ class EvaluatorTest(unittest.TestCase): which are of the form arc[X]h """ - results = [0, 0.5, 1, 2, 1+1j] + results = [0, 0.5, 1, 2, 1 + 1j] sinh_vals = ['0', '0.5211', '1.1752', '3.6269', '0.635+1.2985*j'] self.assert_function_values('arcsinh', sinh_vals, results) From 0f72eedd37285e59a2e93932e4a2c1c967053376 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 10 Jun 2013 10:51:17 -0400 Subject: [PATCH 10/14] Add variable i as an imaginary unit --- common/lib/calc/calc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index d3874639bc..3afc0f91bc 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -57,7 +57,8 @@ DEFAULT_FUNCTIONS = {'sin': numpy.sin, 'arccsch': calcfunctions.arccsch, 'arccoth': calcfunctions.arccoth } -DEFAULT_VARIABLES = {'j': numpy.complex(0, 1), +DEFAULT_VARIABLES = {'i': numpy.complex(0, 1), + 'j': numpy.complex(0, 1), 'e': numpy.e, 'pi': numpy.pi, 'k': scipy.constants.k, From 5735ccaad5ac24c653bf97bc008b3165c18b6684 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Thu, 13 Jun 2013 11:09:22 -0400 Subject: [PATCH 11/14] Address Sarina's comments from the PR -Clean up `SUFFIXES` from interspersed commented-out suffixes -Remove unused `LOG` -Remove unused method `raiseself` of UndefinedVariable and its usage --- common/lib/calc/calc.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 3afc0f91bc..27745826c4 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -5,7 +5,6 @@ Uses pyparsing to parse. Main function as of now is evaluator(). """ import copy -import logging import math import operator import re @@ -67,15 +66,14 @@ DEFAULT_VARIABLES = {'i': numpy.complex(0, 1), 'q': scipy.constants.e } -# We eliminated extreme ones, since they're rarely used, and potentially +# We eliminated the following extreme suffixes: +# P (1e15), E (1e18), Z (1e21), Y (1e24), +# f (1e-15), a (1e-18), z (1e-21), y (1e-24) +# 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, - '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") +SUFFIXES = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12, + 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12} class UndefinedVariable(Exception): @@ -84,10 +82,6 @@ class UndefinedVariable(Exception): instructor. """ pass - # unused for now - # def raiseself(self): - # ''' Helper so we can use inside of a lambda ''' - # raise self def check_variables(string, variables): @@ -96,13 +90,8 @@ def check_variables(string, variables): Otherwise, raise an UndefinedVariable containing all bad variables. - Pyparsing uses a left-to-right parser, which makes the more + Pyparsing uses a left-to-right parser, which makes a more elegant approach pretty hopeless. - - achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character - undefined_variable = achar + Word(alphanums) - undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) - varnames = varnames | undefined_variable """ general_whitespace = re.compile('[^\\w]+') # List of all alnums in string @@ -235,9 +224,6 @@ 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) From 662c577951c6914c8d5f19767c046083863ee1fd Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Thu, 13 Jun 2013 11:35:06 -0400 Subject: [PATCH 12/14] Undo change to jenkins/test.sh in anticipation of merge Original change: SHA 58e98d This was resolved by merging in Cale's branch https://github.com/edx/edx-platform/pull/44 --- jenkins/test.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/jenkins/test.sh b/jenkins/test.sh index 127bf4fa1d..35be3a0121 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -77,7 +77,6 @@ rake test_cms || TESTS_FAILED=1 rake test_lms || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1 -rake test_common/lib/calc || TESTS_FAILED=1 # Run the javascript unit tests rake phantomjs_jasmine_lms || TESTS_FAILED=1 From 46686d8f7da4dccd0ed69771124c46f111bc53f9 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Thu, 13 Jun 2013 16:38:43 -0400 Subject: [PATCH 13/14] Quick fixes in calc.py Especially the var[0].isdigit() one. --- common/lib/calc/calc.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 27745826c4..f0934a9ed5 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -96,11 +96,11 @@ def check_variables(string, variables): general_whitespace = re.compile('[^\\w]+') # List of all alnums in string possible_variables = re.split(general_whitespace, string) - bad_variables = list() + bad_variables = [] for var in possible_variables: if len(var) == 0: continue - if var[0] <= '9' and '0' <= var: # Skip things that begin with numbers + if var[0].isdigit(): # Skip things that begin with numbers continue if var not in variables: bad_variables.append(var) @@ -117,7 +117,7 @@ def lower_dict(input_dict): 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(), input_dict[k]) for k in input_dict]) + return {k.lower(): v for k, v in input_dict.iteritems()} # The following few functions define parse actions, which are run on lists of @@ -151,8 +151,7 @@ def exp_parse_action(parse_result): e.g. [ 3, 2, 3 ] (which is 3^2^3 = 3^(2^3)) -> 6561 """ # pyparsing.ParseResults doesn't play well with reverse() - parse_result = parse_result.asList() - parse_result.reverse() + parse_result = reversed(parse_result) # the result of an exponentiation is called a power power = reduce(lambda a, b: b ** a, parse_result) return power From ab700e6352fd8b4c7c77b36b01af5dd546232384 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Thu, 13 Jun 2013 16:46:17 -0400 Subject: [PATCH 14/14] Add CHANGELOG message --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 89872937bf..640ee524cd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,8 @@ LMS: Some errors handling Non-ASCII data in XML courses have been fixed. LMS: Add page-load tracking using segment-io (if SEGMENT_IO_LMS_KEY and SEGMENT_IO_LMS feature flag is on) +Blades: Simplify calc.py (which is used for the Numerical/Formula responses); add trig/other functions. + LMS: Background colors on login, register, and courseware have been corrected back to white.