# $Id: errorbar.py,v 1.4 2011-04-05 19:20:55 wirawan Exp $ # # Module wpylib.math.stats.errorbar # Errorbar text handling for Python # # Created: 20081118 # Wirawan Purwanto # # Moved to wpylib, 20101007 import math from math import sqrt import os import os.path import re import wpylib.shell_tools as sh class regexp__aux(object): '''Auxiliary objects for routines below. Compiled into regexp objects for speed.''' # CHECKME FIXME: This class is NOT originally designed to be multithread-safe # since regex objects are shared between all threads. # To be thread-safe, the regex objects or aux classes may need to be # instantiated separately for each thread instance, or whatever else way # possible. init = False @staticmethod def initialize(): R = regexp__aux # Each of the regex stuff below is a 2-tuple containing the regex # object and its associated function to extract the value/errbar pair # (as a tuple) from the re.Match object. # # Standard errorbar matcher (errorbar in parantheses), in the form of a tuple # The first element of the matcher is a regexp matcher object, and the # second element is a function to extract the (value,error) tuple from the # successful matching process. R.errbar = \ ( re.compile(r"([-+]?\d+)" # leading digits with optional sign r"((?:\.\d*)?)" # optional digits after decimal point r"\((\d+\.?\d*[Ee]?[-+]?\d*)\)" # errorbar in paranthesis r"((?:[Ee][-+]?\d+)?)$"), lambda M: ( float(M.group(1) + M.group(2) + M.group(4)), # value = simple composition # for errorbar: # - must multiply with value's exponent # - and account for decimal digits (if any) float(M.group(3)) * float("1"+M.group(4)) * 10**(-max(len(M.group(2))-1, 0)) ) ) # Later additional matchers can be added here R.init = True @staticmethod def aux(): R = regexp__aux if not R.init: R.initialize() return R @staticmethod def match(matcher, Str, rslt, flatten=False): '''Matches the string `Str' against the errorbar regexp pattern in `matcher[0]'. If it matches, the value and error are added to the `rslt' list. Depending on whether `flatten' is True or not, it is added as a tuple or as two elements, respectively, into the `rslt' list.''' # Note: matcher is an object like R.errbar above. m = matcher[0].match(Str) if m: if flatten: rslt.extend(matcher[1](m)) else: rslt.append(matcher[1](m)) return True else: return False def expand(obj, convert_float=False, flatten=False): '''Expands compressed errorbar notation to two consecutive numbers (returned as a tuple). Input: The input can be a string, or a list of strings. Output: The list element that has the format of "VAL(ERR)" (or its scientific format twist) will be expanded into two numbers in the output list. All other elements will be passed "as is" to the output. Optionally, the non-float items can be force-converted to floats if the convert_float is set to True. ''' if getattr(obj, "__iter__", False): iterable_inp = True objs = obj else: iterable_inp = False objs = ( obj, ) rgx = regexp__aux.aux() rslt = [] for o in objs: t = type(o) if t == int or t == float or t == complex: rslt.append(o) else: # Assume a string! o = o.strip() #m = rgx.errbar.match(o) if (rgx.match(rgx.errbar, o, rslt, flatten)): #print "match: errbar" pass elif convert_float: # Convert to float right away, store into the `rslt' list rslt.append(float(o)) #rslt.append( (float(o),) ) else: # Unless otherwise requested, the object will not be converted # to float: rslt.append(o) return rslt COMPRESS_ERRORBAR_EXE = os.path.dirname(__file__) + "/compress_errorbar.exe" def compress_errorbar_cxx(v, e, errdigits=2): """Temporary plug-hole measure to get python compress errorbars. Using a small C++ executable to perform the task.""" #perl_lib_dir = sh.getenv("WORK_DIR", "HOME") + "/scripts" return sh.pipe_out((COMPRESS_ERRORBAR_EXE, str(v), str(e), str(errdigits))).strip() class float_decomp(object): """Floating-point decomposer. We are assuming IEEE double precision here.""" def __init__(self, val, decdigits=16): self.val = val V = "%+.*e" % (decdigits, val) self.sign = V[0] self.digits = V[1] + V[3:3+16] self.exp = int(V[20:]) set = __init__ class errorbar(object): """A simple class holding a scalar value with an error bar. When converted to a float, its mean is returned. When converted to a string, its string representation is returned. which usually is meant to be a value with errorbar in parenthesis. This value is custom-made.""" ebproc = staticmethod(compress_errorbar_cxx) lazy_string = True def __init__(self, val, err, eb=None, ebproc=None): self.val = val self.err = err if ebproc != None: self.ebproc = ebproc if eb == None: if not self.lazy_string: self.eb = self.ebproc(val, err) else: self.eb = eb def __float__(self): return self.val def __float__(self): return self.val value = __float__ mean = __float__ def error(self): return self.err def __str__(self): if getattr(self, "eb", None): return self.eb elif getattr(self, "eb", None) == None and getattr(self, "ebproc", None) != None: self.eb = self.ebproc(self.val, self.err) return self.eb else: return "%g +- %g" % (self.val, self.err) display = __str__ def __repr__(self): return "errorbar(%s,%s,'%s')" % (self.val, self.err, self.display()) def update(self, val, err): self.val = val self.err = err self.ebupdate() return self def ebupdate(self): if self.lazy_string: self.eb = None else: self.eb = self.ebproc(self.val, self.err) def copy(self): return self.__class__(self.val, self.err, self.eb, self.ebproc) # Some algebraic operations with scalars are defined here: def __mul__(self, y): """Scales by a scalar value.""" return self.__class__(self.val*y, self.err*abs(y), ebproc=self.ebproc) __rmul__ = __mul__ def __imul__(self, y): """Scales itself by a scalar value.""" self.val *= y self.err *= abs(y) self.ebupdate() return self def __div__(self, y): """Divides by a scalar value.""" return self.__class__(self.val/y, self.err/abs(y), ebproc=self.ebproc) def __idiv__(self, y): """Scales itself by a scalar value (division).""" self.val /= y self.err /= abs(y) self.ebupdate() return self def __add__(self, y): """Adds by a scalar value or another errorbar value. In the latter case, the uncertainty is assumed to be uncorrelated, thus we can use a simple formula to update the errorbar.""" if isinstance(y, errorbar): return self.__class__(self.val+y.val, sqrt(self.err**2 + y.err**2), ebproc=self.ebproc) else: return self.__class__(self.val+y, self.err, ebproc=self.ebproc) __radd__ = __add__ def __sub__(self, y): """Subtracts by a scalar value or another errorbar value. In the latter case, the uncertainty is assumed to be uncorrelated, thus we can use a simple formula to update the errorbar.""" if isinstance(y, errorbar): return self.__class__(self.val-y.val, sqrt(self.err**2 + y.err**2), ebproc=self.ebproc) else: return self.__class__(self.val-y, self.err, ebproc=self.ebproc) def __rsub__(self, y): """Subtracts this errorbar value from a scalar value or another errorbar value. In the latter case, the uncertainty is assumed to be uncorrelated, thus we can use a simple formula to update the errorbar.""" if isinstance(y, errorbar): return self.__class__(y.val-self.val, sqrt(self.err**2 + y.err**2), ebproc=self.ebproc) else: return self.__class__(y-self.val, self.err, ebproc=self.ebproc) @staticmethod def create_str(s): """Creates an errorbar object from an errorbar string.""" eb = expand(s, convert_float=True)[0] return errorbar(*eb) class errorbar_compressor(object): """Compressor for errorbar string.""" def __init__(self): self.errdigits = 2 def __call__(self, val, err, **args): errdigits = args.get("errdigits", self.errdigits) v = float_decomp(val) e = float_decomp(err, decdigits=errdigits) #def errorbar_algebra