Exemple #1
0
def test_is_boolean_expression():
    # dummy "Variable" class
    Var = namedtuple("Var", ['is_boolean'])

    # dummy function object
    class Func(object):
        def __init__(self, returns_bool=False):
            self._returns_bool = returns_bool

    # variables / functions
    a = Constant('a', value=True)
    b = Constant('b', value=False)
    c = Constant('c', value=5)
    f = Func(returns_bool=True)
    g = Func(returns_bool=False)
    s1 = Var(is_boolean=True)
    s2 = Var(is_boolean=False)

    variables = {'a': a, 'b': b, 'c': c, 'f': f, 'g': g, 's1': s1, 's2': s2}

    EVF = [
        (True, 'a or b'),
        (False, 'c'),
        (False, 's2'),
        (False, 'g(s1)'),
        (True, 's2 > c'),
        (True, 'c > 5'),
        (True, 'True'),
        (True, 'a<b'),
        (True, 'not (a>=b)'),
        (False, 'a+b'),
        (True, 'f(c)'),
        (False, 'g(c)'),
        (
            True,
            'f(c) or a<b and s1',
        ),
    ]
    for expect, expr in EVF:
        ret_val = is_boolean_expression(expr, variables)
        if expect != ret_val:
            raise AssertionError(
                ('is_boolean_expression(%r) returned %s, '
                 'but was supposed to return %s') % (expr, ret_val, expect))
    with pytest.raises(SyntaxError):
        is_boolean_expression('a<b and c', variables)
    with pytest.raises(SyntaxError):
        is_boolean_expression('a or foo', variables)
    with pytest.raises(SyntaxError):
        is_boolean_expression(
            'ot a',  # typo
            variables)
    with pytest.raises(SyntaxError):
        is_boolean_expression('g(c) and f(a)', variables)
def test_warning_internal_variables():
    group1 = SimpleGroup(namespace=None,
                         variables={'N': Constant('N', 5)})
    group2 = SimpleGroup(namespace=None,
                         variables={'N': Constant('N', 7)})
    with catch_logs() as l:
        group1.resolve_all(['N'], run_namespace={'N': 5})  # should not raise a warning
        assert len(l) == 0, 'got warnings: %s' % str(l)
    with catch_logs() as l:
        group2.resolve_all(['N'], run_namespace={'N': 5})  # should raise a warning
        assert len(l) == 1, 'got warnings: %s' % str(l)
        assert l[0][1].endswith('.resolution_conflict')
Exemple #3
0
def test_apply_loop_invariant_optimisation():
    variables = {'v': Variable('v', scalar=False),
                 'w': Variable('w', scalar=False),
                 'dt': Constant('dt', dimensions=second.dim, value=0.1*ms),
                 'tau': Constant('tau', dimensions=second.dim, value=10*ms),
                 'exp': DEFAULT_FUNCTIONS['exp']}
    statements = [Statement('v', '=', 'dt*w*exp(-dt/tau)/tau + v*exp(-dt/tau)', '', np.float32),
                  Statement('w', '=', 'w*exp(-dt/tau)', '', np.float32)]
    scalar, vector = optimise_statements([], statements, variables)
    # The optimisation should pull out at least exp(-dt / tau)
    assert len(scalar) >= 1
    assert np.issubdtype(scalar[0].dtype, np.floating)
    assert scalar[0].var == '_lio_1'
    assert len(vector) == 2
    assert all('_lio_' in stmt.expr for stmt in vector)
Exemple #4
0
def test_apply_loop_invariant_optimisation_constant_evaluation():
    variables = {
        'v1': Variable('v1', scalar=False),
        'v2': Variable('v2', scalar=False),
        'i1': Variable('i1', scalar=False, dtype=int),
        'N': Constant('N', 10),
        's1': Variable('s1', scalar=True, dtype=float),
        's2': Variable('s2', scalar=True, dtype=float),
        'exp': DEFAULT_FUNCTIONS['exp']
    }
    statements = [
        Statement('v1', '=', 'v1 * (1 + 2 + 3)', '', np.float),
        Statement('v1', '=', 'exp(N)*v1', '', np.float),
        Statement('v1', '=', 'exp(0)*v1', '', np.float),
    ]
    scalar, vector = optimise_statements([], statements, variables)
    # exp(N) should be pulled out of the vector statements, the rest should be
    # evaluated in place
    assert len(scalar) == 1
    assert scalar[0].expr == 'exp(N)'
    assert len(vector) == 3
    expr = vector[0].expr.replace(' ', '')
    assert expr == '_lio_1*v1' or 'v1*_lio_1'
    expr = vector[1].expr.replace(' ', '')
    assert expr == '6.0*v1' or 'v1*6.0'
    assert vector[2].expr == 'v1'
Exemple #5
0
def test_apply_loop_invariant_optimisation_no_optimisation():
    variables = {
        'v1': Variable('v1', scalar=False),
        'v2': Variable('v2', scalar=False),
        'N': Constant('N', 10),
        's1': Variable('s1', scalar=True, dtype=float),
        's2': Variable('s2', scalar=True, dtype=float),
        'rand': DEFAULT_FUNCTIONS['rand']
    }
    statements = [
        # This hould not be simplified to 0!
        Statement('v1', '=', 'rand() - rand()', '', np.float),
        Statement('v1', '=', '3*rand() - 3*rand()', '', np.float),
        Statement('v1', '=', '3*rand() - ((1+2)*rand())', '', np.float),
        # This should not pull out rand()*N
        Statement('v1', '=', 's1*rand()*N', '', np.float),
        Statement('v1', '=', 's2*rand()*N', '', np.float),
        # This is not important mathematically, but it would change the numbers
        # that are generated
        Statement('v1', '=', '0*rand()*N', '', np.float),
        Statement('v1', '=', '0/rand()*N', '', np.float)
    ]
    scalar, vector = optimise_statements([], statements, variables)
    for vs in vector[:3]:
        assert vs.expr.count(
            'rand()'
        ) == 2, 'Expression should still contain two rand() calls, but got ' + str(
            vs)
    for vs in vector[3:]:
        assert vs.expr.count(
            'rand()'
        ) == 1, 'Expression should still contain a rand() call, but got ' + str(
            vs)
Exemple #6
0
def test_apply_loop_invariant_optimisation_integer():
    variables = {
        'v': Variable('v', scalar=False),
        'N': Constant('N', 10),
        'b': Variable('b', scalar=True, dtype=int),
        'c': Variable('c', scalar=True, dtype=int),
        'd': Variable('d', scalar=True, dtype=int),
        'y': Variable('y', scalar=True, dtype=float),
        'z': Variable('z', scalar=True, dtype=float),
        'w': Variable('w', scalar=True, dtype=float),
    }
    statements = [
        Statement('v', '=', 'v % (2*3*N)', '', np.float32),
        # integer version doesn't get rewritten but float version does
        Statement('a', ':=', 'b//(c//d)', '', int),
        Statement('x', ':=', 'y/(z/w)', '', float),
    ]
    scalar, vector = optimise_statements([], statements, variables)
    assert len(scalar) == 3
    assert np.issubdtype(scalar[0].dtype, np.signedinteger)
    assert scalar[0].var == '_lio_1'
    expr = scalar[0].expr.replace(' ', '')
    assert expr == '6*N' or expr == 'N*6'
    assert np.issubdtype(scalar[1].dtype, np.signedinteger)
    assert scalar[1].var == '_lio_2'
    expr = scalar[1].expr.replace(' ', '')
    assert expr == 'b//(c//d)'
    assert np.issubdtype(scalar[2].dtype, np.floating)
    assert scalar[2].var == '_lio_3'
    expr = scalar[2].expr.replace(' ', '')
    assert expr == '(y*w)/z' or expr == '(w*y)/z'
