Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tolerance expressed in percentage now computes correctly. [BLD-522] #3489

Merged
merged 1 commit into from
May 21, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.

Blades: Tolerance expressed in percentage now computes correctly. BLD-522.

Studio: Add drag-and-drop support to the container page. STUD-1309.

Common: Add extensible third-party auth module.
Expand Down
17 changes: 14 additions & 3 deletions common/lib/capa/capa/tests/test_responsetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
StudentInputError, ResponseError
from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames
from capa.util import compare_with_tolerance
from capa.xqueue_interface import dateformat

from pytz import UTC
Expand Down Expand Up @@ -1120,7 +1121,6 @@ class NumericalResponseTest(ResponseTest):
# We blend the line between integration (using evaluator) and exclusively
# unit testing the NumericalResponse (mocking out the evaluator)
# For simple things its not worth the effort.

def test_grade_range_tolerance(self):
problem_setup = [
# [given_asnwer, [list of correct responses], [list of incorrect responses]]
Expand Down Expand Up @@ -1177,9 +1177,20 @@ def test_grade_decimal_tolerance(self):
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)

def test_grade_percent_tolerance(self):
# Positive only range
problem = self.build_problem(answer=4, tolerance="10%")
correct_responses = ["4.0", "4.3", "3.7", "4.30", "3.70"]
incorrect_responses = ["", "4.5", "3.5", "0"]
correct_responses = ["4.0", "4.00", "4.39", "3.61"]
incorrect_responses = ["", "4.41", "3.59", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
# Negative only range
problem = self.build_problem(answer=-4, tolerance="10%")
correct_responses = ["-4.0", "-4.00", "-4.39", "-3.61"]
incorrect_responses = ["", "-4.41", "-3.59", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
# Mixed negative/positive range
problem = self.build_problem(answer=1, tolerance="200%")
correct_responses = ["1", "1.00", "2.99", "0.99"]
incorrect_responses = ["", "3.01", "-1.01"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)

def test_floats(self):
Expand Down
82 changes: 82 additions & 0 deletions common/lib/capa/capa/tests/test_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Tests capa util"""

import unittest
import textwrap
from . import test_capa_system
from capa.util import compare_with_tolerance


class UtilTest(unittest.TestCase):
"""Tests for util"""
def setUp(self):
super(UtilTest, self).setUp()
self.system = test_capa_system()

def test_compare_with_tolerance(self):
# Test default tolerance '0.001%' (it is relative)
result = compare_with_tolerance(100.0, 100.0)
self.assertTrue(result)
result = compare_with_tolerance(100.001, 100.0)
self.assertTrue(result)
result = compare_with_tolerance(101.0, 100.0)
self.assertFalse(result)
# Test absolute percentage tolerance
result = compare_with_tolerance(109.9, 100.0, '10%', False)
self.assertTrue(result)
result = compare_with_tolerance(110.1, 100.0, '10%', False)
self.assertFalse(result)
# Test relative percentage tolerance
result = compare_with_tolerance(111.0, 100.0, '10%', True)
self.assertTrue(result)
result = compare_with_tolerance(112.0, 100.0, '10%', True)
self.assertFalse(result)
# Test absolute tolerance (string)
result = compare_with_tolerance(109.9, 100.0, '10.0', False)
self.assertTrue(result)
result = compare_with_tolerance(110.1, 100.0, '10.0', False)
self.assertFalse(result)
# Test relative tolerance (string)
result = compare_with_tolerance(111.0, 100.0, '0.1', True)
self.assertTrue(result)
result = compare_with_tolerance(112.0, 100.0, '0.1', True)
self.assertFalse(result)
# Test absolute tolerance (float)
result = compare_with_tolerance(109.9, 100.0, 10.0, False)
self.assertTrue(result)
result = compare_with_tolerance(110.1, 100.0, 10.0, False)
self.assertFalse(result)
# Test relative tolerance (float)
result = compare_with_tolerance(111.0, 100.0, 0.1, True)
self.assertTrue(result)
result = compare_with_tolerance(112.0, 100.0, 0.1, True)
self.assertFalse(result)
##### Infinite values #####
infinity = float('Inf')
# Test relative tolerance (float)
result = compare_with_tolerance(infinity, 100.0, 1.0, True)
self.assertFalse(result)
result = compare_with_tolerance(100.0, infinity, 1.0, True)
self.assertFalse(result)
result = compare_with_tolerance(infinity, infinity, 1.0, True)
self.assertTrue(result)
# Test absolute tolerance (float)
result = compare_with_tolerance(infinity, 100.0, 1.0, False)
self.assertFalse(result)
result = compare_with_tolerance(100.0, infinity, 1.0, False)
self.assertFalse(result)
result = compare_with_tolerance(infinity, infinity, 1.0, False)
self.assertTrue(result)
# Test relative tolerance (string)
result = compare_with_tolerance(infinity, 100.0, '1.0', True)
self.assertFalse(result)
result = compare_with_tolerance(100.0, infinity, '1.0', True)
self.assertFalse(result)
result = compare_with_tolerance(infinity, infinity, '1.0', True)
self.assertTrue(result)
# Test absolute tolerance (string)
result = compare_with_tolerance(infinity, 100.0, '1.0', False)
self.assertFalse(result)
result = compare_with_tolerance(100.0, infinity, '1.0', False)
self.assertFalse(result)
result = compare_with_tolerance(infinity, infinity, '1.0', False)
self.assertTrue(result)
52 changes: 35 additions & 17 deletions common/lib/capa/capa/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,29 @@
default_tolerance = '0.001%'


def compare_with_tolerance(complex1, complex2, tolerance=default_tolerance, relative_tolerance=False):
def compare_with_tolerance(student_complex, instructor_complex, tolerance=default_tolerance, relative_tolerance=False):
"""
Compare complex1 to complex2 with maximum tolerance tol.
Compare student_complex to instructor_complex with maximum tolerance tolerance.

If tolerance is type string, then it is counted as relative if it ends in %; otherwise, it is absolute.
- 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

- 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.
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).
Expand All @@ -29,23 +42,28 @@ def compare_with_tolerance(complex1, complex2, tolerance=default_tolerance, rela
In [212]: 1.9e24 - 1.9*10**24
Out[212]: 268435456.0
"""
if isinstance(tolerance, str):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have not-a-string-type tolerance anywhere?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O, I've found one place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In previous code, when tolerance was not ending on '%', it was going through 'evaluator(dict(), dict(), tolerance)'. Now it is going through only if it is string. Why it is so?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@auraz Is there any reason to evaluate anything else than a string?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not know, It should be investigated.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@auraz compare_with_tolerance is used in only 1 file responsetypes.py 5 times (if we don't count tests). It's value is either float_info.epsilon, the default value (0.001%) or what is specified in the XML. Do you think this might need evaluation? I guess to be on the safe side, we should let it go through evaluator(dict(), dict(), tolerance) every time it's not a percentage. Thanks. Will do.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@auraz I take this back, if you pass a float through the evaluator, the following error is raised:

Traceback (most recent call last):
    common/lib/capa/capa/tests/test_util.py line 44 in test_compare_with_tolerance
      result = compare_with_tolerance(109.9, 100.0, 10.0, False)
    common/lib/capa/capa/util.py line 37 in compare_with_tolerance
      tolerance = evaluator(dict(), dict(), tolerance)
    common/lib/calc/calc/calc.py line 228 in evaluator
      if math_expr.strip() == "":
   AttributeError: 'float' object has no attribute 'strip'

I propose we leave things as is. I tested the code manually and tests all pass. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for looking into. I agree with you.

if tolerance == default_tolerance:
relative_tolerance = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As default tolerance ends with '%' it should go through common path for '%' tolerances.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@auraz When something like 100 +- 10% is entered in Studio, we want the tolerance to be absolute (it was relative before and was the cause of the bug). But when default tolerance ('0.001%') is used throughout the code, we want it to be relative. Hence the if clause.

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(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)
tolerance = tolerance * max(abs(student_complex), abs(instructor_complex))

if isinf(complex1) or isinf(complex2):
# If an input is infinite, we can end up with `abs(complex1-complex2)` and
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 complex1 == complex2
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(complex1 - complex2) <= tolerance
return abs(student_complex - instructor_complex) <= tolerance


def contextualize_text(text, context): # private
Expand Down