Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into resolution_to_timestep
Browse files Browse the repository at this point in the history
  • Loading branch information
C.A.P. Linssen committed Oct 8, 2024
2 parents 5e28c9f + 3867eb5 commit 7d0d242
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 22 deletions.
73 changes: 73 additions & 0 deletions pynestml/cocos/co_co_nest_random_functions_legally_used.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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)
10 changes: 9 additions & 1 deletion pynestml/cocos/co_cos_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down
34 changes: 22 additions & 12 deletions pynestml/codegeneration/nest_code_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
21 changes: 12 additions & 9 deletions pynestml/frontend/pynestml_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
6 changes: 6 additions & 0 deletions pynestml/utils/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
57 changes: 57 additions & 0 deletions tests/nest_tests/nest_random_functions_test.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
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"}})
46 changes: 46 additions & 0 deletions tests/nest_tests/resources/random_functions_illegal_neuron.nestml
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
"""

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
73 changes: 73 additions & 0 deletions tests/nest_tests/resources/random_functions_illegal_synapse.nestml
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
"""

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()

0 comments on commit 7d0d242

Please sign in to comment.