5

I am trying to write a unittest to check the output of an engineering analysis. I have theoretical values which i want to check against the analysis to a certain number of significant figures. so, for example:

Ixx_ther = 0.000123
Iyy_ther = 0.0123

Ixx, Iyy = getI(*args, **kwargs)

self.assertAlmostEqual(Ixx_ther, Ixx, 6)
self.assertAlmostEqual(Iyy_ther, Iyy, 4)

In this case, i need to know the number i am trying to check as setting the tolerance to 6 in both cases would make the test too stringent and setting it to 4 would be too lax. What I need is a test for equality to the same number of significant figures. What would be ideal is to say:

Ixx_ther = 1.23E-4
Iyy_ther = 1.23E-2

Ixx, Iyy = getI(*args, **kwargs)

self.assertAlmostEqual(Ixx_ther, Ixx, 2)
self.assertAlmostEqual(Iyy_ther, Iyy, 2)

and have the assert statement drop exponent and check only the Significand for equality. I imagine this has been done before, but I have not been able to find a built-in function to assert equality in this manner. Has anyone had this problem before,

Questions

1) Has anyone had this problem before, and know of a general guide of unittests for engineering analysis

2) Is there a built-in solution. to this problem

3) Has someone already programmed a custom assert statement which works in this manner?

steve855
  • 53
  • 1
  • 3
  • Be careful about how you're defining "significant figures". It's not just the number of decimal places...the zeroes immediately after the decimal aren't counted in the usual definition, so both your examples Ixx_ther and Iyy_ther would be considered to have three significant figures. – Jim Lewis Oct 28 '13 at 20:11

4 Answers4

6

Re: is there a built-in solution for this: If you can have numpy as a dependency, have a look at numpy.testing.

Here's an example ( verbatim from assert_allclose docs):

>>> x = [1e-5, 1e-3, 1e-1]
>>> y = np.arccos(np.cos(x))
>>> assert_allclose(x, y, rtol=1e-5, atol=0)

EDIT: For completeness, here's the link to the source code: assert_allclose forwards the real work to np.allclose. Which is nearly identical to @Mark Ransom's answer (plus handling of array arguments and infinities).

ev-br
  • 24,968
  • 9
  • 65
  • 78
4

This is a reworking of an answer I left on another question.

def AlmostEqual(a, b, digits):
    epsilon = 10 ** -digits
    return abs(a/b - 1) < epsilon

This needs a little more work if b can be zero.

Community
  • 1
  • 1
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • Thanks, this is a very elegant solution, though i think I am going to use the numpy testing module proposed by @Zhenya – steve855 Oct 31 '13 at 18:36
  • (+1) this works almost always `>>> AlmostEqual(1e320, 1e320, 2)` False – ev-br Nov 01 '13 at 12:13
0

Perhaps not answering the full scope of your question, but this is how I would write such a function:

def assertAlmostEqual(arg1,arg2,tolerance=2):
    str_formatter = '{0:.' + str(tolerance) + 'e}'
    lhs = str_formatter.format(arg1).split('e')[0]
    rhs = str_formatter.format(arg2).split('e')[0]
    assert lhs == rhs

Python's string formatting mini-language can be leveraged to format your floats into a given manner. So what we can do is force them to be formatted in exponent notation such that, for i.e. inputs 0.123 and 0.000123 we have:

str_formatter.format(0.123) == '1.23e-01'
str_formatter.format(0.000123) == '1.23e-04'

And all that remains is to chop off the exponent and assert equality.

Demo:

assertAlmostEqual(0.0123,0.0001234)

assertAlmostEqual(0.123,0.0001234)

assertAlmostEqual(0.123,0.0001234,tolerance=3)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/home/xxx/<ipython-input-83-02fbd71b2e87> in <module>()
----> 1 assertAlmostEqual(0.123,0.0001234,tolerance=3)

/home/xxx/<ipython-input-74-ae32ed74769d> in assertAlmostEqual(arg1, arg2, tolerance)
      3     lhs = str_formatter.format(arg1).split('e')[0]
      4     rhs = str_formatter.format(arg2).split('e')[0]
----> 5     assert lhs == rhs
      6 

AssertionError: 

There might be an off-by-one issue if you don't like the way I defined tolerance. Gets the idea across though.

roippi
  • 25,533
  • 4
  • 48
  • 73
0

Thanks roippi for the great idea, I modified your code somewhat:

def assertAlmostEqualSigFig(self, arg1,arg2,tolerance=2):
    if tolerance > 1: 
        tolerance -= 1
    #end

    str_formatter = '{0:.' + str(tolerance) + 'e}'
    significand_1 = float(str_formatter.format(arg1).split('e')[0])
    significand_2 = float(str_formatter.format(arg2).split('e')[0])

    exponent_1 = int(str_formatter.format(arg1).split('e')[1])
    exponent_2 = int(str_formatter.format(arg2).split('e')[1])

    self.assertEqual(significand_1, significand_2)
    self.assertEqual(exponent_1, exponent_2)

    return

I changed a few things

1) I check the exponent as well as the significand (That's a top drawer word isn't it)

2) I convert the significand and exponent to float / int respectively. This may not be necessary but i am more comfortable checking the equality of numbers as numbers rather than strings.

3) Jim Lewis noted that i need to adjust my tolerance by one since the proper format string {0:.3e} of 0.0123 is 1.230E-2 not 0.123E-1. i.e. if you want three significant figures you only want two digits after the decimal as the digit before the decimal is also significant.

Hers is an example of implementation

class testSigFigs(Parent_test_class):

    @unittest.expectedFailure
    def test_unequal_same_exp(self):
        self.assertAlmostEqualSigFig(0.123, 0.321, 3)

    @unittest.expectedFailure
    def test_unequal_diff_exp(self):
        self.assertAlmostEqualSigFig(0.123, 0.0321, 3)

    @unittest.expectedFailure
    def test_equal_diff_exp(self):
        self.assertAlmostEqualSigFig(0.0123, 0.123, 3)

    def test_equal_same_exp(self):
        self.assertAlmostEqualSigFig(0.123, 0.123, 3)

    def test_equal_within_tolerance(self):
        self.assertAlmostEqualSigFig(0.123, 0.124, 2)
    #end

And the output:

test_equal_diff_exp (__main__.testSigFigs) ... expected failure
test_equal_same_exp (__main__.testSigFigs) ... ok
test_equal_within_tolerance (__main__.testSigFigs) ... ok
test_unequal_diff_exp (__main__.testSigFigs) ... expected failure
test_unequal_same_exp (__main__.testSigFigs) ... expected failure

----------------------------------------------------------------------
Ran 5 tests in 0.081s

OK (expected failures=3)

Thank you both for your feedback.

steve855
  • 53
  • 1
  • 3