Пример #1
0
    def __init__(self, synapses):
        Variable.__init__(self, Unit(1), value=self, constant=True)
        self.source = synapses.source
        self.target = synapses.target
        source_len = len(synapses.source)
        target_len = len(synapses.target)
        self.synapses = weakref.proxy(synapses)
        dtype = smallest_inttype(MAX_SYNAPSES)
        self.synaptic_pre = DynamicArray1D(0, dtype=dtype)
        self.synaptic_post = DynamicArray1D(0, dtype=dtype)
        self.pre_synaptic = [
            DynamicArray1D(0, dtype=dtype) for _ in xrange(source_len)
        ]
        self.post_synaptic = [
            DynamicArray1D(0, dtype=dtype) for _ in xrange(target_len)
        ]

        self._registered_variables = []

        self.variables = {
            'i': DynamicArrayVariable('i', Unit(1), self.synaptic_pre),
            'j': DynamicArrayVariable('j', Unit(1), self.synaptic_post)
        }
        self.i = IndexView(self.synaptic_pre, self)
        self.j = IndexView(self.synaptic_post, self)
        self.k = SynapseIndexView(self)
Пример #2
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)
Пример #3
0
    def __init__(self, synapses):
        Variable.__init__(self, Unit(1), value=self, constant=True)
        self.source = synapses.source
        self.target = synapses.target
        source_len = len(synapses.source)
        target_len = len(synapses.target)
        self.synapses = weakref.proxy(synapses)
        dtype = smallest_inttype(MAX_SYNAPSES)
        self.synaptic_pre = DynamicArray1D(0, dtype=dtype)
        self.synaptic_post = DynamicArray1D(0, dtype=dtype)
        self.pre_synaptic = [DynamicArray1D(0, dtype=dtype)
                             for _ in xrange(source_len)]
        self.post_synaptic = [DynamicArray1D(0, dtype=dtype)
                              for _ in xrange(target_len)]

        self._registered_variables = []

        self.variables = {'i': DynamicArrayVariable('i',
                                                     Unit(1),
                                                     self.synaptic_pre),
                          'j': DynamicArrayVariable('j',
                                                    Unit(1),
                                                    self.synaptic_post)}
        self.i = IndexView(self.synaptic_pre, self)
        self.j = IndexView(self.synaptic_post, self)
        self.k = SynapseIndexView(self)
Пример #4
0
def test_nested_subexpressions():
    '''
    This test checks that code translation works with nested subexpressions.
    '''
    code = '''
    x = a + b + c
    c = 1
    x = a + b + c
    d = 1
    x = a + b + c
    '''
    variables = {
        'a': Subexpression(name='a', dtype=np.float32, owner=FakeGroup(variables={}), device=None,
                           expr='b*b+d'),
        'b': Subexpression(name='b', dtype=np.float32, owner=FakeGroup(variables={}), device=None,
                           expr='c*c*c'),
        'c': Variable(name='c'),
        'd': Variable(name='d'),
        }
    scalar_stmts, vector_stmts = make_statements(code, variables, np.float32)
    assert len(scalar_stmts) == 0
    evalorder = ''.join(stmt.var for stmt in vector_stmts)
    # This is the order that variables ought to be evaluated in (note that
    # previously this test did not expect the last "b" evaluation, because its
    # value did not change (c was not changed). We have since removed this
    # subexpression caching, because it did not seem to apply in practical
    # use cases)
    assert evalorder == 'baxcbaxdbax'
Пример #5
0
def test_nested_subexpressions():
    '''
    This test checks that code translation works with nested subexpressions.
    '''
    code = '''
    x = a + b + c
    c = 1
    x = a + b + c
    d = 1
    x = a + b + c
    '''
    variables = {
        'a':
        Subexpression(name='a',
                      unit=Unit(1),
                      dtype=np.float32,
                      owner=FakeGroup(variables={}),
                      device=None,
                      expr='b*b+d'),
        'b':
        Subexpression(name='b',
                      unit=Unit(1),
                      dtype=np.float32,
                      owner=FakeGroup(variables={}),
                      device=None,
                      expr='c*c*c'),
        'c':
        Variable(unit=None, name='c'),
        'd':
        Variable(unit=None, name='d'),
    }
    stmts = make_statements(code, variables, np.float32)
    evalorder = ''.join(stmt.var for stmt in stmts)
    # This is the order that variables ought to be evaluated in
    assert evalorder == 'baxcbaxdax'
