class Max(NumpyAggregate): np_func = np.amax nan_func = (nanmax, ) dtype = firstarg_dtype # manually defined argspec so that is works with bottleneck (which is a # builtin function) argspec = argspec('a, axis=None', **NumpyAggregate.kwonlyargs)
class Where(NumexprFunction): funcname = 'if' argspec = argspec('cond, iftrue, iffalse') @property def cond(self): return self.args[0] @property def iftrue(self): return self.args[1] @property def iffalse(self): return self.args[2] def as_simple_expr(self, context): cond = as_simple_expr(self.cond, context) # filter is stored as an unevaluated expression context_filter = context.filter_expr local_ctx = context.clone() if context_filter is None: local_ctx.filter_expr = self.cond else: # filter = filter and cond local_ctx.filter_expr = LogicalOp('&', context_filter, self.cond) iftrue = as_simple_expr(self.iftrue, local_ctx) if context_filter is None: local_ctx.filter_expr = UnaryOp('~', self.cond) else: # filter = filter and not cond local_ctx.filter_expr = LogicalOp('&', context_filter, UnaryOp('~', self.cond)) iffalse = as_simple_expr(self.iffalse, local_ctx) return Where(cond, iftrue, iffalse) def as_string(self): args = as_string((self.cond, self.iftrue, self.iffalse)) return 'where(%s)' % self.format_args_str(args, []) def dtype(self, context): assert getdtype(self.cond, context) == bool return coerce_types(context, self.iftrue, self.iffalse)
class Choice(NumpyRandom): np_func = np.random.choice # choice(a, size=None, replace=True, p=None) argspec = argspec('choices, p=None, size=None, replace=True', **NumpyRandom.kwonlyargs) def __init__(self, *args, **kwargs): NumpyRandom.__init__(self, *args, **kwargs) probabilities = self.args[0] if isinstance(probabilities, basestring): fpath = os.path.join(config.input_directory, probabilities) probabilities = load_ndarray(fpath) # XXX: store args in a list so that we can modify it? # self.args[1] = load_ndarray(fpath, float) # XXX: but we should be able to do better than a list, eg. # self.args.need = load_ndarray(fpath, float) self.args = (probabilities, ) + self.args[1:] # TODO: document the change in behavior for the case where the sum of # probabilities is != 1 # random.choice only checks that the error is < 1e-8 but always # divides probabilities by sum(p). It is probably a better choice # because it distributes the error to all bins instead of only # adjusting the probability of the last choice. # We override _eval_args only to change the order of arguments because we # do not use the same order than numpy def _eval_args(self, context): (a, p, size, replace), kwargs = NumpyRandom._eval_args(self, context) return (a, size, replace, p), kwargs def compute(self, context, a, size=None, replace=True, p=None): if isinstance(a, la.LArray): assert p is None outcomes_axis = a.axes['outcomes'] outcomes = outcomes_axis.labels other_axes = a.axes - outcomes_axis if other_axes: a = index_array_by_variables(a, context, other_axes) p = np.asarray(a.transpose('outcomes')) else: p = np.asarray(a) a = outcomes if isinstance(p, (list, np.ndarray)) and len(p) and not np.isscalar(p[0]): assert len(p) == len(a) assert all(len(px) == size for px in p) assert len(a) >= 2 if isinstance(p, list) and any( isinstance(px, la.LArray) for px in p): p = [np.asarray(px) for px in p] ap = np.asarray(p) cdf = ap.cumsum(axis=0) # copied & adapted from numpy/random/mtrand/mtrand.pyx atol = np.sqrt(np.finfo(np.float64).eps) if np.issubdtype(ap.dtype, np.floating): atol = max(atol, np.sqrt(np.finfo(ap.dtype).eps)) if np.any(np.abs(cdf[-1] - 1.) > atol): raise ValueError("probabilities do not sum to 1") cdf /= cdf[-1] # I have not found a way to do this without an explicit loop as # np.digitize only supports a 1d array for bins. What we do is # worse than a linear "search" since we always evaluate all # possibilities (there is no shortcut when the value is found). # It might be faster to rewrite this using numba + np.digitize # for each individual (assuming it has a low setup overhead). # the goal is to build something like: # if(u < proba1, outcome1, # if(u < proba2, outcome2, # outcome3)) data = {'u': np.random.uniform(size=size)} expr = a[-1] # iterate in reverse and skip last pairs = zip(cdf[-2::-1], a[-2::-1]) for i, (proba_x, outcome_x) in enumerate(pairs): data['p%d' % i] = proba_x expr = Where( ComparisonOp('<', Variable(None, 'u'), Variable(None, 'p%d' % i)), outcome_x, expr) local_ctx = context.clone(fresh_data=True, entity_data=data) return expr.evaluate(local_ctx) else: return NumpyRandom.compute(self, context, a, size, replace, p) dtype = firstarg_dtype
def parse_function(self, k, v, context): if isinstance(v, list): # v should be a list of dicts (assignments) or strings (actions) if "(" in k: k, args = split_signature(k) argnames = argspec(args).args code_def = v else: argnames, code_def = [], v template = """\ Function definitions should have parentheses after their name even if they have no argument. Please change "{name}:" to "{name}():". You probably want to use the "upgrade" command to automatically convert your model file to the new syntax.""" warnings.warn(template.format(name=k), UserDeprecationWarning) method_context = self.get_group_context(context, argnames) code = self.parse_process_group(k + "_code", code_def, method_context, purge=False) # TODO: use code.predictors instead (but it currently # fails for some reason) or at least factor this out # with the code in parse_process_group group_expressions = [ list(elem.items())[0] if isinstance(elem, dict) else (None, elem) for elem in code_def ] group_predictors = self.collect_predictors(group_expressions, in_process_group=True) method_context = self.get_group_context(method_context, group_predictors) return Function(k, self, argnames, code) elif isinstance(v, dict) and ('args' in v or 'code' in v or 'return' in v): args = v.get('args', '') code = v.get('code', '') result = v.get('return', '') oldargs = "\n args: {}".format(args) \ if args else '' oldcode = "\n code:\n - ..." \ if code else '' newcode = "\n - ..." if code else '' oldresult = "\n return: " + result \ if result else '' newresult = "\n - return " + result \ if result else '' template = """ This syntax for defining functions with arguments or a return value is not supported anymore: {funcname}:{oldargs}{oldcode}{oldresult} Please use this instead: {funcname}({newargs}):{newcode}{newresult}""" msg = template.format(funcname=k, oldargs=oldargs, oldcode=oldcode, oldresult=oldresult, newargs=args, newcode=newcode, newresult=newresult) raise SyntaxError(msg) elif isinstance(v, dict) and 'predictor' in v: raise ValueError("Using the 'predictor' keyword is " "not supported anymore. " "If you need several processes to " "write to the same variable, you " "should rather use functions.") elif isinstance(v, (basestring, bool, int, float)): if k in self.fields.names: msg = """defining a process outside of a function is deprecated because it is ambiguous. You should: * wrap the '{name}: {expr}' assignment inside a function like this: compute_{name}: # you can name it any way you like but simply '{name}' is not recommended ! - {name}: {expr} * update the simulation.processes list to use 'compute_{name}' (the function name) instead of '{name}'. """ else: msg = """defining a process outside of a function is deprecated because it is ambiguous. 1) If '{name}: {expr}' is an assignment ('{name}' stores the result of '{expr}'), you should: * wrap the assignment inside a function, for example, like this: compute_{name}: # you can name it any way you like but simply '{name}' is not recommended ! - {name}: {expr} * update the simulation.processes list to use 'compute_{name}' (the function name) instead of '{name}'. * add '{name}' in the entities fields with 'output: False' 2) otherwise if '{expr}' is an expression which does not return any value, you can simply transform it into a function, like this: {name}: - {expr} """ warnings.warn(msg.format(name=k, expr=v), UserDeprecationWarning) # TODO: it would be cleaner if the process was wrapped in a function return self.parse_expr(k, v, context) else: raise Exception("unknown expression type for %s: %s (%s)" % (k, v, type(v)))
class FuncClass(baseclass): np_func = evalfunc funcname = name argspec = argspec(args, **baseclass.kwonlyargs) if dtypefunc is not None: dtype = dtypefunc
class Exp(NumexprFunction): argspec = argspec('expr') dtype = always(float)
def parse_function(self, k, v, context): if isinstance(v, list): # v should be a list of dicts (assignments) or strings (actions) if "(" in k: k, args = split_signature(k) argnames = argspec(args).args code_def = v else: argnames, code_def = [], v template = """\ Function definitions should have parentheses after their name even if they have no argument. Please change "{name}:" to "{name}():". You probably want to use the "upgrade" command to automatically convert your model file to the new syntax.""" warnings.warn(template.format(name=k), UserDeprecationWarning) method_context = self.get_group_context(context, argnames) code = self.parse_process_group(k + "_code", code_def, method_context, purge=False) # TODO: use code.predictors instead (but it currently # fails for some reason) or at least factor this out # with the code in parse_process_group group_expressions = [list(elem.items())[0] if isinstance(elem, dict) else (None, elem) for elem in code_def] group_predictors = self.collect_predictors(group_expressions, in_process_group=True) method_context = self.get_group_context(method_context, group_predictors) return Function(k, self, argnames, code) elif isinstance(v, dict) and ('args' in v or 'code' in v or 'return' in v): args = v.get('args', '') code = v.get('code', '') result = v.get('return', '') oldargs = "\n args: {}".format(args) \ if args else '' oldcode = "\n code:\n - ..." \ if code else '' newcode = "\n - ..." if code else '' oldresult = "\n return: " + result \ if result else '' newresult = "\n - return " + result \ if result else '' template = """ This syntax for defining functions with arguments or a return value is not supported anymore: {funcname}:{oldargs}{oldcode}{oldresult} Please use this instead: {funcname}({newargs}):{newcode}{newresult}""" msg = template.format(funcname=k, oldargs=oldargs, oldcode=oldcode, oldresult=oldresult, newargs=args, newcode=newcode, newresult=newresult) raise SyntaxError(msg) elif isinstance(v, dict) and 'predictor' in v: raise ValueError("Using the 'predictor' keyword is " "not supported anymore. " "If you need several processes to " "write to the same variable, you " "should rather use functions.") elif isinstance(v, (basestring, bool, int, float)): if k in self.fields.names: msg = """defining a process outside of a function is deprecated because it is ambiguous. You should: * wrap the '{name}: {expr}' assignment inside a function like this: compute_{name}: # you can name it any way you like but simply '{name}' is not recommended ! - {name}: {expr} * update the simulation.processes list to use 'compute_{name}' (the function name) instead of '{name}'. """ else: msg = """defining a process outside of a function is deprecated because it is ambiguous. 1) If '{name}: {expr}' is an assignment ('{name}' stores the result of '{expr}'), you should: * wrap the assignment inside a function, for example, like this: compute_{name}: # you can name it any way you like but simply '{name}' is not recommended ! - {name}: {expr} * update the simulation.processes list to use 'compute_{name}' (the function name) instead of '{name}'. * add '{name}' in the entities fields with 'output: False' 2) otherwise if '{expr}' is an expression which does not return any value, you can simply transform it into a function, like this: {name}: - {expr} """ warnings.warn(msg.format(name=k, expr=v), UserDeprecationWarning) # TODO: it would be cleaner if the process was wrapped in a function return self.parse_expr(k, v, context) else: raise Exception("unknown expression type for %s: %s (%s)" % (k, v, type(v)))