|
17 | 17 | NESTML_MODEL_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "examples", "nestml") |
18 | 18 |
|
19 | 19 |
|
| 20 | +def _nestml_ver(): |
| 21 | + try: |
| 22 | + import pynestml |
| 23 | + parts = pynestml.__version__.split('.') |
| 24 | + return (int(parts[0]), int(parts[1])) |
| 25 | + except Exception: |
| 26 | + return (0, 0) |
| 27 | + |
| 28 | + |
| 29 | +# NESTML 8.2 syntax: output: spike(weight real, delay ms), emit_spike(w, d) |
| 30 | +_STDP_SYNAPSE_82 = """\ |
| 31 | +model stdp_synapse_82: |
| 32 | + state: |
| 33 | + w real = 1 [[w >= 0]] |
| 34 | + pre_trace real = 0. |
| 35 | + post_trace real = 0. |
| 36 | +
|
| 37 | + parameters: |
| 38 | + d ms = 1 ms |
| 39 | + lambda real = 0.01 |
| 40 | + alpha real = 1 |
| 41 | + tau_tr_pre ms = 20 ms |
| 42 | + tau_tr_post ms = 20 ms |
| 43 | + mu_plus real = 1 |
| 44 | + mu_minus real = 1 |
| 45 | + Wmin real = 0. [[Wmin >= 0]] |
| 46 | + Wmax real = 100. [[Wmax >= 0]] |
| 47 | +
|
| 48 | + equations: |
| 49 | + pre_trace' = -pre_trace / tau_tr_pre |
| 50 | + post_trace' = -post_trace / tau_tr_post |
| 51 | +
|
| 52 | + input: |
| 53 | + pre_spikes <- spike |
| 54 | + post_spikes <- spike |
| 55 | +
|
| 56 | + output: |
| 57 | + spike(weight real, delay ms) |
| 58 | +
|
| 59 | + onReceive(post_spikes): |
| 60 | + post_trace += 1 |
| 61 | + w_ real = Wmax * (w / Wmax + (lambda * (1. - (w / Wmax))**mu_plus * pre_trace)) |
| 62 | + w = min(Wmax, w_) |
| 63 | +
|
| 64 | + onReceive(pre_spikes): |
| 65 | + pre_trace += 1 |
| 66 | + w_ real = Wmax * (w / Wmax - (alpha * lambda * (w / Wmax)**mu_minus * post_trace)) |
| 67 | + w = max(Wmin, w_) |
| 68 | + emit_spike(w, d) |
| 69 | +
|
| 70 | + update: |
| 71 | + integrate_odes() |
| 72 | +""" |
| 73 | + |
| 74 | +_TSODYKS_SYNAPSE_82 = """\ |
| 75 | +model tsodyks_synapse_82_nestml: |
| 76 | + parameters: |
| 77 | + w real = 1 |
| 78 | + d ms = 1 ms |
| 79 | + tau_psc ms = 3 ms |
| 80 | + tau_fac ms = 0 ms |
| 81 | + tau_rec ms = 800 ms |
| 82 | + U real = 0.5 |
| 83 | +
|
| 84 | + state: |
| 85 | + x real = 1 |
| 86 | + y real = 0 |
| 87 | + u real = 0.5 |
| 88 | + t_last_update ms = 0 ms |
| 89 | +
|
| 90 | + input: |
| 91 | + pre_spikes <- spike |
| 92 | +
|
| 93 | + output: |
| 94 | + spike(weight real, delay ms) |
| 95 | +
|
| 96 | + onReceive(pre_spikes): |
| 97 | + dt ms = t - t_last_update |
| 98 | + t_last_update = t |
| 99 | +
|
| 100 | + Puu real = tau_fac == 0 ms ? 0 : exp(-dt / tau_fac) |
| 101 | + Pyy real = exp(-dt / tau_psc) |
| 102 | + Pzz real = exp(-dt / tau_rec) |
| 103 | + Pxy real = ((Pzz - 1) * tau_rec - (Pyy - 1) * tau_psc) / (tau_psc - tau_rec) |
| 104 | + Pxz real = 1 - Pzz |
| 105 | + z real = 1 - x - y |
| 106 | +
|
| 107 | + u *= Puu |
| 108 | + x += Pxy * y + Pxz * z |
| 109 | + y *= Pyy |
| 110 | + u += U * (1 - u) |
| 111 | +
|
| 112 | + delta_y_tsp real = u * x |
| 113 | + x -= delta_y_tsp |
| 114 | + y += delta_y_tsp |
| 115 | +
|
| 116 | + emit_spike(delta_y_tsp * w, d) |
| 117 | +""" |
| 118 | + |
| 119 | + |
20 | 120 | @pytest.fixture(autouse=True) |
21 | 121 | def reset_nestml_state(): |
22 | 122 | """Reset NESTML module-level state before and after each test. |
@@ -66,12 +166,12 @@ def test_nestml_cell_type_vm_trace(): |
66 | 166 | sim.end() |
67 | 167 |
|
68 | 168 |
|
| 169 | +@pytest.mark.skipif(not have_pynestml or _nestml_ver() < (8, 3), |
| 170 | + reason="requires NESTML >= 8.3") |
69 | 171 | def test_nestml_synapse_weight_changes(): |
70 | 172 | """STDP synapse weights should change from their initial value after Poisson-driven activity.""" |
71 | 173 | if not have_nest: |
72 | 174 | pytest.skip("nest not available") |
73 | | - if not have_pynestml: |
74 | | - pytest.skip("pynestml not available") |
75 | 175 |
|
76 | 176 | from pyNN.nest import nestml as pynn_nestml |
77 | 177 | iaf_path = os.path.join(NESTML_MODEL_DIR, "iaf_psc_exp_neuron.nestml") |
@@ -104,12 +204,12 @@ def test_nestml_synapse_weight_changes(): |
104 | 204 | sim.end() |
105 | 205 |
|
106 | 206 |
|
| 207 | +@pytest.mark.skipif(not have_pynestml or _nestml_ver() < (8, 3), |
| 208 | + reason="requires NESTML >= 8.3") |
107 | 209 | def test_nestml_tsodyks_synapse_vm_trace(): |
108 | 210 | """NESTML tsodyks_synapse postsynaptic V_m should be numerically identical to native NEST tsodyks_synapse.""" |
109 | 211 | if not have_nest: |
110 | 212 | pytest.skip("nest not available") |
111 | | - if not have_pynestml: |
112 | | - pytest.skip("pynestml not available") |
113 | 213 |
|
114 | 214 | from pyNN.nest import nestml as pynn_nestml |
115 | 215 | tsodyks_path = os.path.join(NESTML_MODEL_DIR, "tsodyks_synapse.nestml") |
@@ -209,3 +309,117 @@ def test_nestml_setup_without_models(): |
209 | 309 | assert pynn_nestml._pending == [], "_pending should be empty after setup()" |
210 | 310 |
|
211 | 311 | sim.end() |
| 312 | + |
| 313 | + |
| 314 | +@pytest.mark.skipif(not have_pynestml or _nestml_ver() >= (8, 3), |
| 315 | + reason="requires NESTML < 8.3") |
| 316 | +def test_nestml_synapse_weight_changes_82(): |
| 317 | + """STDP synapse with NESTML 8.2 syntax (delay in emit_spike) works on NESTML 8.2.""" |
| 318 | + if not have_nest: |
| 319 | + pytest.skip("nest not available") |
| 320 | + |
| 321 | + from pyNN.nest import nestml as pynn_nestml |
| 322 | + iaf_path = os.path.join(NESTML_MODEL_DIR, "iaf_psc_exp_neuron.nestml") |
| 323 | + stdp_cls = pynn_nestml.nestml_synapse_type( |
| 324 | + "stdp_synapse_82", _STDP_SYNAPSE_82, |
| 325 | + postsynaptic_neuron_nestml_description=iaf_path, |
| 326 | + delay_variable="d", |
| 327 | + ) |
| 328 | + PostCellType = stdp_cls.postsynaptic_cell_type |
| 329 | + |
| 330 | + sim.setup(timestep=0.1, min_delay=1.0) |
| 331 | + |
| 332 | + source = sim.Population(10, sim.SpikeSourcePoisson(rate=100.0), label="source") |
| 333 | + target = sim.Population(10, PostCellType(), label="target") |
| 334 | + |
| 335 | + initial_weight = 1.0 |
| 336 | + prj = sim.Projection( |
| 337 | + source, target, |
| 338 | + sim.AllToAllConnector(), |
| 339 | + stdp_cls(weight=initial_weight, delay=1.0), |
| 340 | + receptor_type="excitatory", |
| 341 | + ) |
| 342 | + |
| 343 | + sim.run(1000.0) |
| 344 | + |
| 345 | + weights = np.array(prj.get("weight", format="list"))[:, 2] |
| 346 | + assert not np.allclose(weights, initial_weight), \ |
| 347 | + "STDP weights did not change from initial value — plasticity may not be active" |
| 348 | + |
| 349 | + sim.end() |
| 350 | + |
| 351 | + |
| 352 | +@pytest.mark.skipif(not have_pynestml or _nestml_ver() >= (8, 3), |
| 353 | + reason="requires NESTML < 8.3") |
| 354 | +def test_nestml_tsodyks_synapse_vm_trace_82(): |
| 355 | + """NESTML tsodyks synapse (NESTML 8.2 syntax) V_m matches native NEST tsodyks_synapse.""" |
| 356 | + if not have_nest: |
| 357 | + pytest.skip("nest not available") |
| 358 | + |
| 359 | + from pyNN.nest import nestml as pynn_nestml |
| 360 | + TsodyksSyn = pynn_nestml.nestml_synapse_type( |
| 361 | + "tsodyks_synapse_82_nestml", _TSODYKS_SYNAPSE_82, |
| 362 | + weight_variable="w", |
| 363 | + delay_variable="d", |
| 364 | + ) |
| 365 | + |
| 366 | + sim.setup(timestep=0.1, min_delay=1.0) |
| 367 | + |
| 368 | + spike_times = [50.0, 100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0] |
| 369 | + source = sim.Population(1, sim.SpikeSourceArray(spike_times=spike_times), label="source") |
| 370 | + |
| 371 | + nestml_target = sim.Population(1, sim.native_cell_type("iaf_psc_exp")(), label="nestml_target") |
| 372 | + native_target = sim.Population(1, sim.native_cell_type("iaf_psc_exp")(), label="native_target") |
| 373 | + |
| 374 | + NativeTsodyks = sim.native_synapse_type("tsodyks_synapse") |
| 375 | + |
| 376 | + sim.Projection( |
| 377 | + source, nestml_target, |
| 378 | + sim.AllToAllConnector(), |
| 379 | + TsodyksSyn(weight=500.0, delay=1.0), |
| 380 | + receptor_type="excitatory", |
| 381 | + ) |
| 382 | + sim.Projection( |
| 383 | + source, native_target, |
| 384 | + sim.AllToAllConnector(), |
| 385 | + NativeTsodyks(weight=500.0, delay=1.0, tau_psc=3.0, tau_fac=0.0, tau_rec=800.0, U=0.5), |
| 386 | + receptor_type="excitatory", |
| 387 | + ) |
| 388 | + |
| 389 | + nestml_target.record("V_m") |
| 390 | + native_target.record("V_m") |
| 391 | + |
| 392 | + sim.run(500.0) |
| 393 | + |
| 394 | + nestml_vm = nestml_target.get_data().segments[0].filter(name="V_m")[0].magnitude |
| 395 | + native_vm = native_target.get_data().segments[0].filter(name="V_m")[0].magnitude |
| 396 | + |
| 397 | + assert nestml_vm.shape == native_vm.shape |
| 398 | + assert np.ptp(native_vm) > 1.0, "native tsodyks_synapse target shows no response" |
| 399 | + np.testing.assert_allclose(nestml_vm, native_vm, atol=1e-6, |
| 400 | + err_msg="V_m traces differ between NESTML 8.2 and native tsodyks_synapse") |
| 401 | + |
| 402 | + sim.end() |
| 403 | + |
| 404 | + |
| 405 | +def test_nestml_wrong_syntax_raises(): |
| 406 | + """Wrong NESTML synapse syntax for the installed version raises an informative RuntimeError.""" |
| 407 | + if not have_nest: |
| 408 | + pytest.skip("nest not available") |
| 409 | + if not have_pynestml: |
| 410 | + pytest.skip("pynestml not available") |
| 411 | + |
| 412 | + from pyNN.nest import nestml as pynn_nestml |
| 413 | + stdp_path = os.path.join(NESTML_MODEL_DIR, "stdp_synapse.nestml") |
| 414 | + |
| 415 | + if _nestml_ver() < (8, 3): |
| 416 | + # NESTML 8.2 installed: new-syntax model (no delay_variable) should raise |
| 417 | + pynn_nestml.nestml_synapse_type("stdp_synapse", stdp_path) |
| 418 | + with pytest.raises(RuntimeError, match="delay_variable"): |
| 419 | + sim.setup(timestep=0.1, min_delay=1.0) |
| 420 | + else: |
| 421 | + # NESTML 8.3+ installed: old-syntax model (with delay_variable) should raise |
| 422 | + pynn_nestml.nestml_synapse_type("stdp_synapse_82", _STDP_SYNAPSE_82, |
| 423 | + delay_variable="d") |
| 424 | + with pytest.raises(RuntimeError, match="delay_variable"): |
| 425 | + sim.setup(timestep=0.1, min_delay=1.0) |
0 commit comments