Пример #6
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'
Пример #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)'
Пример #8
0
    def __init__(self, source, variables, record=None, when=None,
                 name='statemonitor*', codeobj_class=None):
        self.source = weakref.proxy(source)
        self.codeobj_class = codeobj_class

        # run by default on source clock at the end
        scheduler = Scheduler(when)
        if not scheduler.defined_clock:
            scheduler.clock = source.clock
        if not scheduler.defined_when:
            scheduler.when = 'end'

        BrianObject.__init__(self, when=scheduler, name=name)

        # variables should always be a list of strings
        if variables is True:
            variables = source.equations.names
        elif isinstance(variables, str):
            variables = [variables]
        #: The variables to record
        self.record_variables = variables

        # record should always be an array of ints
        self.record_all = False
        if record is True:
            self.record_all = True
            record = source.item_mapping[:]
        elif record is None or record is False:
            record = np.array([], dtype=np.int32)
        elif isinstance(record, int):
            record = np.array([source.item_mapping[record]], dtype=np.int32)
        else:
            record = np.array(source.item_mapping[record], dtype=np.int32)
            
        #: The array of recorded indices
        self.indices = record
        # create data structures
        self.reinit()
        
        # Setup variables
        self.variables = {}
        for varname in variables:
            var = source.variables[varname]
            if not (np.issubdtype(var.dtype, np.float64) and
                        np.issubdtype(np.float64, var.dtype)):
                raise NotImplementedError(('Cannot record %s with data type '
                                           '%s, currently only values stored as '
                                           'doubles can be recorded.') %
                                          (varname, var.dtype))
            self.variables[varname] = var
            self.variables['_recorded_'+varname] = Variable(Unit(1),
                                                            self._values[varname])

        self.variables['_t'] = Variable(Unit(1), self._t)
        self.variables['_clock_t'] = AttributeVariable(second, self.clock, 't_')
        self.variables['_indices'] = ArrayVariable('_indices', Unit(1),
                                                   self.indices,
                                                   constant=True)

        self._group_attribute_access_active = True
Пример #9
0
    def __init__(self,
                 source,
                 when=None,
                 name='ratemonitor*',
                 codeobj_class=None):
        self.source = weakref.proxy(source)

        # run by default on source clock at the end
        scheduler = Scheduler(when)
        if not scheduler.defined_clock:
            scheduler.clock = source.clock
        if not scheduler.defined_when:
            scheduler.when = 'end'

        self.codeobj_class = codeobj_class
        BrianObject.__init__(self, when=scheduler, name=name)

        # create data structures
        self.reinit()

        self.variables = {
            't': AttributeVariable(second, self.clock, 't'),
            'dt': AttributeVariable(second, self.clock, 'dt', constant=True),
            '_spikes': AttributeVariable(Unit(1), self.source, 'spikes'),
            # The template needs to have access to the
            # DynamicArray here, having access to the underlying
            # array is not enough since we want to do the resize
            # in the template
            '_rate': Variable(Unit(1), self._rate),
            '_t': Variable(Unit(1), self._t),
            '_num_source_neurons': Variable(Unit(1), len(self.source))
        }
Пример #10
0
 def __init__(self, N, offset, group):
     self.N = N
     self.offset = int(offset)
     self.group = weakref.proxy(group)
     self._indices = np.arange(self.N + self.offset)
     self.variables = {'i': ArrayVariable('i',
                                           Unit(1),
                                           self._indices - self.offset)}
     Variable.__init__(self, Unit(1), value=self, constant=True)
Пример #11
0
def test_repeated_subexpressions():
    variables = {
        'a':
        Subexpression(name='a',
                      dtype=np.float32,
                      owner=FakeGroup(variables={}),
                      device=None,
                      expr='2*z'),
        'x':
        Variable(name='x'),
        'y':
        Variable(name='y'),
        'z':
        Variable(name='z')
    }
    # subexpression a (referring to z) is used twice, but can be reused the
    # second time (no change to z)
    code = '''
    x = a
    y = a
    '''
    scalar_stmts, vector_stmts = make_statements(code, variables, np.float32)
    assert len(scalar_stmts) == 0
    assert [stmt.var for stmt in vector_stmts] == ['a', 'x', 'y']
    assert vector_stmts[0].constant

    code = '''
    x = a
    z *= 2
    '''
    scalar_stmts, vector_stmts = make_statements(code, variables, np.float32)
    assert len(scalar_stmts) == 0
    assert [stmt.var for stmt in vector_stmts] == ['a', 'x', 'z']
    # Note that we currently do not mark the subexpression as constant in this
    # case, because its use after the "z *=2" line would actually redefine it.
    # Our algorithm is currently not smart enough to detect that it is actually
    # not used afterwards

    # a refers to z, therefore we have to redefine a after z changed, and a
    # cannot be constant
    code = '''
    x = a
    z *= 2
    y = a
    '''
    scalar_stmts, vector_stmts = make_statements(code, variables, np.float32)
    assert len(scalar_stmts) == 0
    assert [stmt.var for stmt in vector_stmts] == ['a', 'x', 'z', 'a', 'y']
    assert not any(stmt.constant for stmt in vector_stmts)
