diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py new file mode 100644 index 0000000000..87b68e36be --- /dev/null +++ b/common/lib/calc/tests/test_calc.py @@ -0,0 +1,266 @@ +""" +unittests for calc.py + +Run like this: + + rake test_common/lib/calc ?? + +""" + +import unittest +import numpy +import calc + + +class EvaluatorTest(unittest.TestCase): + ''' Run tests for calc.evaluator + Go through all functionalities as specifically as possible-- + work from number input to functions and complex expressions + Also test custom variable substitutions (i.e. + `evaluator({'x':3.0},{}, '3*x')` + gives 9.0) and more. + ''' + + def test_number_input(self): + ''' Test different kinds of float inputs ''' + easy_eval = lambda x: calc.evaluator({}, {}, x) + + self.assertEqual(easy_eval("13"), 13) + self.assertEqual(easy_eval("3.14"), 3.14) + self.assertEqual(easy_eval(".618033989"), 0.618033989) + + self.assertEqual(easy_eval("-13"), -13) + self.assertEqual(easy_eval("-3.14"), -3.14) + self.assertEqual(easy_eval("-.618033989"), -0.618033989) + # see also test_exponential_answer + # and test_si_suffix + + def test_exponential_answer(self): + ''' Test for correct interpretation of scientific notation''' + answer = 50 + 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 = "Failed when checking '{0}' against {1} (expected equality)".format( + input_str, answer) + self.assertEqual(answer, result, msg=fail_msg) + + for input_str in incorrect_responses: + result = calc.evaluator({}, {}, input_str) + fail_msg = "Failed when checking '{0}' against {1} (expected inequality)".format( + input_str, answer) + self.assertNotEqual(answer, result, msg=fail_msg) + + def test_si_suffix(self): + ''' Test calc.py's unique functionality of interpreting si 'suffixes'. + 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)] + + for (expr, answer) in test_mapping: + tolerance = answer * 1e-6 # Testing exactly fails for the large values + fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}".format( + expr[-1], expr, answer) + self.assertAlmostEqual(calc.evaluator({}, {}, expr), answer, + delta=tolerance, msg=fail_msg) + + def test_operator_sanity(self): + ''' Test for simple things like "5+2" and "5/2"''' + var1 = 5.0 + var2 = 2.0 + operators = [('+', 7), ('-', 3), ('*', 10), ('/', 2.5), ('^', 25)] + + for (operator, answer) in operators: + 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) + self.assertEqual(answer, result, msg=fail_msg) + + self.assertRaises(ZeroDivisionError, calc.evaluator, + {}, {}, '1/0') + + def test_parallel_resistors(self): + ''' Test the special operator || + The formula is given by + a || b || c ... + = 1 / (1/a + 1/b + 1/c + ...) + It is the resistance of a parallel circuit of resistors with resistance + a, b, c, etc&. See if this evaulates correctly.''' + self.assertEqual(calc.evaluator({}, {}, '1||1'), 0.5) + self.assertEqual(calc.evaluator({}, {}, '1||1||2'), 0.4) + + # I don't know why you would want this, but it works. + self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j) + + self.assertRaises(ZeroDivisionError, calc.evaluator, + {}, {}, '0||1') + + + def assert_function_values(self, fname, ins, outs, tolerance=1e-3): + ''' Helper function to test many values at once + Test the accuracy of evaluator's use of the function given by fname + Specifically, the equality of `fname(ins[i])` against outs[i]. + Used later to test a whole bunch of f(x) = y at a time ''' + + for (arg, val) in zip(ins, outs): + 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) + self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg) + + def test_trig_functions(self): + """Test the trig functions provided in common/lib/calc/calc.py""" + # which are: sin, cos, tan, arccos, arcsin, arctan + + angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', 'j', '1 + j'] + sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.175j, 1.298 + 0.635j] + cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 1.543, 0.834 - 0.989j] + tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.762j, 0.272 + 1.084j] + # Cannot test tan(pi/2) b/c pi/2 is a float and not precise... + + self.assert_function_values('sin', angles, sin_values) + self.assert_function_values('cos', angles, cos_values) + self.assert_function_values('tan', angles, tan_values) + + # include those where the real part is between -pi/2 and pi/2 + arcsin_inputs = ['-0.707', '0', '0.5', '0.588', '1.298 + 0.635*j'] + arcsin_angles = [-0.785, 0, 0.524, 0.629, 1 + 1j] + self.assert_function_values('arcsin', arcsin_inputs, arcsin_angles) + # rather than throwing an exception, numpy.arcsin gives nan + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(-1.1)'))) + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(1.1)'))) + # disabled for now because they are giving a runtime warning... :-/ + + # include those where the real part is between 0 and pi + arccos_inputs = ['1', '0.866', '0.809', '0.834-0.989*j'] + arccos_angles = [0, 0.524, 0.628, 1 + 1j] + self.assert_function_values('arccos', arccos_inputs, arccos_angles) + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(-1.1)'))) + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(1.1)'))) + + # has the same range as arcsin + arctan_inputs = ['-1', '0', '0.577', '0.727', '0.272 + 1.084*j'] + arctan_angles = arcsin_angles + self.assert_function_values('arctan', arctan_inputs, arctan_angles) + + def test_other_functions(self): + """Test the other functions provided in common/lib/calc/calc.py""" + # sqrt, log10, log2, ln, abs, + # fact, factorial + + # test sqrt + 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]) + + # test abs + self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1]) + + # test factorial + fact_inputs = [0, 1, 3, 7] + fact_values = [1, 1, 6, 5040] + self.assert_function_values('fact', fact_inputs, fact_values) + self.assert_function_values('factorial', fact_inputs, fact_values) + + self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(-1)") + self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(0.5)") + self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(-1)") + self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(0.5)") + + def test_constants(self): + """Test the default constants provided in common/lib/calc/calc.py""" + # which are: j (complex number), e, pi, k, c, T, q + + # ('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)] + for (variable, value, tolerance) in default_variables: + fail_msg = "Failed on constant '{0}', not within bounds".format( + variable) + result = calc.evaluator({}, {}, variable) + if tolerance is None: + self.assertEqual(value, result, msg=fail_msg) + else: + self.assertAlmostEqual(value, result, + delta=tolerance, msg=fail_msg) + + def test_complex_expression(self): + """We've only tried simple things so far, make sure it can handle a + more complexity than this.""" + + self.assertAlmostEqual( + calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"), + 10.180, + delta=1e-3) + + self.assertAlmostEqual( + calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"), + 1.6, + delta=1e-3) + self.assertAlmostEqual( + calc.evaluator({}, {}, "10||sin(7+5)"), + -0.567, delta=0.01) + + def test_calc(self): + variables = {'R1': 2.0, 'R3': 4.0} + functions = {'sin': numpy.sin, 'cos': numpy.cos} + + self.assertAlmostEqual( + calc.evaluator(variables, functions, "10||sin(7+5)"), + -0.567, delta=0.01) + self.assertEqual(calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13"), 13) + self.assertEqual(calc.evaluator(variables, functions, "13"), 13) + self.assertEqual( + calc.evaluator({ + 'a': 2.2997471478310274, 'k': 9, 'm': 8, + 'x': 0.66009498411213041}, + {}, "5"), + 5) + self.assertEqual(calc.evaluator(variables, functions, "R1*R3"), 8.0) + self.assertAlmostEqual(calc.evaluator(variables, functions, "sin(e)"), 0.41, delta=0.01) + self.assertAlmostEqual(calc.evaluator(variables, functions, "k*T/q"), 0.025, delta=1e-3) + self.assertAlmostEqual(calc.evaluator(variables, functions, "e^(j*pi)"), -1, delta=1e-5) + + variables['t'] = 1.0 + self.assertAlmostEqual(calc.evaluator(variables, functions, "t"), 1.0, delta=1e-5) + self.assertAlmostEqual(calc.evaluator(variables, functions, "T"), 1.0, delta=1e-5) + self.assertAlmostEqual(calc.evaluator(variables, functions, "t", cs=True), 1.0, delta=1e-5) + self.assertAlmostEqual(calc.evaluator(variables, functions, "T", cs=True), 298, delta=0.2) + + self.assertRaises(calc.UndefinedVariable, calc.evaluator, + {}, {}, "5+7 QWSEKO") + + self.assertRaises(calc.UndefinedVariable, calc.evaluator, + {'r1': 5}, {}, "r1+r2") + + self.assertEqual(calc.evaluator(variables, functions, "r1*r3"), 8.0) + + self.assertRaises(calc.UndefinedVariable, calc.evaluator, + variables, functions, "r1*r3", cs=True)