1 """Functions for parsing strings into various data types, ranging from
2 string (a no-op) to a system of differential equations.
3
4 An "accept" function takes a string, and returns a value of the desired type.
5 If the conversion is impossible, it raises a descriptive exception.
6
7 Accept functions are used to validate input in various components in qubx.GTK and qubx.tables.
8
9
10 Copyright 2007-2014 Research Foundation State University of New York
11 This file is part of QUB Express.
12
13 QUB Express is free software; you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
17
18 QUB Express is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
22
23 You should have received a copy of the GNU General Public License,
24 named LICENSE.txt, in the QUB Express program directory. If not, see
25 <http://www.gnu.org/licenses/>.
26
27 """
28
29 import re
30 import qubx.pyenv
31 import __main__
32 from qubx.util_types import *
33 from itertools import izip
34 from math import *
35
38 self.badStr = badStr
39 self.msg = msg
42
45
50 return accept
51
54
59
65
71
80
82 """Interprets '' as UNSET_VALUE, otherwise uses provided accept function."""
83 def acc(x):
84 if x == '':
85 return UNSET_VALUE
86 else:
87 return accept(x)
88 return acc
89
91 """Like acceptFloat, but acceptFloatOrUnset('') returns qubx.util_types.UNSET_VALUE."""
92 if x == '':
93 return UNSET_VALUE
94 return acceptFloat(x)
95
103 return format
104
106 """Returns False if x is in ['', 'False', 'false']; True otherwise."""
107 try:
108 if x.lower() == 'false':
109 return False
110 except:
111 pass
112 return bool(x)
113
115 """Returns an accept function which enforces `compare(convert(s)) == True.
116
117 @param errmsg: string for complaining
118 """
119 def accept(s):
120 try:
121 x = convert(s)
122 if not compare(x):
123 raise FieldAcceptException(s, errmsg)
124 return x
125 except ValueError:
126 raise FieldAcceptException(s, errmsg)
127 return accept
128
129 errmsg_cmp = lambda cmpname: lambda typename, bound: "must be %s %s %s" % (typename, cmpname, bound)
130 name_int = "an integer"
131 name_float = "a number"
132 errmsg_gt = errmsg_cmp("greater than")
133 errmsg_ge = errmsg_cmp("greater than or equal to")
134 errmsg_lt = errmsg_cmp("less than")
135 errmsg_le = errmsg_cmp("less than or equal to")
136
138 return acceptCmp(int, lambda x:x>lb, errmsg_gt(name_int, lb))
140 return acceptCmp(int, lambda x:x>=lb, errmsg_ge(name_int, lb))
142 return acceptCmp(int, lambda x:x<ub, errmsg_lt(name_int, ub))
144 return acceptCmp(int, lambda x:x<=ub, errmsg_le(name_int, ub))
146 return acceptCmp(int, lambda x:lb<=x<=ub, "must be %s between %s and %s" % (name_int, lb, ub))
148 return acceptCmp(float, lambda x:x>lb, errmsg_gt(name_float, lb))
150 return acceptCmp(float, lambda x:x>=lb, errmsg_ge(name_float, lb))
152 return acceptCmp(float, lambda x:x<ub, errmsg_lt(name_float, ub))
154 return acceptCmp(float, lambda x:x<=ub, errmsg_le(name_float, ub))
156 return acceptCmp(float, lambda x:lb<=x<=ub, "must be %s between %s and %s" % (name_float, lb, ub))
158 return acceptCmp(float, lambda x:(lb<=x<=ub) or ((-ub)<=x<=(-lb)), "must be %s between +/- %s and %s" % (name_float, lb, ub))
159
160
161
163 "e.g. '1, 2, 3-5 7' -> [1, 2, 3, 4, 5, 7]"
164
165 if s == '':
166 return []
167
168
169 s = re.sub(r'([^ -]+) ?- ?([^ -]+)', r'\1-\2', s)
170
171 tokens = re.split(r'[ ,\t]+', s)
172
173 result = []
174 for tok in tokens:
175 match = re.search(r'([^ -]+)-([^ -]+)', tok)
176 try:
177 if match:
178 a, b = accept(match.group(1)), accept(match.group(2))
179 result[len(result):] = dashrange(a, b)
180 elif tok:
181 result.append( accept(tok) )
182 except FieldAcceptException:
183 raise FieldAcceptException(s, 'must be a list of %s, separated by "," and "-"' % description)
184 return result
185
187 return lambda x: acceptAcceptList(x, acceptor, dashrange, description)
188
190 def dashrange(i, j):
191 if i <= j:
192 return xrange(i, j+1)
193 else:
194 return xrange(i, j-1, -1)
195 return acceptList(acceptor, dashrange, description)
196
198 def dashrange(i, j):
199 raise FieldAcceptException('%f-%f', 'How shall I enumerate a continuous range?')
200 return acceptList(acceptor, dashrange, description)
201
207 return lambda x: checkDim( acceptList(x) )
208
216 return fmtList
217
219 u = s if (s.__class__ == unicode) else s.decode('utf-8')
220 return u.replace(u'-', u'\u2212')
221
223 """Returns the symbol named in a NameError's message."""
224 m = re.search("'([^']+)'", msg)
225 if not m:
226 raise NameError, msg
227 return m.group(1)
228
230 """
231 FuncAcceptor instance is callable as fa(x, custom1, custom2, ...).
232
233 @ivar f: the expression as lambda locals..., x, custom1, custom2, ...:
234 @ivar args: a list of argument names, starting with "x"
235 @ivar expr: the function string as it was entered
236 @ivar local_args: names of defined local vars
237 @ivar local_vals: corresponding values
238 """
239 - def __init__(self, f, args, expr, local_args=[], local_vals=[], recipe="", static=None, typ=None):
240 self.f = f
241 self.args = args
242 self.expr = expr
243 self.local_args = local_args
244 self.local_vals = local_vals
245 self.recipe = recipe
246 self.static = static
247 self.typ = typ
248 self.__global_names = None
250
251
252
253 return self.f(*list(self.local_vals+list(args)), **kw)
257 return '%s(%s)' % (self.recipe, repr(self.expr))
258
260 if self.static is None:
261 raise TypeError("Can't pickle qubx.accept.FuncAcceptor without explicit static args")
262
263
264 return (None, self.args, self.expr, self.local_args, self.local_vals, self.recipe, self.static, self.typ)
266 if self.static is None:
267 raise TypeError("Can't pickle qubx.accept.FuncAcceptor without explicit static args")
268 mydict = self.__dict__.copy()
269 del mydict['f']
270 globs = {}
271 main_globals = qubx.pyenv.env.globals if qubx.pyenv.env else __main__.__dict__
272 if self.__global_names is None:
273 self.__global_names = []
274
275 locs = dict( (nm, val) for nm, val in izip(self.local_args, self.local_vals) )
276 for nm in self.args:
277 locs[nm] = 1.0
278 unwanted_names = ['__builtins__']
279 valid = False
280 while not valid:
281 try:
282 try:
283 func = eval(self.expr, globs, locs)
284 except (ValueError, OverflowError, ZeroDivisionError):
285 pass
286 valid = True
287 except NameError as msg:
288 nm = MissingName(str(msg))
289 globs[nm] = main_globals[nm]
290 if 'numpy.ufunc' in str(globs[nm].__class__):
291 unwanted_names.append(nm)
292 else:
293 self.__global_names.append(nm)
294 for nm in unwanted_names:
295 if nm in globs:
296 del globs[nm]
297 else:
298 for nm in self.__global_names:
299 globs[nm] = main_globals[nm]
300 return mydict, globs
302 self.__dict__, globals = state
303
304 main_globals = qubx.pyenv.env.globals if qubx.pyenv.env else __main__.__dict__
305 for k,v in globals:
306 main_globals[k] = v
307 locals = dict( (nm, val) for nm, val in izip(self.local_args, self.local_vals) )
308 try:
309 self.f = acceptF(static=self.static, custom=True, typ=self.typ, locals=locals)(self.expr)
310 except:
311 self.f = acceptF()('0')
312
313 -def acceptF(static=["x"], avail=[], custom=False, typ=float, default="1.0", locals=None):
314 """Returns an accept function which converts a Python expression into a L{FuncAcceptor}
315
316 @param static: names arguments which are always present
317 @param avail: names which are added to args if they are in the expression
318 @param custom: whether to add unrecognized names to args, or re-raise the NameError
319 @param typ: desired result type
320 @param default: expression to use in place of ''
321 @param locals: optional dictionary of stuff avail in expr
322 """
323 def accept(eqn):
324 main_globals = qubx.pyenv.env.globals if qubx.pyenv.env else __main__.__dict__
325 ty = any_type if (typ is None) else typ
326 recipe = 'acceptF(static=%s, avail=%s, custom=%s, typ=%s, default=%s, locals=%s)' % (static, avail, custom, ty.__name__, default, locals)
327 local_args = []
328 local_vals = []
329 args = static[:]
330 valid = False
331 func = lambda : 0.0
332 while not valid:
333 try:
334 try:
335 func = eval( "lambda " + join(local_args+args, ',') + ': ' + (eqn or default), main_globals)
336
337 func_args = list(local_vals + [1.0]*(len(args)))
338 func_out = func(*func_args)
339 fval = ty(func_out)
340 except (ValueError, OverflowError, ZeroDivisionError):
341 pass
342 valid = 1
343 except NameError as msg:
344 missing = MissingName(str(msg))
345 if locals and (missing in locals):
346 local_args.append(missing)
347 local_vals.append(locals[missing])
348 elif custom or (missing in avail):
349 args.append(missing)
350 else:
351 raise
352
353 return FuncAcceptor(f=func, args=args, expr=eqn, local_args=local_args, local_vals=local_vals, recipe=recipe, static=static, typ=typ)
354 return accept
355
358
360 """Given an iterable of subiterables, returns a list of unique elements in the subiterables."""
361 have = {}
362 out = []
363 for l in ls:
364 for x in l:
365 if (not (x in have)):
366 have[x] = 1
367 out.append(x)
368 return out
369
371 """
372 @ivar dy: dy(x, custom1, custom2, ..., yname1, yname2, ...) -> [partial of yname wrt x]
373 @ivar args: list of names of args and fit parameters: [x, custom1, custom2, ...]
374 @ivar ynames: list of names of differentiated variables
375 """
376 - def __init__(self, dy, args, ynames, expr):
377 self.f = None
378 self.dy = dy
379 self.args = args
380 self.ynames = ynames
381 self.expr = expr
384
385 -def acceptODEs(static=["x"], avail=[], custom=False, locals=None):
386 """Returns an accept function which converts a string of ODEs into a L{ODEAcceptor}.
387
388 @param static: names arguments which are always present
389 @param avail: names which are added to args if they are in the expression
390 @param custom: whether to add unrecognized names to args, or re-raise the NameError
391 @param locals: optional dictionary of stuff avail in expr
392
393 The expression can have one or more ODEs separated by ';'. Each ODE has the form
394
395 >>> "foo' = do_something(foo, bar)"
396
397 For example, a sinusoidal:
398
399 >>> "a' = b; b' = -a"
400 """
401
402 def accept(expr):
403 nospace = re.sub(r"[ \t]+", "", expr)
404 eqns = [x for x in re.split(r";", nospace) if x]
405 lrs = [re.split(r"'=", eqn) for eqn in eqns]
406 for i,x in enumerate(lrs):
407 if len(x) != 2:
408 raise Exception("Malformed differential equation: %s" % eqns[i])
409 if len(dict(lrs)) < len(lrs):
410 raise Exception("Duplicate variable name.")
411 vars = [l for l,r in lrs]
412 if any((v in static) or (v in avail) for v in vars):
413 raise Exception("Differential variables can't have the same name as data series.")
414
415
416
417
418 accept1 = acceptF(static+vars, avail, custom, locals=locals)
419 static_vars = static+vars
420 customs = [x for x in Union([accept1(r).args for l,r in lrs]) if not (x in static_vars)]
421
422 accept2 = acceptF(static+customs+vars, [], False, locals=locals)
423 derivs = [accept2(r) for l,r in lrs]
424 def deriv(*args, **kw):
425 return [d(*args, **kw) for d in derivs]
426 return ODEAcceptor(dy=deriv, args=static+customs, ynames=vars, expr=expr)
427
428 return accept
429
430
431 -def acceptODEorF(static=["x"], avail=[], custom=False, locals=None):
432 """Returns an accept function which acts like acceptODEs if ' is present, otherwise like acceptF."""
433 aO = acceptODEs(static, avail, custom, locals=locals)
434 aF = acceptF(static, avail, custom, locals=locals)
435 def accept(x):
436 if re.search("'", x):
437 return aO(x)
438 else:
439 return aF(x)
440 return accept
441
442
443
452