Пример #12
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)
Пример #13
0
def analyse_identifiers(code, variables, recursive=False):
    '''
    Analyses a code string (sequence of statements) to find all identifiers by type.
    
    In a given code block, some variable names (identifiers) must be given as inputs to the code
    block, and some are created by the code block. For example, the line::
    
        a = b+c
        
    This could mean to create a new variable a from b and c, or it could mean modify the existing
    value of a from b or c, depending on whether a was previously known.
    
    Parameters
    ----------
    code : str
        The code string, a sequence of statements one per line.
    variables : dict of `Variable`, set of names
        Specifiers for the model variables or a set of known names
    recursive : bool, optional
        Whether to recurse down into subexpressions (defaults to ``False``).
    
    Returns
    -------
    newly_defined : set
        A set of variables that are created by the code block.
    used_known : set
        A set of variables that are used and already known, a subset of the
        ``known`` parameter.
    unknown : set
        A set of variables which are used by the code block but not defined by
        it and not previously known. Should correspond to variables in the
        external namespace.
    '''
    if isinstance(variables, collections.Mapping):
        known = set(k for k, v in variables.iteritems()
                    if not isinstance(k, AuxiliaryVariable))
    else:
        known = set(variables)
        variables = dict(
            (k, Variable(unit=None, name=k, dtype=np.float64)) for k in known)

    known |= STANDARD_IDENTIFIERS
    scalar_stmts, vector_stmts = make_statements(code, variables, np.float64)
    stmts = scalar_stmts + vector_stmts
    defined = set(stmt.var for stmt in stmts if stmt.op == ':=')
    if len(stmts) == 0:
        allids = set()
    elif recursive:
        if not isinstance(variables, collections.Mapping):
            raise TypeError('Have to specify a variables dictionary.')
        allids = get_identifiers_recursively(
            [stmt.expr
             for stmt in stmts], variables) | set([stmt.var for stmt in stmts])
    else:
        allids = set.union(*[get_identifiers(stmt.expr)
                             for stmt in stmts]) | set(
                                 [stmt.var for stmt in stmts])
    dependent = allids.difference(defined, known)
    used_known = allids.intersection(known) - STANDARD_IDENTIFIERS
    return defined, used_known, dependent
Пример #14
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'
Пример #15
0
def test_analyse_identifiers():
    '''
    Test that the analyse_identifiers function works on a simple clear example.
    '''
    code = '''
    a = b+c
    d = e+f
    '''
    known = {'b': Variable(name='b'),
             'c': Variable(name='c'),
             'd': Variable(name='d'),
             'g': Variable(name='g')}
    
    defined, used_known, dependent = analyse_identifiers(code, known)
    assert 'a' in defined  # There might be an additional constant added by the
                           # loop-invariant optimisation
    assert used_known == {'b', 'c', 'd'}
    assert dependent == {'e', 'f'}
Пример #16
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
Пример #17
0
def test_analyse_identifiers():
    '''
    Test that the analyse_identifiers function works on a simple clear example.
    '''
    code = '''
    a = b+c
    d = e+f
    '''
    known = {
        'b': Variable(unit=None, name='b'),
        'c': Variable(unit=None, name='c'),
        'd': Variable(unit=None, name='d'),
        'g': Variable(unit=None, name='g')
    }

    defined, used_known, dependent = analyse_identifiers(code, known)

    assert defined == set(['a'])
    assert used_known == set(['b', 'c', 'd'])
    assert dependent == set(['e', 'f'])
Пример #18
0
def test_write_to_subexpression():
    variables = {
        'a': Subexpression(name='a', dtype=np.float32,
                           owner=FakeGroup(variables={}), device=None,
                           expr='2*z'),
        'z': Variable(name='z')
    }

    # Writing to a subexpression is not allowed
    code = 'a = z'
    assert_raises(SyntaxError, make_statements, code, variables, np.float32)
Пример #19
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)
Пример #20
0
def test_get_identifiers_recursively():
    '''
    Test finding identifiers including subexpressions.
    '''
    variables = {}
    variables['sub1'] = Subexpression(Unit(1), np.float32, 'sub2 * z',
                                      variables, {})
    variables['sub2'] = Subexpression(Unit(1), np.float32, '5 + y', variables,
                                      {})
    variables['x'] = Variable(unit=None)
    identifiers = get_identifiers_recursively('_x = sub1 + x', variables)
    assert identifiers == set(['x', '_x', 'y', 'z', 'sub1', 'sub2'])
Пример #21
0
def test_get_identifiers_recursively():
    '''
    Test finding identifiers including subexpressions.
    '''
    variables = {'sub1': Subexpression(name='sub1',
                                       dtype=np.float32, expr='sub2 * z',
                                       owner=FakeGroup(variables={}),
                                       device=None),
                 'sub2': Subexpression(name='sub2',
                                       dtype=np.float32, expr='5 + y',
                                       owner=FakeGroup(variables={}),
                                       device=None),
                 'x': Variable(name='x')}
    identifiers = get_identifiers_recursively(['_x = sub1 + x'],
                                              variables)
    assert identifiers == {'x', '_x', 'y', 'z', 'sub1', 'sub2'}