Exemple #7
0
def test_apply_loop_invariant_optimisation_boolean():
    variables = {'v1': Variable('v1', scalar=False),
                 'v2': Variable('v2', scalar=False),
                 'N': Constant('N', 10),
                 'b': Variable('b', scalar=True, dtype=bool),
                 'c': Variable('c', scalar=True, dtype=bool),
                 'int': DEFAULT_FUNCTIONS['int'],
                 'foo': Function(lambda x: None,
                                 arg_units=[Unit(1)], return_unit=Unit(1),
                                 arg_types=['boolean'], return_type='float',
                                 stateless=False)
                 }
    # The calls for "foo" cannot be pulled out, since foo is marked as stateful
    statements = [Statement('v1', '=', '1.0*int(b and c)', '', np.float32),
                  Statement('v1', '=', '1.0*foo(b and c)', '', np.float32),
                  Statement('v2', '=', 'int(not b and True)', '', np.float32),
                  Statement('v2', '=', 'foo(not b and True)', '', np.float32)
                  ]
    scalar, vector = optimise_statements([], statements, variables)
    assert len(scalar) == 4
    assert scalar[0].expr == '1.0 * int(b and c)'
    assert scalar[1].expr == 'b and c'
    assert scalar[2].expr == 'int((not b) and True)'
    assert scalar[3].expr == '(not b) and True'
    assert len(vector) == 4
    assert vector[0].expr == '_lio_1'
    assert vector[1].expr == 'foo(_lio_2)'
    assert vector[2].expr == '_lio_3'
    assert vector[3].expr == 'foo(_lio_4)'
Exemple #8
0
def test_value_from_expression():
    # This function is used to get the value of an exponent, necessary for unit checking

    constants = {'c': 3}
    # dummy class
    class C(object):
        pass
    variables = {'s_constant_scalar': C(), 's_non_constant': C(),
                  's_non_scalar': C()}
    variables['s_constant_scalar'].scalar = True
    variables['s_constant_scalar'].constant = True
    variables['s_constant_scalar'].get_value = lambda: 2.0
    variables['s_non_scalar'].constant = True
    variables['s_non_constant'].scalar = True
    variables['c'] = Constant('c', value=3)

    expressions = ['1', '-0.5', 'c', '2**c', '(c + 3) * 5',
                   'c + s_constant_scalar', 'True', 'False']

    for expr in expressions:
        eval_expr = expr.replace('s_constant_scalar', 's_constant_scalar.get_value()')
        assert float(eval(eval_expr, variables, constants)) == _get_value_from_expression(expr,
                                                                                          variables)

    wrong_expressions = ['s_non_constant', 's_non_scalar', 'c or True']
    for expr in wrong_expressions:
        with pytest.raises(SyntaxError):
            _get_value_from_expression(expr, variables)
Exemple #9
0
def test_apply_loop_invariant_optimisation_integer():
    variables = {
        'v': Variable('v', Unit(1), scalar=False),
        'N': Constant('N', Unit(1), 10)
    }
    statements = [Statement('v', '=', 'v % (2*3*N)', '', np.float32)]
    scalar, vector = apply_loop_invariant_optimisations(
        statements, variables, np.float64)
    # The optimisation should not pull out 2*N
    assert len(scalar) == 0
Exemple #10
0
def test_apply_loop_invariant_optimisation():
    variables = {
        'v': Variable('v', Unit(1), scalar=False),
        'w': Variable('w', Unit(1), scalar=False),
        'dt': Constant('dt', second, 0.1 * ms),
        'tau': Constant('tau', second, 10 * ms),
        'exp': DEFAULT_FUNCTIONS['exp']
    }
    statements = [
        Statement('v', '=', 'dt*w*exp(-dt/tau)/tau + v*exp(-dt/tau)', '',
                  np.float32),
        Statement('w', '=', 'w*exp(-dt/tau)', '', np.float32)
    ]
    scalar, vector = apply_loop_invariant_optimisations(
        statements, variables, np.float64)
    # The optimisation should pull out exp(-dt / tau)
    assert len(scalar) == 1
    assert scalar[0].dtype == np.float64  # We asked for this dtype above
    assert scalar[0].var == '_lio_const_1'
    assert len(vector) == 2
    assert all('_lio_const_1' in stmt.expr for stmt in vector)
