Exemplo n.º 1
0
 def __init__(self, variables, scalar_statements):
     BrianASTRenderer.__init__(self, variables)
     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
Exemplo n.º 2
0
 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
Exemplo n.º 3
0
def test_error_messages():
    nr = NodeRenderer()
    expr_expected = [('3^2', '^', '**'),
                     ('int(not_refractory | (v > 30))', '|', 'or'),
                     ('int((v > 30) & (w < 20))', '&', 'and')]
    for expr, expected_1, expected_2 in expr_expected:
        try:
            nr.render_expr(expr)
            raise AssertionError('Excepted {} to raise a '
                                 'SyntaxError.'.format(expr))
        except SyntaxError as exc:
            message = str(exc)
            assert expected_1 in message
            assert expected_2 in message
Exemplo n.º 4
0
def test_error_messages():
    nr = NodeRenderer()
    expr_expected = [('3^2', '^', '**'),
                     ('int(not_refractory | (v > 30))', '|', 'or'),
                     ('int((v > 30) & (w < 20))', '&', 'and')]
    for expr, expected_1, expected_2 in expr_expected:
        try:
            nr.render_expr(expr)
            raise AssertionError('Excepted {} to raise a '
                                 'SyntaxError.'.format(expr))
        except SyntaxError as exc:
            message = str(exc)
            assert expected_1 in message
            assert expected_2 in message
Exemplo n.º 5
0
    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)
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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
Exemplo n.º 8
0
 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
Exemplo n.º 9
0
 def render_node(self, node):
     '''
     Assumes that the node has already been fully processed by BrianASTRenderer
     '''
     if not hasattr(node, 'simplified'):
         node = super(ArithmeticSimplifier, self).render_node(node)
         node.simplified = True
     # can't evaluate vector expressions, so abandon in this case
     if not node.scalar:
         return node
     # No evaluation necessary for simple names or numbers
     if node.__class__.__name__ in [
             'Name', 'NameConstant', 'Num', 'Constant'
     ]:
         return node
     # Don't evaluate stateful nodes (e.g. those containing a rand() call)
     if not node.stateless:
         return node
     # try fully evaluating using assumptions
     expr = NodeRenderer().render_node(node)
     val, evaluated = evaluate_expr(expr, self.assumptions_ns)
     if evaluated:
         if node.dtype == 'boolean':
             val = bool(val)
             if hasattr(ast, 'Constant'):
                 newnode = ast.Constant(val)
             elif hasattr(ast, 'NameConstant'):
                 newnode = ast.NameConstant(val)
             else:
                 # None is the expression context, we don't use it so we just set to None
                 newnode = ast.Name(repr(val), None)
         elif node.dtype == 'integer':
             val = int(val)
         else:
             val = prefs.core.default_float_dtype(val)
         if node.dtype != 'boolean':
             if hasattr(ast, 'Constant'):
                 newnode = ast.Constant(val)
             else:
                 newnode = ast.Num(val)
         newnode.dtype = node.dtype
         newnode.scalar = True
         newnode.stateless = node.stateless
         newnode.complexity = 0
         return newnode
     return node
Exemplo n.º 10
0
    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)
Exemplo n.º 11
0
    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)
Exemplo n.º 12
0
    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)
Exemplo n.º 13
0
 def render_BinOp(self, node):
     node.left.parent = weakref.proxy(node)
     node.right.parent = weakref.proxy(node)
     node.left = self.render_node(node.left)
     node.right = self.render_node(node.right)
     # TODO: we could capture some syntax errors here, e.g. bool+bool
     # captures, e.g. int+float->float
     newdtype = dtype_hierarchy[max(dtype_hierarchy[subnode.dtype]
                                    for subnode in [node.left, node.right])]
     if node.op.__class__.__name__ == 'Div':
         # Division turns integers into floating point values
         newdtype = 'float'
         # Give a warning if the code uses floating point division where it
         # previously might have used floor division
         if node.left.dtype == node.right.dtype == 'integer':
             # This would have led to floor division in earlier versions of
             # Brian (except for the numpy target on Python 3)
             # Ignore cases where the user already took care of this by
             # wrapping the result of the division in int(...) or
             # floor(...)
             if not (hasattr(node, 'parent')
                     and node.parent.__class__.__name__ == 'Call'
                     and node.parent.func.id in ['int', 'floor']):
                 rendered_expr = NodeRenderer().render_node(node)
                 msg = ('The expression "{}" divides two integer values. '
                        'In previous versions of Brian, this would have '
                        'used either an integer ("flooring") or a floating '
                        'point division, depending on the Python version '
                        'and the code generation target. In the current '
                        'version, it always uses a floating point '
                        'division. Explicitly ask for an  integer division '
                        '("//"), or turn one of the operands into a '
                        'floating point value (e.g. replace "1/2" by '
                        '"1.0/2") to no longer receive this '
                        'warning.'.format(rendered_expr))
                 logger.warn(msg, 'floating_point_division', once=True)
     node.dtype = newdtype
     node.scalar = node.left.scalar and node.right.scalar
     node.complexity = 1 + node.left.complexity + node.right.complexity
     node.stateless = node.left.stateless and node.right.stateless
     return node
