diff --git a/demos/brunel-simulation/brunel_example.py b/demos/brunel-simulation/brunel_example.py
new file mode 100644
index 0000000000000000000000000000000000000000..979a477f4d645a15413829722b55262b679e6507
--- /dev/null
+++ b/demos/brunel-simulation/brunel_example.py
@@ -0,0 +1,264 @@
+
+"""
+Definition of spatially extended Brunel network.
+This module provides layer and projections declarations suitable for
+use with the NEST Topology Module.
+It defines a Brunel-style network with neurons placed on a regular grid.
+Connectivity is probabilitstic from the entire network, i.e., connectivity
+is not structured spatially.
+"""
+
+from copy import deepcopy
+from math import sqrt
+import numpy as np
+from mpl_toolkits.mplot3d import Axes3D
+import matplotlib.pyplot as plt
+
+import nest
+import nest.raster_plot
+import nest.topology as tp
+
+
+class Brunel3D:
+
+    def __init__(self):
+        self.layer_dict = {}
+        # nest.SetKernelStatus({'print_time': True})
+
+    class Parameters:
+        """
+        Define model parameters.
+        """
+        order = 9   # must be square
+        NE = 4 * order  # number of excitatory neurons.
+        NI = 1 * order  # number of inhibitory neurons
+
+        lengthE = int(sqrt(NE))
+        lengthI = int(sqrt(NI))
+
+        if not (lengthE**2 == NE and lengthI**2 == NI):
+            raise ValueError('Order must be a square number (order = n**2)')
+
+        # neuron_model = 'iaf_psc_alpha'
+        neuron_model = 'mat2_psc_exp'
+
+        g = 5.0  # ratio inhibitory weight/excitatory weight
+        eta = 2.0  # external rate relative to threshold rate
+        epsilon = 0.1  # connection probability
+
+        delay = 1.5  # synaptic delay in ms
+
+        tauMem = 20.0  # time constant of membrane potential in ms
+        theta = 20.0  # membrane threshold potential in mV
+
+        J = 0.1  # postsynaptic amplitude in mV
+        J_ex = J  # amplitude of excitatory postsynaptic potential
+        J_in = -g * J_ex  # amplitude of inhibitory postsynaptic potential
+
+        CE = int(epsilon * NE)  # number of excitatory synapses per neuron
+
+        nu_th = theta / (J * CE * tauMem)
+        nu_ex = eta * nu_th
+        p_rate = 1.0 * nu_ex * CE
+
+        neuron_params = {"C_m": 1.0,
+                         "tau_m": tauMem,
+                         "t_ref": 2.0,
+                         "E_L": 0.0,
+                         # "V_reset": 0.0,  # doesn't work with mat2_psc_exp
+                         # "V_th": theta,  # doesn't work with mat2_psc_exp
+                         "V_m": 0.0
+                         }
+        mm_params = {'record_from': ['V_m', 'V_th'],
+                     'record_to': ['nesci_contra']}
+        poisson_params = {"rate": p_rate}
+
+    def modified_copy(self, orig, diff):
+        """
+        Returns a deep copy of dictionary with changes applied.
+        @param orig  original dictionary, will be deep-copied
+        @param diff  copy will be updated with this dict
+        """
+
+        tmp = deepcopy(orig)
+        tmp.update(diff)
+        return tmp
+
+    def make_layer_specs(self):
+        """
+        Returns lists of dictionaries with model, layer, and synapse model
+        specifications.
+        """
+
+        P = self.Parameters
+
+        self.models = [(P.neuron_model, 'excitatory', P.neuron_params),
+                       (P.neuron_model, 'inhibitory', P.neuron_params),
+                       ('poisson_generator', 'noise', P.poisson_params),
+                       ('multimeter', 'recordingNode', P.mm_params)]
+
+        self.syn_models = [('static_synapse', 'static_excitatory', {})]
+
+        dE = 1.0 / float(P.lengthE)
+        limE = (1.0 - dE) / 2.0
+        posE = np.linspace(-limE, limE, num=P.lengthE)
+
+        dI = 1.0 / float(P.lengthI)
+        limI = (1.0 - dI) / 2.0
+        posI = np.linspace(-limI, limI, num=P.lengthI)
+
+        self.layers = [('Excitatory', {'positions': [[x, y, 0.4]
+                                                     for x in posE
+                                                     for y in posE],
+                                       'edge_wrap': True,
+                                       'elements': 'excitatory'}),
+                       ('Inhibitory', {'positions': [[x, y, -0.4]
+                                                     for x in posI
+                                                     for y in posI],
+                                       'edge_wrap': True,
+                                       'elements': 'inhibitory'})]
+        self.layers.append(('PoissonGenerator',
+                            {'positions': [[0.0, 0.0, 0.0]],
+                             'elements': 'noise'}))
+        self.layers.append(('Multimeter',
+                            {'positions': [[0.0, 0.0, 0.0]],
+                             'elements': 'recordingNode'}))
+        self.layers.append(('SpikeDetector',
+                            {'positions': [[0.0, 0.0, 0.0]],
+                             'elements': 'spike_detector'}))
+
+    def make_connection_specs(self):
+        """
+        Returns list of dictionaries specifying projections for Brunel network.
+        """
+
+        P = self.Parameters
+
+        self.projections = [('Excitatory', 'Excitatory',
+                             {'connection_type': 'convergent',
+                              'synapse_model': 'static_synapse',
+                              'kernel': P.epsilon,
+                              'weights': P.J_ex,
+                              'delays': P.delay}),
+                            ('Excitatory', 'Inhibitory',
+                             {'connection_type': 'convergent',
+                              'synapse_model': 'static_synapse',
+                              'kernel': P.epsilon,
+                              'weights': P.J_ex,
+                              'delays': P.delay}),
+                            ('Inhibitory', 'Excitatory',
+                             {'connection_type': 'convergent',
+                              'synapse_model': 'static_synapse',
+                              'kernel': P.epsilon,
+                              'weights': P.J_in,
+                              'delays': P.delay}),
+                            ('Inhibitory', 'Excitatory',
+                             {'connection_type': 'convergent',
+                              'synapse_model': 'static_synapse',
+                              'kernel': P.epsilon,
+                              'weights': P.J_in,
+                              'delays': P.delay}),
+                            ('Multimeter', 'Excitatory',
+                             {'connection_type': 'convergent'}),
+                            ('Excitatory', 'SpikeDetector',
+                             {'connection_type': 'convergent'}),
+                            ('Inhibitory', 'SpikeDetector',
+                             {'connection_type': 'convergent'}),
+                            ('PoissonGenerator', 'Excitatory',
+                             {'connection_type': 'convergent'})]
+
+    def make_layers(self):
+        # First, we copy the models with custom specifications
+        # Neuron models
+        for model in self.models:
+            old_name, new_name, spec = model
+            nest.CopyModel(old_name, new_name, spec)
+        # Synapse models
+        for model in self.syn_models:
+            old_name, new_name, spec = model
+            nest.CopyModel(old_name, new_name, spec)
+
+        # Next we make the layers
+        for l in self.layers:
+            name = l[0]
+            specs = l[1]
+            self.layer_dict.update({name: tp.CreateLayer(specs)})
+
+    def make_connections(self):
+        # Connect layers
+        for proj in self.projections:
+            pre_layer = self.layer_dict[proj[0]]
+            post_layer = self.layer_dict[proj[1]]
+            conn_specs = proj[2]
+            tp.ConnectLayers(pre_layer, post_layer, conn_specs)
+
+    def simulate(self):
+        nest.Simulate(1000)
+
+    def plot_positions(self):
+        ex_pos = self.layers[0][1]['positions']
+        in_pos = self.layers[1][1]['positions']
+        fig = plt.figure()
+        ax = Axes3D(fig)
+        for c, m, positions in [('b', 'o', ex_pos), ('r', '^', in_pos)]:
+            ax.scatter([x for x, y, z in positions],
+                       [y for x, y, z in positions],
+                       [z for x, y, z in positions],
+                       c=c, marker=m)
+
+    def get_results(self):
+        mm = (self.layer_dict['Multimeter'][0] + 1,)
+        sd = (self.layer_dict['SpikeDetector'][0] + 1,)
+        mm_status = nest.GetStatus(mm)[0]
+        sd_status = nest.GetStatus(sd)[0]
+
+        nest.raster_plot.from_device(sd, hist=True)
+
+        senders = mm_status['events']['senders']
+        times = mm_status['events']['times']
+        v_m = mm_status['events']['V_m']
+        v_th = mm_status['events']['V_th']
+        step = int(max(senders)/100 + 1)  # Only plot results from some GIDs
+
+        mm_events = []
+        for i in range(1, max(senders) + 1, step):
+            if i in senders:
+                indices = np.argwhere(senders == i)
+                mm_events.append({'GID': i,
+                      'times': [times[n] for n in indices],
+                      'V_m': [v_m[n] for n in indices],
+                      'V_th': [v_th[n] for n in indices]})
+
+        return {'multimeter': mm_events,
+                'spike_detector': nest.GetStatus(sd)[0]}
+
+
+if __name__ == '__main__':
+    nest.ResetKernel()
+
+    print('Making specifications')
+    brunel = Brunel3D()
+    brunel.make_layer_specs()
+    brunel.make_connection_specs()
+
+    print('Making layers')
+    brunel.make_layers()
+    nest.topology.DumpLayerNodes([l[0] for l in brunel.layer_dict.values()][:2],
+                                     'brunel_nodes.txt')
+
+    print('Making connections')
+    brunel.make_connections()
+
+    brunel.simulate()
+
+    print('Getting results')
+    brunel.plot_positions()
+    results = brunel.get_results()
+
+    for value in ['V_m', 'V_th']:
+        plt.figure()
+        for n in results['multimeter'][::20]:
+            plt.plot(n['times'], n[value], label='{}'.format(n['GID']))
+            plt.legend()
+        plt.title(value)
+    plt.show()
diff --git a/demos/brunel-simulation/nest_sim.py b/demos/brunel-simulation/nest_sim.py
new file mode 100644
index 0000000000000000000000000000000000000000..65788db7fdfa3b6b407bc8f91ba08e04e1dc8dfc
--- /dev/null
+++ b/demos/brunel-simulation/nest_sim.py
@@ -0,0 +1,94 @@
+#-------------------------------------------------------------------------------
+# nest in situ vis
+#
+# Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+# Virtual Reality & Immersive Visualisation Group.
+#-------------------------------------------------------------------------------
+#                                  License
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#-------------------------------------------------------------------------------
+
+import sys
+import nest
+import numpy
+
+from PyQt5.QtWidgets import QApplication, QPushButton
+from PyQt5.QtCore import QTimer
+
+import brunel_example
+
+class Simulation:
+    def __init__(self):
+        self.SetupSim()
+        
+    def SetupSim(self):
+        nest.ResetKernel()
+        
+        print('Making specifications')
+        self.brunel = brunel_example.Brunel3D()
+        self.brunel.make_layer_specs()
+        self.brunel.make_connection_specs()
+
+        print('Making layers')
+        self.brunel.make_layers()
+        nest.topology.DumpLayerNodes([l[0] for l in self.brunel.layer_dict.values()][:2],
+                                     'brunel_nodes.txt')
+
+        print('Making connections')
+        self.brunel.make_connections()
+
+    def Simulate(self, steps):
+        nest.Simulate(steps)
+
+
+class MainWindow:
+    def __init__(self, screen_resolution):
+        self.screen_resolution = screen_resolution
+        self.SetupWindow()
+        self.simulation = Simulation()
+
+    def SetupWindow(self):
+        self.simulate_button = QPushButton("nest.Simulate(1000)")
+        self.simulate_button.clicked.connect(self.SimulateButtonClicked)
+
+    def SimulateButtonClicked(self):
+        self.simulate_button.setEnabled(False)
+        QApplication.processEvents()
+
+        self.simulation.Simulate(1000)
+
+        QApplication.processEvents()
+        self.simulate_button.setEnabled(True)
+
+    def Show(self):
+        self.simulate_button.show()
+        button_geometry = self.simulate_button.geometry()
+        self.simulate_button.setGeometry(
+            self.screen_resolution.width() - button_geometry.width(),
+            self.screen_resolution.height() - button_geometry.height(),
+            button_geometry.width(),
+            button_geometry.height())
+
+
+def main(argv):
+    app = QApplication(argv)
+    screen_resolution = app.desktop().screenGeometry()
+
+    w = MainWindow(screen_resolution)
+    w.Show()
+
+    return app.exec_()
+
+if __name__ == "__main__":
+    main(sys.argv)