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)
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)
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)
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'
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'
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'
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)'
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
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)) }
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)
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)
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)
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
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'
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'}
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
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'])
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)
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)
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'])
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'}
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'
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)
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)
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
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))
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
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]
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))
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.")