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_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' with pytest.raises(SyntaxError): make_statements(code, variables, np.float32)
def translate(self, code, dtype): ''' Translates an abstract code block into the target language. ''' scalar_statements = {} vector_statements = {} for ac_name, ac_code in code.items(): statements = make_statements(ac_code, self.variables, dtype, optimise=True, blockname=ac_name) scalar_statements[ac_name], vector_statements[ac_name] = statements for vs in vector_statements.values(): # Check that the statements are meaningful independent on the order of # execution (e.g. for synapses) try: if self.has_repeated_indices( vs ): # only do order dependence if there are repeated indices check_for_order_independence(vs, self.variables, self.variable_indices) except OrderDependenceError: # If the abstract code is only one line, display it in full if len(vs) <= 1: error_msg = 'Abstract code: "%s"\n' % vs[0] else: error_msg = ('%d lines of abstract code, first line is: ' '"%s"\n') % (len(vs), vs[0]) logger.warn( ('Came across an abstract code block that may not be ' 'well-defined: the outcome may depend on the ' 'order of execution. You can ignore this warning if ' 'you are sure that the order of operations does not ' 'matter. ' + error_msg)) translated = self.translate_statement_sequence(scalar_statements, vector_statements) return translated
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 translate( self, code, dtype ): # TODO: it's not so nice we have to copy the contents of this function.. ''' Translates an abstract code block into the target language. ''' # first check if user code is not using variables that are also used by GSL reserved_variables = [ '_dataholder', '_fill_y_vector', '_empty_y_vector', '_GSL_dataholder', '_GSL_y', '_GSL_func' ] if any([var in self.variables for var in reserved_variables]): # import here to avoid circular import raise ValueError(("The variables %s are reserved for the GSL " "internal code." % (str(reserved_variables)))) # if the following statements are not added, angela translates the # differential expressions in the abstract code for GSL to scalar statements # in the case no non-scalar variables are used in the expression diff_vars = self.find_differential_variables(list(code.values())) self.add_gsl_variables_as_non_scalar(diff_vars) # add arrays we want to use in generated code before self.generator.translate() so # angela does namespace unpacking for us pointer_names = self.add_meta_variables(self.method_options) scalar_statements = {} vector_statements = {} for ac_name, ac_code in code.items(): statements = make_statements(ac_code, self.variables, dtype, optimise=True, blockname=ac_name) scalar_statements[ac_name], vector_statements[ac_name] = statements for vs in vector_statements.values(): # Check that the statements are meaningful independent on the order of # execution (e.g. for synapses) try: if self.has_repeated_indices( vs ): # only do order dependence if there are repeated indices check_for_order_independence( vs, self.generator.variables, self.generator.variable_indices) except OrderDependenceError: # If the abstract code is only one line, display it in ful l if len(vs) <= 1: error_msg = 'Abstract code: "%s"\n' % vs[0] else: error_msg = ( '%_GSL_driver lines of abstract code, first line is: ' '"%s"\n') % (len(vs), vs[0]) # save function names because self.generator.translate_statement_sequence # deletes these from self.variables but we need to know which identifiers # we can safely ignore (i.e. we can ignore the functions because they are # handled by the original generator) self.function_names = self.find_function_names() scalar_code, vector_code, kwds = self.generator.translate_statement_sequence( scalar_statements, vector_statements) ############ translate code for GSL # first check if any indexing other than '_idx' is used (currently not supported) for code_list in list(scalar_code.values()) + list( vector_code.values()): for code in code_list: m = re.search('\[(\w+)\]', code) if m is not None: if m.group(1) != '0' and m.group(1) != '_idx': from angela2.stateupdaters.base import UnsupportedEquationsException raise UnsupportedEquationsException( ("Equations result in state " "updater code with indexing " "other than '_idx', which " "is currently not supported " "in combination with the " "GSL stateupdater.")) # differential variable specific operations to_replace = self.diff_var_to_replace(diff_vars) GSL_support_code = self.get_dimension_code(len(diff_vars)) GSL_support_code += self.yvector_code(diff_vars) # analyze all needed variables; if not in self.variables: put in separate dic. # also keep track of variables needed for scalar statements and vector statements other_variables = self.find_undefined_variables( scalar_statements[None] + vector_statements[None]) variables_in_scalar = self.find_used_variables(scalar_statements[None], other_variables) variables_in_vector = self.find_used_variables(vector_statements[None], other_variables) # so that _dataholder holds diff_vars as well, even if they don't occur # in the actual statements for var in list(diff_vars.keys()): if not var in variables_in_vector: variables_in_vector[var] = self.variables[var] # lets keep track of the variables that eventually need to be added to # the _GSL_dataholder somehow self.variables_to_be_processed = list(variables_in_vector.keys()) # add code for _dataholder struct GSL_support_code = self.write_dataholder( variables_in_vector) + GSL_support_code # add e.g. _lio_1 --> _GSL_dataholder._lio_1 to replacer to_replace.update( self.to_replace_vector_vars(variables_in_vector, ignore=list(diff_vars.keys()))) # write statements that unpack (python) namespace to _dataholder struct # or local namespace GSL_main_code = self.unpack_namespace(variables_in_vector, variables_in_scalar, ['t']) # rewrite actual calculations described by vector_code and put them in _GSL_func func_code = self.translate_one_statement_sequence( vector_statements[None], scalar=False) GSL_support_code += self.make_function_code( self.translate_vector_code(func_code, to_replace)) scalar_func_code = self.translate_one_statement_sequence( scalar_statements[None], scalar=True) # rewrite scalar code, keep variables that are needed in scalar code normal # and add variables to _dataholder for vector_code GSL_main_code += '\n' + self.translate_scalar_code( scalar_func_code, variables_in_scalar, variables_in_vector) if len(self.variables_to_be_processed) > 0: raise AssertionError( ("Not all variables that will be used in the vector " "code have been added to the _GSL_dataholder. This " "might mean that the _GSL_func is using unitialized " "variables." "\nThe unprocessed variables " "are: %s" % (str(self.variables_to_be_processed)))) scalar_code['GSL'] = GSL_main_code kwds['define_GSL_scale_array'] = self.scale_array_code( diff_vars, self.method_options) kwds['n_diff_vars'] = len(diff_vars) kwds['GSL_settings'] = dict(self.method_options) kwds['GSL_settings']['integrator'] = self.integrator kwds['support_code_lines'] += GSL_support_code.split('\n') kwds['t_array'] = self.get_array_name(self.variables['t']) + '[0]' kwds['dt_array'] = self.get_array_name(self.variables['dt']) + '[0]' kwds['define_dt'] = 'dt' not in variables_in_scalar kwds['cpp_standalone'] = self.is_cpp_standalone() for key, value in list(pointer_names.items()): kwds[key] = value return scalar_code, vector_code, kwds
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', owner=None, size=10, device=device), 'y': ArrayVariable('y', owner=None, size=10, device=device), 'z': ArrayVariable('y', owner=None, size=10, device=device), 'b': ArrayVariable('b', 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.0', 'x += 1.0'), ('x = 2.0 * x', 'x *= 2.0'), ('x = x - 3.0', 'x += -3.0'), ('x = x/2.0', 'x *= 0.5'), ('x = y + (x + 1.0)', 'x += y + 1.0'), ('x = x + x', 'x *= 2.0'), ('x = x + y + z', 'x += y + z'), ('x = x + y + z', 'x += y + z'), # examples that should not be rewritten ('x = 1.0/x', 'x = 1.0/x'), ('x = 1.0', 'x = 1.0'), ('x = 2.0*(x + 1.0)', 'x = 2.0*(x + 1.0)'), ('x = clip(x + y, 0.0, inf)', 'x = clip(x + y, 0.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)))