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