summaryrefslogtreecommitdiff
path: root/core/units.py
blob: 660645db4ce90783d438f959c3ab03344767cfe0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
from yamlcrap import FennObject
import re, os
from string import Template
from copy import copy

combined_dat_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'combined.dat')

if not os.access(os.path.join(os.path.dirname(os.path.realpath(__file__)), "combined.dat"), os.F_OK):
    #build the new db for our custom units
    f1 = open('/usr/share/misc/units.dat').read()
    current_path = os.path.dirname(os.path.realpath(__file__))
    f2 = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'supplemental_units.dat')).read()
    f3 = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'combined.dat'), 'w')
    f3.write(f1+f2)
    f3.close()

#unum looks rather immature, perhaps I will write a wrapper for GNU units instead
#scientific.Physics.PhysicalQuantities looks ok-ish        
class UnitError(Exception): pass 
class NaNError(Exception): pass

class Unit(FennObject):
    sci = '([+-]?\d*.?\d+([eE][+-]?\d+)?)' #exp group leaves turds.. better way to do regex without parens?
    yaml_tag = "!unit"
    '''try to preserve the original units, and provide a wrapper to the GNU units program'''
    units_call = "units -f %s -t " % (combined_dat_path) #export LOCALE=en_US; ?
    def __init__(self, string=None):
        #simplify(string) #check if we have a good unit format to begin with. is there a better way to do this?
        self.string = str(string)
        self.simplify() #has no side effects, just raise any exceptions early
        #e_number = '([+-]?\d*\.?\d*([eE][+-]?\d+)?)' #engineering notation
        #match = re.match(e_number + '?(\D*)$', string) #i dunno wtf i was trying to do here
        #match = re.match(e_number + '?(.*)$', string)
        #if match is None: raise UnitError, string
        #try: self.number = float(match.group(1))
        #except ValueError: self.number = 1.0
        #self.unit = match.group(3)

    def __repr__(self):
        return str(self.string)
    
    def yaml_repr(self):
        if hasattr(self, 'uncertainty'): u = self.uncertainty.yaml_repr() #TODO delete this
        else: u = ''
        return self.string +u

    @staticmethod #is this right?
    def sanitize(string):
        '''intercept things that will cause GNU units to screw up'''
        if hasattr(string, 'string'): string = string.string #egads. in case i accidentally pass a Unit or something
        if string is None or str(string) == 'None' or str(string) == '()': string = 0
        #so we can play nice with the 'quantities' package:
        if hasattr(string, "units"): string = string.units.dimensionality.string
        #so we can play nice with sympycore
        #could be more robust if you import sympycore first
        if string.__class__.__module__ == "sympycore.physics.units":
            if string.__class__.__name__ == "Unit":
                #first strip out the variables by assuming they have a value of 1
                symbols = list(string.symbols)
                remove_these = []
                for sym in symbols:
                    if sym.__class__.__name__ == "Calculus": remove_these.append(sym)
                temp = copy(string)
                for sym in remove_these:
                    temp = temp.subs(sym, 1) #substitute the variable/symbol with a "1"
                string = str(temp)
                print "symbols were: ", symbols
                print "the new string is: ", string
        for i in ['..', '--']:
            if str(string).__contains__(i):
                raise UnitError, "Typo? units expression '"+ string + "' contains '" + i + "'"
        return '('+str(string)+')' #units -1 screws up; units (-1) works

    def units_happy(self, call_string, rval):
        '''the conversion or expression evaluated without error'''
        error = re.search('Unknown|Parse|Error|invalid|error', rval)
        if error:  
            raise UnitError, str(call_string) + ': ' + str(rval)
        nan = re.search('^nan', rval) #not sure how to not trip on results like 'nanometer'
        if nan:
            raise NaNError, rval
        return True #well? what else am i gonna do

    def units_operator(self, a, b, operator):
        if str(a)=='None' or str(b)=='None': return None
        s = Template('($a)$operator($b)')
        expression = s.safe_substitute(a=str(a), b=str(b), operator=str(operator))
        rval = Unit(expression)
        return rval
    
    def __mul__(self, other):
        return self.units_operator(self, other, '*')
    __rmul__ = __mul__

    def __div__(self, other):
        return self.units_operator(self, other, '/')
    __rdiv__ = __div__

    def __add__(self, other):
        return self.units_operator(self, other, '+')
    __radd__ = __add__

    def __sub__(self, other):
        return self.units_operator(self, other, '-')
    __rsub__ = __sub__
    
    def __eq__(self, other):
        if hasattr(other, 'string'): other = other.string
        if self.simplify() == Unit(other).simplify(): return True
        else: return False
    
    def __ne__(self, other):
        if self.__eq__(other): return True
        else: return False
    
    def __cmp__(self, other):
        #i should probably be using __lt__, __gt__, etc
        #neither does this work for nonlinear units like tempF() or tempC()
        if self.compatible(other):
            conv = self.conv_factor(other)
            #print conv #god what a mess
            if conv == 1: return 0
            if conv < 1 and conv > 0: return -1
            if conv > 1: return 1
            if conv <0 and conv > -1: return -1
            if conv <-1 : return 1
            if conv == -1: return 1
            if conv == inf: return 1
            if conv == 0: return -1
    
    def conv_factor(self, destination):
        '''the multiplier to go from one unit to another, for example from inch to mm is 25.4'''
        conv_factor = os.popen(self.__class__.units_call + "'" + self.sanitize(self.string) + "' '" + self.sanitize(destination) + "'").read().rstrip('\n')
        if self.units_happy(self.string, conv_factor): 
            return float(conv_factor)
        else: raise UnitError, conv_factor, destination
        
    def convert(self, destination):
        if self.compatible(destination):
            return str(self.conv_factor(destination)) +'*'+ str(destination) #1*mm
        
    def to(self, dest):
            return Unit(self.convert(dest))
    
    def simplify(self, string=None):
        '''returns a string'''
        if string is None: string = self.string
        rval = os.popen(self.__class__.units_call + "'" + self.sanitize(string) + "'").read().rstrip('\n')
        if self.units_happy(string, rval): return rval
        else: raise UnitError, self.string

    def check(self):
        try: self.simplify()
        except UnitError or NaNError: return False
        return True

    def simplified(self):
        '''returns a Unit in simplified format. note that it may actually look more complicated due to the lack of default units'''
        return Unit(self.simplify())
    
    def compatible(self, other):
        '''check if both expressions boil down to the same base units'''
        try: self.simplify(self.string + '+' + self.sanitize(other))
        except UnitError: return False
        else: return True

    def number(self): 
        '''return the number portion of the unit string'''
        return 'not yet implemented, sorry!'
    def unit(self):
        '''return the unit portion of the unit string'''
        return 'not yet implemented, sorry!'

