Commit a85a7f71 by Peter Baratta

Rename variables; get rid of OPS

parent 72d149ca
...@@ -11,16 +11,15 @@ import operator ...@@ -11,16 +11,15 @@ import operator
import re import re
import numpy import numpy
import numbers
import scipy.constants import scipy.constants
from pyparsing import Word, nums, Literal from pyparsing import (Word, nums, Literal,
from pyparsing import ZeroOrMore, MatchFirst ZeroOrMore, MatchFirst,
from pyparsing import Optional, Forward Optional, Forward,
from pyparsing import CaselessLiteral CaselessLiteral,
from pyparsing import NoMatch, stringEnd, Suppress, Combine NoMatch, stringEnd, Suppress, Combine)
default_functions = {'sin': numpy.sin, DEFAULT_FUNCTIONS = {'sin': numpy.sin,
'cos': numpy.cos, 'cos': numpy.cos,
'tan': numpy.tan, 'tan': numpy.tan,
'sqrt': numpy.sqrt, 'sqrt': numpy.sqrt,
...@@ -34,7 +33,7 @@ default_functions = {'sin': numpy.sin, ...@@ -34,7 +33,7 @@ default_functions = {'sin': numpy.sin,
'fact': math.factorial, 'fact': math.factorial,
'factorial': math.factorial 'factorial': math.factorial
} }
default_variables = {'j': numpy.complex(0, 1), DEFAULT_VARIABLES = {'j': numpy.complex(0, 1),
'e': numpy.e, 'e': numpy.e,
'pi': numpy.pi, 'pi': numpy.pi,
'k': scipy.constants.k, 'k': scipy.constants.k,
...@@ -43,22 +42,15 @@ default_variables = {'j': numpy.complex(0, 1), ...@@ -43,22 +42,15 @@ default_variables = {'j': numpy.complex(0, 1),
'q': scipy.constants.e '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 # We eliminated extreme ones, since they're rarely used, and potentially
# confusing. They may also conflict with variables if we ever allow e.g. # confusing. They may also conflict with variables if we ever allow e.g.
# 5R instead of 5*R # 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, 'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
'c': 1e-2, 'm': 1e-3, 'u': 1e-6, '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} '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): class UndefinedVariable(Exception):
...@@ -73,13 +65,12 @@ class UndefinedVariable(Exception): ...@@ -73,13 +65,12 @@ class UndefinedVariable(Exception):
# raise self # raise self
general_whitespace = re.compile('[^\\w]+')
def check_variables(string, variables): def check_variables(string, variables):
""" """
Confirm the only variables in string are defined. 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 Pyparsing uses a left-to-right parser, which makes the more
elegant approach pretty hopeless. elegant approach pretty hopeless.
...@@ -88,19 +79,22 @@ def check_variables(string, variables): ...@@ -88,19 +79,22 @@ def check_variables(string, variables):
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself())
varnames = varnames | undefined_variable 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() bad_variables = list()
for v in possible_variables: for var in possible_variables:
if len(v) == 0: if len(var) == 0:
continue 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 continue
if v not in variables: if var not in variables:
bad_variables.append(v) bad_variables.append(var)
if len(bad_variables) > 0: if len(bad_variables) > 0:
raise UndefinedVariable(' '.join(bad_variables)) 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 takes each key in the dict and makes it lowercase, still mapping to the
same value. same value.
...@@ -109,7 +103,8 @@ def lower_dict(d): ...@@ -109,7 +103,8 @@ def lower_dict(d):
variables that have the same lowercase representation. It would be hard to variables that have the same lowercase representation. It would be hard to
tell which is used in the final dict and which isn't. 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 # The following few functions define parse actions, which are run on lists of
# results from each parse component. They convert the strings and (previously # results from each parse component. They convert the strings and (previously
...@@ -119,32 +114,37 @@ def super_float(text): ...@@ -119,32 +114,37 @@ 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: if text[-1] in SUFFIXES:
return float(text[:-1]) * suffixes[text[-1]] return float(text[:-1]) * SUFFIXES[text[-1]]
else: else:
return float(text) return float(text)
def number_parse_action(x):
def number_parse_action(parse_result):
""" """
Create a float out of its string parts Create a float out of its string parts
e.g. [ '7', '.', '13' ] -> [ 7.13 ] e.g. [ '7', '.', '13' ] -> [ 7.13 ]
Calls super_float above 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 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 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 ^ # pyparsing.ParseResults doesn't play well with reverse()
x.reverse() parse_result = parse_result.asList()
x = reduce(lambda a, b: b ** a, x) parse_result.reverse()
return x # 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 Compute numbers according to the parallel resistors operator
...@@ -154,15 +154,17 @@ def parallel(x): ...@@ -154,15 +154,17 @@ def parallel(x):
Return NaN if there is a zero among the inputs Return NaN if there is a zero among the inputs
""" """
x = list(x) # convert from pyparsing.ParseResults, which doesn't support '0 in parse_result'
if len(x) == 1: parse_result = parse_result.asList()
return x[0] if len(parse_result) == 1:
if 0 in x: return parse_result[0]
if 0 in parse_result:
return float('nan') return float('nan')
x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || reciprocals = [1. / e for e in parse_result]
return 1. / sum(x) return 1. / sum(reciprocals)
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
def sum_parse_action(parse_result):
""" """
Add the inputs Add the inputs
...@@ -171,29 +173,35 @@ def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 ...@@ -171,29 +173,35 @@ def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
Allow a leading + or - Allow a leading + or -
""" """
total = 0.0 total = 0.0
op = ops['+'] current_op = operator.add
for e in x: for token in parse_result:
if e in set('+-'): if token is '+':
op = ops[e] current_op = operator.add
elif token is '-':
current_op = operator.sub
else: else:
total = op(total, e) total = current_op(total, token)
return total return total
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
def prod_parse_action(parse_result):
""" """
Multiply the inputs Multiply the inputs
[ 1, '*', 2, '/', 3 ] => 0.66 [ 1, '*', 2, '/', 3 ] => 0.66
""" """
prod = 1.0 prod = 1.0
op = ops['*'] current_op = operator.mul
for e in x: for token in parse_result:
if e in set('*/'): if token is '*':
op = ops[e] current_op = operator.mul
elif token is '/':
current_op = operator.truediv
else: else:
prod = op(prod, e) prod = current_op(prod, token)
return prod return prod
def evaluator(variables, functions, string, cs=False): def evaluator(variables, functions, string, cs=False):
""" """
Evaluate an expression. Variables are passed as a dictionary Evaluate an expression. Variables are passed as a dictionary
...@@ -202,20 +210,12 @@ def evaluator(variables, functions, string, cs=False): ...@@ -202,20 +210,12 @@ def evaluator(variables, functions, string, cs=False):
cs: Case sensitive cs: Case sensitive
""" """
# log.debug("variables: {0}".format(variables)) # LOG.debug("variables: {0}".format(variables))
# log.debug("functions: {0}".format(functions)) # LOG.debug("functions: {0}".format(functions))
# log.debug("string: {0}".format(string)) # 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)
all_variables = copy.copy(DEFAULT_VARIABLES)
all_functions = copy.copy(DEFAULT_FUNCTIONS)
all_variables.update(variables) all_variables.update(variables)
all_functions.update(functions) all_functions.update(functions)
...@@ -234,7 +234,7 @@ def evaluator(variables, functions, string, cs=False): ...@@ -234,7 +234,7 @@ def evaluator(variables, functions, string, cs=False):
return float('nan') return float('nan')
# SI suffixes and percent # 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('-') plus_minus = Literal('+') | Literal('-')
times_div = Literal('*') | Literal('/') times_div = Literal('*') | Literal('/')
...@@ -249,11 +249,10 @@ def evaluator(variables, functions, string, cs=False): ...@@ -249,11 +249,10 @@ def evaluator(variables, functions, string, cs=False):
number = (inner_number number = (inner_number
+ Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) + Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part)
+ Optional(number_suffix)) + Optional(number_suffix))
number = number.setParseAction(number_parse_action) # Convert to number number.setParseAction(number_parse_action) # Convert to number
# Predefine recursive variables # Predefine recursive variables
expr = Forward() expr = Forward()
factor = Forward()
# Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution. # 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 # 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): ...@@ -261,9 +260,10 @@ def evaluator(variables, functions, string, cs=False):
# We sort the list so that var names (like "e2") match before # We sort the list so that var names (like "e2") match before
# mathematical constants (like "e"). This is kind of a hack. # mathematical constants (like "e"). This is kind of a hack.
all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True) all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True)
literal_all_vars = [CasedLiteral(k) for k in all_variables_keys] varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys])
varnames = MatchFirst(literal_all_vars) varnames.setParseAction(
varnames.setParseAction(lambda x: [all_variables[k] for k in x]) lambda x: [all_variables[k] for k in x]
)
else: else:
# all_variables includes DEFAULT_VARIABLES, which isn't empty # all_variables includes DEFAULT_VARIABLES, which isn't empty
# this is unreachable. Get rid of it? # this is unreachable. Get rid of it?
...@@ -273,7 +273,9 @@ def evaluator(variables, functions, string, cs=False): ...@@ -273,7 +273,9 @@ def evaluator(variables, functions, string, cs=False):
if len(all_functions) > 0: if len(all_functions) > 0:
funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()]) funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()])
function = funcnames + Suppress("(") + expr + Suppress(")") function = funcnames + Suppress("(") + expr + Suppress(")")
function.setParseAction(func_parse_action) function.setParseAction(
lambda x: [all_functions[x[0]](x[1])]
)
else: else:
# see note above (this is unreachable) # see note above (this is unreachable)
function = NoMatch() function = NoMatch()
...@@ -281,11 +283,13 @@ def evaluator(variables, functions, string, cs=False): ...@@ -281,11 +283,13 @@ def evaluator(variables, functions, string, cs=False):
atom = number | function | varnames | Suppress("(") + expr + Suppress(")") atom = number | function | varnames | Suppress("(") + expr + Suppress(")")
# Do the following in the correct order to preserve order of operation # Do the following in the correct order to preserve order of operation
factor << (atom + ZeroOrMore("^" + atom)).setParseAction(exp_parse_action) # 7^6 pow_term = atom + ZeroOrMore(Suppress("^") + atom)
paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k pow_term.setParseAction(exp_parse_action) # 7^6
paritem = paritem.setParseAction(parallel) par_term = pow_term + ZeroOrMore(Suppress('||') + pow_term) # 5k || 4k
term = paritem + ZeroOrMore(times_div + paritem) # 7 * 5 / 4 - 3 par_term.setParseAction(parallel)
term = term.setParseAction(prod_parse_action) prod_term = par_term + ZeroOrMore(times_div + par_term) # 7 * 5 / 4 - 3
expr << Optional(plus_minus) + term + ZeroOrMore(plus_minus + term) # -5 + 4 - 3 prod_term.setParseAction(prod_parse_action)
expr = expr.setParseAction(sum_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] return (expr + stringEnd).parseString(string)[0]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment