From 558cb0686087287a985a8e85f2aac25dcfec3ac6 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Thu, 30 Apr 2026 10:05:47 +0200 Subject: [PATCH 1/8] changes needed for NEST 3.10_rc1 --- pyNN/nest/cells.py | 5 +---- pyNN/nest/populations.py | 2 +- pyNN/nest/projections.py | 14 ++++++++++++-- pyNN/nest/simulator.py | 13 ++++++++++--- test/system/test_nest.py | 4 +++- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/pyNN/nest/cells.py b/pyNN/nest/cells.py index 8bcb6f34..a79db68e 100644 --- a/pyNN/nest/cells.py +++ b/pyNN/nest/cells.py @@ -53,10 +53,7 @@ def get_receptor_types(model_name): def get_recordables(model_name): - try: - return [name for name in nest.GetDefaults(model_name, "recordables")] - except nest.NESTError: - return [] + return list(nest.GetDefaults(model_name).get("recordables", [])) def native_cell_type(model_name): diff --git a/pyNN/nest/populations.py b/pyNN/nest/populations.py index 974aaba0..955ccdcc 100644 --- a/pyNN/nest/populations.py +++ b/pyNN/nest/populations.py @@ -282,7 +282,7 @@ def _set_initial_value_array(self, variable, value): simulator.state.set_status(self.node_collection[self._mask_local], variable, local_values) except nest.NESTError as e: - if "Unused dictionary items" in e.args[0]: + if "Unused dictionary items" in e.args[0] or "Unaccessed" in e.args[0]: logger.warning("NEST does not allow setting an initial value for %s" % variable) # should perhaps check whether value-to-be-set is the same as current value, # and raise an Exception if not, rather than just emit a warning. diff --git a/pyNN/nest/projections.py b/pyNN/nest/projections.py index 2b69b7f4..fef10a5d 100644 --- a/pyNN/nest/projections.py +++ b/pyNN/nest/projections.py @@ -448,7 +448,10 @@ def _get_attributes_as_list(self, names): else: nest_names.append(name) values = nest.GetStatus(self.nest_connections, nest_names) - values = np.array(values) # ought to preserve int type for source, target + if isinstance(values, dict): # NEST 3.10_rc1 + values = np.array([values[key] for key in nest_names]).T + else: + values = np.array(values) # ought to preserve int type for source, target if 'weight' in names: # other attributes could also have scale factors - need to use translation mechanisms scale_factors = np.ones(len(names)) @@ -477,7 +480,14 @@ def _get_attributes_as_arrays(self, names, multiple_synapses='sum'): value_arr = np.nan * np.ones((self.pre.size, self.post.size)) connection_attributes = nest.GetStatus(self.nest_connections, ('source', 'target', attribute_name)) - for conn in connection_attributes: + # NEST 3.10 returns a dict {key: [values]}; earlier versions return a list of tuples + if isinstance(connection_attributes, dict): + conn_iter = zip(connection_attributes['source'], + connection_attributes['target'], + connection_attributes[attribute_name]) + else: + conn_iter = connection_attributes + for conn in conn_iter: # (offset is always 0,0 for connections created with connect()) src, tgt, value = conn addr = self.pre.id_to_index(src), self.post.id_to_index(tgt) diff --git a/pyNN/nest/simulator.py b/pyNN/nest/simulator.py index 43c2d833..2cb83a5f 100644 --- a/pyNN/nest/simulator.py +++ b/pyNN/nest/simulator.py @@ -214,7 +214,11 @@ def _set_spike_precision(self, precision): spike_precision = property(fget=_get_spike_precision, fset=_set_spike_precision) def _set_verbosity(self, verbosity): - nest.set_verbosity('M_{}'.format(verbosity.upper())) + try: + nest.set_verbosity('M_{}'.format(verbosity.upper())) + except AttributeError: + vb_level = getattr(nest.VerbosityLevel, verbosity.upper()) + nest.verbosity = vb_level verbosity = property(fset=_set_verbosity) def set_status(self, nodes, params, val=None): @@ -301,8 +305,11 @@ def clear(self): self.current_sources = [] self.recording_devices = [] self.recorders = set() - # clear the sli stack, if this is not done --> memory leak cause the stack increases - nest.ll_api.sr('clear') + try: + # clear the sli stack, if this is not done --> memory leak cause the stack increases + nest.ll_api.sr('clear') + except AttributeError: # NEST 3.10+ + pass # reset the simulation kernel nest.ResetKernel() # but this reverts some of the PyNN settings, so we have to repeat them (see NEST #716) diff --git a/test/system/test_nest.py b/test/system/test_nest.py index 53f978dd..7b01bfd0 100644 --- a/test/system/test_nest.py +++ b/test/system/test_nest.py @@ -25,7 +25,9 @@ def test_record_native_model(): parameters = {'tau_m': 17.0} n_cells = 10 p1 = nest.Population(n_cells, nest.native_cell_type("ht_neuron")(**parameters)) - p1.initialize(V_m=-70.0, Theta=-50.0) + # 'Theta' was renamed to 'theta' in NEST 3.10 + theta_key = 'theta' if 'theta' in _nest.GetDefaults('ht_neuron') else 'Theta' + p1.initialize(V_m=-70.0, **{theta_key: -50.0}) p1.set(theta_eq=-51.5) #assert_array_equal(p1.get('theta_eq'), -51.5*np.ones((10,))) assert p1.get('theta_eq') == -51.5 From 2318ba596ac45c212a1cbc8ea90e5365a3c00663 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 5 May 2026 12:57:15 +0200 Subject: [PATCH 2/8] run CI tests with NEST v3.10-rc1 --- .github/workflows/full-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/full-test.yml b/.github/workflows/full-test.yml index 26bcc147..cd94e289 100644 --- a/.github/workflows/full-test.yml +++ b/.github/workflows/full-test.yml @@ -45,9 +45,9 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | python -m pip install "cython<3.1.0" - wget https://github.com/nest/nest-simulator/archive/refs/tags/v3.8.tar.gz -O nest-simulator-3.8.tar.gz - tar xzf nest-simulator-3.8.tar.gz - cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local -Dwith-mpi=ON ./nest-simulator-3.8 + wget https://github.com/nest/nest-simulator/archive/refs/tags/v3.10_rc1.tar.gz -O nest-simulator-3.10.tar.gz + tar xzf nest-simulator-3.10.tar.gz + cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local -Dwith-mpi=ON ./nest-simulator-3.10 make make install - name: Install Arbor From ef94c3099864203d82d8e9824ab28be45d8cf046 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 5 May 2026 13:44:44 +0200 Subject: [PATCH 3/8] fix CI config --- .github/workflows/full-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/full-test.yml b/.github/workflows/full-test.yml index 7c9ece7c..aeb8718e 100644 --- a/.github/workflows/full-test.yml +++ b/.github/workflows/full-test.yml @@ -52,7 +52,7 @@ jobs: python -m pip install "cython<3.1.0" wget https://github.com/nest/nest-simulator/archive/refs/tags/v3.10_rc1.tar.gz -O nest-simulator-3.10.tar.gz tar xzf nest-simulator-3.10.tar.gz - cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local -Dwith-mpi=ON ./nest-simulator-3.10 + cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local -Dwith-mpi=ON ./nest-simulator-3.10_rc1 make make install - name: Install Arbor From 3e082be219538e1c7d7648dc1823b5ddbc6f9e0d Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 5 May 2026 15:00:00 +0200 Subject: [PATCH 4/8] Build NEST with Boost on CI (NEST v3.10-rc1 requires Boost, although the final NEST v3.10 will probably make it optional again.) --- .github/workflows/full-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/full-test.yml b/.github/workflows/full-test.yml index aeb8718e..adfc7050 100644 --- a/.github/workflows/full-test.yml +++ b/.github/workflows/full-test.yml @@ -33,7 +33,7 @@ jobs: - name: Install Linux system dependencies if: startsWith(matrix.os, 'ubuntu') run: | - sudo apt-get install libltdl-dev libgsl0-dev python3-all-dev openmpi-bin libopenmpi-dev + sudo apt-get install libltdl-dev libgsl0-dev python3-all-dev openmpi-bin libopenmpi-dev libboost-dev - name: Install basic Python dependencies run: | python -m pip install --upgrade pip From 070097edb2e32f6a68894f9a8674cfcf8f6eca33 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 5 May 2026 15:47:38 +0200 Subject: [PATCH 5/8] NEST v3.10rc2 has been released --- .github/workflows/full-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/full-test.yml b/.github/workflows/full-test.yml index adfc7050..bfdac3c2 100644 --- a/.github/workflows/full-test.yml +++ b/.github/workflows/full-test.yml @@ -50,9 +50,9 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | python -m pip install "cython<3.1.0" - wget https://github.com/nest/nest-simulator/archive/refs/tags/v3.10_rc1.tar.gz -O nest-simulator-3.10.tar.gz + wget https://github.com/nest/nest-simulator/archive/refs/tags/v3.10_rc2.tar.gz -O nest-simulator-3.10.tar.gz tar xzf nest-simulator-3.10.tar.gz - cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local -Dwith-mpi=ON ./nest-simulator-3.10_rc1 + cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local -Dwith-mpi=ON ./nest-simulator-3.10_rc2 make make install - name: Install Arbor From cd973f3e7a2253dc31b9cbd3fdc3e5b96c62123d Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 5 May 2026 16:57:23 +0200 Subject: [PATCH 6/8] More fixes for NEST 3.10 (we can remove the separate "Install NESTML" step once v8.3.0 is released on PyPI) --- .github/workflows/full-test.yml | 4 ++++ pyNN/nest/projections.py | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/full-test.yml b/.github/workflows/full-test.yml index bfdac3c2..f205d912 100644 --- a/.github/workflows/full-test.yml +++ b/.github/workflows/full-test.yml @@ -59,6 +59,10 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: | python -m pip install arbor==0.9.0 libNeuroML morphio + - name: Install NESTML + if: startsWith(matrix.os, 'ubuntu') + run: | + python -m pip install https://github.com/nest/nestml/archive/refs/tags/v8.3.0-rc2.tar.gz - name: Install PyNN itself run: | python -m pip install -e ".[test,nestml]" diff --git a/pyNN/nest/projections.py b/pyNN/nest/projections.py index f9df9dd8..5ffb1b72 100644 --- a/pyNN/nest/projections.py +++ b/pyNN/nest/projections.py @@ -495,11 +495,17 @@ def _get_attributes_as_arrays(self, names, multiple_synapses='sum'): value_arr = np.nan * np.ones((self.pre.size, self.post.size)) connection_attributes = nest.GetStatus(self.nest_connections, ('source', 'target', attribute_name)) - # NEST 3.10 returns a dict {key: [values]}; earlier versions return a list of tuples + # GetStatus return format varies by NEST version: + # dict {key: [values]} — seen in some NEST 3.x builds + # list of dicts — NEST 3.10rc1 + # list of tuples — older NEST if isinstance(connection_attributes, dict): conn_iter = zip(connection_attributes['source'], connection_attributes['target'], connection_attributes[attribute_name]) + elif connection_attributes and isinstance(connection_attributes[0], dict): + conn_iter = ((c['source'], c['target'], c[attribute_name]) + for c in connection_attributes) else: conn_iter = connection_attributes for conn in conn_iter: From 53d4b39edb7eec08f67af6513487b18c5cc10b13 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 5 May 2026 22:04:52 +0200 Subject: [PATCH 7/8] Fix NESTML tests for NEST 3.10 and NESTML 8.3.0rc2 - Filter NESTML-internal __-prefixed parameters (propagator matrix entries, timestep) from get_defaults() to avoid UnaccessedDictionaryEntry when NEST 3.10 rejects them in nest.Create() - Update stdp_synapse.nestml and tsodyks_synapse.nestml to NESTML 8.3.0rc2 syntax: drop delay parameter and delay argument from emit_spike/output block, since delay is now managed by NEST at the connection level - Change nestml_synapse_type() default delay_variable from "d" to None - Fix projections._convergent_connect() to not write weight=None into the nest.Connect() syn_dict when weight has been routed to a custom variable - Update NEST extension C++ files (CMakeLists.txt, simple_stochastic_synapse, stochastic_stp_synapse) for NEST 3.10 API changes: DictionaryDatum -> Dictionary, def<>/updateValue<> -> d[]/d.update_value() --- examples/nestml/stdp_synapse.nestml | 5 ++-- examples/nestml/tsodyks_synapse.nestml | 9 +++--- pyNN/nest/cells.py | 2 +- pyNN/nest/extensions/CMakeLists.txt | 11 ++++++- .../extensions/simple_stochastic_synapse.h | 19 ++++++------ pyNN/nest/extensions/stochastic_stp_synapse.h | 5 ++-- .../extensions/stochastic_stp_synapse_impl.h | 29 +++++++++---------- pyNN/nest/nestml.py | 2 +- pyNN/nest/projections.py | 6 ++-- test/system/test_nest_nestml.py | 1 - 10 files changed, 49 insertions(+), 40 deletions(-) diff --git a/examples/nestml/stdp_synapse.nestml b/examples/nestml/stdp_synapse.nestml index 608ceb13..369029d3 100644 --- a/examples/nestml/stdp_synapse.nestml +++ b/examples/nestml/stdp_synapse.nestml @@ -63,7 +63,6 @@ model stdp_synapse: post_trace real = 0. parameters: - d ms = 1 ms # Synaptic transmission delay lambda real = 0.01 # (dimensionless) learning rate for causal updates alpha real = 1 # relative learning rate for acausal firing tau_tr_pre ms = 20 ms # time constant of presynaptic trace @@ -83,7 +82,7 @@ model stdp_synapse: post_spikes <- spike output: - spike(weight real, delay ms) + spike onReceive(post_spikes): post_trace += 1 @@ -102,7 +101,7 @@ model stdp_synapse: w = max(Wmin, w_) # deliver spike to postsynaptic partner - emit_spike(w, d) + emit_spike(w) update: integrate_odes() diff --git a/examples/nestml/tsodyks_synapse.nestml b/examples/nestml/tsodyks_synapse.nestml index e155be3f..f91f3fe8 100644 --- a/examples/nestml/tsodyks_synapse.nestml +++ b/examples/nestml/tsodyks_synapse.nestml @@ -1,7 +1,6 @@ model tsodyks_synapse_nestml: parameters: w real = 1 # Synaptic weight - d ms = 1 ms # Dendritic delay (required by PyNESTML; NEST applies transmission delay at connection level) tau_psc ms = 3 ms # Time constant of postsynaptic current tau_fac ms = 0 ms # Time constant for facilitation (0 = no facilitation) tau_rec ms = 800 ms # Time constant for recovery from depression @@ -10,20 +9,20 @@ model tsodyks_synapse_nestml: state: x real = 1 # Fraction of synaptic resources available y real = 0 # Fraction of resources in use - u real = 0.5 # Running value of utilisation + u real = U # Running value of utilisation t_last_update ms = 0 ms input: pre_spikes <- spike output: - spike(weight real, delay ms) + spike onReceive(pre_spikes): dt ms = t - t_last_update t_last_update = t - Puu real = tau_fac == 0 ms ? 0 : exp(-dt / tau_fac) + Puu real = tau_fac == 0 ? 0 : exp(-dt / tau_fac) Pyy real = exp(-dt / tau_psc) Pzz real = exp(-dt / tau_rec) Pxy real = ((Pzz - 1) * tau_rec - (Pyy - 1) * tau_psc) / (tau_psc - tau_rec) @@ -39,4 +38,4 @@ model tsodyks_synapse_nestml: x -= delta_y_tsp y += delta_y_tsp - emit_spike(delta_y_tsp * w, d) + emit_spike(delta_y_tsp * w) diff --git a/pyNN/nest/cells.py b/pyNN/nest/cells.py index a79db68e..5355ce56 100644 --- a/pyNN/nest/cells.py +++ b/pyNN/nest/cells.py @@ -39,7 +39,7 @@ def get_defaults(model_name): for name, value in defaults.items(): if name in variables: default_initial_values[name] = value - elif name not in ignore: + elif name not in ignore and not name.startswith('__'): if isinstance(value, valid_types): default_params[name] = conversion.make_pynn_compatible(value) else: diff --git a/pyNN/nest/extensions/CMakeLists.txt b/pyNN/nest/extensions/CMakeLists.txt index 4ac329bf..5cb72bc1 100644 --- a/pyNN/nest/extensions/CMakeLists.txt +++ b/pyNN/nest/extensions/CMakeLists.txt @@ -192,6 +192,15 @@ add_custom_target( dist ) +# On macOS, loadable modules need -undefined dynamic_lookup so that NEST +# kernel symbols (provided by nestkernel_api.so at Python import time) are +# resolved at dlopen time rather than requiring a libnest.so at link time. +if ( APPLE ) + set( MACOS_LINK_FLAGS "-undefined dynamic_lookup" ) +else () + set( MACOS_LINK_FLAGS "" ) +endif () + # Create a module for loading at runtime # with the `Install` command. add_library( ${MODULE_NAME}_module MODULE ${MODULE_SOURCES} ) @@ -199,7 +208,7 @@ target_link_libraries(${MODULE_NAME}_module ${USER_LINK_LIBRARIES}) set_target_properties( ${MODULE_NAME}_module PROPERTIES COMPILE_FLAGS "${NEST_CXXFLAGS} -DLTX_MODULE" - LINK_FLAGS "${NEST_LIBS}" + LINK_FLAGS "${NEST_LIBS} ${MACOS_LINK_FLAGS}" PREFIX "" OUTPUT_NAME ${MODULE_NAME} ) install( TARGETS ${MODULE_NAME}_module diff --git a/pyNN/nest/extensions/simple_stochastic_synapse.h b/pyNN/nest/extensions/simple_stochastic_synapse.h index 6c0fc169..f2efc7c0 100644 --- a/pyNN/nest/extensions/simple_stochastic_synapse.h +++ b/pyNN/nest/extensions/simple_stochastic_synapse.h @@ -9,6 +9,7 @@ // Includes from nestkernel: #include "connection.h" +#include "dictionary.h" #include "kernel_manager.h" @@ -156,7 +157,7 @@ class simple_stochastic_synapse : public nest::Connection< targetidentifierT > // data member holding the weight. //! Store connection status information in dictionary - void get_status( DictionaryDatum& d ) const; + void get_status( Dictionary& d ) const; /** * Set connection status. @@ -164,7 +165,7 @@ class simple_stochastic_synapse : public nest::Connection< targetidentifierT > * @param d Dictionary with new parameter values * @param cm ConnectorModel is passed along to validate new delay values */ - void set_status( const DictionaryDatum& d, nest::ConnectorModel& cm ); + void set_status( const Dictionary& d, nest::ConnectorModel& cm ); //! Allows efficient initialization on construction void @@ -203,23 +204,23 @@ simple_stochastic_synapse< targetidentifierT >::send( nest::Event& e, template < typename targetidentifierT > void simple_stochastic_synapse< targetidentifierT >::get_status( - DictionaryDatum& d ) const + Dictionary& d ) const { ConnectionBase::get_status( d ); - def< double >( d, nest::names::weight, weight_ ); - def< double >( d, nest::names::p, p_ ); - def< long >( d, nest::names::size_of, sizeof( *this ) ); + d[ nest::names::weight ] = weight_; + d[ nest::names::p ] = p_; + d[ nest::names::size_of ] = static_cast< long >( sizeof( *this ) ); } template < typename targetidentifierT > void simple_stochastic_synapse< targetidentifierT >::set_status( - const DictionaryDatum& d, + const Dictionary& d, nest::ConnectorModel& cm ) { ConnectionBase::set_status( d, cm ); - updateValue< double >( d, nest::names::weight, weight_ ); - updateValue< double >( d, nest::names::p, p_ ); + d.update_value( nest::names::weight, weight_ ); + d.update_value( nest::names::p, p_ ); } } // namespace diff --git a/pyNN/nest/extensions/stochastic_stp_synapse.h b/pyNN/nest/extensions/stochastic_stp_synapse.h index b7acf4d2..b8858549 100644 --- a/pyNN/nest/extensions/stochastic_stp_synapse.h +++ b/pyNN/nest/extensions/stochastic_stp_synapse.h @@ -11,6 +11,7 @@ // Includes from nestkernel: #include "connection.h" +#include "dictionary.h" /* BeginUserDocs: synapse, short-term plasticity @@ -93,13 +94,13 @@ class stochastic_stp_synapse : public nest::Connection< targetidentifierT > /** * Get all properties of this connection and put them into a dictionary. */ - void get_status( DictionaryDatum& d ) const; + void get_status( Dictionary& d ) const; /** * Set default properties of this connection from the values given in * dictionary. */ - void set_status( const DictionaryDatum& d, nest::ConnectorModel& cm ); + void set_status( const Dictionary& d, nest::ConnectorModel& cm ); /** * Send an event to the receiver of this connection. diff --git a/pyNN/nest/extensions/stochastic_stp_synapse_impl.h b/pyNN/nest/extensions/stochastic_stp_synapse_impl.h index e5c577c9..644d6c21 100644 --- a/pyNN/nest/extensions/stochastic_stp_synapse_impl.h +++ b/pyNN/nest/extensions/stochastic_stp_synapse_impl.h @@ -16,8 +16,8 @@ #include "connector_model.h" #include "nest_names.h" -// Includes from sli: -#include "dictutils.h" +// Includes from nestkernel: +#include "dictionary.h" namespace pynn { @@ -55,30 +55,29 @@ stochastic_stp_synapse< targetidentifierT >::stochastic_stp_synapse( template < typename targetidentifierT > void stochastic_stp_synapse< targetidentifierT >::get_status( - DictionaryDatum& d ) const + Dictionary& d ) const { ConnectionBase::get_status( d ); - def< double >( d, nest::names::weight, weight_ ); - def< double >( d, nest::names::dU, U_ ); - def< double >( d, nest::names::u, u_ ); - def< double >( d, nest::names::tau_rec, tau_rec_ ); - def< double >( d, nest::names::tau_fac, tau_fac_ ); + d[ nest::names::weight ] = weight_; + d[ nest::names::dU ] = U_; + d[ nest::names::u ] = u_; + d[ nest::names::tau_rec ] = tau_rec_; + d[ nest::names::tau_fac ] = tau_fac_; } template < typename targetidentifierT > void stochastic_stp_synapse< targetidentifierT >::set_status( - const DictionaryDatum& d, + const Dictionary& d, nest::ConnectorModel& cm ) { ConnectionBase::set_status( d, cm ); - updateValue< double >( d, nest::names::weight, weight_ ); - - updateValue< double >( d, nest::names::dU, U_ ); - updateValue< double >( d, nest::names::u, u_ ); - updateValue< double >( d, nest::names::tau_rec, tau_rec_ ); - updateValue< double >( d, nest::names::tau_fac, tau_fac_ ); + d.update_value( nest::names::weight, weight_ ); + d.update_value( nest::names::dU, U_ ); + d.update_value( nest::names::u, u_ ); + d.update_value( nest::names::tau_rec, tau_rec_ ); + d.update_value( nest::names::tau_fac, tau_fac_ ); } } // of namespace pynn diff --git a/pyNN/nest/nestml.py b/pyNN/nest/nestml.py index 98421b9f..5923becd 100644 --- a/pyNN/nest/nestml.py +++ b/pyNN/nest/nestml.py @@ -109,7 +109,7 @@ def nestml_synapse_type( nestml_description, postsynaptic_neuron_nestml_description=None, weight_variable="w", - delay_variable="d" + delay_variable=None ): """ Register a NESTML synapse description and return a synapse type class. diff --git a/pyNN/nest/projections.py b/pyNN/nest/projections.py index 5ffb1b72..9b112b7b 100644 --- a/pyNN/nest/projections.py +++ b/pyNN/nest/projections.py @@ -268,8 +268,10 @@ def _convergent_connect(self, presynaptic_indices, postsynaptic_index, weights = np.array([weights]) if delays is not None and not np.isscalar(delays): delays = np.array([delays]) - if weights is not None or delays is not None: - syn_dict.update({'weight': weights, 'delay': delays}) + if weights is not None: + syn_dict['weight'] = weights + if delays is not None: + syn_dict['delay'] = delays if postsynaptic_cell.celltype.standard_receptor_type: # For the standard TsodyksMarkramSynapse, copy "tau_psc" from the diff --git a/test/system/test_nest_nestml.py b/test/system/test_nest_nestml.py index 8f53c766..dec3a24e 100644 --- a/test/system/test_nest_nestml.py +++ b/test/system/test_nest_nestml.py @@ -116,7 +116,6 @@ def test_nestml_tsodyks_synapse_vm_trace(): TsodyksSyn = pynn_nestml.nestml_synapse_type( "tsodyks_synapse_nestml", tsodyks_path, weight_variable="w", - delay_variable="d", ) sim.setup(timestep=0.1, min_delay=1.0) From 8ad8161e84bf270666bf5356dde69307cce8e772 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Wed, 6 May 2026 08:50:22 +0200 Subject: [PATCH 8/8] Catch NESTML compilation failures. Add tests for both NESTML v8.2 and v8.3 syntax --- pyNN/nest/nestml.py | 18 +++ test/system/test_nest_nestml.py | 222 +++++++++++++++++++++++++++++++- 2 files changed, 236 insertions(+), 4 deletions(-) diff --git a/pyNN/nest/nestml.py b/pyNN/nest/nestml.py index 5923becd..f09e67fc 100644 --- a/pyNN/nest/nestml.py +++ b/pyNN/nest/nestml.py @@ -270,6 +270,24 @@ def _compile_and_resolve(): codegen_opts=codegen_opts, ) nest.Install(module_name) + except nest.NESTErrors.DynamicModuleManagementError as e: + missing_delay = [ + entry["name"] for entry in _pending + if entry["type"] == "synapse" and entry["delay_variable"] is None + ] + hint = "" + if missing_delay: + hint = ( + f"Synapse model(s) {missing_delay} have no delay_variable set. " + "If your NESTML model uses an explicit delay variable, " + "pass delay_variable='' to nestml_synapse_type() " + "to match the delay parameter in your model." + ) + raise RuntimeError( + f"Failed to install NESTML module '{module_name}'. " + "NESTML model compilation failed silently. " + f"{hint}" + ) from e finally: for tmpdir in tmpdirs: shutil.rmtree(tmpdir, ignore_errors=True) diff --git a/test/system/test_nest_nestml.py b/test/system/test_nest_nestml.py index dec3a24e..1acbd9ae 100644 --- a/test/system/test_nest_nestml.py +++ b/test/system/test_nest_nestml.py @@ -17,6 +17,106 @@ NESTML_MODEL_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "examples", "nestml") +def _nestml_ver(): + try: + import pynestml + parts = pynestml.__version__.split('.') + return (int(parts[0]), int(parts[1])) + except Exception: + return (0, 0) + + +# NESTML 8.2 syntax: output: spike(weight real, delay ms), emit_spike(w, d) +_STDP_SYNAPSE_82 = """\ +model stdp_synapse_82: + state: + w real = 1 [[w >= 0]] + pre_trace real = 0. + post_trace real = 0. + + parameters: + d ms = 1 ms + lambda real = 0.01 + alpha real = 1 + tau_tr_pre ms = 20 ms + tau_tr_post ms = 20 ms + mu_plus real = 1 + mu_minus real = 1 + Wmin real = 0. [[Wmin >= 0]] + Wmax real = 100. [[Wmax >= 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(weight real, delay ms) + + onReceive(post_spikes): + post_trace += 1 + w_ real = Wmax * (w / Wmax + (lambda * (1. - (w / Wmax))**mu_plus * pre_trace)) + w = min(Wmax, w_) + + onReceive(pre_spikes): + pre_trace += 1 + w_ real = Wmax * (w / Wmax - (alpha * lambda * (w / Wmax)**mu_minus * post_trace)) + w = max(Wmin, w_) + emit_spike(w, d) + + update: + integrate_odes() +""" + +_TSODYKS_SYNAPSE_82 = """\ +model tsodyks_synapse_82_nestml: + parameters: + w real = 1 + d ms = 1 ms + tau_psc ms = 3 ms + tau_fac ms = 0 ms + tau_rec ms = 800 ms + U real = 0.5 + + state: + x real = 1 + y real = 0 + u real = 0.5 + t_last_update ms = 0 ms + + input: + pre_spikes <- spike + + output: + spike(weight real, delay ms) + + onReceive(pre_spikes): + dt ms = t - t_last_update + t_last_update = t + + Puu real = tau_fac == 0 ms ? 0 : exp(-dt / tau_fac) + Pyy real = exp(-dt / tau_psc) + Pzz real = exp(-dt / tau_rec) + Pxy real = ((Pzz - 1) * tau_rec - (Pyy - 1) * tau_psc) / (tau_psc - tau_rec) + Pxz real = 1 - Pzz + z real = 1 - x - y + + u *= Puu + x += Pxy * y + Pxz * z + y *= Pyy + u += U * (1 - u) + + delta_y_tsp real = u * x + x -= delta_y_tsp + y += delta_y_tsp + + emit_spike(delta_y_tsp * w, d) +""" + + @pytest.fixture(autouse=True) def reset_nestml_state(): """Reset NESTML module-level state before and after each test. @@ -66,12 +166,12 @@ def test_nestml_cell_type_vm_trace(): sim.end() +@pytest.mark.skipif(not have_pynestml or _nestml_ver() < (8, 3), + reason="requires NESTML >= 8.3") def test_nestml_synapse_weight_changes(): """STDP synapse weights should change from their initial value after Poisson-driven activity.""" if not have_nest: pytest.skip("nest not available") - if not have_pynestml: - pytest.skip("pynestml not available") from pyNN.nest import nestml as pynn_nestml iaf_path = os.path.join(NESTML_MODEL_DIR, "iaf_psc_exp_neuron.nestml") @@ -104,12 +204,12 @@ def test_nestml_synapse_weight_changes(): sim.end() +@pytest.mark.skipif(not have_pynestml or _nestml_ver() < (8, 3), + reason="requires NESTML >= 8.3") def test_nestml_tsodyks_synapse_vm_trace(): """NESTML tsodyks_synapse postsynaptic V_m should be numerically identical to native NEST tsodyks_synapse.""" if not have_nest: pytest.skip("nest not available") - if not have_pynestml: - pytest.skip("pynestml not available") from pyNN.nest import nestml as pynn_nestml tsodyks_path = os.path.join(NESTML_MODEL_DIR, "tsodyks_synapse.nestml") @@ -209,3 +309,117 @@ def test_nestml_setup_without_models(): assert pynn_nestml._pending == [], "_pending should be empty after setup()" sim.end() + + +@pytest.mark.skipif(not have_pynestml or _nestml_ver() >= (8, 3), + reason="requires NESTML < 8.3") +def test_nestml_synapse_weight_changes_82(): + """STDP synapse with NESTML 8.2 syntax (delay in emit_spike) works on NESTML 8.2.""" + if not have_nest: + pytest.skip("nest not available") + + from pyNN.nest import nestml as pynn_nestml + iaf_path = os.path.join(NESTML_MODEL_DIR, "iaf_psc_exp_neuron.nestml") + stdp_cls = pynn_nestml.nestml_synapse_type( + "stdp_synapse_82", _STDP_SYNAPSE_82, + postsynaptic_neuron_nestml_description=iaf_path, + delay_variable="d", + ) + PostCellType = stdp_cls.postsynaptic_cell_type + + sim.setup(timestep=0.1, min_delay=1.0) + + source = sim.Population(10, sim.SpikeSourcePoisson(rate=100.0), label="source") + target = sim.Population(10, PostCellType(), label="target") + + initial_weight = 1.0 + prj = sim.Projection( + source, target, + sim.AllToAllConnector(), + stdp_cls(weight=initial_weight, delay=1.0), + receptor_type="excitatory", + ) + + sim.run(1000.0) + + weights = np.array(prj.get("weight", format="list"))[:, 2] + assert not np.allclose(weights, initial_weight), \ + "STDP weights did not change from initial value — plasticity may not be active" + + sim.end() + + +@pytest.mark.skipif(not have_pynestml or _nestml_ver() >= (8, 3), + reason="requires NESTML < 8.3") +def test_nestml_tsodyks_synapse_vm_trace_82(): + """NESTML tsodyks synapse (NESTML 8.2 syntax) V_m matches native NEST tsodyks_synapse.""" + if not have_nest: + pytest.skip("nest not available") + + from pyNN.nest import nestml as pynn_nestml + TsodyksSyn = pynn_nestml.nestml_synapse_type( + "tsodyks_synapse_82_nestml", _TSODYKS_SYNAPSE_82, + weight_variable="w", + delay_variable="d", + ) + + sim.setup(timestep=0.1, min_delay=1.0) + + spike_times = [50.0, 100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0] + source = sim.Population(1, sim.SpikeSourceArray(spike_times=spike_times), label="source") + + nestml_target = sim.Population(1, sim.native_cell_type("iaf_psc_exp")(), label="nestml_target") + native_target = sim.Population(1, sim.native_cell_type("iaf_psc_exp")(), label="native_target") + + NativeTsodyks = sim.native_synapse_type("tsodyks_synapse") + + sim.Projection( + source, nestml_target, + sim.AllToAllConnector(), + TsodyksSyn(weight=500.0, delay=1.0), + receptor_type="excitatory", + ) + sim.Projection( + source, native_target, + sim.AllToAllConnector(), + NativeTsodyks(weight=500.0, delay=1.0, tau_psc=3.0, tau_fac=0.0, tau_rec=800.0, U=0.5), + receptor_type="excitatory", + ) + + nestml_target.record("V_m") + native_target.record("V_m") + + sim.run(500.0) + + nestml_vm = nestml_target.get_data().segments[0].filter(name="V_m")[0].magnitude + native_vm = native_target.get_data().segments[0].filter(name="V_m")[0].magnitude + + assert nestml_vm.shape == native_vm.shape + assert np.ptp(native_vm) > 1.0, "native tsodyks_synapse target shows no response" + np.testing.assert_allclose(nestml_vm, native_vm, atol=1e-6, + err_msg="V_m traces differ between NESTML 8.2 and native tsodyks_synapse") + + sim.end() + + +def test_nestml_wrong_syntax_raises(): + """Wrong NESTML synapse syntax for the installed version raises an informative RuntimeError.""" + if not have_nest: + pytest.skip("nest not available") + if not have_pynestml: + pytest.skip("pynestml not available") + + from pyNN.nest import nestml as pynn_nestml + stdp_path = os.path.join(NESTML_MODEL_DIR, "stdp_synapse.nestml") + + if _nestml_ver() < (8, 3): + # NESTML 8.2 installed: new-syntax model (no delay_variable) should raise + pynn_nestml.nestml_synapse_type("stdp_synapse", stdp_path) + with pytest.raises(RuntimeError, match="delay_variable"): + sim.setup(timestep=0.1, min_delay=1.0) + else: + # NESTML 8.3+ installed: old-syntax model (with delay_variable) should raise + pynn_nestml.nestml_synapse_type("stdp_synapse_82", _STDP_SYNAPSE_82, + delay_variable="d") + with pytest.raises(RuntimeError, match="NESTML model compilation failed silently"): + sim.setup(timestep=0.1, min_delay=1.0)