def cancel_identical_terms(primary, inverted): ''' Cancel terms in a collection, e.g. a+b-a should be cancelled to b Simply renders the nodes into expressions and removes whenever there is a common expression in primary and inverted. Parameters ---------- primary : list of AST nodes These are the nodes that are positive with respect to the operator, e.g. in x*y/z it would be [x, y]. inverted : list of AST nodes These are the nodes that are inverted with respect to the operator, e.g. in x*y/z it would be [z]. Returns ------- primary : list of AST nodes Primary nodes after cancellation inverted : list of AST nodes Inverted nodes after cancellation ''' nr = NodeRenderer(use_vectorisation_idx=False) expressions = dict((node, nr.render_node(node)) for node in primary) expressions.update(dict((node, nr.render_node(node)) for node in inverted)) new_primary = [] inverted_expressions = [expressions[term] for term in inverted] for term in primary: expr = expressions[term] if expr in inverted_expressions and term.stateless: new_inverted = [] for iterm in inverted: if expressions[iterm] == expr: expr = '' # handled else: new_inverted.append(iterm) inverted = new_inverted inverted_expressions = [expressions[term] for term in inverted] else: new_primary.append(term) return new_primary, inverted
def render_node(self, node): expr = NodeRenderer(use_vectorisation_idx=False).render_node(node) # Do not pull out constants or numbers if node.__class__.__name__ in ['Name', 'Num', 'NameConstant']: return expr if is_scalar_expression(expr, self.variables) and not has_non_float(expr, self.variables): if expr in self.optimisations: name = self.optimisations[expr] else: self.n += 1 name = '_lio_const_'+str(self.n) self.optimisations[expr] = name return name else: return NodeRenderer.render_node(self, node)
def render_node(self, node): expr = NodeRenderer(use_vectorisation_idx=False).render_node(node) if is_scalar_expression(expr, self.variables) and not has_non_float(expr, self.variables): if expr in self.optimisations: name = self.optimisations[expr] else: # Do not pull out very simple expressions (including constants # and numbers) sympy_expr = str_to_sympy(expr) if sympy.count_ops(sympy_expr, visual=False) < 2: return expr self.n += 1 name = '_lio_const_'+str(self.n) self.optimisations[expr] = name return name else: return NodeRenderer.render_node(self, node)
def render_node(self, node): expr = NodeRenderer(use_vectorisation_idx=False).render_node(node) if is_scalar_expression(expr, self.variables) and not has_non_float( expr, self.variables): if expr in self.optimisations: name = self.optimisations[expr] else: # Do not pull out very simple expressions (including constants # and numbers) sympy_expr = str_to_sympy(expr) if sympy.count_ops(sympy_expr, visual=False) < 2: return expr self.n += 1 name = '_lio_const_' + str(self.n) self.optimisations[expr] = name return name else: return NodeRenderer.render_node(self, node)
def render_node(self, node): expr = NodeRenderer(use_vectorisation_idx=False).render_node(node) if is_scalar_expression(expr, self.variables) and not has_non_float(expr, self.variables): if expr in self.optimisations: name = self.optimisations[expr] else: # Do not pull out very simple expressions (including constants # and numbers) sympy_expr = str_to_sympy(expr) if expression_complexity(sympy_expr) < 2: return expr self.n += 1 name = '_lio_const_'+str(self.n) self.optimisations[expr] = name return name else: for varname, var in self.boolvars.iteritems(): expr_0 = word_substitute(expr, {varname: '0.0'}) expr_1 = '(%s)-(%s)' % (word_substitute(expr, {varname: '1.0'}), expr_0) if (is_scalar_expression(expr_0, self.variables) and is_scalar_expression(expr_1, self.variables) and not has_non_float(expr, self.variables)): # we do this check here because we don't want to apply it to statements, only expressions if expression_complexity(expr)<=4: break if expr_0 not in self.optimisations: self.n += 1 name_0 = '_lio_const_'+str(self.n) self.optimisations[expr_0] = name_0 else: name_0 = self.optimisations[expr_0] if expr_1 not in self.optimisations: self.n += 1 name_1 = '_lio_const_'+str(self.n) self.optimisations[expr_1] = name_1 else: name_1 = self.optimisations[expr_1] newexpr = '({name_0}+{name_1}*int({varname}))'.format(name_0=name_0, name_1=name_1, varname=varname) return newexpr return NodeRenderer.render_node(self, node)
class Simplifier(BrianASTRenderer): ''' Carry out arithmetic simplifications (see `ArithmeticSimplifier`) and loop invariants Parameters ---------- variables : dict of (str, Variable) Usual definition of variables. scalar_statements : sequence of Statement Predefined scalar statements that can be used as part of simplification Notes ----- After calling `render_expr` on a sequence of expressions (coming from vector statements typically), this object will have some new attributes: ``loop_invariants`` : OrderedDict of (expression, varname) varname will be of the form ``_lio_N`` where ``N`` is some integer, and the expressions will be strings that correspond to scalar-only expressions that can be evaluated outside of the vector block. ``loop_invariant_dtypes`` : dict of (varname, dtypename) dtypename will be one of ``'boolean'``, ``'integer'``, ``'float'``. ''' def __init__(self, variables, scalar_statements, extra_lio_prefix=''): BrianASTRenderer.__init__(self, variables, copy_variables=False) self.loop_invariants = OrderedDict() self.loop_invariant_dtypes = {} self.n = 0 self.node_renderer = NodeRenderer(use_vectorisation_idx=False) self.arithmetic_simplifier = ArithmeticSimplifier(variables) self.scalar_statements = scalar_statements if extra_lio_prefix is None: extra_lio_prefix = '' if len(extra_lio_prefix): extra_lio_prefix = extra_lio_prefix+'_' self.extra_lio_prefix = extra_lio_prefix def render_expr(self, expr): node = brian_ast(expr, self.variables) node = self.arithmetic_simplifier.render_node(node) node = self.render_node(node) return self.node_renderer.render_node(node) def render_node(self, node): ''' Assumes that the node has already been fully processed by BrianASTRenderer ''' # can we pull this out? if node.scalar and node.complexity>0: expr = self.node_renderer.render_node(self.arithmetic_simplifier.render_node(node)) if expr in self.loop_invariants: name = self.loop_invariants[expr] else: self.n += 1 name = '_lio_'+self.extra_lio_prefix+str(self.n) self.loop_invariants[expr] = name self.loop_invariant_dtypes[name] = node.dtype numpy_dtype = {'boolean': bool, 'integer': int, 'float': float}[node.dtype] self.variables[name] = AuxiliaryVariable(name, Unit(1), dtype=numpy_dtype, scalar=True) # None is the expression context, we don't use it so we just set to None newnode = ast.Name(name, None) newnode.scalar = True newnode.dtype = node.dtype newnode.complexity = 0 newnode.stateless = node.stateless return newnode # otherwise, render node as usual return super(Simplifier, self).render_node(node)
class Simplifier(BrianASTRenderer): ''' Carry out arithmetic simplifications (see `ArithmeticSimplifier`) and loop invariants Parameters ---------- variables : dict of (str, Variable) Usual definition of variables. scalar_statements : sequence of Statement Predefined scalar statements that can be used as part of simplification Notes ----- After calling `render_expr` on a sequence of expressions (coming from vector statements typically), this object will have some new attributes: ``loop_invariants`` : OrderedDict of (expression, varname) varname will be of the form ``_lio_N`` where ``N`` is some integer, and the expressions will be strings that correspond to scalar-only expressions that can be evaluated outside of the vector block. ``loop_invariant_dtypes`` : dict of (varname, dtypename) dtypename will be one of ``'boolean'``, ``'integer'``, ``'float'``. ''' def __init__(self, variables, scalar_statements, extra_lio_prefix=''): BrianASTRenderer.__init__(self, variables, copy_variables=False) self.loop_invariants = OrderedDict() self.loop_invariant_dtypes = {} self.n = 0 self.node_renderer = NodeRenderer(use_vectorisation_idx=False) self.arithmetic_simplifier = ArithmeticSimplifier(variables) self.scalar_statements = scalar_statements if extra_lio_prefix is None: extra_lio_prefix = '' if len(extra_lio_prefix): extra_lio_prefix = extra_lio_prefix + '_' self.extra_lio_prefix = extra_lio_prefix def render_expr(self, expr): node = brian_ast(expr, self.variables) node = self.arithmetic_simplifier.render_node(node) node = self.render_node(node) return self.node_renderer.render_node(node) def render_node(self, node): ''' Assumes that the node has already been fully processed by BrianASTRenderer ''' # can we pull this out? if node.scalar and node.complexity > 0: expr = self.node_renderer.render_node( self.arithmetic_simplifier.render_node(node)) if expr in self.loop_invariants: name = self.loop_invariants[expr] else: self.n += 1 name = '_lio_' + self.extra_lio_prefix + str(self.n) self.loop_invariants[expr] = name self.loop_invariant_dtypes[name] = node.dtype numpy_dtype = { 'boolean': bool, 'integer': int, 'float': prefs.core.default_float_dtype }[node.dtype] self.variables[name] = AuxiliaryVariable(name, dtype=numpy_dtype, scalar=True) # None is the expression context, we don't use it so we just set to None newnode = ast.Name(name, None) newnode.scalar = True newnode.dtype = node.dtype newnode.complexity = 0 newnode.stateless = node.stateless return newnode # otherwise, render node as usual return super(Simplifier, self).render_node(node)
def parse_synapse_generator(expr): ''' Returns a parsed form of a synapse generator expression. The general form is: ``element for iteration_variable in iterator_func(...)`` or ``element for iteration_variable in iterator_func(...) if if_expression`` Returns a dictionary with keys: ``original_expression`` The original expression as a string. ``element`` As above, a string expression. ``iteration_variable`` A variable name, as above. ``iterator_func`` String. Either ``range`` or ``sample``. ``if_expression`` String expression or ``None``. ``iterator_kwds`` Dictionary of key/value pairs representing the keywords. See `handle_range` and `handle_sample`. ''' nr = NodeRenderer(use_vectorisation_idx=False) parse_error = ("Error parsing expression '%s'. Expression must have " "generator syntax, for example 'k for k in range(i-10, " "i+10)'." % expr) try: node = ast.parse('[%s]' % expr, mode='eval').body except Exception as e: raise SyntaxError(parse_error + " Error encountered was %s" % e) if _cname(node) != 'ListComp': raise SyntaxError(parse_error + " Expression is not a generator " "expression.") element = node.elt if len(node.generators) != 1: raise SyntaxError(parse_error + " Generator expression must involve " "only one iterator.") generator = node.generators[0] target = generator.target if _cname(target) != 'Name': raise SyntaxError(parse_error + " Generator must iterate over a single " "variable (not tuple, etc.).") iteration_variable = target.id iterator = generator.iter if _cname(iterator) != 'Call' or _cname(iterator.func) != 'Name': raise SyntaxError(parse_error + " Iterator expression must be one of " "the supported functions: " + str(list(iterator_function_handlers))) iterator_funcname = iterator.func.id if iterator_funcname not in iterator_function_handlers: raise SyntaxError(parse_error + " Iterator expression must be one of " "the supported functions: " + str(list(iterator_function_handlers))) if (getattr(iterator, 'starargs', None) is not None or getattr(iterator, 'kwargs', None) is not None): raise SyntaxError(parse_error + " Star arguments not supported.") args = [] for argnode in iterator.args: args.append(nr.render_node(argnode)) keywords = {} for kwdnode in iterator.keywords: keywords[kwdnode.arg] = nr.render_node(kwdnode.value) try: iterator_handler = iterator_function_handlers[iterator_funcname] iterator_kwds = iterator_handler(*args, **keywords) except SyntaxError as exc: raise SyntaxError(parse_error + " " + exc.msg) if len(generator.ifs) == 0: condition = ast.parse('True', mode='eval').body elif len(generator.ifs) > 1: raise SyntaxError(parse_error + " Generator must have at most one if " "statement.") else: condition = generator.ifs[0] parsed = { 'original_expression': expr, 'element': nr.render_node(element), 'iteration_variable': iteration_variable, 'iterator_func': iterator_funcname, 'iterator_kwds': iterator_kwds, 'if_expression': nr.render_node(condition), } return parsed
def parse_synapse_generator(expr): ''' Returns a parsed form of a synapse generator expression. The general form is: ``element for iteration_variable in iterator_func(...)`` or ``element for iteration_variable in iterator_func(...) if if_expression`` Returns a dictionary with keys: ``original_expression`` The original expression as a string. ``element`` As above, a string expression. ``iteration_variable`` A variable name, as above. ``iterator_func`` String. Either ``range`` or ``sample``. ``if_expression`` String expression or ``None``. ``iterator_kwds`` Dictionary of key/value pairs representing the keywords. See `handle_range` and `handle_sample`. ''' nr = NodeRenderer(use_vectorisation_idx=False) parse_error = ("Error parsing expression '%s'. Expression must have " "generator syntax, for example 'k for k in range(i-10, " "i+10)'." % expr) try: node = ast.parse('[%s]' % expr, mode='eval').body except Exception as e: raise SyntaxError(parse_error + " Error encountered was %s" % e) if _cname(node) != 'ListComp': raise SyntaxError(parse_error + " Expression is not a generator " "expression.") element = node.elt if len(node.generators) != 1: raise SyntaxError(parse_error + " Generator expression must involve " "only one iterator.") generator = node.generators[0] target = generator.target if _cname(target) != 'Name': raise SyntaxError(parse_error + " Generator must iterate over a single " "variable (not tuple, etc.).") iteration_variable = target.id iterator = generator.iter if _cname(iterator) != 'Call' or _cname(iterator.func) != 'Name': raise SyntaxError(parse_error + " Iterator expression must be one of " "the supported functions: " + str(iterator_function_handlers.keys())) iterator_funcname = iterator.func.id if iterator_funcname not in iterator_function_handlers: raise SyntaxError(parse_error + " Iterator expression must be one of " "the supported functions: " + str(iterator_function_handlers.keys())) if (getattr(iterator, 'starargs', None) is not None or getattr(iterator, 'kwargs', None) is not None): raise SyntaxError(parse_error + " Star arguments not supported.") args = [] for argnode in iterator.args: args.append(nr.render_node(argnode)) keywords = {} for kwdnode in iterator.keywords: keywords[kwdnode.arg] = nr.render_node(kwdnode.value) try: iterator_handler = iterator_function_handlers[iterator_funcname] iterator_kwds = iterator_handler(*args,**keywords) except SyntaxError as exc: raise SyntaxError(parse_error + " " + exc.msg) if len(generator.ifs) == 0: condition = ast.parse('True', mode='eval').body elif len(generator.ifs) > 1: raise SyntaxError(parse_error + " Generator must have at most one if " "statement.") else: condition = generator.ifs[0] parsed = { 'original_expression': expr, 'element': nr.render_node(element), 'iteration_variable': iteration_variable, 'iterator_func': iterator_funcname, 'iterator_kwds': iterator_kwds, 'if_expression': nr.render_node(condition), } return parsed