Skip to content

Commit 227a6f8

Browse files
committed
Catch NESTML compilation failures.
Add tests for both NESTML v8.2 and v8.3 syntax
1 parent 53d4b39 commit 227a6f8

2 files changed

Lines changed: 236 additions & 4 deletions

File tree

pyNN/nest/nestml.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,24 @@ def _compile_and_resolve():
270270
codegen_opts=codegen_opts,
271271
)
272272
nest.Install(module_name)
273+
except nest.NESTErrors.DynamicModuleManagementError as e:
274+
missing_delay = [
275+
entry["name"] for entry in _pending
276+
if entry["type"] == "synapse" and entry["delay_variable"] is None
277+
]
278+
hint = ""
279+
if missing_delay:
280+
hint = (
281+
f"Synapse model(s) {missing_delay} have no delay_variable set. "
282+
"If your NESTML model uses an explicit delay variable, "
283+
"pass delay_variable='<name>' to nestml_synapse_type() "
284+
"to match the delay parameter in your model."
285+
)
286+
raise RuntimeError(
287+
f"Failed to install NESTML module '{module_name}'. "
288+
"NESTML model compilation failed silently. "
289+
f"{hint}"
290+
) from e
273291
finally:
274292
for tmpdir in tmpdirs:
275293
shutil.rmtree(tmpdir, ignore_errors=True)

test/system/test_nest_nestml.py

Lines changed: 218 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,106 @@
1717
NESTML_MODEL_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "examples", "nestml")
1818

1919

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+
20120
@pytest.fixture(autouse=True)
21121
def reset_nestml_state():
22122
"""Reset NESTML module-level state before and after each test.
@@ -66,12 +166,12 @@ def test_nestml_cell_type_vm_trace():
66166
sim.end()
67167

68168

169+
@pytest.mark.skipif(not have_pynestml or _nestml_ver() < (8, 3),
170+
reason="requires NESTML >= 8.3")
69171
def test_nestml_synapse_weight_changes():
70172
"""STDP synapse weights should change from their initial value after Poisson-driven activity."""
71173
if not have_nest:
72174
pytest.skip("nest not available")
73-
if not have_pynestml:
74-
pytest.skip("pynestml not available")
75175

76176
from pyNN.nest import nestml as pynn_nestml
77177
iaf_path = os.path.join(NESTML_MODEL_DIR, "iaf_psc_exp_neuron.nestml")
@@ -104,12 +204,12 @@ def test_nestml_synapse_weight_changes():
104204
sim.end()
105205

106206

207+
@pytest.mark.skipif(not have_pynestml or _nestml_ver() < (8, 3),
208+
reason="requires NESTML >= 8.3")
107209
def test_nestml_tsodyks_synapse_vm_trace():
108210
"""NESTML tsodyks_synapse postsynaptic V_m should be numerically identical to native NEST tsodyks_synapse."""
109211
if not have_nest:
110212
pytest.skip("nest not available")
111-
if not have_pynestml:
112-
pytest.skip("pynestml not available")
113213

114214
from pyNN.nest import nestml as pynn_nestml
115215
tsodyks_path = os.path.join(NESTML_MODEL_DIR, "tsodyks_synapse.nestml")
@@ -209,3 +309,117 @@ def test_nestml_setup_without_models():
209309
assert pynn_nestml._pending == [], "_pending should be empty after setup()"
210310

211311
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

Comments
 (0)