Welcome, guest | Sign In | My Account | Store | Cart

Do not use this module, but use instead the more powerful uncertainties.py module.

Module for performing calculations with error propagation, such as (1 +- 0.1) * 2 = 2 +- 0.2. Mathematical operations (addition, etc.), operations defined in the math module (sin, atan,...) and logical operations (<, >, etc.) can be used.

Correlations between parts of an expression are correctly taken into account (for instance, the error on "x-x" is strictly zero).

Code written for floats should directly work with the numbers with uncertainty defined here, without much need for modifications.

The module also contains a class that represents non-evaluated mathematical expressions. This class is used for performing the differentiation required by the error propagation calculation, but can be used on its own, for manipulating "semi-formal" expressions whose variables can be accessed.

Python, 320 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
#========================= uncertainties.py =========================

"""
Calculations with uncertainties (and correlations): class Number_with_uncert.

Uncertainties are treated like standard deviations.  Valid operations
include basic mathematical functions (addition,...), as well as operations
from the math module.  Logical operations (>, <, etc.) are also supported,
but the calculated error is generally meaningless.

Applying operations on Number_with_uncert creates 'semi-formal'
expressions (see below), represented by the Semi_formal_expr class.

The method used for managing uncertainties consists in replacing
numbers by a kind of mathematical expressions (of type
Semi_formal_expr) that can be calculated for different values of the
variables on which they depend--even if the dependence is not in the
form of an explicit Python function call with some parameters.  These
expressions are 'semi-formal' in the sense that all their parameters
are bound to a value at all times, but they can nonetheless be
recalculated for different values of these parameters.  In particular,
these semi-formal expressions always have a value.

Example:
>>> x = Number_with_uncert(3.14, 0.01)
>>> y = 2*x
>>> print x-x
0.0
>>> print y-x  # The error should be exactly the error on x
3.14 +- 0.010000000000
>>> x.nominal_value = 1
>>> print y  # 'y' is updated
2.0 +- 0.0200000000001

(c) Eric O. LEBIGOT (EOL), 2009.

Strongly inspired by code by Arnaud Delobelle
(http://groups.google.com/group/comp.lang.python/msg/b92987c7787346ec)
"""

__all__ = ["Number_with_uncert"]

###############################################################################

class _Exact_constant(object):
    """
    Expression that represents an exact (no error) constant function.

    Used in wrap_func_output() for a uniform access to expression parts
    (wrap_func_output() expects some attributes from expression parts).
    """
    # This class exists so as not to uselessly create semi-formal variables
    # (Semi_formal_var objects), which are kept in memory.
    def __init__(self, value):
        self._variables = set()
        self.nominal_value = value

def to_func(x):
    """
    Coerces x into a constant expression, unless x is already an
    expression (Semi_formal_expr object).
    """    
    return x if isinstance(x, Semi_formal_expr) else _Exact_constant(x)
    
def wrap_func_output(f):

    """
    Transforms a Python function into an expression that can return
    the result of 'f', but generally as a Semi_formal_expr object
    (unless its evaluation does not involve variables [Semi_formal_var
    objects], in which case 'f' simply returns its usual result).
    """
    
    def f_with_expr_output(*args):
        
        #! The following does not seem to go into the expression returned
        # by wrap_func_output().  Why?
        """
        Version of %s(...) that returns a semi-formal expression
        (Semi_formal_expr object), if its result depends on variables
        (Semi_formal_var objects).  The new version returns a simple
        constant, when applied to constant arguments.
        
        Original documentation:
        %s
        """ % (f.__name__, f.__doc__)
        
        # Coercion of all arguments to semi-formal expressions:
        sub_exprs = map(to_func, args)

        # We keep track of all Semi_formal_var leaves:
        variables = set()
        for sub_expr in sub_exprs:
            variables |= sub_expr._variables

        # Formal version of 'f': its value can be calculated through
        # the specificaton of the values of the variables used in the
        # arguments 'args'.
        def f_evaluation():
            """
            Returns the value of function 'f', calculated at the
            nominal values of its arguments sub_exprs.
            """
            return f(*(sub_expr.nominal_value for sub_expr in sub_exprs))

        if variables:
            # Delayed evaluation:
            return Semi_formal_expr_node(f_evaluation, variables)
        else:
            # Constant functions do not have to be complicated
            # objects:
            return f_evaluation()

    return f_with_expr_output

