From 64f7b55e7e5e0e285c54aa7a3f1141720fe5de20 Mon Sep 17 00:00:00 2001 From: Robin De Schepper Date: Wed, 25 Jun 2025 10:34:28 +0200 Subject: [PATCH 1/5] removed unreusable reusable code for better readability --- testsuite/pytests/conftest.py | 23 - .../iaf_psc_alpha/test_iaf_psc_alpha.py | 431 ------------------ .../iaf_psc_alpha/test_iaf_psc_alpha_1to2.py | 155 ------- .../iaf_psc_alpha/test_iaf_psc_alpha_dc.py | 209 --------- .../sli2py_neurons/test_iaf_ps_dc_accuracy.py | 123 +++++ .../sli2py_neurons/test_iaf_psc_alpha.py | 330 ++++++++++++++ .../sli2py_neurons/test_iaf_psc_alpha_1to2.py | 230 ++++++++++ .../test_iaf_psc_alpha_fudge.py | 112 +++++ .../test_iaf_psc_alpha_mindelay.py | 135 ++++++ testsuite/pytests/utilities/testsimulation.py | 51 --- testsuite/pytests/utilities/testutil.py | 108 +++-- 11 files changed, 990 insertions(+), 917 deletions(-) delete mode 100644 testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha.py delete mode 100644 testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha_1to2.py delete mode 100644 testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha_dc.py create mode 100644 testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py create mode 100644 testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py create mode 100644 testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py create mode 100644 testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py create mode 100644 testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py delete mode 100644 testsuite/pytests/utilities/testsimulation.py diff --git a/testsuite/pytests/conftest.py b/testsuite/pytests/conftest.py index 67d8428ce6..ebf020d127 100644 --- a/testsuite/pytests/conftest.py +++ b/testsuite/pytests/conftest.py @@ -31,7 +31,6 @@ def test_gsl(): pass """ -import dataclasses import os import pathlib import subprocess @@ -45,7 +44,6 @@ def test_gsl(): # Ignore it during test collection collect_ignore = ["utilities"] -import testsimulation # noqa import testutil # noqa @@ -174,24 +172,3 @@ def skipif_incompatible_mpi(request, subprocess_compatible_mpi): if not subprocess_compatible_mpi and request.node.get_closest_marker("skipif_incompatible_mpi"): pytest.skip("skipped because MPI is incompatible with subprocess") - - -@pytest.fixture(autouse=True) -def simulation_class(request): - return getattr(request, "param", testsimulation.Simulation) - - -@pytest.fixture -def simulation(request): - marker = request.node.get_closest_marker("simulation") - sim_cls = marker.args[0] if marker else testsimulation.Simulation - sim = sim_cls(*(request.getfixturevalue(field.name) for field in dataclasses.fields(sim_cls))) - nest.ResetKernel() - if getattr(sim, "set_resolution", True): - nest.resolution = sim.resolution - nest.local_num_threads = sim.local_num_threads - return sim - - -# Inject the root simulation fixtures into this module to be always available. -testutil.create_dataclass_fixtures(testsimulation.Simulation, __name__) diff --git a/testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha.py b/testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha.py deleted file mode 100644 index 8ef1815eb0..0000000000 --- a/testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha.py +++ /dev/null @@ -1,431 +0,0 @@ -# -*- coding: utf-8 -*- -# -# test_iaf_psc_alpha.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 . - -import dataclasses -import math - -import nest -import numpy as np -import pytest -import testsimulation -import testutil -from scipy.special import lambertw - -# Notes: -# * copy docs -# * add docs & examples -# * add comments -# * restructure -# * test min delay stuff tests sim indep of mindelay, move out? -# * `test_iaf_ps_dc_accuracy` tests kernel precision, move out? - - -@dataclasses.dataclass -class IAFPSCAlphaSimulation(testsimulation.Simulation): - def setup(self): - self.neuron = nest.Create("iaf_psc_alpha") - vm = self.voltmeter = nest.Create("voltmeter") - vm.interval = self.resolution - sr = self.spike_recorder = nest.Create("spike_recorder") - nest.Connect(vm, self.neuron, syn_spec={"weight": 1.0, "delay": self.delay}) - nest.Connect(self.neuron, sr, syn_spec={"weight": 1.0, "delay": self.delay}) - - @property - def spikes(self): - return np.column_stack( - ( - self.spike_recorder.events["senders"], - self.spike_recorder.events["times"], - ) - ) - - -@dataclasses.dataclass -class MinDelaySimulation(IAFPSCAlphaSimulation): - amplitude: float = 1000.0 - min_delay: float = 0.0 - - def setup(self): - dc = self.dc_generator = nest.Create("dc_generator") - dc.amplitude = self.amplitude - - super().setup() - - nest.Connect( - dc, - self.neuron, - syn_spec={"weight": 1.0, "delay": self.delay}, - ) - - -@testutil.use_simulation(IAFPSCAlphaSimulation) -class TestIAFPSCAlpha: - def test_iaf_psc_alpha(self, simulation): - dc = simulation.dc_generator = nest.Create("dc_generator") - dc.amplitude = 1000 - - simulation.setup() - - nest.Connect( - dc, - simulation.neuron, - syn_spec={"weight": 1.0, "delay": simulation.resolution}, - ) - - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expected_default) - assert actual == expected - - @pytest.mark.parametrize("duration", [20.0]) - def test_iaf_psc_alpha_fudge(self, simulation): - simulation.setup() - - tau_m = 20 - tau_syn = 0.5 - C_m = 250.0 - a = tau_m / tau_syn - b = 1.0 / tau_syn - 1.0 / tau_m - t_max = 1.0 / b * (-lambertw(-math.exp(-1.0 / a) / a, k=-1) - 1.0 / a).real - V_max = ( - math.exp(1) - / (tau_syn * C_m * b) - * ((math.exp(-t_max / tau_m) - math.exp(-t_max / tau_syn)) / b - t_max * math.exp(-t_max / tau_syn)) - ) - simulation.neuron.set(tau_m=tau_m, tau_syn_ex=tau_syn, tau_syn_in=tau_syn, C_m=C_m) - sg = nest.Create( - "spike_generator", - params={"precise_times": False, "spike_times": [simulation.resolution]}, - ) - nest.Connect( - sg, - simulation.neuron, - syn_spec={"weight": float(1.0 / V_max), "delay": simulation.resolution}, - ) - - results = simulation.simulate() - - actual_t_max = results[np.argmax(results[:, 1]), 0] - assert actual_t_max == pytest.approx(t_max + 0.2, abs=0.05) - - def test_iaf_psc_alpha_i0(self, simulation): - simulation.setup() - - simulation.neuron.I_e = 1000 - - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expected_i0) - assert actual == expected - assert simulation.spikes == pytest.approx(expected_i0_t) - - @pytest.mark.parametrize("resolution", [0.1, 0.2, 0.5, 1.0]) - def test_iaf_psc_alpha_i0_refractory(self, simulation): - simulation.setup() - - simulation.neuron.I_e = 1450 - - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expected_i0_refr) - assert actual == expected - - -@pytest.mark.parametrize("min_delay", [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 1.0, 2.0]) -@pytest.mark.parametrize("delay, duration", [(2.0, 10.5)]) -@testutil.use_simulation(MinDelaySimulation) -class TestMinDelayUsingIAFPSCAlpha: - def test_iaf_psc_alpha_mindelay_create(self, simulation, min_delay): - simulation.setup() - - # Connect 2 throwaway neurons with `min_delay` to force `min_delay` - nest.Connect(*nest.Create("iaf_psc_alpha", 2), syn_spec={"delay": min_delay, "weight": 1.0}) - - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expected_mindelay) - assert actual == expected - - def test_iaf_psc_alpha_mindelay_set(self, simulation, min_delay, delay): - nest.set(min_delay=min_delay, max_delay=delay) - nest.SetDefaults("static_synapse", {"delay": delay}) - - simulation.setup() - - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expected_mindelay) - assert actual == expected - - def test_iaf_psc_alpha_mindelay_simblocks(self, simulation, min_delay, delay): - nest.set(min_delay=min_delay, max_delay=delay) - nest.SetDefaults("static_synapse", {"delay": delay}) - - simulation.setup() - - for _ in range(22): - nest.Simulate(0.5) - # duration=0 so that `simulation.simulate` is noop but - # still extracts results for us. - simulation.duration = 0 - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expected_mindelay) - assert actual == expected - - -def test_kernel_precision(): - nest.ResetKernel() - nest.set(tics_per_ms=2**14, resolution=2**0) - assert math.frexp(nest.ms_per_tic) == (0.5, -13) - - -@dataclasses.dataclass -class DCAccuracySimulation(testsimulation.Simulation): - # Don't autoset the resolution in the fixture, we do it in setup. - set_resolution = False - model: str = "iaf_psc_alpha" - params: dict = dataclasses.field(default_factory=dict) - - def setup(self): - nest.ResetKernel() - nest.set(tics_per_ms=2**14, resolution=self.resolution) - self.neuron = nest.Create(self.model, params=self.params) - - -@testutil.use_simulation(DCAccuracySimulation) -@pytest.mark.parametrize( - "model", - [ - "iaf_psc_alpha_ps", - "iaf_psc_delta_ps", - "iaf_psc_exp_ps", - "iaf_psc_exp_ps_lossless", - ], -) -@pytest.mark.parametrize("resolution", [2**i for i in range(0, -14, -1)]) -class TestIAFPSDCAccuracy: - @pytest.mark.parametrize( - "params", - [ - { - "E_L": 0.0, # resting potential in mV - "V_m": 0.0, # initial membrane potential in mV - "V_th": 2000.0, # spike threshold in mV - "I_e": 1000.0, # DC current in pA - "tau_m": 10.0, # membrane time constant in ms - "C_m": 250.0, # membrane capacity in pF - } - ], - ) - @pytest.mark.parametrize("duration, tolerance", [(5, 1e-13), (500.0, 1e-9)]) - def test_iaf_ps_dc_accuracy(self, simulation, duration, tolerance, params): - simulation.run() - # Analytical solution - V = params["I_e"] * params["tau_m"] / params["C_m"] * (1.0 - math.exp(-duration / params["tau_m"])) - # Check that membrane potential is within tolerance of analytical solution. - assert math.fabs(simulation.neuron.V_m - V) < tolerance - - @pytest.mark.parametrize( - "params", - [ - { - "E_L": 0.0, # resting potential in mV - "V_m": 0.0, # initial membrane potential in mV - "V_th": 15.0, # spike threshold in mV - "I_e": 1000.0, # DC current in pA - "tau_m": 10.0, # membrane time constant in ms - "C_m": 250.0, # membrane capacity in pF - } - ], - ) - @pytest.mark.parametrize("duration, tolerance", [(5, 1e-13)]) - def test_iaf_ps_dc_t_accuracy(self, simulation, params, tolerance): - simulation.run() - t = -params["tau_m"] * math.log(1.0 - (params["C_m"] * params["V_th"]) / (params["tau_m"] * params["I_e"])) - assert math.fabs(simulation.neuron.t_spike - t) < tolerance - - -expected_default = np.array( - [ - [0.1, -70], - [0.2, -70], - [0.3, -69.602], - [0.4, -69.2079], - [0.5, -68.8178], - [0.6, -68.4316], - [0.7, -68.0492], - [0.8, -67.6706], - [0.9, -67.2958], - [1.0, -66.9247], - [4.5, -56.0204], - [4.6, -55.7615], - [4.7, -55.5051], - [4.8, -55.2513], - [4.9, -55.0001], - [5.0, -70], - [5.1, -70], - [5.2, -70], - [5.3, -70], - [5.4, -70], - [5.5, -70], - [5.6, -70], - [5.7, -70], - [5.8, -70], - [5.9, -70], - [6.0, -70], - [6.1, -70], - [6.2, -70], - [6.3, -70], - [6.4, -70], - [6.5, -70], - [6.6, -70], - [6.7, -70], - [6.8, -70], - [6.9, -70], - [7.0, -70], - [7.1, -69.602], - [7.2, -69.2079], - [7.3, -68.8178], - [7.4, -68.4316], - [7.5, -68.0492], - [7.6, -67.6706], - [7.7, -67.2958], - [7.8, -66.9247], - [7.9, -66.5572], - ] -) - -expected_i0 = np.array( - [ - [0.1, -69.602], - [0.2, -69.2079], - [0.3, -68.8178], - [0.4, -68.4316], - [0.5, -68.0492], - [4.3, -56.0204], - [4.4, -55.7615], - [4.5, -55.5051], - [4.6, -55.2513], - [4.7, -55.0001], - [4.8, -70], - [4.9, -70], - [5.0, -70], - ] -) - -expected_i0_t = np.array( - [ - [1, 4.8], - ] -) - -expected_i0_refr = np.array( - [ - [0.1, -69.4229], - [0.2, -68.8515], - [0.3, -68.2858], - [0.4, -67.7258], - [0.5, -67.1713], - [0.6, -66.6223], - [0.7, -66.0788], - [0.8, -65.5407], - [0.9, -65.008], - [1.0, -64.4806], - [1.1, -63.9584], - [1.2, -63.4414], - [1.3, -62.9295], - [1.4, -62.4228], - [1.5, -61.9211], - [1.6, -61.4243], - [1.7, -60.9326], - [1.8, -60.4457], - [1.9, -59.9636], - [2.0, -59.4864], - [2.1, -59.0139], - [2.2, -58.5461], - [2.3, -58.0829], - [2.4, -57.6244], - [2.5, -57.1704], - [2.6, -56.721], - [2.7, -56.276], - [2.8, -55.8355], - [2.9, -55.3993], - [3.0, -70], - [3.1, -70], - [3.2, -70], - [3.3, -70], - [3.4, -70], - [3.5, -70], - [3.6, -70], - [3.7, -70], - [3.8, -70], - [3.9, -70], - [4.0, -70], - [4.1, -70], - [4.2, -70], - [4.3, -70], - [4.4, -70], - [4.5, -70], - [4.6, -70], - [4.7, -70], - [4.8, -70], - [4.9, -70], - [5.0, -70], - [ - 5.1, - -69.4229, - ], - [5.2, -68.8515], - [5.3, -68.2858], - [5.4, -67.7258], - [5.5, -67.1713], - [5.6, -66.6223], - [5.7, -66.0788], - [5.8, -65.5407], - [5.9, -65.008], - [6.0, -64.4806], - [6.1, -63.9584], - [6.2, -63.4414], - [6.3, -62.9295], - [6.4, -62.4228], - [6.5, -61.9211], - [6.6, -61.4243], - [6.7, -60.9326], - [6.8, -60.4457], - [6.9, -59.9636], - ] -) - -expected_mindelay = np.array( - [ - [1.000000e00, -7.000000e01], - [2.000000e00, -7.000000e01], - [3.000000e00, -6.655725e01], - [4.000000e00, -6.307837e01], - [5.000000e00, -5.993054e01], - [6.000000e00, -5.708227e01], - [7.000000e00, -7.000000e01], - [8.000000e00, -7.000000e01], - [9.000000e00, -6.960199e01], - [1.000000e01, -6.583337e01], - ] -) diff --git a/testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha_1to2.py b/testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha_1to2.py deleted file mode 100644 index ce7fcc9d8d..0000000000 --- a/testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha_1to2.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- coding: utf-8 -*- -# -# test_iaf_psc_alpha_1to2.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 . - -import dataclasses - -import nest -import numpy as np -import pytest -import testsimulation -import testutil - - -@dataclasses.dataclass -class IAFPSCAlpha1to2Simulation(testsimulation.Simulation): - weight: float = 100.0 - delay: float = None - min_delay: float = None - - def __post_init__(self): - self.syn_spec = {"weight": self.weight} - if self.delay is not None: - self.syn_spec["delay"] = self.delay - - def setup(self): - n1, n2 = self.neurons = nest.Create("iaf_psc_alpha", 2) - n1.I_e = 1450.0 - vm = self.voltmeter = nest.Create("voltmeter") - vm.interval = self.resolution - vm_spec = {} - if self.delay is not None: - vm_spec["delay"] = self.delay - nest.Connect(vm, n2, syn_spec=vm_spec) - nest.Connect(n1, n2, syn_spec=self.syn_spec) - - -@pytest.mark.parametrize("resolution", [0.1, 0.2, 0.5, 1.0]) -@testutil.use_simulation(IAFPSCAlpha1to2Simulation) -class TestIAFPSCAlpha1to2WithMultiRes: - @pytest.mark.parametrize("delay", [1.0]) - def test_1to2(self, simulation): - simulation.setup() - - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expect_default) - assert actual == expected - - def test_default_delay(self, simulation): - nest.SetDefaults("static_synapse", {"delay": 1.0}) - simulation.setup() - - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expect_default) - assert actual == expected - - -@testutil.use_simulation(IAFPSCAlpha1to2Simulation) -@pytest.mark.parametrize("delay,resolution", [(2.0, 0.1)]) -@pytest.mark.parametrize("min_delay", [0.1, 0.5, 2.0]) -def test_mindelay_invariance(simulation): - assert simulation.min_delay <= simulation.delay - nest.set(min_delay=simulation.min_delay, max_delay=simulation.delay) - simulation.setup() - results = simulation.simulate() - actual, expected = testutil.get_comparable_timesamples(results, expect_inv) - assert actual == expected - - -expect_default = np.array( - [ - [2.5, -70], - [2.6, -70], - [2.7, -70], - [2.8, -70], - [2.9, -70], - [3.0, -70], - [3.1, -70], - [3.2, -70], - [3.3, -70], - [3.4, -70], - [3.5, -70], - [3.6, -70], - [3.7, -70], - [3.8, -70], - [3.9, -70], - [4.0, -70], - [4.1, -69.9974], - [4.2, -69.9899], - [4.3, -69.9781], - [4.4, -69.9624], - [4.5, -69.9434], - [4.6, -69.9213], - [4.7, -69.8967], - [4.8, -69.8699], - [4.9, -69.8411], - [5.0, -69.8108], - [5.1, -69.779], - [5.2, -69.7463], - [5.3, -69.7126], - [5.4, -69.6783], - [5.5, -69.6435], - [5.6, -69.6084], - [5.7, -69.5732], - ] -) - -expect_inv = np.array( - [ - [0.1, -70], - [0.2, -70], - [0.3, -70], - [0.4, -70], - [0.5, -70], - [2.8, -70], - [2.9, -70], - [3.0, -70], - [3.1, -70], - [3.2, -70], - [3.3, -70], - [3.4, -70], - [3.5, -70], - [4.8, -70], - [4.9, -70], - [5.0, -70], - [5.1, -69.9974], - [5.2, -69.9899], - [5.3, -69.9781], - [5.4, -69.9624], - [5.5, -69.9434], - [5.6, -69.9213], - [5.7, -69.8967], - [5.8, -69.8699], - [5.9, -69.8411], - [6.0, -69.8108], - ] -) diff --git a/testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha_dc.py b/testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha_dc.py deleted file mode 100644 index 666f339da9..0000000000 --- a/testsuite/pytests/sli2py_neurons/iaf_psc_alpha/test_iaf_psc_alpha_dc.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: utf-8 -*- -# -# test_iaf_psc_alpha_dc.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 . - -import dataclasses - -import nest -import numpy as np -import pytest -import testsimulation -import testutil - - -@dataclasses.dataclass -class IAFPSCAlphaDCSimulation(testsimulation.Simulation): - amplitude: float = 1000.0 - origin: float = 0.0 - arrival: float = 3.0 - dc_delay: float = 1.0 - dc_visible: float = 3.0 - dc_duration: float = 2.0 - - def __post_init__(self): - self.dc_on = self.dc_visible - self.dc_delay - self.dc_off = self.dc_on + self.dc_duration - - def setup(self): - super().setup() - n1 = self.neuron = nest.Create("iaf_psc_alpha") - dc = self.dc_generator = nest.Create("dc_generator") - dc.amplitude = self.amplitude - vm = self.voltmeter = nest.Create("voltmeter") - vm.interval = self.resolution - nest.Connect(vm, n1) - - -@pytest.mark.parametrize("weight", [1.0]) -@testutil.use_simulation(IAFPSCAlphaDCSimulation) -class TestIAFPSCAlphaDC: - @pytest.mark.parametrize("delay", [0.1]) - def test_dc(self, simulation): - simulation.setup() - - dc_gen_spec = {"delay": simulation.delay, "weight": simulation.weight} - nest.Connect(simulation.dc_generator, simulation.neuron, syn_spec=dc_gen_spec) - - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expect_default) - assert actual == expected - - @pytest.mark.parametrize("resolution,delay", [(0.1, 0.1), (0.2, 0.2), (0.5, 0.5), (1.0, 1.0)]) - def test_dc_aligned(self, simulation): - simulation.setup() - - simulation.dc_generator.set( - amplitude=simulation.amplitude, - origin=simulation.origin, - start=simulation.arrival - simulation.resolution, - ) - nest.Connect( - simulation.dc_generator, - simulation.neuron, - syn_spec={"delay": simulation.delay}, - ) - - results = simulation.simulate() - - actual, expected = testutil.get_comparable_timesamples(results, expect_aligned) - assert actual == expected - - @pytest.mark.parametrize("resolution,delay", [(0.1, 0.1), (0.2, 0.2), (0.5, 0.5), (1.0, 1.0)]) - def test_dc_aligned_auto(self, simulation): - simulation.setup() - - simulation.dc_generator.set( - amplitude=simulation.amplitude, - origin=simulation.origin, - start=simulation.dc_on, - ) - dc_gen_spec = {"delay": simulation.dc_delay, "weight": simulation.weight} - nest.Connect(simulation.dc_generator, simulation.neuron, syn_spec=dc_gen_spec) - - results = simulation.simulate() - actual, expected = testutil.get_comparable_timesamples(results, expect_aligned) - assert actual == expected - - @pytest.mark.parametrize("resolution,delay", [(0.1, 0.1), (0.2, 0.2), (0.5, 0.5), (1.0, 1.0)]) - @pytest.mark.parametrize("duration", [10.0]) - def test_dc_aligned_stop(self, simulation): - simulation.setup() - - simulation.dc_generator.set( - amplitude=simulation.amplitude, - origin=simulation.origin, - start=simulation.dc_on, - stop=simulation.dc_off, - ) - dc_gen_spec = {"delay": simulation.dc_delay, "weight": simulation.weight} - nest.Connect(simulation.dc_generator, simulation.neuron, syn_spec=dc_gen_spec) - - results = simulation.simulate() - actual, expected = testutil.get_comparable_timesamples(results, expect_stop) - assert actual == expected - - -expect_default = np.array( - [ - [0.1, -70], - [0.2, -70], - [0.3, -69.602], - [0.4, -69.2079], - [0.5, -68.8178], - [0.6, -68.4316], - [0.7, -68.0492], - [0.8, -67.6706], - [0.9, -67.2958], - [1.0, -66.9247], - [1.1, -66.5572], - [1.2, -66.1935], - [1.3, -65.8334], - [1.4, -65.4768], - [1.5, -65.1238], - [1.6, -64.7743], - ] -) - - -expect_aligned = np.array( - [ - [2.5, -70], - [2.6, -70], - [2.7, -70], - [2.8, -70], - [2.9, -70], - [3.0, -70], - [3.1, -69.602], - [3.2, -69.2079], - [3.3, -68.8178], - [3.4, -68.4316], - [3.5, -68.0492], - [3.6, -67.6706], - [3.7, -67.2958], - [3.8, -66.9247], - [3.9, -66.5572], - [4.0, -66.1935], - [4.1, -65.8334], - [4.2, -65.4768], - ] -) - - -expect_stop = np.array( - [ - [2.5, -70], - [2.6, -70], - [2.7, -70], - [2.8, -70], - [2.9, -70], - [3.0, -70], - [3.1, -69.602], - [3.2, -69.2079], - [3.3, -68.8178], - [3.4, -68.4316], - [3.5, -68.0492], - [3.6, -67.6706], - [3.7, -67.2958], - [3.8, -66.9247], - [3.9, -66.5572], - [4.0, -66.1935], - [4.1, -65.8334], - [4.2, -65.4768], - [4.3, -65.1238], - [4.4, -64.7743], - [4.5, -64.4283], - [4.6, -64.0858], - [4.7, -63.7466], - [4.8, -63.4108], - [4.9, -63.0784], - [5.0, -62.7492], - [5.1, -62.8214], - [5.2, -62.8928], - [5.3, -62.9635], - [5.4, -63.0335], - [5.5, -63.1029], - [5.6, -63.1715], - [5.7, -63.2394], - [5.8, -63.3067], - [5.9, -63.3733], - [6.0, -63.4392], - ] -) diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py new file mode 100644 index 0000000000..dbc7976aa9 --- /dev/null +++ b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py @@ -0,0 +1,123 @@ +import nest +import pytest +import math + +@pytest.mark.parametrize( + "model", + [ + "iaf_psc_alpha_ps", + "iaf_psc_delta_ps", + "iaf_psc_exp_ps", + "iaf_psc_exp_ps_lossless", + ], +) +@pytest.mark.parametrize("resolution", [2**i for i in range(0, -14, -1)]) +@pytest.mark.parametrize( + "params", + [ + { + "E_L": 0.0, # resting potential in mV + "V_m": 0.0, # initial membrane potential in mV + "V_th": 2000.0, # spike threshold in mV + "I_e": 1000.0, # DC current in pA + "tau_m": 10.0, # membrane time constant in ms + "C_m": 250.0, # membrane capacity in pF + } + ], +) +@pytest.mark.parametrize("duration, tolerance", [(5.0, 1e-13), (500.0, 1e-9)]) +def test_iaf_ps_dc_accuracy(model, resolution, params, duration, tolerance): + """ + A DC current is injected for a finite duration. The membrane potential at + the end of the simulated interval is compared to the theoretical value for + different computation step sizes. + + Computation step sizes are specified as base 2 values. + + Two different intervals are tested. At the end of the first interval the membrane + potential still steeply increases. At the end of the second, the membrane + potential has within double precision already reached the limit for large t. + + The high accuracy of the neuron models is achieved by the use of Exact Integration [1] + and an appropriate arrangement of the terms [2]. For small computation step sizes the + accuracy at large simulation time decreases because of the accumulation of errors. + + Reference output is documented at the end of the script. + + Individual simulation results can be inspected by uncommented the call + to function print_details. + """ + nest.ResetKernel() + nest.SetKernelStatus({"tics_per_ms": 2**14, "resolution": resolution}) + + neuron = nest.Create(model, params=params) + nest.Simulate(duration) + + V_m = nest.GetStatus(neuron, "V_m")[0] + expected_V_m = ( + params["I_e"] * params["tau_m"] / params["C_m"] + * (1.0 - math.exp(-duration / params["tau_m"])) + ) + + assert math.fabs(V_m - expected_V_m) < tolerance + + +@pytest.mark.parametrize( + "model", + [ + "iaf_psc_alpha_ps", + "iaf_psc_delta_ps", + "iaf_psc_exp_ps", + "iaf_psc_exp_ps_lossless", + ], +) +@pytest.mark.parametrize("resolution", [2**i for i in range(0, -14, -1)]) +@pytest.mark.parametrize( + "params", + [ + { + "E_L": 0.0, # resting potential in mV + "V_m": 0.0, # initial membrane potential in mV + "V_th": 15.0, # spike threshold in mV + "I_e": 1000.0, # DC current in pA + "tau_m": 10.0, # membrane time constant in ms + "C_m": 250.0, # membrane capacity in pF + } + ], +) +@pytest.mark.parametrize("duration, tolerance", [(5.0, 1e-13)]) +def test_iaf_ps_dc_t_accuracy(model, resolution, params, duration, tolerance): + """ + A DC current is injected for a finite duration. The time of the first + spike is compared to the theoretical value for different computation + step sizes. + + Computation step sizes are specified as base 2 values. + + The high accuracy of the neuron models is achieved by the use of + Exact Integration [1] and an appropriate arrangement of the terms + [2]. For small computation step sizes the accuracy at large + simulation time decreases because of the accumulation of errors. + + The expected output is documented at the end of the script. + Individual simulation results can be inspected by uncommented the + call to function print_details. + """ + nest.ResetKernel() + nest.SetKernelStatus({"tics_per_ms": 2**14, "resolution": resolution}) + + neuron = nest.Create(model, params=params) + spike_recorder = nest.Create("spike_recorder") + nest.Connect(neuron, spike_recorder) + + nest.Simulate(duration) + + spike_times = nest.GetStatus(spike_recorder, "events")[0]["times"] + assert len(spike_times) == 1, "Neuron did not spike exactly once." + + t_spike = spike_times[0] + expected_t = -params["tau_m"] * math.log( + 1.0 - (params["C_m"] * params["V_th"]) / (params["tau_m"] * params["I_e"]) + ) + + assert math.fabs(t_spike - expected_t) < tolerance diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py new file mode 100644 index 0000000000..6ea2d5a62f --- /dev/null +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- +# +# test_iaf_psc_alpha.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 . + +import numpy as np +import nest +import pytest +import testutil + + +def test_iaf_psc_alpha_basic(): + """ + An overall test of the iaf_psc_alpha model connected + to some useful devices. + + A DC current is injected into the neuron using a current generator + device. The membrane potential as well as the spiking activity are + recorded by corresponding devices. + + It can be observed how the current charges the membrane, a spike + is emitted, the neuron becomes absolute refractory, and finally + starts to recover. + + The timing of the various events on the simulation grid is of + particular interest and crucial for the consistency of the + simulation scheme. + + Although 0.1 cannot be represented in the IEEE double data type, it + is safe to simulate with a resolution (computation step size) of 0.1 + ms because by default nest is built with a timebase enabling exact + representation of 0.1 ms. + """ + nest.ResetKernel() + + # Simulation variables + duration = 8.0 + resolution = 0.1 + delay = 1.0 + + # Create neuron model + neuron = nest.Create("iaf_psc_alpha") + + # Create devices + voltmeter = nest.Create("voltmeter") + dc_generator = nest.Create("dc_generator") + + voltmeter.interval = resolution + dc_generator.amplitude = 1000.0 + + # Connect devices + nest.Connect(voltmeter, neuron, syn_spec={"weight": 1.0, "delay": delay}) + nest.Connect(dc_generator, neuron, syn_spec={"weight": 1.0, "delay": resolution}) + + # Simulate + nest.Simulate(duration) + + # Collect spikes from spike_recorder + voltage = np.column_stack((voltmeter.events["times"], voltmeter.events["V_m"])) + + # Compare actual and expected spike times + results, approx_expected = testutil.get_comparable_timesamples( + voltage, + np.array( + [ + [0.1, -70], + [0.2, -70], + [0.3, -69.602], + [0.4, -69.2079], + [0.5, -68.8178], + [0.6, -68.4316], + [0.7, -68.0492], + [0.8, -67.6706], + [0.9, -67.2958], + [1.0, -66.9247], + [4.5, -56.0204], + [4.6, -55.7615], + [4.7, -55.5051], + [4.8, -55.2513], + [4.9, -55.0001], + [5.0, -70], + [5.1, -70], + [5.2, -70], + [5.3, -70], + [5.4, -70], + [5.5, -70], + [5.6, -70], + [5.7, -70], + [5.8, -70], + [5.9, -70], + [6.0, -70], + [6.1, -70], + [6.2, -70], + [6.3, -70], + [6.4, -70], + [6.5, -70], + [6.6, -70], + [6.7, -70], + [6.8, -70], + [6.9, -70], + [7.0, -70], + [7.1, -69.602], + [7.2, -69.2079], + [7.3, -68.8178], + [7.4, -68.4316], + [7.5, -68.0492], + [7.6, -67.6706], + [7.7, -67.2958], + [7.8, -66.9247], + [7.9, -66.5572], + ] + ), + ) + + # Assert approximate equality + assert results == approx_expected + + +def test_iaf_psc_alpha_i0(): + """ + Test of a specific feature of the iaf_psc_alpha + model. It is tested whether an internal DC current that is present + from the time of neuron initialization, correctly affects the membrane + potential. + + This is probably the simplest setup in which we can study how the + dynamics develops from an initial condition. + + When the DC current is supplied by a device external to the neuron + the situation is more complex because additional delays are introduced. + """ + nest.ResetKernel() + + # Simulation variables + resolution = 0.1 + delay = resolution + duration = 8.0 + + # Create neuron and devices + neuron = nest.Create("iaf_psc_alpha", params={"I_e": 1000.0}) + voltmeter = nest.Create("voltmeter") + voltmeter.interval = resolution + spike_recorder = nest.Create("spike_recorder") + + # Connect devices + nest.Connect(voltmeter, neuron, syn_spec={"weight": 1.0, "delay": delay}) + nest.Connect(neuron, spike_recorder, syn_spec={"weight": 1.0, "delay": delay}) + + # Simulate + nest.Simulate(duration) + + # Get voltmeter output + results = np.column_stack((voltmeter.events["times"], voltmeter.events["V_m"])) + + # Get spike times + spikes = np.column_stack( + ( + spike_recorder.events["senders"], + spike_recorder.events["times"], + ) + ) + + # Compare voltmeter output to expected + actual, expected = testutil.get_comparable_timesamples( + results, + np.array( + [ + [0.1, -69.602], + [0.2, -69.2079], + [0.3, -68.8178], + [0.4, -68.4316], + [0.5, -68.0492], + [4.3, -56.0204], + [4.4, -55.7615], + [4.5, -55.5051], + [4.6, -55.2513], + [4.7, -55.0001], + [4.8, -70], + [4.9, -70], + [5.0, -70], + ] + ), + ) + assert actual == expected + + # Compare spike times + assert spikes == pytest.approx( + np.array( + [ + [1, 4.8], + ] + ) + ) + + +@pytest.mark.parametrize("resolution", [0.1, 0.2, 0.5, 1.0]) +def test_iaf_psc_alpha_i0_refractory(resolution): + """ + Test a specific feature of the iaf_psc_alpha model. + + It is tested whether the voltage traces of simulations + carried out at different resolutions (computation step sizes) are well + aligned and identical when the neuron recovers from refractoriness. + + In grid based simulation a prerequisite is that the spike is reported at + a grid position shared by all the resolutions compared. + + Here, we compare resolutions 0.1, 0.2, 0.5, and 1.0 ms. Therefore, the + internal DC current is adjusted such (1450.0 pA) that the spike is + reported at time 3.0 ms, corresponding to computation step 30, 15, 6, + and 3, respectively. + + The results are consistent with those of iaf_psc_alpha_ps capable of + handling off-grid spike timing when the interpolation order is set to + 0. + """ + # Simulation variables + delay = resolution + duration = 8.0 + + nest.ResetKernel() + nest.SetKernelStatus({"resolution": resolution}) + + neuron = nest.Create("iaf_psc_alpha", params={"I_e": 1450.0}) + voltmeter = nest.Create("voltmeter", params={"interval": resolution}) + spike_recorder = nest.Create("spike_recorder") + + nest.Connect(voltmeter, neuron, syn_spec={"weight": 1.0, "delay": delay}) + nest.Connect(neuron, spike_recorder, syn_spec={"weight": 1.0, "delay": delay}) + + nest.Simulate(duration) + + events = voltmeter.events + times = events["times"] + V_m = events["V_m"] + results = np.column_stack((times, V_m)) + + actual, expected = testutil.get_comparable_timesamples( + results, + np.array( + [ + [0.1, -69.4229], + [0.2, -68.8515], + [0.3, -68.2858], + [0.4, -67.7258], + [0.5, -67.1713], + [0.6, -66.6223], + [0.7, -66.0788], + [0.8, -65.5407], + [0.9, -65.008], + [1.0, -64.4806], + [1.1, -63.9584], + [1.2, -63.4414], + [1.3, -62.9295], + [1.4, -62.4228], + [1.5, -61.9211], + [1.6, -61.4243], + [1.7, -60.9326], + [1.8, -60.4457], + [1.9, -59.9636], + [2.0, -59.4864], + [2.1, -59.0139], + [2.2, -58.5461], + [2.3, -58.0829], + [2.4, -57.6244], + [2.5, -57.1704], + [2.6, -56.721], + [2.7, -56.276], + [2.8, -55.8355], + [2.9, -55.3993], + [3.0, -70], + [3.1, -70], + [3.2, -70], + [3.3, -70], + [3.4, -70], + [3.5, -70], + [3.6, -70], + [3.7, -70], + [3.8, -70], + [3.9, -70], + [4.0, -70], + [4.1, -70], + [4.2, -70], + [4.3, -70], + [4.4, -70], + [4.5, -70], + [4.6, -70], + [4.7, -70], + [4.8, -70], + [4.9, -70], + [5.0, -70], + [5.1, -69.4229], + [5.2, -68.8515], + [5.3, -68.2858], + [5.4, -67.7258], + [5.5, -67.1713], + [5.6, -66.6223], + [5.7, -66.0788], + [5.8, -65.5407], + [5.9, -65.008], + [6.0, -64.4806], + [6.1, -63.9584], + [6.2, -63.4414], + [6.3, -62.9295], + [6.4, -62.4228], + [6.5, -61.9211], + [6.6, -61.4243], + [6.7, -60.9326], + [6.8, -60.4457], + [6.9, -59.9636], + ] + ), + ) + assert actual == expected diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py new file mode 100644 index 0000000000..a5e97fd068 --- /dev/null +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py @@ -0,0 +1,230 @@ +import nest +import pytest +import testutil +import numpy as np + +@pytest.mark.parametrize("resolution", [0.1, 0.2, 0.5, 1.0]) +@pytest.mark.parametrize("delay", [1.0]) +def test_1to2(resolution, delay): + """ + Checks the spike interaction of two iaf_psc_alpha model neurons. + + In order to obtain identical results for different computation step + sizes h, the SLI script needs to be independent of h. This is + achieved by specifying all time parameters in milliseconds (ms). In + particular the time of spike emission and the synaptic delay need to + be integer multiples of the computation step sizes to be + tested. test_iaf_dc_aligned_delay demonstrates the strategy for the + case of DC current input. + + A DC current in the pre-synaptic neuron is adjusted to cause a spike + at a grid position (t=3.0 ms) joined by all computation step sizes to + be tested. + + Note that in a neuron model where synaptic events are modeled by a + truncated exponential the effect of the incoming spike would be + visible at the time of impact (here, t=4.0 ms). This is because the + initial condition for the postsynaptic potential (PSP) has a + non-zero voltage component. For PSPs with finite rise time the + situation is different. In this case the voltage component of the + initial condition is zero (see documentation of + test_iaf_psp). Therefore, at the time of impact the PSP is only + visible in other components of the state vector. + """ + nest.ResetKernel() + nest.SetKernelStatus({"resolution": resolution}) + + # Create neurons + n1, n2 = nest.Create("iaf_psc_alpha", 2) + n1.I_e = 1450.0 + + # Create and connect voltmeter to n2 + vm = nest.Create("voltmeter", params={"interval": resolution}) + nest.Connect(vm, n2) + + # Connect neuron n1 to n2 + nest.Connect(n1, n2, syn_spec={"weight": 100.0, "delay": delay}) + + # Run the simulation + nest.Simulate(100.0) + + # Extract voltmeter data + events = nest.GetStatus(vm, "events")[0] + times = np.array(events["times"]).reshape(-1, 1) + V_m = np.array(events["V_m"]).reshape(-1, 1) + results = np.hstack((times, V_m)) + + actual, expected = testutil.get_comparable_timesamples(results, np.array( + [ + [2.5, -70], + [2.6, -70], + [2.7, -70], + [2.8, -70], + [2.9, -70], + [3.0, -70], + [3.1, -70], + [3.2, -70], + [3.3, -70], + [3.4, -70], + [3.5, -70], + [3.6, -70], + [3.7, -70], + [3.8, -70], + [3.9, -70], + [4.0, -70], + [4.1, -69.9974], + [4.2, -69.9899], + [4.3, -69.9781], + [4.4, -69.9624], + [4.5, -69.9434], + [4.6, -69.9213], + [4.7, -69.8967], + [4.8, -69.8699], + [4.9, -69.8411], + [5.0, -69.8108], + [5.1, -69.779], + [5.2, -69.7463], + [5.3, -69.7126], + [5.4, -69.6783], + [5.5, -69.6435], + [5.6, -69.6084], + [5.7, -69.5732], + ] + )) + assert actual == expected + + +@pytest.mark.parametrize("resolution", [0.1, 0.2, 0.5, 1.0]) +def test_1to2_default_delay(resolution): + """ + Same test but with the delay set via defaults of the model + """ + nest.ResetKernel() + nest.SetKernelStatus({"resolution": resolution}) + + # Set the delay via SetDefaults instead + nest.SetDefaults("static_synapse", {"delay": 1.0}) + + # Create neurons + n1, n2 = nest.Create("iaf_psc_alpha", 2) + n1.I_e = 1450.0 + + # Create and connect voltmeter to n2 + vm = nest.Create("voltmeter", params={"interval": resolution}) + nest.Connect(vm, n2) + + # Connect neuron n1 to n2 + nest.Connect(n1, n2, syn_spec={"weight": 100.0}) + + # Run the simulation + nest.Simulate(100.0) + + # Extract voltmeter data + events = nest.GetStatus(vm, "events")[0] + times = np.array(events["times"]).reshape(-1, 1) + V_m = np.array(events["V_m"]).reshape(-1, 1) + results = np.hstack((times, V_m)) + + actual, expected = testutil.get_comparable_timesamples(results, np.array( + [ + [2.5, -70], + [2.6, -70], + [2.7, -70], + [2.8, -70], + [2.9, -70], + [3.0, -70], + [3.1, -70], + [3.2, -70], + [3.3, -70], + [3.4, -70], + [3.5, -70], + [3.6, -70], + [3.7, -70], + [3.8, -70], + [3.9, -70], + [4.0, -70], + [4.1, -69.9974], + [4.2, -69.9899], + [4.3, -69.9781], + [4.4, -69.9624], + [4.5, -69.9434], + [4.6, -69.9213], + [4.7, -69.8967], + [4.8, -69.8699], + [4.9, -69.8411], + [5.0, -69.8108], + [5.1, -69.779], + [5.2, -69.7463], + [5.3, -69.7126], + [5.4, -69.6783], + [5.5, -69.6435], + [5.6, -69.6084], + [5.7, -69.5732], + ] + )) + assert actual == expected + + +@pytest.mark.parametrize("delay,resolution", [(2.0, 0.1)]) +@pytest.mark.parametrize("min_delay", [0.1, 0.5, 2.0]) +def test_1to2_mindelay_invariance(delay, resolution, min_delay): + """ + Same test with different mindelays. + """ + nest.ResetKernel() + nest.SetKernelStatus({"resolution": resolution}) + + assert min_delay <= delay + nest.set(min_delay=min_delay, max_delay=delay) + + # Create neurons + n1, n2 = nest.Create("iaf_psc_alpha", 2) + n1.I_e = 1450.0 + + # Create and connect voltmeter to n2 + vm = nest.Create("voltmeter", params={"interval": resolution}) + nest.Connect(vm, n2, syn_spec={"delay": delay}) + + # Connect neuron n1 to n2 + nest.Connect(n1, n2, syn_spec={"weight": 100.0, "delay": delay}) + + # Run the simulation + nest.Simulate(100.0) + + # Extract voltmeter data + events = nest.GetStatus(vm, "events")[0] + times = np.array(events["times"]).reshape(-1, 1) + V_m = np.array(events["V_m"]).reshape(-1, 1) + results = np.hstack((times, V_m)) + + actual, expected = testutil.get_comparable_timesamples(results, np.array( + [ + [0.1, -70], + [0.2, -70], + [0.3, -70], + [0.4, -70], + [0.5, -70], + [2.8, -70], + [2.9, -70], + [3.0, -70], + [3.1, -70], + [3.2, -70], + [3.3, -70], + [3.4, -70], + [3.5, -70], + [4.8, -70], + [4.9, -70], + [5.0, -70], + [5.1, -69.9974], + [5.2, -69.9899], + [5.3, -69.9781], + [5.4, -69.9624], + [5.5, -69.9434], + [5.6, -69.9213], + [5.7, -69.8967], + [5.8, -69.8699], + [5.9, -69.8411], + [6.0, -69.8108], + ] + )) + assert actual == expected diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py new file mode 100644 index 0000000000..0e83a3a9a8 --- /dev/null +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# +# test_iaf_psc_alpha_fudge.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 . + +import math +import numpy as np +import nest +import pytest +import testutil +from scipy.special import lambertw +def test_iaf_psc_alpha_fudge(): + """ + The peak time of the postsynaptic potential (PSP) is calculated using + the LambertW function. The theoretical peak voltage amplitude for a + postsynaptic current of amplitude 1pA is then used to adjust the + synaptic weight such that a PSP of amplitude 1mV is generated. The + success of this approach is verified by comparing the theoretical + value with the result of a simulation where a single spike is sent to + the neuron. + + The name of this test script has a historical explanation. Prior to + July 2009 the analytical expression for the peak time of the PSP was + not known to the NEST developers. Therefore the normalization factor + required to adjust the PSP amplitude was computed by root finding + outside of NEST. The factor was called "fudge" in examples and + application code. The root finding was not done in NEST because infix + mathematical notation only become available in SLI in January + 2009. The name "fudge" indicated that the origin of this value was not + obvious from the simulation scripts and usage was inherently dangerous + because a change of the time constants of the neuron model would + invalidate the value of "fudge". + """ + # Simulation variables + resolution = 0.1 + delay = resolution + duration = 20.0 + + # Create neuron and devices + neuron = nest.Create("iaf_psc_alpha") + voltmeter = nest.Create("voltmeter") + voltmeter.interval = resolution + + # Connect voltmeter + nest.Connect(voltmeter, neuron, syn_spec={"weight": 1.0, "delay": delay}) + + # Biophysical parameters + tau_m = 20.0 + tau_syn = 0.5 + C_m = 250.0 + + # Set neuron parameters + nest.SetStatus(neuron, { + "tau_m": tau_m, + "tau_syn_ex": tau_syn, + "tau_syn_in": tau_syn, + "C_m": C_m + }) + + # Compute fudge factors + a = tau_m / tau_syn + b = 1.0 / tau_syn - 1.0 / tau_m + t_max = (1.0 / b) * (-lambertw(-math.exp(-1.0 / a) / a, k=-1) - 1.0 / a).real + + V_max = ( + math.exp(1) + / (tau_syn * C_m * b) + * ((math.exp(-t_max / tau_m) - math.exp(-t_max / tau_syn)) / b - t_max * math.exp(-t_max / tau_syn)) + ) + + # Create spike generator to fire once at resolution + spike_gen = nest.Create("spike_generator", params={ + "precise_times": False, + "spike_times": [resolution], + }) + + # Connect spike generator to neuron + nest.Connect(spike_gen, neuron, syn_spec={ + "weight": float(1.0 / V_max), + "delay": delay + }) + + # Simulate + nest.Simulate(duration) + + # Extract membrane potential trace + volt_data = voltmeter.events + times = volt_data["times"] + V_m = volt_data["V_m"] + results = np.column_stack((times, V_m)) + + # Find time of peak voltage + actual_t_max = results[np.argmax(results[:, 1]), 0] + + # Assert the peak is close to theoretical t_max + 0.2 + assert actual_t_max == pytest.approx(t_max + 0.2, abs=0.05) \ No newline at end of file diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py new file mode 100644 index 0000000000..4a7e4399aa --- /dev/null +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py @@ -0,0 +1,135 @@ +import math + +import nest +import numpy as np +import pytest +import testutil + + +@pytest.mark.parametrize("min_delay", [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 1.0, 2.0]) +def test_iaf_psc_alpha_mindelay_create(min_delay): + """ + Tests automatic adjustment of min_delay. + + The simulation is run with a range of different min_delays. All + should give identical results. This is achieved by sampling the + membrane potential at a fixed interval. + """ + nest.ResetKernel() + + # Simulation variables + delay = 2.0 + duration = 10.5 + + neuron = nest.Create("iaf_psc_alpha") + voltmeter = nest.Create("voltmeter", params={"interval": 0.1}) + spike_recorder = nest.Create("spike_recorder") + dc_generator = nest.Create("dc_generator", params={"amplitude": 1000.0}) + + nest.Connect(voltmeter, neuron, syn_spec={"weight": 1.0, "delay": delay}) + nest.Connect(neuron, spike_recorder, syn_spec={"weight": 1.0, "delay": delay}) + nest.Connect(dc_generator, neuron, syn_spec={"weight": 1.0, "delay": delay}) + + # Force `min_delay` by connecting two throwaway neurons + throwaway = nest.Create("iaf_psc_alpha", 2) + nest.Connect(throwaway[0], throwaway[1], syn_spec={"weight": 1.0, "delay": min_delay}) + + nest.Simulate(duration) + + results = np.column_stack((voltmeter.events["times"], voltmeter.events["V_m"])) + + actual, expected = testutil.get_comparable_timesamples(results, expected_mindelay) + assert actual == expected + + +@pytest.mark.parametrize("min_delay", [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 1.0, 2.0]) +def test_iaf_psc_alpha_mindelay_set(min_delay): + """ + Tests explicitly setting min_delay. + + The simulation is run with a range of different min_delays. All + should give identical results. This is achieved by sampling the + membrane potential at a fixed interval. + """ + nest.ResetKernel() + + # Simulation variables + delay = 2.0 + duration = 10.5 + + # Set up test min_delay conditions + nest.set(min_delay=min_delay, max_delay=delay) + nest.SetDefaults("static_synapse", {"delay": delay}) + + neuron = nest.Create("iaf_psc_alpha") + voltmeter = nest.Create("voltmeter", params={"interval": 0.1}) + spike_recorder = nest.Create("spike_recorder") + dc_generator = nest.Create("dc_generator", params={"amplitude": 1000.0}) + + nest.Connect(voltmeter, neuron, syn_spec={"weight": 1.0, "delay": delay}) + nest.Connect(neuron, spike_recorder, syn_spec={"weight": 1.0, "delay": delay}) + nest.Connect(dc_generator, neuron, syn_spec={"weight": 1.0, "delay": delay}) + + nest.Simulate(duration) + + results = np.column_stack((voltmeter.events["times"], voltmeter.events["V_m"])) + + actual, expected = testutil.get_comparable_timesamples(results, expected_mindelay) + assert actual == expected + +@pytest.mark.parametrize("min_delay", [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 1.0, 2.0]) +def test_iaf_psc_alpha_mindelay_simblocks(min_delay): + """ + Tests explicitly setting min_delay across 21 simulation calls. + + The simulation is run with a range of different min_delays. All + should give identical results. This is achieved by sampling the + membrane potential at a fixed interval. + """ + nest.ResetKernel() + + # Simulation variables + delay = 2.0 + + # Set up test min_delay conditions + nest.set(min_delay=min_delay, max_delay=delay) + nest.SetDefaults("static_synapse", {"delay": delay}) + + neuron = nest.Create("iaf_psc_alpha") + voltmeter = nest.Create("voltmeter", params={"interval": 0.1}) + spike_recorder = nest.Create("spike_recorder") + dc_generator = nest.Create("dc_generator", params={"amplitude": 1000.0}) + + nest.Connect(voltmeter, neuron, syn_spec={"weight": 1.0, "delay": delay}) + nest.Connect(neuron, spike_recorder, syn_spec={"weight": 1.0, "delay": delay}) + nest.Connect(dc_generator, neuron, syn_spec={"weight": 1.0, "delay": delay}) + + for _ in range(22): + nest.Simulate(0.5) + + results = np.column_stack((voltmeter.events["times"], voltmeter.events["V_m"])) + + actual, expected = testutil.get_comparable_timesamples(results, expected_mindelay) + assert actual == expected + + +def test_kernel_precision(): + nest.ResetKernel() + nest.set(tics_per_ms=2**14, resolution=2**0) + assert math.frexp(nest.ms_per_tic) == (0.5, -13) + + +expected_mindelay = np.array( + [ + [1.000000e00, -7.000000e01], + [2.000000e00, -7.000000e01], + [3.000000e00, -6.655725e01], + [4.000000e00, -6.307837e01], + [5.000000e00, -5.993054e01], + [6.000000e00, -5.708227e01], + [7.000000e00, -7.000000e01], + [8.000000e00, -7.000000e01], + [9.000000e00, -6.960199e01], + [1.000000e01, -6.583337e01], + ] +) diff --git a/testsuite/pytests/utilities/testsimulation.py b/testsuite/pytests/utilities/testsimulation.py deleted file mode 100644 index 53db86a37b..0000000000 --- a/testsuite/pytests/utilities/testsimulation.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# -# testsimulation.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 . - -import dataclasses - -import nest -import numpy as np -import testutil - - -@dataclasses.dataclass -class Simulation: - local_num_threads: int = 1 - resolution: float = 0.1 - duration: float = 8.0 - weight: float = 100.0 - delay: float = 1.0 - - def setup(self): - pass - - def simulate(self): - nest.Simulate(self.duration) - if hasattr(self, "voltmeter"): - return np.column_stack((self.voltmeter.events["times"], self.voltmeter.events["V_m"])) - - def run(self): - self.setup() - return self.simulate() - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - testutil.create_dataclass_fixtures(cls) diff --git a/testsuite/pytests/utilities/testutil.py b/testsuite/pytests/utilities/testutil.py index 54a9e0dcdc..5c7e277ff5 100644 --- a/testsuite/pytests/utilities/testutil.py +++ b/testsuite/pytests/utilities/testutil.py @@ -19,17 +19,10 @@ # You should have received a copy of the GNU General Public License # along with NEST. If not, see . -import dataclasses -import sys - import numpy as np import pytest -def parameter_fixture(name, default_factory=lambda: None): - return pytest.fixture(autouse=True, name=name)(lambda request: getattr(request, "param", default_factory())) - - def dict_is_subset_of(small, big): """ Return true if dict `small` is subset of dict `big`. @@ -48,6 +41,32 @@ def dict_is_subset_of(small, big): def isin_approx(A, B, tol=1e-06): + """ + Determine whether each element in array A approximately exists in array B within a given tolerance. + + Parameters + ---------- + A : array-like + Input array of values to check for approximate membership in B. + B : array-like + Reference array to check against. + tol : float, optional + Absolute tolerance within which two values are considered equal. Default is 1e-6. + + Returns + ------- + np.ndarray + A boolean array of the same shape as A, where each element is True if the corresponding + value in A is within `tol` of any value in B. + + Notes + ----- + - Uses a vectorized, sorted search to efficiently find nearest neighbors in B for each value in A. + - Both lower and upper neighbor distances are computed to detect proximity. + - Especially useful when comparing floating-point timestamps or simulation outputs where exact + equality is unrealistic due to numerical noise. + """ + A = np.asarray(A) B = np.asarray(B) @@ -68,48 +87,41 @@ def isin_approx(A, B, tol=1e-06): def get_comparable_timesamples(actual, expected): + """ + Filter and align time-series data from simulation and reference arrays + for meaningful comparison with approximate equality. + + Parameters + ---------- + actual : np.ndarray + A 2D array of shape (N, 2), where the first column is time and the second is the simulated value. + expected : np.ndarray + A 2D array of shape (M, 2), with the same structure as `actual`, representing expected reference data. + + Returns + ------- + Tuple[np.ndarray, pytest.approx] + A pair of arrays aligned by approximately matching time samples: + - A filtered version of `actual` at time points close to those in `expected`. + - A `pytest.approx`-wrapped array of `expected` values corresponding to the matched points. + + Raises + ------ + AssertionError + If no matching time points are found between `actual` and `expected`, the test fails early. + + Notes + ----- + This function supports robust comparison of simulation outputs against expected results + by: + - Tolerantly aligning timestamps via approximate equality (`isin_approx`). + - Ensuring non-empty overlap to prevent false test passes on mismatched data. + - Returning results in a format suitable for `assert actual == expected`. + + Typical use-case is inside a pytest unit test that compares time-series simulation output + to a precomputed reference array, using `pytest.approx` to allow for small numerical errors. + """ simulated_points = isin_approx(actual[:, 0], expected[:, 0]) expected_points = isin_approx(expected[:, 0], actual[:, 0]) assert len(actual[simulated_points]) > 0, "The recorded data did not contain any relevant timesamples" return actual[simulated_points], pytest.approx(expected[expected_points]) - - -def create_dataclass_fixtures(cls, module_name=None): - for field, type_ in getattr(cls, "__annotations__", {}).items(): - if isinstance(field, dataclasses.Field): - name = field.name - if field.default_factory is not dataclasses.MISSING: - default = field.default_factory - else: - - def default(d=field.default): - return d - - else: - name = field - attr = getattr(cls, field) - # We may be receiving a mixture of literal default values, field defaults, - # and field default factories. - if isinstance(attr, dataclasses.Field): - if attr.default_factory is not dataclasses.MISSING: - default = attr.default_factory - else: - - def default(d=attr.default): - return d - - else: - - def default(d=attr): - return d - - setattr( - sys.modules[module_name or cls.__module__], - name, - parameter_fixture(name, default), - ) - - -def use_simulation(cls): - # If `mark` receives one argument that is a class, it decorates that arg. - return pytest.mark.simulation(cls, "") From c2b741498563d67381d4efe362bdee4ae56ee7c7 Mon Sep 17 00:00:00 2001 From: Robin De Schepper Date: Wed, 25 Jun 2025 10:40:02 +0200 Subject: [PATCH 2/5] fix copyright headers --- .../sli2py_neurons/test_iaf_ps_dc_accuracy.py | 21 ++++++++++++++++++ .../sli2py_neurons/test_iaf_psc_alpha_1to2.py | 22 +++++++++++++++++++ .../test_iaf_psc_alpha_mindelay.py | 22 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py index dbc7976aa9..ab6930a3a8 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py @@ -1,3 +1,24 @@ +# -*- coding: utf-8 -*- +# +# test_iaf_ps_dc_accuracy.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 . + import nest import pytest import math diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py index a5e97fd068..1163cce34e 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py @@ -1,3 +1,25 @@ +# -*- coding: utf-8 -*- +# +# test_iaf_psc_alpha_1to2.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 . + + import nest import pytest import testutil diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py index 4a7e4399aa..c3668d7a35 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py @@ -1,3 +1,25 @@ +# -*- coding: utf-8 -*- +# +# test_iaf_psc_alpha_mindelay.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 . + + import math import nest From 9ed820a52c4c1e7dd4a339989b5789ae73a75ac9 Mon Sep 17 00:00:00 2001 From: Robin De Schepper Date: Wed, 25 Jun 2025 10:40:30 +0200 Subject: [PATCH 3/5] black formatting --- .../sli2py_neurons/test_iaf_ps_dc_accuracy.py | 32 ++- .../sli2py_neurons/test_iaf_psc_alpha_1to2.py | 218 +++++++++--------- .../test_iaf_psc_alpha_fudge.py | 33 ++- .../test_iaf_psc_alpha_mindelay.py | 1 + 4 files changed, 144 insertions(+), 140 deletions(-) diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py index ab6930a3a8..3512e9691e 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py @@ -23,6 +23,7 @@ import pytest import math + @pytest.mark.parametrize( "model", [ @@ -37,12 +38,12 @@ "params", [ { - "E_L": 0.0, # resting potential in mV - "V_m": 0.0, # initial membrane potential in mV + "E_L": 0.0, # resting potential in mV + "V_m": 0.0, # initial membrane potential in mV "V_th": 2000.0, # spike threshold in mV - "I_e": 1000.0, # DC current in pA - "tau_m": 10.0, # membrane time constant in ms - "C_m": 250.0, # membrane capacity in pF + "I_e": 1000.0, # DC current in pA + "tau_m": 10.0, # membrane time constant in ms + "C_m": 250.0, # membrane capacity in pF } ], ) @@ -75,10 +76,7 @@ def test_iaf_ps_dc_accuracy(model, resolution, params, duration, tolerance): nest.Simulate(duration) V_m = nest.GetStatus(neuron, "V_m")[0] - expected_V_m = ( - params["I_e"] * params["tau_m"] / params["C_m"] - * (1.0 - math.exp(-duration / params["tau_m"])) - ) + expected_V_m = params["I_e"] * params["tau_m"] / params["C_m"] * (1.0 - math.exp(-duration / params["tau_m"])) assert math.fabs(V_m - expected_V_m) < tolerance @@ -97,12 +95,12 @@ def test_iaf_ps_dc_accuracy(model, resolution, params, duration, tolerance): "params", [ { - "E_L": 0.0, # resting potential in mV - "V_m": 0.0, # initial membrane potential in mV - "V_th": 15.0, # spike threshold in mV - "I_e": 1000.0, # DC current in pA - "tau_m": 10.0, # membrane time constant in ms - "C_m": 250.0, # membrane capacity in pF + "E_L": 0.0, # resting potential in mV + "V_m": 0.0, # initial membrane potential in mV + "V_th": 15.0, # spike threshold in mV + "I_e": 1000.0, # DC current in pA + "tau_m": 10.0, # membrane time constant in ms + "C_m": 250.0, # membrane capacity in pF } ], ) @@ -137,8 +135,6 @@ def test_iaf_ps_dc_t_accuracy(model, resolution, params, duration, tolerance): assert len(spike_times) == 1, "Neuron did not spike exactly once." t_spike = spike_times[0] - expected_t = -params["tau_m"] * math.log( - 1.0 - (params["C_m"] * params["V_th"]) / (params["tau_m"] * params["I_e"]) - ) + expected_t = -params["tau_m"] * math.log(1.0 - (params["C_m"] * params["V_th"]) / (params["tau_m"] * params["I_e"])) assert math.fabs(t_spike - expected_t) < tolerance diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py index 1163cce34e..6622f0b81f 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py @@ -25,6 +25,7 @@ import testutil import numpy as np + @pytest.mark.parametrize("resolution", [0.1, 0.2, 0.5, 1.0]) @pytest.mark.parametrize("delay", [1.0]) def test_1to2(resolution, delay): @@ -76,43 +77,46 @@ def test_1to2(resolution, delay): V_m = np.array(events["V_m"]).reshape(-1, 1) results = np.hstack((times, V_m)) - actual, expected = testutil.get_comparable_timesamples(results, np.array( - [ - [2.5, -70], - [2.6, -70], - [2.7, -70], - [2.8, -70], - [2.9, -70], - [3.0, -70], - [3.1, -70], - [3.2, -70], - [3.3, -70], - [3.4, -70], - [3.5, -70], - [3.6, -70], - [3.7, -70], - [3.8, -70], - [3.9, -70], - [4.0, -70], - [4.1, -69.9974], - [4.2, -69.9899], - [4.3, -69.9781], - [4.4, -69.9624], - [4.5, -69.9434], - [4.6, -69.9213], - [4.7, -69.8967], - [4.8, -69.8699], - [4.9, -69.8411], - [5.0, -69.8108], - [5.1, -69.779], - [5.2, -69.7463], - [5.3, -69.7126], - [5.4, -69.6783], - [5.5, -69.6435], - [5.6, -69.6084], - [5.7, -69.5732], - ] - )) + actual, expected = testutil.get_comparable_timesamples( + results, + np.array( + [ + [2.5, -70], + [2.6, -70], + [2.7, -70], + [2.8, -70], + [2.9, -70], + [3.0, -70], + [3.1, -70], + [3.2, -70], + [3.3, -70], + [3.4, -70], + [3.5, -70], + [3.6, -70], + [3.7, -70], + [3.8, -70], + [3.9, -70], + [4.0, -70], + [4.1, -69.9974], + [4.2, -69.9899], + [4.3, -69.9781], + [4.4, -69.9624], + [4.5, -69.9434], + [4.6, -69.9213], + [4.7, -69.8967], + [4.8, -69.8699], + [4.9, -69.8411], + [5.0, -69.8108], + [5.1, -69.779], + [5.2, -69.7463], + [5.3, -69.7126], + [5.4, -69.6783], + [5.5, -69.6435], + [5.6, -69.6084], + [5.7, -69.5732], + ] + ), + ) assert actual == expected @@ -147,43 +151,46 @@ def test_1to2_default_delay(resolution): V_m = np.array(events["V_m"]).reshape(-1, 1) results = np.hstack((times, V_m)) - actual, expected = testutil.get_comparable_timesamples(results, np.array( - [ - [2.5, -70], - [2.6, -70], - [2.7, -70], - [2.8, -70], - [2.9, -70], - [3.0, -70], - [3.1, -70], - [3.2, -70], - [3.3, -70], - [3.4, -70], - [3.5, -70], - [3.6, -70], - [3.7, -70], - [3.8, -70], - [3.9, -70], - [4.0, -70], - [4.1, -69.9974], - [4.2, -69.9899], - [4.3, -69.9781], - [4.4, -69.9624], - [4.5, -69.9434], - [4.6, -69.9213], - [4.7, -69.8967], - [4.8, -69.8699], - [4.9, -69.8411], - [5.0, -69.8108], - [5.1, -69.779], - [5.2, -69.7463], - [5.3, -69.7126], - [5.4, -69.6783], - [5.5, -69.6435], - [5.6, -69.6084], - [5.7, -69.5732], - ] - )) + actual, expected = testutil.get_comparable_timesamples( + results, + np.array( + [ + [2.5, -70], + [2.6, -70], + [2.7, -70], + [2.8, -70], + [2.9, -70], + [3.0, -70], + [3.1, -70], + [3.2, -70], + [3.3, -70], + [3.4, -70], + [3.5, -70], + [3.6, -70], + [3.7, -70], + [3.8, -70], + [3.9, -70], + [4.0, -70], + [4.1, -69.9974], + [4.2, -69.9899], + [4.3, -69.9781], + [4.4, -69.9624], + [4.5, -69.9434], + [4.6, -69.9213], + [4.7, -69.8967], + [4.8, -69.8699], + [4.9, -69.8411], + [5.0, -69.8108], + [5.1, -69.779], + [5.2, -69.7463], + [5.3, -69.7126], + [5.4, -69.6783], + [5.5, -69.6435], + [5.6, -69.6084], + [5.7, -69.5732], + ] + ), + ) assert actual == expected @@ -219,34 +226,37 @@ def test_1to2_mindelay_invariance(delay, resolution, min_delay): V_m = np.array(events["V_m"]).reshape(-1, 1) results = np.hstack((times, V_m)) - actual, expected = testutil.get_comparable_timesamples(results, np.array( - [ - [0.1, -70], - [0.2, -70], - [0.3, -70], - [0.4, -70], - [0.5, -70], - [2.8, -70], - [2.9, -70], - [3.0, -70], - [3.1, -70], - [3.2, -70], - [3.3, -70], - [3.4, -70], - [3.5, -70], - [4.8, -70], - [4.9, -70], - [5.0, -70], - [5.1, -69.9974], - [5.2, -69.9899], - [5.3, -69.9781], - [5.4, -69.9624], - [5.5, -69.9434], - [5.6, -69.9213], - [5.7, -69.8967], - [5.8, -69.8699], - [5.9, -69.8411], - [6.0, -69.8108], - ] - )) + actual, expected = testutil.get_comparable_timesamples( + results, + np.array( + [ + [0.1, -70], + [0.2, -70], + [0.3, -70], + [0.4, -70], + [0.5, -70], + [2.8, -70], + [2.9, -70], + [3.0, -70], + [3.1, -70], + [3.2, -70], + [3.3, -70], + [3.4, -70], + [3.5, -70], + [4.8, -70], + [4.9, -70], + [5.0, -70], + [5.1, -69.9974], + [5.2, -69.9899], + [5.3, -69.9781], + [5.4, -69.9624], + [5.5, -69.9434], + [5.6, -69.9213], + [5.7, -69.8967], + [5.8, -69.8699], + [5.9, -69.8411], + [6.0, -69.8108], + ] + ), + ) assert actual == expected diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py index 0e83a3a9a8..3d267d4e43 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py @@ -25,6 +25,8 @@ import pytest import testutil from scipy.special import lambertw + + def test_iaf_psc_alpha_fudge(): """ The peak time of the postsynaptic potential (PSP) is calculated using @@ -66,12 +68,7 @@ def test_iaf_psc_alpha_fudge(): C_m = 250.0 # Set neuron parameters - nest.SetStatus(neuron, { - "tau_m": tau_m, - "tau_syn_ex": tau_syn, - "tau_syn_in": tau_syn, - "C_m": C_m - }) + nest.SetStatus(neuron, {"tau_m": tau_m, "tau_syn_ex": tau_syn, "tau_syn_in": tau_syn, "C_m": C_m}) # Compute fudge factors a = tau_m / tau_syn @@ -79,22 +76,22 @@ def test_iaf_psc_alpha_fudge(): t_max = (1.0 / b) * (-lambertw(-math.exp(-1.0 / a) / a, k=-1) - 1.0 / a).real V_max = ( - math.exp(1) - / (tau_syn * C_m * b) - * ((math.exp(-t_max / tau_m) - math.exp(-t_max / tau_syn)) / b - t_max * math.exp(-t_max / tau_syn)) + math.exp(1) + / (tau_syn * C_m * b) + * ((math.exp(-t_max / tau_m) - math.exp(-t_max / tau_syn)) / b - t_max * math.exp(-t_max / tau_syn)) ) # Create spike generator to fire once at resolution - spike_gen = nest.Create("spike_generator", params={ - "precise_times": False, - "spike_times": [resolution], - }) + spike_gen = nest.Create( + "spike_generator", + params={ + "precise_times": False, + "spike_times": [resolution], + }, + ) # Connect spike generator to neuron - nest.Connect(spike_gen, neuron, syn_spec={ - "weight": float(1.0 / V_max), - "delay": delay - }) + nest.Connect(spike_gen, neuron, syn_spec={"weight": float(1.0 / V_max), "delay": delay}) # Simulate nest.Simulate(duration) @@ -109,4 +106,4 @@ def test_iaf_psc_alpha_fudge(): actual_t_max = results[np.argmax(results[:, 1]), 0] # Assert the peak is close to theoretical t_max + 0.2 - assert actual_t_max == pytest.approx(t_max + 0.2, abs=0.05) \ No newline at end of file + assert actual_t_max == pytest.approx(t_max + 0.2, abs=0.05) diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py index c3668d7a35..5ad75a51ac 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_mindelay.py @@ -99,6 +99,7 @@ def test_iaf_psc_alpha_mindelay_set(min_delay): actual, expected = testutil.get_comparable_timesamples(results, expected_mindelay) assert actual == expected + @pytest.mark.parametrize("min_delay", [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 1.0, 2.0]) def test_iaf_psc_alpha_mindelay_simblocks(min_delay): """ From e11a6b9b3d426b902d4185eca64b1a17749b9385 Mon Sep 17 00:00:00 2001 From: Robin De Schepper Date: Wed, 25 Jun 2025 10:51:28 +0200 Subject: [PATCH 4/5] isort --- testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py | 3 ++- testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py | 2 +- testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py | 2 +- testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py index 3512e9691e..f77a4d2b48 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py @@ -19,9 +19,10 @@ # You should have received a copy of the GNU General Public License # along with NEST. If not, see . +import math + import nest import pytest -import math @pytest.mark.parametrize( diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py index 6ea2d5a62f..f2578f2af9 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py @@ -19,8 +19,8 @@ # You should have received a copy of the GNU General Public License # along with NEST. If not, see . -import numpy as np import nest +import numpy as np import pytest import testutil diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py index 6622f0b81f..30e7dd6bec 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_1to2.py @@ -21,9 +21,9 @@ import nest +import numpy as np import pytest import testutil -import numpy as np @pytest.mark.parametrize("resolution", [0.1, 0.2, 0.5, 1.0]) diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py index 3d267d4e43..afed8574b3 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py @@ -20,8 +20,9 @@ # along with NEST. If not, see . import math -import numpy as np + import nest +import numpy as np import pytest import testutil from scipy.special import lambertw From 57e0d29666eb0bc838e6066f20d1c046943045e9 Mon Sep 17 00:00:00 2001 From: Robin De Schepper Date: Thu, 26 Jun 2025 10:35:08 +0200 Subject: [PATCH 5/5] use modern nest notations --- .../sli2py_neurons/test_iaf_ps_dc_accuracy.py | 65 ++++++++----------- .../sli2py_neurons/test_iaf_psc_alpha.py | 7 +- .../test_iaf_psc_alpha_fudge.py | 13 ++-- 3 files changed, 39 insertions(+), 46 deletions(-) diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py index f77a4d2b48..e9783c9285 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_ps_dc_accuracy.py @@ -34,22 +34,9 @@ "iaf_psc_exp_ps_lossless", ], ) -@pytest.mark.parametrize("resolution", [2**i for i in range(0, -14, -1)]) -@pytest.mark.parametrize( - "params", - [ - { - "E_L": 0.0, # resting potential in mV - "V_m": 0.0, # initial membrane potential in mV - "V_th": 2000.0, # spike threshold in mV - "I_e": 1000.0, # DC current in pA - "tau_m": 10.0, # membrane time constant in ms - "C_m": 250.0, # membrane capacity in pF - } - ], -) +@pytest.mark.parametrize("resolution", [2 ** i for i in range(0, -14, -1)]) @pytest.mark.parametrize("duration, tolerance", [(5.0, 1e-13), (500.0, 1e-9)]) -def test_iaf_ps_dc_accuracy(model, resolution, params, duration, tolerance): +def test_iaf_ps_dc_accuracy(model, resolution, duration, tolerance): """ A DC current is injected for a finite duration. The membrane potential at the end of the simulated interval is compared to the theoretical value for @@ -71,15 +58,23 @@ def test_iaf_ps_dc_accuracy(model, resolution, params, duration, tolerance): to function print_details. """ nest.ResetKernel() - nest.SetKernelStatus({"tics_per_ms": 2**14, "resolution": resolution}) + nest.set(tics_per_ms=2 ** 14, resolution=resolution) + + params = { + "E_L": 0.0, # resting potential in mV + "V_m": 0.0, # initial membrane potential in mV + "V_th": 2000.0, # spike threshold in mV + "I_e": 1000.0, # DC current in pA + "tau_m": 10.0, # membrane time constant in ms + "C_m": 250.0, # membrane capacity in pF + } neuron = nest.Create(model, params=params) nest.Simulate(duration) - V_m = nest.GetStatus(neuron, "V_m")[0] - expected_V_m = params["I_e"] * params["tau_m"] / params["C_m"] * (1.0 - math.exp(-duration / params["tau_m"])) + expected_vm = params["I_e"] * params["tau_m"] / params["C_m"] * (1.0 - math.exp(-duration / params["tau_m"])) - assert math.fabs(V_m - expected_V_m) < tolerance + assert neuron.V_m - pytest.approx(expected_vm, abs=tolerance) @pytest.mark.parametrize( @@ -91,22 +86,9 @@ def test_iaf_ps_dc_accuracy(model, resolution, params, duration, tolerance): "iaf_psc_exp_ps_lossless", ], ) -@pytest.mark.parametrize("resolution", [2**i for i in range(0, -14, -1)]) -@pytest.mark.parametrize( - "params", - [ - { - "E_L": 0.0, # resting potential in mV - "V_m": 0.0, # initial membrane potential in mV - "V_th": 15.0, # spike threshold in mV - "I_e": 1000.0, # DC current in pA - "tau_m": 10.0, # membrane time constant in ms - "C_m": 250.0, # membrane capacity in pF - } - ], -) +@pytest.mark.parametrize("resolution", [2 ** i for i in range(0, -14, -1)]) @pytest.mark.parametrize("duration, tolerance", [(5.0, 1e-13)]) -def test_iaf_ps_dc_t_accuracy(model, resolution, params, duration, tolerance): +def test_iaf_ps_dc_t_accuracy(model, resolution, duration, tolerance): """ A DC current is injected for a finite duration. The time of the first spike is compared to the theoretical value for different computation @@ -124,7 +106,16 @@ def test_iaf_ps_dc_t_accuracy(model, resolution, params, duration, tolerance): call to function print_details. """ nest.ResetKernel() - nest.SetKernelStatus({"tics_per_ms": 2**14, "resolution": resolution}) + nest.set(tics_per_ms=2 ** 14, resolution=resolution) + + params = { + "E_L": 0.0, # resting potential in mV + "V_m": 0.0, # initial membrane potential in mV + "V_th": 15.0, # spike threshold in mV + "I_e": 1000.0, # DC current in pA + "tau_m": 10.0, # membrane time constant in ms + "C_m": 250.0, # membrane capacity in pF + } neuron = nest.Create(model, params=params) spike_recorder = nest.Create("spike_recorder") @@ -132,10 +123,10 @@ def test_iaf_ps_dc_t_accuracy(model, resolution, params, duration, tolerance): nest.Simulate(duration) - spike_times = nest.GetStatus(spike_recorder, "events")[0]["times"] + spike_times = spike_recorder.get("events", "times") assert len(spike_times) == 1, "Neuron did not spike exactly once." t_spike = spike_times[0] expected_t = -params["tau_m"] * math.log(1.0 - (params["C_m"] * params["V_th"]) / (params["tau_m"] * params["I_e"])) - assert math.fabs(t_spike - expected_t) < tolerance + assert t_spike == pytest.approx(expected_t, abs=tolerance) diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py index f2578f2af9..1299d4d2fb 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha.py @@ -235,7 +235,7 @@ def test_iaf_psc_alpha_i0_refractory(resolution): duration = 8.0 nest.ResetKernel() - nest.SetKernelStatus({"resolution": resolution}) + nest.resolution = resolution neuron = nest.Create("iaf_psc_alpha", params={"I_e": 1450.0}) voltmeter = nest.Create("voltmeter", params={"interval": resolution}) @@ -246,9 +246,8 @@ def test_iaf_psc_alpha_i0_refractory(resolution): nest.Simulate(duration) - events = voltmeter.events - times = events["times"] - V_m = events["V_m"] + times = voltmeter.events["times"] + V_m = voltmeter.events["V_m"] results = np.column_stack((times, V_m)) actual, expected = testutil.get_comparable_timesamples( diff --git a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py index afed8574b3..80e11e1798 100644 --- a/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py +++ b/testsuite/pytests/sli2py_neurons/test_iaf_psc_alpha_fudge.py @@ -69,14 +69,17 @@ def test_iaf_psc_alpha_fudge(): C_m = 250.0 # Set neuron parameters - nest.SetStatus(neuron, {"tau_m": tau_m, "tau_syn_ex": tau_syn, "tau_syn_in": tau_syn, "C_m": C_m}) + neuron.tau_m = tau_m + neuron.tau_syn_ex = tau_syn + neuron.tau_syn_in = tau_syn + neuron.C_m = C_m # Compute fudge factors a = tau_m / tau_syn b = 1.0 / tau_syn - 1.0 / tau_m t_max = (1.0 / b) * (-lambertw(-math.exp(-1.0 / a) / a, k=-1) - 1.0 / a).real - V_max = ( + v_max = ( math.exp(1) / (tau_syn * C_m * b) * ((math.exp(-t_max / tau_m) - math.exp(-t_max / tau_syn)) / b - t_max * math.exp(-t_max / tau_syn)) @@ -92,7 +95,7 @@ def test_iaf_psc_alpha_fudge(): ) # Connect spike generator to neuron - nest.Connect(spike_gen, neuron, syn_spec={"weight": float(1.0 / V_max), "delay": delay}) + nest.Connect(spike_gen, neuron, syn_spec={"weight": float(1.0 / v_max), "delay": delay}) # Simulate nest.Simulate(duration) @@ -100,8 +103,8 @@ def test_iaf_psc_alpha_fudge(): # Extract membrane potential trace volt_data = voltmeter.events times = volt_data["times"] - V_m = volt_data["V_m"] - results = np.column_stack((times, V_m)) + v_m = volt_data["V_m"] + results = np.column_stack((times, v_m)) # Find time of peak voltage actual_t_max = results[np.argmax(results[:, 1]), 0]