Tutorial

A quick tour of what can be done with DeNSE.

A single neuron

First, one should import all the modules and variables that are necessary for the simulation:

1import dense as ds
2from dense.units import *

The first line makes the DeNSE simulator available and let us call it through the variable ds. The second line imports all the units (e.g. time units like ms, minutes…) which will be used to set the properties of the simulation and of the neurons.

Once this is done, we can create our first neuron:

1n = ds.create_neurons()

This creates a default neuron without any neurites (a single soma). Let’s add an axon and a dendrite:

1n.create_neurites(2, names=["axon", "dendrite_1"])

by default, neurons have their has_axon variable set to True, meaning that the first created neurite will be an axon and all subsequent neurites will be dendrites. Now that the neurites have been created, they can be accessed through n.axon and n.dendrites (or n.neurites to get all of them at the same time). We will set the parameters of the dendrite to make it grow more slowly than the axon. Because the default name of the first dendrite is "dendrite_1", this reads:

 1print("{!r}".format(n.axon))
 2print(n.dendrites)
 3print(n.neurites)
 4
 5dprop = {
 6    "speed_growth_cone": 0.2*um/minute,
 7    "taper_rate": 0.01,
 8    "use_uniform_branching": True,
 9    "uniform_branching_rate": 0.05*cph
10}
11
12n.axon.set_properties({"speed_growth_cone": 0.5*um/minute})
13n.dendrites["dendrite_1"].set_properties(dprop)

One can then plot and simulate the growth of this neuron:

1''' Plot the initial state '''
2
3ds.plot.plot_neurons()
4
5
6''' Simulate a few days then plot the result '''
7
8ds.simulate(7*day)
9ds.plot.plot_neurons()

Two interacting neurons

Again, import all necessary modules and variables:

1import dense as ds
2from dense.units import *

Once this is done, we can set the various parameters for the simulation and the neuronal properties:

 1num_neurons = 2
 2
 3simu_params = {
 4
 5    "resolution": 1.*minute,
 6    "num_local_threads": num_omp,
 7    "seeds": [0, 1],
 8    "environment_required": False,
 9}
10
11neuron_params = {
12    "growth_cone_model": "simple-random-walk",
13    "position": [(0., 0.), (100., 100.)]*um,
14    "persistence_length": 200.*um,
15    "speed_growth_cone": 0.03*um/minute,
16    "taper_rate": 1./400.,
17    "use_uniform_branching": True,
18    "uniform_branching_rate": 0.009*cph,
19}
20
21neurite_params = {
22    "axon": {"initial_diameter": 4.*um},
23    "dendrites": {"taper_rate": 1./200., "initial_diameter": 3.*um}
24}

The first line here declares the number of OpenMP processes that will be used, i.e. how many parallel threads will be used to perform the simulation. The second line will be used to set the number of neurons that will be simulated.

The third part contains the parameters related to the simulation: the timestep used to run the simulation, the number of threads used in the parallel simulation, the random seeds that will be used to generate random numbers (one per OpenMP thread), and whether the neurons are embedded in spatial boundaries.

Eventually, the neuron_params dictionary on line 11 contains the information that will be used to describe the growth of the two neurons, all expressed with their proper units (distances in microns, speed in micron per minutes, and branching events in ``counts per hour’’). As no environment was specified here, the positions of the neurons is also specified; there orientation (the direction of the axon) will be set randomly. Though we used the same parameters for both neurons here, this is not necessary and different parameters can be passed for each neurons through a list, as shown for the positions on line 15.

Once all these parameters are declared, we can configure DeNSE and create the neurons so that everything is ready for the simulation.

1# configure DeNSE
2ds.set_kernel_status(simu_params)
3
4# create neurons
5n = ds.create_neurons(n=num_neurons, params=neuron_params,
6                      neurite_params=neurite_params, num_neurites=2)

As can be seen above, one uses the ds variable to access the simulator main function. The set_kernel_status() function is used here to transfer the parameters to the kernel of DeNSE (the main simulator units). Once this is done, the create_neurons() is called in order to obtain two neurons with the specific set of parameters declared in neuron_params, and two neurites (by default the first is an axon, and the second is a dendrite) which initially possess a single branch protruding from the soma.

Once the neurons are created one can visualize them using plot_neurons(). The initial condition of the neurons can thus be visualized before starting the simulation.

Following neuron creation, the simulation can be started and its result can be visualized again:

1''' Plot the initial state '''
2
3ds.plot.plot_neurons()
4
5
6''' Simulate a few days then plot the result '''
7
8ds.simulate(7*day)
9ds.plot.plot_neurons()

After this first 7 day simulation, the parameters of the neurons can be changed to account for changes in developmental mechanisms, so that these new parameters can be used to simulate the next part of these cells’ growth.

 1''' Change parameters and simulate again '''
 2
 3# new dendritic and axonal parameters
 4axon_params = {
 5    "speed_growth_cone": 0.02*um/minute,
 6}
 7
 8dend_params = {
 9    "use_uniform_branching": False,
10    "speed_growth_cone": 0.01*um/minute,
11}
12
13neurite_params = {"axon": axon_params, "dendrites": dend_params}
14
15# update the properties of the neurons
16ds.set_object_properties(n, neurite_params=neurite_params)
17
18# simulate and plot again
19ds.simulate(7*day)
20ds.plot.plot_neurons()