class Semi_formal_expr(object):

    """
    Semi-formal expression object that supports the mathematical
    operations of Python floats and give objects of the same type
    (Semi_formal_expr).

    Semi_formal_expr objects can thus be summed, etc.

    They are ssentially identical to a mathematical expression
    involving floats, with the difference that it effectively contains
    a way of recalculating its value and adapt to changes in its
    variables.

    The only 'variables' considered in this class are Semi_formal_var
    objects (numbers with an uncertainty).

    Attributes and methods defined through inheritance:

    - (nominal_value, error): nominal (central) value and error on the
      expression.  Before accessing these, no calculation of the
      expression is performed.  These quantities are dynamically
      calculated.

    - derivative_value(variable): value of the partial derivative
      with respect to the given variable (Semi_formal_var object), at
      the point currently defined by the nominal values of the
      variables.
    
    - _variables: semi-formal variables (Semi_formal_var objects) on
      which the expression depends.
    """

    def __repr__(self):
        return "%s object with result %s" % (type(self), str(self))
            
    def __str__(self):
        (nominal_value, error) = self.nominal_value, self.error    
        return ("%s +- %s" % (nominal_value, error) if error
                else str(nominal_value))

    # Conversion to float would be risky: calculations could be
    # performed without error handling, and without the user noticing
    # it.  A number with an uncertainty is not a pure number.  Note
    # that float(1j) is not allowed, for instance.
    def __float__(self):
        raise TypeError("can't convert a number with uncertainty (%s)"
                        " to float; use x.nominal_value"
                        % self.__class__)

    # Operators with no reflection:

    # Logical operators: warning: the resulting value cannot always be
    # differentiated.
    for operator in ('eq', 'ge', 'gt', 'le', 'lt', 'ne'):
        exec ("__%s__ = wrap_func_output(float.__%s__)"
              % (operator, operator))    

    # __nonzero__() is supposed to return a boolean value (it is used
    # by bool()).  It is for instance used for converting the result
    # of comparison operators to a boolean, in sorted().  If we want
    # to be able to sort Semi_formal_expr objects, __nonzero__ cannot
    # return a Semi_formal_expr object.  Since boolean results (such
    # as the result of bool()) don't have a very meaningful
    # uncertainty, this should not be a big deal:
    def __nonzero__(self):
        return bool(self.nominal_value)

    # Operators that return a numerical value:
    for operator in ('abs', 'neg', 'pos'):
        exec ("__%s__ = wrap_func_output(float.__%s__)"
              % (operator, operator))
    
    # Operators with a reflection:
    for operator in ('add', 'div', 'divmod', 'floordiv','mod', 'mul',
                     'pow', 'sub', 'truediv'):
        for prefix in ('', 'r'):
            method_name = "__%s%s__" % (prefix, operator)
            exec("%s = wrap_func_output(float.%s)"
                 % (method_name, method_name))

    del operator, prefix, method_name


class Semi_formal_var(Semi_formal_expr):
    
    """
    Semi-formal variable.

    The variable is bound to a value+uncertainty at all times.

    This is a special kind of semi-formal expression (Semi_formal_expr object),
    with a constant value.
    """
    
    def __init__(self, nominal_value, error = 0):
        
        """
        'nominal_value' is the nominal value of the semi-formal variable.
        'error' is the error on the value.
        """

        # We initialize the value in the same way as users of
        # instances who would set it: users use the 'nominal_value' attribute:
        self.nominal_value = nominal_value  # Defines the (constant) expression
        self.error = error
        # Expression 'x' depends on 'x':
        self._variables = set([self])

    def get_value(self): return self.__nominal_value
    def set_value(self, nominal_value):
        """
        Since evaluations of Semi_formal_expr expression use functions
        that use float arguments, the value of the variables needs to
        be a float.
        """
        self.__nominal_value = float(nominal_value)        
    nominal_value = property(get_value, set_value)
    
    def derivative_value(self, variable, step = 1):
        return 1. if self is variable else 0.

