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.
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
An improved version (more precise, faster, more compatible with Numpy, with improved documentation) is available from the Python Package Index.