Пример #22
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'
Пример #23
0
    def __init__(self, synapses, code, prepost, objname=None):
        self.code = code
        if prepost == 'pre':
            self.source = synapses.source
            self.target = synapses.target
            self.synapse_indices = synapses.item_mapping.pre_synaptic
        elif prepost == 'post':
            self.source = synapses.target
            self.target = synapses.source
            self.synapse_indices = synapses.item_mapping.post_synaptic
        else:
            raise ValueError('prepost argument has to be either "pre" or '
                             '"post"')
        self.synapses = synapses

        if objname is None:
            objname = prepost + '*'

        GroupCodeRunner.__init__(self,
                                 synapses,
                                 'synapses',
                                 code=code,
                                 when=(synapses.clock, 'synapses'),
                                 name=synapses.name + '_' + objname)

        self._delays = DynamicArray1D(synapses.N, dtype=np.float64)
        # Register the object with the `SynapticIndex` object so it gets
        # automatically resized
        synapses.item_mapping.register_variable(self._delays)
        self.queue = SpikeQueue()
        self.spiking_synapses = []
        self.variables = {
            '_spiking_synapses':
            AttributeVariable(Unit(1),
                              self,
                              'spiking_synapses',
                              constant=False),
            '_source_offset':
            Variable(Unit(1), self.source.offset, constant=True),
            '_target_offset':
            Variable(Unit(1), self.target.offset, constant=True),
            'delay':
            DynamicArrayVariable('delay',
                                 second,
                                 self._delays,
                                 group_name=self.name,
                                 constant=True)
        }

        # Re-extract the last part of the name from the full name
        self.objname = self.name[len(synapses.name) + 1:]

        #: The simulation dt (necessary for the delays)
        self.dt = self.synapses.clock.dt_

        self.item_mapping = synapses.item_mapping

        self.indices = self.synapses.indices

        # Enable access to the delay attribute via the specifier
        Group.__init__(self)
Пример #24
0
    def _add_synapses(self, sources, targets, n, p, condition=None, level=0):

        if condition is None:
            sources = np.atleast_1d(sources)
            targets = np.atleast_1d(targets)
            n = np.atleast_1d(n)
            p = np.atleast_1d(p)
            if not len(p) == 1 or p != 1:
                use_connections = np.random.rand(len(sources)) < p
                sources = sources[use_connections]
                targets = targets[use_connections]
                n = n[use_connections]
            sources = sources.repeat(n)
            targets = targets.repeat(n)
            new_synapses = len(sources)

            old_N = self.N
            new_N = old_N + new_synapses
            self._resize(new_N)

            self.synaptic_pre[old_N:new_N] = sources
            self.synaptic_post[old_N:new_N] = targets
            synapse_idx = old_N
            for source, target in zip(sources, targets):
                synapses = self.pre_synaptic[source]
                synapses.resize(len(synapses) + 1)
                synapses[-1] = synapse_idx
                synapses = self.post_synaptic[target]
                synapses.resize(len(synapses) + 1)
                synapses[-1] = synapse_idx
                synapse_idx += 1
        else:
            abstract_code = '_cond = ' + condition + '\n'
            abstract_code += '_n = ' + str(n) + '\n'
            abstract_code += '_p = ' + str(p)
            namespace = get_local_namespace(level + 1)
            additional_namespace = ('implicit-namespace', namespace)
            variables = {
                '_source_neurons':
                ArrayVariable('_source_neurons',
                              Unit(1),
                              self.source.item_mapping[:] - self.source.offset,
                              constant=True),
                '_target_neurons':
                ArrayVariable('_target_neurons',
                              Unit(1),
                              self.target.item_mapping[:] - self.target.offset,
                              constant=True),
                # The template needs to have access to the DynamicArray here,
                # having access to the underlying array (which would be much
                # faster), is not enough
                '_synaptic_pre':
                Variable(Unit(1), self.synaptic_pre, constant=True),
                '_synaptic_post':
                Variable(Unit(1), self.synaptic_post, constant=True),
                '_pre_synaptic':
                Variable(Unit(1), self.pre_synaptic, constant=True),
                '_post_synaptic':
                Variable(Unit(1), self.post_synaptic, constant=True),
                # Will be set in the template
                'i':
                Variable(unit=Unit(1), constant=True),
                'j':
                Variable(unit=Unit(1), constant=True)
            }
            codeobj = create_runner_codeobj(
                self.synapses,
                abstract_code,
                'synapses_create',
                additional_variables=variables,
                additional_namespace=additional_namespace,
                check_units=False)
            codeobj()
            number = len(self.synaptic_pre)
            for variable in self._registered_variables:
                variable.resize(number)
Пример #25
0
    def _create_variables(self):
        '''
        Create the variables dictionary for this `Synapses`, containing
        entries for the equation variables and some standard entries.
        '''
        # Add all the pre and post variables with _pre and _post suffixes
        v = {}
        self.variable_indices = defaultdict(lambda: '_idx')
        for name, var in getattr(self.source, 'variables', {}).iteritems():
            if isinstance(var, (ArrayVariable, Subexpression)):
                v[name + '_pre'] = var
                self.variable_indices[name + '_pre'] = '_presynaptic_idx'
        for name, var in getattr(self.target, 'variables', {}).iteritems():
            if isinstance(var, (ArrayVariable, Subexpression)):
                v[name + '_post'] = var
                self.variable_indices[name + '_post'] = '_postsynaptic_idx'
                # Also add all the post variables without a suffix -- if this
                # clashes with the name of a state variable defined in this
                # Synapses group, the latter will overwrite the entry later and
                # take precedence
                v[name] = var
                self.variable_indices[name] = '_postsynaptic_idx'

        # Standard variables always present
        v.update({
            't':
            AttributeVariable(second, self.clock, 't_', constant=False),
            'dt':
            AttributeVariable(second, self.clock, 'dt_', constant=True),
            '_num_source_neurons':
            Variable(Unit(1), len(self.source), constant=True),
            '_num_target_neurons':
            Variable(Unit(1), len(self.target), constant=True),
            '_synaptic_pre':
            DynamicArrayVariable('_synaptic_pre', Unit(1),
                                 self.item_mapping.synaptic_pre),
            '_synaptic_post':
            DynamicArrayVariable('_synaptic_pre', Unit(1),
                                 self.item_mapping.synaptic_post),
            # We don't need "proper" specifier for these -- they go
            # back to Python code currently
            '_pre_synaptic':
            Variable(Unit(1), self.item_mapping.pre_synaptic),
            '_post_synaptic':
            Variable(Unit(1), self.item_mapping.post_synaptic)
        })

        for eq in itertools.chain(
                self.equations.itervalues(),
                self.event_driven.itervalues()
                if self.event_driven is not None else []):
            if eq.type in (DIFFERENTIAL_EQUATION, PARAMETER):
                array = self.arrays[eq.varname]
                constant = ('constant' in eq.flags)
                # We are dealing with dynamic arrays here, code generation
                # shouldn't directly access the specifier.array attribute but
                # use specifier.get_value() to get a reference to the underlying
                # array
                v[eq.varname] = DynamicArrayVariable(eq.varname,
                                                     eq.unit,
                                                     array,
                                                     group_name=self.name,
                                                     constant=constant,
                                                     is_bool=eq.is_bool)
                if eq.varname in self.variable_indices:
                    # we are overwriting a postsynaptic variable of the same
                    # name, delete the reference to the postsynaptic index
                    del self.variable_indices[eq.varname]
                # Register the array with the `SynapticItemMapping` object so
                # it gets automatically resized
                self.item_mapping.register_variable(array)
            elif eq.type == STATIC_EQUATION:
                v.update({
                    eq.varname:
                    Subexpression(eq.unit,
                                  brian_prefs['core.default_scalar_dtype'],
                                  str(eq.expr),
                                  variables=v,
                                  namespace=self.namespace,
                                  is_bool=eq.is_bool)
                })
            else:
                raise AssertionError('Unknown type of equation: ' + eq.eq_type)

        # Stochastic variables
        for xi in self.equations.stochastic_variables:
            v.update({xi: StochasticVariable()})

        return v
Пример #26
0
def check_units_statements(code, variables):
    '''
    Check the units for a series of statements. Setting a model variable has to
    use the correct unit. For newly introduced temporary variables, the unit
    is determined and used to check the following statements to ensure
    consistency.
    
    Parameters
    ----------
    code : str
        The statements as a (multi-line) string
    variables : dict of `Variable` objects
        The information about all variables used in `code` (including
        `Constant` objects for external variables)
    
    Raises
    ------
    KeyError
        In case on of the identifiers cannot be resolved.
    DimensionMismatchError
        If an unit mismatch occurs during the evaluation.
    '''
    # Avoid a circular import
    from brian2.codegen.translation import analyse_identifiers
    known = set(variables.keys())
    newly_defined, _, unknown = analyse_identifiers(code, known)
    
    if len(unknown):
        raise AssertionError(('Encountered unknown identifiers, this should '
                             'not happen at this stage. Unkown identifiers: %s'
                             % unknown))

    
    code = re.split(r'[;\n]', code)
    for line in code:
        line = line.strip()
        if not len(line):
            continue  # skip empty lines
        
        varname, op, expr, comment = parse_statement(line)
        if op in ('+=', '-=', '*=', '/=', '%='):
            # Replace statements such as "w *=2" by "w = w * 2"
            expr = '{var} {op_first} {expr}'.format(var=varname,
                                                    op_first=op[0],
                                                    expr=expr)
            op = '='
        elif op == '=':
            pass
        else:
            raise AssertionError('Unknown operator "%s"' % op) 

        expr_unit = parse_expression_unit(expr, variables)

        if varname in variables:
            expected_unit = variables[varname].unit
            fail_for_dimension_mismatch(expr_unit, expected_unit,
                                        ('The right-hand-side of code '
                                         'statement ""%s" does not have the '
                                         'expected unit %r') % (line,
                                                               expected_unit))
        elif varname in newly_defined:
            # note the unit for later
            variables[varname] = Variable(name=varname, unit=expr_unit,
                                          scalar=False)
        else:
            raise AssertionError(('Variable "%s" is neither in the variables '
                                  'dictionary nor in the list of undefined '
                                  'variables.' % varname))