class Semi_formal_expr_node(Semi_formal_expr):
    """
    Semi-formal expression, as defined by a 'regular' Python function.
    """

    def __init__(self, func, variables):
        """
        Node for a semi-formal expression (Semi_formal_expr).
        
        For the meaning of object attributes see the documentation for
        Semi_formal_expr.
        """
        self._evaluate = func  # Way of evaluating the function
        self._variables = variables

    #! It might be numerically more relevant to evaluate at .nominal_value+.err
    # ... but this is not standard.  AND: I'm not sure this would yield
    # a more correct way of estimating the variance (does this take the
    # second order derivative into account??)  THIS WOULD BE USEFUL
    # if .err is a more reasonnable value than step...
    def derivative_value(self, variable, step = 1e-5):
        """
        Calculates the derivative of the expression with respect to
        the given variable (Semi_formal_var object), at the point
        defined by the nominal (central) value of its variables.

        'step' is the numerical variation on 'variable' for the numerical
        calculation of the derivative.  -log10(step) is an estimate of
        the number of digits lost in the derivative calculation.
        """
        
        # Value of the function:
        central_value = self._evaluate()

        # We temporarily shift the value of the variable:
        previous_nominal_value = variable.nominal_value
        variable.nominal_value += step
        new_value = self._evaluate()
        variable.nominal_value = previous_nominal_value
        
        derivative_value = (new_value-central_value)/step
        
        return derivative_value

    @property
    def nominal_value(self):
        """
        Returns the nominal (central) value of the expression, for the
        current nominal value of its variables.
        """
        return self._evaluate()

    @property
    def error(self):
        """
        Returns the error on the expression, at the current nominal
        value of its variables.
        """
        # Caculation of the variance:
        variance = 0
        for variable in self._variables:
            #! This is not efficient: the "central" function value is
            # calculated many times: (an alternative would be to
            # effectively calculate the derivative here)
            value_shift = (self.derivative_value(variable)*variable.error)
            variance += value_shift**2
        error = variance**0.5
        return error

# We wrap the expressions from the math module so that they keep track
# of uncertainties.
BUILTIN_FUNC = type(sum)  # Built-in expression type
import math
for name in dir(math):
    obj = getattr(math, name)
    if isinstance(obj, BUILTIN_FUNC):
        setattr(math, name,  wrap_func_output(obj))

###############################################################################
# Values with uncertainties:
#
Number_with_uncert = Semi_formal_var
###############################################################################

This recipe is meant to be an improvement over other recipes found here. At the time of writing, I have not found any other Python module that could perform error calculations in the transparent way offered here. In particular, correlations between parts of an expression are correctly handled ("x-x" is strictly zero, for instance), as are operations from the math module, and logical operators <, >, etc.

The main idea behind the calculation was proposed by Arnaud Delobelle. It consists in wrapping functions that operate on floats so that they return a kind of formal expression instead of their normal result.

Thus, a point to be kept in mind is that expressions that use the "numbers with an uncertainty" defined here generally return a "semi-formal expression" object, which behaves very much like a float. These objects have properties that go beyond those of floats: their nominal (central) value and error can be accessed (with the "nominal_value" and "error" attributes); they can be printed in a legible format ("3.14 +- 0.01"); they can also be used in additional formulas written for floats; they can be also differentiated (derivative_value() method); they can also be dynamically calculated (if a variable of type Number_with_uncert is changed, the value of expressions that use it can be recalculated).

Examples:

>>> from uncertainties import Number_with_uncert
>>> from math import cos
>>> x = Number_with_uncert(3.14, 0.01)
>>> print x*2*cos(x)
-6.27999203525 +- 0.0200996795563
>>>
>>> ## Semi-formal expressions:
...
>>> a = Number_with_uncert(1)
>>> y = x*2 + a
>>> # The following two values are equal, and both have non-zero uncertainties:
... print y
7.28 +- 0.0200000000001
>>> print x*2 + a
7.28 +- 0.0200000000001
>>> # However, their difference is exactly zero since they have exactly
... # the same formula:
... print y - (x*2 + a)
0.0
>>>
>>> ## Access to specific values:
...
>>> y.nominal_value  # Nominal value
7.2800000000000002
>>> y.error  # Error on the expression
0.020000000000131024
>>>
>>> ## Partial differentiation is possible (evaluated at the current nominal
... ## value of the variables):
... y.derivative_value(x)
2.0000000000131024
>>>
>>> ## Dynamic modification of variables:
...
>>> x.nominal_value = 1.
>>> # 'y' is an semi-formal expression, and is thus recalculated so that
... # the new value of 'x' is used:
... y
<class 'uncertainties.Semi_formal_expr_node'> object with result 3.0 +- 0.0200000000001
>>> print y
3.0 +- 0.0200000000001
>>>
>>> # However, only values with uncertainties are considered variables of
... # semi-formal expression (Semi_formal_expr) objects:
...
>>> coef = 2
>>> x = Number_with_uncert(1)
>>> v = coef * x
>>> print v
2.0
>>>
>>> coef = 555555
>>> x.nominal_value = 10
>>> print v  # Only 'x' is a variable (coef was set during 'v =...')
20.0

1 comment

Eric-Olivier LE BIGOT (author) 14 years, 9 months ago  # | flag

An improved version (more precise, faster, more compatible with Numpy, with improved documentation) is available from the Python Package Index.