def is_formula_equal(expected, given, samples, cs=True, tolerance=0.01): try: variables = samples.split('@')[0].split(',') sranges = zip(*map(lambda x: map(float, x.split(",")), samples.split('@')[1].split('#')[0].split(':'))) ranges = dict(zip(variables, sranges)) except Exception as err: raise Exception("is_formula_eq: failed to evaluate samples expression '%s', err=%s" % (samples, str(err))) try: numsamples = int(samples.split('@')[1].split('#')[1]) except Exception as err: raise Exception("is_formula_eq: failed to evaluate samples expression '%s', bad specification of number of samples, err=%s" % (samples, str(err))) if not len(variables)==len(sranges): raise Exception("is_formula_eq: bad samples expression '%s', # variables = %s, but # ranges = %s" % (samples, len(variables), len(sranges))) for i in range(numsamples): vvariables = {} for var in ranges: value = random.uniform(*ranges[var]) vvariables[str(var)] = value try: instructor_result = evaluator(vvariables, dict(), expected, case_sensitive=cs) except Exception as err: raise Exception("is_formula_eq: failed to evaluate expected instructor result, formula='%s', vvariables=%s, err=%s" % (expected, vvariables, str(err))) try: student_result = evaluator(vvariables, dict(), given, case_sensitive=cs) except Exception as err: raise Exception("is_formula_eq: failed to evaluate student result entry, formula='%s', vvariables=%s, err=%s" % (given, vvariables, str(err))) if abs(instructor_result-student_result) > tolerance: return False return True
def test_complex_expression(self): """ Calculate combinations of operators and default functions """ 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 ) self.assertAlmostEqual( calc.evaluator({}, {}, "sin(e)"), 0.41, delta=0.01 ) self.assertAlmostEqual( calc.evaluator({}, {}, "e^(j*pi)"), -1, delta=1e-5 )
def test_parallel_resistors_with_zero(self): """ Check the behavior of the || operator with 0 """ self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0||1'))) self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0.0||1'))) self.assertTrue(numpy.isnan(calc.evaluator({'x': 0.0}, {}, 'x||1')))
def test_period(self): """ The string '.' should not evaluate to anything. """ with self.assertRaises(ParseException): calc.evaluator({}, {}, '.') with self.assertRaises(ParseException): calc.evaluator({}, {}, '1+.')
def test_mismatched_parens(self): """ Check to see if the evaluator catches mismatched parens """ with self.assertRaisesRegexp(calc.UnmatchedParenthesis, 'opened but never closed'): calc.evaluator({}, {}, "(1+2") with self.assertRaisesRegexp(calc.UnmatchedParenthesis, 'no matching opening parenthesis'): calc.evaluator({}, {}, "(1+2))")
def compare_with_tolerance(student_complex, instructor_complex, tolerance=default_tolerance, relative_tolerance=False): """ Compare student_complex to instructor_complex with maximum tolerance tolerance. - student_complex : student result (float complex number) - instructor_complex : instructor result (float complex number) - tolerance : float, or string (representing a float or a percentage) - relative_tolerance: bool, to explicitly use passed tolerance as relative Note: when a tolerance is a percentage (i.e. '10%'), it will compute that percentage of the instructor result and yield a number. If relative_tolerance is set to False, it will use that value and the instructor result to define the bounds of valid student result: instructor_complex = 10, tolerance = '10%' will give [9.0, 11.0]. If relative_tolerance is set to True, it will use that value and both instructor result and student result to define the bounds of valid student result: instructor_complex = 10, student_complex = 20, tolerance = '10%' will give [8.0, 12.0]. This is typically used internally to compare float, with a default_tolerance = '0.001%'. Default tolerance of 1e-3% is added to compare two floats for near-equality (to handle machine representation errors). Default tolerance is relative, as the acceptable difference between two floats depends on the magnitude of the floats. (http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/) Examples: In [183]: 0.000016 - 1.6*10**-5 Out[183]: -3.3881317890172014e-21 In [212]: 1.9e24 - 1.9*10**24 Out[212]: 268435456.0 """ if isinstance(tolerance, str): if tolerance == default_tolerance: relative_tolerance = True if tolerance.endswith('%'): tolerance = evaluator(dict(), dict(), tolerance[:-1]) * 0.01 if not relative_tolerance: tolerance = tolerance * abs(instructor_complex) else: tolerance = evaluator(dict(), dict(), tolerance) if relative_tolerance: tolerance = tolerance * max(abs(student_complex), abs(instructor_complex)) if isinf(student_complex) or isinf(instructor_complex): # If an input is infinite, we can end up with `abs(student_complex-instructor_complex)` and # `tolerance` both equal to infinity. Then, below we would have # `inf <= inf` which is a fail. Instead, compare directly. return student_complex == instructor_complex else: # v1 and v2 are, in general, complex numbers: # there are some notes about backward compatibility issue: see responsetypes.get_staff_ans()). return abs(student_complex - instructor_complex) <= tolerance
def test_function_case_sensitivity(self): """ Test the case sensitivity of functions """ 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)'), calc.evaluator({}, functions, 'F(6)')) # Test case sensitive evaluation self.assertNotEqual(calc.evaluator({}, functions, 'f(6)', cs=True), calc.evaluator({}, functions, 'F(6)', cs=True))
def test_simple_funcs(self): """ Subsitution of custom functions """ variables = {'x': 4.712} functions = {'id': lambda x: x} self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81) self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81) 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)
def test_parallel_resistors(self): """ Test the parallel resistor 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) self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j)
def test_variable_case_sensitivity(self): """ Test the case sensitivity flag and corresponding behavior """ self.assertEqual( calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"), 8.0) variables = {'t': 1.0} self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0) self.assertEqual(calc.evaluator(variables, {}, "T"), 1.0) self.assertEqual(calc.evaluator(variables, {}, "t", cs=True), 1.0) # Recall 'T' is a default constant, with value 298.15 self.assertAlmostEqual(calc.evaluator(variables, {}, "T", cs=True), 298, delta=0.2)
def test_constants(self): """ Test the default constants provided in calc.py which are: j (complex number), e, pi, k, c, T, q """ # Of the form ('expr', python value, tolerance (or None for exact)) default_variables = [ ('i', 1j, None), ('j', 1j, None), ('e', 2.7183, 1e-4), ('pi', 3.1416, 1e-4), ('k', 1.3806488e-23, 1e-26), # Boltzmann constant (Joules/Kelvin) ('c', 2.998e8, 1e5), # Light Speed in (m/s) ('T', 298.15, 0.01), # 0 deg C = T Kelvin ('q', 1.602176565e-19, 1e-22) # Fund. Charge (Coulombs) ] 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_constants(self): """ Test the default constants provided in calc.py which are: j (complex number), e, pi """ # Of the form ('expr', python value, tolerance (or None for exact)) default_variables = [ ('i', 1j, None), ('j', 1j, None), ('e', 2.7183, 1e-4), ('pi', 3.1416, 1e-4), ] 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_trailing_period(self): """ Test that things like '4.' will be 4 and not throw an error """ try: self.assertEqual(4.0, calc.evaluator({}, {}, '4.')) except ParseException: self.fail("'4.' is a valid input, but threw an exception")
def _evaluate_row(self, index, row): # funcs_dict is used as a required positional argument in the formularesponse grader calc.evaluator. However, it is never used for anything. funcs_dict = {} if row['submission']==self.metadata['empty_encoding']: raise EmptySubmissionException else: for sample_index, vars_dict in enumerate(self.metadata['vars_dict_list']): self.data.loc[index,'eval.'+str(sample_index) ] = calc.evaluator(vars_dict, funcs_dict, row['submission'],case_sensitive=self.metadata['case_sensitive']) return
def test_function_case_sensitive(self): """ Test case sensitive evaluation Incorrectly capitilized should fail Also, it should pick the correct version of a function. """ with self.assertRaisesRegexp(calc.UndefinedVariable, 'SiN'): calc.evaluator({}, {}, 'SiN(6)', case_sensitive=True) # With case sensitive turned on, it should pick the right function functions = {'f': lambda x: x, 'F': lambda x: x + 1} self.assertEqual( 6, calc.evaluator({}, functions, 'f(6)', case_sensitive=True) ) self.assertEqual( 7, calc.evaluator({}, functions, 'F(6)', case_sensitive=True) )
def is_formula_equal(expected, given, samples, cs=True, tolerance=0.01): variables = samples.split('@')[0].split(',') numsamples = int(samples.split('@')[1].split('#')[1]) sranges = zip(*map(lambda x: map(float, x.split(",")), samples.split('@')[1].split('#')[0].split(':'))) ranges = dict(zip(variables, sranges)) for i in range(numsamples): vvariables = {} for var in ranges: value = random.uniform(*ranges[var]) vvariables[str(var)] = value try: instructor_result = evaluator(vvariables, dict(), expected, case_sensitive=cs) student_result = evaluator(vvariables, dict(), given, case_sensitive=cs) except Exception as err: raise Exception("is_formula_eq: vvariables=%s, err=%s" % (vvariables, str(err))) if abs(instructor_result-student_result) > tolerance: return False return True
def test_variable_case_sensitivity(self): """ Test the case sensitivity flag and corresponding behavior """ self.assertEqual( calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"), 8.0 ) variables = {'E': 1.0} self.assertEqual( calc.evaluator(variables, {}, "E", case_sensitive=True), 1.0 ) # Recall 'e' is a default constant, with value 2.718 self.assertAlmostEqual( calc.evaluator(variables, {}, "e", case_sensitive=True), 2.718, delta=0.02 )
def calculate(request): """ Calculator in footer of every page. """ equation = request.GET["equation"] try: result = calc.evaluator({}, {}, equation) except: event = {"error": map(str, sys.exc_info()), "equation": equation} track.views.server_track(request, "error:calc", event, page="calc") return HttpResponse(json.dumps({"result": "Invalid syntax"})) return HttpResponse(json.dumps({"result": str(result)}))
def hint_mag(answer_ids, student_answers, new_cmap, old_cmap, anum=0, sign=False): global expected try: aid = answer_ids[0] except Exception as err: raise Exception( "cannot get answer_ids[%d], answer_ids=%s, new_cmap=%s, err=%s" % (anum, answer_ids, new_cmap, err) ) ans = student_answers[aid] try: ans = float(ans) except Exception as err: try: ans = evaluator({}, {}, ans) except Exception as err: hint = '<font color="red">Cannot evaluate your answer</font>' new_cmap.set_hint_and_mode(aid, hint, "always") return try: if type(expected) == list: expect = expected[anum] else: expect = expected except Exception as err: raise Exception("expected answer not evaluated, expected=%s, anum=%s, err=%s" % (expected, anum, str(err))) # if expect is a dict, then generate hints by range in addition to extra_hints = [] hint = "" if type(expect) == dict: expect_dict = expect expect = expect_dict["val"] extra_hints = expect_dict.get("extra_hints", []) if new_cmap.is_correct(aid): # if correct, make sure answer is close, else direct student to look at solution if not is_tight(ans, expect, 0.01): hint = '<font color="green">Your answer is accepted as correct, but more than 1% from the expected. Please check the solutions, and use the expected answer in any further calculations.</font>' else: hint = is_close(ans, expect) if not hint and sign: hint = is_sign_correct(ans, expect) for eh in extra_hints: range = eh.get("range", "") if range: if in_range(ans, range): hint += " " + eh["hint"] if hint: new_cmap.set_hint_and_mode(aid, hint, "always")
def calculate(request): ''' Calculator in footer of every page. ''' equation = request.GET['equation'] try: result = calc.evaluator({}, {}, equation) except: event = {'error': map(str, sys.exc_info()), 'equation': equation} track.views.server_track(request, 'error:calc', event, page='calc') return HttpResponse(json.dumps({'result': 'Invalid syntax'})) return HttpResponse(json.dumps({'result': str(result)}))
def test_function_case_insensitive(self): """ Test case insensitive evaluation Normal functions with some capitals should be fine """ self.assertAlmostEqual(-0.28, calc.evaluator({}, {}, 'SiN(6)', case_sensitive=False), delta=1e-3)
def test_variable_case_sensitivity(self): """ Test the case sensitivity flag and corresponding behavior """ self.assertEqual(calc.evaluator({ 'R1': 2.0, 'R3': 4.0 }, {}, "r1*r3"), 8.0) variables = {'t': 1.0} self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0) self.assertEqual(calc.evaluator(variables, {}, "T"), 1.0) self.assertEqual( calc.evaluator(variables, {}, "t", case_sensitive=True), 1.0) # Recall 'T' is a default constant, with value 298.15 self.assertAlmostEqual(calc.evaluator(variables, {}, "T", case_sensitive=True), 298, delta=0.2)
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 = "Expected '{0}' to equal {1}".format(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) self.assertNotEqual(answer, result, msg=fail_msg)
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 = "Expected '{0}' to equal {1}".format( 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) self.assertNotEqual(answer, result, msg=fail_msg)
def test_function_case_insensitive(self): """ Test case insensitive evaluation Normal functions with some capitals should be fine """ self.assertAlmostEqual( -0.28, calc.evaluator({}, {}, 'SiN(6)', case_sensitive=False), delta=1e-3 )
def test_complex_expression(self): """ Calculate combinations of operators and default functions """ 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) self.assertAlmostEqual(calc.evaluator({}, {}, "sin(e)"), 0.41, delta=0.01) self.assertAlmostEqual(calc.evaluator({}, {}, "e^(j*pi)"), -1, delta=1e-5)
def test_explicit_sci_notation(self): """ Expressions like 1.6*10^-3 (not 1.6e-3) it should evaluate. """ self.assertEqual( calc.evaluator({}, {}, "-1.6*10^-3"), -0.0016 ) self.assertEqual( calc.evaluator({}, {}, "-1.6*10^(-3)"), -0.0016 ) self.assertEqual( calc.evaluator({}, {}, "-1.6*10^3"), -1600 ) self.assertEqual( calc.evaluator({}, {}, "-1.6*10^(3)"), -1600 )
def hint_mag(answer_ids, student_answers, new_cmap, old_cmap, anum=0, sign=False): global expected try: aid = answer_ids[0] except Exception as err: raise Exception('cannot get answer_ids[%d], answer_ids=%s, new_cmap=%s, err=%s' % (anum, answer_ids, new_cmap, err)) ans = student_answers[aid] try: ans = float(ans) except Exception as err: try: ans = evaluator({},{}, ans) except Exception as err: hint = '<font color="red">Cannot evaluate your answer</font>' new_cmap.set_hint_and_mode(aid, hint, 'always') return try: if type(expected)==list: expect = expected[anum] else: expect = expected except Exception as err: raise Exception('expected answer not evaluated, expected=%s, anum=%s, err=%s' % (expected, anum, str(err))) # if expect is a dict, then generate hints by range in addition to extra_hints = [] hint = '' if type(expect)==dict: expect_dict = expect expect = expect_dict['val'] extra_hints = expect_dict.get('extra_hints', []) if new_cmap.is_correct(aid): # if correct, make sure answer is close, else direct student to look at solution if not is_tight(ans, expect, 0.01): hint = '<font color="green">Your answer is accepted as correct, but more than 1% from the expected. Please check the solutions, and use the expected answer in any further calculations.</font>' else: hint = is_close(ans, expect) if not hint and sign: hint = is_sign_correct(ans, expect) for eh in extra_hints: range = eh.get('range','') if range: if in_range(ans, range): hint += ' ' + eh['hint'] if hint: new_cmap.set_hint_and_mode(aid, hint, 'always')
def test_undefined_vars(self): """ Check to see if the evaluator catches undefined variables """ variables = {'R1': 2.0, 'R3': 4.0} with self.assertRaisesRegexp(calc.UndefinedVariable, r'QWSEKO'): calc.evaluator({}, {}, "5+7*QWSEKO") with self.assertRaisesRegexp(calc.UndefinedVariable, r'r2'): calc.evaluator({'r1': 5}, {}, "r1+r2") with self.assertRaisesRegexp(calc.UndefinedVariable, r'r1, r3'): calc.evaluator(variables, {}, "r1*r3", case_sensitive=True) with self.assertRaisesRegexp(calc.UndefinedVariable, r'did you forget to use \*'): calc.evaluator(variables, {}, "R1(R3 + 1)")
def compare_with_tolerance(complex1, complex2, tolerance=default_tolerance, relative_tolerance=False): """ Compare complex1 to complex2 with maximum tolerance tol. If tolerance is type string, then it is counted as relative if it ends in %; otherwise, it is absolute. - complex1 : student result (float complex number) - complex2 : instructor result (float complex number) - tolerance : string representing a number or float - relative_tolerance: bool, used when`tolerance` is float to explicitly use passed tolerance as relative. Default tolerance of 1e-3% is added to compare two floats for near-equality (to handle machine representation errors). Default tolerance is relative, as the acceptable difference between two floats depends on the magnitude of the floats. (http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/) Examples: In [183]: 0.000016 - 1.6*10**-5 Out[183]: -3.3881317890172014e-21 In [212]: 1.9e24 - 1.9*10**24 Out[212]: 268435456.0 """ if relative_tolerance: tolerance = tolerance * max(abs(complex1), abs(complex2)) elif tolerance.endswith('%'): tolerance = evaluator(dict(), dict(), tolerance[:-1]) * 0.01 tolerance = tolerance * max(abs(complex1), abs(complex2)) else: tolerance = evaluator(dict(), dict(), tolerance) if isinf(complex1) or isinf(complex2): # If an input is infinite, we can end up with `abs(complex1-complex2)` and # `tolerance` both equal to infinity. Then, below we would have # `inf <= inf` which is a fail. Instead, compare directly. return complex1 == complex2 else: # v1 and v2 are, in general, complex numbers: # there are some notes about backward compatibility issue: see responsetypes.get_staff_ans()). return abs(complex1 - complex2) <= tolerance
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)
def compare_with_tolerance(v1, v2, tol): ''' Compare v1 to v2 with maximum tolerance tol tol is relative if it ends in %; otherwise, it is absolute - v1 : student result (number) - v2 : instructor result (number) - tol : tolerance (string representing a number) ''' relative = tol.endswith('%') if relative: tolerance_rel = evaluator(dict(), dict(), tol[:-1]) * 0.01 tolerance = tolerance_rel * max(abs(v1), abs(v2)) else: tolerance = evaluator(dict(), dict(), tol) if isinf(v1) or isinf(v2): # If an input is infinite, we can end up with `abs(v1-v2)` and # `tolerance` both equal to infinity. Then, below we would have # `inf <= inf` which is a fail. Instead, compare directly. return v1 == v2 else: return abs(v1 - v2) <= tolerance
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]. This is 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 calc.py which are: sin, cos, tan, arccos, arcsin, arctan """ angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.298 + 0.635j] cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 0.834 - 0.989j] tan_values = [-1, 0, 0.577, 0.727, 1, 1, 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 a complex number, numpy.arcsin gives nan self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(-1.1)'))) self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(1.1)'))) # 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 compare_with_tolerance(v1, v2, tol=default_tolerance): ''' Compare v1 to v2 with maximum tolerance tol. tol is relative if it ends in %; otherwise, it is absolute - v1 : student result (float complex number) - v2 : instructor result (float complex number) - tol : tolerance (string representing a number) Default tolerance of 1e-3% is added to compare two floats for near-equality (to handle machine representation errors). It is relative, as the acceptable difference between two floats depends on the magnitude of the floats. (http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/) Examples: In [183]: 0.000016 - 1.6*10**-5 Out[183]: -3.3881317890172014e-21 In [212]: 1.9e24 - 1.9*10**24 Out[212]: 268435456.0 ''' relative = tol.endswith('%') if relative: tolerance_rel = evaluator(dict(), dict(), tol[:-1]) * 0.01 tolerance = tolerance_rel * max(abs(v1), abs(v2)) else: tolerance = evaluator(dict(), dict(), tol) if isinf(v1) or isinf(v2): # If an input is infinite, we can end up with `abs(v1-v2)` and # `tolerance` both equal to infinity. Then, below we would have # `inf <= inf` which is a fail. Instead, compare directly. return v1 == v2 else: # v1 and v2 are, in general, complex numbers: # there are some notes about backward compatibility issue: see responsetypes.get_staff_ans()). return abs(v1 - v2) <= tolerance
def test_raises_zero_division_err(self): """ Ensure division by zero gives an error """ with self.assertRaises(ZeroDivisionError): calc.evaluator({}, {}, '1/0') with self.assertRaises(ZeroDivisionError): calc.evaluator({}, {}, '1/0.0') with self.assertRaises(ZeroDivisionError): calc.evaluator({'x': 0.0}, {}, '1/x')
def test_si_suffix(self): """ Test calc.py's unique functionality of interpreting si 'suffixes'. For instance '%' stand for 1/100th so '1%' should be 0.01 """ test_mapping = [('4.2%', 0.042)] 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)
def test_number_input(self): """ Test different kinds of float inputs See also test_trailing_period (slightly different) test_exponential_answer test_si_suffix """ 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)
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 # 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)
def compare_with_tolerance(student_complex, instructor_complex, tolerance=default_tolerance, relative_tolerance=False): """ Compare student_complex to instructor_complex with maximum tolerance tolerance. - student_complex : student result (float complex number) - instructor_complex : instructor result (float complex number) - tolerance : float, or string (representing a float or a percentage) - relative_tolerance: bool, to explicitly use passed tolerance as relative Note: when a tolerance is a percentage (i.e. '10%'), it will compute that percentage of the instructor result and yield a number. If relative_tolerance is set to False, it will use that value and the instructor result to define the bounds of valid student result: instructor_complex = 10, tolerance = '10%' will give [9.0, 11.0]. If relative_tolerance is set to True, it will use that value and both instructor result and student result to define the bounds of valid student result: instructor_complex = 10, student_complex = 20, tolerance = '10%' will give [8.0, 12.0]. This is typically used internally to compare float, with a default_tolerance = '0.001%'. Default tolerance of 1e-3% is added to compare two floats for near-equality (to handle machine representation errors). Default tolerance is relative, as the acceptable difference between two floats depends on the magnitude of the floats. (http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/) Examples: In [183]: 0.000016 - 1.6*10**-5 Out[183]: -3.3881317890172014e-21 In [212]: 1.9e24 - 1.9*10**24 Out[212]: 268435456.0 """ if isinstance(tolerance, str): if tolerance == default_tolerance: relative_tolerance = True if tolerance.endswith('%'): tolerance = evaluator(dict(), dict(), tolerance[:-1]) * 0.01 if not relative_tolerance: tolerance = tolerance * abs(instructor_complex) else: tolerance = evaluator(dict(), dict(), tolerance) if relative_tolerance: tolerance = tolerance * max(abs(student_complex), abs(instructor_complex)) if isinf(student_complex) or isinf(instructor_complex): # If an input is infinite, we can end up with `abs(student_complex-instructor_complex)` and # `tolerance` both equal to infinity. Then, below we would have # `inf <= inf` which is a fail. Instead, compare directly. return student_complex == instructor_complex # because student_complex and instructor_complex are not necessarily # complex here, we enforce it here: student_complex = complex(student_complex) instructor_complex = complex(instructor_complex) # if both the instructor and student input are real, # compare them as Decimals to avoid rounding errors if not (instructor_complex.imag or student_complex.imag): # if either of these are not a number, short circuit and return False if isnan(instructor_complex.real) or isnan(student_complex.real): return False student_decimal = Decimal(str(student_complex.real)) instructor_decimal = Decimal(str(instructor_complex.real)) tolerance_decimal = Decimal(str(tolerance)) return abs(student_decimal - instructor_decimal) <= tolerance_decimal else: # v1 and v2 are, in general, complex numbers: # there are some notes about backward compatibility issue: see responsetypes.get_staff_ans()). return abs(student_complex - instructor_complex) <= tolerance
def compare_with_tolerance(student_complex, instructor_complex, tolerance=default_tolerance, relative_tolerance=False): """ Compare student_complex to instructor_complex with maximum tolerance tolerance. - student_complex : student result (float complex number) - instructor_complex : instructor result (float complex number) - tolerance : float, or string (representing a float or a percentage) - relative_tolerance: bool, to explicitly use passed tolerance as relative Note: when a tolerance is a percentage (i.e. '10%'), it will compute that percentage of the instructor result and yield a number. If relative_tolerance is set to False, it will use that value and the instructor result to define the bounds of valid student result: instructor_complex = 10, tolerance = '10%' will give [9.0, 11.0]. If relative_tolerance is set to True, it will use that value and both instructor result and student result to define the bounds of valid student result: instructor_complex = 10, student_complex = 20, tolerance = '10%' will give [8.0, 12.0]. This is typically used internally to compare float, with a default_tolerance = '0.001%'. Default tolerance of 1e-3% is added to compare two floats for near-equality (to handle machine representation errors). Default tolerance is relative, as the acceptable difference between two floats depends on the magnitude of the floats. (http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/) Examples: In [183]: 0.000016 - 1.6*10**-5 Out[183]: -3.3881317890172014e-21 In [212]: 1.9e24 - 1.9*10**24 Out[212]: 268435456.0 """ if isinstance(tolerance, str): if tolerance == default_tolerance: relative_tolerance = True if tolerance.endswith('%'): tolerance = evaluator(dict(), dict(), tolerance[:-1]) * 0.01 if not relative_tolerance: tolerance = tolerance * abs(instructor_complex) else: tolerance = evaluator(dict(), dict(), tolerance) if relative_tolerance: tolerance = tolerance * max(abs(student_complex), abs(instructor_complex)) if isinf(student_complex) or isinf(instructor_complex): # If an input is infinite, we can end up with `abs(student_complex-instructor_complex)` and # `tolerance` both equal to infinity. Then, below we would have # `inf <= inf` which is a fail. Instead, compare directly. return student_complex == instructor_complex else: # v1 and v2 are, in general, complex numbers: # there are some notes about backward compatibility issue: see responsetypes.get_staff_ans()). decimal_places = None # count the "decimal_places" for "student_complex". e.g, for # "student_complex" value "152.3667" the "decimal_places" will be # 4 as there are 4 digits "3667" after decimal if isinstance(student_complex, float): decimal_places = Decimal( str(student_complex)).as_tuple().exponent * -1 # pylint: disable=E1103 abs_value = abs(student_complex - instructor_complex) # decimal_places could be NaN in some cases if decimal_places and isinstance(decimal_places, int): # abs_value contains 17 digits exponent value so # round it up to "decimal_places" abs_value = round(abs_value, decimal_places) return abs_value <= tolerance
def test_trailing_period(self): """ Test that things like '4.' will be 4 and not throw an error """ self.assertEqual(4.0, calc.evaluator({}, {}, '4.'))
def easy_eval(x): return calc.evaluator({}, {}, x)
def test_simple_vars(self): """ Substitution of variables into simple equations """ variables = { 'x': 9.72, 'y': 7.91, 'loooooong': 6.4, "f_0'": 2.0, "T_{ijk}^{123}''": 5.2 } # Should not change value of constant # even with different numbers of variables... self.assertEqual(calc.evaluator({'x': 9.72}, {}, '13'), 13) self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, '13'), 13) self.assertEqual(calc.evaluator(variables, {}, '13'), 13) # Easy evaluation self.assertEqual(calc.evaluator(variables, {}, 'x'), 9.72) self.assertEqual(calc.evaluator(variables, {}, 'y'), 7.91) self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4) self.assertEqual(calc.evaluator(variables, {}, "f_0'"), 2.0) self.assertEqual(calc.evaluator(variables, {}, "T_{ijk}^{123}''"), 5.2) # 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.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.6600949841121 }, {}, "5"), 5)
def test_calc(self): variables = {'R1': 2.0, 'R3': 4.0} functions = {'sin': numpy.sin, 'cos': numpy.cos} self.assertTrue(abs(calc.evaluator(variables, functions, "10000||sin(7+5)+0.5356")) < 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({}, {}, "-1"), -1) self.assertEqual(calc.evaluator({}, {}, "-0.33"), -.33) self.assertEqual(calc.evaluator({}, {}, "-.33"), -.33) self.assertEqual(calc.evaluator(variables, functions, "R1*R3"), 8.0) self.assertTrue(abs(calc.evaluator(variables, functions, "sin(e)-0.41")) < 0.01) self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025")) < 0.001) self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)") + 1) < 0.00001) self.assertTrue(abs(calc.evaluator(variables, functions, "j||1") - 0.5 - 0.5j) < 0.00001) variables['t'] = 1.0 # Use self.assertAlmostEqual here... self.assertTrue(abs(calc.evaluator(variables, functions, "t") - 1.0) < 0.00001) self.assertTrue(abs(calc.evaluator(variables, functions, "T") - 1.0) < 0.00001) self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True) - 1.0) < 0.00001) self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True) - 298) < 0.2) # Use self.assertRaises here... exception_happened = False try: calc.evaluator({}, {}, "5+7 QWSEKO") except: exception_happened = True self.assertTrue(exception_happened) try: calc.evaluator({'r1': 5}, {}, "r1+r2") except calc.UndefinedVariable: pass self.assertEqual(calc.evaluator(variables, functions, "r1*r3"), 8.0) exception_happened = False try: calc.evaluator(variables, functions, "r1*r3", cs=True) except: exception_happened = True self.assertTrue(exception_happened)