Exemplo n.º 14
0
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)
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
Exemplo n.º 16
0
 def __init__(self, variables):
     self.variables = variables
     self.boolvars = dict((k, v) for k, v in self.variables.iteritems() if hasattr(v, 'dtype') and v.dtype==bool)
     self.optimisations = OrderedDict()
     self.n = 0
     NodeRenderer.__init__(self, use_vectorisation_idx=False)
Exemplo n.º 17
0
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)
Exemplo n.º 18
0
 def __init__(self, variables):
     self.variables = variables
     self.optimisations = OrderedDict()
     self.n = 0
     NodeRenderer.__init__(self, use_vectorisation_idx=False)
Exemplo n.º 19
0
def parse_expression_dimensions(expr, variables):
    '''
    Returns the unit value of an expression, and checks its validity
    
    Parameters
    ----------
    expr : str
        The expression to check.
    variables : dict
        Dictionary of all variables used in the `expr` (including `Constant`
        objects for external variables)
    
    Returns
    -------
    unit : Quantity
        The output unit of the expression
    
    Raises
    ------
    SyntaxError
        If the expression cannot be parsed, or if it uses ``a**b`` for ``b``
        anything other than a constant number.
    DimensionMismatchError
        If any part of the expression is dimensionally inconsistent.
    '''

    # If we are working on a string, convert to the top level node
    if isinstance(expr, basestring):
        mod = ast.parse(expr, mode='eval')
        expr = mod.body
    if expr.__class__ is getattr(ast, 'NameConstant', None):
        # new class for True, False, None in Python 3.4
        value = expr.value
        if value is True or value is False:
            return DIMENSIONLESS
        else:
            raise ValueError('Do not know how to handle value %s' % value)
    if expr.__class__ is ast.Name:
        name = expr.id
        # Raise an error if a function is called as if it were a variable
        # (most of the time this happens for a TimedArray)
        if name in variables and isinstance(variables[name], Function):
            raise SyntaxError(
                '%s was used like a variable/constant, but it is '
                'a function.' % name)
        if name in variables:
            return variables[name].dim
        elif name in ['True', 'False']:
            return DIMENSIONLESS
        else:
            raise KeyError('Unknown identifier %s' % name)
    elif (expr.__class__ is ast.Num
          or expr.__class__ is getattr(ast, 'Constant', None)):  # Python 3.8
        return DIMENSIONLESS
    elif expr.__class__ is ast.BoolOp:
        # check that the units are valid in each subexpression
        for node in expr.values:
            parse_expression_dimensions(node, variables)
        # but the result is a bool, so we just return 1 as the unit
        return DIMENSIONLESS
    elif expr.__class__ is ast.Compare:
        # check that the units are consistent in each subexpression
        subexprs = [expr.left] + expr.comparators
        subunits = []
        for node in subexprs:
            subunits.append(parse_expression_dimensions(node, variables))
        for left_dim, right_dim in zip(subunits[:-1], subunits[1:]):
            if not have_same_dimensions(left_dim, right_dim):
                msg = (
                    'Comparison of expressions with different units. Expression '
                    '"{}" has unit ({}), while expression "{}" has units ({})'
                ).format(NodeRenderer().render_node(expr.left),
                         get_dimensions(left_dim),
                         NodeRenderer().render_node(expr.comparators[0]),
                         get_dimensions(right_dim))
                raise DimensionMismatchError(msg)
        # but the result is a bool, so we just return 1 as the unit
        return DIMENSIONLESS
    elif expr.__class__ is ast.Call:
        if len(expr.keywords):
            raise ValueError("Keyword arguments not supported.")
        elif getattr(expr, 'starargs', None) is not None:
            raise ValueError("Variable number of arguments not supported")
        elif getattr(expr, 'kwargs', None) is not None:
            raise ValueError("Keyword arguments not supported")

        func = variables.get(expr.func.id, None)
        if func is None:
            raise SyntaxError('Unknown function %s' % expr.func.id)
        if not hasattr(func, '_arg_units') or not hasattr(
                func, '_return_unit'):
            raise ValueError(('Function %s does not specify how it '
                              'deals with units.') % expr.func.id)

        if len(func._arg_units) != len(expr.args):
            raise SyntaxError(
                'Function %s was called with %d parameters, '
                'needs %d.' %
                (expr.func.id, len(expr.args), len(func._arg_units)))

        for idx, (arg,
                  expected_unit) in enumerate(zip(expr.args, func._arg_units)):
            # A "None" in func._arg_units means: No matter what unit
            if expected_unit is None:
                continue
            elif expected_unit == bool:
                if not is_boolean_expression(arg, variables):
                    raise TypeError(
                        ('Argument number %d for function %s was '
                         'expected to be a boolean value, but is '
                         '"%s".') % (idx + 1, expr.func.id,
                                     NodeRenderer().render_node(arg)))
            else:
                arg_unit = parse_expression_dimensions(arg, variables)
                if not have_same_dimensions(arg_unit, expected_unit):
                    msg = (
                        'Argument number {} for function {} does not have the '
                        'correct units. Expression "{}" has units ({}), but '
                        'should be ({}).').format(
                            idx + 1, expr.func.id,
                            NodeRenderer().render_node(arg),
                            get_dimensions(arg_unit),
                            get_dimensions(expected_unit))
                    raise DimensionMismatchError(msg)

        if func._return_unit == bool:
            return DIMENSIONLESS
        elif isinstance(func._return_unit, (Unit, int)):
            # Function always returns the same unit
            return getattr(func._return_unit, 'dim', DIMENSIONLESS)
        else:
            # Function returns a unit that depends on the arguments
            arg_units = [
                parse_expression_dimensions(arg, variables)
                for arg in expr.args
            ]
            return func._return_unit(*arg_units).dim

    elif expr.__class__ is ast.BinOp:
        op = expr.op.__class__.__name__
        left_dim = parse_expression_dimensions(expr.left, variables)
        right_dim = parse_expression_dimensions(expr.right, variables)
        if op in ['Add', 'Sub', 'Mod']:
            # dimensions should be the same
            if left_dim is not right_dim:
                op_symbol = {'Add': '+', 'Sub': '-', 'Mod': '%'}.get(op)
                left_str = NodeRenderer().render_node(expr.left)
                right_str = NodeRenderer().render_node(expr.right)
                left_unit = get_unit_for_display(left_dim)
                right_unit = get_unit_for_display(right_dim)
                error_msg = ('Expression "{left} {op} {right}" uses '
                             'inconsistent units ("{left}" has unit '
                             '{left_unit}; "{right}" '
                             'has unit {right_unit})').format(
                                 left=left_str,
                                 right=right_str,
                                 op=op_symbol,
                                 left_unit=left_unit,
                                 right_unit=right_unit)
                raise DimensionMismatchError(error_msg)
            u = left_dim
        elif op == 'Mult':
            u = left_dim * right_dim
        elif op == 'Div':
            u = left_dim / right_dim
        elif op == 'FloorDiv':
            if not (left_dim is DIMENSIONLESS and right_dim is DIMENSIONLESS):
                raise SyntaxError('Floor division can only be used on '
                                  'dimensionless values.')
            u = DIMENSIONLESS
        elif op == 'Pow':
            if left_dim is DIMENSIONLESS and right_dim is DIMENSIONLESS:
                return DIMENSIONLESS
            n = _get_value_from_expression(expr.right, variables)
            u = left_dim**n
        else:
            raise SyntaxError("Unsupported operation " + op)
        return u
    elif expr.__class__ is ast.UnaryOp:
        op = expr.op.__class__.__name__
        # check validity of operand and get its unit
        u = parse_expression_dimensions(expr.operand, variables)
        if op == 'Not':
            return DIMENSIONLESS
        else:
            return u
    else:
        raise SyntaxError('Unsupported operation ' + str(expr.__class__))