Пример #27
0
def test_determination():
    '''
    Test the determination of suitable state updaters.
    '''
    
    # To save some typing
    determine_stateupdater = StateUpdateMethod.determine_stateupdater
    
    # Save state before tests
    before = list(StateUpdateMethod.stateupdaters)
    
    eqs = Equations('dv/dt = -v / (10*ms) : 1')
    # Just make sure that state updaters know about the two state variables
    variables = {'v': Variable(name='v', unit=None),
                 'w': Variable(name='w', unit=None)}
    
    # all methods should work for these equations.
    # First, specify them explicitly (using the object)
    for integrator in (linear, euler, exponential_euler, #TODO: Removed "independent" here due to the issue in sympy 0.7.4
                       rk2, rk4, milstein):
        with catch_logs() as logs:
            returned = determine_stateupdater(eqs, variables,
                                              method=integrator)
            assert returned is integrator, 'Expected state updater %s, got %s' % (integrator, returned)
            assert len(logs) == 0, 'Got %d unexpected warnings: %s' % (len(logs), str([l[2] for l in logs]))
    
    # Equation with multiplicative noise, only milstein should work without
    # a warning
    eqs = Equations('dv/dt = -v / (10*ms) + v*xi*second**-.5: 1')
    for integrator in (linear, independent, euler, exponential_euler, rk2, rk4):
        with catch_logs() as logs:
            returned = determine_stateupdater(eqs, variables,
                                              method=integrator)
            assert returned is integrator, 'Expected state updater %s, got %s' % (integrator, returned)
            # We should get a warning here
            assert len(logs) == 1, 'Got %d warnings but expected 1: %s' % (len(logs), str([l[2] for l in logs]))
            
    with catch_logs() as logs:
        returned = determine_stateupdater(eqs, variables,
                                          method=milstein)
        assert returned is milstein, 'Expected state updater milstein, got %s' % (integrator, returned)
        # No warning here
        assert len(logs) == 0, 'Got %d unexpected warnings: %s' % (len(logs), str([l[2] for l in logs]))
    
    
    # Arbitrary functions (converting equations into abstract code) should
    # always work
    my_stateupdater = lambda eqs: 'x_new = x'
    with catch_logs() as logs:
        returned = determine_stateupdater(eqs, variables,
                                          method=my_stateupdater)
        assert returned is my_stateupdater
        # No warning here
        assert len(logs) == 0
    
    
    # Specification with names
    eqs = Equations('dv/dt = -v / (10*ms) : 1')
    for name, integrator in [('linear', linear), ('euler', euler),
                             #('independent', independent), #TODO: Removed "independent" here due to the issue in sympy 0.7.4
                             ('exponential_euler', exponential_euler),
                             ('rk2', rk2), ('rk4', rk4),
                             ('milstein', milstein)]:
        with catch_logs() as logs:
            returned = determine_stateupdater(eqs, variables,
                                              method=name)
        assert returned is integrator
        # No warning here
        assert len(logs) == 0    

    # Now all except milstein should refuse to work
    eqs = Equations('dv/dt = -v / (10*ms) + v*xi*second**-.5: 1')
    for name in ['linear', 'independent', 'euler', 'exponential_euler',
                 'rk2', 'rk4']:
        assert_raises(ValueError, lambda: determine_stateupdater(eqs,
                                                                 variables,
                                                                 method=name))
    # milstein should work
    with catch_logs() as logs:
        determine_stateupdater(eqs, variables, method='milstein')
        assert len(logs) == 0
    
    # non-existing name
    assert_raises(ValueError, lambda: determine_stateupdater(eqs,
                                                             variables,
                                                             method='does_not_exist'))
    
    # Automatic state updater choice should return linear for linear equations,
    # euler for non-linear, non-stochastic equations and equations with
    # additive noise, milstein for equations with multiplicative noise
    eqs = Equations('dv/dt = -v / (10*ms) : 1')
    assert determine_stateupdater(eqs, variables) is linear
    
    # This is conditionally linear
    eqs = Equations('''dv/dt = -(v + w**2)/ (10*ms) : 1
                       dw/dt = -w/ (10*ms) : 1''')
    assert determine_stateupdater(eqs, variables) is exponential_euler

    eqs = Equations('dv/dt = sin(t) / (10*ms) : 1')
    assert determine_stateupdater(eqs, variables) is independent

    eqs = Equations('dv/dt = -sqrt(v) / (10*ms) : 1')
    assert determine_stateupdater(eqs, variables) is euler

    eqs = Equations('dv/dt = -v / (10*ms) + 0.1*second**-.5*xi: 1')
    assert determine_stateupdater(eqs, variables) is euler

    eqs = Equations('dv/dt = -v / (10*ms) + v*0.1*second**-.5*xi: 1')
    assert determine_stateupdater(eqs, variables) is milstein

    # remove all registered state updaters --> automatic choice no longer works
    StateUpdateMethod.stateupdaters = {}
    assert_raises(ValueError, lambda: determine_stateupdater(eqs, variables))

    # reset to state before the test
    StateUpdateMethod.stateupdaters = before
