def remove_initial_values_for_kernels(cls, neuron: ASTNeuron) -> None: """ Remove initial values for original declarations (e.g. g_in, g_in', V_m); these might conflict with the initial value expressions returned from ODE-toolbox. """ assert isinstance( neuron.get_equations_blocks(), ASTEquationsBlock), "only one equation block should be present" equations_block = neuron.get_equations_block() symbols_to_remove = set() for kernel in equations_block.get_kernels(): for kernel_var in kernel.get_variables(): kernel_var_order = kernel_var.get_differential_order() for order in range(kernel_var_order): symbol_name = kernel_var.get_name() + "'" * order symbols_to_remove.add(symbol_name) decl_to_remove = set() for symbol_name in symbols_to_remove: for decl in neuron.get_state_blocks().get_declarations(): if len(decl.get_variables()) == 1: if decl.get_variables()[0].get_name() == symbol_name: decl_to_remove.add(decl) else: for var in decl.get_variables(): if var.get_name() == symbol_name: decl.variables.remove(var) for decl in decl_to_remove: neuron.get_state_blocks().get_declarations().remove(decl)
def ode_toolbox_analysis(self, neuron: ASTNeuron, kernel_buffers: Mapping[ASTKernel, ASTInputPort]): """ Prepare data for ODE-toolbox input format, invoke ODE-toolbox analysis via its API, and return the output. """ assert isinstance( neuron.get_equations_blocks(), ASTEquationsBlock), "only one equation block should be present" equations_block = neuron.get_equations_block() if len(equations_block.get_kernels()) == 0 and len( equations_block.get_ode_equations()) == 0: # no equations defined -> no changes to the neuron return None, None code, message = Messages.get_neuron_analyzed(neuron.get_name()) Logger.log_message(neuron, code, message, neuron.get_source_position(), LoggingLevel.INFO) parameters_block = neuron.get_parameter_blocks() odetoolbox_indict = self.transform_ode_and_kernels_to_json( neuron, parameters_block, kernel_buffers) odetoolbox_indict["options"] = {} odetoolbox_indict["options"]["output_timestep_symbol"] = "__h" solver_result = analysis( odetoolbox_indict, disable_stiffness_check=True, debug=FrontendConfiguration.logging_level == "DEBUG") analytic_solver = None analytic_solvers = [ x for x in solver_result if x["solver"] == "analytical" ] assert len( analytic_solvers ) <= 1, "More than one analytic solver not presently supported" if len(analytic_solvers) > 0: analytic_solver = analytic_solvers[0] # if numeric solver is required, generate a stepping function that includes each state variable numeric_solver = None numeric_solvers = [ x for x in solver_result if x["solver"].startswith("numeric") ] if numeric_solvers: solver_result = analysis( odetoolbox_indict, disable_stiffness_check=True, disable_analytic_solver=True, debug=FrontendConfiguration.logging_level == "DEBUG") numeric_solvers = [ x for x in solver_result if x["solver"].startswith("numeric") ] assert len( numeric_solvers ) <= 1, "More than one numeric solver not presently supported" if len(numeric_solvers) > 0: numeric_solver = numeric_solvers[0] return analytic_solver, numeric_solver
def replace_convolution_aliasing_inlines(cls, neuron: ASTNeuron) -> None: """ Replace all occurrences of kernel names (e.g. ``I_dend`` and ``I_dend'`` for a definition involving a second-order kernel ``inline kernel I_dend = convolve(kern_name, spike_buf)``) with the ODE-toolbox generated variable ``kern_name__X__spike_buf``. """ def replace_var(_expr, replace_var_name: str, replace_with_var_name: str): if isinstance(_expr, ASTSimpleExpression) and _expr.is_variable(): var = _expr.get_variable() if var.get_name() == replace_var_name: ast_variable = ASTVariable( replace_with_var_name + '__d' * var.get_differential_order(), differential_order=0) ast_variable.set_source_position(var.get_source_position()) _expr.set_variable(ast_variable) elif isinstance(_expr, ASTVariable): var = _expr if var.get_name() == replace_var_name: var.set_name(replace_with_var_name + '__d' * var.get_differential_order()) var.set_differential_order(0) for decl in neuron.get_equations_block().get_declarations(): from pynestml.utils.ast_utils import ASTUtils if isinstance(decl, ASTInlineExpression) \ and isinstance(decl.get_expression(), ASTSimpleExpression) \ and '__X__' in str(decl.get_expression()): replace_with_var_name = decl.get_expression().get_variable( ).get_name() neuron.accept( ASTHigherOrderVisitor(lambda x: replace_var( x, decl.get_variable_name(), replace_with_var_name)))
def remove_ode_definitions_from_equations_block(cls, neuron: ASTNeuron) -> None: """ Removes all ODEs in this block. """ equations_block = neuron.get_equations_block() decl_to_remove = set() for decl in equations_block.get_ode_equations(): decl_to_remove.add(decl) for decl in decl_to_remove: equations_block.get_declarations().remove(decl)
def analyse_neuron(self, neuron: ASTNeuron) -> List[ASTAssignment]: """ Analyse and transform a single neuron. :param neuron: a single neuron. :return: spike_updates: list of spike updates, see documentation for get_spike_update_expressions() for more information. """ code, message = Messages.get_start_processing_neuron(neuron.get_name()) Logger.log_message(neuron, code, message, neuron.get_source_position(), LoggingLevel.INFO) equations_block = neuron.get_equations_block() if equations_block is None: return [] delta_factors = self.get_delta_factors_(neuron, equations_block) kernel_buffers = self.generate_kernel_buffers_(neuron, equations_block) self.replace_convolve_calls_with_buffers_(neuron, equations_block, kernel_buffers) self.make_inline_expressions_self_contained( equations_block.get_inline_expressions()) self.replace_inline_expressions_through_defining_expressions( equations_block.get_ode_equations(), equations_block.get_inline_expressions()) analytic_solver, numeric_solver = self.ode_toolbox_analysis( neuron, kernel_buffers) self.analytic_solver[neuron.get_name()] = analytic_solver self.numeric_solver[neuron.get_name()] = numeric_solver self.remove_initial_values_for_kernels(neuron) kernels = self.remove_kernel_definitions_from_equations_block(neuron) self.update_initial_values_for_odes(neuron, [analytic_solver, numeric_solver], kernels) self.remove_ode_definitions_from_equations_block(neuron) self.create_initial_values_for_kernels( neuron, [analytic_solver, numeric_solver], kernels) self.replace_variable_names_in_expressions( neuron, [analytic_solver, numeric_solver]) self.add_timestep_symbol(neuron) if self.analytic_solver[neuron.get_name()] is not None: neuron = add_declarations_to_internals( neuron, self.analytic_solver[neuron.get_name()]["propagators"]) self.update_symbol_table(neuron, kernel_buffers) spike_updates = self.get_spike_update_expressions( neuron, kernel_buffers, [analytic_solver, numeric_solver], delta_factors) return spike_updates
def remove_kernel_definitions_from_equations_block( cls, neuron: ASTNeuron) -> ASTDeclaration: """ Removes all kernels in this block. """ equations_block = neuron.get_equations_block() decl_to_remove = set() for decl in equations_block.get_declarations(): if type(decl) is ASTKernel: decl_to_remove.add(decl) for decl in decl_to_remove: equations_block.get_declarations().remove(decl) return decl_to_remove
def transform_ode_and_kernels_to_json(self, neuron: ASTNeuron, parameters_block, kernel_buffers): """ Converts AST node to a JSON representation suitable for passing to ode-toolbox. Each kernel has to be generated for each spike buffer convolve in which it occurs, e.g. if the NESTML model code contains the statements convolve(G, ex_spikes) convolve(G, in_spikes) then `kernel_buffers` will contain the pairs `(G, ex_spikes)` and `(G, in_spikes)`, from which two ODEs will be generated, with dynamical state (variable) names `G__X__ex_spikes` and `G__X__in_spikes`. :param equations_block: ASTEquationsBlock :return: Dict """ odetoolbox_indict = {} gsl_converter = ODEToolboxReferenceConverter() gsl_printer = UnitlessExpressionPrinter(gsl_converter) odetoolbox_indict["dynamics"] = [] equations_block = neuron.get_equations_block() for equation in equations_block.get_ode_equations(): # n.b. includes single quotation marks to indicate differential order lhs = to_ode_toolbox_name(equation.get_lhs().get_complete_name()) rhs = gsl_printer.print_expression(equation.get_rhs()) entry = {"expression": lhs + " = " + rhs} symbol_name = equation.get_lhs().get_name() symbol = equations_block.get_scope().resolve_to_symbol( symbol_name, SymbolKind.VARIABLE) entry["initial_values"] = {} symbol_order = equation.get_lhs().get_differential_order() for order in range(symbol_order): iv_symbol_name = symbol_name + "'" * order initial_value_expr = neuron.get_initial_value(iv_symbol_name) if initial_value_expr: expr = gsl_printer.print_expression(initial_value_expr) entry["initial_values"][to_ode_toolbox_name( iv_symbol_name)] = expr odetoolbox_indict["dynamics"].append(entry) # write a copy for each (kernel, spike buffer) combination for kernel, spike_input_port in kernel_buffers: if is_delta_kernel(kernel): # delta function -- skip passing this to ode-toolbox continue for kernel_var in kernel.get_variables(): expr = get_expr_from_kernel_var(kernel, kernel_var.get_complete_name()) kernel_order = kernel_var.get_differential_order() kernel_X_spike_buf_name_ticks = construct_kernel_X_spike_buf_name( kernel_var.get_name(), spike_input_port, kernel_order, diff_order_symbol="'") replace_rhs_variables(expr, kernel_buffers) entry = {} entry[ "expression"] = kernel_X_spike_buf_name_ticks + " = " + str( expr) # initial values need to be declared for order 1 up to kernel order (e.g. none for kernel function f(t) = ...; 1 for kernel ODE f'(t) = ...; 2 for f''(t) = ... and so on) entry["initial_values"] = {} for order in range(kernel_order): iv_sym_name_ode_toolbox = construct_kernel_X_spike_buf_name( kernel_var.get_name(), spike_input_port, order, diff_order_symbol="'") symbol_name_ = kernel_var.get_name() + "'" * order symbol = equations_block.get_scope().resolve_to_symbol( symbol_name_, SymbolKind.VARIABLE) assert symbol is not None, "Could not find initial value for variable " + symbol_name_ initial_value_expr = symbol.get_declaring_expression() assert initial_value_expr is not None, "No initial value found for variable name " + symbol_name_ entry["initial_values"][ iv_sym_name_ode_toolbox] = gsl_printer.print_expression( initial_value_expr) odetoolbox_indict["dynamics"].append(entry) odetoolbox_indict["parameters"] = {} if parameters_block is not None: for decl in parameters_block.get_declarations(): for var in decl.variables: odetoolbox_indict["parameters"][var.get_complete_name( )] = gsl_printer.print_expression(decl.get_expression()) return odetoolbox_indict
def setup_generation_helpers(self, neuron: ASTNeuron) -> Dict: """ Returns a standard namespace with often required functionality. :param neuron: a single neuron instance :type neuron: ASTNeuron :return: a map from name to functionality. :rtype: dict """ gsl_converter = GSLReferenceConverter() gsl_printer = UnitlessExpressionPrinter(gsl_converter) # helper classes and objects converter = NESTReferenceConverter(False) unitless_pretty_printer = UnitlessExpressionPrinter(converter) namespace = dict() namespace['neuronName'] = neuron.get_name() namespace['neuron'] = neuron namespace['moduleName'] = FrontendConfiguration.get_module_name() namespace['printer'] = NestPrinter(unitless_pretty_printer) namespace['assignments'] = NestAssignmentsHelper() namespace['names'] = NestNamesConverter() namespace['declarations'] = NestDeclarationsHelper() namespace['utils'] = ASTUtils() namespace['idemPrinter'] = UnitlessExpressionPrinter() namespace['outputEvent'] = namespace['printer'].print_output_event( neuron.get_body()) namespace['is_spike_input'] = ASTUtils.is_spike_input( neuron.get_body()) namespace['is_current_input'] = ASTUtils.is_current_input( neuron.get_body()) namespace['odeTransformer'] = OdeTransformer() namespace['printerGSL'] = gsl_printer namespace['now'] = datetime.datetime.utcnow() namespace['tracing'] = FrontendConfiguration.is_dev namespace[ 'PredefinedUnits'] = pynestml.symbols.predefined_units.PredefinedUnits namespace[ 'UnitTypeSymbol'] = pynestml.symbols.unit_type_symbol.UnitTypeSymbol namespace['initial_values'] = {} namespace['uses_analytic_solver'] = neuron.get_name() in self.analytic_solver.keys() \ and self.analytic_solver[neuron.get_name()] is not None if namespace['uses_analytic_solver']: namespace['analytic_state_variables'] = self.analytic_solver[ neuron.get_name()]["state_variables"] namespace['analytic_variable_symbols'] = { sym: neuron.get_equations_block().get_scope().resolve_to_symbol( sym, SymbolKind.VARIABLE) for sym in namespace['analytic_state_variables'] } namespace['update_expressions'] = {} for sym, expr in self.analytic_solver[ neuron.get_name()]["initial_values"].items(): namespace['initial_values'][sym] = expr for sym in namespace['analytic_state_variables']: expr_str = self.analytic_solver[ neuron.get_name()]["update_expressions"][sym] expr_ast = ModelParser.parse_expression(expr_str) # pretend that update expressions are in "equations" block, which should always be present, as differential equations must have been defined to get here expr_ast.update_scope( neuron.get_equations_blocks().get_scope()) expr_ast.accept(ASTSymbolTableVisitor()) namespace['update_expressions'][sym] = expr_ast namespace['propagators'] = self.analytic_solver[ neuron.get_name()]["propagators"] namespace['uses_numeric_solver'] = neuron.get_name() in self.analytic_solver.keys() \ and self.numeric_solver[neuron.get_name()] is not None if namespace['uses_numeric_solver']: namespace['numeric_state_variables'] = self.numeric_solver[ neuron.get_name()]["state_variables"] namespace['numeric_variable_symbols'] = { sym: neuron.get_equations_block().get_scope().resolve_to_symbol( sym, SymbolKind.VARIABLE) for sym in namespace['numeric_state_variables'] } assert not any([ sym is None for sym in namespace['numeric_variable_symbols'].values() ]) namespace['numeric_update_expressions'] = {} for sym, expr in self.numeric_solver[ neuron.get_name()]["initial_values"].items(): namespace['initial_values'][sym] = expr for sym in namespace['numeric_state_variables']: expr_str = self.numeric_solver[ neuron.get_name()]["update_expressions"][sym] expr_ast = ModelParser.parse_expression(expr_str) # pretend that update expressions are in "equations" block, which should always be present, as differential equations must have been defined to get here expr_ast.update_scope( neuron.get_equations_blocks().get_scope()) expr_ast.accept(ASTSymbolTableVisitor()) namespace['numeric_update_expressions'][sym] = expr_ast namespace['useGSL'] = namespace['uses_numeric_solver'] namespace['names'] = GSLNamesConverter() converter = NESTReferenceConverter(True) unitless_pretty_printer = UnitlessExpressionPrinter(converter) namespace['printer'] = NestPrinter(unitless_pretty_printer) namespace["spike_updates"] = neuron.spike_updates rng_visitor = ASTRandomNumberGeneratorVisitor() neuron.accept(rng_visitor) namespace['norm_rng'] = rng_visitor._norm_rng_is_used return namespace
def analyse_neuron(self, neuron: ASTNeuron) -> List[ASTAssignment]: """ Analyse and transform a single neuron. :param neuron: a single neuron. :return: spike_updates: list of spike updates, see documentation for get_spike_update_expressions() for more information. """ code, message = Messages.get_start_processing_neuron(neuron.get_name()) Logger.log_message(neuron, code, message, neuron.get_source_position(), LoggingLevel.INFO) equations_block = neuron.get_equations_block() if equations_block is None: # add all declared state variables as none of them are used in equations block self.non_equations_state_variables[neuron.get_name()] = [] self.non_equations_state_variables[neuron.get_name()].extend(ASTUtils.all_variables_defined_in_block(neuron.get_initial_values_blocks())) self.non_equations_state_variables[neuron.get_name()].extend(ASTUtils.all_variables_defined_in_block(neuron.get_state_blocks())) return [] delta_factors = self.get_delta_factors_(neuron, equations_block) kernel_buffers = self.generate_kernel_buffers_(neuron, equations_block) self.replace_convolve_calls_with_buffers_(neuron, equations_block, kernel_buffers) self.make_inline_expressions_self_contained(equations_block.get_inline_expressions()) self.replace_inline_expressions_through_defining_expressions( equations_block.get_ode_equations(), equations_block.get_inline_expressions()) analytic_solver, numeric_solver = self.ode_toolbox_analysis(neuron, kernel_buffers) self.analytic_solver[neuron.get_name()] = analytic_solver self.numeric_solver[neuron.get_name()] = numeric_solver self.non_equations_state_variables[neuron.get_name()] = [] for decl in neuron.get_initial_values_blocks().get_declarations(): for var in decl.get_variables(): # check if this variable is not in equations if not neuron.get_equations_blocks(): self.non_equations_state_variables[neuron.get_name()].append(var) continue used_in_eq = False for ode_eq in neuron.get_equations_blocks().get_ode_equations(): if ode_eq.get_lhs().get_name() == var.get_name(): used_in_eq = True break for kern in neuron.get_equations_blocks().get_kernels(): for kern_var in kern.get_variables(): if kern_var.get_name() == var.get_name(): used_in_eq = True break if not used_in_eq: self.non_equations_state_variables[neuron.get_name()].append(var) self.remove_initial_values_for_kernels(neuron) kernels = self.remove_kernel_definitions_from_equations_block(neuron) self.update_initial_values_for_odes(neuron, [analytic_solver, numeric_solver], kernels) self.remove_ode_definitions_from_equations_block(neuron) self.create_initial_values_for_kernels(neuron, [analytic_solver, numeric_solver], kernels) self.replace_variable_names_in_expressions(neuron, [analytic_solver, numeric_solver]) self.add_timestep_symbol(neuron) if self.analytic_solver[neuron.get_name()] is not None: neuron = add_declarations_to_internals(neuron, self.analytic_solver[neuron.get_name()]["propagators"]) self.update_symbol_table(neuron, kernel_buffers) spike_updates = self.get_spike_update_expressions( neuron, kernel_buffers, [analytic_solver, numeric_solver], delta_factors) return spike_updates
def check_co_co(cls, node: ASTNeuron, after_ast_rewrite: bool = False): """ Checks if this coco applies for the handed over neuron. Models which contain undefined variables are not correct. :param node: a single neuron instance. :param after_ast_rewrite: indicates whether this coco is checked after the code generator has done rewriting of the abstract syntax tree. If True, checks are not as rigorous. Use False where possible. """ # for each variable in all expressions, check if the variable has been defined previously expression_collector_visitor = ASTExpressionCollectorVisitor() node.accept(expression_collector_visitor) expressions = expression_collector_visitor.ret for expr in expressions: if isinstance(expr, ASTVariable): vars = [expr] else: vars = expr.get_variables() for var in vars: symbol = var.get_scope().resolve_to_symbol( var.get_complete_name(), SymbolKind.VARIABLE) # this part is required to check that we handle invariants differently expr_par = node.get_parent(expr) # test if the symbol has been defined at least if symbol is None: if after_ast_rewrite: # after ODE-toolbox transformations, convolutions are replaced by state variables, so cannot perform this check properly symbol2 = node.get_scope().resolve_to_symbol( var.get_name(), SymbolKind.VARIABLE) if symbol2 is not None: # an inline expression defining this variable name (ignoring differential order) exists if "__X__" in str( symbol2 ): # if this variable was the result of a convolution... continue else: # for kernels, also allow derivatives of that kernel to appear if node.get_equations_block() is not None: inline_expr_names = [ inline_expr.variable_name for inline_expr in node.get_equations_block(). get_inline_expressions() ] if var.get_name() in inline_expr_names: inline_expr_idx = inline_expr_names.index( var.get_name()) inline_expr = node.get_equations_block( ).get_inline_expressions()[inline_expr_idx] from pynestml.utils.ast_utils import ASTUtils if ASTUtils.inline_aliases_convolution( inline_expr): symbol2 = node.get_scope( ).resolve_to_symbol( var.get_name(), SymbolKind.VARIABLE) if symbol2 is not None: # actually, no problem detected, skip error # XXX: TODO: check that differential order is less than or equal to that of the kernel continue # check if this symbol is actually a type, e.g. "mV" in the expression "(1 + 2) * mV" symbol2 = var.get_scope().resolve_to_symbol( var.get_complete_name(), SymbolKind.TYPE) if symbol2 is not None: continue # symbol is a type symbol code, message = Messages.get_variable_not_defined( var.get_complete_name()) Logger.log_message( code=code, message=message, error_position=node.get_source_position(), log_level=LoggingLevel.ERROR, node=node) return # check if it is part of an invariant # if it is the case, there is no "recursive" declaration # so check if the parent is a declaration and the expression the invariant if isinstance( expr_par, ASTDeclaration) and expr_par.get_invariant() == expr: # in this case its ok if it is recursive or defined later on continue # check if it has been defined before usage, except for predefined symbols, input ports and variables added by the AST transformation functions if (not symbol.is_predefined) \ and symbol.block_type != BlockType.INPUT \ and not symbol.get_referenced_object().get_source_position().is_added_source_position(): # except for parameters, those can be defined after if ((not symbol.get_referenced_object( ).get_source_position().before(var.get_source_position())) and (not symbol.block_type in [ BlockType.PARAMETERS, BlockType.INTERNALS, BlockType.STATE ])): code, message = Messages.get_variable_used_before_declaration( var.get_name()) Logger.log_message( node=node, message=message, error_position=var.get_source_position(), code=code, log_level=LoggingLevel.ERROR) # now check that they are not defined recursively, e.g. V_m mV = V_m + 1 # todo: we should not check this for invariants if (symbol.get_referenced_object().get_source_position( ).encloses(var.get_source_position()) and not symbol.get_referenced_object(). get_source_position().is_added_source_position()): code, message = Messages.get_variable_defined_recursively( var.get_name()) Logger.log_message( code=code, message=message, error_position=symbol.get_referenced_object( ).get_source_position(), log_level=LoggingLevel.ERROR, node=node)