From a2dd9544ac714b8638549b04fbea1bb14685ea85 Mon Sep 17 00:00:00 2001
From: Thorsten Hater <24411438+thorstenhater@users.noreply.github.com>
Date: Mon, 25 Nov 2024 16:55:41 +0100
Subject: [PATCH] Plasticity tutorial (#2426)
Add a tutorial on how to engineer structural plasticity in Arbor using a homeostatic rule.
Read the preview here
https://arbor--2418.org.readthedocs.build/en/2418/tutorial/plasticity.html
Here's a teaser of the final result:

Update: finished the tutorial.
---
.github/workflows/test-docs.yaml | 2 +-
doc/tutorial/index.rst | 1 +
doc/tutorial/plasticity.rst | 342 +
python/example/plasticity/01-raster.svg | 1863 ++
python/example/plasticity/01-rates.svg | 2107 ++
python/example/plasticity/02-graph.svg | 808 +
python/example/plasticity/02-matrix.svg | 388 +
python/example/plasticity/02-raster.svg | 4005 ++++
python/example/plasticity/02-rates.svg | 2090 ++
python/example/plasticity/03-final-graph.svg | 1208 +
python/example/plasticity/03-final-matrix.svg | 388 +
.../example/plasticity/03-initial-graph.svg | 408 +
.../example/plasticity/03-initial-matrix.svg | 388 +
python/example/plasticity/03-raster.svg | 19409 ++++++++++++++++
python/example/plasticity/03-rates.svg | 2013 ++
python/example/plasticity/homeostasis.py | 75 +
python/example/plasticity/random_network.py | 79 +
python/example/plasticity/unconnected.py | 62 +
python/example/plasticity/util.py | 71 +
19 files changed, 35706 insertions(+), 1 deletion(-)
create mode 100644 doc/tutorial/plasticity.rst
create mode 100644 python/example/plasticity/01-raster.svg
create mode 100644 python/example/plasticity/01-rates.svg
create mode 100644 python/example/plasticity/02-graph.svg
create mode 100644 python/example/plasticity/02-matrix.svg
create mode 100644 python/example/plasticity/02-raster.svg
create mode 100644 python/example/plasticity/02-rates.svg
create mode 100644 python/example/plasticity/03-final-graph.svg
create mode 100644 python/example/plasticity/03-final-matrix.svg
create mode 100644 python/example/plasticity/03-initial-graph.svg
create mode 100644 python/example/plasticity/03-initial-matrix.svg
create mode 100644 python/example/plasticity/03-raster.svg
create mode 100644 python/example/plasticity/03-rates.svg
create mode 100644 python/example/plasticity/homeostasis.py
create mode 100644 python/example/plasticity/random_network.py
create mode 100644 python/example/plasticity/unconnected.py
create mode 100644 python/example/plasticity/util.py
diff --git a/.github/workflows/test-docs.yaml b/.github/workflows/test-docs.yaml
index 21a546845..d4020b509 100644
--- a/.github/workflows/test-docs.yaml
+++ b/.github/workflows/test-docs.yaml
@@ -37,5 +37,5 @@ jobs:
run: |
mkdir build
cd build
- cmake .. -DARB_WITH_PYTHON=ON -DPython3_EXECUTABLE=`which python`
+ cmake .. -DARB_WITH_PYTHON=ON -DARB_BUILD_PYTHON_STUBS=OFF -DPython3_EXECUTABLE=`which python`
make html
diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst
index 8ef283e2d..3adb1567c 100644
--- a/doc/tutorial/index.rst
+++ b/doc/tutorial/index.rst
@@ -78,6 +78,7 @@ Advanced
:maxdepth: 1
nmodl
+ plasticity
Demonstrations
--------------
diff --git a/doc/tutorial/plasticity.rst b/doc/tutorial/plasticity.rst
new file mode 100644
index 000000000..6dd615f4f
--- /dev/null
+++ b/doc/tutorial/plasticity.rst
@@ -0,0 +1,342 @@
+.. _tutorial_plasticity:
+
+Structural Plasticity in Arbor
+==============================
+
+In this tutorial, we are going to demonstrate how a network can be built using
+plasticity and homeostatic connection rules. Despite not playing towards Arbor's
+strengths, we choose a LIF (Leaky Integrate and Fire) neuron model, as we are
+primarily interested in examining the required scaffolding.
+
+We will build up the simulation in stages, starting with an unconnected network
+and finishing with a dynamically built connectome.
+
+.. admonition:: Concepts and Requirements
+
+ We cover some advanced topics in this tutorial, mainly structural
+ plasticity. Please refer to other tutorials for the basics of network
+ building. The model employed here --- storing an explicit connection matrix
+ --- is not advisable in most scenarios.
+
+ In addition to Arbor and its requirements, ``scipy``, ``matplotlib``, and
+ ``networkx`` need to be installed.
+
+Unconnected Network
+-------------------
+
+Consider a collection of ``N`` LIF cells. This will be the starting point for
+our exploration. For now, we set up each cell with a Poissonian input such that
+it will produce spikes periodically at a low frequency.
+
+The Python file ``01-setup.py`` is the scaffolding we will build our simulation
+around and thus contains some passages that might seem redundant now, but will
+be helpful in later steps.
+
+We begin by defining the global settings:
+
+.. literalinclude:: ../../python/example/plasticity/unconnected.py
+ :language: python
+ :lines: 7-15
+
+- ``N`` is the cell count of the simulation.
+- ``T`` is the total runtime of the simulation in ``ms``.
+- ``t_interval`` defines the _interval_ such that the simulation advances in
+ discrete steps ``[0, 1, 2, ...] t_interval``. Later, this will be the timescale of
+ plasticity.
+- ``dt`` is the numerical timestep on which cells evolve.
+
+These parameters are used here:
+
+.. literalinclude:: ../../python/example/plasticity/unconnected.py
+ :language: python
+ :lines: 52-62
+
+where we run the simulation in increments of ``t_interval``.
+
+Back to the recipe, we set a prototypical cell:
+
+.. literalinclude:: ../../python/example/plasticity/unconnected.py
+ :language: python
+ :lines: 23
+
+and deliver it for all ``gid``:
+
+.. literalinclude:: ../../python/example/plasticity/unconnected.py
+ :language: python
+ :lines: 42-43
+
+Also, each cell has an event generator attached, using a Poisson point process seeded with the cell's ``gid``.
+
+.. literalinclude:: ../../python/example/plasticity/unconnected.py
+ :language: python
+ :lines: 33-40
+
+
+All other parameters are set in the constructor:
+
+.. literalinclude:: ../../python/example/plasticity/unconnected.py
+ :language: python
+ :lines: 19-28
+
+We also proceed to add spike recording and generate plots using a helper
+function ``plot_spikes`` from ``util.py``. You can skip the following details
+for now and come back later if you are interested in how it works. Rates are
+computed by binning spikes into ``t_interval`` and the neuron id; the mean rate
+is the average across the neurons smoothed using a Savitzky-Golay filter
+(``scipy.signal.savgol_filter``).
+
+We plot per-neuron and mean rates:
+
+.. figure:: ../../python/example/plasticity/01-rates.svg
+ :width: 400
+ :align: center
+
+We also generate raster plots via ``scatter``:
+
+.. figure:: ../../python/example/plasticity/01-raster.svg
+ :width: 400
+ :align: center
+
+
+A Randomly Wired Network
+------------------------
+
+We use inheritance to derive a new recipe that contains all the functionality of
+the ```unconnected`` recipe. We then add a random connectivity matrix during
+construction, fix connection weights, and deliver the resulting connections
+via the ``connections_on`` callback, with the only extra consideration of
+allowing multiple connections between two neurons.
+
+In detail, the recipe stores the connection matrix, the current
+incoming/outgoing connections per neuron, and the maximum for both directions
+
+.. literalinclude:: ../../python/example/plasticity/random_network.py
+ :language: python
+ :lines: 26-31
+
+The connection matrix is used to construct connections,
+
+.. literalinclude:: ../../python/example/plasticity/random_network.py
+ :language: python
+ :lines: 33-38
+
+together with the fixed connection parameters:
+
+.. literalinclude:: ../../python/example/plasticity/random_network.py
+ :language: python
+ :lines: 24-25
+
+We define helper functions ``add|del_connections`` to manipulate the connection
+table while upholding these invariants:
+
+- no self-connections, i.e., ``connection[i, i] == 0``
+- ``inc[i]`` the sum of ``connections[:, i]``
+- no more incoming connections than allowed by ``max_inc``, i.e., ``inc[i] <= max_inc``
+- ``out[i]`` the sum of ``connections[i, :]``
+- no more outgoing connections than allowed by ``max_out``, i.e., ``out[i] <= max_out``
+
+These methods return ``True`` on success and ``False`` otherwise.
+
+.. literalinclude:: ../../python/example/plasticity/random_network.py
+ :language: python
+ :lines: 40-54
+
+Both are used in ``rewire`` to produce a random connection matrix.
+
+.. literalinclude:: ../../python/example/plasticity/random_network.py
+ :language: python
+ :lines: 56-65
+
+We then proceed to run the simulation:
+
+.. literalinclude:: ../../python/example/plasticity/random_network.py
+ :language: python
+ :lines: 68-79
+
+ and plot the results as before:
+
+.. figure:: ../../python/example/plasticity/02-rates.svg
+ :width: 400
+ :align: center
+
+
+Note that we added a plot of the network connectivity using ``plot_network``
+from ``util`` as well. This generates images of the graph and connection matrix.
+
+.. figure:: ../../python/example/plasticity/02-matrix.svg
+ :width: 400
+ :align: center
+
+.. figure:: ../../python/example/plasticity/02-graph.svg
+ :width: 400
+ :align: center
+
+
+
+Adding Homeostasis
+------------------
+
+Under the homeostatic model, each cell was a setpoint for the firing rate :math:`\nu^*`
+which is used to determine the creation or destruction of synaptic connections via
+
+.. math::
+
+ \frac{dC}{dt} = \alpha(\nu - \nu^*).
+
+Thus we need to add some extra information to our simulation, namely the
+setpoint :math:`\nu^*_i` for each neuron :math:`i` and the sensitivity parameter
+:math:`\alpha`. We will also use a simplified version of the differential
+equation above, namely adding/deleting exactly one connection if the difference
+of observed to desired spiking frequency exceeds :math:`\pm\alpha`. This is both
+for simplicity and to avoid sudden changes in the network structure.
+
+As before, we set up global parameters:
+
+.. literalinclude:: ../../python/example/plasticity/homeostasis.py
+ :language: python
+ :lines: 10-24
+
+and prepare our simulation:
+
+.. literalinclude:: ../../python/example/plasticity/homeostasis.py
+ :language: python
+ :lines: 37-39
+
+Note that our new recipe is almost unaltered from the random network.
+
+.. literalinclude:: ../../python/example/plasticity/homeostasis.py
+ :language: python
+ :lines: 27-33
+
+All changes are contained in the way we run the simulation. To add a further
+interesting feature, we skip the rewiring for the first half of the simulation.
+The initial network is unconnected, but could be populated randomly (or any
+other way) if desired by calling ``self.rewire()`` in the constructor of
+``homeostatic_network`` before setting the maxima to eight.
+
+Plasticity is implemented by tweaking the connection table inside the recipe
+between calls to ``run`` and calling ``simulation.update`` with the modified
+recipe:
+
+.. literalinclude:: ../../python/example/plasticity/homeostasis.py
+ :language: python
+ :lines: 70
+
+<<<<<<< HEAD
+.. note::
+
+ As it is the central point here, it is worth emphasizing why this yields a
+ changed network. The call to ``sim.update(rec)`` causes Arbor to internally
+ re-build the connection table from scratch based on the data returned by
+ ``rec.connections_on``. However, here, this method just inspects the matrix
+ in ``rec.connections`` and converts the data into a ``arbor.connection``.
+ Thus, changing this matrix before ``update`` will build a different network.
+
+ Important caveats:
+
+ - without ``update``, changes to the recipe have no effect.
+ - vice versa ``update`` has no effect if the recipe doesn't return a different
+ data than before.
+ - ``update`` will delete all existing connections and their parameters, so
+ all connections to be kept must be explicitly re-instantiated.
+ - ``update`` will **not** delete synapses or their state, e.g., ODEs will
+ still be integrated even if not connected and currents might be produced;
+ - neither synapses/targets nor detectors/sources can be altered. Create all
+ endpoints up front.
+ - only the network is updated (this might change in future versions!).
+ - be very aware that ``connections_on`` might be called in arbitrary order
+ and by multiple (potentially different) threads and processes! This
+ requires some thought and synchronization when dealing with random numbers
+ and updating data *inside* ``connections_on``.
+
+Changes are based on the difference from the current rate we compute from the spikes
+during the last interval,
+||||||| parent of df94162f (Final thoughts and warnings.)
+Changes are based on the difference of current rate we compute from the spikes
+during the last interval
+=======
+.. note::
+
+ As it is the central point here, it is worth emphasizing why this yields a
+ changed network. The call to ``sim.update(rec)`` causes Arbor to internally
+ re-build the connection table from scratch based on the data returned by
+ ``rec.connections_on``. However, here, this method just inspects the matrix
+ in ``rec.connections`` and converts the data into a ``arbor.connection``.
+ Thus, changing this matrix before ``update`` will build a different network.
+
+ Important caveats:
+
+ - without ``update``, changes to the recipe have no effect
+ - vice versa ``update`` has no effect if the recipe doesn't return different
+ data than before
+ - ``update`` will delete all existing connections and their parameters, so
+ all connections to be kept must be explicitly re-instantiated
+ - ``update`` will **not** delete synapses or their state, e.g. ODEs will
+ still be integrated even if not connected and currents might be produced
+ - neither synapses/targets nor detectors/sources can be altered. Create all
+ endpoints up front.
+ - only the network is updated (this might change in future versions!)
+ - be very aware that ``connections_on`` might be called in arbitrary order
+ and by multiples (potentially different) threads and processes! This
+ requires some thought and synchronization when dealing with random numbers
+ and updating data *inside* ``connections_on``.
+
+Changes are based on the difference of current rate we compute from the spikes
+during the last interval
+>>>>>>> df94162f (Final thoughts and warnings.)
+
+.. literalinclude:: ../../python/example/plasticity/homeostasis.py
+ :language: python
+ :lines: 49-54
+
+and the setpoint times the sensitivity.
+
+.. literalinclude:: ../../python/example/plasticity/homeostasis.py
+ :language: python
+ :lines: 55
+
+Then, each potential pairing of target and source is checked in random
+order for whether adding or removing a connection is required:
+
+.. literalinclude:: ../../python/example/plasticity/homeostasis.py
+ :language: python
+ :lines: 59-68
+
+If we find an option to fulfill that requirement, we do so and proceed to the
+next target. The randomization is important here, especially for adding
+connections to avoid biases, in particular when there are too few eligible
+connection partners. The ``randrange`` function produces a shuffled range ``[0,
+N)``. We leverage the helper functions from the random network recipe to
+manipulate the connection table, see the discussion above.
+
+Finally, we plot spiking rates as before; the jump at the half-way point is the
+effect of the plasticity activating after which each neuron moves to the
+setpoint.
+
+.. figure:: ../../python/example/plasticity/03-rates.svg
+ :width: 400
+ :align: center
+
+And the resulting network:
+
+.. figure:: ../../python/example/plasticity/03-final-graph.svg
+ :width: 400
+ :align: center
+
+Conclusion
+----------
+
+This concludes our foray into structural plasticity. While the building blocks
+
+- an explicit representation of the connections;
+- running the simulation in batches (and calling ``simulation.update``!);
+- a rule to derive the change;
+
+will likely be the same in all approaches, the concrete implementation of the
+rules is the centerpiece here. For example, although spike rate homeostasis was
+used here, mechanism states and ion concentrations --- extracted via the normal
+probe and sample interface --- can be leveraged to build rules. Due to the way
+the Python interface is required to link to measurements, using the C++ API for
+access to streaming spike and measurement data could help to address performance
+issues. Plasticity as shown also meshes with the high-level connection builder.
+External tools to build and update connections might be useful as well.
diff --git a/python/example/plasticity/01-raster.svg b/python/example/plasticity/01-raster.svg
new file mode 100644
index 000000000..11b1a2fbd
--- /dev/null
+++ b/python/example/plasticity/01-raster.svg
@@ -0,0 +1,1863 @@
+
+
+
diff --git a/python/example/plasticity/01-rates.svg b/python/example/plasticity/01-rates.svg
new file mode 100644
index 000000000..087291dbb
--- /dev/null
+++ b/python/example/plasticity/01-rates.svg
@@ -0,0 +1,2107 @@
+
+
+
diff --git a/python/example/plasticity/02-graph.svg b/python/example/plasticity/02-graph.svg
new file mode 100644
index 000000000..e40f6759e
--- /dev/null
+++ b/python/example/plasticity/02-graph.svg
@@ -0,0 +1,808 @@
+
+
+
diff --git a/python/example/plasticity/02-matrix.svg b/python/example/plasticity/02-matrix.svg
new file mode 100644
index 000000000..ce78d1a5e
--- /dev/null
+++ b/python/example/plasticity/02-matrix.svg
@@ -0,0 +1,388 @@
+
+
+
diff --git a/python/example/plasticity/02-raster.svg b/python/example/plasticity/02-raster.svg
new file mode 100644
index 000000000..eba26e802
--- /dev/null
+++ b/python/example/plasticity/02-raster.svg
@@ -0,0 +1,4005 @@
+
+
+
diff --git a/python/example/plasticity/02-rates.svg b/python/example/plasticity/02-rates.svg
new file mode 100644
index 000000000..c2b4f9acf
--- /dev/null
+++ b/python/example/plasticity/02-rates.svg
@@ -0,0 +1,2090 @@
+
+
+
diff --git a/python/example/plasticity/03-final-graph.svg b/python/example/plasticity/03-final-graph.svg
new file mode 100644
index 000000000..6b31fdae9
--- /dev/null
+++ b/python/example/plasticity/03-final-graph.svg
@@ -0,0 +1,1208 @@
+
+
+
diff --git a/python/example/plasticity/03-final-matrix.svg b/python/example/plasticity/03-final-matrix.svg
new file mode 100644
index 000000000..746c6e5d7
--- /dev/null
+++ b/python/example/plasticity/03-final-matrix.svg
@@ -0,0 +1,388 @@
+
+
+
diff --git a/python/example/plasticity/03-initial-graph.svg b/python/example/plasticity/03-initial-graph.svg
new file mode 100644
index 000000000..f01b66872
--- /dev/null
+++ b/python/example/plasticity/03-initial-graph.svg
@@ -0,0 +1,408 @@
+
+
+
diff --git a/python/example/plasticity/03-initial-matrix.svg b/python/example/plasticity/03-initial-matrix.svg
new file mode 100644
index 000000000..42c20f7b9
--- /dev/null
+++ b/python/example/plasticity/03-initial-matrix.svg
@@ -0,0 +1,388 @@
+
+
+
diff --git a/python/example/plasticity/03-raster.svg b/python/example/plasticity/03-raster.svg
new file mode 100644
index 000000000..e091e70cc
--- /dev/null
+++ b/python/example/plasticity/03-raster.svg
@@ -0,0 +1,19409 @@
+
+
+
diff --git a/python/example/plasticity/03-rates.svg b/python/example/plasticity/03-rates.svg
new file mode 100644
index 000000000..dbb954ad0
--- /dev/null
+++ b/python/example/plasticity/03-rates.svg
@@ -0,0 +1,2013 @@
+
+
+
diff --git a/python/example/plasticity/homeostasis.py b/python/example/plasticity/homeostasis.py
new file mode 100644
index 000000000..4edf354ee
--- /dev/null
+++ b/python/example/plasticity/homeostasis.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+
+import arbor as A
+from arbor import units as U
+import numpy as np
+
+from util import plot_spikes, plot_network, randrange
+from random_network import random_network
+
+# global parameters
+# cell count
+N = 10
+# total runtime [ms]
+T = 10000
+# one interval [ms]
+t_interval = 100
+# numerical time step [ms]
+dt = 0.1
+# Set seed for numpy
+np.random.seed = 23
+# setpoint rate in kHz
+setpoint_rate = 0.1
+# sensitivty towards deviations from setpoint
+sensitivity = 200
+
+
+class homeostatic_network(random_network):
+ def __init__(self, N, setpoint_rate, sensitivity) -> None:
+ super().__init__(N)
+ self.max_inc = 8
+ self.max_out = 8
+ self.setpoint = setpoint_rate
+ self.alpha = sensitivity
+
+
+if __name__ == "__main__":
+ rec = homeostatic_network(N, setpoint_rate, sensitivity)
+ sim = A.simulation(rec)
+ sim.record(A.spike_recording.all)
+
+ plot_network(rec, prefix="03-initial-")
+
+ t = 0
+ while t < T:
+ sim.run((t + t_interval) * U.ms, dt * U.ms)
+ if t < T / 2:
+ t += t_interval
+ continue
+ rates = np.zeros(N)
+ for (gid, _), time in sim.spikes():
+ if time < t:
+ continue
+ rates[gid] += 1
+ rates /= t_interval # kHz
+ dC = ((rec.setpoint - rates) * rec.alpha).astype(int)
+ unchangeable = set()
+ added = []
+ deled = []
+ for tgt in randrange(N):
+ if dC[tgt] == 0:
+ continue
+ for src in randrange(N):
+ if dC[tgt] > 0 and rec.add_connection(src, tgt):
+ added.append((src, tgt))
+ break
+ elif dC[tgt] < 0 and rec.del_connection(src, tgt):
+ deled.append((src, tgt))
+ break
+ unchangeable.add(tgt)
+ sim.update(rec)
+ print(f" * t={t:>4} f={rates} [!] {list(unchangeable)} [+] {added} [-] {deled}")
+ t += t_interval
+
+ plot_network(rec, prefix="03-final-")
+ plot_spikes(sim, N, t_interval, T, prefix="03-")
diff --git a/python/example/plasticity/random_network.py b/python/example/plasticity/random_network.py
new file mode 100644
index 000000000..cc58c90af
--- /dev/null
+++ b/python/example/plasticity/random_network.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+
+import arbor as A
+from arbor import units as U
+import numpy as np
+
+from util import plot_spikes, plot_network
+from unconnected import unconnected
+
+# global parameters
+# cell count
+N = 10
+# total runtime [ms]
+T = 1000
+# one interval [ms]
+t_interval = 10
+# numerical time step [ms]
+dt = 0.1
+
+
+class random_network(unconnected):
+ def __init__(self, N) -> None:
+ super().__init__(N)
+ self.syn_weight = 80
+ self.syn_delay = 0.5 * U.ms
+ # format [to, from]
+ self.connections = np.zeros(shape=(N, N), dtype=np.uint8)
+ self.inc = np.zeros(N, np.uint8)
+ self.out = np.zeros(N, np.uint8)
+ self.max_inc = 4
+ self.max_out = 4
+
+ def connections_on(self, gid: int):
+ return [
+ A.connection((source, "src"), "tgt", self.syn_weight, self.syn_delay)
+ for source in range(self.N)
+ for _ in range(self.connections[gid, source])
+ ]
+
+ def add_connection(self, src: int, tgt: int) -> bool:
+ if tgt == src or self.inc[tgt] >= self.max_inc or self.out[src] >= self.max_out:
+ return False
+ self.inc[tgt] += 1
+ self.out[src] += 1
+ self.connections[tgt, src] += 1
+ return True
+
+ def del_connection(self, src: int, tgt: int) -> bool:
+ if tgt == src or self.connections[tgt, src] <= 0:
+ return False
+ self.inc[tgt] -= 1
+ self.out[src] -= 1
+ self.connections[tgt, src] -= 1
+ return True
+
+ def rewire(self):
+ tries = self.N * self.N * self.max_inc * self.max_out
+ while (
+ tries > 0
+ and self.inc.sum() < self.N * self.max_inc
+ and self.out.sum() < self.N * self.max_out
+ ):
+ src, tgt = np.random.randint(self.N, size=2, dtype=int)
+ self.add_connection(src, tgt)
+ tries -= 1
+
+
+if __name__ == "__main__":
+ rec = random_network(10)
+ rec.rewire()
+ sim = A.simulation(rec)
+ sim.record(A.spike_recording.all)
+ t = 0
+ while t < T:
+ t += t_interval
+ sim.run(t * U.ms, dt * U.ms)
+
+ plot_network(rec, prefix="02-")
+ plot_spikes(sim, N, t_interval, T, prefix="02-")
diff --git a/python/example/plasticity/unconnected.py b/python/example/plasticity/unconnected.py
new file mode 100644
index 000000000..e9a07ec99
--- /dev/null
+++ b/python/example/plasticity/unconnected.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+
+import arbor as A
+from arbor import units as U
+from util import plot_spikes
+
+# global parameters
+# cell count
+N = 10
+# total runtime [ms]
+T = 1000
+# one interval [ms]
+t_interval = 10
+# numerical time step [ms]
+dt = 0.1
+
+
+class unconnected(A.recipe):
+ def __init__(self, N) -> None:
+ super().__init__()
+ self.N = N
+ # Cell prototype
+ self.cell = A.lif_cell("src", "tgt")
+ # random seed [0, 100]
+ self.seed = 42
+ # event generator parameters
+ self.gen_weight = 20
+ self.gen_freq = 1 * U.kHz
+
+ def num_cells(self) -> int:
+ return self.N
+
+ def event_generators(self, gid: int):
+ return [
+ A.event_generator(
+ "tgt",
+ self.gen_weight,
+ A.poisson_schedule(freq=self.gen_freq, seed=self.cell_seed(gid)),
+ )
+ ]
+
+ def cell_description(self, gid: int):
+ return self.cell
+
+ def cell_kind(self, gid: int) -> A.cell_kind:
+ return A.cell_kind.lif
+
+ def cell_seed(self, gid: int):
+ return self.seed + gid * 100
+
+
+if __name__ == "__main__":
+ rec = unconnected(N)
+ sim = A.simulation(rec)
+ sim.record(A.spike_recording.all)
+
+ t = 0
+ while t < T:
+ t += t_interval
+ sim.run(t * U.ms, dt * U.ms)
+
+ plot_spikes(sim, rec.num_cells(), t_interval, T, prefix="01-")
diff --git a/python/example/plasticity/util.py b/python/example/plasticity/util.py
new file mode 100644
index 000000000..554259ad3
--- /dev/null
+++ b/python/example/plasticity/util.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+import matplotlib.pyplot as plt
+import numpy as np
+from scipy.signal import savgol_filter
+import networkx as nx
+
+
+def plot_network(rec, prefix=""):
+ fg, ax = plt.subplots()
+ ax.matshow(rec.connections)
+ fg.savefig(f"{prefix}matrix.pdf")
+ fg.savefig(f"{prefix}matrix.png")
+ fg.savefig(f"{prefix}matrix.svg")
+
+ n = rec.num_cells()
+ fg, ax = plt.subplots()
+ g = nx.MultiDiGraph()
+ g.add_nodes_from(np.arange(n))
+ for i in range(n):
+ for j in range(n):
+ for _ in range(rec.connections[i, j]):
+ g.add_edge(i, j)
+ nx.draw(g, with_labels=True, font_weight="bold")
+ fg.savefig(f"{prefix}graph.pdf")
+ fg.savefig(f"{prefix}graph.png")
+ fg.savefig(f"{prefix}graph.svg")
+
+
+def plot_spikes(sim, n_cells, t_interval, T, prefix=""):
+ # number of intervals
+ n_interval = int((T + t_interval - 1) // t_interval)
+ print(n_interval, T, t_interval)
+
+ # Extract spikes
+ times = []
+ gids = []
+ rates = np.zeros(shape=(n_interval, n_cells))
+ for (gid, _), time in sim.spikes():
+ times.append(time)
+ gids.append(gid)
+ it = int(time // t_interval)
+ rates[it, gid] += 1
+
+ fg, ax = plt.subplots()
+ ax.scatter(times, gids, c=gids)
+ ax.set_xlabel("Time $(t/ms)$")
+ ax.set_ylabel("GID")
+ ax.set_xlim(0, T)
+ fg.savefig(f"{prefix}raster.pdf")
+ fg.savefig(f"{prefix}raster.png")
+ fg.savefig(f"{prefix}raster.svg")
+
+ ts = np.arange(n_interval) * t_interval
+ mean_rate = savgol_filter(rates.mean(axis=1), window_length=5, polyorder=2)
+ fg, ax = plt.subplots()
+ ax.plot(ts, rates)
+ ax.plot(ts, mean_rate, color="0.8", lw=4, label="Mean rate")
+ ax.set_xlabel("Time $(t/ms)$")
+ ax.legend()
+ ax.set_ylabel("Rate $(kHz)$")
+ ax.set_xlim(0, T)
+ fg.savefig(f"{prefix}rates.pdf")
+ fg.savefig(f"{prefix}rates.png")
+ fg.savefig(f"{prefix}rates.svg")
+
+
+def randrange(n: int):
+ res = np.arange(n, dtype=int)
+ np.random.shuffle(res)
+ return res