Here we changed separately the dendritic and axonal parameters using the set_object_properties() function on the two neurons which are stored in the n variable (the neurons stored in a Population object).

Once the simulation is over, the shapes of the neurons that were obtained can be saved in standard morphology formats such as SWC or MorphoML (NeuroML). Note that the neuroml python module is necessary to use save_to_neuroml().

1# In the swc format
2ds.io.save_to_swc("neurons.swc", n)

Multiprocessing and random number generation

DeNSE support parallel simulations at the C++ level using OpenMP shared memory thread parallelism. Currently, the number of threads must be set at the beginning of a script, before any objects are created because objects are then allocated to a given thread which will be responsible for it. The number of thread can be set through

ds.set_kernel_status({"num_local_threads": 4})

if you want your simulation to run on four threads. Parallelism is implemented at the neuronal level: all neurites of a given neuron are associated to the same thread. There is therefore no point in allcating a number of threads which would be higher than the total amount of neurons in the simulation.

Warning

For people using simulateously DeNSE and other parallel simulation software, make sure that you or the software used do not set the OMP_NUM_THREADS variable to a value other than that used for DeNSE: this would lead to undefined behavior.

Since DeNSE also uses random numbers to make most of the actions associated to growth cone navigation, you can seed the random number generators used during the simulation through:

ds.set_kernel_status({"seeds": [1, 5, 3, 6]})

Note that one seed must be given for each thread. The result of a simulation should always be the same provided the number of threads and seeds used are identical.

Interactions and simulation speed, various tips

Simulation speed with DeNSE will vary greatly depending on the time resolution and interactions. By default, DeNSE uses a 1 minute timestep, with neuron-neuron interactions turned on, which can lead to rather long simulation times. To increase speed, several approaches may or may not be attractive to you.

  • Increasing the time resolution. Using ds.set_kernel_status({"resolution": 5.*minute}), you can reduce the number of steps necessary to complete the simulation; this also has another advantage because it increases the size of the compartments composing the neuron, thus reducing their number and speeding up the interactions. However, this will obviously make the final results more “crude”, as you subsample the real path of the neurites.

  • Increase the value of the "growth_threshold_polygon" kernel parameter; this parameter is related to the distance the growth cone must move away from its previous position for a polygon to be generated, at which point other growth cones will be able to interact with this new part of the branch. This brings speedups that are similar to increasing time resolution for the same reason mentioned previously: fewer geometric operations and polygons to deal with. Note that increasing this value could lead to approximations: growth cones that should have interacted may miss one another if you increase this value too far (over a few microns, for instance), though it remains very low for values below a micron.

  • Switch the interactions off using ds.set_kernel_status({"interactions": False}). Neuron-neuron interactions are currently the main source of CPU requirement and, though we know how we could reduce them further, it is a non-trivial task for which we currently do not have the necessary time. If you are interested an would like to help on that, feel free to contact us.

  • Do not use a spatial environment. Though this leads to lower speed gains compared to the other solutions, it might also help in specific cases.

Embedding neurons in space

One of the original aims for DeNSE was the study of neuronal cultures and devices, where the neurons can be embedded in more or less complex structures. In the simulator, simple spatial structures such as disks or rectangles can be generated directly from the Shape object. For more complex shapes, pre-constructed images in SVG or DXF formats can be imported using the culture_from_file() function.

 1''' Configuring the simulator and the neuronal properties '''
 2
 3num_omp = 5
 4num_neurons = 50
 5
 6simu_params = {
 7    "resolution": 10.*minute,
 8    "num_local_threads": num_omp,
 9    "seeds": [i for i in range(num_omp)],
10    "environment_required": True,
11    "interactions": False,
12}
13
14# configure DeNSE
15ds.set_kernel_status(simu_params)
16
17
18''' Create and set the environment '''
19
20env = ds.environment.Shape.rectangle(500*um, 500*um)
21
22ds.set_environment(env)
23

Once the environment is created, we can seed randomly the neurons inside it.

1soma_radius = 4.*um
2pos = env.seed_neurons(num_neurons, soma_radius=soma_radius, unit="um",
3                       return_quantity=False)

Warning

Setting the soma radius correctly is critical, otherwise the soma might “protrude” out of the environment, which will lead to issues for neurite creation.

We can then create the neurons:

 1neuron_params = {
 2    "growth_cone_model": "simple-random-walk",
 3    "position": [tuple(p) for p in pos]*um,
 4    "persistence_length": 200.*um,
 5    "speed_growth_cone": 0.03*um/minute,
 6    "soma_radius": soma_radius,
 7    "use_uniform_branching": True,
 8    "uniform_branching_rate": 0.009*cph,
 9}
