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 __init__(self, dt, name='clock*'): # We need a name right away because some devices (e.g. cpp_standalone) # need a name for the object when creating the variables Nameable.__init__(self, name=name) #: Note that right after a change of dt, this #: will not equal the new dt (which is stored in `Clock._new_dt`). Call #: `Clock._set_t_update_t` to update the internal clock representation. self._new_dt = None self.variables = Variables(self) self.variables.add_array('timestep', unit=Unit(1), size=1, dtype=np.uint64, read_only=True, scalar=True) self.variables.add_array('t', unit=second, size=1, dtype=np.double, read_only=True, scalar=True) self.variables.add_array('dt', unit=second, size=1, values=float(dt), dtype=np.float, read_only=True, constant=True, scalar=True) self.variables.add_constant('N', unit=Unit(1), value=1) self._enable_group_attributes() self.dt = dt logger.diagnostic("Created clock {name} with dt={dt}".format( name=self.name, dt=self.dt))
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, 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, 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 __init__(self, dt, name='clock*'): # We need a name right away because some devices (e.g. cpp_standalone) # need a name for the object when creating the variables Nameable.__init__(self, name=name) self._old_dt = None self.variables = Variables(self) self.variables.add_array('timestep', unit=Unit(1), size=1, dtype=np.uint64, read_only=True, scalar=True) self.variables.add_array('t', unit=second, size=1, dtype=np.double, read_only=True, scalar=True) self.variables.add_array('dt', unit=second, size=1, values=float(dt), dtype=np.float, read_only=True, constant=True, scalar=True) self.variables.add_constant('N', unit=Unit(1), value=1) self._enable_group_attributes() self.dt = dt logger.diagnostic("Created clock {name} with dt={dt}".format( name=self.name, dt=self.dt))
def __getitem__(self, item): if isinstance(item, basestring): variables = Variables(None) variables.add_auxiliary_variable('_indices', unit=Unit(1), dtype=np.int32) variables.add_auxiliary_variable('_cond', unit=Unit(1), dtype=np.bool) abstract_code = '_cond = ' + item check_code_units(abstract_code, self.group, additional_variables=variables, level=1) from brian2.devices.device import get_default_codeobject_class codeobj = create_runner_codeobj( self.group, abstract_code, 'group_get_indices', additional_variables=variables, level=1, codeobj_class=get_default_codeobject_class( 'codegen.string_expression_target')) return codeobj() else: return self.indices(item)
def test_apply_loop_invariant_optimisation_no_optimisation(): variables = { 'v1': Variable('v1', Unit(1), scalar=False), 'v2': Variable('v2', Unit(1), scalar=False), 'N': Constant('N', Unit(1), 10), 's1': Variable('s1', Unit(1), scalar=True, dtype=float), 's2': Variable('s2', Unit(1), 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, 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, name='ratemonitor*', codeobj_class=None): #: The group we are recording from self.source = source self.codeobj_class = codeobj_class CodeRunner.__init__(self, group=self, code='', template='ratemonitor', clock=source.clock, when='end', order=0, name=name) self.add_dependency(source) self.variables = Variables(self) # Handle subgroups correctly start = getattr(source, 'start', 0) stop = getattr(source, 'stop', len(source)) self.variables.add_constant('_source_start', Unit(1), start) self.variables.add_constant('_source_stop', Unit(1), stop) self.variables.add_reference('_spikespace', source) self.variables.add_dynamic_array('rate', size=0, unit=hertz) self.variables.add_dynamic_array('t', size=0, unit=second) self.variables.add_reference('_num_source_neurons', source, 'N') self.variables.add_array('N', unit=Unit(1), dtype=np.int32, size=1, scalar=True, read_only=True) self.variables.create_clock_variables(self._clock, prefix='_clock_') self._enable_group_attributes()
def __init__(self, N, rates, dt=None, clock=None, when='thresholds', order=0, name='poissongroup*', codeobj_class=None): Group.__init__(self, dt=dt, clock=clock, when=when, order=order, name=name) self.codeobj_class = codeobj_class self._N = N = int(N) # TODO: In principle, it would be nice to support Poisson groups with # refactoriness, but we can't currently, since the refractoriness # information is reset in the state updater which we are not using # We could either use a specific template or simply not bother and make # users write their own NeuronGroup (with threshold rand() < rates*dt) # for more complex use cases. self.variables = Variables(self) # standard variables self.variables.add_constant('N', unit=Unit(1), value=self._N) self.variables.add_arange('i', self._N, constant=True, read_only=True) self.variables.add_array('_spikespace', size=N + 1, unit=Unit(1), dtype=np.int32) self.variables.create_clock_variables(self._clock) # The firing rates self.variables.add_array('rates', size=N, unit=Hz) self.start = 0 self.stop = N self._refractory = False # To avoid a warning about the local variable rates, we set the real # threshold condition only after creating the object self.events = {'spike': 'False'} self.thresholder = {'spike': Thresholder(self)} self.events = {'spike': 'rand() < rates * dt'} self.contained_objects.append(self.thresholder['spike']) self._enable_group_attributes() # Here we want to use the local namespace, but at the level where the # constructor was called self.rates.set_item(slice(None), rates, level=2)
def __init__(self, N, indices, times, when=None, name='spikegeneratorgroup*', codeobj_class=None): if when is None: when = Scheduler(when='thresholds') Group.__init__(self, when=when, name=name) self.codeobj_class = codeobj_class if N < 1 or int(N) != N: raise ValueError('N has to be an integer >=1.') if len(indices) != len(times): raise ValueError( ('Length of the indices and times array must ' 'match, but %d != %d') % (len(indices), len(times))) self.start = 0 self.stop = N # sort times and indices first by time, then by indices rec = np.rec.fromarrays([times, indices], names=['t', 'i']) rec.sort() times = np.ascontiguousarray(rec.t) indices = np.ascontiguousarray(rec.i) self.variables = Variables(self) # standard variables self.variables.add_clock_variables(self.clock) self.variables.add_constant('N', unit=Unit(1), value=N) self.variables.add_arange('i', N) self.variables.add_arange('spike_number', len(indices)) self.variables.add_array('neuron_index', values=indices, size=len(indices), unit=Unit(1), dtype=np.int32, index='spike_number', read_only=True) self.variables.add_array('spike_time', values=times, size=len(times), unit=second, index='spike_number', read_only=True) self.variables.add_array('_spikespace', size=N + 1, unit=Unit(1), dtype=np.int32) # Activate name attribute access self._enable_group_attributes() CodeRunner.__init__(self, self, 'spikegenerator', when=when, name=None)
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 __init__(self, source, start, stop, name=None): # First check if the source is itself a Subgroup # If so, then make this a Subgroup of the original Group if isinstance(source, Subgroup): source = source.source start = start + source.start stop = stop + source.start self.source = source else: self.source = weakproxy_with_fallback(source) if name is None: name = source.name + '_subgroup*' # We want to update the spikes attribute after it has been updated # by the parent, we do this in slot 'thresholds' with an order # one higher than the parent order to ensure it takes place after the # parent threshold operation schedule = Scheduler(clock=source.clock, when='thresholds', order=source.order + 1) Group.__init__(self, when=schedule, name=name) self._N = stop - start self.start = start self.stop = stop # All the variables have to go via the _sub_idx to refer to the # appropriate values in the source group self.variables = Variables(self, default_index='_sub_idx') # overwrite the meaning of N and i self.variables.add_constant('_offset', unit=Unit(1), value=self.start) self.variables.add_reference('_source_i', source, 'i') self.variables.add_subexpression('i', unit=Unit(1), dtype=source.variables['i'].dtype, expr='_source_i - _offset') self.variables.add_constant('N', unit=Unit(1), value=self._N) # add references for all variables in the original group self.variables.add_references(source, source.variables.keys()) # Only the variable _sub_idx itself is stored in the subgroup # and needs the normal index for this group self.variables.add_arange('_sub_idx', size=self._N, start=self.start, index='_idx') for key, value in self.source.variables.indices.iteritems(): if value not in ('_idx', '0'): raise ValueError( ('Do not know how to deal with variable %s ' 'using index %s in a subgroup') % (key, value)) self.namespace = self.source.namespace self.codeobj_class = self.source.codeobj_class self._enable_group_attributes()
def test_str_repr(): ''' Test that str representations do not raise any errors and that repr fullfills eval(repr(x)) == x. ''' from numpy import array # necessary for evaluating repr units_which_should_exist = [ metre, meter, kilogram, kilogramme, second, amp, kelvin, mole, candle, radian, steradian, hertz, newton, pascal, joule, watt, coulomb, volt, farad, ohm, siemens, weber, tesla, henry, lumen, lux, becquerel, gray, sievert, katal, gram, gramme, molar, liter, litre ] # scaled versions of all these units should exist (we just check farad as an example) some_scaled_units = [ Yfarad, Zfarad, Efarad, Pfarad, Tfarad, Gfarad, Mfarad, kfarad, hfarad, dafarad, dfarad, cfarad, mfarad, ufarad, nfarad, pfarad, ffarad, afarad, zfarad, yfarad ] # some powered units powered_units = [cmetre2, Yfarad3] # Combined units complex_units = [ (kgram * metre2) / (amp * second3), 5 * (kgram * metre2) / (amp * second3), metre * second**-1, 10 * metre * second**-1, array([1, 2, 3]) * kmetre / second, np.ones(3) * nS / cm**2, Unit(1, dim=get_or_create_dimension(length=5, time=2)), 8000 * umetre**3, [0.0001, 10000] * umetre**3, 1 / metre, 1 / (coulomb * metre**2), Unit(1) / second, 3. * mM, 5 * mole / liter, 7 * liter / meter3 ] unitless = [second / second, 5 * second / second, Unit(1)] for u in itertools.chain(units_which_should_exist, some_scaled_units, powered_units, complex_units, unitless): assert (len(str(u)) > 0) assert_allclose(eval(repr(u)), u) # test the `DIMENSIONLESS` object assert str(DIMENSIONLESS) == '1' assert repr(DIMENSIONLESS) == 'Dimension()' # test DimensionMismatchError (only that it works without raising an error for error in [ DimensionMismatchError('A description'), DimensionMismatchError('A description', DIMENSIONLESS), DimensionMismatchError('A description', DIMENSIONLESS, second.dim) ]: assert len(str(error)) assert len(repr(error))
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_construction(): ''' Test the construction of quantity objects ''' q = 500 * ms assert_quantity(q, 0.5, second) q = np.float64(500) * ms assert_quantity(q, 0.5, second) q = np.array(500) * ms assert_quantity(q, 0.5, second) q = np.array([500, 1000]) * ms assert_quantity(q, np.array([0.5, 1]), second) q = Quantity(500) assert_quantity(q, 500, 1) q = Quantity(500, dim=second.dim) assert_quantity(q, 500, second) q = Quantity([0.5, 1], dim=second.dim) assert_quantity(q, np.array([0.5, 1]), second) q = Quantity(np.array([0.5, 1]), dim=second.dim) assert_quantity(q, np.array([0.5, 1]), second) q = Quantity([500 * ms, 1 * second]) assert_quantity(q, np.array([0.5, 1]), second) q = Quantity.with_dimensions(np.array([0.5, 1]), second=1) assert_quantity(q, np.array([0.5, 1]), second) q = [0.5, 1] * second assert_quantity(q, np.array([0.5, 1]), second) # dimensionless quantities q = Quantity([1, 2, 3]) assert_quantity(q, np.array([1, 2, 3]), Unit(1)) q = Quantity(np.array([1, 2, 3])) assert_quantity(q, np.array([1, 2, 3]), Unit(1)) q = Quantity([]) assert_quantity(q, np.array([]), Unit(1)) # copying/referencing a quantity q1 = Quantity.with_dimensions(np.array([0.5, 1]), second=1) q2 = Quantity(q1) # no copy assert_quantity(q2, np.asarray(q1), q1) q2[0] = 3 * second assert_equal(q1[0], 3 * second) q1 = Quantity.with_dimensions(np.array([0.5, 1]), second=1) q2 = Quantity(q1, copy=True) # copy assert_quantity(q2, np.asarray(q1), q1) q2[0] = 3 * second assert_equal(q1[0], 0.5 * second) # Illegal constructor calls assert_raises(TypeError, lambda: Quantity([500 * ms, 1])) assert_raises(TypeError, lambda: Quantity(['some', 'nonsense'])) assert_raises(DimensionMismatchError, lambda: Quantity([500 * ms, 1 * volt])) assert_raises(DimensionMismatchError, lambda: Quantity([500 * ms], dim=volt.dim)) q = Quantity.with_dimensions(np.array([0.5, 1]), second=1) assert_raises(DimensionMismatchError, lambda: Quantity(q, dim=volt.dim))
def __init__(self, source, record=True, when=None, name='spikemonitor*', codeobj_class=None): self.record = bool(record) #: The source we are recording from self.source = 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 CodeRunner.__init__(self, group=self, template='spikemonitor', name=name, when=scheduler) # Handle subgroups correctly start = getattr(source, 'start', 0) stop = getattr(source, 'stop', len(source)) self.variables = Variables(self) self.variables.add_clock_variables(scheduler.clock, prefix='_clock_') self.variables.add_reference('_spikespace', source.variables['_spikespace']) self.variables.add_dynamic_array('i', size=0, unit=Unit(1), dtype=np.int32, constant_size=False) self.variables.add_dynamic_array('t', size=0, unit=second, constant_size=False) self.variables.add_array('_count', size=len(source), unit=Unit(1), dtype=np.int32) self.variables.add_constant('_source_start', Unit(1), start) self.variables.add_constant('_source_stop', Unit(1), stop) self.variables.add_attribute_variable('N', unit=Unit(1), obj=self, attribute='_N', dtype=np.int32) self._N = 0 self._enable_group_attributes()
def __init__(self, source, record=True, when='end', order=0, name='spikemonitor*', codeobj_class=None): self.record = bool(record) #: The source we are recording from self.source = source self.codeobj_class = codeobj_class CodeRunner.__init__(self, group=self, code='', template='spikemonitor', name=name, clock=source.clock, when=when, order=order) self.add_dependency(source) # Handle subgroups correctly start = getattr(source, 'start', 0) stop = getattr(source, 'stop', len(source)) self.variables = Variables(self) self.variables.add_reference('_spikespace', source) self.variables.add_dynamic_array('i', size=0, unit=Unit(1), dtype=np.int32, constant_size=False) self.variables.add_dynamic_array('t', size=0, unit=second, constant_size=False) self.variables.add_arange('_source_i', size=len(source)) self.variables.add_array('_count', size=len(source), unit=Unit(1), dtype=np.int32, read_only=True, index='_source_i') self.variables.add_constant('_source_start', Unit(1), start) self.variables.add_constant('_source_stop', Unit(1), stop) self.variables.add_attribute_variable('N', unit=Unit(1), obj=self, attribute='_N', dtype=np.int32) self.variables.create_clock_variables(self._clock, prefix='_clock_') self._enable_group_attributes()
def _create_variables(self): ''' Create the variables dictionary for this `NeuronGroup`, containing entries for the equation variables and some standard entries. ''' # Get the standard variables for all groups s = Group._create_variables(self) # Standard variables always present s.update({ '_spikespace': ArrayVariable('_spikespace', Unit(1), self._spikespace, group_name=self.name) }) s.update({ '_spikes': AttributeVariable(Unit(1), self, 'spikes', constant=False) }) for eq in self.equations.itervalues(): if eq.type in (DIFFERENTIAL_EQUATION, PARAMETER): array = self.arrays[eq.varname] constant = ('constant' in eq.flags) s.update({ eq.varname: ArrayVariable(eq.varname, eq.unit, array, group_name=self.name, constant=constant, is_bool=eq.is_bool) }) elif eq.type == STATIC_EQUATION: s.update({ eq.varname: Subexpression(eq.unit, brian_prefs['core.default_scalar_dtype'], str(eq.expr), variables=s, 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: s.update({xi: StochasticVariable()}) return s
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_warning_internal_variables(): N = 5 group1 = SimpleGroup(namespace=None, variables={'N': Constant('N', Unit(1), 5)}) group2 = SimpleGroup(namespace=None, variables={'N': Constant('N', Unit(1), 7)}) with catch_logs() as l: group1.resolve('N') # should not raise a warning assert len(l) == 0, 'got warnings: %s' % str(l) with catch_logs() as l: group2.resolve('N') # should raise a warning assert len(l) == 1, 'got warnings: %s' % str(l) assert l[0][1].endswith('.resolution_conflict')
def test_apply_loop_invariant_optimisation_integer(): variables = { 'v': Variable('v', Unit(1), scalar=False), 'N': Constant('N', Unit(1), 10), 'b': Variable('b', Unit(1), scalar=True, dtype=int), 'c': Variable('c', Unit(1), scalar=True, dtype=int), 'd': Variable('d', Unit(1), scalar=True, dtype=int), 'y': Variable('y', Unit(1), scalar=True, dtype=float), 'z': Variable('z', Unit(1), scalar=True, dtype=float), 'w': Variable('w', Unit(1), 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.integer, int)) 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.integer, int)) assert scalar[1].var == '_lio_2' expr = scalar[1].expr.replace(' ', '') assert expr == 'b/(c/d)' assert np.issubdtype(scalar[2].dtype, (np.float, float)) assert scalar[2].var == '_lio_3' expr = scalar[2].expr.replace(' ', '') assert expr == '(y*w)/z' or expr == '(w*y)/z'
def test_automatic_augmented_assignments(): # We test that statements that could be rewritten as augmented assignments # are correctly rewritten (using sympy to test for symbolic equality) variables = { 'x': ArrayVariable('x', unit=Unit(1), owner=None, size=10, device=device), 'y': ArrayVariable('y', unit=Unit(1), owner=None, size=10, device=device), 'z': ArrayVariable('y', unit=Unit(1), owner=None, size=10, device=device), 'b': ArrayVariable('b', unit=Unit(1), owner=None, size=10, dtype=np.bool, device=device), 'clip': DEFAULT_FUNCTIONS['clip'], 'inf': DEFAULT_CONSTANTS['inf'] } statements = [ # examples that should be rewritten # Note that using our approach, we will never get -= or /= but always # the equivalent += or *= statements ('x = x + 1', 'x += 1'), ('x = 2 * x', 'x *= 2'), ('x = x - 3', 'x += -3'), ('x = x/2', 'x *= 0.5'), ('x = y + (x + 1)', 'x += y + 1'), ('x = x + x', 'x *= 2'), ('x = x + y + z', 'x += y + z'), ('x = x + y + z', 'x += y + z'), # examples that should not be rewritten ('x = 1/x', 'x = 1/x'), ('x = 1', 'x = 1'), ('x = 2*(x + 1)', 'x = 2*(x + 1)'), ('x = clip(x + y, 0, inf)', 'x = clip(x + y, 0, inf)'), ('b = b or False', 'b = b or False') ] for orig, rewritten in statements: scalar, vector = make_statements(orig, variables, np.float32) try: # we augment the assertion error with the original statement assert len(scalar) == 0, 'Did not expect any scalar statements but got ' + str(scalar) assert len(vector) == 1, 'Did expect a single statement but got ' + str(vector) statement = vector[0] expected_var, expected_op, expected_expr, _ = parse_statement(rewritten) assert expected_var == statement.var, 'expected write to variable %s, not to %s' % (expected_var, statement.var) assert expected_op == statement.op, 'expected operation %s, not %s' % (expected_op, statement.op) # Compare the two expressions using sympy to allow for different order etc. sympy_expected = str_to_sympy(expected_expr) sympy_actual = str_to_sympy(statement.expr) assert sympy_expected == sympy_actual, ('RHS expressions "%s" and "%s" are not identical' % (sympy_to_str(sympy_expected), sympy_to_str(sympy_actual))) except AssertionError as ex: raise AssertionError('Transformation for statement "%s" gave an unexpected result: %s' % (orig, str(ex)))
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 = 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, float)) assert scalar[0].var == '_lio_1' assert len(vector) == 2 assert all('_lio_' in stmt.expr for stmt in vector)
def update_abstract_code(self): # Update the not_refractory variable for the refractory period mechanism ref = self.group._refractory if ref is None: # No refractoriness self.abstract_code = '' elif isinstance(ref, Quantity): self.abstract_code = 'not_refractory = 1*((t - lastspike) > %f)\n' % ref else: namespace = self.group.namespace unit = parse_expression_unit(str(ref), namespace, self.group.variables) if have_same_dimensions(unit, second): self.abstract_code = 'not_refractory = 1*((t - lastspike) > %s)\n' % ref elif have_same_dimensions(unit, Unit(1)): if not is_boolean_expression(str(ref), namespace, self.group.variables): raise TypeError(('Refractory expression is dimensionless ' 'but not a boolean value. It needs to ' 'either evaluate to a timespan or to a ' 'boolean value.')) # boolean condition # we have to be a bit careful here, we can't just use the given # condition as it is, because we only want to *leave* # refractoriness, based on the condition self.abstract_code = 'not_refractory = 1*(not_refractory or not (%s))\n' % ref else: raise TypeError(('Refractory expression has to evaluate to a ' 'timespan or a boolean value, expression' '"%s" has units %s instead') % (ref, unit)) self.abstract_code += self.method(self.group.equations, self.group.variables)
def render_node(self, node): ''' Assumes that the node has already been fully processed by BrianASTRenderer ''' # can we pull this out? if node.scalar and node.complexity > 0: expr = self.node_renderer.render_node( self.arithmetic_simplifier.render_node(node)) if expr in self.loop_invariants: name = self.loop_invariants[expr] else: self.n += 1 name = '_lio_' + str(self.n) self.loop_invariants[expr] = name self.loop_invariant_dtypes[name] = node.dtype numpy_dtype = { 'boolean': bool, 'integer': int, 'float': float }[node.dtype] self.variables[name] = AuxiliaryVariable(name, Unit(1), dtype=numpy_dtype, scalar=True) # None is the expression context, we don't use it so we just set to None newnode = ast.Name(name, None) newnode.scalar = True newnode.dtype = node.dtype newnode.complexity = 0 newnode.stateless = node.stateless return newnode # otherwise, render node as usual return super(Simplifier, self).render_node(node)
def __init__(self, group, when='thresholds', event='spike'): self.event = event if group._refractory is False or event != 'spike': template_kwds = {'_uses_refractory': False} needed_variables = [] else: template_kwds = {'_uses_refractory': True} needed_variables = ['t', 'not_refractory', 'lastspike'] # Since this now works for general events not only spikes, we have to # pass the information about which variable to use to the template, # it can not longer simply refer to "_spikespace" eventspace_name = '_{}space'.format(event) template_kwds['eventspace_variable'] = group.variables[eventspace_name] needed_variables.append(eventspace_name) self.variables = Variables(self) self.variables.add_auxiliary_variable('_cond', unit=Unit(1), dtype=np.bool) CodeRunner.__init__( self, group, 'threshold', code='', # will be set in update_abstract_code clock=group.clock, when=when, order=group.order, name=group.name + '_thresholder*', needed_variables=needed_variables, template_kwds=template_kwds)
def test_get_identifiers_recursively(): ''' Test finding identifiers including subexpressions. ''' variables = {'sub1': Subexpression(name='sub1', unit=Unit(1), dtype=np.float32, expr='sub2 * z', owner=FakeGroup(variables={}), device=None), 'sub2': Subexpression(name='sub2', unit=Unit(1), dtype=np.float32, expr='5 + y', owner=FakeGroup(variables={}), device=None), 'x': Variable(unit=None, name='x')} identifiers = get_identifiers_recursively(['_x = sub1 + x'], variables) assert identifiers == {'x', '_x', 'y', 'z', 'sub1', 'sub2'}
def _get_refractory_code(self, run_namespace, level=0): ref = self.group._refractory if ref is False: # No refractoriness abstract_code = '' elif isinstance(ref, Quantity): abstract_code = 'not_refractory = (t - lastspike) > %f\n' % ref else: identifiers = get_identifiers(ref) variables = self.group.resolve_all(identifiers, identifiers, run_namespace=run_namespace, level=level + 1) unit = parse_expression_unit(str(ref), variables) if have_same_dimensions(unit, second): abstract_code = 'not_refractory = (t - lastspike) > %s\n' % ref elif have_same_dimensions(unit, Unit(1)): if not is_boolean_expression(str(ref), variables): raise TypeError(('Refractory expression is dimensionless ' 'but not a boolean value. It needs to ' 'either evaluate to a timespan or to a ' 'boolean value.')) # boolean condition # we have to be a bit careful here, we can't just use the given # condition as it is, because we only want to *leave* # refractoriness, based on the condition abstract_code = 'not_refractory = not_refractory or not (%s)\n' % ref else: raise TypeError(('Refractory expression has to evaluate to a ' 'timespan or a boolean value, expression' '"%s" has units %s instead') % (ref, unit)) return abstract_code