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)
|