diff --git a/pynestml/cocos/co_co_nest_random_functions_legally_used.py b/pynestml/cocos/co_co_nest_random_functions_legally_used.py new file mode 100644 index 000000000..81e2fc464 --- /dev/null +++ b/pynestml/cocos/co_co_nest_random_functions_legally_used.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# co_co_nest_random_functions_legally_used.py +# +# This file is part of NEST. +# +# Copyright (C) 2004 The NEST Initiative +# +# NEST is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# NEST is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NEST. If not, see . + +from pynestml.cocos.co_co import CoCo +from pynestml.meta_model.ast_model import ASTModel +from pynestml.meta_model.ast_node import ASTNode +from pynestml.meta_model.ast_on_condition_block import ASTOnConditionBlock +from pynestml.meta_model.ast_on_receive_block import ASTOnReceiveBlock +from pynestml.meta_model.ast_update_block import ASTUpdateBlock +from pynestml.symbols.predefined_functions import PredefinedFunctions +from pynestml.utils.logger import LoggingLevel, Logger +from pynestml.utils.messages import Messages +from pynestml.visitors.ast_visitor import ASTVisitor + + +class CoCoNestRandomFunctionsLegallyUsed(CoCo): + """ + This CoCo ensure that the random functions are used only in the ``update``, ``onReceive``, and ``onCondition`` blocks. + This CoCo is only checked for the NEST Simulator target. + """ + + @classmethod + def check_co_co(cls, node: ASTNode): + """ + Checks the coco. + :param node: a single node (typically, a neuron or synapse) + """ + visitor = CoCoNestRandomFunctionsLegallyUsedVisitor() + visitor.neuron = node + node.accept(visitor) + + +class CoCoNestRandomFunctionsLegallyUsedVisitor(ASTVisitor): + def visit_function_call(self, node): + """ + Visits a function call + :param node: a function call + """ + function_name = node.get_name() + if function_name == PredefinedFunctions.RANDOM_NORMAL or function_name == PredefinedFunctions.RANDOM_UNIFORM \ + or function_name == PredefinedFunctions.RANDOM_POISSON: + parent = node + while parent: + parent = parent.get_parent() + + if isinstance(parent, ASTUpdateBlock) or isinstance(parent, ASTOnReceiveBlock) \ + or isinstance(parent, ASTOnConditionBlock): + # the random function is correctly defined, hence return + return + + if isinstance(parent, ASTModel): + # the random function is defined in other blocks (parameters, state, internals). Hence, an error. + code, message = Messages.get_random_functions_legally_used(function_name) + Logger.log_message(node=self.neuron, code=code, message=message, error_position=node.get_source_position(), + log_level=LoggingLevel.ERROR) diff --git a/pynestml/cocos/co_cos_manager.py b/pynestml/cocos/co_cos_manager.py index 1b7e52aba..54d8468d0 100644 --- a/pynestml/cocos/co_cos_manager.py +++ b/pynestml/cocos/co_cos_manager.py @@ -28,7 +28,6 @@ from pynestml.cocos.co_co_cm_synapse_model import CoCoCmSynapseModel from pynestml.cocos.co_co_convolve_has_correct_parameter import CoCoConvolveHasCorrectParameter from pynestml.cocos.co_co_convolve_cond_correctly_built import CoCoConvolveCondCorrectlyBuilt -from pynestml.cocos.co_co_convolve_has_correct_parameter import CoCoConvolveHasCorrectParameter from pynestml.cocos.co_co_correct_numerator_of_unit import CoCoCorrectNumeratorOfUnit from pynestml.cocos.co_co_correct_order_in_equation import CoCoCorrectOrderInEquation from pynestml.cocos.co_co_each_block_defined_at_most_once import CoCoEachBlockDefinedAtMostOnce @@ -49,6 +48,7 @@ from pynestml.cocos.co_co_invariant_is_boolean import CoCoInvariantIsBoolean from pynestml.cocos.co_co_kernel_type import CoCoKernelType from pynestml.cocos.co_co_model_name_unique import CoCoModelNameUnique +from pynestml.cocos.co_co_nest_random_functions_legally_used import CoCoNestRandomFunctionsLegallyUsed from pynestml.cocos.co_co_no_kernels_except_in_convolve import CoCoNoKernelsExceptInConvolve from pynestml.cocos.co_co_no_nest_name_space_collision import CoCoNoNestNameSpaceCollision from pynestml.cocos.co_co_no_duplicate_compilation_unit_names import CoCoNoDuplicateCompilationUnitNames @@ -418,6 +418,14 @@ def check_input_port_size_type(cls, model: ASTModel): """ CoCoVectorInputPortsCorrectSizeType.check_co_co(model) + @classmethod + def check_co_co_nest_random_functions_legally_used(cls, model: ASTModel): + """ + Checks if the random number functions are used only in the update block. + :param model: a single model object. + """ + CoCoNestRandomFunctionsLegallyUsed.check_co_co(model) + @classmethod def post_symbol_table_builder_checks(cls, model: ASTModel, after_ast_rewrite: bool = False): """ diff --git a/pynestml/codegeneration/nest_code_generator.py b/pynestml/codegeneration/nest_code_generator.py index 0551e9a6e..66e0c9e13 100644 --- a/pynestml/codegeneration/nest_code_generator.py +++ b/pynestml/codegeneration/nest_code_generator.py @@ -28,6 +28,7 @@ import pynestml from pynestml.cocos.co_co_nest_synapse_delay_not_assigned_to import CoCoNESTSynapseDelayNotAssignedTo +from pynestml.cocos.co_cos_manager import CoCosManager from pynestml.codegeneration.code_generator import CodeGenerator from pynestml.codegeneration.code_generator_utils import CodeGeneratorUtils from pynestml.codegeneration.nest_assignments_helper import NestAssignmentsHelper @@ -172,21 +173,30 @@ def __init__(self, options: Optional[Mapping[str, Any]] = None): self.setup_printers() def run_nest_target_specific_cocos(self, neurons: Sequence[ASTModel], synapses: Sequence[ASTModel]): - for synapse in synapses: - synapse_name_stripped = removesuffix(removesuffix(synapse.name.split("_with_")[0], "_"), FrontendConfiguration.suffix) + for model in neurons + synapses: + # Check if the random number functions are used in the right blocks + CoCosManager.check_co_co_nest_random_functions_legally_used(model) + + if Logger.has_errors(model): + raise Exception("Error(s) occurred during code generation") + + if self.get_option("neuron_synapse_pairs"): + for model in synapses: + synapse_name_stripped = removesuffix(removesuffix(model.name.split("_with_")[0], "_"), + FrontendConfiguration.suffix) + # special case for NEST delay variable (state or parameter) + assert synapse_name_stripped in self.get_option("delay_variable").keys(), "Please specify a delay variable for synapse '" + synapse_name_stripped + "' in the code generator options (see https://nestml.readthedocs.io/en/latest/running/running_nest.html#dendritic-delay-and-synaptic-weight)" + assert ASTUtils.get_variable_by_name(model, self.get_option("delay_variable")[synapse_name_stripped]), "Delay variable '" + self.get_option("delay_variable")[synapse_name_stripped] + "' not found in synapse '" + synapse_name_stripped + "' (see https://nestml.readthedocs.io/en/latest/running/running_nest.html#dendritic-delay-and-synaptic-weight)" - # special case for NEST delay variable (state or parameter) - assert synapse_name_stripped in self.get_option("delay_variable").keys(), "Please specify a delay variable for synapse '" + synapse_name_stripped + "' in the code generator options (see https://nestml.readthedocs.io/en/latest/running/running_nest.html#dendritic-delay-and-synaptic-weight)" - assert ASTUtils.get_variable_by_name(synapse, self.get_option("delay_variable")[synapse_name_stripped]), "Delay variable '" + self.get_option("delay_variable")[synapse_name_stripped] + "' not found in synapse '" + synapse_name_stripped + "' (see https://nestml.readthedocs.io/en/latest/running/running_nest.html#dendritic-delay-and-synaptic-weight)" + # special case for NEST weight variable (state or parameter) + assert synapse_name_stripped in self.get_option("weight_variable").keys(), "Please specify a weight variable for synapse '" + synapse_name_stripped + "' in the code generator options (see https://nestml.readthedocs.io/en/latest/running/running_nest.html#dendritic-delay-and-synaptic-weight)" + assert ASTUtils.get_variable_by_name(model, self.get_option("weight_variable")[synapse_name_stripped]), "Weight variable '" + self.get_option("weight_variable")[synapse_name_stripped] + "' not found in synapse '" + synapse_name_stripped + "' (see https://nestml.readthedocs.io/en/latest/running/running_nest.html#dendritic-delay-and-synaptic-weight)" - # special case for NEST weight variable (state or parameter) - assert synapse_name_stripped in self.get_option("weight_variable").keys(), "Please specify a weight variable for synapse '" + synapse_name_stripped + "' in the code generator options (see https://nestml.readthedocs.io/en/latest/running/running_nest.html#dendritic-delay-and-synaptic-weight)" - assert ASTUtils.get_variable_by_name(synapse, self.get_option("weight_variable")[synapse_name_stripped]), "Weight variable '" + self.get_option("weight_variable")[synapse_name_stripped] + "' not found in synapse '" + synapse_name_stripped + "' (see https://nestml.readthedocs.io/en/latest/running/running_nest.html#dendritic-delay-and-synaptic-weight)" + if self.option_exists("delay_variable") and synapse_name_stripped in self.get_option("delay_variable").keys(): + delay_variable = self.get_option("delay_variable")[synapse_name_stripped] + CoCoNESTSynapseDelayNotAssignedTo.check_co_co(delay_variable, model) - if self.option_exists("delay_variable") and synapse_name_stripped in self.get_option("delay_variable").keys(): - delay_variable = self.get_option("delay_variable")[synapse_name_stripped] - CoCoNESTSynapseDelayNotAssignedTo.check_co_co(delay_variable, synapse) - if Logger.has_errors(synapse): + if Logger.has_errors(model): raise Exception("Error(s) occurred during code generation") def setup_printers(self): diff --git a/pynestml/frontend/pynestml_frontend.py b/pynestml/frontend/pynestml_frontend.py index c257822de..59b9bb5f8 100644 --- a/pynestml/frontend/pynestml_frontend.py +++ b/pynestml/frontend/pynestml_frontend.py @@ -464,19 +464,22 @@ def process(): Flag indicating whether errors occurred during processing """ - # initialize and set options for transformers, code generator and builder - codegen_and_builder_opts = FrontendConfiguration.get_codegen_opts() - - transformers, codegen_and_builder_opts = transformers_from_target_name(FrontendConfiguration.get_target_platform(), - options=codegen_and_builder_opts) + # initialise model transformers + transformers, unused_opts_transformer = transformers_from_target_name(FrontendConfiguration.get_target_platform(), + options=FrontendConfiguration.get_codegen_opts()) + # initialise code generator code_generator = code_generator_from_target_name(FrontendConfiguration.get_target_platform()) - codegen_and_builder_opts = code_generator.set_options(codegen_and_builder_opts) + unused_opts_codegen = code_generator.set_options(FrontendConfiguration.get_codegen_opts()) - _builder, codegen_and_builder_opts = builder_from_target_name(FrontendConfiguration.get_target_platform(), options=codegen_and_builder_opts) + # initialise builder + _builder, unused_opts_builder = builder_from_target_name(FrontendConfiguration.get_target_platform(), + options=FrontendConfiguration.get_codegen_opts()) - if len(codegen_and_builder_opts) > 0: - raise CodeGeneratorOptionsException("The code generator option(s) \"" + ", ".join(codegen_and_builder_opts.keys()) + "\" do not exist.") + # check for unused codegen options + for opt_key in FrontendConfiguration.get_codegen_opts().keys(): + if opt_key in unused_opts_transformer.keys() and opt_key in unused_opts_codegen.keys() and opt_key in unused_opts_builder.keys(): + raise CodeGeneratorOptionsException("The code generator option \"" + opt_key + "\" does not exist.") models, errors_occurred = get_parsed_models() diff --git a/pynestml/utils/messages.py b/pynestml/utils/messages.py index 22601566c..36e783260 100644 --- a/pynestml/utils/messages.py +++ b/pynestml/utils/messages.py @@ -133,6 +133,7 @@ class MessageCode(Enum): NON_CONSTANT_EXPONENT = 111 RESOLUTION_FUNC_USED = 112 TIMESTEP_FUNCTION_LEGALLY_USED = 113 + RANDOM_FUNCTIONS_LEGALLY_USED = 113 EXPONENT_MUST_BE_INTEGER = 114 @@ -1386,3 +1387,8 @@ def get_non_constant_exponent(cls) -> Tuple[MessageCode, str]: message = "Cannot calculate value of exponent. Must be a constant value!" return MessageCode.NON_CONSTANT_EXPONENT, message + + @classmethod + def get_random_functions_legally_used(cls, name): + message = "The function '" + name + "' can only be used in the update, onReceive, or onCondition blocks." + return MessageCode.RANDOM_FUNCTIONS_LEGALLY_USED, message diff --git a/tests/nest_tests/nest_random_functions_test.py b/tests/nest_tests/nest_random_functions_test.py new file mode 100644 index 000000000..3d24ed963 --- /dev/null +++ b/tests/nest_tests/nest_random_functions_test.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# nest_random_functions_test.py +# +# This file is part of NEST. +# +# Copyright (C) 2004 The NEST Initiative +# +# NEST is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# NEST is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NEST. If not, see . +import os + +import pytest + +from pynestml.frontend.pynestml_frontend import generate_nest_target + + +class TestNestRandomFunctions: + """ + Tests that, for the NEST target, random number functions are called only in ``update``, ``onReceive``, and ``onCondition`` block + """ + + @pytest.mark.xfail(strict=True, raises=Exception) + def test_nest_random_function_neuron_illegal(self): + input_path = os.path.realpath(os.path.join(os.path.dirname(__file__), + "resources", "random_functions_illegal_neuron.nestml")) + generate_nest_target(input_path=input_path, + target_path="target", + logging_level="INFO", + suffix="_nestml") + + @pytest.mark.xfail(strict=True, raises=Exception) + def test_nest_random_function_synapse_illegal(self): + input_path = [ + os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "models", "neurons", + "iaf_psc_exp_neuron.nestml")), + os.path.realpath(os.path.join(os.path.dirname(__file__), + "resources", "random_functions_illegal_synapse.nestml"))] + + generate_nest_target(input_path=input_path, + target_path="target", + logging_level="INFO", + suffix="_nestml", + codegen_opts={"neuron_synapse_pairs": [{"neuron": "iaf_psc_exp_neuron", + "synapse": "random_functions_illegal_synapse", + "post_ports": ["post_spikes"]}], + "weight_variable": {"stdp_synapse": "w"}}) diff --git a/tests/nest_tests/resources/random_functions_illegal_neuron.nestml b/tests/nest_tests/resources/random_functions_illegal_neuron.nestml new file mode 100644 index 000000000..9954459d3 --- /dev/null +++ b/tests/nest_tests/resources/random_functions_illegal_neuron.nestml @@ -0,0 +1,46 @@ +""" +random_functions_illegal_neuron.nestml +###################################### + + +Copyright statement ++++++++++++++++++++ + +This file is part of NEST. + +Copyright (C) 2004 The NEST Initiative + +NEST is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +NEST is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with NEST. If not, see . +""" + +model random_functions_illegal_neuron: + state: + noise real = random_normal(0,sigma_noise) + v mV = -15 mV + + parameters: + rate ms**-1 = 15.5 s**-1 + sigma_noise real = 16. + u real = random_uniform(0,1) + + internals: + poisson_input integer = random_poisson(rate * resolution() * 1E-3) + + update: + if u < 0.5: + noise = 0. + else: + noise = random_normal(0,sigma_noise) + + v += (poisson_input + noise) * mV diff --git a/tests/nest_tests/resources/random_functions_illegal_synapse.nestml b/tests/nest_tests/resources/random_functions_illegal_synapse.nestml new file mode 100644 index 000000000..473791ec7 --- /dev/null +++ b/tests/nest_tests/resources/random_functions_illegal_synapse.nestml @@ -0,0 +1,73 @@ +""" +random_functions_illegal_synapse.nestml +####################################### + + +Copyright statement ++++++++++++++++++++ + +This file is part of NEST. + +Copyright (C) 2004 The NEST Initiative + +NEST is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +NEST is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with NEST. If not, see . +""" + +model random_functions_illegal_synapse: + state: + w real = 1 # Synaptic weight + pre_trace real = 0. + post_trace real = 0. + + parameters: + d ms = 1 ms # Synaptic transmission delay + lambda real = .01 + tau_tr_pre ms = random_normal(110 ms, 55 ms) + tau_tr_post ms = random_normal(5 ms, 2.5 ms) + alpha real = 1 + mu_plus real = 1 + mu_minus real = 1 + Wmax real = 100. + Wmin real = 0. + + equations: + pre_trace' = -pre_trace / tau_tr_pre + post_trace' = -post_trace / tau_tr_post + + input: + pre_spikes <- spike + post_spikes <- spike + + output: + spike + + onReceive(post_spikes): + post_trace += 1 + + # potentiate synapse + w_ real = Wmax * ( w / Wmax + (lambda * ( 1. - ( w / Wmax ) )**mu_plus * pre_trace )) + w = min(Wmax, w_) + + onReceive(pre_spikes): + pre_trace += 1 + + # depress synapse + w_ real = Wmax * ( w / Wmax - ( alpha * lambda * ( w / Wmax )**mu_minus * post_trace )) + w = max(Wmin, w_) + + # deliver spike to postsynaptic partner + emit_spike(w, d) + + update: + integrate_odes()