Exemple #11
0
def test_apply_loop_invariant_optimisation_simplification():
    variables = {
        'v1': Variable('v1', scalar=False),
        'v2': Variable('v2', scalar=False),
        'i1': Variable('i1', scalar=False, dtype=int),
        'N': Constant('N', 10)
    }
    statements = [
        # Should be simplified to 0.0
        Statement('v1', '=', 'v1 - v1', '', np.float),
        Statement('v1', '=', 'N*v1 - N*v1', '', np.float),
        Statement('v1', '=', 'v1*N * 0', '', np.float),
        Statement('v1', '=', 'v1 * 0', '', np.float),
        Statement('v1', '=', 'v1 * 0.0', '', np.float),
        Statement('v1', '=', '0.0 / (v1*N)', '', np.float),
        # Should be simplified to 0
        Statement('i1', '=', 'i1*N * 0', '', np.int),
        Statement('i1', '=', '0 * i1', '', np.int),
        Statement('i1', '=', '0 * i1*N', '', np.int),
        Statement('i1', '=', 'i1 * 0', '', np.int),
        # Should be simplified to v1*N
        Statement('v2', '=', '0 + v1*N', '', np.float),
        Statement('v2', '=', 'v1*N + 0.0', '', np.float),
        Statement('v2', '=', 'v1*N - 0', '', np.float),
        Statement('v2', '=', 'v1*N - 0.0', '', np.float),
        Statement('v2', '=', '1 * v1*N', '', np.float),
        Statement('v2', '=', '1.0 * v1*N', '', np.float),
        Statement('v2', '=', 'v1*N / 1.0', '', np.float),
        Statement('v2', '=', 'v1*N / 1', '', np.float),
        # Should be simplified to i1
        Statement('i1', '=', 'i1*1', '', int),
        Statement('i1', '=', 'i1//1', '', int),
        Statement('i1', '=', 'i1+0', '', int),
        Statement('i1', '=', '0+i1', '', int),
        Statement('i1', '=', 'i1-0', '', int),
        # Should *not* be simplified (because it would change the type,
        # important for integer division, for example)
        Statement('v1', '=', 'i1*1.0', '', float),
        Statement('v1', '=', '1.0*i1', '', float),
        Statement('v1', '=', 'i1/1.0', '', float),
        Statement('v1', '=', 'i1/1', '', float),
        Statement('v1', '=', 'i1+0.0', '', float),
        Statement('v1', '=', '0.0+i1', '', float),
        Statement('v1', '=', 'i1-0.0', '', float),
        ## Should *not* be simplified, flooring division by 1 changes the value
        Statement('v1', '=', 'v2//1.0', '', float),
        Statement('i1', '=', 'i1//1.0', '', float)  # changes type
    ]
    scalar, vector = optimise_statements([], statements, variables)
    assert len(scalar) == 0
    for s in vector[:6]:
        assert s.expr == '0.0'
    for s in vector[6:10]:
        assert s.expr == '0', s.expr  # integer
    for s in vector[10:18]:
        expr = s.expr.replace(' ', '')
        assert expr == 'v1*N' or expr == 'N*v1'
    for s in vector[18:23]:
        expr = s.expr.replace(' ', '')
        assert expr == 'i1'
    for s in vector[23:27]:
        expr = s.expr.replace(' ', '')
        assert expr == '1.0*i1' or expr == 'i1*1.0' or expr == 'i1/1.0'
    for s in vector[27:30]:
        expr = s.expr.replace(' ', '')
        assert expr == '0.0+i1' or expr == 'i1+0.0'
    for s in vector[30:31]:
        expr = s.expr.replace(' ', '')
        assert expr == 'v2//1.0' or expr == 'v2//1'
    for s in vector[31:]:
        expr = s.expr.replace(' ', '')
        assert expr == 'i1//1.0'