Exemplo n.º 20
0
def test_parse_expressions_python():
    parse_expressions(NodeRenderer(), eval)
Exemplo n.º 21
0
 def __init__(self, variables):
     self.variables = variables
     self.optimisations = OrderedDict()
     self.n = 0
     NodeRenderer.__init__(self, use_vectorisation_idx=False)
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
Exemplo n.º 23
0
def parse_expression_dimensions(expr, variables, orig_expr=None):
    """
    Returns the unit value of an expression, and checks its validity
    
    Parameters
    ----------
    expr : str
        The expression to check.
    variables : dict
        Dictionary of all variables used in the `expr` (including `Constant`
        objects for external variables)
    
    Returns
    -------
    unit : Quantity
        The output unit of the expression
    
    Raises
    ------
    SyntaxError
        If the expression cannot be parsed, or if it uses ``a**b`` for ``b``
        anything other than a constant number.
    DimensionMismatchError
        If any part of the expression is dimensionally inconsistent.
    """

    # If we are working on a string, convert to the top level node
    if isinstance(expr, str):
        orig_expr = expr
        mod = ast.parse(expr, mode='eval')
        expr = mod.body
    if expr.__class__ is getattr(ast, 'NameConstant', None):
        # new class for True, False, None in Python 3.4
        value = expr.value
        if value is True or value is False:
            return DIMENSIONLESS
        else:
            raise ValueError(f'Do not know how to handle value {value}')
    if expr.__class__ is ast.Name:
        name = expr.id
        # Raise an error if a function is called as if it were a variable
        # (most of the time this happens for a TimedArray)
        if name in variables and isinstance(variables[name], Function):
            raise SyntaxError(
                f'{name} was used like a variable/constant, but it is a function.',
                ("<string>", expr.lineno, expr.col_offset + 1, orig_expr))
        if name in variables:
            return get_dimensions(variables[name])
        elif name in ['True', 'False']:
            return DIMENSIONLESS
        else:
            raise KeyError(f'Unknown identifier {name}')
    elif (expr.__class__ is ast.Num
          or expr.__class__ is getattr(ast, 'Constant', None)):  # Python 3.8
        return DIMENSIONLESS
    elif expr.__class__ is ast.BoolOp:
        # check that the units are valid in each subexpression
        for node in expr.values:
            parse_expression_dimensions(node, variables, orig_expr=orig_expr)
        # but the result is a bool, so we just return 1 as the unit
        return DIMENSIONLESS
    elif expr.__class__ is ast.Compare:
        # check that the units are consistent in each subexpression
        subexprs = [expr.left] + expr.comparators
        subunits = []
        for node in subexprs:
            subunits.append(
                parse_expression_dimensions(node,
                                            variables,
                                            orig_expr=orig_expr))
        for left_dim, right_dim in zip(subunits[:-1], subunits[1:]):
            if not have_same_dimensions(left_dim, right_dim):
                left_expr = NodeRenderer().render_node(expr.left)
                right_expr = NodeRenderer().render_node(expr.comparators[0])
                dim_left = get_dimensions(left_dim)
                dim_right = get_dimensions(right_dim)
                msg = (
                    f"Comparison of expressions with different units. Expression "
                    f"'{left_expr}' has unit ({dim_left}), while expression "
                    f"'{right_expr}' has units ({dim_right}).")
                raise DimensionMismatchError(msg)
        # but the result is a bool, so we just return 1 as the unit
        return DIMENSIONLESS
    elif expr.__class__ is ast.Call:
        if len(expr.keywords):
            raise ValueError("Keyword arguments not supported.")
        elif getattr(expr, 'starargs', None) is not None:
            raise ValueError("Variable number of arguments not supported")
        elif getattr(expr, 'kwargs', None) is not None:
            raise ValueError("Keyword arguments not supported")

        func = variables.get(expr.func.id, None)
        if func is None:
            raise SyntaxError(
                f'Unknown function {expr.func.id}',
                ("<string>", expr.lineno, expr.col_offset + 1, orig_expr))
        if not hasattr(func, '_arg_units') or not hasattr(
                func, '_return_unit'):
            raise ValueError(
                f"Function {expr.func_id} does not specify how it "
                f"deals with units.")

        if len(func._arg_units) != len(expr.args):
            raise SyntaxError(
                f"Function '{expr.func.id}' was called with "
                f"{len(expr.args)} parameters, needs "
                f"{len(func._arg_units)}.",
                ("<string>", expr.lineno,
                 expr.col_offset + len(expr.func.id) + 1, orig_expr))

        for idx, (arg,
                  expected_unit) in enumerate(zip(expr.args, func._arg_units)):
            arg_unit = parse_expression_dimensions(arg,
                                                   variables,
                                                   orig_expr=orig_expr)
            # A "None" in func._arg_units means: No matter what unit
            if expected_unit is None:
                continue
            # A string means: same unit as other argument
            elif isinstance(expected_unit, str):
                arg_idx = func._arg_names.index(expected_unit)
                expected_unit = parse_expression_dimensions(
                    expr.args[arg_idx], variables, orig_expr=orig_expr)
                if not have_same_dimensions(arg_unit, expected_unit):
                    msg = (
                        f'Argument number {idx + 1} for function '
                        f'{expr.func.id} was supposed to have the '
                        f'same units as argument number {arg_idx + 1}, but '
                        f"'{NodeRenderer().render_node(arg)}' has unit "
                        f'{get_unit_for_display(arg_unit)}, while '
                        f"'{NodeRenderer().render_node(expr.args[arg_idx])}' "
                        f'has unit {get_unit_for_display(expected_unit)}')
                    raise DimensionMismatchError(msg)
            elif expected_unit == bool:
                if not is_boolean_expression(arg, variables):
                    rendered_arg = NodeRenderer().render_node(arg)
                    raise TypeError(
                        f"Argument number {idx + 1} for function "
                        f"'{expr.func.id}' was expected to be a boolean "
                        f"value, but is '{rendered_arg}'.")
            else:
                if not have_same_dimensions(arg_unit, expected_unit):
                    rendered_arg = NodeRenderer().render_node(arg)
                    arg_unit_dim = get_dimensions(arg_unit)
                    expected_unit_dim = get_dimensions(expected_unit)
                    msg = (
                        f"Argument number {idx+1} for function {expr.func.id} does "
                        f"not have the correct units. Expression '{rendered_arg}' "
                        f"has units ({arg_unit_dim}), but "
                        f"should be "
                        f"({expected_unit_dim}).")
                    raise DimensionMismatchError(msg)

        if func._return_unit == bool:
            return DIMENSIONLESS
        elif isinstance(func._return_unit, (Unit, int)):
            # Function always returns the same unit
            return getattr(func._return_unit, 'dim', DIMENSIONLESS)
        else:
            # Function returns a unit that depends on the arguments
            arg_units = [
                parse_expression_dimensions(arg,
                                            variables,
                                            orig_expr=orig_expr)
                for arg in expr.args
            ]
            return func._return_unit(*arg_units).dim

    elif expr.__class__ is ast.BinOp:
        op = expr.op.__class__.__name__
        left_dim = parse_expression_dimensions(expr.left,
                                               variables,
                                               orig_expr=orig_expr)
        right_dim = parse_expression_dimensions(expr.right,
                                                variables,
                                                orig_expr=orig_expr)
        if op in ['Add', 'Sub', 'Mod']:
            # dimensions should be the same
            if left_dim is not right_dim:
                op_symbol = {'Add': '+', 'Sub': '-', 'Mod': '%'}.get(op)
                left_str = NodeRenderer().render_node(expr.left)
                right_str = NodeRenderer().render_node(expr.right)
                left_unit = get_unit_for_display(left_dim)
                right_unit = get_unit_for_display(right_dim)
                error_msg = (
                    f"Expression '{left_str} {op_symbol} {right_str}' uses "
                    f"inconsistent units ('{left_str}' has unit "
                    f"{left_unit}; '{right_str}' "
                    f"has unit {right_unit}).")
                raise DimensionMismatchError(error_msg)
            u = left_dim
        elif op == 'Mult':
            u = left_dim * right_dim
        elif op == 'Div':
            u = left_dim / right_dim
        elif op == 'FloorDiv':
            if not (left_dim is DIMENSIONLESS and right_dim is DIMENSIONLESS):
                if left_dim is DIMENSIONLESS:
                    col_offset = expr.right.col_offset + 1
                else:
                    col_offset = expr.left.col_offset + 1
                raise SyntaxError(
                    "Floor division can only be used on "
                    "dimensionless values.",
                    ("<string>", expr.lineno, col_offset, orig_expr))
            u = DIMENSIONLESS
        elif op == 'Pow':
            if left_dim is DIMENSIONLESS and right_dim is DIMENSIONLESS:
                return DIMENSIONLESS
            n = _get_value_from_expression(expr.right, variables)
            u = left_dim**n
        else:
            raise SyntaxError(
                f"Unsupported operation {op}",
                ("<string>", expr.lineno,
                 getattr(expr.left, 'end_col_offset',
                         len(NodeRenderer().render_node(expr.left))) + 1,
                 orig_expr))
        return u
    elif expr.__class__ is ast.UnaryOp:
        op = expr.op.__class__.__name__
        # check validity of operand and get its unit
        u = parse_expression_dimensions(expr.operand,
                                        variables,
                                        orig_expr=orig_expr)
        if op == 'Not':
            return DIMENSIONLESS
        else:
            return u
    else:
        raise SyntaxError(
            f"Unsupported operation {str(expr.__class__.__name__)}",
            ("<string>", expr.lineno, expr.col_offset + 1, orig_expr))