class Range(FennObject):
    yaml_tag = "!range"
    sci =Unit.sci
    #expression should look something like: 1e4 m .. 2km
    yaml_pattern = sci+'\s*(\D?.*)?\s*\.\.\s*'+sci+'\s*(\D?.*)$'
    def __init__(self, min=None, max=None):
        self.min = min
        self.max = max
    def __repr__(self):
        return "Range(%s, %s)" %(self.min, self.max)
    def yaml_repr(self):
        return "%s .. %s" %(self.min, self.max)
    def __eq__(self, other):
        if type(other) == type(self):
            return self.min == other.min and self.max == other.max
        else: return None
    @classmethod
    def from_yaml(cls, loader, node):
        '''see http://pyyaml.org/wiki/PyYAMLDocumentation#Constructorsrepresentersresolvers'''
        data = loader.construct_scalar(node)
        match = re.search(cls.yaml_pattern, data)
        a, crap, units1, b, crap2, units2 = match.groups() 
        if units2 != '':
            if units1 != '':
                a = Unit(a+units1)
                b = Unit(b+units2)
            else:
                a = Unit(a+units2)
                b = Unit(b+units2)
        else: 
            #double yuck. maybe i should just pass this to units instead?
            a = eval(a)
            b = eval(b)
        return cls(min(a,b), max(a,b))

class Uncertainty(Range):
    '''predicted or observed range of error in the measurement'''
    yaml_tag = "!uncertainty" #ehh.. going to do something with this eventually
    sci =Unit.sci
    yaml_pattern = '^\+-' + sci + '\s*(\D?.*)$' #+-, number, units
    #TODO from_yaml method using existing __init__
    #TODO args should be (min, max), assert isinstance Unit
    def __init__(self, string=None):
        match = re.match('^\+-(.*)', string)
        if match: unit = match.group(1)
        else: raise SyntaxError, "'"+ string +"'" + ": uncertainty must begin with +-, for now at least" #got any better ideas?
        Unit.__init__(self, unit)
    def __repr__(self):
        return 'Uncertainty('+ Unit.__repr__(self) +')'
    def yaml_repr(self):
        return "+-%s" % (self.string)