Пример #28
0
def test_determination():
    '''
    Test the determination of suitable state updaters.
    '''
    # To save some typing
    apply_stateupdater = StateUpdateMethod.apply_stateupdater

    eqs = Equations('dv/dt = -v / (10*ms) : 1')
    # Just make sure that state updaters know about the two state variables
    variables = {'v': Variable(name='v'), 'w': Variable(name='w')}

    # all methods should work for these equations.
    # First, specify them explicitly (using the object)
    for integrator in (
            linear,
            euler,
            exponential_euler,  #TODO: Removed "independent" here due to the issue in sympy 0.7.4
            rk2,
            rk4,
            heun,
            milstein):
        with catch_logs() as logs:
            returned = apply_stateupdater(eqs, variables, method=integrator)
            assert len(logs) == 0, 'Got %d unexpected warnings: %s' % (
                len(logs), str([l[2] for l in logs]))

    # Equation with multiplicative noise, only milstein and heun should work
    eqs = Equations('dv/dt = -v / (10*ms) + v*xi*second**-.5: 1')
    for integrator in (linear, independent, euler, exponential_euler, rk2,
                       rk4):
        assert_raises(UnsupportedEquationsException,
                      lambda: apply_stateupdater(eqs, variables, integrator))

    for integrator in (heun, milstein):
        with catch_logs() as logs:
            returned = apply_stateupdater(eqs, variables, method=integrator)
            assert len(logs) == 0, 'Got %d unexpected warnings: %s' % (
                len(logs), str([l[2] for l in logs]))

    # Arbitrary functions (converting equations into abstract code) should
    # always work
    my_stateupdater = lambda eqs, vars, options: 'x_new = x'
    with catch_logs() as logs:
        returned = apply_stateupdater(eqs, variables, method=my_stateupdater)
        # No warning here
        assert len(logs) == 0

    # Specification with names
    eqs = Equations('dv/dt = -v / (10*ms) : 1')
    for name, integrator in [
        ('exact', exact),
        ('linear', linear),
        ('euler', euler),
            #('independent', independent), #TODO: Removed "independent" here due to the issue in sympy 0.7.4
        ('exponential_euler', exponential_euler),
        ('rk2', rk2),
        ('rk4', rk4),
        ('heun', heun),
        ('milstein', milstein)
    ]:
        with catch_logs() as logs:
            returned = apply_stateupdater(eqs, variables, method=name)
            # No warning here
            assert len(logs) == 0

    # Now all except heun and milstein should refuse to work
    eqs = Equations('dv/dt = -v / (10*ms) + v*xi*second**-.5: 1')
    for name in [
            'linear', 'exact', 'independent', 'euler', 'exponential_euler',
            'rk2', 'rk4'
    ]:
        assert_raises(UnsupportedEquationsException,
                      lambda: apply_stateupdater(eqs, variables, method=name))

    # milstein should work
    with catch_logs() as logs:
        apply_stateupdater(eqs, variables, method='milstein')
        assert len(logs) == 0

    # heun should work
    with catch_logs() as logs:
        apply_stateupdater(eqs, variables, method='heun')
        assert len(logs) == 0

    # non-existing name
    assert_raises(
        ValueError,
        lambda: apply_stateupdater(eqs, variables, method='does_not_exist'))

    # Automatic state updater choice should return linear for linear equations,
    # euler for non-linear, non-stochastic equations and equations with
    # additive noise, heun for equations with multiplicative noise
    # Because it is somewhat fragile, the "independent" state updater is not
    # included in this list
    all_methods = [
        'linear', 'exact', 'exponential_euler', 'euler', 'heun', 'milstein'
    ]
    eqs = Equations('dv/dt = -v / (10*ms) : 1')
    with catch_logs(log_level=logging.INFO) as logs:
        apply_stateupdater(eqs, variables, all_methods)
        assert len(logs) == 1
        assert ('linear' in logs[0][2]) or ('exact' in logs[0][2])

    # This is conditionally linear
    eqs = Equations('''dv/dt = -(v + w**2)/ (10*ms) : 1
                       dw/dt = -w/ (10*ms) : 1''')
    with catch_logs(log_level=logging.INFO) as logs:
        apply_stateupdater(eqs, variables, all_methods)
        assert len(logs) == 1
        assert 'exponential_euler' in logs[0][2]

    # # Do not test for now
    # eqs = Equations('dv/dt = sin(t) / (10*ms) : 1')
    # assert apply_stateupdater(eqs, variables) is independent

    eqs = Equations('dv/dt = -sqrt(v) / (10*ms) : 1')
    with catch_logs(log_level=logging.INFO) as logs:
        apply_stateupdater(eqs, variables, all_methods)
        assert len(logs) == 1
        assert "'euler'" in logs[0][2]

    eqs = Equations('dv/dt = -v / (10*ms) + 0.1*second**-.5*xi: 1')
    with catch_logs(log_level=logging.INFO) as logs:
        apply_stateupdater(eqs, variables, all_methods)
        assert len(logs) == 1
        assert "'euler'" in logs[0][2]

    eqs = Equations('dv/dt = -v / (10*ms) + v*0.1*second**-.5*xi: 1')
    with catch_logs(log_level=logging.INFO) as logs:
        apply_stateupdater(eqs, variables, all_methods)
        assert len(logs) == 1
        assert "'heun'" in logs[0][2]
