diff --git a/.github/workflows/full-test.yml b/.github/workflows/full-test.yml index 1bc68f40..f205d912 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 @@ -50,15 +50,19 @@ 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.9.tar.gz -O nest-simulator-3.9.tar.gz - tar xzf nest-simulator-3.9.tar.gz - cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local -Dwith-mpi=ON ./nest-simulator-3.9 + 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_rc2 make make install - name: Install Arbor 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/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 8bcb6f34..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: @@ -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/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..f09e67fc 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. @@ -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/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 6b961073..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 @@ -463,7 +465,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)) @@ -492,7 +497,20 @@ 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: + # 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: # (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 6f56dd3e..df7b8233 100644 --- a/pyNN/nest/simulator.py +++ b/pyNN/nest/simulator.py @@ -221,7 +221,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): @@ -308,8 +312,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 fcba9a16..60e2343d 100644 --- a/test/system/test_nest.py +++ b/test/system/test_nest.py @@ -33,7 +33,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 diff --git a/test/system/test_nest_nestml.py b/test/system/test_nest_nestml.py index 8f53c766..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,19 +204,18 @@ 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") 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) @@ -210,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)