From 4320b0ce425a7f2cfb93a1ee5d076115d565534e Mon Sep 17 00:00:00 2001 From: "C.A.P. Linssen" Date: Thu, 16 Jan 2025 20:37:42 +0100 Subject: [PATCH] add attributes to spiking input ports --- models/neurons/iaf_psc_delta_neuron.nestml | 4 +- .../codegeneration/nest_code_generator.py | 6 +- .../point_neuron/common/NeuronClass.jinja2 | 161 +++++++++++++++++- .../point_neuron/common/NeuronHeader.jinja2 | 33 +++- tests/nest_tests/recordable_variables_test.py | 2 +- .../resources/RecordableVariables.nestml | 6 +- ..._time_invariant_input_port_optimisation.py | 130 ++++++++++++++ tests/nest_tests/test_multisynapse.py | 3 +- 8 files changed, 328 insertions(+), 17 deletions(-) create mode 100644 tests/nest_tests/test_linear_time_invariant_input_port_optimisation.py diff --git a/models/neurons/iaf_psc_delta_neuron.nestml b/models/neurons/iaf_psc_delta_neuron.nestml index b17c852b3..e48cbba85 100644 --- a/models/neurons/iaf_psc_delta_neuron.nestml +++ b/models/neurons/iaf_psc_delta_neuron.nestml @@ -47,7 +47,7 @@ model iaf_psc_delta_neuron: equations: kernel K_delta = delta(t) - V_m' = -(V_m - E_L) / tau_m + convolve(K_delta, spikes.weight) / s + (I_e + I_stim) / C_m # XXX: TODO: instead of the convolution, this should just read ``... + spikes.weight + ...``. This is a known issue (see https://github.com/nest/nestml/pull/1050). + V_m' = -(V_m - E_L) / tau_m + convolve(K_delta, spike_in_port.weight) / s + (I_e + I_stim) / C_m # XXX: TODO: instead of the convolution, this should just read ``... + spike_in_port.weight + ...``. This is a known issue (see https://github.com/nest/nestml/pull/1050). refr_t' = -1e3 * ms/s # refractoriness is implemented as an ODE, representing a timer counting back down to zero. XXX: TODO: This should simply read ``refr_t' = -1 / s`` (see https://github.com/nest/nestml/issues/984) parameters: @@ -64,7 +64,7 @@ model iaf_psc_delta_neuron: I_e pA = 0 pA input: - spikes <- spike(weight mV) + spike_in_port <- spike(weight mV) I_stim pA <- continuous output: diff --git a/pynestml/codegeneration/nest_code_generator.py b/pynestml/codegeneration/nest_code_generator.py index fd6d4531e..ee667f38b 100644 --- a/pynestml/codegeneration/nest_code_generator.py +++ b/pynestml/codegeneration/nest_code_generator.py @@ -118,6 +118,7 @@ class NESTCodeGenerator(CodeGenerator): - **continuous_state_buffering_method**: Which method to use for buffering state variables between neuron and synapse pairs. When a synapse has a "continuous" input port, connected to a postsynaptic neuron, either the value is obtained taking the synaptic (dendritic, that is, synapse-soma) delay into account, requiring a buffer to store the value at each timepoint (``continuous_state_buffering_method = "continuous_time_buffer"); or the value is obtained at the times of the somatic spikes of the postsynaptic neuron, ignoring the synaptic delay (``continuous_state_buffering_method == "post_spike_based"``). The former is more physically accurate but requires a large buffer and can require a long time to simulate. The latter ignores the dendritic delay but is much more computationally efficient. - **delay_variable**: A mapping identifying, for each synapse (the name of which is given as a key), the variable or parameter in the model that corresponds with the NEST ``Connection`` class delay property. - **weight_variable**: Like ``delay_variable``, but for synaptic weight. + - **linear_time_invariant_spiking_input_ports**: A list of spiking input ports which can be treated as linear and time-invariant; this implies that, for the given port(s), the weight of all spikes received within a timestep can be added together, improving memory consumption and runtime performance. Use with caution, for example, this is not compatible with using an input port as one processing inhibitory vs. excitatory spikes depending on the sign of the weight of the spike event. - **redirect_build_output**: An optional boolean key for redirecting the build output. Setting the key to ``True``, two files will be created for redirecting the ``stdout`` and the ``stderr`. The ``target_path`` will be used as the default location for creating the two files. - **build_output_dir**: An optional string key representing the new path where the files corresponding to the output of the build phase will be created. This key requires that the ``redirect_build_output`` is set to ``True``. @@ -150,7 +151,8 @@ class NESTCodeGenerator(CodeGenerator): "numeric_solver": "rk45", "continuous_state_buffering_method": "continuous_time_buffer", "delay_variable": {}, - "weight_variable": {} + "weight_variable": {}, + "linear_time_invariant_spiking_input_ports": [] } def __init__(self, options: Optional[Mapping[str, Any]] = None): @@ -531,6 +533,8 @@ def _get_model_namespace(self, astnode: ASTModel) -> Dict: if "continuous_post_ports" in dir(astnode): namespace["continuous_post_ports"] = astnode.continuous_post_ports + namespace["linear_time_invariant_spiking_input_ports"] = self.get_option("linear_time_invariant_spiking_input_ports") + return namespace def _get_synapse_model_namespace(self, synapse: ASTModel) -> Dict: diff --git a/pynestml/codegeneration/resources_nest/point_neuron/common/NeuronClass.jinja2 b/pynestml/codegeneration/resources_nest/point_neuron/common/NeuronClass.jinja2 index f4682e567..3bcaec42a 100644 --- a/pynestml/codegeneration/resources_nest/point_neuron/common/NeuronClass.jinja2 +++ b/pynestml/codegeneration/resources_nest/point_neuron/common/NeuronClass.jinja2 @@ -201,19 +201,37 @@ namespace nest {%- for i in range(size) %} {%- if inputPort.get_parameters() %} {%- for parameter in inputPort.get_parameters() %} +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + , spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_( nest::RingBuffer() ) + , spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_grid_sum_( 0. ) +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} , spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_( nest::ListRingBuffer() ) - // , spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_grid_sum_( 0. ) +{%- endif %} {%- endfor %} {%- endif %} {%- endfor %} {%- else %} {%- for parameter in inputPort.get_parameters() %} +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + , spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_( nest::RingBuffer() ) + , spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_grid_sum_( 0. ) +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} , spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_( nest::ListRingBuffer() ) - //, spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_grid_sum_( 0. ) +{%- endif %} {%- endfor %} +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + , spike_input_{{ inputPort.name }}_( nest::RingBuffer() ) + , spike_input_{{ inputPort.name }}_grid_sum_( 0. ) +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} , spike_input_{{ inputPort.name }}_( nest::ListRingBuffer() ) +{%- endif %} , spike_input_{{ inputPort.name }}_spike_input_received_( nest::RingBuffer() ) - //, spike_input_{{ inputPort.name }}_spike_input_received_grid_sum_( 0. ) {%- endif %} {%- endfor %} {%- endif %} @@ -236,8 +254,15 @@ namespace nest {%- for i in range(size) %} {%- if inputPort.get_parameters() %} {%- for parameter in inputPort.get_parameters() %} + +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + , spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_( nest::RingBuffer() ) + , spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_grid_sum_( 0. ) +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} , spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_( nest::ListRingBuffer() ) - //, spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_grid_sum_( 0. ) +{%- endif %} {%- endfor %} {%- else %} ????????????? @@ -245,12 +270,27 @@ namespace nest {%- endfor %} {%- else %} {%- for parameter in inputPort.get_parameters() %} + + + +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + , spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_( nest::RingBuffer() ) + , spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_grid_sum_( 0. ) +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} , spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_( nest::ListRingBuffer() ) - //, spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_grid_sum_( 0. ) +{%- endif %} {%- endfor %} +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + , spike_input_{{ inputPort.name }}_( nest::RingBuffer() ) + , spike_input_{{ inputPort.name }}_grid_sum_( 0. ) +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} , spike_input_{{ inputPort.name }}_( nest::ListRingBuffer() ) +{%- endif %} , spike_input_{{ inputPort.name }}_spike_input_received_( nest::RingBuffer() ) - //, spike_input_{{ inputPort.name }}_spike_input_received_grid_sum_( 0. ) {%- endif %} {%- endfor %} {%- endif %} @@ -773,19 +813,62 @@ void {{ neuronName }}::update(nest::Time const & origin, const long from, const {%- for inputPortSymbol in neuron.get_spike_input_ports() %} {%- set inputPort = utils.get_input_port_by_name(astnode.get_input_blocks(), inputPortSymbol.name.split(".")[0]) %} + + + {%- if inputPortSymbol.has_vector_parameter() %} {%- set size = utils.get_numeric_vector_size(inputPortSymbol) %} {%- for i in range(size) %} {%- if inputPort.get_parameters() %} {%- for parameter in inputPort.get_parameters() %} - const double __spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }} = std::accumulate(B_.spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_.get_list(lag).begin(), spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_.get_list(lag).end(), 0.0); + + +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + B_.spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_grid_sum_ = B_.spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_.get_value(lag); + const double __spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }} = B_.spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_grid_sum_; +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} + const double __spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }} = std::accumulate(B_.spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_.get_list(lag).begin(), B_.spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_.get_list(lag).end(), 0.0); +{%- endif %} + + + + {%- endfor %} {%- endif %} {%- endfor %} {%- else %} {%- for parameter in inputPort.get_parameters() %} + + +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + B_.spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_grid_sum_ = B_.spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_.get_value(lag); + const double __spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }} = B_.spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_grid_sum_; +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} const double __spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }} = std::accumulate(B_.spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_.get_list(lag).begin(), B_.spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_.get_list(lag).end(), 0.0); +{%- endif %} + + + + + {%- endfor %} + +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + B_.spike_input_{{ inputPort.name }}_grid_sum_ = B_.spike_input_{{ inputPort.name }}_.get_value(lag); + const double __spike_input_{{ inputPort.name }} = B_.spike_input_{{ inputPort.name }}_grid_sum_; +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} + const double __spike_input_{{ inputPort.name }} = std::accumulate(B_.spike_input_{{ inputPort.name }}_.get_list(lag).begin(), B_.spike_input_{{ inputPort.name }}_.get_list(lag).end(), 0.0); +{%- endif %} + + + + {%- endif %} {%- endfor %} @@ -948,6 +1031,10 @@ void {{ neuronName }}::update(nest::Time const & origin, const long from, const {%- for inputPortSymbol in neuron.get_spike_input_ports() %} {%- set inputPort = utils.get_input_port_by_name(astnode.get_input_blocks(), inputPortSymbol.name.split(".")[0]) %} + +{%- if inputPortSymbol.name not in linear_time_invariant_spiking_input_ports %} + + {%- if inputPortSymbol.has_vector_parameter() %} {%- set size = utils.get_numeric_vector_size(inputPortSymbol) %} {%- for i in range(size) %} @@ -963,10 +1050,13 @@ void {{ neuronName }}::update(nest::Time const & origin, const long from, const std::list< double >& __spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_list = B_.spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_.get_list(lag); __spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_list.clear(); {%- endfor %} -{%- endif %} std::list< double >& __spike_input_{{ inputPort.name }}_list = B_.spike_input_{{ inputPort.name }}_.get_list(lag); __spike_input_{{ inputPort.name }}_list.clear(); + +{%- endif %} +{%- endif %} + {%- endfor %} {%- endif %} @@ -1235,9 +1325,21 @@ void {{ neuronName }}::handle(nest::SpikeEvent &e) { {%- if spike_in_port.get_parameters() %} {%- for attribute in spike_in_port.get_parameters() %} + + +{%- if spike_in_port_name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + B_.spike_input_{{ spike_in_port_name }}__DOT__{{ attribute.name }}_.add_value( + e.get_rel_delivery_steps( nest::kernel().simulation_manager.get_slice_origin() ), + e.get_weight() * e.get_multiplicity() ); +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} B_.spike_input_{{ spike_in_port_name }}__DOT__{{ attribute.name }}_.append_value( e.get_rel_delivery_steps( nest::kernel().simulation_manager.get_slice_origin() ), e.get_weight() * e.get_multiplicity() ); +{%- endif %} + + {%- endfor %} {%- endif %} @@ -1245,9 +1347,18 @@ void {{ neuronName }}::handle(nest::SpikeEvent &e) //std::cout << "\tappending spike at offset = " << e.get_rel_delivery_steps( nest::kernel().simulation_manager.get_slice_origin()) << "; buffer size = " << B_.spike_input_{{ spike_in_port_name }}_.size() << "; nest::kernel().connection_manager.get_min_delay() = " << nest::kernel().connection_manager.get_min_delay() << "\n"; //std::cout << "\tappending spike at offset = " << e.get_rel_delivery_steps( nest::kernel().simulation_manager.get_slice_origin()) << " to B_.spike_input_{{ spike_in_port_name }}_, before length = " << B_.spike_input_{{ spike_in_port_name }}_.get_list(e.get_rel_delivery_steps( nest::kernel().simulation_manager.get_slice_origin())).size() << "\n"; // B_.spike_input_{{ spike_in_port_name }}_.resize(); + +{%- if spike_in_port_name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + B_.spike_input_{{ spike_in_port_name }}_.add_value( + e.get_rel_delivery_steps( nest::kernel().simulation_manager.get_slice_origin() ), + e.get_multiplicity() ); +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} B_.spike_input_{{ spike_in_port_name }}_.append_value( e.get_rel_delivery_steps( nest::kernel().simulation_manager.get_slice_origin() ), e.get_multiplicity() ); +{%- endif %} //std::cout << "\tappending spike to B_.spike_input_{{ spike_in_port_name }}_, after length = " << B_.spike_input_{{ spike_in_port_name }}_.get_list(e.get_rel_delivery_steps( nest::kernel().simulation_manager.get_slice_origin())).size() << "\n"; @@ -1299,6 +1410,39 @@ void const double __timestep = nest::Time::get_resolution().get_ms(); // do not remove, this is necessary for the timestep() function auto get_t = [origin, lag](){ return nest::Time( nest::Time::step( origin.get_steps() + lag + 1) ).get_ms(); }; +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + /** + * Grab the actual spike event data from the buffers (for the current timepoint ``origin + lag``) + **/ +{%- if inputPortSymbol.has_vector_parameter() %} +{%- set size = utils.get_numeric_vector_size(inputPortSymbol) %} +{%- for i in range(size) %} +{%- if inputPort.get_parameters() %} +{%- for parameter in inputPort.get_parameters() %} + const double __spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }} = B_.spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_grid_sum_; +{%- endfor %} +{%- endif %} +{%- endfor %} +{%- else %} +{%- for parameter in inputPort.get_parameters() %} + const double __spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }} = B_.spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_grid_sum_; +{%- endfor %} +{%- endif %} + const double __spike_input_{{ inputPort.name }} = B_.spike_input_{{ inputPort.name }}_grid_sum_; + + + /** + * Begin NESTML generated code for the onReceive() block statements + **/ + +{{ printer._expression_printer._simple_expression_printer._variable_printer.set_cpp_variable_suffix(" ") }} {# prevent printing origin #} +{% filter indent(4, True) -%} +{%- include "directives_cpp/StmtsBody.jinja2" %} +{%- endfilter %} +{{ printer._expression_printer._simple_expression_printer._variable_printer.set_cpp_variable_suffix("") }} +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} // grab the lists of spike events from the buffers for the current timepoint {%- if inputPortSymbol.has_vector_parameter() %} {%- set size = utils.get_numeric_vector_size(inputPortSymbol) %} @@ -1398,6 +1542,7 @@ std::cout << "\tclearing spike buffers....\n"; std::cout << "\tafter clearing " << __spike_input_{{ inputPort.name }}_list.size() << " spikes\n"; std::cout << "\tafter clearing (orig list) " << B_.spike_input_{{ inputPort.name }}_.get_list(lag).size() << " spikes\n"; */ +{%- endif %} } {% endfor %} diff --git a/pynestml/codegeneration/resources_nest/point_neuron/common/NeuronHeader.jinja2 b/pynestml/codegeneration/resources_nest/point_neuron/common/NeuronHeader.jinja2 index edf934e87..0f9c5c77c 100644 --- a/pynestml/codegeneration/resources_nest/point_neuron/common/NeuronHeader.jinja2 +++ b/pynestml/codegeneration/resources_nest/point_neuron/common/NeuronHeader.jinja2 @@ -788,6 +788,36 @@ private: {%- set inputPort = utils.get_input_port_by_name(astnode.get_input_blocks(), inputPortSymbol.name.split(".")[0]) %} // input port: {{ inputPort.name }} +{%- if inputPortSymbol.name in linear_time_invariant_spiking_input_ports %} +{#- linear, time-invariant input port: all spike events for a specific buffer slot can be added together into a single number #} + +{%- if inputPortSymbol.has_vector_parameter() %} +{%- set size = utils.get_numeric_vector_size(inputPortSymbol) %} +{%- for i in range(size) %} +{%- if inputPort.get_parameters() %} +{%- for parameter in inputPort.get_parameters() %} + nest::RingBuffer spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_; + double spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}__DOT__{{ parameter.get_name() }}_grid_sum_; +{%- endfor %} +{%- endif %} + nest::RingBuffer spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}_; + double spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}_grid_sum_; + nest::RingBuffer spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}_spike_input_received_; + double spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}_spike_input_received_grid_sum_; +{%- endfor %} +{%- else %} +{%- for parameter in inputPort.get_parameters() %} + nest::RingBuffer spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_; + double spike_input_{{ inputPort.name }}__DOT__{{ parameter.get_name() }}_grid_sum_; +{%- endfor %} + nest::RingBuffer spike_input_{{ inputPort.name }}_; // buffer for unweighted spikes + double spike_input_{{ inputPort.name }}_grid_sum_; // buffer for unweighted spikes + nest::RingBuffer spike_input_{{ inputPort.name }}_spike_input_received_; // buffer for the "spike received" boolean flag + double spike_input_{{ inputPort.name }}_spike_input_received_grid_sum_; // buffer for the "spike received" boolean flag +{%- endif %} +{%- else %} +{#- generic input port: use lists of spike events for each buffer slot #} + {%- if inputPortSymbol.has_vector_parameter() %} {%- set size = utils.get_numeric_vector_size(inputPortSymbol) %} {%- for i in range(size) %} @@ -799,7 +829,7 @@ private: {%- endif %} nest::ListRingBuffer spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}_; double spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}_grid_sum_; - nest::ListRingBuffer spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}_spike_input_received_; + nest::RingBuffer spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}_spike_input_received_; double spike_input_{{ inputPort.name }}_VEC_IDX_{{ i }}_spike_input_received_grid_sum_; {%- endfor %} {%- else %} @@ -811,6 +841,7 @@ private: nest::RingBuffer spike_input_{{ inputPort.name }}_spike_input_received_; // buffer for the "spike received" boolean flag double spike_input_{{ inputPort.name }}_spike_input_received_grid_sum_; // buffer for the "spike received" boolean flag {%- endif %} +{%- endif %} {%- endfor %} // ----------------------------------------------------------------------- diff --git a/tests/nest_tests/recordable_variables_test.py b/tests/nest_tests/recordable_variables_test.py index b8c126d23..9b6f59966 100644 --- a/tests/nest_tests/recordable_variables_test.py +++ b/tests/nest_tests/recordable_variables_test.py @@ -62,7 +62,7 @@ def test_recordable_variables(self): sg = nest.Create("spike_generator", params={"spike_times": [20., 80.]}) nest.Connect(sg, neuron) - mm = nest.Create('multimeter', params={'record_from': ['V_ex', 'V_rel', 'V_m', 'I_kernel__X__spikes'], + mm = nest.Create('multimeter', params={'record_from': ['V_ex', 'V_rel', 'V_m', 'I_kernel__X__spike_in_port__DOT__weight'], 'interval': 0.1}) nest.Connect(mm, neuron) diff --git a/tests/nest_tests/resources/RecordableVariables.nestml b/tests/nest_tests/resources/RecordableVariables.nestml index d31496002..4be47c96f 100644 --- a/tests/nest_tests/resources/RecordableVariables.nestml +++ b/tests/nest_tests/resources/RecordableVariables.nestml @@ -34,8 +34,8 @@ model recordable_variables: V_rel mV = 0 mV # Membrane potential relative to the reset potential equations: - kernel I_kernel = exp(-1/tau_syn*t) - inline I_syn pA = convolve(I_kernel, spikes) * pA + kernel I_kernel = exp(-t / tau_syn) + inline I_syn pA = convolve(I_kernel, spike_in_port.weight) recordable inline V_m mV = V_rel + V_reset V_rel' = -V_rel / tau_m + (I_syn + I_e + I_stim) / C_m @@ -48,7 +48,7 @@ model recordable_variables: V_thr mV = -55 mV input: - spikes <- spike + spike_in_port <- spike(weight pA) I_stim pA <- continuous update: diff --git a/tests/nest_tests/test_linear_time_invariant_input_port_optimisation.py b/tests/nest_tests/test_linear_time_invariant_input_port_optimisation.py new file mode 100644 index 000000000..51fbf27e8 --- /dev/null +++ b/tests/nest_tests/test_linear_time_invariant_input_port_optimisation.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# +# test_linear_time_invariant_input_port_optimisation.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 typing import Optional + +import matplotlib.pyplot as plt +import numpy as np +import os +import pytest + +import nest + +from pynestml.frontend.pynestml_frontend import generate_nest_target + +TestLinearTimeInvariantInputPortOptimisation_neuron_types = ["aeif_cond_exp", "iaf_psc_delta"] + + +class TestLinearTimeInvariantInputPortOptimisation: + """ + Test that the optimisations with the ``linear_time_invariant_spiking_input_ports`` NEST code generator option are working correctly. + """ + + module_name: Optional[str] = None + + @pytest.fixture(scope="module", autouse=True) + def generate_code(self): + TestLinearTimeInvariantInputPortOptimisation.module_name = "nestmlmodule" # unfortunately, pytest only allows us to set static attributes on the class + input_path = [os.path.join(os.path.realpath(os.path.join(os.path.dirname(__file__), os.path.join(os.pardir, os.pardir, "models", "neurons", neuron_name + "_neuron.nestml")))) for neuron_name in TestLinearTimeInvariantInputPortOptimisation_neuron_types] + target_path = "nestmlmodule" + logging_level = "DEBUG" + suffix = "_nestml" + codegen_opts = {"linear_time_invariant_spiking_input_ports": ["spike_in_port"]} + + generate_nest_target(input_path, target_path, + module_name=TestLinearTimeInvariantInputPortOptimisation.module_name, + logging_level=logging_level, + suffix=suffix, + codegen_opts=codegen_opts) + + @pytest.mark.xfail + @pytest.mark.parametrize("neuron_name", TestLinearTimeInvariantInputPortOptimisation_neuron_types) + def test_simultaneous_spikes_different_ports(self, neuron_name: str): + r"""This is known to not work if there are simultaneous spikes!""" + spike_times_sg_exc = [10., 20., 30., 40., 50.] + spike_times_sg_exc2 = [40.] + spike_times_sg_inh = [20., 40.] + self.run_experiment(neuron_name, + spike_times_sg_exc, + spike_times_sg_exc2, + spike_times_sg_inh) + + @pytest.mark.xfail + @pytest.mark.parametrize("neuron_name", TestLinearTimeInvariantInputPortOptimisation_neuron_types) + def test_simultaneous_spikes_different_ports2(self, neuron_name: str): + r"""This is known to not work if there are simultaneous spikes!""" + spike_times_sg_exc = [10., 20., 30., 40., 50.] + spike_times_sg_exc2 = [0.] + spike_times_sg_inh = [20., 40.] + self.run_experiment(neuron_name, + spike_times_sg_exc, + spike_times_sg_exc2, + spike_times_sg_inh) + + @pytest.mark.parametrize("neuron_name", TestLinearTimeInvariantInputPortOptimisation_neuron_types) + def test_non_simultaneous_spikes_different_ports(self, neuron_name: str): + spike_times_sg_exc = [10., 20., 30., 40., 50.] + spike_times_sg_exc2 = [45.] + spike_times_sg_inh = [25., 55.] + self.run_experiment(neuron_name, + spike_times_sg_exc, + spike_times_sg_exc2, + spike_times_sg_inh) + + def run_experiment(self, neuron_name, spike_times_sg_exc, spike_times_sg_exc2, spike_times_sg_inh): + nest.ResetKernel() + nest.Install(TestLinearTimeInvariantInputPortOptimisation.module_name) + + sg_exc = nest.Create("spike_generator", {"spike_times": spike_times_sg_exc}) + sg_exc2 = nest.Create("spike_generator", {"spike_times": spike_times_sg_exc2}) + sg_inh = nest.Create("spike_generator", {"spike_times": spike_times_sg_inh}) + + neuron_nest = nest.Create(neuron_name) + neuron_nestml = nest.Create(neuron_name + "_neuron_nestml") + mm_nest = nest.Create("voltmeter") + mm_nestml = nest.Create("voltmeter") + + nest.Connect(sg_exc, neuron_nest) + nest.Connect(sg_exc, neuron_nestml) + nest.Connect(sg_exc2, neuron_nest) + nest.Connect(sg_exc2, neuron_nestml) + nest.Connect(sg_inh, neuron_nest, syn_spec={"weight": -1}) + nest.Connect(sg_inh, neuron_nestml, syn_spec={"weight": -1}) + + nest.Connect(mm_nest, neuron_nest) + nest.Connect(mm_nestml, neuron_nestml) + + nest.Simulate(60.) + + # plot the results + + fig, ax = plt.subplots(nrows=1) + + ax.plot(mm_nest.events["times"], mm_nest.events["V_m"], label="NEST") + ax.plot(mm_nestml.events["times"], mm_nestml.events["V_m"], label="NESTML") + ax.legend() + + fig.savefig("/tmp/test_simultaneous_spikes_different_ports_[neuron=" + neuron_name + "].png") + + # test that membrane potential is the same at the end of the simulation + + assert neuron_nestml.V_m != neuron_nestml.E_L + np.testing.assert_allclose(neuron_nest.V_m, neuron_nestml.V_m) diff --git a/tests/nest_tests/test_multisynapse.py b/tests/nest_tests/test_multisynapse.py index 3080b2260..add2b5fee 100644 --- a/tests/nest_tests/test_multisynapse.py +++ b/tests/nest_tests/test_multisynapse.py @@ -19,11 +19,12 @@ # You should have received a copy of the GNU General Public License # along with NEST. If not, see . -import nest import numpy as np import os import pytest +import nest + from pynestml.codegeneration.nest_tools import NESTTools from pynestml.frontend.pynestml_frontend import generate_nest_target