10
11axon_params = {
12    "initial_diameter": 4.*um,
13    "max_arbor_length": 1.*cm,
14    "taper_rate": 1./400.,
15    "initial_diameter": 4.*um,
16}
17
18dend_params = {
19    "initial_diameter": 3.*um,
20    "max_arbor_length": 500.*um,
21    "taper_rate": 1./200.,
22    "initial_diameter": 3.*um,
23}
24
25neurite_params = {"axon": axon_params, "dendrite": dend_params}
26
27# create neurons
28n = ds.create_neurons(n=num_neurons, params=neuron_params,
29                      neurite_params=neurite_params,
30                      num_neurites=2)

And simulate:

1ds.simulate(4*day)
2ds.plot.plot_neurons(scale_text=False)

Which leads to the following structure:

../_images/neurons_in_square.webp

Complex neurons and parameters (Acimovic2011)

If you want to make a full neuronal network, you will probably want to specify specific neuronal shapes that can vary depending on the neurons. In this tutorial file, we show how to reproduce a study [Acimovic2011] using DeNSE.

This focuses especially on reproducing the shape of pyramidal neurons, with their specific neurite orientations and having varying number of neurites.

To do so, we start by defining the orientations for all possible neurites (between 4 and 6, with always at least an axon, an apical, and 2 basal dendrites) as well as their names.

 1'''
 2Neuronal parameters
 3'''
 4
 5num_neurons = 100
 6num_pyr     = 80
 7num_nonpyr  = 20
 8
 9density = 100 / (um**2)
10radius = 564.2*um
11
12soma_diam = 10.*um
13
14neuron_params = {
15    "growth_cone_model": "simple-random-walk",
16    "soma_radius": 0.5*soma_diam,
17    "use_van_pelt": True,
18    "persistence_length": 100.*um,
19    "random_rotation_angles": True,
20}
21
22# pyramidal parameters
23
24neurite_angles = {
25    "axon": 0.*deg,
26    "apical": 180.*deg,
27    "basal_1": 75.*deg,
28    "basal_2": -75.*deg,
29    "basal_4": 45.*deg,
30    "basal_5": -45.*deg
31}
32
33names = list(neurite_angles)

The combination of "random_rotation_angles": True and the specific angles means that all neurites will be randomly rotated “as a block” for each neuron.

Then we generate a random number of neurites for each neuron and generate the associated names:

1pyr_num_neur = rng.integers(4, 6, num_pyr, endpoint=True)
2
3neurite_names = [names[:n] for n in pyr_num_neur]

Because simulating many neurons can take time, in this example we set the kernel parameters so that neuron-neuron interactions are ignored and the progress of the simulation is printed:

1kernel_params = {
2    "resolution": resolution,
3    "num_local_threads": num_omp,
4    "interactions": False,
5    "print_progress": True
6}
7
8ds.set_kernel_status(kernel_params)

Finally we set the parameters and create the neurons:

1pyr_params = neuron_params.copy()
2pyr_params["neurite_angles"] = neurite_angles
3
4pyr = ds.create_neurons(num_pyr, pyr_params, num_neurites=pyr_num_neur,
5                        neurite_names=neurite_names,

If you run the whole file, you will get the following structure (scale bar is 50 \(\mu m\)):

../_images/acimovic.webp

Other Examples

The examples/ folder contains a collections of different applications cases illustrating DeNSE functions.

Models

examples/models/ contains examples concerning growth models.

examples/models/neurons/ contains a collections of DeNSE models built to generate various neuronal shapes : pyramidal cells, Purkinje cells, multipolar cells, chandelier cells, starbus amacrine, etc… The models were hand tuned for a qualitative resemblance with the corresponding cell types.

examples/models/neurons/ is an example code for importing and displaying a morphology file in neurom format.

examples/models/competition/ illustrates resource-based elongation models.

examples/models/branching/ shows codes to explore the branching models.

examples/models/random_walks/ Contains a code to run successively different simulations with different persistence lengths for a random walk base growth cone model.

Space Embedding

examples/space_embedding/ contains examples to make neurons grow in spatially bounded regions:

  • polygons/ shows an example of multiple neurons growing in a space limited region containing obstacles. The connectivity network is generated at the end of the simulation.

  • patterns/ illustrates growing of neurons in a complex geometry.

  • ordered_neurons/ shows a code to generate neurons placed at precise locations and with controlled initial orientation of dendrites.

  • droplets/ shows a code to generate a neuronal network in a complex geometry.

examples/multi_chambers/ is a special case of space embedding, where the culture is made of different chambers that communicate through channels of different shapes.

Growth Studies

examples/growth_studies/ contants different applications how to study network growth:

  • turning_angles/, where neurons arrive on an obstacle with different inclination

  • turning_walls_exp/, where neurons arrive in a chamber with different angles of the opening funnel

  • recorders/ shows the usage of recorders

References

Acimovic2011

J. Aćimović, T. Mäki-Marttunen, R. Havela, H. Teppola, and M.-L. Linne (2011). Modeling of Neuronal Growth In Vitro: Comparison of Simulation Tools NETMORPH and CX3D. EURASIP Journal on Bioinformatics & Systems Biology, 616382.