diff --git a/testsuite/do_tests.sh b/testsuite/do_tests.sh index 640570bbc4..470a3576af 100755 --- a/testsuite/do_tests.sh +++ b/testsuite/do_tests.sh @@ -145,6 +145,7 @@ ls -la "${TEST_BASEDIR}" NEST="nest_serial" HAVE_MPI="$(sli -c 'statusdict/have_mpi :: =only')" +HAVE_OPENMP="$(sli -c 'is_threaded =only')" if test "${HAVE_MPI}" = "true"; then MPI_LAUNCHER="$(sli -c 'statusdict/mpiexec :: =only')" @@ -503,17 +504,28 @@ if test "${PYTHON}"; then PYNEST_TEST_DIR="${TEST_BASEDIR}/pytests" XUNIT_NAME="07_pynesttests" - # Run all tests except those in the mpi* subdirectories because they cannot be run concurrently + # Run all tests except those in the mpi* and sli2py_mpi subdirectories because they cannot be run concurrently XUNIT_FILE="${REPORTDIR}/${XUNIT_NAME}.xml" env set +e "${PYTHON}" -m pytest --verbose --timeout $TIME_LIMIT --junit-xml="${XUNIT_FILE}" --numprocesses=1 \ - --ignore="${PYNEST_TEST_DIR}/mpi" "${PYNEST_TEST_DIR}" 2>&1 | tee -a "${TEST_LOGFILE}" + --ignore="${PYNEST_TEST_DIR}/mpi" --ignore="${PYNEST_TEST_DIR}/sli2py_mpi" "${PYNEST_TEST_DIR}" 2>&1 | tee -a "${TEST_LOGFILE}" set -e - # Run tests in the mpi* subdirectories, grouped by number of processes + # Run tests in the sli2py_mpi subdirectory. The must be run without loading conftest.py. + if test "${HAVE_MPI}" = "true" && test "${HAVE_OPENMP}" = "true" ; then + XUNIT_FILE="${REPORTDIR}/${XUNIT_NAME}_sli2py_mpi.xml" + env + set +e + "${PYTHON}" -m pytest --noconftest --verbose --timeout $TIME_LIMIT --junit-xml="${XUNIT_FILE}" --numprocesses=1 \ + "${PYNEST_TEST_DIR}/sli2py_mpi" 2>&1 | tee -a "${TEST_LOGFILE}" + set -e + fi + + # Run tests in the mpi/* subdirectories, with one subdirectory per number of processes to use if test "${HAVE_MPI}" = "true"; then if test "${MPI_LAUNCHER}"; then + # Loop over subdirectories whose names are the number of mpi procs to use for numproc in $(cd ${PYNEST_TEST_DIR}/mpi/; ls -d */ | tr -d '/'); do XUNIT_FILE="${REPORTDIR}/${XUNIT_NAME}_mpi_${numproc}.xml" PYTEST_ARGS="--verbose --timeout $TIME_LIMIT --junit-xml=${XUNIT_FILE} ${PYNEST_TEST_DIR}/mpi/${numproc}" diff --git a/testsuite/mpitests/issue-1957.sli b/testsuite/mpitests/issue-1957.sli deleted file mode 100644 index 0f8a4825b4..0000000000 --- a/testsuite/mpitests/issue-1957.sli +++ /dev/null @@ -1,51 +0,0 @@ -/* - * issue-1957.sli - * - * 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 . - * - */ - - -/** @BeginDocumentation - Name: testsuite::issue-1957 - Test GetConnections after creating and deleting connections - with more ranks than connections - Synopsis: (issue-1957) run -> - - - Description: - issue-1957.sli checks that calling GetConnections after creating and deleting connections - is possible when there are fewer connections than ranks. - - Author: Stine Brekke Vennemo -*/ - -(unittest) run -/unittest using - - -[2 4] -{ - /nrns /iaf_psc_alpha Create def - - nrns nrns /all_to_all Connect - - << >> GetConnections { cva 2 Take } Map /conns Set - - nrns nrns << /rule /all_to_all >> << /synapse_model /static_synapse >> Disconnect_g_g_D_D - << >> GetConnections { cva 2 Take } Map conns join - -} distributed_process_invariant_collect_assert_or_die diff --git a/testsuite/mpitests/test_all_to_all.sli b/testsuite/mpitests/test_all_to_all.sli deleted file mode 100644 index f2a47971ee..0000000000 --- a/testsuite/mpitests/test_all_to_all.sli +++ /dev/null @@ -1,50 +0,0 @@ -/* - * test_all_to_all.sli - * - * 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 . - * - */ - - -/** @BeginDocumentation - Name: testsuite::test_all_to_all - Test correct connection with many targets - Synopsis: (test_all_to_all) run -> - - - Description: - test_all_to_all.sli checks that all-to-all connections are created - correctly if the number of targets exceeds the number of local nodes. - - Author: Hans Ekkehard Plesser - SeeAlso: testsuite::test_one_to_one, testsuite::test_fixed_indegree, - testsuite::test_pairwise_bernoulli -*/ - -(unittest) run -/unittest using - -% With one MPI process, conventional looping is used. We use that as -% reference case. For 4 processes, we will have fewer local nodes than -% targets and inverse looping is used. -[1 4] -{ - /nrns /iaf_psc_alpha 4 Create def - - nrns nrns /all_to_all Connect - - << >> GetConnections { cva 2 Take } Map -} distributed_process_invariant_collect_assert_or_die diff --git a/testsuite/mpitests/test_mini_brunel_ps.sli b/testsuite/mpitests/test_mini_brunel_ps.sli deleted file mode 100644 index 7b7b969f65..0000000000 --- a/testsuite/mpitests/test_mini_brunel_ps.sli +++ /dev/null @@ -1,195 +0,0 @@ -/* - * test_mini_brunel_ps.sli - * - * 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 . - * - */ - - /** @BeginDocumentation -Name: testsuite::test_mini_brunel_ps - Test parallel simulation of small Brunel-style network - -Synopsis: nest_indirect test_mini_brunel_ps.sli -> - - -Description: - Simulates scaled-down Brunel net with precise timing for different numbers of MPI - processes and compares results. - -Author: May 2012, Plesser, based on brunel_ps.sli - -See: brunel_ps.sli -*/ - -(unittest) run -/unittest using -/distributed_process_invariant_events_assert_or_die << /show_results true >> SetOptions - -skip_if_not_threaded - -/brunel_setup { -/brunel << >> def - -brunel begin -/order 50 def % scales size of network (total 5*order neurons) - -/g 250.0 def % rel strength, inhibitory synapses -/eta 2.0 def % nu_ext / nu_thresh - -/simtime 200.0 def % simulation time [ms] -/dt 0.1 def % simulation step length [ms] - -% Number of POSIX threads per program instance. -% When using MPI, the mpirun call determines the number -% of MPI processes (=program instances). The total number -% of virtual processes is #MPI processes x local_num_threads. -/total_num_virtual_procs 4 def - -% Compute the maximum of postsynaptic potential -% for a synaptic input current of unit amplitude -% (1 pA) -/ComputePSPnorm -{ - % calculate the normalization factor for the PSP - ( - a = tauMem / tauSyn; - b = 1.0 / tauSyn - 1.0 / tauMem; - % time of maximum - t_max = 1.0/b * (-LambertWm1(-exp(-1.0/a)/a)-1.0/a); - % maximum of PSP for current of unit amplitude - exp(1.0)/(tauSyn*CMem*b) * ((exp(-t_max/tauMem) - exp(-t_max/tauSyn)) / b - t_max*exp(-t_max/tauSyn)) - ) ExecMath -} -def - -%%% PREPARATION SECTION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -/NE 4 order mul cvi def % number of excitatory neurons -/NI 1 order mul cvi def % number of inhibitory neurons -/N NI NE add def % total number of neurons - -/epsilon 0.1 def % connectivity -/CE epsilon NE mul cvi def % number of excitatory synapses on neuron -/CI epsilon NI mul cvi def % number of inhibitory synapses on neuron -/C CE CI add def % total number of internal synapses per n. -/Cext CE def % number of external synapses on neuron - -/tauMem 20.0 def % neuron membrane time constant [ms] -/CMem 250.0 def % membrane capacity [pF] -/tauSyn 0.5 def % synaptic time constant [ms] -/tauRef 2.0 def % refractory time [ms] -/E_L 0.0 def % resting potential [mV] -/theta 20.0 def % threshold - - -% amplitude of PSP given 1pA current -ComputePSPnorm /J_max_unit Set - -% synaptic weights, scaled for our alpha functions, such that -% for constant membrane potential, the peak amplitude of the PSP -% equals J - -/delay 1.5 def % synaptic delay, all connections [ms] -/J 0.1 def % synaptic weight [mV] -/JE J J_max_unit div def % synaptic weight [pA] -/JI g JE mul neg def % inhibitory - -% threshold rate, equivalent rate of events needed to -% have mean input current equal to threshold -/nu_thresh ((theta * CMem) / (JE*CE*exp(1)*tauMem*tauSyn)) ExecMath def -/nu_ext eta nu_thresh mul def % external rate per synapse -/p_rate nu_ext Cext mul 1000. mul def % external input rate per neuron - % must be given in Hz - -% number of neurons to record from -/Nrec 20 def - -end -} -def % brunel_setup - -%%% CONSTRUCTION SECTION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - -[1 2 4] -{ - brunel_setup - /brunel using - % set resolution and total/local number of threads - << - /resolution dt - /total_num_virtual_procs total_num_virtual_procs - >> SetKernelStatus - - /E_neurons /iaf_psc_alpha_ps NE Create def % create excitatory neurons - /I_neurons /iaf_psc_alpha_ps NI Create def % create inhibitory neurons - /allNeurons E_neurons I_neurons join def - - /expoisson /poisson_generator_ps Create def - expoisson - << % set firing rate - /rate p_rate - >> SetStatus - - /inpoisson /poisson_generator_ps Create def - inpoisson - << - /rate p_rate - >> SetStatus - - /exsr /spike_recorder Create def - exsr << /time_in_steps true >> SetStatus - - allNeurons - { - << - /tau_m tauMem - /C_m CMem - /tau_syn_ex tauSyn - /tau_syn_in tauSyn - /t_ref tauRef - /E_L E_L - /V_th theta - /V_m E_L - /V_reset E_L - /C_m 1.0 % capacitance is unity in Brunel model - >> SetStatus - } forall - - /static_synapse << /delay delay >> SetDefaults - /static_synapse /syn_ex << /weight JE >> CopyModel - /static_synapse /syn_in << /weight JI >> CopyModel - - expoisson E_neurons stack /all_to_all /syn_ex Connect - E_neurons E_neurons << /rule /fixed_indegree /indegree CE >> << /synapse_model /syn_ex >> Connect - I_neurons E_neurons << /rule /fixed_indegree /indegree CI >> << /synapse_model /syn_in >> Connect - - - inpoisson I_neurons /all_to_all /syn_ex Connect - E_neurons I_neurons << /rule /fixed_indegree /indegree CE >> << /synapse_model /syn_ex >> Connect - I_neurons I_neurons << /rule /fixed_indegree /indegree CI >> << /synapse_model /syn_in >> Connect - - E_neurons Nrec Take exsr Connect - - simtime Simulate - - % get events, replace vectors with SLI arrays - /ev exsr /events get def - ev keys { /k Set ev dup k get cva k exch put } forall - ev - endusing - -} distributed_process_invariant_events_assert_or_die diff --git a/testsuite/pytests/sli2py_mpi/README.md b/testsuite/pytests/sli2py_mpi/README.md new file mode 100644 index 0000000000..cb48da01fc --- /dev/null +++ b/testsuite/pytests/sli2py_mpi/README.md @@ -0,0 +1,5 @@ +# MPI tests + +Test in this directory run NEST with different numbers of MPI ranks and compare results. + +See documentation in mpi_test_wrappe.py for details. diff --git a/testsuite/pytests/sli2py_mpi/mpi_test_wrapper.py b/testsuite/pytests/sli2py_mpi/mpi_test_wrapper.py new file mode 100644 index 0000000000..9a76568b2b --- /dev/null +++ b/testsuite/pytests/sli2py_mpi/mpi_test_wrapper.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +# +# mpi_test_wrapper.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 . + + +""" +Support for NEST-style MPI Tests. + +NEST-style MPI tests run the same simulation script for different number of MPI +processes and then compare results. Often, the number of virtual processes will +be fixed while the number of MPI processes is varied, but this is not required. + +- The process is managed by subclasses of the `MPITestWrapper` base class +- Each test file must contain exactly one test function + - The test function must be decorated with a subclass of `MPITestWrapper` + - The wrapper will write a modified version of the test file as `runner.py` + to a temporary directory and mpirun it from there; results are collected + in the temporary directory + - The test function can be decorated with other pytest decorators. These + are evaluated in the wrapping process + - No decorators are written to the `runner.py` file. + - Test files **must not import nest** outside the test function + - In `runner.py`, the following constants are defined: + - `SPIKE_LABEL` + - `MULTI_LABEL` + - `OTHER_LABEL` + They must be used as `label` for spike recorders and multimeters, respectively, + or for other files for output data (CSV files). They are format strings expecting + the number of processes with which NEST is run as argument. +- `conftest.py` must not be loaded, otherwise mpirun will return a non-zero exit code; + use `pytest --noconftest` +- Set `debug=True` on the decorator to see debug output and keep the + temporary directory that has been created (latter works only in + Python 3.12 and later) +- Evaluation criteria are determined by the `MPITestWrapper` subclass + +This is still work in progress. +""" + +import ast +import inspect +import subprocess +import tempfile +import textwrap +from functools import wraps +from pathlib import Path + +import pandas as pd +import pytest + + +class _RemoveDecoratorsAndMPITestImports(ast.NodeTransformer): + """ + Remove any decorators set on function definitions and imports of MPITest* entities. + + Returning None (falling off the end) of visit_* deletes a node. + See https://docs.python.org/3/library/ast.html#ast.NodeTransformer for details. + + """ + + def visit_FunctionDef(self, node): + """Remove any decorators""" + + node.decorator_list.clear() + return node + + def visit_Import(self, node): + """Drop import""" + if not any(alias.name.startswith("MPITest") for alias in node.names): + return node + + def visit_ImportFrom(self, node): + """Drop from import""" + if not any(alias.name.startswith("MPITest") for alias in node.names): + return node + + +class MPITestWrapper: + """ + Base class that parses the test module to retrieve imports, test code and + test parametrization. + """ + + RUNNER = "runner.py" + SPIKE_LABEL = "spike-{}" + MULTI_LABEL = "multi-{}" + OTHER_LABEL = "other-{}" + + RUNNER_TEMPLATE = textwrap.dedent( + """\ + SPIKE_LABEL = '{spike_lbl}' + MULTI_LABEL = '{multi_lbl}' + OTHER_LABEL = '{other_lbl}' + + {fcode} + + if __name__ == '__main__': + {fname}({params}) + """ + ) + + def __init__(self, procs_lst, debug=False): + try: + iter(procs_lst) + except TypeError: + raise TypeError("procs_lst must be a list of numbers") + + self._procs_lst = procs_lst + self._debug = debug + self._spike = None + self._multi = None + self._other = None + + @staticmethod + def _pure_test_func(func): + source_file = inspect.getsourcefile(func) + tree = ast.parse(open(source_file).read()) + _RemoveDecoratorsAndMPITestImports().visit(tree) + return ast.unparse(tree) + + def _params_as_str(self, *args, **kwargs): + return ", ".join( + part + for part in ( + ", ".join(f"{arg}" for arg in args), + ", ".join(f"{key}={value}" for key, value in kwargs.items()), + ) + if part + ) + + def _write_runner(self, tmpdirpath, func, *args, **kwargs): + with open(tmpdirpath / self.RUNNER, "w") as fp: + fp.write( + self.RUNNER_TEMPLATE.format( + spike_lbl=self.SPIKE_LABEL, + multi_lbl=self.MULTI_LABEL, + other_lbl=self.OTHER_LABEL, + fcode=self._pure_test_func(func), + fname=func.__name__, + params=self._params_as_str(*args, **kwargs), + ) + ) + + def __call__(self, func): + @wraps(func) + def wrapper(*args, **kwargs): + # "delete" parameter only available in Python 3.12 and later + try: + tmpdir = tempfile.TemporaryDirectory(delete=not self._debug) + except TypeError: + tmpdir = tempfile.TemporaryDirectory() + + # TemporaryDirectory() is not os.PathLike, so we need to define a Path explicitly + # To ensure that tmpdirpath has the same lifetime as tmpdir, we define it as a local + # variable in the wrapper() instead of as an attribute of the decorator. + tmpdirpath = Path(tmpdir.name) + self._write_runner(tmpdirpath, func, *args, **kwargs) + + res = {} + for procs in self._procs_lst: + res[procs] = subprocess.run( + ["mpirun", "-np", str(procs), "--oversubscribe", "python", self.RUNNER], + check=True, + cwd=tmpdirpath, + capture_output=self._debug, + ) + + if self._debug: + print("\n\n") + print(res) + print(f"\n\nTMPDIR: {tmpdirpath}\n\n") + + self.assert_correct_results(tmpdirpath) + + return wrapper + + def _collect_result_by_label(self, tmpdirpath, label): + try: + next(tmpdirpath.glob(f"{label.format('*')}.dat")) + except StopIteration: + return None # no data for this label + + return { + n_procs: [pd.read_csv(f, sep="\t", comment="#") for f in tmpdirpath.glob(f"{label.format(n_procs)}-*.dat")] + for n_procs in self._procs_lst + } + + def collect_results(self, tmpdirpath): + """ + For each of the result types, build a dictionary mapping number of MPI procs to a list of + dataframes, collected per rank or VP. + """ + + self._spike = self._collect_result_by_label(tmpdirpath, self.SPIKE_LABEL) + self._multi = self._collect_result_by_label(tmpdirpath, self.MULTI_LABEL) + self._other = self._collect_result_by_label(tmpdirpath, self.OTHER_LABEL) + + def assert_correct_results(self, tmpdirpath): + assert False, "Test-specific checks not implemented" + + +class MPITestAssertEqual(MPITestWrapper): + """ + Assert that combined, sorted output from all VPs is identical for all numbers of MPI ranks. + """ + + def assert_correct_results(self, tmpdirpath): + self.collect_results(tmpdirpath) + + all_res = [] + if self._spike: + # For each number of procs, combine results across VPs and sort by time and sender + all_res.append( + [ + pd.concat(spikes, ignore_index=True).sort_values( + by=["time_step", "time_offset", "sender"], ignore_index=True + ) + for spikes in self._spike.values() + ] + ) + + if self._multi: + raise NotImplementedError("MULTI is not ready yet") + + if self._other: + # For each number of procs, combine across ranks or VPs (depends on what test has written) and + # sort by all columns so that if results for different proc numbers are equal up to a permutation + # of rows, the sorted frames will compare equal + + # next(iter(...)) returns the first value in the _other dictionary + # [0] then picks the first DataFrame from that list + # columns need to be converted to list() to be passed to sort_values() + all_columns = list(next(iter(self._other.values()))[0].columns) + all_res.append( + [ + pd.concat(others, ignore_index=True).sort_values(by=all_columns, ignore_index=True) + for others in self._other.values() + ] + ) + + assert all_res, "No test data collected" + + for res in all_res: + assert len(res) == len(self._procs_lst), "Could not collect data for all procs" + + for r in res[1:]: + pd.testing.assert_frame_equal(res[0], r) diff --git a/testsuite/pytests/sli2py_mpi/test_all_to_all.py b/testsuite/pytests/sli2py_mpi/test_all_to_all.py new file mode 100644 index 0000000000..e01e07094f --- /dev/null +++ b/testsuite/pytests/sli2py_mpi/test_all_to_all.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# +# test_all_to_all.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 pandas +import pytest +from mpi_test_wrapper import MPITestAssertEqual + + +# Parametrization over the number of nodes here only so show hat it works +@pytest.mark.parametrize("N", [4, 7]) +@MPITestAssertEqual([1, 4], debug=False) +def test_all_to_all(N): + """ + Confirm that all-to-all connections created correctly for more targets than local nodes. + """ + + import nest + import pandas as pd + + nest.ResetKernel() + + nrns = nest.Create("parrot_neuron", n=N) + nest.Connect(nrns, nrns, "all_to_all") + + conns = nest.GetConnections().get(output="pandas").drop(labels=["target_thread", "port"], axis=1) + conns.to_csv(OTHER_LABEL.format(nest.num_processes) + f"-{nest.Rank()}.dat", index=False) # noqa: F821 diff --git a/testsuite/pytests/sli2py_mpi/test_issue_1957.py b/testsuite/pytests/sli2py_mpi/test_issue_1957.py new file mode 100644 index 0000000000..5b85761443 --- /dev/null +++ b/testsuite/pytests/sli2py_mpi/test_issue_1957.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# test_issue_1957.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 . + + +from mpi_test_wrapper import MPITestAssertEqual + + +@MPITestAssertEqual([2, 4]) +def test_issue_1957(): + """ + Confirm that GetConnections works in parallel without hanging if not all ranks have connections. + """ + + import nest + import pandas as pd + + nest.ResetKernel() + + nrn = nest.Create("parrot_neuron") + + # Create two connections so we get lists back from pre_conns.get() and can build a DataFrame + nest.Connect(nrn, nrn) + nest.Connect(nrn, nrn) + + pre_conns = nest.GetConnections() + if pre_conns: + # need to do this here, Disconnect invalidates pre_conns + df = pd.DataFrame.from_dict(pre_conns.get()).drop(labels="target_thread", axis=1) + df.to_csv(OTHER_LABEL.format(nest.num_processes) + f"-{nest.Rank()}.dat", index=False) # noqa: F821 + + nest.Disconnect(nrn, nrn) + nest.Disconnect(nrn, nrn) + post_conns = nest.GetConnections() + assert not post_conns diff --git a/testsuite/pytests/sli2py_mpi/test_mini_brunel_ps.py b/testsuite/pytests/sli2py_mpi/test_mini_brunel_ps.py new file mode 100644 index 0000000000..76ac862665 --- /dev/null +++ b/testsuite/pytests/sli2py_mpi/test_mini_brunel_ps.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# +# test_mini_brunel_ps.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 . + + +from mpi_test_wrapper import MPITestAssertEqual + + +@MPITestAssertEqual([1, 2, 4]) +def test_mini_brunel_ps(): + """ + Confirm that downscaled Brunel net with precise neurons is invariant under number of MPI ranks. + """ + + import nest + + nest.ResetKernel() + + nest.set(total_num_virtual_procs=4, overwrite_files=True) + + # Model parameters + NE = 1000 # number of excitatory neurons + NI = 250 # number of inhibitory neurons + CE = 100 # number of excitatory synapses per neuron + CI = 250 # number of inhibitory synapses per neuron + N_rec = 10 # number of (excitatory) neurons to record + D = 1.5 # synaptic delay, all connections [ms] + JE = 0.1 # peak of EPSP [mV] + eta = 2.0 # external rate relative to threshold rate + g = 5.0 # ratio inhibitory weight/excitatory weight + JI = -g * JE # peak of IPSP [mV] + + neuron_params = { + "tau_m": 20, # membrance time constant [ms] + "t_ref": 2.0, # refractory period [ms] + "C_m": 250.0, # membrane capacitance [pF] + "E_L": 0.0, # resting membrane potential [mV] + "V_th": 20.0, # threshold potential [mV] + "V_reset": 0.0, # reset potential [mV] + } + + # Threshold rate; the external rate needed for a neuron to reach + # threshold in absence of feedback + nu_thresh = neuron_params["V_th"] / (JE * CE * neuron_params["tau_m"]) + + # External firing rate; firing rate of a neuron in the external population + nu_ext = eta * nu_thresh + + # Build network + enodes = nest.Create("iaf_psc_delta_ps", NE, params=neuron_params) + inodes = nest.Create("iaf_psc_delta_ps", NI, params=neuron_params) + ext = nest.Create("poisson_generator_ps", 1, params={"rate": nu_ext * CE * 1000.0}) + srec = nest.Create( + "spike_recorder", + 1, + params={ + "label": SPIKE_LABEL.format(nest.num_processes), # noqa: F821 + "record_to": "ascii", + "time_in_steps": True, + }, + ) + + nest.CopyModel("static_synapse", "esyn", params={"weight": JE, "delay": D}) + nest.CopyModel("static_synapse", "isyn", params={"weight": JI, "delay": D}) + + nest.Connect(ext, enodes, conn_spec="all_to_all", syn_spec="esyn") + nest.Connect(ext, inodes, conn_spec="all_to_all", syn_spec="esyn") + nest.Connect(enodes[:N_rec], srec) + nest.Connect( + enodes, + enodes + inodes, + conn_spec={"rule": "fixed_indegree", "indegree": CE, "allow_autapses": False, "allow_multapses": True}, + syn_spec="esyn", + ) + nest.Connect( + inodes, + enodes + inodes, + conn_spec={"rule": "fixed_indegree", "indegree": CI, "allow_autapses": False, "allow_multapses": True}, + syn_spec="isyn", + ) + + # Simulate network + nest.Simulate(400) + + # Uncomment next line to provoke test failure + # nest.Simulate(200 if -nest.num_processes == 1 else 400)