Welcome, guest | Sign In | My Account | Store | Cart
```======= 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___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
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):
break
return l

class UnmatchedParenthesis(Exception):
pass

def __init__(self,value):
self.value=value
def __str__(self):
return repr(self.value)

'__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
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
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
for prefix in self.prefixes:
if key.startswith(prefix):
return self.prefixes[prefix]*self.units[key[len(prefix):]]
'''add a unit to the unit database'''
val=utuple[0]
dims=utuple[1]
self.units[name]=Q(val,dims)
'''add a base type to the unit database'''
val=1
self.units[name]=Q(val,name,utype='BASE')
'''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:

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:
for pref in PREFIXES:
for unit, dimtup in UNITS_LIST:
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)
srate=Q(36,'ksample/s')
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)

try:
Q(2,'ft')+Q(3,'s')
except DimensionsError:

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
# 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')),
('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')),
('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'))]
```