Exemple #12
0
    def _resolve_external(self,
                          identifier,
                          run_namespace,
                          user_identifier=True,
                          internal_variable=None):
        '''
        Resolve an external identifier in the context of a `Group`. If the `Group`
        declares an explicit namespace, this namespace is used in addition to the
        standard namespace for units and functions. Additionally, the namespace in
        the `run_namespace` argument (i.e. the namespace provided to `Network.run`)
        is used.

        Parameters
        ----------
        identifier : str
            The name to resolve.
        group : `Group`
            The group that potentially defines an explicit namespace for looking up
            external names.
        run_namespace : dict
            A namespace (mapping from strings to objects), as provided as an
            argument to the `Network.run` function or returned by
            `get_local_namespace`.
        user_identifier : bool, optional
            Whether this is an identifier that was used by the user (and not
            something automatically generated that the user might not even
            know about). Will be used to determine whether to display a
            warning in the case of namespace clashes. Defaults to ``True``.
        internal_variable : `Variable`, optional
            The internal variable object that corresponds to this name (if any).
            This is used to give warnings if it also corresponds to a variable
            from an external namespace.
        '''
        # We save tuples of (namespace description, referred object) to
        # give meaningful warnings in case of duplicate definitions
        matches = []

        namespaces = OrderedDict()
        # Default namespaces (units and functions)
        namespaces['constants'] = DEFAULT_CONSTANTS
        namespaces['units'] = DEFAULT_UNITS
        namespaces['functions'] = DEFAULT_FUNCTIONS
        if getattr(self, 'namespace', None) is not None:
            namespaces['group-specific'] = self.namespace

        # explicit or implicit run namespace
        namespaces['run'] = run_namespace

        for description, namespace in namespaces.iteritems():
            if identifier in namespace:
                match = namespace[identifier]
                if ((isinstance(match, (numbers.Number, np.ndarray, np.number,
                                        Function, Variable)))
                        or (inspect.isfunction(match)
                            and hasattr(match, '_arg_units')
                            and hasattr(match, '_return_unit'))):
                    matches.append((description, match))

        if len(matches) == 0:
            # No match at all
            if internal_variable is not None:
                return None
            else:
                raise KeyError(('The identifier "%s" could not be resolved.') %
                               (identifier))

        elif len(matches) > 1:
            # Possibly, all matches refer to the same object
            first_obj = matches[0][1]
            found_mismatch = False
            for m in matches:
                if _same_value(m[1], first_obj):
                    continue
                if _same_function(m[1], first_obj):
                    continue
                try:
                    proxy = weakref.proxy(first_obj)
                    if m[1] is proxy:
                        continue
                except TypeError:
                    pass

                # Found a mismatch
                found_mismatch = True
                break

            if found_mismatch and user_identifier and internal_variable is None:
                _conflict_warning(
                    ('The name "%s" refers to different objects '
                     'in different namespaces used for resolving '
                     'names in the context of group "%s". '
                     'Will use the object from the %s namespace '
                     'with the value %s,') %
                    (identifier, getattr(self, 'name', '<unknown>'),
                     matches[0][0], _display_value(first_obj)), matches[1:])

        if internal_variable is not None and user_identifier:
            # Filter out matches that are identical (a typical case being an
            # externally defined "N" with the the number of neurons and a later
            # use of "N" in an expression (which refers to the internal variable
            # storing the number of neurons in the group)
            if isinstance(internal_variable, Constant):
                filtered_matches = []
                for match in matches:
                    if not _same_value(match[1], internal_variable):
                        filtered_matches.append(match)
            else:
                filtered_matches = matches
            if len(filtered_matches) == 0:
                pass  # Nothing to warn about
            else:
                warning_message = ('"{name}" is an internal variable of group '
                                   '"{group}", but also exists in the ')
                if len(matches) == 1:
                    warning_message += ('{namespace} namespace with the value '
                                        '{value}. ').format(
                                            namespace=filtered_matches[0][0],
                                            value=_display_value(
                                                filtered_matches[0][1]))
                else:
                    warning_message += ('following namespaces: '
                                        '{namespaces}. ').format(
                                            namespaces=' ,'.join(
                                                match[0]
                                                for match in filtered_matches))
                warning_message += 'The internal variable will be used.'
                logger.warn(warning_message.format(name=identifier,
                                                   group=self.name),
                            'Group.resolve.resolution_conflict',
                            once=True)

        if internal_variable is not None:
            return None  # We were only interested in the warnings above

        # use the first match (according to resolution order)
        resolved = matches[0][1]

        # Replace pure Python functions by a Functions object
        if callable(resolved) and not isinstance(resolved, Function):
            resolved = Function(resolved, stateless=False)

        if not isinstance(resolved, (Function, Variable)):
            # Wrap the value in a Constant object
            unit = get_unit(resolved)
            value = np.asarray(resolved)
            if value.shape != ():
                raise KeyError('Variable %s was found in the namespace, but is'
                               ' not a scalar value' % identifier)
            resolved = Constant(identifier, unit=unit, value=value)

        return resolved
