Rename variables; get rid of OPS
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user