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().
import copy
import math
import operator
import re
import numbers
import numpy
import scipy.constants
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
numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise'
from pyparsing import (Word, nums, Literal,
ZeroOrMore, MatchFirst,
Optional, Forward,
CaselessLiteral,
stringEnd, Suppress, Combine)
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,
'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 = {'i': numpy.complex(0, 1),
'j': numpy.complex(0, 1),
'e': numpy.e,
'pi': numpy.pi,
'k': scipy.constants.k,
'c': scipy.constants.c,
'T': 298.15,
'q': scipy.constants.e
}
CaselessLiteral, Group, ParseResults,
stringEnd, Suppress, Combine, alphas, alphanums)
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,
'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 = {
'i': numpy.complex(0, 1),
'j': numpy.complex(0, 1),
'e': numpy.e,
'pi': numpy.pi,
'k': scipy.constants.k,
'c': scipy.constants.c,
'T': 298.15,
'q': scipy.constants.e
}
# We eliminated the following extreme suffixes:
# P (1e15), E (1e18), Z (1e21), Y (1e24),
......@@ -72,56 +73,76 @@ DEFAULT_VARIABLES = {'i': numpy.complex(0, 1),
# 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,
'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12}
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):
"""
Used to indicate the student input of a variable, which was unused by the
instructor.
Indicate the student input of a variable which was unused by the instructor
"""
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
elegant approach pretty hopeless.
Otherwise, raise an UndefinedVariable containing all bad variables.
"""
general_whitespace = re.compile('[^\\w]+') # TODO consider non-ascii
# List of all alnums in string
possible_variables = re.split(general_whitespace, string)
bad_variables = []
for var in possible_variables:
if len(var) == 0:
continue
if var[0].isdigit(): # Skip things that begin with numbers
continue
if var not in variables:
bad_variables.append(var)
if len(bad_variables) > 0:
raise UndefinedVariable(' '.join(bad_variables))
used_vars, used_funcs = find_vars_funcs(tree)
if not case_sensitive:
used_vars = set((k.lower() for k in used_vars))
used_funcs = set((k.lower() for k in used_funcs))
# Test that `used_vars` is a subset of `all_vars` and the same for functions
if not (used_vars <= all_variables and
used_funcs <= all_functions):
bad_vars = (used_vars - all_variables) & (used_funcs - all_functions)
raise UndefinedVariable(' '.join(bad_vars))
def lower_dict(input_dict):
"""
takes each key in the dict and makes it lowercase, still mapping to the
same value.
Convert all keys in a dictionary to lowercase; keep their original values
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
tell which is used in the final dict and which isn't.
"""
return {k.lower(): v for k, v in input_dict.iteritems()}
# The following few functions define parse actions, which are run on lists of
# results from each parse component. They convert the strings and (previously
# The following few functions define evaluation 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):
......@@ -134,50 +155,61 @@ def super_float(text):
return float(text)
def number_parse_action(parse_result):
def eval_number(parse_result):
"""
Create a float out of its string parts
e.g. [ '7', '.', '13' ] -> [ 7.13 ]
e.g. [ '7', '.', '13' ] -> 7.13
Calls super_float above
"""
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
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)
# the result of an exponentiation is called a power
parse_result = reversed(
[k for k in parse_result
if isinstance(k, numbers.Number)]
)
# The result of an exponentiation is called a power
power = reduce(lambda a, b: b ** a, parse_result)
return power
def parallel(parse_result):
def eval_parallel(parse_result):
"""
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
e.g. [ 1, 2 ] -> 2/3
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:
return parse_result[0]
if 0 in parse_result:
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)
def sum_parse_action(parse_result):
def eval_sum(parse_result):
"""
Add the inputs
......@@ -197,11 +229,11 @@ def sum_parse_action(parse_result):
return total
def prod_parse_action(parse_result):
def eval_product(parse_result):
"""
Multiply the inputs
[ 1, '*', 2, '/', 3 ] => 0.66
[ 1, '*', 2, '/', 3 ] -> 0.66
"""
prod = 1.0
current_op = operator.mul
......@@ -215,87 +247,132 @@ def prod_parse_action(parse_result):
return prod
def evaluator(variables, functions, string, cs=False):
def parse_algebra(string):
"""
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
Parse an algebraic expression into a tree.
"""
all_variables = copy.copy(DEFAULT_VARIABLES)
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)
Return a `pyparsing.ParseResult` with proper groupings to reflect
parenthesis and order of operations. Leave all operators in the tree and do
not parse any strings of numbers into their float versions.
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.
number_part = Word(nums)
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)
# 0.33k or -17
number = (inner_number
+ Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part)
+ Optional(number_suffix))
number.setParseAction(number_parse_action) # Convert to number
# SI suffixes and percent
number_suffix = MatchFirst((Literal(k) for k in SUFFIXES.keys()))
# 0.33k or 17
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
expr = Forward()
# 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
# Handle variables passed in. They must start with letters/underscores and
# may contain numbers afterward
inner_varname = Word(alphas + "_", alphanums + "_")
varname = Group(inner_varname)("variable")
# Same thing for functions.
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])]
)
function = Group(inner_varname + Suppress("(") + expr + Suppress(")"))("function")
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
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)
pow_term = atom + ZeroOrMore("^" + atom)
pow_term = Group(pow_term)("power")
par_term = pow_term + ZeroOrMore('||' + pow_term) # 5k || 4k
par_term = Group(par_term)("parallel")
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.setParseAction(sum_parse_action)
expr << sum_term # finish the recursion
sum_term = Group(sum_term)("sum")
# Finish the recursion
expr << sum_term # pylint: disable=W0104
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):
Test for correct interpretation of scientific notation
"""
answer = 50
correct_responses = ["50", "50.0", "5e1", "5e+1",
"50e0", "50.0e0", "500e-1"]
correct_responses = [
"50", "50.0", "5e1", "5e+1",
"50e0", "50.0e0", "500e-1"
]
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
for input_str in correct_responses:
result = calc.evaluator({}, {}, input_str)
fail_msg = "Expected '{0}' to equal {1}".format(
input_str, answer)
input_str, answer
)
self.assertEqual(answer, result, msg=fail_msg)
for input_str in incorrect_responses:
result = calc.evaluator({}, {}, input_str)
fail_msg = "Expected '{0}' to not equal {1}".format(
input_str, answer)
input_str, answer
)
self.assertNotEqual(answer, result, msg=fail_msg)
def test_si_suffix(self):
......@@ -80,17 +84,21 @@ class EvaluatorTest(unittest.TestCase):
For instance 'k' stand for 'kilo-' so '1k' should be 1,000
"""
test_mapping = [('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000),
('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074),
('5.4m', 0.0054), ('8.7u', 0.0000087),
('5.6n', 5.6e-9), ('4.2p', 4.2e-12)]
test_mapping = [
('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000),
('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074),
('5.4m', 0.0054), ('8.7u', 0.0000087),
('5.6n', 5.6e-9), ('4.2p', 4.2e-12)
]
for (expr, answer) in test_mapping:
tolerance = answer * 1e-6 # Make rel. tolerance, because of floats
fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}"
fail_msg = fail_msg.format(expr[-1], expr, answer)
self.assertAlmostEqual(calc.evaluator({}, {}, expr), answer,
delta=tolerance, msg=fail_msg)
self.assertAlmostEqual(
calc.evaluator({}, {}, expr), answer,
delta=tolerance, msg=fail_msg
)
def test_operator_sanity(self):
"""
......@@ -104,19 +112,26 @@ class EvaluatorTest(unittest.TestCase):
input_str = "{0} {1} {2}".format(var1, operator, var2)
result = calc.evaluator({}, {}, input_str)
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)
def test_raises_zero_division_err(self):
"""
Ensure division by zero gives an error
"""
self.assertRaises(ZeroDivisionError, calc.evaluator,
{}, {}, '1/0')
self.assertRaises(ZeroDivisionError, calc.evaluator,
{}, {}, '1/0.0')
self.assertRaises(ZeroDivisionError, calc.evaluator,
{'x': 0.0}, {}, '1/x')
self.assertRaises(
ZeroDivisionError, calc.evaluator,
{}, {}, '1/0'
)
self.assertRaises(
ZeroDivisionError, calc.evaluator,
{}, {}, '1/0.0'
)
self.assertRaises(
ZeroDivisionError, calc.evaluator,
{'x': 0.0}, {}, '1/x'
)
def test_parallel_resistors(self):
"""
......@@ -153,7 +168,8 @@ class EvaluatorTest(unittest.TestCase):
input_str = "{0}({1})".format(fname, arg)
result = calc.evaluator({}, {}, input_str)
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)
def test_trig_functions(self):
......@@ -303,21 +319,29 @@ class EvaluatorTest(unittest.TestCase):
"""
# Test sqrt
self.assert_function_values('sqrt',
[0, 1, 2, 1024], # -1
[0, 1, 1.414, 32]) # 1j
self.assert_function_values(
'sqrt',
[0, 1, 2, 1024], # -1
[0, 1, 1.414, 32] # 1j
)
# sqrt(-1) is NAN not j (!!).
# Test logs
self.assert_function_values('log10',
[0.1, 1, 3.162, 1000000, '1+j'],
[-1, 0, 0.5, 6, 0.151 + 0.341j])
self.assert_function_values('log2',
[0.5, 1, 1.414, 1024, '1+j'],
[-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])
self.assert_function_values(
'log10',
[0.1, 1, 3.162, 1000000, '1+j'],
[-1, 0, 0.5, 6, 0.151 + 0.341j]
)
self.assert_function_values(
'log2',
[0.5, 1, 1.414, 1024, '1+j'],
[-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
self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1])
......@@ -341,20 +365,23 @@ class EvaluatorTest(unittest.TestCase):
"""
# Of the form ('expr', python value, tolerance (or None for exact))
default_variables = [('j', 1j, None),
('e', 2.7183, 1e-3),
('pi', 3.1416, 1e-3),
# c = speed of light
('c', 2.998e8, 1e5),
# 0 deg C = T Kelvin
('T', 298.15, 0.01),
# Note k = scipy.constants.k = 1.3806488e-23
('k', 1.3806488e-23, 1e-26),
# Note q = scipy.constants.e = 1.602176565e-19
('q', 1.602176565e-19, 1e-22)]
default_variables = [
('j', 1j, None),
('e', 2.7183, 1e-3),
('pi', 3.1416, 1e-3),
# c = speed of light
('c', 2.998e8, 1e5),
# 0 deg C = T Kelvin
('T', 298.15, 0.01),
# Note k = scipy.constants.k = 1.3806488e-23
('k', 1.3806488e-23, 1e-26),
# Note q = scipy.constants.e = 1.602176565e-19
('q', 1.602176565e-19, 1e-22)
]
for (variable, value, tolerance) in default_variables:
fail_msg = "Failed on constant '{0}', not within bounds".format(
variable)
variable
)
result = calc.evaluator({}, {}, variable)
if tolerance is None:
self.assertEqual(value, result, msg=fail_msg)
......@@ -370,21 +397,29 @@ class EvaluatorTest(unittest.TestCase):
self.assertAlmostEqual(
calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"),
10.180,
delta=1e-3)
delta=1e-3
)
self.assertAlmostEqual(
calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"),
1.6,
delta=1e-3)
delta=1e-3
)
self.assertAlmostEqual(
calc.evaluator({}, {}, "10||sin(7+5)"),
-0.567, delta=0.01)
self.assertAlmostEqual(calc.evaluator({}, {}, "sin(e)"),
0.41, delta=0.01)
self.assertAlmostEqual(calc.evaluator({}, {}, "k*T/q"),
0.025, delta=1e-3)
self.assertAlmostEqual(calc.evaluator({}, {}, "e^(j*pi)"),
-1, delta=1e-5)
-0.567, delta=0.01
)
self.assertAlmostEqual(
calc.evaluator({}, {}, "sin(e)"),
0.41, delta=0.01
)
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):
"""
......@@ -404,19 +439,24 @@ class EvaluatorTest(unittest.TestCase):
self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4)
# Test a simple equation
self.assertAlmostEqual(calc.evaluator(variables, {}, '3*x-y'),
21.25, delta=0.01) # = 3 * 9.72 - 7.91
self.assertAlmostEqual(calc.evaluator(variables, {}, 'x*y'),
76.89, delta=0.01)
self.assertAlmostEqual(
calc.evaluator(variables, {}, '3*x-y'),
21.25, delta=0.01 # = 3 * 9.72 - 7.91
)
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(variables, {}, "13"), 13)
self.assertEqual(
calc.evaluator({
'a': 2.2997471478310274, 'k': 9, 'm': 8,
'x': 0.66009498411213041},
{}, "5"),
5)
calc.evaluator(
{'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.6600949841121},
{}, "5"
),
5
)
def test_variable_case_sensitivity(self):
"""
......@@ -424,7 +464,8 @@ class EvaluatorTest(unittest.TestCase):
"""
self.assertEqual(
calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"),
8.0)
8.0
)
variables = {'t': 1.0}
self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0)
......@@ -445,15 +486,17 @@ class EvaluatorTest(unittest.TestCase):
self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712)
functions.update({'f': numpy.sin})
self.assertAlmostEqual(calc.evaluator(variables, functions, 'f(x)'),
-1, delta=1e-3)
self.assertAlmostEqual(
calc.evaluator(variables, functions, 'f(x)'),
-1, delta=1e-3
)
def test_function_case_sensitivity(self):
"""
Test the case sensitivity of functions
"""
functions = {'f': lambda x: x,
'F': lambda x: x + 1}
functions = {'f': lambda x: x, 'F': lambda x: x + 1}
# Test case insensitive evaluation
# Both evaulations should call the same function
self.assertEqual(calc.evaluator({}, functions, 'f(6)'),
......@@ -468,9 +511,15 @@ class EvaluatorTest(unittest.TestCase):
"""
variables = {'R1': 2.0, 'R3': 4.0}
self.assertRaises(calc.UndefinedVariable, calc.evaluator,
{}, {}, "5+7 QWSEKO")
self.assertRaises(calc.UndefinedVariable, calc.evaluator,
{'r1': 5}, {}, "r1+r2")
self.assertRaises(calc.UndefinedVariable, calc.evaluator,
variables, {}, "r1*r3", cs=True)
self.assertRaises(
calc.UndefinedVariable, calc.evaluator,
{}, {}, "5+7*QWSEKO"
)
self.assertRaises(
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