Add docstrings and comments
This commit is contained in:
@@ -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 copy
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
@@ -56,6 +62,10 @@ log = logging.getLogger("mitx.courseware.capa")
|
|||||||
|
|
||||||
|
|
||||||
class UndefinedVariable(Exception):
|
class UndefinedVariable(Exception):
|
||||||
|
"""
|
||||||
|
Used to indicate the student input of a variable, which was unused by the
|
||||||
|
instructor.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
# unused for now
|
# unused for now
|
||||||
# def raiseself(self):
|
# def raiseself(self):
|
||||||
@@ -67,7 +77,8 @@ 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.
|
||||||
|
|
||||||
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.
|
||||||
@@ -76,7 +87,7 @@ def check_variables(string, variables):
|
|||||||
undefined_variable = achar + Word(alphanums)
|
undefined_variable = achar + Word(alphanums)
|
||||||
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
|
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
|
||||||
bad_variables = list()
|
bad_variables = list()
|
||||||
for v in possible_variables:
|
for v in possible_variables:
|
||||||
@@ -90,26 +101,59 @@ def check_variables(string, variables):
|
|||||||
raise UndefinedVariable(' '.join(bad_variables))
|
raise UndefinedVariable(' '.join(bad_variables))
|
||||||
|
|
||||||
def lower_dict(d):
|
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])
|
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):
|
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): # [ '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))]
|
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 = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^
|
||||||
x.reverse()
|
x.reverse()
|
||||||
x = reduce(lambda a, b: b ** a, x)
|
x = reduce(lambda a, b: b ** a, x)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
|
def parallel(x):
|
||||||
# convert from pyparsing.ParseResults, which doesn't support '0 in 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)
|
x = list(x)
|
||||||
if len(x) == 1:
|
if len(x) == 1:
|
||||||
return x[0]
|
return x[0]
|
||||||
@@ -119,6 +163,13 @@ def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
|
|||||||
return 1. / sum(x)
|
return 1. / sum(x)
|
||||||
|
|
||||||
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
|
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
|
||||||
|
"""
|
||||||
|
Add the inputs
|
||||||
|
|
||||||
|
[ 1, '+', 2, '-', 3 ] -> 0
|
||||||
|
|
||||||
|
Allow a leading + or -
|
||||||
|
"""
|
||||||
total = 0.0
|
total = 0.0
|
||||||
op = ops['+']
|
op = ops['+']
|
||||||
for e in x:
|
for e in x:
|
||||||
@@ -129,6 +180,11 @@ def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
|
|||||||
return total
|
return total
|
||||||
|
|
||||||
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
|
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
|
||||||
|
"""
|
||||||
|
Multiply the inputs
|
||||||
|
|
||||||
|
[ 1, '*', 2, '/', 3 ] => 0.66
|
||||||
|
"""
|
||||||
prod = 1.0
|
prod = 1.0
|
||||||
op = ops['*']
|
op = ops['*']
|
||||||
for e in x:
|
for e in x:
|
||||||
@@ -139,14 +195,13 @@ def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
|
|||||||
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
|
||||||
from string to value. Unary functions are passed as a dictionary
|
from string to value. Unary functions are passed as a dictionary
|
||||||
from string to function. Variables must be floats.
|
from string to function. Variables must be floats.
|
||||||
cs: Case sensitive
|
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("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))
|
||||||
@@ -187,7 +242,8 @@ def evaluator(variables, functions, string, cs=False):
|
|||||||
|
|
||||||
# 0.33 or 7 or .34 or 16.
|
# 0.33 or 7 or .34 or 16.
|
||||||
inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
|
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
|
# 0.33k or -17
|
||||||
number = (inner_number
|
number = (inner_number
|
||||||
@@ -209,6 +265,8 @@ def evaluator(variables, functions, string, cs=False):
|
|||||||
varnames = MatchFirst(literal_all_vars)
|
varnames = MatchFirst(literal_all_vars)
|
||||||
varnames.setParseAction(lambda x: [all_variables[k] for k in x])
|
varnames.setParseAction(lambda x: [all_variables[k] for k in x])
|
||||||
else:
|
else:
|
||||||
|
# all_variables includes DEFAULT_VARIABLES, which isn't empty
|
||||||
|
# this is unreachable. Get rid of it?
|
||||||
varnames = NoMatch()
|
varnames = NoMatch()
|
||||||
|
|
||||||
# Same thing for functions.
|
# Same thing for functions.
|
||||||
@@ -217,6 +275,7 @@ def evaluator(variables, functions, string, cs=False):
|
|||||||
function = funcnames + Suppress("(") + expr + Suppress(")")
|
function = funcnames + Suppress("(") + expr + Suppress(")")
|
||||||
function.setParseAction(func_parse_action)
|
function.setParseAction(func_parse_action)
|
||||||
else:
|
else:
|
||||||
|
# see note above (this is unreachable)
|
||||||
function = NoMatch()
|
function = NoMatch()
|
||||||
|
|
||||||
atom = number | function | varnames | Suppress("(") + expr + Suppress(")")
|
atom = number | function | varnames | Suppress("(") + expr + Suppress(")")
|
||||||
|
|||||||
@@ -1717,6 +1717,7 @@ class FormulaResponse(LoncapaResponse):
|
|||||||
student_variables = dict()
|
student_variables = dict()
|
||||||
# ranges give numerical ranges for testing
|
# ranges give numerical ranges for testing
|
||||||
for var in ranges:
|
for var in ranges:
|
||||||
|
# TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
|
||||||
value = random.uniform(*ranges[var])
|
value = random.uniform(*ranges[var])
|
||||||
instructor_variables[str(var)] = value
|
instructor_variables[str(var)] = value
|
||||||
student_variables[str(var)] = value
|
student_variables[str(var)] = value
|
||||||
|
|||||||
Reference in New Issue
Block a user