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)