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___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'))]

History