Exemple #13
0
    def _resolve_external(self,
                          identifier,
                          run_namespace=None,
                          level=0,
                          do_warn=True):
        '''
        Resolve an external identifier in the context of a `Group`. If the `Group`
        declares an explicit namespace, this namespace is used in addition to the
        standard namespace for units and functions. Additionally, the namespace in
        the `run_namespace` argument (i.e. the namespace provided to `Network.run`)
        or, if this argument is unspecified, the implicit namespace of
        surrounding variables in the stack frame where the original call was made
        is used (to determine this stack frame, the `level` argument has to be set
        correctly).

        Parameters
        ----------
        identifier : str
            The name to resolve.
        group : `Group`
            The group that potentially defines an explicit namespace for looking up
            external names.
        run_namespace : dict, optional
            A namespace (mapping from strings to objects), as provided as an
            argument to the `Network.run` function.
        level : int, optional
            How far to go up in the stack to find the calling frame.
        do_warn : int, optional
            Whether to display a warning if an identifier resolves to different
            objects in different namespaces. Defaults to ``True``.
        '''
        # We save tuples of (namespace description, referred object) to
        # give meaningful warnings in case of duplicate definitions
        matches = []

        namespaces = OrderedDict()
        # Default namespaces (units and functions)
        namespaces['constants'] = DEFAULT_CONSTANTS
        namespaces['units'] = DEFAULT_UNITS
        namespaces['functions'] = DEFAULT_FUNCTIONS
        if getattr(self, 'namespace', None) is not None:
            namespaces['group-specific'] = self.namespace

        # explicit or implicit run namespace
        if run_namespace is not None:
            namespaces['run'] = run_namespace
        else:
            namespaces['implicit'] = get_local_namespace(level + 1)

        for description, namespace in namespaces.iteritems():
            if identifier in namespace:
                matches.append((description, namespace[identifier]))

        if len(matches) == 0:
            # No match at all
            raise KeyError(
                ('The identifier "%s" could not be resolved.') % (identifier))
        elif len(matches) > 1:
            # Possibly, all matches refer to the same object
            first_obj = matches[0][1]
            found_mismatch = False
            for m in matches:
                if _same_value(m[1], first_obj):
                    continue
                if _same_function(m[1], first_obj):
                    continue
                try:
                    proxy = weakref.proxy(first_obj)
                    if m[1] is proxy:
                        continue
                except TypeError:
                    pass

                # Found a mismatch
                found_mismatch = True
                break

            if found_mismatch and do_warn:
                _conflict_warning(
                    ('The name "%s" refers to different objects '
                     'in different namespaces used for resolving '
                     'names in the context of group "%s". '
                     'Will use the object from the %s namespace '
                     'with the value %r') %
                    (identifier, getattr(
                        self, 'name', '<unknown>'), matches[0][0], first_obj),
                    matches[1:])

        # use the first match (according to resolution order)
        resolved = matches[0][1]

        # Replace pure Python functions by a Functions object
        if callable(resolved) and not isinstance(resolved, Function):
            resolved = Function(resolved)

        if not isinstance(resolved, (Function, Variable)):
            # Wrap the value in a Constant object
            unit = get_unit(resolved)
            value = np.asarray(resolved)
            if value.shape != ():
                raise KeyError('Variable %s was found in the namespace, but is'
                               ' not a scalar value' % identifier)
            resolved = Constant(identifier, unit=unit, value=value)

        return resolved