Commit 80befacb by Peter Baratta

Seperate out parsing from calc.py

calc.py:
-Create a method called `parse_algebra`. It takes a string of math and returns with a `pyparsing.ParseResults` object representing it.
-`evaluator` takes this tree and applies the old "parse actions" to it to get the same number as it used to.
-Change a few things in the parse actions (i.e. rename them to eval actions)
-Add `atom_parse_action`
-Add `find_vars_funcs` to aggregate used functions and vars in the tree
-Change strategy of `check_variables` to instead take the set difference of `find_vars_funcs` with the allowed ones
-Create `add_defaults` to take user defined lists of vars and funcs and return a combined list
-Change `x.setResultsName(...)` to `x(...)` for brevity

In test_calc.py:
-Fix a bad test (which expected something to pass that shouldn't)
-Fix indentation
parent 0f9d7230
...@@ -7,64 +7,65 @@ Uses pyparsing to parse. Main function as of now is evaluator(). ...@@ -7,64 +7,65 @@ Uses pyparsing to parse. Main function as of now is evaluator().
import copy import copy
import math import math
import operator import operator
import re import numbers
import numpy import numpy
import scipy.constants import scipy.constants
import calcfunctions import calcfunctions
# have numpy raise errors on functions outside its domain # Have numpy ignore errors on functions outside its domain
# See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html # See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html
numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise' numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise'
from pyparsing import (Word, nums, Literal, from pyparsing import (Word, nums, Literal,
ZeroOrMore, MatchFirst, ZeroOrMore, MatchFirst,
Optional, Forward, Optional, Forward,
CaselessLiteral, CaselessLiteral, Group, ParseResults,
stringEnd, Suppress, Combine) stringEnd, Suppress, Combine, alphas, alphanums)
DEFAULT_FUNCTIONS = {'sin': numpy.sin, DEFAULT_FUNCTIONS = {
'cos': numpy.cos, 'sin': numpy.sin,
'tan': numpy.tan, 'cos': numpy.cos,
'sec': calcfunctions.sec, 'tan': numpy.tan,
'csc': calcfunctions.csc, 'sec': calcfunctions.sec,
'cot': calcfunctions.cot, 'csc': calcfunctions.csc,
'sqrt': numpy.sqrt, 'cot': calcfunctions.cot,
'log10': numpy.log10, 'sqrt': numpy.sqrt,
'log2': numpy.log2, 'log10': numpy.log10,
'ln': numpy.log, 'log2': numpy.log2,
'exp': numpy.exp, 'ln': numpy.log,
'arccos': numpy.arccos, 'exp': numpy.exp,
'arcsin': numpy.arcsin, 'arccos': numpy.arccos,
'arctan': numpy.arctan, 'arcsin': numpy.arcsin,
'arcsec': calcfunctions.arcsec, 'arctan': numpy.arctan,
'arccsc': calcfunctions.arccsc, 'arcsec': calcfunctions.arcsec,
'arccot': calcfunctions.arccot, 'arccsc': calcfunctions.arccsc,
'abs': numpy.abs, 'arccot': calcfunctions.arccot,
'fact': math.factorial, 'abs': numpy.abs,
'factorial': math.factorial, 'fact': math.factorial,
'sinh': numpy.sinh, 'factorial': math.factorial,
'cosh': numpy.cosh, 'sinh': numpy.sinh,
'tanh': numpy.tanh, 'cosh': numpy.cosh,
'sech': calcfunctions.sech, 'tanh': numpy.tanh,
'csch': calcfunctions.csch, 'sech': calcfunctions.sech,
'coth': calcfunctions.coth, 'csch': calcfunctions.csch,
'arcsinh': numpy.arcsinh, 'coth': calcfunctions.coth,
'arccosh': numpy.arccosh, 'arcsinh': numpy.arcsinh,
'arctanh': numpy.arctanh, 'arccosh': numpy.arccosh,
'arcsech': calcfunctions.arcsech, 'arctanh': numpy.arctanh,
'arccsch': calcfunctions.arccsch, 'arcsech': calcfunctions.arcsech,
'arccoth': calcfunctions.arccoth 'arccsch': calcfunctions.arccsch,
} 'arccoth': calcfunctions.arccoth
DEFAULT_VARIABLES = {'i': numpy.complex(0, 1), }
'j': numpy.complex(0, 1), DEFAULT_VARIABLES = {
'e': numpy.e, 'i': numpy.complex(0, 1),
'pi': numpy.pi, 'j': numpy.complex(0, 1),
'k': scipy.constants.k, 'e': numpy.e,
'c': scipy.constants.c, 'pi': numpy.pi,
'T': 298.15, 'k': scipy.constants.k,
'q': scipy.constants.e 'c': scipy.constants.c,
} 'T': 298.15,
'q': scipy.constants.e
}
# We eliminated the following extreme suffixes: # We eliminated the following extreme suffixes:
# P (1e15), E (1e18), Z (1e21), Y (1e24), # P (1e15), E (1e18), Z (1e21), Y (1e24),
...@@ -72,56 +73,76 @@ DEFAULT_VARIABLES = {'i': numpy.complex(0, 1), ...@@ -72,56 +73,76 @@ DEFAULT_VARIABLES = {'i': numpy.complex(0, 1),
# since they're rarely used, and potentially # 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, 'T': 1e12, SUFFIXES = {
'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12} '%': 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): class UndefinedVariable(Exception):
""" """
Used to indicate the student input of a variable, which was unused by the Indicate the student input of a variable which was unused by the instructor
instructor.
""" """
pass pass
def check_variables(string, variables): def find_vars_funcs(tree):
""" """
Confirm the only variables in string are defined. Aggregate a list of the variables and functions used in `tree`
Otherwise, raise an UndefinedVariable containing all bad variables. variables and functions are nodes identified by `branch.getName()`.
As the tips of the branches are strings, avoid any possible AttributeErrors
by looking just at the nodes which are `ParseResults`.
"""
variables = set()
functions = set()
def check_branch(branch):
"""
Add variables and functions to their respective sets, using recursion.
"""
if isinstance(branch, ParseResults):
if branch.getName() == "variable":
variables.add(branch[0])
elif branch.getName() == "function":
functions.add(branch[0])
for sub_branch in branch:
check_branch(sub_branch)
check_branch(tree)
return (variables, functions)
def check_variables(tree, all_variables, all_functions, case_sensitive):
"""
Confirm the only variables in the tree are defined.
Pyparsing uses a left-to-right parser, which makes a more Otherwise, raise an UndefinedVariable containing all bad variables.
elegant approach pretty hopeless.
""" """
general_whitespace = re.compile('[^\\w]+') # TODO consider non-ascii used_vars, used_funcs = find_vars_funcs(tree)
# List of all alnums in string if not case_sensitive:
possible_variables = re.split(general_whitespace, string) used_vars = set((k.lower() for k in used_vars))
bad_variables = [] used_funcs = set((k.lower() for k in used_funcs))
for var in possible_variables:
if len(var) == 0: # Test that `used_vars` is a subset of `all_vars` and the same for functions
continue if not (used_vars <= all_variables and
if var[0].isdigit(): # Skip things that begin with numbers used_funcs <= all_functions):
continue bad_vars = (used_vars - all_variables) & (used_funcs - all_functions)
if var not in variables: raise UndefinedVariable(' '.join(bad_vars))
bad_variables.append(var)
if len(bad_variables) > 0:
raise UndefinedVariable(' '.join(bad_variables))
def lower_dict(input_dict): def lower_dict(input_dict):
""" """
takes each key in the dict and makes it lowercase, still mapping to the Convert all keys in a dictionary to lowercase; keep their original values
same value.
keep in mind that it is possible (but not useful?) to define different 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 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 {k.lower(): v for k, v in input_dict.iteritems()} return {k.lower(): v for k, v in input_dict.iteritems()}
# The following few functions define parse actions, which are run on lists of # The following few functions define evaluation actions, which are run on lists
# results from each parse component. They convert the strings and (previously # of results from each parse component. They convert the strings and (previously
# calculated) numbers into the number that component represents. # calculated) numbers into the number that component represents.
def super_float(text): def super_float(text):
...@@ -134,50 +155,61 @@ def super_float(text): ...@@ -134,50 +155,61 @@ def super_float(text):
return float(text) return float(text)
def number_parse_action(parse_result): def eval_number(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(parse_result)) return super_float("".join(parse_result))
def exp_parse_action(parse_result): def eval_atom(parse_result):
"""
Return the value wrapped by the atom.
In the case of parenthesis, ignore them.
"""
float_children = [k for k in parse_result if isinstance(k, numbers.Number)]
return float_children[0]
def eval_power(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
""" """
# pyparsing.ParseResults doesn't play well with reverse() parse_result = reversed(
parse_result = reversed(parse_result) [k for k in parse_result
# the result of an exponentiation is called a power if isinstance(k, numbers.Number)]
)
# The result of an exponentiation is called a power
power = reduce(lambda a, b: b ** a, parse_result) power = reduce(lambda a, b: b ** a, parse_result)
return power return power
def parallel(parse_result): def eval_parallel(parse_result):
""" """
Compute numbers according to the parallel resistors operator Compute numbers according to the parallel resistors operator
BTW it is commutative. Its formula is given by BTW it is commutative. Its formula is given by
out = 1 / (1/in1 + 1/in2 + ...) out = 1 / (1/in1 + 1/in2 + ...)
e.g. [ 1, 2 ] => 2/3 e.g. [ 1, 2 ] -> 2/3
Return NaN if there is a zero among the inputs Return NaN if there is a zero among the inputs
""" """
# convert from pyparsing.ParseResults, which doesn't support '0 in parse_result'
parse_result = parse_result.asList()
if len(parse_result) == 1: if len(parse_result) == 1:
return parse_result[0] return parse_result[0]
if 0 in parse_result: if 0 in parse_result:
return float('nan') return float('nan')
reciprocals = [1. / e for e in parse_result] reciprocals = [1. / e for e in parse_result
if isinstance(e, numbers.Number)]
return 1. / sum(reciprocals) return 1. / sum(reciprocals)
def sum_parse_action(parse_result): def eval_sum(parse_result):
""" """
Add the inputs Add the inputs
...@@ -197,11 +229,11 @@ def sum_parse_action(parse_result): ...@@ -197,11 +229,11 @@ def sum_parse_action(parse_result):
return total return total
def prod_parse_action(parse_result): def eval_product(parse_result):
""" """
Multiply the inputs Multiply the inputs
[ 1, '*', 2, '/', 3 ] => 0.66 [ 1, '*', 2, '/', 3 ] -> 0.66
""" """
prod = 1.0 prod = 1.0
current_op = operator.mul current_op = operator.mul
...@@ -215,87 +247,132 @@ def prod_parse_action(parse_result): ...@@ -215,87 +247,132 @@ def prod_parse_action(parse_result):
return prod return prod
def evaluator(variables, functions, string, cs=False): def parse_algebra(string):
""" """
Evaluate an expression. Variables are passed as a dictionary Parse an algebraic expression into a tree.
from string to value. Unary functions are passed as a dictionary
from string to function. Variables must be floats.
cs: Case sensitive
""" Return a `pyparsing.ParseResult` with proper groupings to reflect
parenthesis and order of operations. Leave all operators in the tree and do
all_variables = copy.copy(DEFAULT_VARIABLES) not parse any strings of numbers into their float versions.
all_functions = copy.copy(DEFAULT_FUNCTIONS)
all_variables.update(variables)
all_functions.update(functions)
if not cs:
string_cs = string.lower()
all_functions = lower_dict(all_functions)
all_variables = lower_dict(all_variables)
CasedLiteral = CaselessLiteral
else:
string_cs = string
CasedLiteral = Literal
check_variables(string_cs, set(all_variables.keys() + all_functions.keys()))
if string.strip() == "":
return float('nan')
# SI suffixes and percent
number_suffix = MatchFirst([Literal(k) for k in SUFFIXES.keys()])
plus_minus = Literal('+') | Literal('-')
times_div = Literal('*') | Literal('/')
number_part = Word(nums)
Adding the groups and result names makes the string representation of the
result really gross. For debugging, use something like
print parse_algebra("1+1/2").asXML()
"""
# 0.33 or 7 or .34 or 16. # 0.33 or 7 or .34 or 16.
number_part = Word(nums)
inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part) inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
# by default pyparsing allows spaces between tokens--Combine prevents that # By default pyparsing allows spaces between tokens--`Combine` prevents that
inner_number = Combine(inner_number) inner_number = Combine(inner_number)
# 0.33k or -17 # SI suffixes and percent
number = (inner_number number_suffix = MatchFirst((Literal(k) for k in SUFFIXES.keys()))
+ Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part)
+ Optional(number_suffix)) # 0.33k or 17
number.setParseAction(number_parse_action) # Convert to number plus_minus = Literal('+') | Literal('-')
number = Group(
inner_number +
Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) +
Optional(number_suffix)
)
number = number("number")
# Predefine recursive variables # Predefine recursive variables
expr = Forward() expr = Forward()
# Handle variables passed in. # Handle variables passed in. They must start with letters/underscores and
# E.g. if we have {'R':0.5}, we make the substitution. # may contain numbers afterward
# We sort the list so that var names (like "e2") match before inner_varname = Word(alphas + "_", alphanums + "_")
# mathematical constants (like "e"). This is kind of a hack. varname = Group(inner_varname)("variable")
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. # Same thing for functions.
all_functions_keys = sorted(all_functions.keys(), key=len, reverse=True) function = Group(inner_varname + Suppress("(") + expr + Suppress(")"))("function")
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(")") atom = number | function | varname | "(" + expr + ")"
atom = Group(atom)("atom")
# Do the following in the correct order to preserve order of operation # Do the following in the correct order to preserve order of operation
pow_term = atom + ZeroOrMore(Suppress("^") + atom) pow_term = atom + ZeroOrMore("^" + atom)
pow_term.setParseAction(exp_parse_action) # 7^6 pow_term = Group(pow_term)("power")
par_term = pow_term + ZeroOrMore(Suppress('||') + pow_term) # 5k || 4k
par_term.setParseAction(parallel) par_term = pow_term + ZeroOrMore('||' + pow_term) # 5k || 4k
prod_term = par_term + ZeroOrMore(times_div + par_term) # 7 * 5 / 4 - 3 par_term = Group(par_term)("parallel")
prod_term.setParseAction(prod_parse_action)
prod_term = par_term + ZeroOrMore((Literal('*') | Literal('/')) + par_term) # 7 * 5 / 4
prod_term = Group(prod_term)("product")
sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3 sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3
sum_term.setParseAction(sum_parse_action) sum_term = Group(sum_term)("sum")
expr << sum_term # finish the recursion
# Finish the recursion
expr << sum_term # pylint: disable=W0104
return (expr + stringEnd).parseString(string)[0] return (expr + stringEnd).parseString(string)[0]
def add_defaults(variables, functions, case_sensitive):
"""
Create dictionaries with both the default and user-defined variables.
"""
all_variables = copy.copy(DEFAULT_VARIABLES)
all_functions = copy.copy(DEFAULT_FUNCTIONS)
all_variables.update(variables)
all_functions.update(functions)
if not case_sensitive:
all_variables = lower_dict(all_variables)
all_functions = lower_dict(all_functions)
return (all_variables, all_functions)
def evaluator(variables, functions, string, cs=False):
"""
Evaluate an expression; that is, take a string of math and return a float
-Variables are passed as a dictionary from string to value. They must be
python numbers
-Unary functions are passed as a dictionary from string to function.
-cs: Case sensitive
"""
# No need to go further
if string.strip() == "":
return float('nan')
# Parse tree
tree = parse_algebra(string)
# Get our variables together
all_variables, all_functions = add_defaults(variables, functions, cs)
# ...and check them
check_variables(tree, set(all_variables), set(all_functions), cs)
# Create a recursion to evaluate the tree
casify = lambda x: x if cs else x.lower() # Lowercase for case insens.
evaluate_action = {
'number': eval_number,
'variable': lambda x: all_variables[casify(x[0])],
'function': lambda x: all_functions[casify(x[0])](x[1]),
'atom': eval_atom,
'power': eval_power,
'parallel': eval_parallel,
'product': eval_product,
'sum': eval_sum
}
def evaluate_branch(branch):
"""
Return the float representing the branch, using recursion.
Call the appropriate `evaluate_action` for this branch. As its inputs,
feed it the output of `evaluate_branch` for each child branch.
"""
if not isinstance(branch, ParseResults):
return branch
action = evaluate_action[branch.getName()]
evaluated_kids = [evaluate_branch(k) for k in branch]
return action(evaluated_kids)
# Find the value of the entire tree
return evaluate_branch(tree)
...@@ -58,20 +58,24 @@ class EvaluatorTest(unittest.TestCase): ...@@ -58,20 +58,24 @@ class EvaluatorTest(unittest.TestCase):
Test for correct interpretation of scientific notation Test for correct interpretation of scientific notation
""" """
answer = 50 answer = 50
correct_responses = ["50", "50.0", "5e1", "5e+1", correct_responses = [
"50e0", "50.0e0", "500e-1"] "50", "50.0", "5e1", "5e+1",
"50e0", "50.0e0", "500e-1"
]
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"] incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
for input_str in correct_responses: for input_str in correct_responses:
result = calc.evaluator({}, {}, input_str) result = calc.evaluator({}, {}, input_str)
fail_msg = "Expected '{0}' to equal {1}".format( fail_msg = "Expected '{0}' to equal {1}".format(
input_str, answer) input_str, answer
)
self.assertEqual(answer, result, msg=fail_msg) self.assertEqual(answer, result, msg=fail_msg)
for input_str in incorrect_responses: for input_str in incorrect_responses:
result = calc.evaluator({}, {}, input_str) result = calc.evaluator({}, {}, input_str)
fail_msg = "Expected '{0}' to not equal {1}".format( fail_msg = "Expected '{0}' to not equal {1}".format(
input_str, answer) input_str, answer
)
self.assertNotEqual(answer, result, msg=fail_msg) self.assertNotEqual(answer, result, msg=fail_msg)
def test_si_suffix(self): def test_si_suffix(self):
...@@ -80,17 +84,21 @@ class EvaluatorTest(unittest.TestCase): ...@@ -80,17 +84,21 @@ class EvaluatorTest(unittest.TestCase):
For instance 'k' stand for 'kilo-' so '1k' should be 1,000 For instance 'k' stand for 'kilo-' so '1k' should be 1,000
""" """
test_mapping = [('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000), test_mapping = [
('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074), ('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000),
('5.4m', 0.0054), ('8.7u', 0.0000087), ('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074),
('5.6n', 5.6e-9), ('4.2p', 4.2e-12)] ('5.4m', 0.0054), ('8.7u', 0.0000087),
('5.6n', 5.6e-9), ('4.2p', 4.2e-12)
]
for (expr, answer) in test_mapping: for (expr, answer) in test_mapping:
tolerance = answer * 1e-6 # Make rel. tolerance, because of floats tolerance = answer * 1e-6 # Make rel. tolerance, because of floats
fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}" fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}"
fail_msg = fail_msg.format(expr[-1], expr, answer) fail_msg = fail_msg.format(expr[-1], expr, answer)
self.assertAlmostEqual(calc.evaluator({}, {}, expr), answer, self.assertAlmostEqual(
delta=tolerance, msg=fail_msg) calc.evaluator({}, {}, expr), answer,
delta=tolerance, msg=fail_msg
)
def test_operator_sanity(self): def test_operator_sanity(self):
""" """
...@@ -104,19 +112,26 @@ class EvaluatorTest(unittest.TestCase): ...@@ -104,19 +112,26 @@ class EvaluatorTest(unittest.TestCase):
input_str = "{0} {1} {2}".format(var1, operator, var2) input_str = "{0} {1} {2}".format(var1, operator, var2)
result = calc.evaluator({}, {}, input_str) result = calc.evaluator({}, {}, input_str)
fail_msg = "Failed on operator '{0}': '{1}' was not {2}".format( fail_msg = "Failed on operator '{0}': '{1}' was not {2}".format(
operator, input_str, answer) operator, input_str, answer
)
self.assertEqual(answer, result, msg=fail_msg) self.assertEqual(answer, result, msg=fail_msg)
def test_raises_zero_division_err(self): def test_raises_zero_division_err(self):
""" """
Ensure division by zero gives an error Ensure division by zero gives an error
""" """
self.assertRaises(ZeroDivisionError, calc.evaluator, self.assertRaises(
{}, {}, '1/0') ZeroDivisionError, calc.evaluator,
self.assertRaises(ZeroDivisionError, calc.evaluator, {}, {}, '1/0'
{}, {}, '1/0.0') )
self.assertRaises(ZeroDivisionError, calc.evaluator, self.assertRaises(
{'x': 0.0}, {}, '1/x') ZeroDivisionError, calc.evaluator,
{}, {}, '1/0.0'
)
self.assertRaises(
ZeroDivisionError, calc.evaluator,
{'x': 0.0}, {}, '1/x'
)
def test_parallel_resistors(self): def test_parallel_resistors(self):
""" """
...@@ -153,7 +168,8 @@ class EvaluatorTest(unittest.TestCase): ...@@ -153,7 +168,8 @@ class EvaluatorTest(unittest.TestCase):
input_str = "{0}({1})".format(fname, arg) input_str = "{0}({1})".format(fname, arg)
result = calc.evaluator({}, {}, input_str) result = calc.evaluator({}, {}, input_str)
fail_msg = "Failed on function {0}: '{1}' was not {2}".format( fail_msg = "Failed on function {0}: '{1}' was not {2}".format(
fname, input_str, val) fname, input_str, val
)
self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg) self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg)
def test_trig_functions(self): def test_trig_functions(self):
...@@ -303,21 +319,29 @@ class EvaluatorTest(unittest.TestCase): ...@@ -303,21 +319,29 @@ class EvaluatorTest(unittest.TestCase):
""" """
# Test sqrt # Test sqrt
self.assert_function_values('sqrt', self.assert_function_values(
[0, 1, 2, 1024], # -1 'sqrt',
[0, 1, 1.414, 32]) # 1j [0, 1, 2, 1024], # -1
[0, 1, 1.414, 32] # 1j
)
# sqrt(-1) is NAN not j (!!). # sqrt(-1) is NAN not j (!!).
# Test logs # Test logs
self.assert_function_values('log10', self.assert_function_values(
[0.1, 1, 3.162, 1000000, '1+j'], 'log10',
[-1, 0, 0.5, 6, 0.151 + 0.341j]) [0.1, 1, 3.162, 1000000, '1+j'],
self.assert_function_values('log2', [-1, 0, 0.5, 6, 0.151 + 0.341j]
[0.5, 1, 1.414, 1024, '1+j'], )
[-1, 0, 0.5, 10, 0.5 + 1.133j]) self.assert_function_values(
self.assert_function_values('ln', 'log2',
[0.368, 1, 1.649, 2.718, 42, '1+j'], [0.5, 1, 1.414, 1024, '1+j'],
[-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j]) [-1, 0, 0.5, 10, 0.5 + 1.133j]
)
self.assert_function_values(
'ln',
[0.368, 1, 1.649, 2.718, 42, '1+j'],
[-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j]
)
# Test abs # Test abs
self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1]) self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1])
...@@ -341,20 +365,23 @@ class EvaluatorTest(unittest.TestCase): ...@@ -341,20 +365,23 @@ class EvaluatorTest(unittest.TestCase):
""" """
# Of the form ('expr', python value, tolerance (or None for exact)) # Of the form ('expr', python value, tolerance (or None for exact))
default_variables = [('j', 1j, None), default_variables = [
('e', 2.7183, 1e-3), ('j', 1j, None),
('pi', 3.1416, 1e-3), ('e', 2.7183, 1e-3),
# c = speed of light ('pi', 3.1416, 1e-3),
('c', 2.998e8, 1e5), # c = speed of light
# 0 deg C = T Kelvin ('c', 2.998e8, 1e5),
('T', 298.15, 0.01), # 0 deg C = T Kelvin
# Note k = scipy.constants.k = 1.3806488e-23 ('T', 298.15, 0.01),
('k', 1.3806488e-23, 1e-26), # Note k = scipy.constants.k = 1.3806488e-23
# Note q = scipy.constants.e = 1.602176565e-19 ('k', 1.3806488e-23, 1e-26),
('q', 1.602176565e-19, 1e-22)] # Note q = scipy.constants.e = 1.602176565e-19
('q', 1.602176565e-19, 1e-22)
]
for (variable, value, tolerance) in default_variables: for (variable, value, tolerance) in default_variables:
fail_msg = "Failed on constant '{0}', not within bounds".format( fail_msg = "Failed on constant '{0}', not within bounds".format(
variable) variable
)
result = calc.evaluator({}, {}, variable) result = calc.evaluator({}, {}, variable)
if tolerance is None: if tolerance is None:
self.assertEqual(value, result, msg=fail_msg) self.assertEqual(value, result, msg=fail_msg)
...@@ -370,21 +397,29 @@ class EvaluatorTest(unittest.TestCase): ...@@ -370,21 +397,29 @@ class EvaluatorTest(unittest.TestCase):
self.assertAlmostEqual( self.assertAlmostEqual(
calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"), calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"),
10.180, 10.180,
delta=1e-3) delta=1e-3
)
self.assertAlmostEqual( self.assertAlmostEqual(
calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"), calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"),
1.6, 1.6,
delta=1e-3) delta=1e-3
)
self.assertAlmostEqual( self.assertAlmostEqual(
calc.evaluator({}, {}, "10||sin(7+5)"), calc.evaluator({}, {}, "10||sin(7+5)"),
-0.567, delta=0.01) -0.567, delta=0.01
self.assertAlmostEqual(calc.evaluator({}, {}, "sin(e)"), )
0.41, delta=0.01) self.assertAlmostEqual(
self.assertAlmostEqual(calc.evaluator({}, {}, "k*T/q"), calc.evaluator({}, {}, "sin(e)"),
0.025, delta=1e-3) 0.41, delta=0.01
self.assertAlmostEqual(calc.evaluator({}, {}, "e^(j*pi)"), )
-1, delta=1e-5) self.assertAlmostEqual(
calc.evaluator({}, {}, "k*T/q"),
0.025, delta=1e-3
)
self.assertAlmostEqual(
calc.evaluator({}, {}, "e^(j*pi)"),
-1, delta=1e-5
)
def test_simple_vars(self): def test_simple_vars(self):
""" """
...@@ -404,19 +439,24 @@ class EvaluatorTest(unittest.TestCase): ...@@ -404,19 +439,24 @@ class EvaluatorTest(unittest.TestCase):
self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4) self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4)
# Test a simple equation # Test a simple equation
self.assertAlmostEqual(calc.evaluator(variables, {}, '3*x-y'), self.assertAlmostEqual(
21.25, delta=0.01) # = 3 * 9.72 - 7.91 calc.evaluator(variables, {}, '3*x-y'),
self.assertAlmostEqual(calc.evaluator(variables, {}, 'x*y'), 21.25, delta=0.01 # = 3 * 9.72 - 7.91
76.89, delta=0.01) )
self.assertAlmostEqual(
calc.evaluator(variables, {}, 'x*y'),
76.89, delta=0.01
)
self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13) self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13)
self.assertEqual(calc.evaluator(variables, {}, "13"), 13) self.assertEqual(calc.evaluator(variables, {}, "13"), 13)
self.assertEqual( self.assertEqual(
calc.evaluator({ calc.evaluator(
'a': 2.2997471478310274, 'k': 9, 'm': 8, {'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.6600949841121},
'x': 0.66009498411213041}, {}, "5"
{}, "5"), ),
5) 5
)
def test_variable_case_sensitivity(self): def test_variable_case_sensitivity(self):
""" """
...@@ -424,7 +464,8 @@ class EvaluatorTest(unittest.TestCase): ...@@ -424,7 +464,8 @@ class EvaluatorTest(unittest.TestCase):
""" """
self.assertEqual( self.assertEqual(
calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"), calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"),
8.0) 8.0
)
variables = {'t': 1.0} variables = {'t': 1.0}
self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0) self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0)
...@@ -445,15 +486,17 @@ class EvaluatorTest(unittest.TestCase): ...@@ -445,15 +486,17 @@ class EvaluatorTest(unittest.TestCase):
self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712) self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712)
functions.update({'f': numpy.sin}) functions.update({'f': numpy.sin})
self.assertAlmostEqual(calc.evaluator(variables, functions, 'f(x)'), self.assertAlmostEqual(
-1, delta=1e-3) calc.evaluator(variables, functions, 'f(x)'),
-1, delta=1e-3
)
def test_function_case_sensitivity(self): def test_function_case_sensitivity(self):
""" """
Test the case sensitivity of functions Test the case sensitivity of functions
""" """
functions = {'f': lambda x: x, functions = {'f': lambda x: x, 'F': lambda x: x + 1}
'F': lambda x: x + 1}
# Test case insensitive evaluation # Test case insensitive evaluation
# Both evaulations should call the same function # Both evaulations should call the same function
self.assertEqual(calc.evaluator({}, functions, 'f(6)'), self.assertEqual(calc.evaluator({}, functions, 'f(6)'),
...@@ -468,9 +511,15 @@ class EvaluatorTest(unittest.TestCase): ...@@ -468,9 +511,15 @@ class EvaluatorTest(unittest.TestCase):
""" """
variables = {'R1': 2.0, 'R3': 4.0} variables = {'R1': 2.0, 'R3': 4.0}
self.assertRaises(calc.UndefinedVariable, calc.evaluator, self.assertRaises(
{}, {}, "5+7 QWSEKO") calc.UndefinedVariable, calc.evaluator,
self.assertRaises(calc.UndefinedVariable, calc.evaluator, {}, {}, "5+7*QWSEKO"
{'r1': 5}, {}, "r1+r2") )
self.assertRaises(calc.UndefinedVariable, calc.evaluator, self.assertRaises(
variables, {}, "r1*r3", cs=True) calc.UndefinedVariable, calc.evaluator,
{'r1': 5}, {}, "r1+r2"
)
self.assertRaises(
calc.UndefinedVariable, calc.evaluator,
variables, {}, "r1*r3", cs=True
)
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