Пример #29
0
def check_units_statements(code, namespace, variables):
    '''
    Check the units for a series of statements. Setting a model variable has to
    use the correct unit. For newly introduced temporary variables, the unit
    is determined and used to check the following statements to ensure
    consistency.
    
    Parameters
    ----------
    expression : str
        The expression to evaluate.
    namespace : dict-like
        The namespace of external variables.
    variables : dict of `Variable` objects
        The information about the internal variables
    
    Raises
    ------
    KeyError
        In case on of the identifiers cannot be resolved.
    DimensionMismatchError
        If an unit mismatch occurs during the evaluation.
    '''
    known = set(variables.keys()) | set(namespace.keys())
    newly_defined, _, unknown = analyse_identifiers(code, known)

    if len(unknown):
        raise AssertionError(
            ('Encountered unknown identifiers, this should '
             'not happen at this stage. Unkown identifiers: %s' % unknown))

    # We want to add newly defined variables to the variables dictionary so we
    # make a copy now
    variables = dict(variables)

    code = re.split(r'[;\n]', code)
    for line in code:
        line = line.strip()
        if not len(line):
            continue  # skip empty lines

        varname, op, expr = parse_statement(line)
        if op in ('+=', '-=', '*=', '/=', '%='):
            # Replace statements such as "w *=2" by "w = w * 2"
            expr = '{var} {op_first} {expr}'.format(var=varname,
                                                    op_first=op[0],
                                                    expr=expr)
            op = '='
        elif op == '=':
            pass
        else:
            raise AssertionError('Unknown operator "%s"' % op)

        expr_unit = parse_expression_unit(expr, namespace, variables)

        if varname in variables:
            fail_for_dimension_mismatch(variables[varname].unit, expr_unit,
                                        ('Code statement "%s" does not use '
                                         'correct units' % line))
        elif varname in newly_defined:
            # note the unit for later
            variables[varname] = Variable(expr_unit,
                                          is_bool=False,
                                          scalar=False)
        else:
            raise AssertionError(('Variable "%s" is neither in the variables '
                                  'dictionary nor in the list of undefined '
                                  'variables.' % varname))
Пример #30
0
def check_units_statements(code, variables):
    """
    Check the units for a series of statements. Setting a model variable has to
    use the correct unit. For newly introduced temporary variables, the unit
    is determined and used to check the following statements to ensure
    consistency.
    
    Parameters
    ----------
    code : str
        The statements as a (multi-line) string
    variables : dict of `Variable` objects
        The information about all variables used in `code` (including
        `Constant` objects for external variables)
    
    Raises
    ------
    KeyError
        In case on of the identifiers cannot be resolved.
    DimensionMismatchError
        If an unit mismatch occurs during the evaluation.
    """
    variables = dict(variables)
    # Avoid a circular import
    from brian2.codegen.translation import analyse_identifiers
    newly_defined, _, unknown = analyse_identifiers(code, variables)

    if len(unknown):
        raise AssertionError(
            f"Encountered unknown identifiers, this should not "
            f"happen at this stage. Unknown identifiers: {unknown}")

    code = re.split(r'[;\n]', code)
    for line in code:
        line = line.strip()
        if not len(line):
            continue  # skip empty lines

        varname, op, expr, comment = parse_statement(line)
        if op in ('+=', '-=', '*=', '/=', '%='):
            # Replace statements such as "w *=2" by "w = w * 2"
            expr = f'{varname} {op[0]} {expr}'
        elif op == '=':
            pass
        else:
            raise AssertionError(f'Unknown operator "{op}"')

        expr_unit = parse_expression_dimensions(expr, variables)

        if varname in variables:
            expected_unit = variables[varname].dim
            fail_for_dimension_mismatch(expr_unit,
                                        expected_unit,
                                        ('The right-hand-side of code '
                                         'statement "%s" does not have the '
                                         'expected unit {expected}') % line,
                                        expected=expected_unit)
        elif varname in newly_defined:
            # note the unit for later
            variables[varname] = Variable(name=varname,
                                          dimensions=get_dimensions(expr_unit),
                                          scalar=False)
        else:
            raise AssertionError(
                f"Variable '{varname}' is neither in the variables "
                f"dictionary nor in the list of undefined "
                f"variables.")