======= dimensions.py ============= '''A module for adding dimensions to numeric types for dimensional analysis and automatic unit conversions. ''' from __future__ import division import math import re import os t___add__ = r'(?P<__add__>\+)' t___sub__ = r'(?P<__sub__>\-)' t___pow__ = r'(?P<__pow__>\^|(\*\*))' t___mul__ = r'(?P<__mul__>\*)' t___truediv__ = r'(?P<__truediv__>/)' t_LPAREN = r'(?P<LPAREN>\()' t_NUMBER = r'(?P<NUMBER>(\+|-)?((\d+\.\d+)|(\.\d+)|(\d+\.)|(\d+))([eE](\+|-)?\d+)?)' t_RPAREN = r'(?P<RPAREN>\))' t_IDENT = r'(?P<IDENT>[a-zA-Z]\w*)' # ident names begin with letter ulexres= r'\s*(' + "|".join([t___add__, t_IDENT, t___pow__, t___truediv__, t___sub__, t___mul__, t_NUMBER, t_LPAREN, t_RPAREN]) + r')\s*' lexre = re.compile(ulexres) def tokenize(s): '''return a list of token tuples: (token, value, start)''' l=[] # list to hold tokens start=0 # used to check for bad tokens sc=lexre.scanner(s) while 1: tg=sc.search() if tg: if tg.start()!=start: # we should start at end of last token raise BadToken(start) l.append( [ (token, value, (tg.start(), tg.end())) for (token, value) in tg.groupdict().items() if value][0]) start=tg.end() # prepare start for next pass else: if start!=len(s): raise BadToken(start) break return l class UnmatchedParenthesis(Exception): pass class BadToken(Exception): def __init__(self,value): self.value=value def __str__(self): return repr(self.value) PRIORITY={ '__add__':1, '__sub__':1, '__mul__':2, '__truediv__':2, '__pow__':3} def convert(s): '''convert a list of token tuples from infix order to RPN''' input=s[:] stack=[] # 'Texas' LIFO output=[] # 'California' output accumulator while len(input)>0: # Careful! We're iterating over changing input list. if input[0][0]=='IDENT' or input[0][0]=='NUMBER': # move operands directly to output output.append(input.pop(0)) elif input[0][0]=='LPAREN': # Now handle a left parenthesis stack.append(input.pop(0)) elif input[0][0]=='RPAREN': # Now handle a right parenthesis if not stack: # We shouldn't have an RPAREN without an LPAREN raise UnmatchedParenthesis elif stack[-1][0]=='LPAREN': # when RPAREN catches up with LPAREN, vaporize both stack.pop() input.pop(0) else: # pop operands until we hit LPAREN output.append(stack.pop()) else: # logically, we should have an operand at the input, but # don't know about stack if stack: # there is at least one item in the stack if stack[-1][0]=='LPAREN': # LPAREN on stack, push next operand to stack stack.append(input.pop(0)) elif PRIORITY[input[0][0]]>PRIORITY[stack[-1][0]]: # higher priority input item goes to stack stack.append(input.pop(0)) else: output.append(stack.pop()) else: # no stack, push input to stack stack.append(input.pop(0)) while len(stack)>0: # when input is empty we still may have stack items to move over if stack[-1][0]=='LPAREN': # Shouldn't be any LPARENs left! raise UnmatchedParenthesis else: output.append(stack.pop()) return output def proc_stack(input, vardict): '''process IDENTs, binary operators in input list using vardict namespace''' stack=[] for item in input: # anything left to process? if item[0]=='IDENT': # push object referred to by IDENT onto stack stack.append(vardict[item[1]]) elif item[0]=='NUMBER': stack.append(float(item[1])) else: # we must have a binary operator # and assuming well formed input two operands on stack operand=item[0] y=stack.pop() x=stack.pop() # XXX the following hack should be fixed some time just # because it is really ugly! if operand=='__truediv__' and type(y)!=type(1.0) and \ type(x)==type(1.0) and x==1 : # special case of empty numerator and Q denom x=vardict[''] # this is coordinated with hack in UNITS_LIST in dimensions.py stack.append(x.__getattribute__(operand)(y)) return stack[0] def ap_eval(instr,vardict): '''evaluate an expression string using vardict namespace''' t=tokenize(instr) s=convert(t) ans=proc_stack(s,vardict) return ans class DimensionsError(Exception): def __init__(self, value): self.value=value def __str__(self): return repr(self.value) class Q(object): EXPTOL=1e-14 '''simple class for creating base dimensions''' def __init__(self, value, dimstr, utype=None): '''takes name of base unit and creates a Base object''' if utype=='BASE': # manually create the dimension for a base unit self.value=value # value of dimension self.dims={} # dict of dimensions, keys are base units self.dims[dimstr]=1 else: # we're building a compound unit if dimstr.strip()=='': # no Dim object yet, so build it self.value=value self.dims={} else: tmp=ap_eval(dimstr,units) # At this point we have a Dim object self.value=value*tmp.value self.dims=tmp.dims def copy(self): '''make a copy''' tmp=Q(1,'') tmp.value=self.value tmp.dims=self.dims.copy() return tmp def __neg__(self): '''return negative of self''' tmp=self.copy() tmp.value=-self.value return tmp def __pos__(self): '''unary positive operator''' return self.copy() def __abs__(self): '''abs operator''' tmp=self.copy() tmp.value=abs(self.value) return tmp def __add__(self, other): '''add like units together''' tmp=self.copy() if other.dims==tmp.dims: tmp.value=self.value+other.value return tmp else: raise DimensionsError, 'Units not consistent' # Rich comparison operators def __lt__(self, other): '''lt method for units''' if self.dims!=other.dims: raise DimensionsError, 'Units not consistent' else: return self.value<other.value def __ge__(self, other): '''ge method for units''' return not self.__lt__(other) def __eq__(self, other): '''eq method for units''' if self.dims!=other.dims: raise DimensionsError, 'Units not consistent' else: return self.value==other.value def __ne__(self, other): '''ne method for units''' return not self.__eq__(other) def __gt__(self, other): '''gt method for units''' if self.dims!=other.dims: raise DimensionsError, 'Units not consistent' else: return self.value>other.value def __le__(self, other): '''le method for units''' return not self.__gt__(other) def __sub__(self, other): '''subtract like units''' tmp=self.copy() if other.dims==tmp.dims: tmp.value=self.value-other.value return tmp else: raise DimensionsError, 'Units not consistent' def __mul__(self, other): '''multiply two units together''' # get list of dims used in both units try: tmp=Q(1,'') tmp.value=self.value*other.value superset=dict.fromkeys(self.dims.keys()+other.dims.keys()).keys() for dim in superset: tmp.dims[dim]=self.dims.get(dim,0)+ \ other.dims.get(dim,0) if abs(tmp.dims[dim] %1)<self.EXPTOL: # This is a hack to eliminate creeping floating point error. # It's not a real substitute for using a Rational # number type but will suffice for now. tmp.dims[dim]=int(round(tmp.dims[dim])) if tmp.dims[dim]==0: del tmp.dims[dim] return tmp except AttributeError: # this should happen if we multiply by a number tmp=self.copy() tmp.value=self.value*other return tmp def __rmul__(self,other): '''multiply number by unit''' return self.__mul__(other) def __truediv__(self, other): '''divide one unit by another''' # get list of dims used in both units tmp=self.copy() try: # if two Dim objects tmp.value=tmp.value/other.value superset=dict.fromkeys(self.dims.keys()+other.dims.keys()).keys() for dim in superset: tmp.dims[dim]=self.dims.get(dim,0)-other.dims.get(dim,0) if abs(tmp.dims[dim] %1)<self.EXPTOL: # this is a hack to eliminate creeping floating point error tmp.dims[dim]=int(round(tmp.dims[dim])) if tmp.dims[dim]==0: del tmp.dims[dim] except AttributeError: # if dividing by number tmp.value/=other return tmp def __div__(self,other): '''calls __truediv__''' return self.__truediv__(other) def __rdiv__(self,other): '''calls __rtruediv__''' return self.__rtruediv__(other) def __rtruediv__(self,other): tmp=self.copy() tmp.value=1/tmp.value for dim in tmp.dims: tmp.dims[dim]=-tmp.dims[dim] return other*tmp def __pow__(self,power): tmp=self.copy() for dim in tmp.dims: newexponent=power*tmp.dims[dim] if abs(newexponent%1)<self.EXPTOL: # this is a hack to eliminate creeping floating point error newexponent=int(round(newexponent)) tmp.dims[dim]=newexponent tmp.value=tmp.value**power return tmp def sqrt(self): '''returns square root of self''' # Added for compatibility with std function in scipy/numpy return self.__pow__(0.5) def __hash__(self): '''hash method allows use of Dims as dict key. Manually modifying Dim attributes will break the dict so don't do that!''' return hash(self.value)^reduce( lambda x,y: x^y, [hash(item) for item in self.dims.items()],0) def __repr__(self): '''return a string representation''' # numerator string will be either 1 or a string of the dims numdims=[(dim,self.dims[dim]) for dim in self.dims if self.dims[dim]>0] numdims.sort() numstrlist=[] for dim,power in numdims: if power==1: numstrlist.append(dim) else: numstrlist.append(''.join([dim,'**',str(power)])) numstr='*'.join(numstrlist) dendims=[(dim,-self.dims[dim]) for dim in self.dims if self.dims[dim]<0] denstrlist=[] for dim,power in dendims: if power==1: denstrlist.append(dim) else: denstrlist.append(''.join([dim,'**',str(power)])) denstr='*'.join(denstrlist) if len(denstrlist)>1: denstr='(%s)' % denstr if len(numdims)==0 and len(dendims)==0: dimstr='' elif len(numdims)==0: dimstr='1/'+denstr elif len(dendims)==0: dimstr=numstr else: dimstr='%s/%s' % (numstr,denstr) return 'Q(%s, \'%s\')' % (self.value, dimstr) def __call__(self,otherdim, fmt=None): '''return value when expressed in otherdim dimensions''' o=Q(1,otherdim) if self.dims==o.dims: if fmt==None: return self.value/o.value else: return ''.join([fmt % (self.value/o.value),' [', otherdim,']']) else: raise DimensionsError, 'Units not consistent' def str(self,dims,valfmt='%s',dimfmt=' [%s]'): '''return a string in dimensions (dims) using value format specifier (valfmt) and dims format specifier (dimfmt)''' return ''.join(((valfmt % self(dims)),(dimfmt % dims))) class UnitsDatabase(object): def __init__(self, base_types, prefixes): '''create a UnitsDatabase object''' # start creation of units dictionary self.units={} for base in base_types: self.units[base]=Q(1, base, utype='BASE') self.prefixes=prefixes def __getitem__(self, key): '''grab item from dictionary if it exists, otherwise try prefixes''' if key in self.units: return self.units[key] else: if len(key)==1: # don't look for prefix if one letter unit raise DimensionsError, 'unit %s not found' % key for prefix in self.prefixes: if key.startswith(prefix): return self.prefixes[prefix]*self.units[key[len(prefix):]] raise DimensionsError, 'unit %s not found' % key def addUnit(self,name,utuple): '''add a unit to the unit database''' val=utuple[0] dims=utuple[1] self.units[name]=Q(val,dims) def addBase(self,name): '''add a base type to the unit database''' val=1 self.units[name]=Q(val,name,utype='BASE') def addPrefix(self,name,val): '''add a prefix to the unit database''' self.prefixes[name]=val def find(self,s): '''lists units containing the substring s''' re_find=re.compile(r'.*%s.*' % s ,re.I) res=[key for key in units.units.keys() if re_find.match(key)] res.sort() for unit in res: print unit # read the bases, prefixes and units from the dimensions.data file execfile(os.path.join(os.path.dirname(__file__),'dimensions.data')) units=UnitsDatabase(BASE_TYPES, PREFIXES) for unit,tup in UNITS_LIST: units.addUnit(unit,tup) BASE_TYPES=[] PREFIXES={} UNITS_LIST=[] # now add any from the user.data file try: execfile(os.path.join(os.path.dirname(__file__),'user.data')) for base in BASE_TYPES: units.addBase(base) for pref in PREFIXES: units.addPrefix(pref,PREFIXES[pref]) for unit, dimtup in UNITS_LIST: units.addUnit(unit,dimtup) except IOError: pass if __name__=='__main__': # this is where the tests live D0=Q(3, 'mN*m/A') D1=Q(4, 'A') assert(repr(D0*D1)=="Q(0.012, 'kg*m**2/s**2')") D2=Q(0.25, '1/A') assert(repr(D0/D2)=="Q(0.012, 'kg*m**2/s**2')") D3=Q(1,'1/W**(1/2)') D4=Q(1,'1/W**(1/2.)') D5=Q(1,'1/W**(1./2)') D6=Q(1,'1/W**(1./2.)') assert(D3==D4) assert(D3==D5) assert(D3==D6) D7=Q(3.3,'N*cm/W**0.5') assert(repr(D7)=="Q(0.033, 'kg**0.5*m/s**0.5')") assert(repr(D7*D7)=="Q(0.001089, 'kg*m**2/s')") assert(repr(D7**2)=="Q(0.001089, 'kg*m**2/s')") T=Q(15.2,'mN*m') ke=Q(3.6,'V/krpm') assert(repr(T/ke)=="Q(0.442150077172, 'A')") vel=Q(4000,'rpm') P=T*vel assert(str(P)=="Q(6.36696111128, 'kg*m**2/s**3')") assert(str(P('W'))=="6.36696111128") assert(P('horsepower')==0.0085348004172591339) units.addBase('sample') srate=Q(36,'ksample/s') units.addBase('interrupt') irate=Q(600,'interrupt/s') assert(srate/irate==Q(60.0, 'sample/interrupt')) km=Q(2.035,'N*cm/W**0.5') assert(str((T/km)**2)== "Q(0.557902552989, 'kg*m**2/s**3')") assert(((T/km)**2)('W')==0.55790255298854785) AddedInconsistentDims=0 try: Q(2,'ft')+Q(3,'s') except DimensionsError: AddedInconsistentDims=1 assert(AddedInconsistentDims) SubtracedInconsistentDims=0 try: Q(2,'ft')-Q(3,'s') except DimensionsError: SubtracedInconsistentDims=1 assert(SubtracedInconsistentDims) assert (-Q(5,'')==Q(-5,'')) assert (+Q(5,'')==Q(5,'')) assert (abs(Q(-5,''))==Q(5,'')) assert (abs(Q(5,''))==Q(5,'')) ============= dimensions.data ============== # Do not modify this file as it will be replace by the next installation # of the dimensions module. # If you wish to add a file with units that will get added every time # the dimensions module is loaded, add a file called user.data # in the dimensions directory, with the same format as this file. BASE_TYPES=['m', 'kg', 's', 'A', 'K'] PREFIXES=dict( yotta= 1e24, Y= 1e24, zetta= 1e21, Z= 1e21, exa= 1e18, E= 1e18, peta= 1e15, P= 1e15, tera= 1e12, T= 1e12, giga= 1e9, G= 1e9, mega= 1e6, M= 1e6, myria= 1e4, kilo= 1000, k= 1000, hecto= 100, h= 100, deca= 10, deka= 10, da= 10, deci= 1/10, d= 1/10, centi= 1/100, c= 1/100, milli= 1/1000, m= 1/1000, micro= 1e-6, u= 1e-6, nano= 1e-9, n= 1e-9, pico= 1e-12, p= 1e-12, femto= 1e-15, f= 1e-15, atto= 1e-18, a= 1e-18, zepto= 1e-21, z= 1e-21, yocto= 1e-24, y= 1e-24) UNITS_LIST= [('',(1,'')), # a hack to help handle the '1/unit' case ('meter',(1,'m')), ('second',(1,'s')), ('kilogram',(1,'kg')), ('gram',(0.001,'kg')), ('kelvin',(1,'K')), ('ampere',(1,'A')), ('amp',(1,'A')), ('radian',(1,'')), ('rd',(1,'radian')), ('newton',(1,'kg*m/s**2')), ('N',(1,'newton')), ('pascal',(1,'N/m**2')), ('Pa',(1,'pascal')), ('joule',(1,'N*m')), ('J',(1,'joule')), ('watt',(1,'J/s')), ('W',(1,'watt')), ('coulomb',(1, 'A*s')), ('C',(1,'coulomb')), ('volt',(1,'W/A')), ('V',(1,'volt')), ('ohm',(1,'V/A')), ('siemens',(1,'1/ohm')), ('S',(1,'siemens')), ('farad',(1,'C/V')), ('F',(1,'farad')), ('weber',(1,'V*s')), ('Wb',(1,'weber')), ('henry',(1,'Wb/A')), ('H',(1,'henry')), ('tesla',(1,'Wb/m**2')), ('T',(1,'tesla')), ('hertz',(1,'1/s')), ('Hz',(1, 'hertz')), ('sec',(1, 's')), ('minute',(60,'s')), ('min',(1,'minute')), ('hour',(60,'min')), ('hr',(1,'hour')), ('day',(24, 'hr')), ('week',(7,'day')), ('fortnight',(14,'day')), ('gm',(1,'gram')), ('g',(1,'gm')), ('tonne',(1000,'kg')), ('t',(1,'tonne')), ('cc',(1,'cm**3')), ('liter',(1000,'cc')), ('l',(1,'liter')), ('L',(1,'l')), ('mho',(1,'siemens')), ('angstrom',(1e-10, 'm')), ('fermi',(1e-15, 'm')), ('barn',(1e-28, 'm**2')), ('c',(299792458, 'm/s')), ('G',(6.6742e-11, 'N*m**2/kg**2')), ('au',(1.49559787e11,'m')), ('pi',(math.pi,'')), ('e',(math.e,'')), ('circle',(2,'pi')), ('rev',(1,'circle')), ('rpm',(1,'rev/min')), ('degC',(1,'K')), ('degF',(5/9, 'degC')), ('gravity',(9.80665, 'm/s**2')), ('force',(1,'gravity')), ('inch', (2.54, 'cm')), ('in', (2.54, 'cm')), ('foot', (12, 'inch')), ('feet', (1, 'foot')), ('ft', (1,'feet')), ('nauticalmile',(6080,'feet')), ('acre',(43560,'feet**2')), ('yard',(3,'ft')), ('mile',(5280,'ft')), ('calorie',(4.1868,'J')), ('cal',(1,'calorie')), ('lightyear',(365.25, 'day*c')), ('ly', (1,'lightyear')), ('torr',(101325/760, 'Pa')), ('Torr',(1,'torr')), ('kgf',(1, 'kg*gravity')), ('at',(1,'kgf/cm**2')), ('pound',(0.45359237,'kg')), ('lb',(1,'pound')), ('lbf',(1,'pound*force')), ('ounce',(1/16,'lb')), ('oz',(1,'ounce')), ('ozf',(1,'ounce*force')), ('ton',(2000,'lb')), ('gallon',(231,'inch**3')), ('gal', (1, 'gallon')), ('quart',(1/4, 'gal')), ('pint',(1/2, 'quart')), ('fluidounce',(1/16, 'pint')), ('floz',(1,'fluidounce')), ('cup', (8, 'floz')), ('tablespoon', (1/16, 'cup')), ('tbl', (1,'tablespoon')), ('tbsp',(1,'tbl')), ('Tbsp',(1,'tbsp')), ('Tsp', (1,'tablespoon')), ('teaspoon', (1/3, 'tablespoon')), ('tsp', (1,'teaspoon')), ('psi', (1, 'pound*force/inch**2')), ('slug', (1,'lbf*s**2/ft')), ('Btu',(1, 'cal*lb/gram')), ('btu',(1,'Btu')), ('BTU',(1,'Btu')), ('horsepower',(746, 'W')), ('hp',(1,'horsepower')), ('Wh',(1,'W*hour'))]