def __init__(self, tau_rec=100.0, tau_facil=0.01, U=0.5): if tau_facil<= 0.0: _error('tau_facil must be positive. Choose a very small value if you have to, or derive a new synapse.') exit(0) parameters = """ tau_rec = %(tau_rec)s tau_facil = %(tau_facil)s U = %(U)s """ % {'tau_rec': tau_rec, 'tau_facil': tau_facil, 'U': U} equations = """ dx/dt = (1 - x)/tau_rec : init = 1.0, event-driven du/dt = (U - u)/tau_facil : init = %(U)s, event-driven """ % {'tau_rec': tau_rec, 'tau_facil': tau_facil, 'U': U} pre_spike=""" g_target += w * u * x x *= (1 - u) u += U * (1 - u) """ Synapse.__init__(self, parameters=parameters, equations=equations, pre_spike=pre_spike, name="Short-term plasticity", description="Synapse exhibiting short-term facilitation and depression, implemented using the model of Tsodyks, Markram et al.") # For reporting self._instantiated.append(True)
def report(filename="./report.tex", standalone=True, gather_subprojections=False, title=None, author=None, date=None, net_id=0): """ Generates a report describing the network. If the filename ends with ``.tex``, the TeX file is generated according to: Nordlie E, Gewaltig M-O, Plesser HE (2009). Towards Reproducible Descriptions of Neuronal Network Models. PLoS Comput Biol 5(8): e1000456. If the filename ends with ``.md``, a (more complete) Markdown file is generated, which can be converted to pdf or html by ``pandoc``:: pandoc report.md -sSN -V geometry:margin=1in -o report.pdf pandoc report.md -sSN -o report.html :param filename: name of the file where the report will be written (default: "./report.tex") :param standalone: tells if the generated TeX file should be directly compilable or only includable (default: True). Ignored for Markdown. :param gather_subprojections: if a projection between two populations has been implemented as a multiple of projections between sub-populations, this flag allows to group them in the summary (default: False). :param title: title of the document (Markdown only) :param author: author of the document (Markdown only) :param date: date of the document (Markdown only) :param net_id: id of the network to be used for reporting (default: 0, everything that was declared) """ if filename.endswith('.tex'): from .LatexReport import report_latex report_latex(filename, standalone, gather_subprojections, net_id) elif filename.endswith('.md'): from .MarkdownReport import report_markdown report_markdown(filename, standalone, gather_subprojections, title, author, date, net_id) else: _error('report(): the filename must end with .tex or .md.')
def eventdriven(self, expression): # Standardize the equation real_tau, stepsize, steadystate = self.standardize_ODE(expression) if real_tau == None: # the equation can not be standardized _print(self.expression) _error( 'The equation is not a linear ODE and can not be evaluated exactly.' ) exit(0) # Check the steady state is not dependent on other variables for var in self.variables: if self.local_dict[var] in steadystate.atoms(): _print(self.expression) _error('The equation can not depend on other variables (' + var + ') to be evaluated exactly.') exit(0) # Obtain C code variable_name = self.c_code(self.local_dict[self.name]) steady = self.c_code(steadystate) if steady == '0': code = variable_name + '*= exp(dt*(_last_event[i][j] - (t))/(' + self.c_code( real_tau) + '));' else: code = variable_name + ' = ' + steady + ' + (' + variable_name + ' - ' + steady + ')*exp(dt*(_last_event[i][j] - (t))/(' + self.c_code( real_tau) + '));' return code
def __init__(self, parameters="", equations="", psp=None, operation='sum', pre_spike=None, post_spike=None, functions=None, pruning=None, creating=None, name=None, description=None, extra_values={} ): """ *Parameters*: * **parameters**: parameters of the neuron and their initial value. * **equations**: equations defining the temporal evolution of variables. * **psp**: influence of a single synapse on the post-synaptic neuron (default for rate-coded: w*pre.r). * **operation**: operation (sum, max, min, mean) performed by the post-synaptic neuron on the individual psp (rate-coded only, default=sum). * **pre_spike**: updating of variables when a pre-synaptic spike is received (spiking only). * **post_spike**: updating of variables when a post-synaptic spike is emitted (spiking only). * **functions**: additional functions used in the equations. * **name**: name of the synapse type (used for reporting only). * **description**: short description of the synapse type (used for reporting). """ # Store the parameters and equations self.parameters = parameters self.equations = equations self.functions = functions self.pre_spike = pre_spike self.post_spike = post_spike self.psp = psp self.operation = operation self.extra_values = extra_values self.pruning = pruning self.creating = creating # Type of the synapse TODO: smarter self.type = 'spike' if pre_spike else 'rate' # Check the operation if self.type == 'spike' and self.operation != 'sum': _error('Spiking synapses can only perform a sum of presynaptic potentials.') if not self.operation in ['sum', 'min', 'max', 'mean']: _error('The only operations permitted are: sum (default), min, max, mean.') # Description self.description = None # Reporting if not hasattr(self, '_instantiated') : # User-defined _objects['synapses'].append(self) elif len(self._instantiated) == 0: # First instantiated of the class _objects['synapses'].append(self) if name: self.name = name else: self.name = self._default_names[self.type] if description: self.short_description = description else: if self.type == 'spike': self.short_description = "Instantaneous increase of the post-synaptic conductance after a spike is received." else: self.short_description = "Weighted sum of firing rates."
def report(filename="./report.tex", standalone=True, gather_subprojections=False, title=None, author=None, date=None, net_id=0): """ Generates a report describing the network. If the filename ends with ``.tex``, the TeX file is generated according to: Nordlie E, Gewaltig M-O, Plesser HE (2009). Towards Reproducible Descriptions of Neuronal Network Models. PLoS Comput Biol 5(8): e1000456. If the filename ends with ``.md``, a (more complete) Markdown file is generated, which can be converted to pdf or html by ``pandoc``:: pandoc report.md -sSN -V geometry:margin=1in -o report.pdf pandoc report.md -sSN -o report.html *Parameters:* * **filename**: name of the file where the report will be written (default: "./report.tex") * **standalone**: tells if the generated TeX file should be directly compilable or only includable (default: True). Ignored for Markdown. * **gather_subprojections**: if a projection between two populations has been implemented as a multiple of projections between sub-populations, this flag allows to group them in the summary (default: False). * **title**: title of the document (Markdown only) * **author**: author of the document (Markdown only) * **date**: date of the document (Markdown only) * **net_id**: id of the network to be used for reporting (default: 0, everything that was declared) """ if filename.endswith('.tex'): from .LatexReport import report_latex report_latex(filename, standalone, gather_subprojections, net_id) elif filename.endswith('.md'): from .MarkdownReport import report_markdown report_markdown(filename, standalone, gather_subprojections, title, author, date, net_id) else: _error('report(): the filename must end with .tex or .md.')
def extract_randomdist(description): " Extracts RandomDistribution objects from all variables" rk_rand = 0 random_objects = [] for variable in description['variables']: eq = variable['eq'] # Search for all distributions for dist in available_distributions: matches = re.findall('(?P<pre>[^\w.])'+dist+'\(([^()]+)\)', eq) if matches == ' ': continue for l, v in matches: # Check the arguments arguments = v.split(',') # Check the number of provided arguments if len(arguments) < distributions_arguments[dist]: _error(eq) _error('The distribution ' + dist + ' requires ' + str(distributions_arguments[dist]) + 'parameters') elif len(arguments) > distributions_arguments[dist]: _error(eq) _error('Too many parameters provided to the distribution ' + dist) # Process the arguments processed_arguments = "" for idx in range(len(arguments)): try: arg = float(arguments[idx]) except: # A global parameter if arguments[idx].strip() in description['global']: if description['object'] == 'neuron': arg = arguments[idx].strip() else: arg = arguments[idx].strip() else: _error(arguments[idx] + ' is not a global parameter of the neuron/synapse. It can not be used as an argument to the random distribution ' + dist + '(' + v + ')') exit(0) processed_arguments += str(arg) if idx != len(arguments)-1: # not the last one processed_arguments += ', ' definition = distributions_equivalents[dist] + '(' + processed_arguments + ')' # Store its definition desc = {'name': 'rand_' + str(rk_rand) , 'dist': dist, 'definition': definition, 'args' : processed_arguments, 'template': distributions_equivalents[dist]} rk_rand += 1 random_objects.append(desc) # Replace its definition by its temporary name # Problem: when one uses twice the same RD in a single equation (perverse...) eq = eq.replace(dist+'('+v+')', desc['name']) # Add the new variable to the vocabulary description['attributes'].append(desc['name']) if variable['name'] in description['local']: description['local'].append(desc['name']) else: # Why not on a population-wide variable? description['global'].append(desc['name']) variable['transformed_eq'] = eq return random_objects
def extract_spike_variable(description): cond = prepare_string(description['raw_spike']) if len(cond) > 1: _print(description['raw_spike']) _error('The spike condition must be a single expression') translator = Equation('raw_spike_cond', cond[0].strip(), description) raw_spike_code = translator.parse() # Also store the variables used in the condition, as it may be needed for CUDA generation spike_code_dependencies = translator.dependencies() reset_desc = [] if 'raw_reset' in description.keys() and description['raw_reset']: reset_desc = process_equations(description['raw_reset']) for var in reset_desc: translator = Equation(var['name'], var['eq'], description) var['cpp'] = translator.parse() var['dependencies'] = translator.dependencies() return { 'spike_cond': raw_spike_code, 'spike_cond_dependencies': spike_code_dependencies, 'spike_reset': reset_desc}
def idx_target(val): target = val.group(1).strip() if target == '': _print(eq) _error('pre.sum() requires one argument.') exit(0) rep = '_pre_sum_' + target.strip() dependencies['pre'].append('sum(' + target + ')') untouched[rep] = '_sum_' + target + '%(pre_index)s' return rep
def idx_target(val): target = val.group(1).strip() if target == '': _print(eq) _error('post.sum() requires one argument.') exit(0) dependencies['post'].append('sum('+target+')') rep = '_post_sum_' + target.strip() untouched[rep] = '%(post_prefix)s_sum_' + target + '%(post_index)s' return rep
def __add__(self, other): """Allows to join two neurons if they have the same population.""" if other.population == self.population: if isinstance(other, IndividualNeuron): return PopulationView(self.population, list(set([self.rank, other.rank]))) elif isinstance(other, PopulationView): return PopulationView(self.population, list(set([self.rank] + other.ranks))) else: _error("can only add two PopulationViews of the same population.") return None
def extract_functions(description, local_global=False): """ Extracts all functions from a multiline description.""" if not description: return [] # Split the multilines into individual lines function_list = process_equations(description) # Process each function functions = [] for f in function_list: eq = f['eq'] var_name, content = eq.split('=', 1) # Extract the name of the function func_name = var_name.split('(', 1)[0].strip() # Extract the arguments arguments = (var_name.split('(', 1)[1].split(')')[0]).split(',') arguments = [arg.strip() for arg in arguments] # Extract their types types = f['constraint'] if types == '': return_type = 'double' arg_types = ['double' for a in arguments] else: types = types.split(',') return_type = types[0].strip() arg_types = [arg.strip() for arg in types[1:]] if not len(arg_types) == len(arguments): _error('You must specify exactly the types of return value and arguments in ' + eq) arg_line = "" for i in range(len(arguments)): arg_line += arg_types[i] + " " + arguments[i] if not i == len(arguments) -1: arg_line += ', ' # Process the content eq2, condition = extract_ite('', content, {'attributes': [], 'local':[], 'global': [], 'variables': [], 'parameters': []}, split=False) if condition == []: parser = FunctionParser(content, arguments) parsed_content = parser.parse() else: parser = FunctionParser(content, arguments) parsed_content = translate_ITE("", eq2, condition, {'attributes': [], 'local':[], 'global': [], 'variables': [], 'parameters': []}, {}, split=False) # Create the one-liner fdict = {'name': func_name, 'args': arguments, 'content': content, 'return_type': return_type, 'arg_types': arg_types, 'parsed_content': parsed_content, 'arg_line': arg_line} if not local_global: # local to a class oneliner = """%(return_type)s %(name)s (%(arg_line)s) {return %(parsed_content)s ;}; """ % fdict else: # global oneliner = """inline %(return_type)s %(name)s (%(arg_line)s) {return %(parsed_content)s ;}; """ % fdict fdict['cpp'] = oneliner functions.append(fdict) return functions
def parse_expression(self, expression, local_dict): " Parses a string with respect to the vocabulary defined in local_dict." try: res = parse_expr(transform_condition(expression), local_dict = local_dict, transformations = (standard_transformations + (convert_xor,)) ) except Exception as e: _print(e) _error('Can not analyse the expression :' + str(expression)) exit(0) else: return res
def parse_expression(self, expression, local_dict): " Parses a string with respect to the vocabulary defined in local_dict." try: res = parse_expr(transform_condition(expression), local_dict=local_dict, transformations=(standard_transformations + (convert_xor, ))) except Exception as e: _print(e) _error('Can not analyse the expression :' + str(expression)) exit(0) else: return res
def process_variables(self): # Check if the numerical method is the same for all ODEs methods = [] for var in self.variables: methods.append(var['method']) if len(list(set(methods))) > 1: # mixture of methods _error('Can not mix different numerical methods when solving a coupled system of equations.') exit(0) else: method = methods[0] if method == 'implicit' or method == 'semiimplicit': return self.solve_implicit(self.expression_list) elif method == 'midpoint': return self.solve_midpoint(self.expression_list)
def parse(self): try: if self.type == 'ODE': code = self.analyse_ODE(self.expression) elif self.type == 'cond': code = self.analyse_condition(self.expression) elif self.type == 'inc': code = self.analyse_increment(self.expression) elif self.type == 'return': code = self.analyse_return(self.expression) elif self.type == 'simple': code = self.analyse_assignment(self.expression) except Exception as e: _print(e) _error('can not analyse', self.expression) return code
def extract_targets(variables): targets = [] for var in variables: # Rate-coded neurons code = re.findall('(?P<pre>[^\w.])sum\(\s*([^()]+)\s*\)', var['eq']) for l, t in code: if t.strip() == '': _print(var['eq']) _error('sum() must have one argument.') exit(0) targets.append(t.strip()) # Spiking neurons code = re.findall('([^\w.])g_([\w]+)', var['eq']) for l, t in code: targets.append(t.strip()) return list(set(targets))
def process_variables(self): # Check if the numerical method is the same for all ODEs methods = [] for var in self.variables: methods.append(var['method']) if len(list(set(methods))) > 1: # mixture of methods _error( 'Can not mix different numerical methods when solving a coupled system of equations.' ) exit(0) else: method = methods[0] if method == 'implicit' or method == 'semiimplicit': return self.solve_implicit(self.expression_list) elif method == 'midpoint': return self.solve_midpoint(self.expression_list)
def extract_spike_variable(description): cond = prepare_string(description['raw_spike']) if len(cond) > 1: _error('The spike condition must be a single expression') _print(description['raw_spike']) exit(0) translator = Equation('raw_spike_cond', cond[0].strip(), description) raw_spike_code = translator.parse() reset_desc = [] if 'raw_reset' in description.keys() and description['raw_reset']: reset_desc = process_equations(description['raw_reset']) for var in reset_desc: translator = Equation(var['name'], var['eq'], description) var['cpp'] = translator.parse() return {'spike_cond': raw_spike_code, 'spike_reset': reset_desc}
def implicit(self, expression): "Full implicit method, linearising for example (V - E)^2, but this is not desired." # Transform the gradient into a difference TODO: more robust... new_expression = expression.replace('d' + self.name, '_t_gradient_') new_expression = re.sub(r'([^\w]+)' + self.name + r'([^\w]+)', r'\1_' + self.name + r'\2', new_expression) new_expression = new_expression.replace( '_t_gradient_', '(_' + self.name + ' - ' + self.name + ')') # Add a sympbol for the next value of the variable new_var = Symbol('_' + self.name) self.local_dict['_' + self.name] = new_var # Parse the string analysed = parse_expr(new_expression, local_dict=self.local_dict, transformations=(standard_transformations + (convert_xor, ))) self.analysed = analysed # Solve the equation for delta_mp solved = solve(analysed, new_var) if len(solved) > 1: _print(self.expression) _error('the ODE is not linear, can not use the implicit method.') exit(0) else: solved = solved[0] equation = simplify(collect(solved, self.local_dict['dt'])) # Obtain C code variable_name = self.c_code(self.local_dict[self.name]) explicit_code = 'double _' + self.name + ' = '\ + self.c_code(equation) + ';' switch = variable_name + ' = _' + self.name + ' ;' # Return result return [explicit_code, switch]
def __init__(self, tau_rec=100.0, tau_facil=0.01, U=0.5): if tau_facil <= 0.0: _error( 'tau_facil must be positive. Choose a very small value if you have to, or derive a new synapse.' ) exit(0) parameters = """ tau_rec = %(tau_rec)s tau_facil = %(tau_facil)s U = %(U)s """ % { 'tau_rec': tau_rec, 'tau_facil': tau_facil, 'U': U } equations = """ dx/dt = (1 - x)/tau_rec : init = 1.0, event-driven du/dt = (U - u)/tau_facil : init = %(U)s, event-driven """ % { 'tau_rec': tau_rec, 'tau_facil': tau_facil, 'U': U } pre_spike = """ g_target += w * u * x x *= (1 - u) u += U * (1 - u) """ Synapse.__init__( self, parameters=parameters, equations=equations, pre_spike=pre_spike, name="Short-term plasticity", description= "Synapse exhibiting short-term facilitation and depression, implemented using the model of Tsodyks, Markram et al." ) # For reporting self._instantiated.append(True)
def extract_globalops_neuron(name, eq, description): """ Replaces global operations (mean(r), etc) with arbitrary names and returns a dictionary of changes. """ untouched = {} globs = [] # Global ops glop_names = ['min', 'max', 'mean', 'norm1', 'norm2'] for op in glop_names: matches = re.findall('([^\w]*)' + op + '\(([\s\w]*)\)', eq) for pre, var in matches: if var.strip() in description['local']: globs.append({'function': op, 'variable': var.strip()}) oldname = op + '(' + var + ')' newname = '_' + op + '_' + var.strip() eq = eq.replace(oldname, newname) untouched[newname] = '_' + op + '_' + var.strip() else: _error(eq + '\nThere is no local attribute ' + var + '.') exit(0) return eq, untouched, globs
def extract_globalops_neuron(name, eq, description): """ Replaces global operations (mean(r), etc) with arbitrary names and returns a dictionary of changes. """ untouched = {} globs = [] # Global ops glop_names = ['min', 'max', 'mean', 'norm1', 'norm2'] for op in glop_names: matches = re.findall('([^\w]*)'+op+'\(([\s\w]*)\)', eq) for pre, var in matches: if var.strip() in description['local']: globs.append({'function': op, 'variable': var.strip()}) oldname = op + '(' + var + ')' newname = '_' + op + '_' + var.strip() eq = eq.replace(oldname, newname) untouched[newname] = '_' + op + '_' + var.strip() else: _error(eq+'\nThere is no local attribute '+var+'.') exit(0) return eq, untouched, globs
def extract_parameters(description, extra_values={}): """ Extracts all variable information from a multiline description.""" parameters = [] # Split the multilines into individual lines parameter_list = prepare_string(description) # Analyse all variables for definition in parameter_list: # Check if there are flags after the : symbol equation, constraint = split_equation(definition) # Extract the name of the variable name = extract_name(equation) if name in ['_undefined', ""]: _error("Definition can not be analysed: " + equation) exit(0) # Process constraint bounds, flags, ctype, init = extract_boundsflags( constraint, equation, extra_values) # Determine locality for f in ['population', 'postsynaptic']: if f in flags: locality = 'global' break else: locality = 'local' # Store the result desc = { 'name': name, 'locality': locality, 'eq': equation, 'bounds': bounds, 'flags': flags, 'ctype': ctype, 'init': init, } parameters.append(desc) return parameters
def implicit(self, expression): "Full implicit method, linearising for example (V - E)^2, but this is not desired." # Transform the gradient into a difference TODO: more robust... new_expression = expression.replace('d'+self.name, '_t_gradient_') new_expression = re.sub(r'([^\w]+)'+self.name+r'([^\w]+)', r'\1_'+self.name+r'\2', new_expression) new_expression = new_expression.replace('_t_gradient_', '(_'+self.name+' - '+self.name+')') # Add a sympbol for the next value of the variable new_var = Symbol('_'+self.name) self.local_dict['_'+self.name] = new_var # Parse the string analysed = parse_expr(new_expression, local_dict = self.local_dict, transformations = (standard_transformations + (convert_xor,)) ) self.analysed = analysed # Solve the equation for delta_mp solved = solve(analysed, new_var) if len(solved) > 1: _print(self.expression) _error('the ODE is not linear, can not use the implicit method.') exit(0) else: solved = solved[0] equation = simplify(collect( solved, self.local_dict['dt'])) # Obtain C code variable_name = self.c_code(self.local_dict[self.name]) explicit_code = 'double _' + self.name + ' = '\ + self.c_code(equation) + ';' switch = variable_name + ' = _' + self.name + ' ;' # Return result return [explicit_code, switch]
def extract_spike_variable(description): cond = prepare_string(description['raw_spike']) if len(cond) > 1: _error('The spike condition must be a single expression') _print(description['raw_spike']) exit(0) translator = Equation('raw_spike_cond', cond[0].strip(), description) raw_spike_code = translator.parse() reset_desc = [] if 'raw_reset' in description.keys() and description['raw_reset']: reset_desc = process_equations(description['raw_reset']) for var in reset_desc: translator = Equation(var['name'], var['eq'], description) var['cpp'] = translator.parse() return { 'spike_cond': raw_spike_code, 'spike_reset': reset_desc}
def extract_name(equation, left=False): " Extracts the name of a parameter/variable by looking the left term of an equation." equation = equation.replace(' ','') if not left: # there is potentially an equal sign try: name = equation.split('=')[0] except: # No equal sign. Eg: baseline : init=0.0 return equation.strip() # Search for increments operators = ['+=', '-=', '*=', '/=', '>=', '<='] for op in operators: if op in equation: return equation.split(op)[0] else: name = equation.strip() # Search for increments operators = ['+', '-', '*', '/'] for op in operators: if equation.endswith(op): return equation.split(op)[0] # Check for error if name.strip() == "": _error('the variable name can not be extracted from ' + equation) # Search for any operation in the left side operators = ['+', '-', '*', '/'] ode = False for op in operators: if not name.find(op) == -1: ode = True if not ode: # variable name is alone on the left side return name # ODE: the variable name is between d and /dt name = re.findall("d([\w]+)/dt", name) if len(name) == 1: return name[0].strip() else: return '_undefined'
def extract_name(equation, left=False): " Extracts the name of a parameter/variable by looking the left term of an equation." equation = equation.replace(' ', '') if not left: # there is potentially an equal sign try: name = equation.split('=')[0] except: # No equal sign. Eg: baseline : init=0.0 return equation.strip() # Search for increments operators = ['+=', '-=', '*=', '/=', '>=', '<='] for op in operators: if op in equation: return equation.split(op)[0] else: name = equation.strip() # Search for increments operators = ['+', '-', '*', '/'] for op in operators: if equation.endswith(op): return equation.split(op)[0] # Check for error if name.strip() == "": _error('the variable name can not be extracted from ' + equation) exit(0) # Search for any operation in the left side operators = ['+', '-', '*', '/'] ode = False for op in operators: if not name.find(op) == -1: ode = True if not ode: # variable name is alone on the left side return name # ODE: the variable name is between d and /dt name = re.findall("d([\w]+)/dt", name) if len(name) == 1: return name[0].strip() else: return '_undefined'
def extract_parameters(description, extra_values={}): """ Extracts all variable information from a multiline description.""" parameters = [] # Split the multilines into individual lines parameter_list = prepare_string(description) # Analyse all variables for definition in parameter_list: # Check if there are flags after the : symbol equation, constraint = split_equation(definition) # Extract the name of the variable name = extract_name(equation) if name in ['_undefined', ""]: _error("Definition can not be analysed: " + equation) exit(0) # Process constraint bounds, flags, ctype, init = extract_boundsflags(constraint, equation, extra_values) # Determine locality for f in ['population', 'postsynaptic']: if f in flags: locality = 'global' break else: locality = 'local' # Store the result desc = {'name': name, 'locality': locality, 'eq': equation, 'bounds': bounds, 'flags' : flags, 'ctype' : ctype, 'init' : init, } parameters.append(desc) return parameters
def eventdriven(self, expression): # Standardize the equation real_tau, stepsize, steadystate = self.standardize_ODE(expression) if real_tau == None: # the equation can not be standardized _print(self.expression) _error('The equation is not a linear ODE and can not be evaluated exactly.') exit(0) # Check the steady state is not dependent on other variables for var in self.variables: if self.local_dict[var] in steadystate.atoms(): _print(self.expression) _error('The equation can not depend on other variables ('+var+') to be evaluated exactly.') exit(0) # Obtain C code variable_name = self.c_code(self.local_dict[self.name]) steady = self.c_code(steadystate) if steady == '0': code = variable_name + '*= exp(dt*(_last_event[i][j] - (t))/(' + self.c_code(real_tau) + '));' else: code = variable_name + ' = ' + steady + ' + (' + variable_name + ' - ' + steady + ')*exp(dt*(_last_event[i][j] - (t))/(' + self.c_code(real_tau) + '));' return code
def standardize_ODE(self, expression): """ Transform any 1rst order ODE into the standardized form: tau * dV/dt + V = S Non-linear functions of V are left in the steady-state argument. Returns: * tau : the time constant associated to the standardized equation. * stepsize: a simplified version of dt/tau. * steadystate: the right term of the equation after standardization """ # Replace the gradient with a temporary variable expression = expression.replace('d' + self.name +'/dt', '_gradvar_') # TODO: robust to spaces # Add the gradient sympbol grad_var = Symbol('_gradvar_') # Parse the string analysed = self.parse_expression(expression, local_dict = self.local_dict ) self.analysed = analysed # Collect factor on the gradient and main variable A*dV/dt + B*V = C expanded = analysed.expand( modulus=None, power_base=False, power_exp=False, mul=True, log=False, multinomial=False) # Make sure the expansion went well collected_var = collect(expanded, self.local_dict[self.name], evaluate=False, exact=False) if self.method == 'exponential': if not self.local_dict[self.name] in collected_var.keys() or len(collected_var)>2: _print(self.expression) _error('The exponential method is reserved for linear first-order ODEs of the type tau*d'+ self.name+'/dt + '+self.name+' = f(t). Use the explicit method instead.') exit(0) factor_var = collected_var[self.local_dict[self.name]] collected_gradient = collect(expand(analysed, grad_var), grad_var, evaluate=False, exact=True) if grad_var in collected_gradient.keys(): factor_gradient = collected_gradient[grad_var] else: factor_gradient = S(1.0) # Real time constant when using the form tau*dV/dt + V = A real_tau = factor_gradient / factor_var # Normalized equation tau*dV/dt + V = A normalized = analysed / factor_var # Steady state A steadystate = together(real_tau * grad_var + self.local_dict[self.name] - normalized) # Stepsize stepsize = together(self.local_dict['dt']/real_tau) return real_tau, stepsize, steadystate
def standardize_ODE(self, expression): """ Transform any 1rst order ODE into the standardized form: tau * dV/dt + V = S Non-linear functions of V are left in the steady-state argument. Returns: * tau : the time constant associated to the standardized equation. * stepsize: a simplified version of dt/tau. * steadystate: the right term of the equation after standardization """ # Replace the gradient with a temporary variable expression = expression.replace('d' + self.name + '/dt', '_gradvar_') # TODO: robust to spaces # Add the gradient sympbol grad_var = Symbol('_gradvar_') # Parse the string analysed = self.parse_expression(expression, local_dict=self.local_dict) self.analysed = analysed # Collect factor on the gradient and main variable A*dV/dt + B*V = C expanded = analysed.expand(modulus=None, power_base=False, power_exp=False, mul=True, log=False, multinomial=False) # Make sure the expansion went well collected_var = collect(expanded, self.local_dict[self.name], evaluate=False, exact=False) if self.method == 'exponential': if not self.local_dict[self.name] in collected_var.keys() or len( collected_var) > 2: _print(self.expression) _error( 'The exponential method is reserved for linear first-order ODEs of the type tau*d' + self.name + '/dt + ' + self.name + ' = f(t). Use the explicit method instead.') exit(0) factor_var = collected_var[self.local_dict[self.name]] collected_gradient = collect(expand(analysed, grad_var), grad_var, evaluate=False, exact=True) if grad_var in collected_gradient.keys(): factor_gradient = collected_gradient[grad_var] else: factor_gradient = S(1.0) # Real time constant when using the form tau*dV/dt + V = A real_tau = factor_gradient / factor_var # Normalized equation tau*dV/dt + V = A normalized = analysed / factor_var # Steady state A steadystate = together(real_tau * grad_var + self.local_dict[self.name] - normalized) # Stepsize stepsize = together(self.local_dict['dt'] / real_tau) return real_tau, stepsize, steadystate
def __init__(self, parameters="", equations="", psp=None, operation='sum', pre_spike=None, post_spike=None, functions=None, pruning=None, creating=None, name=None, description=None, extra_values={}): """ *Parameters*: * **parameters**: parameters of the neuron and their initial value. * **equations**: equations defining the temporal evolution of variables. * **psp**: influence of a single synapse on the post-synaptic neuron (default for rate-coded: w*pre.r). * **operation**: operation (sum, max, min, mean) performed by the post-synaptic neuron on the individual psp (rate-coded only, default=sum). * **pre_spike**: updating of variables when a pre-synaptic spike is received (spiking only). * **post_spike**: updating of variables when a post-synaptic spike is emitted (spiking only). * **functions**: additional functions used in the equations. * **name**: name of the synapse type (used for reporting only). * **description**: short description of the synapse type (used for reporting). """ # Store the parameters and equations self.parameters = parameters self.equations = equations self.functions = functions self.pre_spike = pre_spike self.post_spike = post_spike self.psp = psp self.operation = operation self.extra_values = extra_values self.pruning = pruning self.creating = creating # Type of the synapse TODO: smarter self.type = 'spike' if pre_spike else 'rate' # Check the operation if self.type == 'spike' and self.operation != 'sum': _error( 'Spiking synapses can only perform a sum of presynaptic potentials.' ) exit(0) if not self.operation in ['sum', 'min', 'max', 'mean']: _error( 'The only operations permitted are: sum (default), min, max, mean.' ) exit(0) # Description self.description = None # Reporting if not hasattr(self, '_instantiated'): # User-defined _objects['synapses'].append(self) elif len(self._instantiated) == 0: # First instantiated of the class _objects['synapses'].append(self) if name: self.name = name else: self.name = self._default_names[self.type] if description: self.short_description = description else: if self.type == 'spike': self.short_description = "Instantaneous increase of the post-synaptic conductance after a spike is received." else: self.short_description = "Weighted sum of firing rates."
def analyse_synapse(synapse): """ Performs the analysis for a single synapse.""" concurrent_odes = [] # Store basic information description = { 'object': 'synapse', 'type': synapse.type, 'raw_parameters': synapse.parameters, 'raw_equations': synapse.equations, 'raw_functions': synapse.functions } if synapse.psp: description['raw_psp'] = synapse.psp elif synapse.type == 'rate': description['raw_psp'] = "w*pre.r" if synapse.type == 'spike': # Additionally store pre_spike and post_spike description['raw_pre_spike'] = synapse.pre_spike description['raw_post_spike'] = synapse.post_spike # Extract parameters and variables names parameters = extract_parameters(synapse.parameters, synapse.extra_values) variables = extract_variables(synapse.equations) # Extract functions functions = extract_functions(synapse.functions, False) # Check the presence of w description['plasticity'] = False for var in parameters + variables: if var['name'] == 'w': break else: parameters.append( #TODO: is this exception really needed? Maybe we could find # a better solution instead of the hard-coded 'w' ... [hdin: 26.05.2015] {'name': 'w', 'bounds': {}, 'ctype': 'double', 'init': 0.0, 'flags': [], 'eq': 'w=0.0', 'locality': 'local'} ) # Find out a plasticity rule for var in variables: if var['name'] == 'w': description['plasticity'] = True break # Build lists of all attributes (param+var), which are local or global attributes, local_var, global_var = get_attributes(parameters, variables) # Test if attributes are declared only once if len(attributes) != len(list(set(attributes))): _error('Attributes must be declared only once.', attributes) # Add this info to the description description['parameters'] = parameters description['variables'] = variables description['functions'] = functions description['attributes'] = attributes description['local'] = local_var description['global'] = global_var description['global_operations'] = [] description['pre_global_operations'] = [] description['post_global_operations'] = [] # Extract RandomDistribution objects description['random_distributions'] = extract_randomdist(description) # Extract event-driven info if description['type'] == 'spike': # pre_spike event description['pre_spike'] = extract_pre_spike_variable(description) for var in description['pre_spike']: if var['name'] in ['g_target']: # Already dealt with continue for avar in description['variables']: if var['name'] == avar['name']: break else: # not defined already description['variables'].append( {'name': var['name'], 'bounds': var['bounds'], 'ctype': var['ctype'], 'init': var['init'], 'locality': var['locality'], 'flags': [], 'transformed_eq': '', 'eq': '', 'cpp': '', 'switch': '', 'untouched': '', 'method':'explicit'} ) description['local'].append(var['name']) description['attributes'].append(var['name']) # post_spike event description['post_spike'] = extract_post_spike_variable(description) for var in description['post_spike']: if var['name'] in ['g_target', 'w']: # Already dealt with continue for avar in description['variables']: if var['name'] == avar['name']: break else: # not defined already description['variables'].append( {'name': var['name'], 'bounds': var['bounds'], 'ctype': var['ctype'], 'init': var['init'], 'locality': var['locality'], 'flags': [], 'transformed_eq': '', 'eq': '', 'cpp': '', 'switch': '', 'untouched': '', 'method':'explicit'} ) description['local'].append(var['name']) description['attributes'].append(var['name']) # Variables names for the parser which should be left untouched untouched = {} description['dependencies'] = {'pre': [], 'post': []} # Iterate over all variables for variable in description['variables']: eq = variable['transformed_eq'] # Event-driven variables if eq.strip() == '': continue # Extract global operations eq, untouched_globs, global_ops = extract_globalops_synapse(variable['name'], eq, description) description['pre_global_operations'] += global_ops['pre'] description['post_global_operations'] += global_ops['post'] # Extract pre- and post_synaptic variables eq, untouched_var, dependencies = extract_prepost(variable['name'], eq, description) description['dependencies']['pre'] += dependencies['pre'] description['dependencies']['post'] += dependencies['post'] # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Add the untouched variables to the global list for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val for name, val in untouched_var.items(): if not name in untouched.keys(): untouched[name] = val # Save the tranformed equation variable['transformed_eq'] = eq # Find the numerical method if any method = find_method(variable) # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation(variable['name'], variable['bounds']['min'], description, type = 'return', untouched = untouched.keys()) variable['bounds']['min'] = translator.parse().replace(';', '') if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation(variable['name'], variable['bounds']['max'], description, type = 'return', untouched = untouched.keys()) variable['bounds']['max'] = translator.parse().replace(';', '') # Analyse the equation if condition == []: # Call Equation translator = Equation(variable['name'], eq, description, method = method, untouched = untouched.keys()) code = translator.parse() dependencies = translator.dependencies() else: # An if-then-else statement code = translate_ITE(variable['name'], eq, condition, description, untouched) dependencies = [] if isinstance(code, str): cpp_eq = code switch = None else: # ODE cpp_eq = code[0] switch = code[1] # Replace untouched variables with their original name # print cpp_eq for prev, new in untouched.items(): cpp_eq = cpp_eq.replace(prev, new) # print cpp_eq # Replace local functions for f in description['functions']: cpp_eq = re.sub(r'([^\w]*)'+f['name']+'\(', r'\1'+ f['name'] + '(', ' ' + cpp_eq).strip() # Store the result variable['cpp'] = cpp_eq # the C++ equation variable['switch'] = switch # switch value id ODE variable['untouched'] = untouched # may be needed later variable['method'] = method # may be needed later variable['dependencies'] = dependencies # may be needed later # If the method is implicit or midpoint, the equations must be solved concurrently (depend on v[t+1]) if method in ['implicit', 'midpoint']: concurrent_odes.append(variable) # After all variables are processed, do it again if they are concurrent if len(concurrent_odes) > 1 : solver = CoupledEquations(description, concurrent_odes) new_eqs = solver.process_variables() for idx, variable in enumerate(description['variables']): for new_eq in new_eqs: if variable['name'] == new_eq['name']: description['variables'][idx] = new_eq # Translate the psp code if any if 'raw_psp' in description.keys(): psp = {'eq' : description['raw_psp'].strip() } # Extract global operations eq, untouched_globs, global_ops = extract_globalops_synapse('psp', " " + psp['eq'] + " ", description) description['pre_global_operations'] += global_ops['pre'] description['post_global_operations'] += global_ops['post'] # Replace pre- and post_synaptic variables eq, untouched, dependencies = extract_prepost('psp', eq, description) description['dependencies']['pre'] += dependencies['pre'] description['dependencies']['post'] += dependencies['post'] for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val # Extract if-then-else statements eq, condition = extract_ite('psp', eq, description, split=False) # Analyse the equation if condition == []: translator = Equation('psp', eq, description, method = 'explicit', untouched = untouched.keys(), type='return') code = translator.parse() else: code = translate_ITE('psp', eq, condition, description, untouched, split=False) # Replace untouched variables with their original name for prev, new in untouched.items(): code = code.replace(prev, new) # Store the result psp['cpp'] = code description['psp'] = psp # Process event-driven info if description['type'] == 'spike': for variable in description['pre_spike'] + description['post_spike']: # Find plasticity if variable['name'] == 'w': description['plasticity'] = True # Retrieve the equation eq = variable['eq'] # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Analyse the equation if condition == []: translator = Equation(variable['name'], eq, description, method = 'explicit', untouched = {}) code = translator.parse() else: code = translate_ITE(variable['name'], eq, condition, description, {}, split=True) # Store the result variable['cpp'] = code # the C++ equation # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation( variable['name'], variable['bounds']['min'], description, type = 'return', untouched = untouched ) variable['bounds']['min'] = translator.parse().replace(';', '') if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation( variable['name'], variable['bounds']['max'], description, type = 'return', untouched = untouched) variable['bounds']['max'] = translator.parse().replace(';', '') # Structural plasticity if synapse.pruning: description['pruning'] = extract_structural_plasticity(synapse.pruning, description) if synapse.creating: description['creating'] = extract_structural_plasticity(synapse.creating, description) return description
def analyse_synapse(synapse): """ Performs the analysis for a single synapse.""" concurrent_odes = [] # Store basic information description = { 'object': 'synapse', 'type': synapse.type, 'raw_parameters': synapse.parameters, 'raw_equations': synapse.equations, 'raw_functions': synapse.functions } if synapse.psp: description['raw_psp'] = synapse.psp elif synapse.type == 'rate': description['raw_psp'] = "w*pre.r" if synapse.type == 'spike': # Additionally store pre_spike and post_spike description['raw_pre_spike'] = synapse.pre_spike description['raw_post_spike'] = synapse.post_spike # Extract parameters and variables names parameters = extract_parameters(synapse.parameters, synapse.extra_values) variables = extract_variables(synapse.equations) # Extract functions functions = extract_functions(synapse.functions, False) # Check the presence of w description['plasticity'] = False for var in parameters + variables: if var['name'] == 'w': break else: parameters.append( #TODO: is this exception really needed? Maybe we could find # a better solution instead of the hard-coded 'w' ... [hdin: 26.05.2015] { 'name': 'w', 'bounds': {}, 'ctype': 'double', 'init': 0.0, 'flags': [], 'eq': 'w=0.0', 'locality': 'local' }) # Find out a plasticity rule for var in variables: if var['name'] == 'w': description['plasticity'] = True break # Build lists of all attributes (param+var), which are local or global attributes, local_var, global_var = get_attributes(parameters, variables) # Test if attributes are declared only once if len(attributes) != len(list(set(attributes))): _error('Attributes must be declared only once.', attributes) exit(0) # Add this info to the description description['parameters'] = parameters description['variables'] = variables description['functions'] = functions description['attributes'] = attributes description['local'] = local_var description['global'] = global_var description['global_operations'] = [] description['pre_global_operations'] = [] description['post_global_operations'] = [] # Extract RandomDistribution objects description['random_distributions'] = extract_randomdist(description) # Extract event-driven info if description['type'] == 'spike': # pre_spike event description['pre_spike'] = extract_pre_spike_variable(description) for var in description['pre_spike']: if var['name'] in ['g_target']: # Already dealt with continue for avar in description['variables']: if var['name'] == avar['name']: break else: # not defined already description['variables'].append({ 'name': var['name'], 'bounds': var['bounds'], 'ctype': var['ctype'], 'init': var['init'], 'locality': var['locality'], 'flags': [], 'transformed_eq': '', 'eq': '', 'cpp': '', 'switch': '', 'untouched': '', 'method': 'explicit' }) description['local'].append(var['name']) description['attributes'].append(var['name']) # post_spike event description['post_spike'] = extract_post_spike_variable(description) for var in description['post_spike']: if var['name'] in ['g_target', 'w']: # Already dealt with continue for avar in description['variables']: if var['name'] == avar['name']: break else: # not defined already description['variables'].append({ 'name': var['name'], 'bounds': var['bounds'], 'ctype': var['ctype'], 'init': var['init'], 'locality': var['locality'], 'flags': [], 'transformed_eq': '', 'eq': '', 'cpp': '', 'switch': '', 'untouched': '', 'method': 'explicit' }) description['local'].append(var['name']) description['attributes'].append(var['name']) # Variables names for the parser which should be left untouched untouched = {} description['dependencies'] = {'pre': [], 'post': []} # Iterate over all variables for variable in description['variables']: eq = variable['transformed_eq'] # Event-driven variables if eq.strip() == '': continue # Extract global operations eq, untouched_globs, global_ops = extract_globalops_synapse( variable['name'], eq, description) description['pre_global_operations'] += global_ops['pre'] description['post_global_operations'] += global_ops['post'] # Extract pre- and post_synaptic variables eq, untouched_var, dependencies = extract_prepost( variable['name'], eq, description) description['dependencies']['pre'] += dependencies['pre'] description['dependencies']['post'] += dependencies['post'] # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Add the untouched variables to the global list for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val for name, val in untouched_var.items(): if not name in untouched.keys(): untouched[name] = val # Save the tranformed equation variable['transformed_eq'] = eq # Find the numerical method if any method = find_method(variable) # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation(variable['name'], variable['bounds']['min'], description, type='return', untouched=untouched.keys()) variable['bounds']['min'] = translator.parse().replace(';', '') if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation(variable['name'], variable['bounds']['max'], description, type='return', untouched=untouched.keys()) variable['bounds']['max'] = translator.parse().replace(';', '') # Analyse the equation if condition == []: # Call Equation translator = Equation(variable['name'], eq, description, method=method, untouched=untouched.keys()) code = translator.parse() dependencies = translator.dependencies() else: # An if-then-else statement code = translate_ITE(variable['name'], eq, condition, description, untouched) dependencies = [] if isinstance(code, str): cpp_eq = code switch = None else: # ODE cpp_eq = code[0] switch = code[1] # Replace untouched variables with their original name for prev, new in untouched.items(): cpp_eq = cpp_eq.replace(prev, new) # Replace local functions for f in description['functions']: cpp_eq = re.sub(r'([^\w]*)' + f['name'] + '\(', r'\1' + f['name'] + '(', ' ' + cpp_eq).strip() # Store the result variable['cpp'] = cpp_eq # the C++ equation variable['switch'] = switch # switch value id ODE variable['untouched'] = untouched # may be needed later variable['method'] = method # may be needed later variable['dependencies'] = dependencies # may be needed later # If the method is implicit or midpoint, the equations must be solved concurrently (depend on v[t+1]) if method in ['implicit', 'midpoint']: concurrent_odes.append(variable) # After all variables are processed, do it again if they are concurrent if len(concurrent_odes) > 1: solver = CoupledEquations(description, concurrent_odes) new_eqs = solver.process_variables() for idx, variable in enumerate(description['variables']): for new_eq in new_eqs: if variable['name'] == new_eq['name']: description['variables'][idx] = new_eq # Translate the psp code if any if 'raw_psp' in description.keys(): psp = {'eq': description['raw_psp'].strip()} # Extract global operations eq, untouched_globs, global_ops = extract_globalops_synapse( 'psp', " " + psp['eq'] + " ", description) description['pre_global_operations'] += global_ops['pre'] description['post_global_operations'] += global_ops['post'] # Replace pre- and post_synaptic variables eq, untouched, dependencies = extract_prepost('psp', eq, description) description['dependencies']['pre'] += dependencies['pre'] description['dependencies']['post'] += dependencies['post'] for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val # Extract if-then-else statements eq, condition = extract_ite('psp', eq, description, split=False) # Analyse the equation if condition == []: translator = Equation('psp', eq, description, method='explicit', untouched=untouched.keys(), type='return') code = translator.parse() else: code = translate_ITE('psp', eq, condition, description, untouched, split=False) # Replace untouched variables with their original name for prev, new in untouched.items(): code = code.replace(prev, new) # Store the result psp['cpp'] = code description['psp'] = psp # Process event-driven info if description['type'] == 'spike': for variable in description['pre_spike'] + description['post_spike']: # Find plasticity if variable['name'] == 'w': description['plasticity'] = True # Retrieve the equation eq = variable['eq'] # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Analyse the equation if condition == []: translator = Equation(variable['name'], eq, description, method='explicit', untouched={}) code = translator.parse() else: code = translate_ITE(variable['name'], eq, condition, description, {}, split=True) # Store the result variable['cpp'] = code # the C++ equation # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation(variable['name'], variable['bounds']['min'], description, type='return', untouched=untouched) variable['bounds']['min'] = translator.parse().replace( ';', '') if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation(variable['name'], variable['bounds']['max'], description, type='return', untouched=untouched) variable['bounds']['max'] = translator.parse().replace( ';', '') # Structural plasticity if synapse.pruning: description['pruning'] = extract_structural_plasticity( synapse.pruning, description) if synapse.creating: description['creating'] = extract_structural_plasticity( synapse.creating, description) return description
def __add__(self, synapse): _error('adding synapse models is not implemented yet.')
def extract_functions(description, local_global=False): """ Extracts all functions from a multiline description.""" if not description: return [] # Split the multilines into individual lines function_list = process_equations(description) # Process each function functions = [] for f in function_list: eq = f['eq'] var_name, content = eq.split('=', 1) # Extract the name of the function func_name = var_name.split('(', 1)[0].strip() # Extract the arguments arguments = (var_name.split('(', 1)[1].split(')')[0]).split(',') arguments = [arg.strip() for arg in arguments] # Extract their types types = f['constraint'] if types == '': return_type = 'double' arg_types = ['double' for a in arguments] else: types = types.split(',') return_type = types[0].strip() arg_types = [arg.strip() for arg in types[1:]] if not len(arg_types) == len(arguments): _error( 'You must specify exactly the types of return value and arguments in ' + eq) exit(0) arg_line = "" for i in range(len(arguments)): arg_line += arg_types[i] + " " + arguments[i] if not i == len(arguments) - 1: arg_line += ', ' # Process the content eq2, condition = extract_ite('', content, None, split=False) if condition == []: parser = FunctionParser(content, arguments) parsed_content = parser.parse() else: parser = FunctionParser(content, arguments) parsed_content = parser.process_ITE(condition) # Create the one-liner fdict = { 'name': func_name, 'args': arguments, 'content': content, 'return_type': return_type, 'arg_types': arg_types, 'parsed_content': parsed_content, 'arg_line': arg_line } if not local_global: # local to a class oneliner = """%(return_type)s %(name)s (%(arg_line)s) {return %(parsed_content)s ;}; """ % fdict else: # global oneliner = """inline %(return_type)s %(name)s (%(arg_line)s) {return %(parsed_content)s ;}; """ % fdict fdict['cpp'] = oneliner functions.append(fdict) return functions
def extract_structural_plasticity(statement, description): # Extract flags try: eq, constraint = statement.rsplit(':', 1) bounds, flags = extract_flags(constraint) except: eq = statement.strip() bounds = {} flags = [] # Extract RD rd = None for dist in available_distributions: matches = re.findall('(?P<pre>[^\w.])' + dist + '\(([^()]+)\)', eq) for l, v in matches: # Check the arguments arguments = v.split(',') # Check the number of provided arguments if len(arguments) < distributions_arguments[dist]: _error(eq) _error('The distribution ' + dist + ' requires ' + str(distributions_arguments[dist]) + 'parameters') elif len(arguments) > distributions_arguments[dist]: _error(eq) _error('Too many parameters provided to the distribution ' + dist) # Process the arguments processed_arguments = "" for idx in range(len(arguments)): try: arg = float(arguments[idx]) except: # A global parameter _error(eq) _error( 'Random distributions for creating/pruning synapses must use foxed values.' ) exit(0) processed_arguments += str(arg) if idx != len(arguments) - 1: # not the last one processed_arguments += ', ' definition = distributions_equivalents[ dist] + '(' + processed_arguments + ')' # Store its definition if rd: _error(eq) _error('Only one random distribution per equation is allowed.') exit(0) rd = { 'name': 'rand_' + str(0), 'origin': dist + '(' + v + ')', 'dist': dist, 'definition': definition, 'args': processed_arguments, 'template': distributions_equivalents[dist] } if rd: eq = eq.replace(rd['origin'], 'rd(rng)') # Extract pre/post dependencies eq, untouched, dependencies = extract_prepost('test', eq, description) # Parse code translator = Equation('test', eq, description, method='cond', untouched={}) code = translator.parse() # Replace untouched variables with their original name for prev, new in untouched.items(): code = code.replace(prev, new) # Add new dependencies for dep in dependencies['pre']: description['dependencies']['pre'].append(dep) for dep in dependencies['post']: description['dependencies']['post'].append(dep) return {'eq': eq, 'cpp': code, 'bounds': bounds, 'flags': flags, 'rd': rd}
def analyse_synapse(synapse): """ Parses the structure and generates code snippets for the synapse type. It returns a ``description`` dictionary with the following fields: * 'object': 'synapse' by default, to distinguish it from 'neuron' * 'type': either 'rate' or 'spiking' * 'raw_parameters': provided field * 'raw_equations': provided field * 'raw_functions': provided field * 'raw_psp': provided field * 'raw_pre_spike': provided field * 'raw_post_spike': provided field * 'parameters': list of parameters defined for the synapse type * 'variables': list of variables defined for the synapse type * 'functions': list of functions defined for the synapse type * 'attributes': list of names of all parameters and variables * 'local': list of names of parameters and variables which are local to each synapse * 'semiglobal': list of names of parameters and variables which are local to each postsynaptic neuron * 'global': list of names of parameters and variables which are global to the projection * 'random_distributions': list of random number generators used in the neuron equations * 'global_operations': list of global operations (min/max/mean...) used in the equations * 'pre_global_operations': list of global operations (min/max/mean...) on the pre-synaptic population * 'post_global_operations': list of global operations (min/max/mean...) on the post-synaptic population * 'pre_spike': list of variables updated after a pre-spike event * 'post_spike': list of variables updated after a post-spike event * 'dependencies': dictionary ('pre', 'post') of lists of pre (resp. post) variables accessed by the synapse (used for delaying variables) * 'psp': dictionary ('eq' and 'psp') for the psp code to be summed * 'pruning' and 'creating': statements for structural plasticity Each parameter is a dictionary with the following elements: * 'bounds': unused * 'ctype': 'type of the parameter: 'float', 'double', 'int' or 'bool' * 'eq': original equation in text format * 'flags': list of flags provided after the : * 'init': initial value * 'locality': 'local', 'semiglobal' or 'global' * 'name': name of the parameter Each variable is a dictionary with the following elements: * 'bounds': dictionary of bounds ('init', 'min', 'max') provided after the : * 'cpp': C++ code snippet updating the variable * 'ctype': type of the variable: 'float', 'double', 'int' or 'bool' * 'dependencies': list of variable and parameter names on which the equation depends * 'eq': original equation in text format * 'flags': list of flags provided after the : * 'init': initial value * 'locality': 'local', 'semiglobal' or 'global' * 'method': numericalmethod for ODEs * 'name': name of the variable * 'pre_loop': ODEs have a pre_loop term for precomputing dt/tau * 'switch': ODEs have a switch term * 'transformed_eq': same as eq, except special terms (sums, rds) are replaced with a temporary name * 'untouched': dictionary of special terms, with their new name as keys and replacement values as values. """ # Store basic information description = { 'object': 'synapse', 'type': synapse.type, 'raw_parameters': synapse.parameters, 'raw_equations': synapse.equations, 'raw_functions': synapse.functions } # Psps is what is actually summed over the incoming weights if synapse.psp: description['raw_psp'] = synapse.psp elif synapse.type == 'rate': description['raw_psp'] = "w*pre.r" # Spiking synapses additionally store pre_spike and post_spike if synapse.type == 'spike': description['raw_pre_spike'] = synapse.pre_spike description['raw_post_spike'] = synapse.post_spike # Extract parameters and variables names parameters = extract_parameters(synapse.parameters, synapse.extra_values) variables = extract_variables(synapse.equations) # Extract functions functions = extract_functions(synapse.functions, False) # Check the presence of w description['plasticity'] = False for var in parameters + variables: if var['name'] == 'w': break else: parameters.append({ 'name': 'w', 'bounds': {}, 'ctype': config['precision'], 'init': 0.0, 'flags': [], 'eq': 'w=0.0', 'locality': 'local' }) # Find out a plasticity rule for var in variables: if var['name'] == 'w': description['plasticity'] = True break # Build lists of all attributes (param+var), which are local or global attributes, local_var, global_var, semiglobal_var = get_attributes( parameters, variables, neuron=False) # Test if attributes are declared only once if len(attributes) != len(list(set(attributes))): _error('Attributes must be declared only once.', attributes) # Add this info to the description description['parameters'] = parameters description['variables'] = variables description['functions'] = functions description['attributes'] = attributes description['local'] = local_var description['semiglobal'] = semiglobal_var description['global'] = global_var description['global_operations'] = [] # Lists of global operations needed at the pre and post populations description['pre_global_operations'] = [] description['post_global_operations'] = [] # Extract RandomDistribution objects description['random_distributions'] = extract_randomdist(description) # Extract event-driven info if description['type'] == 'spike': # pre_spike event description['pre_spike'] = extract_pre_spike_variable(description) for var in description['pre_spike']: if var['name'] in ['g_target']: # Already dealt with continue for avar in description['variables']: if var['name'] == avar['name']: break else: # not defined already description['variables'].append({ 'name': var['name'], 'bounds': var['bounds'], 'ctype': var['ctype'], 'init': var['init'], 'locality': var['locality'], 'flags': [], 'transformed_eq': '', 'eq': '', 'cpp': '', 'switch': '', 're_loop': '', 'untouched': '', 'method': 'explicit' }) description['local'].append(var['name']) description['attributes'].append(var['name']) # post_spike event description['post_spike'] = extract_post_spike_variable(description) for var in description['post_spike']: if var['name'] in ['g_target', 'w']: # Already dealt with continue for avar in description['variables']: if var['name'] == avar['name']: break else: # not defined already description['variables'].append({ 'name': var['name'], 'bounds': var['bounds'], 'ctype': var['ctype'], 'init': var['init'], 'locality': var['locality'], 'flags': [], 'transformed_eq': '', 'eq': '', 'cpp': '', 'switch': '', 'untouched': '', 'method': 'explicit' }) description['local'].append(var['name']) description['attributes'].append(var['name']) # Variables names for the parser which should be left untouched untouched = {} description['dependencies'] = {'pre': [], 'post': []} # The ODEs may be interdependent (implicit, midpoint), so they need to be passed explicitely to CoupledEquations concurrent_odes = [] # Iterate over all variables for variable in description['variables']: # Equation eq = variable['transformed_eq'] if eq.strip() == '': continue # Dependencies must be gathered dependencies = [] # Extract global operations eq, untouched_globs, global_ops = extract_globalops_synapse( variable['name'], eq, description) description['pre_global_operations'] += global_ops['pre'] description['post_global_operations'] += global_ops['post'] # Remove doubled entries description['pre_global_operations'] = [ i for n, i in enumerate(description['pre_global_operations']) if i not in description['pre_global_operations'][n + 1:] ] description['post_global_operations'] = [ i for n, i in enumerate(description['post_global_operations']) if i not in description['post_global_operations'][n + 1:] ] # Extract pre- and post_synaptic variables eq, untouched_var, prepost_dependencies = extract_prepost( variable['name'], eq, description) # Store the pre-post dependencies at the synapse level description['dependencies']['pre'] += prepost_dependencies['pre'] description['dependencies']['post'] += prepost_dependencies['post'] # and also on the variable for checking variable['prepost_dependencies'] = prepost_dependencies # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Add the untouched variables to the global list for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val for name, val in untouched_var.items(): if not name in untouched.keys(): untouched[name] = val # Save the tranformed equation variable['transformed_eq'] = eq # Find the numerical method if any method = find_method(variable) # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation(variable['name'], variable['bounds']['min'], description, type='return', untouched=untouched.keys()) variable['bounds']['min'] = translator.parse().replace(';', '') dependencies += translator.dependencies() if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation(variable['name'], variable['bounds']['max'], description, type='return', untouched=untouched.keys()) variable['bounds']['max'] = translator.parse().replace(';', '') dependencies += translator.dependencies() # Analyse the equation if condition == []: # Call Equation translator = Equation(variable['name'], eq, description, method=method, untouched=untouched.keys()) code = translator.parse() dependencies += translator.dependencies() else: # An if-then-else statement code, deps = translate_ITE(variable['name'], eq, condition, description, untouched) dependencies += deps if isinstance(code, str): pre_loop = {} cpp_eq = code switch = None else: # ODE pre_loop = code[0] cpp_eq = code[1] switch = code[2] # Replace untouched variables with their original name for prev, new in untouched.items(): cpp_eq = cpp_eq.replace(prev, new) # Replace local functions for f in description['functions']: cpp_eq = re.sub(r'([^\w]*)' + f['name'] + '\(', r'\1' + f['name'] + '(', ' ' + cpp_eq).strip() # Store the result variable[ 'pre_loop'] = pre_loop # Things to be declared before the for loop (eg. dt) variable['cpp'] = cpp_eq # the C++ equation variable['switch'] = switch # switch value id ODE variable['untouched'] = untouched # may be needed later variable['method'] = method # may be needed later variable['dependencies'] = dependencies # If the method is implicit or midpoint, the equations must be solved concurrently (depend on v[t+1]) if method in ['implicit', 'midpoint'] and switch is not None: concurrent_odes.append(variable) # After all variables are processed, do it again if they are concurrent if len(concurrent_odes) > 1: solver = CoupledEquations(description, concurrent_odes) new_eqs = solver.parse() for idx, variable in enumerate(description['variables']): for new_eq in new_eqs: if variable['name'] == new_eq['name']: description['variables'][idx] = new_eq # Translate the psp code if any if 'raw_psp' in description.keys(): psp = {'eq': description['raw_psp'].strip()} # Extract global operations eq, untouched_globs, global_ops = extract_globalops_synapse( 'psp', " " + psp['eq'] + " ", description) description['pre_global_operations'] += global_ops['pre'] description['post_global_operations'] += global_ops['post'] # Replace pre- and post_synaptic variables eq, untouched, prepost_dependencies = extract_prepost( 'psp', eq, description) description['dependencies']['pre'] += prepost_dependencies['pre'] description['dependencies']['post'] += prepost_dependencies['post'] for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val # Extract if-then-else statements eq, condition = extract_ite('psp', eq, description, split=False) # Analyse the equation if condition == []: translator = Equation('psp', eq, description, method='explicit', untouched=untouched.keys(), type='return') code = translator.parse() deps = translator.dependencies() else: code, deps = translate_ITE('psp', eq, condition, description, untouched) # Replace untouched variables with their original name for prev, new in untouched.items(): code = code.replace(prev, new) # Store the result psp['cpp'] = code psp['dependencies'] = deps description['psp'] = psp # Process event-driven info if description['type'] == 'spike': for variable in description['pre_spike'] + description['post_spike']: # Find plasticity if variable['name'] == 'w': description['plasticity'] = True # Retrieve the equation eq = variable['eq'] # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Extract pre- and post_synaptic variables eq, untouched, prepost_dependencies = extract_prepost( variable['name'], eq, description) # Update dependencies description['dependencies']['pre'] += prepost_dependencies['pre'] description['dependencies']['post'] += prepost_dependencies['post'] # and also on the variable for checking variable['prepost_dependencies'] = prepost_dependencies # Analyse the equation dependencies = [] if condition == []: translator = Equation(variable['name'], eq, description, method='explicit', untouched=untouched) code = translator.parse() dependencies += translator.dependencies() else: code, deps = translate_ITE(variable['name'], eq, condition, description, untouched) dependencies += deps if isinstance(code, list): # an ode in a pre/post statement Global._print(eq) if variable in description['pre_spike']: Global._error( 'It is forbidden to use ODEs in a pre_spike term.') elif variable in description['posz_spike']: Global._error( 'It is forbidden to use ODEs in a post_spike term.') else: Global._error('It is forbidden to use ODEs here.') # Replace untouched variables with their original name for prev, new in untouched.items(): code = code.replace(prev, new) # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation(variable['name'], variable['bounds']['min'], description, type='return', untouched=untouched) variable['bounds']['min'] = translator.parse().replace( ';', '') dependencies += translator.dependencies() if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation(variable['name'], variable['bounds']['max'], description, type='return', untouched=untouched) variable['bounds']['max'] = translator.parse().replace( ';', '') dependencies += translator.dependencies() # Store the result variable['cpp'] = code # the C++ equation variable['dependencies'] = dependencies # Structural plasticity if synapse.pruning: description['pruning'] = extract_structural_plasticity( synapse.pruning, description) if synapse.creating: description['creating'] = extract_structural_plasticity( synapse.creating, description) return description
def process_equations(equations): """ Takes a multi-string describing equations and returns a list of dictionaries, where: * 'name' is the name of the variable * 'eq' is the equation * 'constraints' is all the constraints given after the last :. _extract_flags() should be called on it. Warning: one equation can now be on multiple lines, without needing the ... newline symbol. TODO: should this be used for other arguments as equations? pre_event and so on """ def is_constraint(eq): " Internal method to determine if a string contains reserved keywords." eq = ',' + eq.replace(' ', '') + ',' for key in authorized_keywords: pattern = '([,]+)' + key + '([=,]+)' if re.match(pattern, eq): return True return False # All equations will be stored there, in the order of their definition variables = [] try: equations = equations.replace(';', '\n').split('\n') except: # equations is empty return variables # Iterate over all lines for line in equations: # Skip empty lines definition = line.strip() if definition == '': continue # Remove comments com = definition.split('#') if len(com) > 1: definition = com[0] if definition.strip() == '': continue # Process the line try: equation, constraint = definition.rsplit(':', 1) except ValueError: # There is no :, only equation is concerned equation = definition constraint = '' else: # there is a : # Check if the constraint contains the reserved keywords has_constraint = is_constraint(constraint) # If the right part of : is a constraint, just store it # Otherwise, it is an if-then-else statement if has_constraint: equation = equation.strip() constraint = constraint.strip() else: equation = definition.strip() # there are no constraints constraint = '' # Split the equation around operators = += -= *= /=, but not == split_operators = re.findall('([\s\w\+\-\*\/\)]+)=([^=])', equation) if len(split_operators) == 1: # definition of a new variable # Retrieve the name eq = split_operators[0][0] if eq.strip() == "": _print(equation) _error('The equation can not be analysed, check the syntax.') name = extract_name(eq, left=True) if name in ['_undefined', '']: _error('No variable name can be found in ' + equation) # Append the result variables.append({'name': name, 'eq': equation.strip(), 'constraint': constraint.strip()}) elif len(split_operators) == 0: # Continuation of the equation on a new line: append the equation to the previous variable variables[-1]['eq'] += ' ' + equation.strip() variables[-1]['constraint'] += constraint else: _error('Only one assignement operator is allowed per equation.') return variables
def solve_implicit(self, expression_list): equations = {} new_vars = {} # Pre-processing to replace the gradient for name, expression in self.expression_list.items(): # transform the expression to suppress = if '=' in expression: expression = expression.replace('=', '- (') expression += ')' # Suppress spaces to extract dvar/dt expression = expression.replace(' ', '') # Transform the gradient into a difference TODO: more robust... expression = expression.replace('d' + name, '_t_gradient_') expression_list[name] = expression # replace the variables by their future value for name, expression in expression_list.items(): for n in self.names: expression = re.sub(r'([^\w]+)' + n + r'([^\w]+)', r'\1_' + n + r'\2', expression) expression = expression.replace('_t_gradient_', '(_' + name + ' - ' + name + ')') expression_list[name] = expression + '-' + name new_var = Symbol('_' + name) self.local_dict['_' + name] = new_var new_vars[new_var] = name for name, expression in expression_list.items(): analysed = parse_expr(expression, local_dict=self.local_dict, transformations=(standard_transformations + (convert_xor, ))) equations[name] = analysed try: solution = solve(equations.values(), new_vars.keys()) except: _error( 'The multiple ODEs can not be solved together using the implicit Euler method.' ) exit(0) for var, sol in solution.items(): # simplify the solution sol = collect(sol, self.local_dict['dt']) # Generate the code cpp_eq = 'double _' + new_vars[var] + ' = ' + ccode(sol) + ';' switch = ccode( self.local_dict[new_vars[var]]) + ' += _' + new_vars[var] + ';' # Replace untouched variables with their original name for prev, new in self.untouched.items(): cpp_eq = re.sub(prev, new, cpp_eq) switch = re.sub(prev, new, switch) # Store the result for variable in self.variables: if variable['name'] == new_vars[var]: variable['cpp'] = cpp_eq variable['switch'] = switch return self.variables
def process_equations(equations): """ Takes a multi-string describing equations and returns a list of dictionaries, where: * 'name' is the name of the variable * 'eq' is the equation * 'constraints' is all the constraints given after the last :. _extract_flags() should be called on it. Warning: one equation can now be on multiple lines, without needing the ... newline symbol. TODO: should this be used for other arguments as equations? pre_event and so on """ def is_constraint(eq): " Internal method to determine if a string contains reserved keywords." eq = ',' + eq.replace(' ', '') + ',' for key in authorized_keywords: pattern = '([,]+)' + key + '([=,]+)' if re.match(pattern, eq): return True return False # All equations will be stored there, in the order of their definition variables = [] try: equations = equations.replace(';', '\n').split('\n') except: # euqations is empty return variables # Iterate over all lines for line in equations: # Skip empty lines definition = line.strip() if definition == '': continue # Remove comments com = definition.split('#') if len(com) > 1: definition = com[0] if definition.strip() == '': continue # Process the line try: equation, constraint = definition.rsplit(':', 1) except ValueError: # There is no :, only equation is concerned equation = line constraint = '' else: # there is a : # Check if the constraint contains the reserved keywords has_constraint = is_constraint(constraint) # If the right part of : is a constraint, just store it # Otherwise, it is an if-then-else statement if has_constraint: equation = equation.strip() constraint = constraint.strip() else: equation = definition.strip() # there are no constraints constraint = '' # Split the equation around operators = += -= *= /=, but not == split_operators = re.findall('([\s\w\+\-\*\/\)]+)=([^=])', equation) if len(split_operators) == 1: # definition of a new variable # Retrieve the name eq = split_operators[0][0] if eq.strip() == "": _error(equation) _print('The equation can not be analysed, check the syntax.') exit(0) name = extract_name(eq, left=True) if name in ['_undefined', '']: _error('No variable name can be found in ' + equation) exit(0) # Append the result variables.append({ 'name': name, 'eq': equation.strip(), 'constraint': constraint.strip() }) elif len(split_operators) == 0: # Continuation of the equation on a new line: append the equation to the previous variable variables[-1]['eq'] += ' ' + equation.strip() variables[-1]['constraint'] += constraint else: _print( 'Error: only one assignement operator is allowed per equation.' ) exit(0) return variables
def extract_ite(name, eq, description, split=True): """ Extracts if-then-else statements and processes them. If-then-else statements must be of the form: .. code-block:: python variable = if condition: ... val1 ... else: ... val2 Conditional statements can be nested, but they should return only one value! """ def transform(code): " Transforms the code into a list of lines." res = [] items = [] for arg in code.split(':'): items.append( arg.strip()) for i in range(len(items)): if items[i].startswith('if '): res.append( items[i].strip() ) elif items[i].endswith('else'): res.append(items[i].split('else')[0].strip() ) res.append('else' ) else: # the last then res.append( items[i].strip() ) return res def parse(lines): " Recursive analysis of if-else statements" result = [] while lines: if lines[0].startswith('if'): block = [lines.pop(0).split('if')[1], parse(lines)] if lines[0].startswith('else'): lines.pop(0) block.append(parse(lines)) result.append(block) elif not lines[0].startswith(('else')): result.append(lines.pop(0)) else: break return result[0] # If no if, not a conditional if not 'if ' in eq: return eq, [] # Process the equation condition = [] # Eventually split around = if split: left, right = eq.split('=', 1) else: left = '' right = eq nb_then = len(re.findall(':', right)) nb_else = len(re.findall('else', right)) # The equation contains a conditional statement if nb_then > 0: # A if must be right after the equal sign if not right.strip().startswith('if'): _error(eq, '\nThe right term must directly start with a if statement.') # It must have the same number of : and of else if not nb_then == 2*nb_else: _error(eq, '\nConditional statements must use both : and else.') multilined = transform(right) condition = parse(multilined) right = ' __conditional__0 ' # only one conditional allowed in that case if split: eq = left + '=' + right else: eq = right else: _print(eq) _error('Conditional statements must define "then" and "else" values.\n var = if condition: a else: b') return eq, [condition]
def extract_ite(name, eq, description, split=True): """ Extracts if-then-else statements and processes them. If-then-else statements must be of the form: .. code-block:: python variable = if condition: ... val1 ... else: ... val2 Conditional statements can be nested, but they should return only one value! """ def transform(code): " Transforms the code into a list of lines." res = [] items = [] for arg in code.split(':'): items.append(arg.strip()) for i in range(len(items)): if items[i].startswith('if '): res.append(items[i].strip()) elif items[i].endswith('else'): res.append(items[i].split('else')[0].strip()) res.append('else') else: # the last then res.append(items[i].strip()) return res def parse(lines): " Recursive analysis of if-else statements" result = [] while lines: if lines[0].startswith('if'): block = [lines.pop(0).split('if')[1], parse(lines)] if lines[0].startswith('else'): lines.pop(0) block.append(parse(lines)) result.append(block) elif not lines[0].startswith(('else')): result.append(lines.pop(0)) else: break return result[0] # If no if, not a conditional if not 'if ' in eq: return eq, [] # Process the equation condition = [] # Eventually split around = if split: left, right = eq.split('=', 1) else: left = '' right = eq nb_then = len(re.findall(':', right)) nb_else = len(re.findall('else', right)) # The equation contains a conditional statement if nb_then > 0: # A if must be right after the equal sign if not right.strip().startswith('if'): _error( eq, '\nThe right term must directly start with a if statement.') # It must have the same number of : and of else if not nb_then == 2 * nb_else: _error(eq, '\nConditional statements must use both : and else.') multilined = transform(right) condition = parse(multilined) right = ' __conditional__0 ' # only one conditional allowed in that case if split: eq = left + '=' + right else: eq = right else: _print(eq) _error( 'Conditional statements must define "then" and "else" values.\n var = if condition: a else: b' ) return eq, [condition]
def check_equation(equation): "Makes a formal check on the equation (matching parentheses, etc)" # Matching parentheses if equation.count('(') != equation.count(')'): _print(equation) _error('The number of parentheses does not match.')
def analyse_neuron(neuron): """ Performs the initial analysis for a single neuron type.""" concurrent_odes = [] # Store basic information description = { 'object': 'neuron', 'type': neuron.type, 'raw_parameters': neuron.parameters, 'raw_equations': neuron.equations, 'raw_functions': neuron.functions, } if neuron.type == 'spike': # Additionally store reset and spike description['raw_reset'] = neuron.reset description['raw_spike'] = neuron.spike description['refractory'] = neuron.refractory # Extract parameters and variables names parameters = extract_parameters(neuron.parameters, neuron.extra_values) variables = extract_variables(neuron.equations) description['parameters'] = parameters description['variables'] = variables # Make sure r is defined for rate-coded networks if neuron.type == 'rate': for var in description['parameters'] + description['variables']: if var['name'] == 'r': break else: _error('Rate-coded neurons must define the variable "r".') exit(0) else: # spiking neurons define r by default, it contains the average FR if enabled for var in description['parameters'] + description['variables']: if var['name'] == 'r': _error( 'Spiking neurons use the variable "r" for the average FR, use another name.' ) exit(0) description['variables'].append({ 'name': 'r', 'locality': 'local', 'bounds': {}, 'ctype': 'double', 'init': 0.0, 'flags': [], 'eq': '', 'cpp': "" }) # Extract functions functions = extract_functions(neuron.functions, False) description['functions'] = functions # Build lists of all attributes (param+var), which are local or global attributes, local_var, global_var = get_attributes(parameters, variables) # Test if attributes are declared only once if len(attributes) != len(list(set(attributes))): _error('Attributes must be declared only once.', attributes) exit(0) description['attributes'] = attributes description['local'] = local_var description['global'] = global_var # Extract all targets targets = extract_targets(variables) description['targets'] = targets if neuron.type == 'spike': # Add a default reset behaviour for conductances for target in targets: found = False for var in description['variables']: if var['name'] == 'g_' + target: found = True break if not found: description['variables'].append({ 'name': 'g_' + target, 'locality': 'local', 'bounds': {}, 'ctype': 'double', 'init': 0.0, 'flags': [], 'eq': 'g_' + target + ' = 0.0' }) description['attributes'].append('g_' + target) description['local'].append('g_' + target) # Extract RandomDistribution objects random_distributions = extract_randomdist(description) description['random_distributions'] = random_distributions # Extract the spike condition if any if neuron.type == 'spike': description['spike'] = extract_spike_variable(description) # Global operation TODO description['global_operations'] = [] # Translate the equations to C++ for variable in description['variables']: eq = variable['transformed_eq'] if eq.strip() == "": continue untouched = {} # Replace sum(target) with pop%(id)s.sum_exc[i] for target in description['targets']: eq = re.sub('sum\(\s*' + target + '\s*\)', '__sum_' + target + '__', eq) untouched['__sum_' + target + '__'] = '_sum_' + target + '%(local_index)s' # Extract global operations eq, untouched_globs, global_ops = extract_globalops_neuron( variable['name'], eq, description) # Add the untouched variables to the global list for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val description['global_operations'] += global_ops # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Find the numerical method if any method = find_method(variable) # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation(variable['name'], variable['bounds']['min'], description, type='return', untouched=untouched) variable['bounds']['min'] = translator.parse().replace(';', '') if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation(variable['name'], variable['bounds']['max'], description, type='return', untouched=untouched) variable['bounds']['max'] = translator.parse().replace(';', '') # Analyse the equation if condition == []: translator = Equation(variable['name'], eq, description, method=method, untouched=untouched) code = translator.parse() dependencies = translator.dependencies() else: # An if-then-else statement code = translate_ITE(variable['name'], eq, condition, description, untouched) dependencies = [] if isinstance(code, str): cpp_eq = code switch = None else: # ODE cpp_eq = code[0] switch = code[1] # Replace untouched variables with their original name for prev, new in untouched.items(): if prev.startswith('g_'): cpp_eq = re.sub(r'([^_]+)' + prev, r'\1' + new, ' ' + cpp_eq).strip() if switch: switch = re.sub(r'([^_]+)' + prev, new, ' ' + switch).strip() else: cpp_eq = re.sub(prev, new, cpp_eq) if switch: switch = re.sub(prev, new, switch) # Replace local functions for f in description['functions']: cpp_eq = re.sub(r'([^\w]*)' + f['name'] + '\(', r'\1' + f['name'] + '(', ' ' + cpp_eq).strip() # Store the result variable['cpp'] = cpp_eq # the C++ equation variable['switch'] = switch # switch value of ODE variable['untouched'] = untouched # may be needed later variable['method'] = method # may be needed later variable['dependencies'] = dependencies # may be needed later # If the method is implicit or midpoint, the equations must be solved concurrently (depend on v[t+1]) if method in ['implicit', 'midpoint']: concurrent_odes.append(variable) # After all variables are processed, do it again if they are concurrent if len(concurrent_odes) > 1: solver = CoupledEquations(description, concurrent_odes) new_eqs = solver.process_variables() for idx, variable in enumerate(description['variables']): for new_eq in new_eqs: if variable['name'] == new_eq['name']: description['variables'][idx] = new_eq return description
def analyse_neuron(neuron): """ Parses the structure and generates code snippets for the neuron type. It returns a ``description`` dictionary with the following fields: * 'object': 'neuron' by default, to distinguish it from 'synapse' * 'type': either 'rate' or 'spiking' * 'raw_parameters': provided field * 'raw_equations': provided field * 'raw_functions': provided field * 'raw_reset': provided field * 'raw_spike': provided field * 'refractory': provided field * 'parameters': list of parameters defined for the neuron type * 'variables': list of variables defined for the neuron type * 'functions': list of functions defined for the neuron type * 'attributes': list of names of all parameters and variables * 'local': list of names of parameters and variables which are local to each neuron * 'global': list of names of parameters and variables which are global to the population * 'targets': list of targets used in the equations * 'random_distributions': list of random number generators used in the neuron equations * 'global_operations': list of global operations (min/max/mean...) used in the equations (unused) * 'spike': when defined, contains the equations of the spike conditions and reset. Each parameter is a dictionary with the following elements: * 'bounds': unused * 'ctype': 'type of the parameter: 'float', 'double', 'int' or 'bool' * 'eq': original equation in text format * 'flags': list of flags provided after the : * 'init': initial value * 'locality': 'local' or 'global' * 'name': name of the parameter Each variable is a dictionary with the following elements: * 'bounds': dictionary of bounds ('init', 'min', 'max') provided after the : * 'cpp': C++ code snippet updating the variable * 'ctype': type of the variable: 'float', 'double', 'int' or 'bool' * 'dependencies': list of variable and parameter names on which the equation depends * 'eq': original equation in text format * 'flags': list of flags provided after the : * 'init': initial value * 'locality': 'local' or 'global' * 'method': numericalmethod for ODEs * 'name': name of the variable * 'pre_loop': ODEs have a pre_loop term for precomputing dt/tau. dict with 'name' and 'value'. type must be inferred. * 'switch': ODEs have a switch term * 'transformed_eq': same as eq, except special terms (sums, rds) are replaced with a temporary name * 'untouched': dictionary of special terms, with their new name as keys and replacement values as values. The 'spike' element (when present) is a dictionary containing: * 'spike_cond': the C++ code snippet containing the spike condition ("v%(local_index)s > v_T") * 'spike_cond_dependencies': list of variables/parameters on which the spike condition depends * 'spike_reset': a list of reset statements, each of them composed of : * 'constraint': either '' or 'unless_refractory' * 'cpp': C++ code snippet * 'dependencies': list of variables on which the reset statement depends * 'eq': original equation in text format * 'name': name of the reset variable """ # Store basic information description = { 'object': 'neuron', 'type': neuron.type, 'raw_parameters': neuron.parameters, 'raw_equations': neuron.equations, 'raw_functions': neuron.functions, } # Spiking neurons additionally store the spike condition, the reset statements and a refractory period if neuron.type == 'spike': description['raw_reset'] = neuron.reset description['raw_spike'] = neuron.spike description['raw_axon_spike'] = neuron.axon_spike description['raw_axon_reset'] = neuron.axon_reset description['refractory'] = neuron.refractory # Extract parameters and variables names parameters = extract_parameters(neuron.parameters, neuron.extra_values) variables = extract_variables(neuron.equations) description['parameters'] = parameters description['variables'] = variables # Make sure r is defined for rate-coded networks from ANNarchy.extensions.bold.BoldModel import BoldModel if isinstance(neuron, BoldModel): found = False for var in description['parameters'] + description['variables']: if var['name'] == 'r': found = True if not found: description['variables'].append({ 'name': 'r', 'locality': 'local', 'bounds': {}, 'ctype': config['precision'], 'init': 0.0, 'flags': [], 'eq': '', 'cpp': "" }) elif neuron.type == 'rate': for var in description['parameters'] + description['variables']: if var['name'] == 'r': break else: _error('Rate-coded neurons must define the variable "r".') else: # spiking neurons define r by default, it contains the average FR if enabled for var in description['parameters'] + description['variables']: if var['name'] == 'r': _error( 'Spiking neurons use the variable "r" for the average FR, use another name.' ) description['variables'].append({ 'name': 'r', 'locality': 'local', 'bounds': {}, 'ctype': config['precision'], 'init': 0.0, 'flags': [], 'eq': '', 'cpp': "" }) # Extract functions functions = extract_functions(neuron.functions, False) description['functions'] = functions # Build lists of all attributes (param + var), which are local or global attributes, local_var, global_var, _ = get_attributes(parameters, variables, neuron=True) # Test if attributes are declared only once if len(attributes) != len(list(set(attributes))): _error('Attributes must be declared only once.', attributes) # Store the attributes description['attributes'] = attributes description['local'] = local_var description['semiglobal'] = [] # only for projections description['global'] = global_var # Extract all targets targets = sorted(list(set(extract_targets(variables)))) description['targets'] = targets if neuron.type == 'spike': # Add a default reset behaviour for conductances for target in targets: found = False for var in description['variables']: if var['name'] == 'g_' + target: found = True break if not found: description['variables'].append({ 'name': 'g_' + target, 'locality': 'local', 'bounds': {}, 'ctype': config['precision'], 'init': 0.0, 'flags': [], 'eq': 'g_' + target + ' = 0.0' }) description['attributes'].append('g_' + target) description['local'].append('g_' + target) # Extract RandomDistribution objects random_distributions = extract_randomdist(description) description['random_distributions'] = random_distributions # Extract the spike condition if any if neuron.type == 'spike': description['spike'] = extract_spike_variable(description) description['axon_spike'] = extract_axon_spike_condition(description) # Global operation TODO description['global_operations'] = [] # The ODEs may be interdependent (implicit, midpoint), so they need to be passed explicitely to CoupledEquations concurrent_odes = [] # Translate the equations to C++ for variable in description['variables']: # Get the equation eq = variable['transformed_eq'] if eq.strip() == "": continue # Special variables (sums, global operations, rd) are placed in untouched, so that Sympy ignores them untouched = {} # Dependencies must be gathered dependencies = [] # Replace sum(target) with _sum_exc__[i] for target in description['targets']: # sum() is valid for all targets eq = re.sub(r'(?P<pre>[^\w.])sum\(\)', r'\1sum(__all__)', eq) # Replace sum(target) with __sum_target__ eq = re.sub('sum\(\s*' + target + '\s*\)', '__sum_' + target + '__', eq) untouched['__sum_' + target + '__'] = '_sum_' + target + '%(local_index)s' # Extract global operations eq, untouched_globs, global_ops = extract_globalops_neuron( variable['name'], eq, description) # Add the untouched variables to the global list for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val description['global_operations'] += global_ops # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Find the numerical method if any method = find_method(variable) # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation(variable['name'], variable['bounds']['min'], description, type='return', untouched=untouched) variable['bounds']['min'] = translator.parse().replace(';', '') dependencies += translator.dependencies() if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation(variable['name'], variable['bounds']['max'], description, type='return', untouched=untouched) variable['bounds']['max'] = translator.parse().replace(';', '') dependencies += translator.dependencies() # Analyse the equation if condition == []: # No if-then-else translator = Equation(variable['name'], eq, description, method=method, untouched=untouched) code = translator.parse() dependencies += translator.dependencies() else: # An if-then-else statement code, deps = translate_ITE(variable['name'], eq, condition, description, untouched) dependencies += deps # ODEs have a switch statement: # double _r = (1.0 - r)/tau; # r[i] += dt* _r; # while direct assignments are one-liners: # r[i] = 1.0 if isinstance(code, str): pre_loop = {} cpp_eq = code switch = None else: # ODE pre_loop = code[0] cpp_eq = code[1] switch = code[2] # Replace untouched variables with their original name for prev, new in untouched.items(): if prev.startswith('g_'): cpp_eq = re.sub(r'([^_]+)' + prev, r'\1' + new, ' ' + cpp_eq).strip() if len(pre_loop) > 0: pre_loop['value'] = re.sub(r'([^_]+)' + prev, new, ' ' + pre_loop['value']).strip() if switch: switch = re.sub(r'([^_]+)' + prev, new, ' ' + switch).strip() else: cpp_eq = re.sub(prev, new, cpp_eq) if len(pre_loop) > 0: pre_loop['value'] = re.sub(prev, new, pre_loop['value']) if switch: switch = re.sub(prev, new, switch) # Replace local functions for f in description['functions']: cpp_eq = re.sub(r'([^\w]*)' + f['name'] + '\(', r'\1' + f['name'] + '(', ' ' + cpp_eq).strip() # Store the result variable[ 'pre_loop'] = pre_loop # Things to be declared before the for loop (eg. dt) variable['cpp'] = cpp_eq # the C++ equation variable['switch'] = switch # switch value of ODE variable['untouched'] = untouched # may be needed later variable['method'] = method # may be needed later variable['dependencies'] = list( set(dependencies)) # may be needed later # If the method is implicit or midpoint, the equations must be solved concurrently (depend on v[t+1]) if method in ['implicit', 'midpoint', 'runge-kutta4' ] and switch is not None: concurrent_odes.append(variable) # After all variables are processed, do it again if they are concurrent if len(concurrent_odes) > 1: solver = CoupledEquations(description, concurrent_odes) new_eqs = solver.parse() for idx, variable in enumerate(description['variables']): for new_eq in new_eqs: if variable['name'] == new_eq['name']: description['variables'][idx] = new_eq return description
def extract_randomdist(description): " Extracts RandomDistribution objects from all variables" rk_rand = 0 random_objects = [] for variable in description['variables']: eq = variable['eq'] # Search for all distributions for dist in available_distributions: matches = re.findall('(?P<pre>[^\w.])' + dist + '\(([^()]+)\)', eq) if matches == ' ': continue for l, v in matches: # Check the arguments arguments = v.split(',') # Check the number of provided arguments if len(arguments) < distributions_arguments[dist]: _error(eq) _error('The distribution ' + dist + ' requires ' + str(distributions_arguments[dist]) + 'parameters') elif len(arguments) > distributions_arguments[dist]: _error(eq) _error( 'Too many parameters provided to the distribution ' + dist) # Process the arguments processed_arguments = "" for idx in range(len(arguments)): try: arg = float(arguments[idx]) except: # A global parameter if arguments[idx].strip() in description['global']: if description['object'] == 'neuron': arg = arguments[idx].strip() else: arg = arguments[idx].strip() else: _error( arguments[idx] + ' is not a global parameter of the neuron/synapse. It can not be used as an argument to the random distribution ' + dist + '(' + v + ')') exit(0) processed_arguments += str(arg) if idx != len(arguments) - 1: # not the last one processed_arguments += ', ' definition = distributions_equivalents[ dist] + '(' + processed_arguments + ')' # Store its definition desc = { 'name': 'rand_' + str(rk_rand), 'dist': dist, 'definition': definition, 'args': processed_arguments, 'template': distributions_equivalents[dist] } rk_rand += 1 random_objects.append(desc) # Replace its definition by its temporary name # Problem: when one uses twice the same RD in a single equation (perverse...) eq = eq.replace(dist + '(' + v + ')', desc['name']) # Add the new variable to the vocabulary description['attributes'].append(desc['name']) if variable['name'] in description['local']: description['local'].append(desc['name']) else: # Why not on a population-wide variable? description['global'].append(desc['name']) variable['transformed_eq'] = eq return random_objects
def analyse_neuron(neuron): """ Performs the initial analysis for a single neuron type.""" concurrent_odes = [] # Store basic information description = { 'object': 'neuron', 'type': neuron.type, 'raw_parameters': neuron.parameters, 'raw_equations': neuron.equations, 'raw_functions': neuron.functions, } if neuron.type == 'spike': # Additionally store reset and spike description['raw_reset'] = neuron.reset description['raw_spike'] = neuron.spike description['refractory'] = neuron.refractory # Extract parameters and variables names parameters = extract_parameters(neuron.parameters, neuron.extra_values) variables = extract_variables(neuron.equations) description['parameters'] = parameters description['variables'] = variables # Make sure r is defined for rate-coded networks if neuron.type == 'rate': for var in description['parameters'] + description['variables']: if var['name'] == 'r': break else: _error('Rate-coded neurons must define the variable "r".') else: # spiking neurons define r by default, it contains the average FR if enabled for var in description['parameters'] + description['variables']: if var['name'] == 'r': _error('Spiking neurons use the variable "r" for the average FR, use another name.') description['variables'].append( {'name': 'r', 'locality': 'local', 'bounds': {}, 'ctype': 'double', 'init': 0.0, 'flags': [], 'eq': '', 'cpp': ""}) # Extract functions functions = extract_functions(neuron.functions, False) description['functions'] = functions # Build lists of all attributes (param+var), which are local or global attributes, local_var, global_var = get_attributes(parameters, variables) # Test if attributes are declared only once if len(attributes) != len(list(set(attributes))): _error('Attributes must be declared only once.', attributes) description['attributes'] = attributes description['local'] = local_var description['global'] = global_var # Extract all targets targets = extract_targets(variables) description['targets'] = targets if neuron.type == 'spike': # Add a default reset behaviour for conductances for target in targets: found = False for var in description['variables']: if var['name'] == 'g_' + target: found = True break if not found: description['variables'].append( { 'name': 'g_'+target, 'locality': 'local', 'bounds': {}, 'ctype': 'double', 'init': 0.0, 'flags': [], 'eq': 'g_' + target+ ' = 0.0'} ) description['attributes'].append('g_'+target) description['local'].append('g_'+target) # Extract RandomDistribution objects random_distributions = extract_randomdist(description) description['random_distributions'] = random_distributions # Extract the spike condition if any if neuron.type == 'spike': description['spike'] = extract_spike_variable(description) # Global operation TODO description['global_operations'] = [] # Translate the equations to C++ for variable in description['variables']: eq = variable['transformed_eq'] if eq.strip() == "": continue untouched={} # Replace sum(target) with pop%(id)s.sum_exc[i] for target in description['targets']: eq = re.sub('sum\(\s*'+target+'\s*\)', '__sum_'+target+'__', eq) untouched['__sum_'+target+'__'] = '_sum_' + target + '%(local_index)s' # Extract global operations eq, untouched_globs, global_ops = extract_globalops_neuron(variable['name'], eq, description) # Add the untouched variables to the global list for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val description['global_operations'] += global_ops # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Find the numerical method if any method = find_method(variable) # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation( variable['name'], variable['bounds']['min'], description, type = 'return', untouched = untouched ) variable['bounds']['min'] = translator.parse().replace(';', '') if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation( variable['name'], variable['bounds']['max'], description, type = 'return', untouched = untouched) variable['bounds']['max'] = translator.parse().replace(';', '') # Analyse the equation if condition == []: translator = Equation( variable['name'], eq, description, method = method, untouched = untouched ) code = translator.parse() dependencies = translator.dependencies() else: # An if-then-else statement code = translate_ITE( variable['name'], eq, condition, description, untouched ) dependencies = [] if isinstance(code, str): cpp_eq = code switch = None else: # ODE cpp_eq = code[0] switch = code[1] # Replace untouched variables with their original name for prev, new in untouched.items(): if prev.startswith('g_'): cpp_eq = re.sub(r'([^_]+)'+prev, r'\1'+new, ' ' + cpp_eq).strip() if switch: switch = re.sub(r'([^_]+)'+prev, new, ' ' + switch).strip() else: cpp_eq = re.sub(prev, new, cpp_eq) if switch: switch = re.sub(prev, new, switch) # Replace local functions for f in description['functions']: cpp_eq = re.sub(r'([^\w]*)'+f['name']+'\(', r'\1'+f['name'] + '(', ' ' + cpp_eq).strip() # Store the result variable['cpp'] = cpp_eq # the C++ equation variable['switch'] = switch # switch value of ODE variable['untouched'] = untouched # may be needed later variable['method'] = method # may be needed later variable['dependencies'] = dependencies # may be needed later # If the method is implicit or midpoint, the equations must be solved concurrently (depend on v[t+1]) if method in ['implicit', 'midpoint']: concurrent_odes.append(variable) # After all variables are processed, do it again if they are concurrent if len(concurrent_odes) > 1 : solver = CoupledEquations(description, concurrent_odes) new_eqs = solver.process_variables() for idx, variable in enumerate(description['variables']): for new_eq in new_eqs: if variable['name'] == new_eq['name']: description['variables'][idx] = new_eq return description
def solve_implicit(self, expression_list): equations = {} new_vars = {} # Pre-processing to replace the gradient for name, expression in self.expression_list.items(): # transform the expression to suppress = if '=' in expression: expression = expression.replace('=', '- (') expression += ')' # Suppress spaces to extract dvar/dt expression = expression.replace(' ', '') # Transform the gradient into a difference TODO: more robust... expression = expression.replace('d'+name, '_t_gradient_') expression_list[name] = expression # replace the variables by their future value for name, expression in expression_list.items(): for n in self.names: expression = re.sub(r'([^\w]+)'+n+r'([^\w]+)', r'\1_'+n+r'\2', expression) expression = expression.replace('_t_gradient_', '(_'+name+' - '+name+')') expression_list[name] = expression + '-' + name new_var = Symbol('_'+name) self.local_dict['_'+name] = new_var new_vars[new_var] = name for name, expression in expression_list.items(): analysed = parse_expr(expression, local_dict = self.local_dict, transformations = (standard_transformations + (convert_xor,)) ) equations[name] = analysed try: solution = solve(equations.values(), new_vars.keys()) except: _print(expression_list) _error('The multiple ODEs can not be solved together using the implicit Euler method.') for var, sol in solution.items(): # simplify the solution sol = collect( sol, self.local_dict['dt']) # Generate the code cpp_eq = 'double _' + new_vars[var] + ' = ' + ccode(sol) + ';' switch = ccode(self.local_dict[new_vars[var]] ) + ' += _' + new_vars[var] + ';' # Replace untouched variables with their original name for prev, new in self.untouched.items(): cpp_eq = re.sub(prev, new, cpp_eq) switch = re.sub(prev, new, switch) # Store the result for variable in self.variables: if variable['name'] == new_vars[var]: variable['cpp'] = cpp_eq variable['switch'] = switch return self.variables
def __init__(self, parameters="", equations="", spike=None, axon_spike=None, reset=None, axon_reset=None, refractory=None, functions=None, name="", description="", extra_values={}): """ :param parameters: parameters of the neuron and their initial value. :param equations: equations defining the temporal evolution of variables. :param functions: additional functions used in the variables' equations. :param spike: condition to emit a spike (only for spiking neurons). :param axon_spike: condition to emit an axonal spike (only for spiking neurons and optional). The axonal spike can appear additional to the spike and is independent from refractoriness of a neuron. :param reset: changes to the variables after a spike (only for spiking neurons). :param axon_reset: changes to the variables after an axonal spike (only for spiking neurons). :param refractory: refractory period of a neuron after a spike (only for spiking neurons). :param name: name of the neuron type (used for reporting only). :param description: short description of the neuron type (used for reporting). """ # Store the parameters and equations self.parameters = parameters self.equations = equations self.functions = functions self.spike = spike self.axon_spike = axon_spike self.reset = reset self.axon_reset = axon_reset self.refractory = refractory self.extra_values = extra_values # Find the type of the neuron self.type = 'spike' if self.spike else 'rate' # Not available by now ... if axon_spike and config['paradigm'] != "openmp": _error( "Axonal spike conditions are only available for openMP by now." ) # Reporting if not hasattr(self, '_instantiated'): # User-defined _objects['neurons'].append(self) elif len(self._instantiated) == 0: # First instantiated of the class _objects['neurons'].append(self) self._rk_neurons_type = len(_objects['neurons']) if name: self.name = name else: self.name = self._default_names[self.type] if description: self.short_description = description else: self.short_description = "User-defined model of a spiking neuron." if self.type == 'spike' else "User-defined model of a rate-coded neuron." # Analyse the neuron type self.description = None
def extract_structural_plasticity(statement, description): # Extract flags try: eq, constraint = statement.rsplit(':', 1) bounds, flags = extract_flags(constraint) except: eq = statement.strip() bounds = {} flags = [] # Extract RD rd = None for dist in available_distributions: matches = re.findall('(?P<pre>[^\w.])'+dist+'\(([^()]+)\)', eq) for l, v in matches: # Check the arguments arguments = v.split(',') # Check the number of provided arguments if len(arguments) < distributions_arguments[dist]: _error(eq) _error('The distribution ' + dist + ' requires ' + str(distributions_arguments[dist]) + 'parameters') elif len(arguments) > distributions_arguments[dist]: _error(eq) _error('Too many parameters provided to the distribution ' + dist) # Process the arguments processed_arguments = "" for idx in range(len(arguments)): try: arg = float(arguments[idx]) except: # A global parameter _error(eq) _error('Random distributions for creating/pruning synapses must use foxed values.') exit(0) processed_arguments += str(arg) if idx != len(arguments)-1: # not the last one processed_arguments += ', ' definition = distributions_equivalents[dist] + '(' + processed_arguments + ')' # Store its definition if rd: _error(eq) _error('Only one random distribution per equation is allowed.') exit(0) rd = {'name': 'rand_' + str(0) , 'origin': dist+'('+v+')', 'dist': dist, 'definition': definition, 'args' : processed_arguments, 'template': distributions_equivalents[dist]} if rd: eq = eq.replace(rd['origin'], 'rd(rng)') # Extract pre/post dependencies eq, untouched, dependencies = extract_prepost('test', eq, description) # Parse code translator = Equation('test', eq, description, method = 'cond', untouched = {}) code = translator.parse() # Replace untouched variables with their original name for prev, new in untouched.items(): code = code.replace(prev, new) # Add new dependencies for dep in dependencies['pre']: description['dependencies']['pre'].append(dep) for dep in dependencies['post']: description['dependencies']['post'].append(dep) return {'eq': eq, 'cpp': code, 'bounds': bounds, 'flags': flags, 'rd': rd}
def __add__(self, synapse): _error('adding synapse models is not implemented yet.') exit(0)
def analyse_synapse(synapse): """ Parses the structure and generates code snippets for the synapse type. It returns a ``description`` dictionary with the following fields: * 'object': 'synapse' by default, to distinguish it from 'neuron' * 'type': either 'rate' or 'spiking' * 'raw_parameters': provided field * 'raw_equations': provided field * 'raw_functions': provided field * 'raw_psp': provided field * 'raw_pre_spike': provided field * 'raw_post_spike': provided field * 'parameters': list of parameters defined for the synapse type * 'variables': list of variables defined for the synapse type * 'functions': list of functions defined for the synapse type * 'attributes': list of names of all parameters and variables * 'local': list of names of parameters and variables which are local to each synapse * 'semiglobal': list of names of parameters and variables which are local to each postsynaptic neuron * 'global': list of names of parameters and variables which are global to the projection * 'random_distributions': list of random number generators used in the neuron equations * 'global_operations': list of global operations (min/max/mean...) used in the equations * 'pre_global_operations': list of global operations (min/max/mean...) on the pre-synaptic population * 'post_global_operations': list of global operations (min/max/mean...) on the post-synaptic population * 'pre_spike': list of variables updated after a pre-spike event * 'post_spike': list of variables updated after a post-spike event * 'dependencies': dictionary ('pre', 'post') of lists of pre (resp. post) variables accessed by the synapse (used for delaying variables) * 'psp': dictionary ('eq' and 'psp') for the psp code to be summed * 'pruning' and 'creating': statements for structural plasticity Each parameter is a dictionary with the following elements: * 'bounds': unused * 'ctype': 'type of the parameter: 'float', 'double', 'int' or 'bool' * 'eq': original equation in text format * 'flags': list of flags provided after the : * 'init': initial value * 'locality': 'local', 'semiglobal' or 'global' * 'name': name of the parameter Each variable is a dictionary with the following elements: * 'bounds': dictionary of bounds ('init', 'min', 'max') provided after the : * 'cpp': C++ code snippet updating the variable * 'ctype': type of the variable: 'float', 'double', 'int' or 'bool' * 'dependencies': list of variable and parameter names on which the equation depends * 'eq': original equation in text format * 'flags': list of flags provided after the : * 'init': initial value * 'locality': 'local', 'semiglobal' or 'global' * 'method': numericalmethod for ODEs * 'name': name of the variable * 'pre_loop': ODEs have a pre_loop term for precomputing dt/tau * 'switch': ODEs have a switch term * 'transformed_eq': same as eq, except special terms (sums, rds) are replaced with a temporary name * 'untouched': dictionary of special terms, with their new name as keys and replacement values as values. """ # Store basic information description = { 'object': 'synapse', 'type': synapse.type, 'raw_parameters': synapse.parameters, 'raw_equations': synapse.equations, 'raw_functions': synapse.functions } # Psps is what is actually summed over the incoming weights if synapse.psp: description['raw_psp'] = synapse.psp elif synapse.type == 'rate': description['raw_psp'] = "w*pre.r" # Spiking synapses additionally store pre_spike and post_spike if synapse.type == 'spike': description['raw_pre_spike'] = synapse.pre_spike description['raw_post_spike'] = synapse.post_spike # Extract parameters and variables names parameters = extract_parameters(synapse.parameters, synapse.extra_values) variables = extract_variables(synapse.equations) # Extract functions functions = extract_functions(synapse.functions, False) # Check the presence of w description['plasticity'] = False for var in parameters + variables: if var['name'] == 'w': break else: parameters.append( { 'name': 'w', 'bounds': {}, 'ctype': config['precision'], 'init': 0.0, 'flags': [], 'eq': 'w=0.0', 'locality': 'local' } ) # Find out a plasticity rule for var in variables: if var['name'] == 'w': description['plasticity'] = True break # Build lists of all attributes (param+var), which are local or global attributes, local_var, global_var, semiglobal_var = get_attributes(parameters, variables, neuron=False) # Test if attributes are declared only once if len(attributes) != len(list(set(attributes))): _error('Attributes must be declared only once.', attributes) # Add this info to the description description['parameters'] = parameters description['variables'] = variables description['functions'] = functions description['attributes'] = attributes description['local'] = local_var description['semiglobal'] = semiglobal_var description['global'] = global_var description['global_operations'] = [] # Lists of global operations needed at the pre and post populations description['pre_global_operations'] = [] description['post_global_operations'] = [] # Extract RandomDistribution objects description['random_distributions'] = extract_randomdist(description) # Extract event-driven info if description['type'] == 'spike': # pre_spike event description['pre_spike'] = extract_pre_spike_variable(description) for var in description['pre_spike']: if var['name'] in ['g_target']: # Already dealt with continue for avar in description['variables']: if var['name'] == avar['name']: break else: # not defined already description['variables'].append( {'name': var['name'], 'bounds': var['bounds'], 'ctype': var['ctype'], 'init': var['init'], 'locality': var['locality'], 'flags': [], 'transformed_eq': '', 'eq': '', 'cpp': '', 'switch': '', 're_loop': '', 'untouched': '', 'method':'explicit'} ) description['local'].append(var['name']) description['attributes'].append(var['name']) # post_spike event description['post_spike'] = extract_post_spike_variable(description) for var in description['post_spike']: if var['name'] in ['g_target', 'w']: # Already dealt with continue for avar in description['variables']: if var['name'] == avar['name']: break else: # not defined already description['variables'].append( {'name': var['name'], 'bounds': var['bounds'], 'ctype': var['ctype'], 'init': var['init'], 'locality': var['locality'], 'flags': [], 'transformed_eq': '', 'eq': '', 'cpp': '', 'switch': '', 'untouched': '', 'method':'explicit'} ) description['local'].append(var['name']) description['attributes'].append(var['name']) # Variables names for the parser which should be left untouched untouched = {} description['dependencies'] = {'pre': [], 'post': []} # The ODEs may be interdependent (implicit, midpoint), so they need to be passed explicitely to CoupledEquations concurrent_odes = [] # Iterate over all variables for variable in description['variables']: # Equation eq = variable['transformed_eq'] if eq.strip() == '': continue # Dependencies must be gathered dependencies = [] # Extract global operations eq, untouched_globs, global_ops = extract_globalops_synapse(variable['name'], eq, description) description['pre_global_operations'] += global_ops['pre'] description['post_global_operations'] += global_ops['post'] # Remove doubled entries description['pre_global_operations'] = [i for n, i in enumerate(description['pre_global_operations']) if i not in description['pre_global_operations'][n + 1:]] description['post_global_operations'] = [i for n, i in enumerate(description['post_global_operations']) if i not in description['post_global_operations'][n + 1:]] # Extract pre- and post_synaptic variables eq, untouched_var, prepost_dependencies = extract_prepost(variable['name'], eq, description) # Store the pre-post dependencies at the synapse level description['dependencies']['pre'] += prepost_dependencies['pre'] description['dependencies']['post'] += prepost_dependencies['post'] # and also on the variable for checking variable['prepost_dependencies'] = prepost_dependencies # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Add the untouched variables to the global list for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val for name, val in untouched_var.items(): if not name in untouched.keys(): untouched[name] = val # Save the tranformed equation variable['transformed_eq'] = eq # Find the numerical method if any method = find_method(variable) # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation(variable['name'], variable['bounds']['min'], description, type = 'return', untouched = untouched.keys()) variable['bounds']['min'] = translator.parse().replace(';', '') dependencies += translator.dependencies() if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation(variable['name'], variable['bounds']['max'], description, type = 'return', untouched = untouched.keys()) variable['bounds']['max'] = translator.parse().replace(';', '') dependencies += translator.dependencies() # Analyse the equation if condition == []: # Call Equation translator = Equation(variable['name'], eq, description, method = method, untouched = untouched.keys()) code = translator.parse() dependencies += translator.dependencies() else: # An if-then-else statement code, deps = translate_ITE(variable['name'], eq, condition, description, untouched) dependencies += deps if isinstance(code, str): pre_loop = {} cpp_eq = code switch = None else: # ODE pre_loop = code[0] cpp_eq = code[1] switch = code[2] # Replace untouched variables with their original name for prev, new in untouched.items(): cpp_eq = cpp_eq.replace(prev, new) # Replace local functions for f in description['functions']: cpp_eq = re.sub(r'([^\w]*)'+f['name']+'\(', r'\1'+ f['name'] + '(', ' ' + cpp_eq).strip() # Store the result variable['pre_loop'] = pre_loop # Things to be declared before the for loop (eg. dt) variable['cpp'] = cpp_eq # the C++ equation variable['switch'] = switch # switch value id ODE variable['untouched'] = untouched # may be needed later variable['method'] = method # may be needed later variable['dependencies'] = dependencies # If the method is implicit or midpoint, the equations must be solved concurrently (depend on v[t+1]) if method in ['implicit', 'midpoint'] and switch is not None: concurrent_odes.append(variable) # After all variables are processed, do it again if they are concurrent if len(concurrent_odes) > 1 : solver = CoupledEquations(description, concurrent_odes) new_eqs = solver.parse() for idx, variable in enumerate(description['variables']): for new_eq in new_eqs: if variable['name'] == new_eq['name']: description['variables'][idx] = new_eq # Translate the psp code if any if 'raw_psp' in description.keys(): psp = {'eq' : description['raw_psp'].strip() } # Extract global operations eq, untouched_globs, global_ops = extract_globalops_synapse('psp', " " + psp['eq'] + " ", description) description['pre_global_operations'] += global_ops['pre'] description['post_global_operations'] += global_ops['post'] # Replace pre- and post_synaptic variables eq, untouched, prepost_dependencies = extract_prepost('psp', eq, description) description['dependencies']['pre'] += prepost_dependencies['pre'] description['dependencies']['post'] += prepost_dependencies['post'] for name, val in untouched_globs.items(): if not name in untouched.keys(): untouched[name] = val # Extract if-then-else statements eq, condition = extract_ite('psp', eq, description, split=False) # Analyse the equation if condition == []: translator = Equation('psp', eq, description, method = 'explicit', untouched = untouched.keys(), type='return') code = translator.parse() deps = translator.dependencies() else: code, deps = translate_ITE('psp', eq, condition, description, untouched) # Replace untouched variables with their original name for prev, new in untouched.items(): code = code.replace(prev, new) # Store the result psp['cpp'] = code psp['dependencies'] = deps description['psp'] = psp # Process event-driven info if description['type'] == 'spike': for variable in description['pre_spike'] + description['post_spike']: # Find plasticity if variable['name'] == 'w': description['plasticity'] = True # Retrieve the equation eq = variable['eq'] # Extract if-then-else statements eq, condition = extract_ite(variable['name'], eq, description) # Extract pre- and post_synaptic variables eq, untouched, prepost_dependencies = extract_prepost(variable['name'], eq, description) # Update dependencies description['dependencies']['pre'] += prepost_dependencies['pre'] description['dependencies']['post'] += prepost_dependencies['post'] # and also on the variable for checking variable['prepost_dependencies'] = prepost_dependencies # Analyse the equation dependencies = [] if condition == []: translator = Equation(variable['name'], eq, description, method = 'explicit', untouched = untouched) code = translator.parse() dependencies += translator.dependencies() else: code, deps = translate_ITE(variable['name'], eq, condition, description, untouched) dependencies += deps if isinstance(code, list): # an ode in a pre/post statement Global._print(eq) if variable in description['pre_spike']: Global._error('It is forbidden to use ODEs in a pre_spike term.') elif variable in description['posz_spike']: Global._error('It is forbidden to use ODEs in a post_spike term.') else: Global._error('It is forbidden to use ODEs here.') # Replace untouched variables with their original name for prev, new in untouched.items(): code = code.replace(prev, new) # Process the bounds if 'min' in variable['bounds'].keys(): if isinstance(variable['bounds']['min'], str): translator = Equation( variable['name'], variable['bounds']['min'], description, type = 'return', untouched = untouched ) variable['bounds']['min'] = translator.parse().replace(';', '') dependencies += translator.dependencies() if 'max' in variable['bounds'].keys(): if isinstance(variable['bounds']['max'], str): translator = Equation( variable['name'], variable['bounds']['max'], description, type = 'return', untouched = untouched) variable['bounds']['max'] = translator.parse().replace(';', '') dependencies += translator.dependencies() # Store the result variable['cpp'] = code # the C++ equation variable['dependencies'] = dependencies # Structural plasticity if synapse.pruning: description['pruning'] = extract_structural_plasticity(synapse.pruning, description) if synapse.creating: description['creating'] = extract_structural_plasticity(synapse.creating, description) return description