diff --git a/INSTALL.txt b/INSTALL.txt index 1aa398cc..3408197f 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -15,14 +15,7 @@ or, if additional access rights are needed (Unix): sudo python setup.py install -* The tests programs (test_*.py) are meant to be run through the Nose -testing framework. This can be achieved for instance with a command +* The tests programs (test_*.py) are meant to be run through pytest. This can be achieved for instance with a command like - nosetests -sv uncertainties/ - -or simply - - nosetests uncertainties/ - -(for a less verbose output). + pytest ./tests \ No newline at end of file diff --git a/doc/index.rst b/doc/index.rst index f6945510..104fe5cf 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -247,49 +247,6 @@ total). :mod:`uncertainties` is thus a **lightweight, portable package** with abundant documentation and tests. -Migration from version 1 to version 2 -===================================== - -Some **incompatible changes** were introduced in version 2 of -:mod:`uncertainties` (see the `version history`_). While the version 2 -line will support the version 1 syntax for some time, it is -recommended to **update existing programs** as soon as possible. This -can be made easier through the provided **automatic updater**. - -The automatic updater works like Python's `2to3 -`_ updater. It can be run -(in a Unix or DOS shell) with: - -.. code-block:: sh - - python -m uncertainties.1to2 - -For example, updating a single Python program can be done with - -.. code-block:: sh - - python -m uncertainties.1to2 -w example.py - -All the Python programs contained under a directory ``Programs`` -(including in nested sub-directories) can be automatically updated -with - -.. code-block:: sh - - python -m uncertainties.1to2 -w Programs - -Backups are automatically created, unless the ``-n`` option is given. - -Some **manual adjustments** might be necessary after running the -updater (incorrectly modified lines, untouched obsolete syntax). - -While the updater creates backup copies by default, it is generally -useful to **first create a backup** of the modified directory, or -alternatively to use some `version control -`_ -system. Reviewing the modifications with a `file comparison tool -`_ might also be useful. - What others say =============== diff --git a/pyproject.toml b/pyproject.toml index 6b8043cb..5798fcaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,3 +57,6 @@ Changelog = "https://github.com/lmfit/uncertainties/blob/master/CHANGES.rst" [project.optional-dependencies] optional = ["numpy"] + +[tool.pytest.ini_options] +testpaths = ["tests"] \ No newline at end of file diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 00000000..13c55f79 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,152 @@ +from uncertainties.core import ufloat +from math import isnan + +def power_all_cases(op): + ''' + Checks all cases for the value and derivatives of power-like + operator op (op is typically the built-in pow(), or math.pow()). + + Checks only the details of special results like 0, 1 or NaN). + + Different cases for the value of x**p and its derivatives are + tested by dividing the (x, p) plane with: + + - x < 0, x = 0, x > 0 + - p integer or not, p < 0, p = 0, p > 0 + + (not all combinations are distinct: for instance x > 0 gives + identical formulas for all p). + ''' + + zero = ufloat(0, 0.1) + zero2 = ufloat(0, 0.1) + one = ufloat(1, 0.1) + positive = ufloat(0.3, 0.01) + positive2 = ufloat(0.3, 0.01) + negative = ufloat(-0.3, 0.01) + integer = ufloat(-3, 0) + non_int_larger_than_one = ufloat(3.1, 0.01) + positive_smaller_than_one = ufloat(0.3, 0.01) + + ## negative**integer + + result = op(negative, integer) + assert not isnan(result.derivatives[negative]) + assert isnan(result.derivatives[integer]) + + # Limit cases: + result = op(negative, one) + assert result.derivatives[negative] == 1 + assert isnan(result.derivatives[one]) + + result = op(negative, zero) + assert result.derivatives[negative] == 0 + assert isnan(result.derivatives[zero]) + + ## negative**non-integer + + ## zero**... + + result = op(zero, non_int_larger_than_one) + assert isnan(result.derivatives[zero]) + assert result.derivatives[non_int_larger_than_one] == 0 + + # Special cases: + result = op(zero, one) + assert result.derivatives[zero] == 1 + assert result.derivatives[one] == 0 + + result = op(zero, 2*one) + assert result.derivatives[zero] == 0 + assert result.derivatives[one] == 0 + + result = op(zero, positive_smaller_than_one) + assert isnan(result.derivatives[zero]) + assert result.derivatives[positive_smaller_than_one] == 0 + + result = op(zero, zero2) + assert result.derivatives[zero] == 0 + assert isnan(result.derivatives[zero2]) + + ## positive**...: this is a quite regular case where the value and + ## the derivatives are all defined. + + result = op(positive, positive2) + assert not isnan(result.derivatives[positive]) + assert not isnan(result.derivatives[positive2]) + + result = op(positive, zero) + assert result.derivatives[positive] == 0 + assert not isnan(result.derivatives[zero]) + + result = op(positive, negative) + assert not isnan(result.derivatives[positive]) + assert not isnan(result.derivatives[negative]) + + +def power_special_cases(op): + ''' + Checks special cases of the uncertainty power operator op (where + op is typically the built-in pow or uncertainties.umath.pow). + + The values x = 0, x = 1 and x = NaN are special, as are null, + integral and NaN values of p. + ''' + + zero = ufloat(0, 0) + one = ufloat(1, 0) + p = ufloat(0.3, 0.01) + + assert op(0, p) == 0 + assert op(zero, p) == 0 + + # The outcome of 1**nan and nan**0 was undefined before Python + # 2.6 (http://docs.python.org/library/math.html#math.pow): + assert op(float('nan'), zero) == 1.0 + assert op(one, float('nan')) == 1.0 + + # …**0 == 1.0: + assert op(p, 0) == 1.0 + assert op(zero, 0) == 1.0 + assert op((-p), 0) == 1.0 + # …**zero: + assert op((-10.3), zero) == 1.0 + assert op(0, zero) == 1.0 + assert op(0.3, zero) == 1.0 + assert op((-p), zero) == 1.0 + assert op(zero, zero) == 1.0 + assert op(p, zero) == 1.0 + + # one**… == 1.0 + assert op(one, -3) == 1.0 + assert op(one, -3.1) == 1.0 + assert op(one, 0) == 1.0 + assert op(one, 3) == 1.0 + assert op(one, 3.1) == 1.0 + + # … with two numbers with uncertainties: + assert op(one, (-p)) == 1.0 + assert op(one, zero) == 1.0 + assert op(one, p) == 1.0 + # 1**… == 1.0: + assert op(1., (-p)) == 1.0 + assert op(1., zero) == 1.0 + assert op(1., p) == 1.0 + +def power_wrt_ref(op, ref_op): + ''' + Checks special cases of the uncertainty power operator op (where + op is typically the built-in pow or uncertainties.umath.pow), by + comparing its results to the reference power operator ref_op + (which is typically the built-in pow or math.pow). + ''' + + # Negative numbers with uncertainty can be exponentiated to an + # integral power: + assert op(ufloat(-1.1, 0.1), -9).nominal_value == ref_op(-1.1, -9) + + # Case of numbers with no uncertainty: should give the same result + # as numbers with uncertainties: + assert op(ufloat(-1, 0), 9) == ref_op(-1, 9) + assert op(ufloat(-1.1, 0), 9) == ref_op(-1.1, 9) + diff --git a/uncertainties/unumpy/test_ulinalg.py b/tests/test_ulinalg.py similarity index 91% rename from uncertainties/unumpy/test_ulinalg.py rename to tests/test_ulinalg.py index b72a0e11..32604206 100644 --- a/uncertainties/unumpy/test_ulinalg.py +++ b/tests/test_ulinalg.py @@ -1,11 +1,3 @@ -""" -Tests for uncertainties.unumpy.ulinalg. - -These tests can be run through the Nose testing framework. - -(c) 2010-2016 by Eric O. LEBIGOT (EOL) . -""" - # Some tests are already performed in test_unumpy (unumpy contains a # matrix inversion, for instance). They are not repeated here. @@ -18,7 +10,7 @@ sys.exit() # There is no reason to test the interface to NumPy from uncertainties import unumpy, ufloat -from uncertainties.unumpy.test_unumpy import arrays_close +from uncertainties.testing import arrays_close def test_list_inverse(): "Test of the inversion of a square matrix" diff --git a/uncertainties/test_umath.py b/tests/test_umath.py similarity index 91% rename from uncertainties/test_umath.py rename to tests/test_umath.py index 687c2028..52aa4269 100644 --- a/uncertainties/test_umath.py +++ b/tests/test_umath.py @@ -1,25 +1,13 @@ -""" -Tests of the code in uncertainties.umath. - -These tests can be run through the Nose testing framework. - -(c) 2010-2016 by Eric O. LEBIGOT (EOL). -""" - -from __future__ import division -from __future__ import absolute_import - -# Standard modules import sys import math +from math import isnan, isinf -# Local modules: from uncertainties import ufloat import uncertainties.core as uncert_core import uncertainties.umath_core as umath_core -from . import test_uncertainties - +from uncertainties.testing import compare_derivatives, numbers_close +from helpers import power_special_cases, power_all_cases, power_wrt_ref ############################################################################### # Unit tests @@ -31,13 +19,12 @@ def test_fixed_derivatives_math_funcs(): """ for name in umath_core.many_scalars_to_scalar_funcs: - # print "Checking %s..." % name func = getattr(umath_core, name) # Numerical derivatives of func: the nominal value of func() results # is used as the underlying function: numerical_derivatives = uncert_core.NumericalDerivatives( lambda *args: func(*args)) - test_uncertainties.compare_derivatives(func, numerical_derivatives) + compare_derivatives(func, numerical_derivatives) # Functions that are not in umath_core.many_scalars_to_scalar_funcs: @@ -48,11 +35,11 @@ def frac_part_modf(x): def int_part_modf(x): return umath_core.modf(x)[1] - test_uncertainties.compare_derivatives( + compare_derivatives( frac_part_modf, uncert_core.NumericalDerivatives( lambda x: frac_part_modf(x))) - test_uncertainties.compare_derivatives( + compare_derivatives( int_part_modf, uncert_core.NumericalDerivatives( lambda x: int_part_modf(x))) @@ -64,11 +51,11 @@ def mantissa_frexp(x): def exponent_frexp(x): return umath_core.frexp(x)[1] - test_uncertainties.compare_derivatives( + compare_derivatives( mantissa_frexp, uncert_core.NumericalDerivatives( lambda x: mantissa_frexp(x))) - test_uncertainties.compare_derivatives( + compare_derivatives( exponent_frexp, uncert_core.NumericalDerivatives( lambda x: exponent_frexp(x))) @@ -172,7 +159,7 @@ def monte_carlo_calc(n_samples): # or assert_array_max_ulp. This is relevant for all vectorized # occurrences of numbers_close. - assert numpy.vectorize(test_uncertainties.numbers_close)( + assert numpy.vectorize(numbers_close)( covariances_this_module, covariances_samples, 0.06).all(), ( @@ -183,7 +170,7 @@ def monte_carlo_calc(n_samples): ) # The nominal values must be close: - assert test_uncertainties.numbers_close( + assert numbers_close( nominal_value_this_module, nominal_value_samples, # The scale of the comparison depends on the standard @@ -279,14 +266,14 @@ def test_hypot(): # Derivatives that cannot be calculated simply return NaN, with no # exception being raised, normally: result = umath_core.hypot(x, y) - assert test_uncertainties.isnan(result.derivatives[x]) - assert test_uncertainties.isnan(result.derivatives[y]) + assert isnan(result.derivatives[x]) + assert isnan(result.derivatives[y]) def test_power_all_cases(): ''' Test special cases of umath_core.pow(). ''' - test_uncertainties.power_all_cases(umath_core.pow) + power_all_cases(umath_core.pow) # test_power_special_cases() is similar to # test_uncertainties.py:test_power_special_cases(), but with small @@ -297,7 +284,7 @@ def test_power_special_cases(): Checks special cases of umath_core.pow(). ''' - test_uncertainties.power_special_cases(umath_core.pow) + power_special_cases(umath_core.pow) # We want the same behavior for numbers with uncertainties and for # math.pow() at their nominal values. @@ -341,4 +328,4 @@ def test_power_wrt_ref(): ''' Checks special cases of the umath_core.pow() power operator. ''' - test_uncertainties.power_wrt_ref(umath_core.pow, math.pow) + power_wrt_ref(umath_core.pow, math.pow) \ No newline at end of file diff --git a/uncertainties/test_uncertainties.py b/tests/test_uncertainties.py similarity index 81% rename from uncertainties/test_uncertainties.py rename to tests/test_uncertainties.py index c5ed4ccc..ae739656 100644 --- a/uncertainties/test_uncertainties.py +++ b/tests/test_uncertainties.py @@ -1,230 +1,15 @@ -# coding=utf-8 -""" -Tests of the code in uncertainties/__init__.py. - -These tests can be run through the Nose testing framework. - -(c) 2010-2016 by Eric O. LEBIGOT (EOL). -""" - -from __future__ import division -from __future__ import print_function - - -# Standard modules -from builtins import str -from builtins import zip -from builtins import map -from builtins import range import copy -import weakref import math -from math import isnan, isinf -import random import sys - -# 3rd-party modules -# import nose.tools - -# Local modules +from math import isnan, isinf import uncertainties.core as uncert_core from uncertainties.core import ufloat, AffineScalarFunc, ufloat_fromstr from uncertainties import umath +from uncertainties.testing import numbers_close, ufloats_close, compare_derivatives, arrays_close +from helpers import power_special_cases, power_all_cases, power_wrt_ref -# The following information is useful for making sure that the right -# version of Python is running the tests (for instance with the Travis -# Continuous Integration system): -print("Testing with Python", sys.version) - -############################################################################### - -# Utilities for unit testing - -def numbers_close(x, y, tolerance=1e-6): - """ - Returns True if the given floats are close enough. - - The given tolerance is the relative difference allowed, or the absolute - difference, if one of the numbers is 0. - - NaN is allowed: it is considered close to itself. - """ - - # !!! Python 3.5+ has math.isclose(): maybe it could be used here. - - # Instead of using a try and ZeroDivisionError, we do a test, - # NaN could appear silently: - - if x != 0 and y != 0: - if isinf(x): - return isinf(y) - elif isnan(x): - return isnan(y) - else: - # Symmetric form of the test: - return 2*abs(x-y)/(abs(x)+abs(y)) < tolerance - - else: # Either x or y is zero - return abs(x or y) < tolerance - -def ufloats_close(x, y, tolerance=1e-6): - ''' - Tests if two numbers with uncertainties are close, as random - variables: this is stronger than testing whether their nominal - value and standard deviation are close. - - The tolerance is applied to both the nominal value and the - standard deviation of the difference between the numbers. - ''' - - diff = x-y - return (numbers_close(diff.nominal_value, 0, tolerance) - and numbers_close(diff.std_dev, 0, tolerance)) - -class DerivativesDiffer(Exception): - pass - - -def compare_derivatives(func, numerical_derivatives, - num_args_list=None): - """ - Checks the derivatives of a function 'func' (as returned by the - wrap() wrapper), by comparing them to the - 'numerical_derivatives' functions. - - Raises a DerivativesDiffer exception in case of problem. - - These functions all take the number of arguments listed in - num_args_list. If num_args is None, it is automatically obtained. - - Tests are done on random arguments. - """ - - try: - funcname = func.name - except AttributeError: - funcname = func.__name__ - - # print "Testing", func.__name__ - - if not num_args_list: - - # Detecting automatically the correct number of arguments is not - # always easy (because not all values are allowed, etc.): - - num_args_table = { - 'atanh': [1], - 'log': [1, 2] # Both numbers of arguments are tested - } - if funcname in num_args_table: - num_args_list = num_args_table[funcname] - else: - - num_args_list = [] - - # We loop until we find reasonable function arguments: - # We get the number of arguments by trial and error: - for num_args in range(10): - try: - #! Giving integer arguments is good for preventing - # certain functions from failing even though num_args - # is their correct number of arguments - # (e.g. math.ldexp(x, i), where i must be an integer) - func(*(1,)*num_args) - except TypeError: - pass # Not the right number of arguments - else: # No error - # num_args is a good number of arguments for func: - num_args_list.append(num_args) - - if not num_args_list: - raise Exception("Can't find a reasonable number of arguments" - " for function '%s'." % funcname) - - for num_args in num_args_list: - - # Argument numbers that will have a random integer value: - integer_arg_nums = set() - - if funcname == 'ldexp': - # The second argument must be an integer: - integer_arg_nums.add(1) - - while True: - try: - - # We include negative numbers, for more thorough tests: - args = [] - for arg_num in range(num_args): - if arg_num in integer_arg_nums: - args.append(random.choice(range(-10, 10))) - else: - args.append( - uncert_core.Variable(random.random()*4-2, 0)) - - # 'args', but as scalar values: - args_scalar = [uncert_core.nominal_value(v) - for v in args] - - func_approx = func(*args) - - # Some functions yield simple Python constants, after - # wrapping in wrap(): no test has to be performed. - # Some functions also yield tuples... - if isinstance(func_approx, AffineScalarFunc): - - # We compare all derivatives: - for (arg_num, (arg, numerical_deriv)) in ( - enumerate(zip(args, numerical_derivatives))): - - # Some arguments might not be differentiable: - if isinstance(arg, int): - continue - - fixed_deriv_value = func_approx.derivatives[arg] - - num_deriv_value = numerical_deriv(*args_scalar) - - # This message is useful: the user can see that - # tests are really performed (instead of not being - # performed, silently): - print("Testing derivative #%d of %s at %s" % ( - arg_num, funcname, args_scalar)) - - if not numbers_close(fixed_deriv_value, - num_deriv_value, 1e-4): - - # It is possible that the result is NaN: - if not isnan(func_approx): - raise DerivativesDiffer( - "Derivative #%d of function '%s' may be" - " wrong: at args = %s," - " value obtained = %.16f," - " while numerical approximation = %.16f." - % (arg_num, funcname, args, - fixed_deriv_value, num_deriv_value)) - - except ValueError as err: # Arguments out of range, or of wrong type - # Factorial(real) lands here: - if str(err).startswith('factorial'): - integer_arg_nums = set([0]) - continue # We try with different arguments - # Some arguments might have to be integers, for instance: - except TypeError as err: - if len(integer_arg_nums) == num_args: - raise Exception("Incorrect testing procedure: unable to " - "find correct argument values for %s: %s" - % (funcname, err)) - - # Another argument might be forced to be an integer: - integer_arg_nums.add(random.choice(range(num_args))) - else: - # We have found reasonable arguments, and the test passed: - break - -############################################################################### def test_value_construction(): ''' @@ -250,39 +35,6 @@ def test_value_construction(): assert x.std_dev == 0.14 assert x.tag == 'pi' - ## Comparison with the obsolete tuple form: - - # The following tuple is stored in a variable instead of being - # repeated in the calls below, so that the automatic code update - # does not replace ufloat((3, 0.14)) by ufloat(3, 14): the goal - # here is to make sure that the obsolete form gives the same - # result as the new form. - - representation = (3, 0.14) # Obsolete representation - - x = ufloat(3, 0.14) - x2 = ufloat(representation) # Obsolete - assert x.nominal_value == x2.nominal_value - assert x.std_dev == x2.std_dev - assert x.tag is None - assert x2.tag is None - - # With tag as positional argument: - x = ufloat(3, 0.14, "pi") - x2 = ufloat(representation, "pi") # Obsolete - assert x.nominal_value == x2.nominal_value - assert x.std_dev == x2.std_dev - assert x.tag == 'pi' - assert x2.tag == 'pi' - - # With tag keyword: - x = ufloat(3, 0.14, tag="pi") - x2 = ufloat(representation, tag="pi") # Obsolete - assert x.nominal_value == x2.nominal_value - assert x.std_dev == x2.std_dev - assert x.tag == 'pi' - assert x2.tag == 'pi' - # Negative standard deviations should be caught in a nice way # (with the right exception): try: @@ -290,12 +42,6 @@ def test_value_construction(): except uncert_core.NegativeStdDev: pass - try: - # Obsolete form: - x = ufloat((3, -0.1)) - except uncert_core.NegativeStdDev: - pass - ## Incorrect forms should not raise any deprecation warning, but ## raise an exception: @@ -389,25 +135,6 @@ def test_ufloat_fromstr(): assert numbers_close(num.std_dev, values[1]) assert num.tag == 'test variable' - ## Obsolete forms - - num = ufloat(representation) # Obsolete - assert numbers_close(num.nominal_value, values[0]) - assert numbers_close(num.std_dev, values[1]) - assert num.tag is None - - # Call with a tag list argument: - num = ufloat(representation, 'test variable') # Obsolete - assert numbers_close(num.nominal_value, values[0]) - assert numbers_close(num.std_dev, values[1]) - assert num.tag == 'test variable' - - # Call with a tag keyword argument: - num = ufloat(representation, tag='test variable') # Obsolete - assert numbers_close(num.nominal_value, values[0]) - assert numbers_close(num.std_dev, values[1]) - assert num.tag == 'test variable' - ############################################################################### # Test of correctness of the fixed (usually analytical) derivatives: @@ -442,8 +169,6 @@ def check_op(op, num_args): for op in uncert_core.modified_ops_with_reflection: check_op(op, 2) -# Additional, more complex checks, for use with the nose unit testing -# framework. def test_copy(): "Standard copy module integration" @@ -742,14 +467,6 @@ def test_logic(): assert bool(z) == True assert bool(t) == True # Only infinitseimal neighborhood are used -def test_obsolete(): - 'Tests some obsolete creation of number with uncertainties' - x = ufloat(3, 0.1) - # Obsolete function, protected against automatic modification: - x.set_std_dev.__call__(0.2) # Obsolete - - x_std_dev = x.std_dev - assert x_std_dev() == 0.2 # Obsolete call def test_basic_access_to_data(): "Access to data from Variable and AffineScalarFunc objects." @@ -1303,88 +1020,6 @@ def test_power_all_cases(): power_all_cases(pow) -def power_all_cases(op): - ''' - Checks all cases for the value and derivatives of power-like - operator op (op is typically the built-in pow(), or math.pow()). - - Checks only the details of special results like 0, 1 or NaN). - - Different cases for the value of x**p and its derivatives are - tested by dividing the (x, p) plane with: - - - x < 0, x = 0, x > 0 - - p integer or not, p < 0, p = 0, p > 0 - - (not all combinations are distinct: for instance x > 0 gives - identical formulas for all p). - ''' - - zero = ufloat(0, 0.1) - zero2 = ufloat(0, 0.1) - one = ufloat(1, 0.1) - positive = ufloat(0.3, 0.01) - positive2 = ufloat(0.3, 0.01) - negative = ufloat(-0.3, 0.01) - integer = ufloat(-3, 0) - non_int_larger_than_one = ufloat(3.1, 0.01) - positive_smaller_than_one = ufloat(0.3, 0.01) - - ## negative**integer - - result = op(negative, integer) - assert not isnan(result.derivatives[negative]) - assert isnan(result.derivatives[integer]) - - # Limit cases: - result = op(negative, one) - assert result.derivatives[negative] == 1 - assert isnan(result.derivatives[one]) - - result = op(negative, zero) - assert result.derivatives[negative] == 0 - assert isnan(result.derivatives[zero]) - - ## negative**non-integer - - ## zero**... - - result = op(zero, non_int_larger_than_one) - assert isnan(result.derivatives[zero]) - assert result.derivatives[non_int_larger_than_one] == 0 - - # Special cases: - result = op(zero, one) - assert result.derivatives[zero] == 1 - assert result.derivatives[one] == 0 - - result = op(zero, 2*one) - assert result.derivatives[zero] == 0 - assert result.derivatives[one] == 0 - - result = op(zero, positive_smaller_than_one) - assert isnan(result.derivatives[zero]) - assert result.derivatives[positive_smaller_than_one] == 0 - - result = op(zero, zero2) - assert result.derivatives[zero] == 0 - assert isnan(result.derivatives[zero2]) - - ## positive**...: this is a quite regular case where the value and - ## the derivatives are all defined. - - result = op(positive, positive2) - assert not isnan(result.derivatives[positive]) - assert not isnan(result.derivatives[positive2]) - - result = op(positive, zero) - assert result.derivatives[positive] == 0 - assert not isnan(result.derivatives[zero]) - - result = op(positive, negative) - assert not isnan(result.derivatives[positive]) - assert not isnan(result.derivatives[negative]) - ############################################################################### @@ -1428,79 +1063,12 @@ def test_power_special_cases(): else: raise Exception('A proper exception should have been raised') -def power_special_cases(op): - ''' - Checks special cases of the uncertainty power operator op (where - op is typically the built-in pow or uncertainties.umath.pow). - - The values x = 0, x = 1 and x = NaN are special, as are null, - integral and NaN values of p. - ''' - - zero = ufloat(0, 0) - one = ufloat(1, 0) - p = ufloat(0.3, 0.01) - - assert op(0, p) == 0 - assert op(zero, p) == 0 - - # The outcome of 1**nan and nan**0 was undefined before Python - # 2.6 (http://docs.python.org/library/math.html#math.pow): - assert op(float('nan'), zero) == 1.0 - assert op(one, float('nan')) == 1.0 - - # …**0 == 1.0: - assert op(p, 0) == 1.0 - assert op(zero, 0) == 1.0 - assert op((-p), 0) == 1.0 - # …**zero: - assert op((-10.3), zero) == 1.0 - assert op(0, zero) == 1.0 - assert op(0.3, zero) == 1.0 - assert op((-p), zero) == 1.0 - assert op(zero, zero) == 1.0 - assert op(p, zero) == 1.0 - - # one**… == 1.0 - assert op(one, -3) == 1.0 - assert op(one, -3.1) == 1.0 - assert op(one, 0) == 1.0 - assert op(one, 3) == 1.0 - assert op(one, 3.1) == 1.0 - - # … with two numbers with uncertainties: - assert op(one, (-p)) == 1.0 - assert op(one, zero) == 1.0 - assert op(one, p) == 1.0 - # 1**… == 1.0: - assert op(1., (-p)) == 1.0 - assert op(1., zero) == 1.0 - assert op(1., p) == 1.0 - - def test_power_wrt_ref(): ''' Checks special cases of the built-in pow() power operator. ''' power_wrt_ref(pow, pow) -def power_wrt_ref(op, ref_op): - ''' - Checks special cases of the uncertainty power operator op (where - op is typically the built-in pow or uncertainties.umath.pow), by - comparing its results to the reference power operator ref_op - (which is typically the built-in pow or math.pow). - ''' - - # Negative numbers with uncertainty can be exponentiated to an - # integral power: - assert op(ufloat(-1.1, 0.1), -9).nominal_value == ref_op(-1.1, -9) - - # Case of numbers with no uncertainty: should give the same result - # as numbers with uncertainties: - assert op(ufloat(-1, 0), 9) == ref_op(-1, 9) - assert op(ufloat(-1.1, 0), 9) == ref_op(-1.1, 9) - ############################################################################### @@ -2163,43 +1731,6 @@ def test_custom_pretty_print_and_latex(): pass else: - def arrays_close(m1, m2, precision=1e-4): - """ - Returns True iff m1 and m2 are almost equal, where elements - can be either floats or AffineScalarFunc objects. - - Two independent AffineScalarFunc objects are deemed equal if - both their nominal value and uncertainty are equal (up to the - given precision). - - m1, m2 -- NumPy arrays. - - precision -- precision passed through to - uncertainties.test_uncertainties.numbers_close(). - """ - - # ! numpy.allclose() is similar to this function, but does not - # work on arrays that contain numbers with uncertainties, because - # of the isinf() function. - - for (elmt1, elmt2) in zip(m1.flat, m2.flat): - - # For a simpler comparison, both elements are - # converted to AffineScalarFunc objects: - elmt1 = uncert_core.to_affine_scalar(elmt1) - elmt2 = uncert_core.to_affine_scalar(elmt2) - - if not numbers_close(elmt1.nominal_value, - elmt2.nominal_value, precision): - return False - - if not numbers_close(elmt1.std_dev, - elmt2.std_dev, precision): - return False - - return True - - def test_numpy_comparison(): "Comparison with a NumPy array." diff --git a/uncertainties/unumpy/test_unumpy.py b/tests/test_unumpy.py similarity index 89% rename from uncertainties/unumpy/test_unumpy.py rename to tests/test_unumpy.py index 5923dfd3..d487cdc1 100644 --- a/uncertainties/unumpy/test_unumpy.py +++ b/tests/test_unumpy.py @@ -1,26 +1,14 @@ -""" -Tests of the code in uncertainties/unumpy/__init__.py. - -These tests can be run through the Nose testing framework. - -(c) 2010-2016 by Eric O. LEBIGOT (EOL). -""" - -from __future__ import division - -# 3rd-party modules: try: import numpy except ImportError: import sys sys.exit() # There is no reason to test the interface to NumPy -# Local modules: import uncertainties import uncertainties.core as uncert_core -from uncertainties import ufloat, unumpy, test_uncertainties +from uncertainties import ufloat, unumpy from uncertainties.unumpy import core -from uncertainties.test_uncertainties import numbers_close, arrays_close +from uncertainties.testing import numbers_close, arrays_close def test_numpy(): @@ -309,23 +297,4 @@ def test_array_comparisons(): # For matrices, 1D arrays are converted to 2D arrays: mat = unumpy.umatrix([1, 2], [1, 4]) - assert numpy.all((mat == [mat[0,0], 4]) == [True, False]) - -def test_obsolete(): - 'Test of obsolete functions' - - # The new and old calls should give the same results: - - # The unusual syntax is here to protect against automatic code - # update: - arr_obs = unumpy.uarray.__call__(([1, 2], [1, 4])) # Obsolete call - arr = unumpy.uarray([1, 2], [1, 4]) - assert arrays_close(arr_obs, arr) - - # The new and old calls should give the same results: - - # The unusual syntax is here to protect against automatic code - # update: - mat_obs = unumpy.umatrix.__call__(([1, 2], [1, 4])) # Obsolete call - mat = unumpy.umatrix([1, 2], [1, 4]) - assert arrays_close(mat_obs, mat) + assert numpy.all((mat == [mat[0,0], 4]) == [True, False]) \ No newline at end of file diff --git a/uncertainties/1to2.py b/uncertainties/1to2.py deleted file mode 100755 index 66db72ef..00000000 --- a/uncertainties/1to2.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python - -''' -Fixes code like the 2to3 Python utility, but with fixers from the local fixes -directory. - -(c) 2013 by Eric O. LEBIGOT (EOL). -''' - -# Code inspired by the 2to3 Python code. - -import sys - -if sys.version_info < (2, 6): - sys.exit("Please run this program with Python 2.6+.") - -import lib2to3.main - -sys.exit(lib2to3.main.main('uncertainties.lib1to2.fixes')) diff --git a/uncertainties/core.py b/uncertainties/core.py index 8ed7d433..a5870dfe 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -98,25 +98,6 @@ def set_doc_string(func): CONSTANT_TYPES = FLOAT_LIKE_TYPES+(complex,) ############################################################################### - -# Utility for issuing deprecation warnings - -def deprecation(message): - ''' - Warn the user with the given message, by issuing a - DeprecationWarning. - ''' - - # stacklevel = 3 points to the original user call (not to the - # function from this module that called deprecation()). - # DeprecationWarning is ignored by default: not used. - - warnings.warn('Obsolete: %s Code can be automatically updated with' - ' python -m uncertainties.1to2 -w ProgramDirectory.' - % message, stacklevel=3) - -############################################################################### - ## Definitions that depend on the availability of NumPy: @@ -934,21 +915,6 @@ def PDG_precision(std_dev): # used instead of the % formatting operator, if available: robust_format = format -class CallableStdDev(float): - ''' - Class for standard deviation results, which used to be - callable. Provided for compatibility with old code. Issues an - obsolescence warning upon call. - ''' - - # This class is a float. It must be set to the standard deviation - # upon construction. - - def __call__ (self): - deprecation('the std_dev attribute should not be called' - ' anymore: use .std_dev instead of .std_dev().') - return self - # Exponent letter: the keys are the possible main_fmt_type values of # format_num(): EXP_LETTERS = {'f': 'e', 'F': 'E'} @@ -1843,7 +1809,7 @@ def std_dev(self): #std_dev value (in fact, many intermediate AffineScalarFunc do #not need to have their std_dev calculated: only the final #AffineScalarFunc returned to the user does). - return CallableStdDev(sqrt(sum( + return float(sqrt(sum( delta**2 for delta in self.error_components().values()))) # Abbreviation (for formulas, etc.): @@ -2798,13 +2764,7 @@ def std_dev(self, std_dev): if std_dev < 0 and not isinfinite(std_dev): raise NegativeStdDev("The standard deviation cannot be negative") - self._std_dev = CallableStdDev(std_dev) - - # Support for legacy method: - def set_std_dev(self, value): # Obsolete - deprecation('instead of set_std_dev(), please use' - ' .std_dev = ...') - self.std_dev = value + self._std_dev = float(std_dev) # The following method is overridden so that we can represent the tag: def __repr__(self): @@ -3263,21 +3223,9 @@ def ufloat(nominal_value, std_dev=None, tag=None): """ Return a new random variable (Variable object). - The only non-obsolete use is: - - ufloat(nominal_value, std_dev), - ufloat(nominal_value, std_dev, tag=...). - Other input parameters are temporarily supported: - - - ufloat((nominal_value, std_dev)), - - ufloat((nominal_value, std_dev), tag), - - ufloat(str_representation), - - ufloat(str_representation, tag). - - Valid string representations str_representation are listed in - the documentation for ufloat_fromstr(). - nominal_value -- nominal value of the random variable. It is more meaningful to use a value close to the central value or to the mean. This value is propagated by mathematical operations as if it @@ -3292,28 +3240,4 @@ def ufloat(nominal_value, std_dev=None, tag=None): error_components() method). """ - try: - # Standard case: - return Variable(nominal_value, std_dev, tag=tag) - # Exception types raised by, respectively: tuple or string that - # can be converted through float() (case of a number with no - # uncertainty), and string that cannot be converted through - # float(): - except (TypeError, ValueError): - - if tag is not None: - tag_arg = tag # tag keyword used: - else: - tag_arg = std_dev # 2 positional arguments form - - try: - final_ufloat = ufloat_obsolete(nominal_value, tag_arg) - except: # The input is incorrect, not obsolete - raise - else: - # Obsolete, two-argument call: - deprecation( - 'either use ufloat(nominal_value, std_dev),' - ' ufloat(nominal_value, std_dev, tag), or the' - ' ufloat_fromstr() function, for string representations.') - return final_ufloat + return Variable(nominal_value, std_dev, tag=tag) \ No newline at end of file diff --git a/uncertainties/lib1to2/__init__.py b/uncertainties/lib1to2/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uncertainties/lib1to2/fixes/__init__.py b/uncertainties/lib1to2/fixes/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uncertainties/lib1to2/fixes/fix_std_dev.py b/uncertainties/lib1to2/fixes/fix_std_dev.py deleted file mode 100644 index fd15912b..00000000 --- a/uncertainties/lib1to2/fixes/fix_std_dev.py +++ /dev/null @@ -1,38 +0,0 @@ -''' -Fixer for lib2to3. - -Transforms .std_dev() calls into .std_dev attribute access. - -(c) 2013 by Eric O. LEBIGOT. -''' - -from lib2to3.fixer_base import BaseFix -from lib2to3.fixer_util import Name, Assign - -class FixStdDev(BaseFix): - - PATTERN = """ - power< any* trailer< '.' 'std_dev' > trailer< '(' ')' > > - | - power< any* trailer< '.' 'set_std_dev' > trailer< '(' set_arg=any ')' > > - """ - - def transform(self, node, results): - - if 'set_arg' in results: # Case of .set_std_dev() - - # set_std_dev => std_dev - attribute = node.children[-2] # .set_std_dev - attribute.children[1].replace(Name('std_dev')) - - # Call "(arg)": removed - node.children[-1].remove() - - # Replacement by an assignment: - node.replace(Assign(node.clone(), results['set_arg'].clone())) - - else: - # '.std_dev' is followed by a call with no argument: the call - # is removed: - node.children[-1].remove() - diff --git a/uncertainties/lib1to2/fixes/fix_std_devs.py b/uncertainties/lib1to2/fixes/fix_std_devs.py deleted file mode 100644 index 31afaf7e..00000000 --- a/uncertainties/lib1to2/fixes/fix_std_devs.py +++ /dev/null @@ -1,22 +0,0 @@ -''' -Fixer for lib2to3. - -Transform .std_devs() calls into .std_devs attribute access. - -(c) 2016 by Eric O. LEBIGOT. -''' - -from lib2to3.fixer_base import BaseFix -from lib2to3.fixer_util import Name, Assign - -class FixStdDevs(BaseFix): - - PATTERN = """ - power< any* trailer< '.' 'std_devs' > trailer< '(' ')' > > - """ - - def transform(self, node, results): - - # '.std_dev' is followed by a call with no argument: the call - # is removed: - node.children[-1].remove() diff --git a/uncertainties/lib1to2/fixes/fix_uarray_umatrix.py b/uncertainties/lib1to2/fixes/fix_uarray_umatrix.py deleted file mode 100644 index a47284ad..00000000 --- a/uncertainties/lib1to2/fixes/fix_uarray_umatrix.py +++ /dev/null @@ -1,80 +0,0 @@ -''' -Fixer for lib2to3. - -Transforms uarray(tuple) into uarray(nominal_values, std_devs) and -uarray(single_arg) into uarray(*single_arg). - -(c) 2013 by Eric O. LEBIGOT (EOL). -''' - -from lib2to3.fixer_base import BaseFix -from lib2to3.fixer_util import String, ArgList, Comma, syms - -############################################################################### -# lib2to3 grammar parts. - -#! Warning: indentation is meaningful! - -# (tuple): -tuple_call = """ - trailer< '(' - atom< '(' testlist_gexp< arg0=any ',' arg1=any > ')' > - ')' >""" - - -############################################################################### - -class FixUarrayUmatrix(BaseFix): - - # Non dotted access, then dotted access. - # Tuple call, then single-argument call - PATTERN = """ - power< 'uarray' {tuple_call} any* > - | - power< object=NAME trailer< '.' 'uarray' > {tuple_call} any* > - | - power< 'uarray' trailer< '(' args=any ')' > any* > - | - power< object=NAME trailer< '.' 'uarray' > - trailer< '(' args=any ')' > - any* > - """.format(tuple_call=tuple_call) - - # Same pattern, for umatrix(): - PATTERN = '{}|{}'.format(PATTERN, PATTERN.replace('uarray', 'umatrix')) - - def transform(self, node, results): - - if 'object' in results: # If dotted access: unc.uarray() - args = node.children[2] - else: - args = node.children[1] - - if 'args' in results: # Non-tuple argument - - # A star will be inserted in from of the single argument: - - # ! The following keeps spaces in front of the argument, - # if any (but this is safer than adding forcefully a star - # in front of the value of the argument: the argument can - # be a name (where it works), but also anything else, - # including a lib2to3.pytree.Node that has no value.) This - # is OK, as the syntax f(* (2, 1)) is valid. - - args_node = results['args'] - - # We must make sure that there is a single argument: - if args_node.type == syms.arglist: - return # Nothing modified - - # Single argument (in position 1): - new_args = [String('*'), args.children[1].clone()] - - else: # Tuple argument - - # New arguments: - new_args = [results['arg0'].clone(), - Comma(), results['arg1'].clone()] - - # Argument list update: - args.replace(ArgList(new_args)) diff --git a/uncertainties/lib1to2/fixes/fix_ufloat.py b/uncertainties/lib1to2/fixes/fix_ufloat.py deleted file mode 100644 index a6e92f7f..00000000 --- a/uncertainties/lib1to2/fixes/fix_ufloat.py +++ /dev/null @@ -1,105 +0,0 @@ -''' -Fixer for lib2to3. - -Transforms ufloat(tuple,...) and ufloat(string,...) into -ufloat(nominal_value, std_dev,...) and ufloat_fromstr - -(c) 2013 by Eric O. LEBIGOT. -''' - -from lib2to3.fixer_base import BaseFix -from lib2to3.fixer_util import ArgList, Call, Comma, Name, syms - -############################################################################### -# lib2to3 grammar parts. - -#! Warning: indentation is meaningful! - -# (tuple): -tuple_call = """ - trailer< '(' - atom< '(' testlist_gexp< arg0=any ',' arg1=any > ')' > - ')' >""" - -# (tuple, any): -tuple_any_call = """ - trailer< '(' - arglist< - atom< '(' testlist_gexp< arg0=any ',' arg1=any > ')' > - ',' tag=any - > - ')' >""" - - -############################################################################### - -class FixUfloat(BaseFix): - - # Non dotted access, then dotted access. - # Tuple call, then string call. - # No-tag call, then tag call. - PATTERN = """ - power< 'ufloat' {tuple_call} any* > - | - power< 'ufloat' {tuple_any_call} any* > - | - power< 'ufloat' trailer< '(' string=STRING ')' > any* > - | - power< 'ufloat' trailer< '(' - arglist< - string=STRING - ',' tag=any - > - ')' > any* > - | - power< object=NAME trailer< '.' 'ufloat' > {tuple_call} any* > - | - power< object=NAME trailer< '.' 'ufloat' > {tuple_any_call} any* > - | - power< object=NAME trailer< '.' 'ufloat' > - trailer< '(' string=STRING ')' > - any* > - | - power< object=NAME trailer< '.' 'ufloat' > - trailer< '(' arglist< string=STRING ',' tag=any > ')' > - any* > - """.format(tuple_call=tuple_call, - tuple_any_call=tuple_any_call) - - - def transform(self, node, results): - - # Handling of the first argument: - - if 'string' in results: # String as first argument - - new_func_name = 'ufloat_fromstr' - - # New arguments: - new_args=[results['string'].clone()] - - else: # Tuple as first argument - - new_func_name = 'ufloat' - - # New arguments: - new_args = [results['arg0'].clone(), - Comma(), results['arg1'].clone()] - - # Handling of the second argument (call with a tag): - if 'tag' in results: - new_args.extend([Comma(), results['tag'].clone()]) - - if 'object' in results: # If dotted access: unc.ufloat() - func_name = node.children[1].children[1] - args = node.children[2] - else: - func_name = node.children[0] - args = node.children[1] - - # Function name update: - func_name.value = new_func_name - #! func_name.changed() # Necessary when only .value is changed - - # Argument list update: - args.replace(ArgList(new_args)) diff --git a/uncertainties/lib1to2/test_1to2.py b/uncertainties/lib1to2/test_1to2.py deleted file mode 100644 index f23f8cc4..00000000 --- a/uncertainties/lib1to2/test_1to2.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python - -''' -Unit tests for the uncertainties.lib1to2 code update package. - -Meant to be run through nosetests. - -(c) 2013-2020 by Eric O. LEBIGOT (EOL). -''' - -# Code inspired by: -# -# - lib2to3.tests.test_fixers.py - -from builtins import str -import sys -import os - -# !! Would it be possible to use an import hook so as to stop the -# import if the Python version is not high enough, instead of having -# like here a whole indented block? - - -if sys.version_info < (2, 7) or "TRAVIS" in os.environ or "APPVEYOR" in os.environ: - - # This package uses lib2to3, which requires Python 2.6+. - - # lib2to3.tests.support is missing from 2.7.3 Travis Python packages. - - # !! Nosetests for Python 2.6 also fails (it looks like it tries - # to run tests via lib2to3/tests/test_refactor.py): - - pass - -else: - - import os - try: - # lib2to3 test support seems to have moved to a new place in 2013: - import test.test_lib2to3.support as support - except ImportError: - # Pre-~2013 path for lib2to3 test support - import lib2to3.tests.support as support - - # The lib1to2.fixes package given to lib2to3 is the *local* package - # (not to another installed module). This is important for the - # __import__() used via support.get_refactorer(). - sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) - - def check_refactor(refactorer, source, expected): - """ - Raises an AssertionError if the given - lib2to3.refactor.RefactoringTool does not refactor 'source' into - 'expected'. - - source, expected -- strings (typically with Python code). - """ - - # !! str() is from future's builtins and is only needed for Python 2, - # where it is mostly equivalent to unicode(): - new = str( - refactorer.refactor_string(support.reformat(source), '')) - - assert support.reformat(expected) == new, ( - "Refactoring failed: '{}' => '{}' instead of '{}'".format( - source, new.strip(), expected)) - - # print 'Checked:', source, '=>', expected - - def check_all(fixer, tests): - ''' - Takes a fixer name (module from fixes) and a mapping that maps - code using the obsolete syntax into updated code, and checks - whether the code is correctly updated. - ''' - - refactorer = support.get_refactorer( - fixer_pkg='lib1to2', fixers=[fixer]) - - for (input_str, out_str) in tests.items(): - check_refactor(refactorer, input_str, out_str) - - def test_fix_std_dev(): - 'Tests the transformation of std_dev() into std_dev.' - - - tests = { - 'x.std_dev()': 'x.std_dev', - 'y.std_dev(); unc.std_dev(z)': 'y.std_dev; unc.std_dev(z)', - 'uncertainties.std_dev(x)': 'uncertainties.std_dev(x)', - 'std_dev(x)': 'std_dev(x)', - 'obj.x.std_dev()': 'obj.x.std_dev', - - """ - long_name.std_dev( - # No argument! - )""": - """ - long_name.std_dev""", - - # set_std_dev => .std_dev: - 'x.set_std_dev(3)': 'x.std_dev = 3', - 'y = set_std_dev(3)': 'y = set_std_dev(3)', # None - 'func = x.set_std_dev': 'func = x.set_std_dev', - 'obj.x.set_std_dev(sin(y))': 'obj.x.std_dev = sin(y)' - } - - check_all('std_dev', tests) - - def test_ufloat(): - ''' - Test of the transformation of ufloat(tuple,...) and - ufloat(string,...) into ufloat(nominal_value, std_dev, tag=...). - ''' - - tests = { - # Tuples: - 'ufloat((3, 0.14))': 'ufloat(3, 0.14)', - 'ufloat((3, 0.14), "pi")': 'ufloat(3, 0.14, "pi")', - "ufloat((3, 0.14), 'pi')": "ufloat(3, 0.14, 'pi')", - "x = ufloat((3, 0.14), tag='pi')": "x = ufloat(3, 0.14, tag='pi')", - - # Simple expressions that can be transformed: - 'ufloat((n, s), tag="var")': 'ufloat(n, s, tag="var")', - - # Simple expressions that cannot be transformed automatically: - 'ufloat(str_repr, tag="var")': 'ufloat(str_repr, tag="var")', - 'ufloat(*tuple_repr, tag="var")': 'ufloat(*tuple_repr, tag="var")', - 'ufloat(*t[0, 0])': 'ufloat(*t[0, 0])', - - # Strings: - 'ufloat("-1.23(3.4)")': 'ufloat_fromstr("-1.23(3.4)")', - "ufloat('-1.23(3.4)')": "ufloat_fromstr('-1.23(3.4)')", - 'ufloat("-1.23(3.4)", "var")': - 'ufloat_fromstr("-1.23(3.4)", "var")', - 'ufloat("-1.23(3.4)", tag="var")': - 'ufloat_fromstr("-1.23(3.4)", tag="var")' - - } - - # Automatic addition of a dotted access: - tests.update(dict( - # !! Dictionary comprehension usable with Python 2.7+ - (orig.replace('ufloat', 'unc.ufloat'), - new.replace('ufloat', 'unc.ufloat')) - for (orig, new) in tests.items())) - - # Test for space consistency: - tests[' t = u.ufloat("3")'] = ' t = u.ufloat_fromstr("3")' - - # Exponentiation test: - tests.update(dict( - # !! Dictionary comprehension usable with Python 2.7+ - (orig+'**2', new+'**2') - for (orig, new) in tests.items())) - - # Exponent test: - tests['2**ufloat("3")'] = '2**ufloat_fromstr("3")' - - # Opposite test: - tests['-ufloat("3")'] = '-ufloat_fromstr("3")' - - check_all('ufloat', tests) - - def test_uarray_umatrix(): - ''' - Test of the transformation of uarray(tuple,...) into - uarray(nominal_values, std_devs). Also performs the same tests - on umatrix(). - ''' - - tests = { - 'uarray((arange(3), std_devs))': 'uarray(arange(3), std_devs)', - 'uarray(tuple_arg)': 'uarray(*tuple_arg)', - # Unmodified, correct code: - 'uarray(values, std_devs)': 'uarray(values, std_devs)', - # Spaces tests: - 'uarray( ( arange(3), std_devs ) ) ': - 'uarray( arange(3), std_devs) ', - 'uarray( tuple_arg )': 'uarray(* tuple_arg)' - - } - - # Automatic addition of a dotted access: - tests.update(dict( - # !! Dictionary comprehension usable with Python 2.7+ - (orig.replace('uarray', 'un.uarray'), - new.replace('uarray', 'un.uarray')) - for (orig, new) in tests.items())) - - # Exponentiation test: - tests.update(dict( - # !! Dictionary comprehension usable with Python 2.7+ - (orig+'**2', new+'**2') - for (orig, new) in tests.items())) - - # Test for space consistency: - tests[' t = u.uarray(args)'] = ' t = u.uarray(*args)' - - # Same tests, but for umatrix: - tests.update(dict( - (orig.replace('uarray', 'umatrix'), - new.replace('uarray', 'umatrix')) - for (orig, new) in tests.items())) - - check_all('uarray_umatrix', tests) - diff --git a/uncertainties/testing.py b/uncertainties/testing.py new file mode 100644 index 00000000..66c9236d --- /dev/null +++ b/uncertainties/testing.py @@ -0,0 +1,237 @@ +import weakref + +import random +from math import isnan, isinf +from uncertainties.core import ufloat, AffineScalarFunc, ufloat_fromstr + +import uncertainties.core as uncert_core + +############################################################################### + +# Utilities for unit testing + +def numbers_close(x, y, tolerance=1e-6): + """ + Returns True if the given floats are close enough. + + The given tolerance is the relative difference allowed, or the absolute + difference, if one of the numbers is 0. + + NaN is allowed: it is considered close to itself. + """ + + # !!! Python 3.5+ has math.isclose(): maybe it could be used here. + + # Instead of using a try and ZeroDivisionError, we do a test, + # NaN could appear silently: + + if x != 0 and y != 0: + if isinf(x): + return isinf(y) + elif isnan(x): + return isnan(y) + else: + # Symmetric form of the test: + return 2*abs(x-y)/(abs(x)+abs(y)) < tolerance + + else: # Either x or y is zero + return abs(x or y) < tolerance + +def ufloats_close(x, y, tolerance=1e-6): + ''' + Tests if two numbers with uncertainties are close, as random + variables: this is stronger than testing whether their nominal + value and standard deviation are close. + + The tolerance is applied to both the nominal value and the + standard deviation of the difference between the numbers. + ''' + + diff = x-y + return (numbers_close(diff.nominal_value, 0, tolerance) + and numbers_close(diff.std_dev, 0, tolerance)) + +class DerivativesDiffer(Exception): + pass + + +def compare_derivatives(func, numerical_derivatives, + num_args_list=None): + """ + Checks the derivatives of a function 'func' (as returned by the + wrap() wrapper), by comparing them to the + 'numerical_derivatives' functions. + + Raises a DerivativesDiffer exception in case of problem. + + These functions all take the number of arguments listed in + num_args_list. If num_args is None, it is automatically obtained. + + Tests are done on random arguments. + """ + + try: + funcname = func.name + except AttributeError: + funcname = func.__name__ + + # print "Testing", func.__name__ + + if not num_args_list: + + # Detecting automatically the correct number of arguments is not + # always easy (because not all values are allowed, etc.): + + num_args_table = { + 'atanh': [1], + 'log': [1, 2] # Both numbers of arguments are tested + } + if funcname in num_args_table: + num_args_list = num_args_table[funcname] + else: + + num_args_list = [] + + # We loop until we find reasonable function arguments: + # We get the number of arguments by trial and error: + for num_args in range(10): + try: + #! Giving integer arguments is good for preventing + # certain functions from failing even though num_args + # is their correct number of arguments + # (e.g. math.ldexp(x, i), where i must be an integer) + func(*(1,)*num_args) + except TypeError: + pass # Not the right number of arguments + else: # No error + # num_args is a good number of arguments for func: + num_args_list.append(num_args) + + if not num_args_list: + raise Exception("Can't find a reasonable number of arguments" + " for function '%s'." % funcname) + + for num_args in num_args_list: + + # Argument numbers that will have a random integer value: + integer_arg_nums = set() + + if funcname == 'ldexp': + # The second argument must be an integer: + integer_arg_nums.add(1) + + while True: + try: + + # We include negative numbers, for more thorough tests: + args = [] + for arg_num in range(num_args): + if arg_num in integer_arg_nums: + args.append(random.choice(range(-10, 10))) + else: + args.append( + uncert_core.Variable(random.random()*4-2, 0)) + + # 'args', but as scalar values: + args_scalar = [uncert_core.nominal_value(v) + for v in args] + + func_approx = func(*args) + + # Some functions yield simple Python constants, after + # wrapping in wrap(): no test has to be performed. + # Some functions also yield tuples... + if isinstance(func_approx, AffineScalarFunc): + + # We compare all derivatives: + for (arg_num, (arg, numerical_deriv)) in ( + enumerate(zip(args, numerical_derivatives))): + + # Some arguments might not be differentiable: + if isinstance(arg, int): + continue + + fixed_deriv_value = func_approx.derivatives[arg] + + num_deriv_value = numerical_deriv(*args_scalar) + + # This message is useful: the user can see that + # tests are really performed (instead of not being + # performed, silently): + print("Testing derivative #%d of %s at %s" % ( + arg_num, funcname, args_scalar)) + + if not numbers_close(fixed_deriv_value, + num_deriv_value, 1e-4): + + # It is possible that the result is NaN: + if not isnan(func_approx): + raise DerivativesDiffer( + "Derivative #%d of function '%s' may be" + " wrong: at args = %s," + " value obtained = %.16f," + " while numerical approximation = %.16f." + % (arg_num, funcname, args, + fixed_deriv_value, num_deriv_value)) + + except ValueError as err: # Arguments out of range, or of wrong type + # Factorial(real) lands here: + if str(err).startswith('factorial'): + integer_arg_nums = set([0]) + continue # We try with different arguments + # Some arguments might have to be integers, for instance: + except TypeError as err: + if len(integer_arg_nums) == num_args: + raise Exception("Incorrect testing procedure: unable to " + "find correct argument values for %s: %s" + % (funcname, err)) + + # Another argument might be forced to be an integer: + integer_arg_nums.add(random.choice(range(num_args))) + else: + # We have found reasonable arguments, and the test passed: + break + +############################################################################### + +try: + import numpy +except ImportError: + pass +else: + + def arrays_close(m1, m2, precision=1e-4): + """ + Returns True iff m1 and m2 are almost equal, where elements + can be either floats or AffineScalarFunc objects. + + Two independent AffineScalarFunc objects are deemed equal if + both their nominal value and uncertainty are equal (up to the + given precision). + + m1, m2 -- NumPy arrays. + + precision -- precision passed through to + uncertainties.test_uncertainties.numbers_close(). + """ + + # ! numpy.allclose() is similar to this function, but does not + # work on arrays that contain numbers with uncertainties, because + # of the isinf() function. + + for (elmt1, elmt2) in zip(m1.flat, m2.flat): + + # For a simpler comparison, both elements are + # converted to AffineScalarFunc objects: + elmt1 = uncert_core.to_affine_scalar(elmt1) + elmt2 = uncert_core.to_affine_scalar(elmt2) + + if not numbers_close(elmt1.nominal_value, + elmt2.nominal_value, precision): + return False + + if not numbers_close(elmt1.std_dev, + elmt2.std_dev, precision): + return False + + return True diff --git a/uncertainties/unumpy/core.py b/uncertainties/unumpy/core.py index 3507db4d..54addf06 100644 --- a/uncertainties/unumpy/core.py +++ b/uncertainties/unumpy/core.py @@ -25,7 +25,6 @@ # Local modules: import uncertainties.umath_core as umath_core import uncertainties.core as uncert_core -from uncertainties.core import deprecation __all__ = [ # Factory functions: @@ -285,8 +284,7 @@ def uarray(nominal_values, std_devs=None): """ if std_devs is None: # Obsolete, single tuple argument call - deprecation('uarray() should now be called with two arguments.') - (nominal_values, std_devs) = nominal_values + raise TypeError('uarray() should be called with two arguments.') return (numpy.vectorize( # ! Looking up uncert_core.Variable beforehand through @@ -573,28 +571,6 @@ def pinv(array_like, rcond=pinv_default): ########## Matrix class -class CallableStdDevs(numpy.matrix): - ''' - Class for standard deviation results, which used to be - callable. Provided for compatibility with old code. Issues an - obsolescence warning upon call. - - New objects must be created by passing an existing - ''' - - def __new__(cls, matrix): - # The following prevents a copy of the original matrix, which - # could be expensive, and is unnecessary (the CallableStdDevs - # is just a wrapping around the original matrix, which can be - # modified): - matrix.__class__ = cls - return matrix - - def __call__ (self): - deprecation('the std_devs attribute should not be called' - ' anymore: use .std_devs instead of .std_devs().') - return self - class matrix(numpy.matrix): # The name of this class is the same as NumPy's, which is why it # does not follow PEP 8. @@ -639,7 +615,7 @@ def nominal_values(self): # the first ones to have such methods? @property def std_devs(self): - return CallableStdDevs(std_devs(self)) + return numpy.matrix(std_devs(self)) def umatrix(nominal_values, std_devs=None): """ @@ -653,8 +629,7 @@ def umatrix(nominal_values, std_devs=None): """ if std_devs is None: # Obsolete, single tuple argument call - deprecation('umatrix() should now be called with two arguments.') - (nominal_values, std_devs) = nominal_values + raise TypeError('umatrix() should be called with two arguments.') return uarray(nominal_values, std_devs).view(matrix)