Create your own components¶
Note
This guide assumes that you have a good understanding of the basis of BSB; i.e., that
you are familiar with the concept of BSB components
. You should have read the whole
the getting started section.
In this tutorial, we are going to guide you through the process of creating your own component for a new Connection strategy. We will start from the following configuration file (corresponds to the first network file from the getting started tutorial):
{
"name": "Starting example",
"storage": {
"engine": "hdf5",
"root": "network.hdf5"
},
"network": {
"x": 200.0,
"y": 200.0,
"z": 200.0
},
"regions": {
"brain_region": {
"type": "stack",
"children": ["base_layer", "top_layer"]
}
},
"partitions": {
"base_layer": {
"type": "layer",
"thickness": 100
},
"top_layer": {
"type": "layer",
"thickness": 100
}
},
"cell_types": {
"base_type": {
"spatial": {
"radius": 2.5,
"density": 3.9e-4
}
},
"top_type": {
"spatial": {
"radius": 7,
"count": 40
}
}
},
"placement": {
"example_placement": {
"strategy": "bsb.placement.RandomPlacement",
"cell_types": ["base_type"],
"partitions": ["base_layer"]
},
"top_placement": {
"strategy": "bsb.placement.RandomPlacement",
"cell_types": ["top_type"],
"partitions": ["top_layer"]
}
},
"connectivity": {
"A_to_B": {
"strategy": "bsb.connectivity.AllToAll",
"presynaptic": {
"cell_types": ["base_type"]
},
"postsynaptic": {
"cell_types": ["top_type"]
}
}
}
}
Let’s save this new configuration in our project folder under the name config_connectome.json
Description of the strategy to implement¶
For the connectivity, we will consider that the cells base_type
can connect to all top_type
cells
within a sphere of radius
100
µm. You can consider this as a simplified model of
distance based connectivity.
Components boiler plate¶
BSB components are written as Python classes. When the BSB parses your Configuration component, it resolves the path to its class or function and import it. Hence, your components should be written as importable modules.
In our case, we will place our ConnectionStrategy class in connectome.py
:
from bsb import config, ConnectionStrategy
@config.node
class DistanceConnectivity(ConnectionStrategy):
# add your attributes here
def connect(self, presyn_collection, postsyn_collection):
# write your code here
pass
The ConnectionStrategy
here requires you to
implement the connect
function.
Tip
Take the time to familiarize yourself with the class and the function before continuing.
Note
In this example, we only implemented the required function for the strategy but you can also overwrite the other functions of the interface, if you need it. Please refer to the documentation on the classes you want to implement for more information.
Note that this strategy leverages the @config.node
python decorator.
The configuration node decorator allows you to pass the parameters
defined in the configuration file to the class. It will also handle the
type testing of your configuration attributes (e.g., make sure your
radius
parameter is a positive float number). We will see in the following sections how
to create your class configuration attributes.
Add configuration attributes¶
For our strategy, we need to pass a list of parameters to its class, through our configuration file.
Here, we need a radius parameter which translates into the following code in our class:
from bsb import config, ConnectionStrategy, types
@config.node
class DistanceConnectivity(ConnectionStrategy):
radius: float = config.attr(type=types.float(min=0), required=True)
def connect(self, presyn_collection, postsyn_collection):
# write your code here
pass
Here, radius is a positive float that is required, this means that the BSB will throw a
ConfigurationError
if the parameter is not provided.
This will also happen if the parameters provided for the configuration attributes do not match
the expected types.
At this stage, you have created a python class with minimal code implementation, you should now link it to your configuration file. To import our class in our configuration file, we will modify the connectivity block:
"connectivity": {
"A_to_B": {
"strategy": "connectome.DistanceConnectivity",
"radius": 100,
"presynaptic": {
"cell_types": ["base_type"]
},
"postsynaptic": {
"cell_types": ["top_type"]
}
}
}
Implement the python methods¶
Starting from now, we introduce the term of Chunk which is a volume unit used to decompose your circuit topology into independent pieces to parallelize the circuit reconstruction (see this section for more details).
Here, we are going to use the connect function to produce and store
ConnectivitySets
.
First some definition:
Hemitype
. An Hemitype
serves as the interface to define a connection population and its parameters in the
Configuration.HemitypeCollection
allows
you to filter the cells of an Hemitype according to a list of Chunk.The parameters of connect are therefore the pre- and post-synaptic HemitypeCollection
.
This class provides a placement
method that we will use to iterate over its cell types populations PlacementSet
.
def connect(self, presyn_collection, postsyn_collection):
# For each presynaptic placement set
for pre_ps in presyn_collection.placement:
# Load all presynaptic positions
presyn_positions = pre_ps.load_positions()
# For each postsynaptic placement set
for post_ps in postsyn_collection.placement:
# Load all postsynaptic positions
postsyn_positions = post_ps.load_positions()
The next step is to filter the postsynaptic cells that are within the sphere of each of our
presynaptic cell. We can use the norm
function to measure the distance between one presynaptic cell and all its potential targets.
The ones we keep are within the radius
defined as attribute of the class.
# For each presynaptic cell to connect
for j, pre_position in enumerate(presyn_positions):
# We measure the distance of each postsyn cell with respect to the
# presyn cell
dist = np.linalg.norm(postsyn_positions - pre_position, axis=1)
# We keep only the ids that are within the sphere radius
ids_to_keep = np.where(dist <= self.radius)[0]
nb_connections = len(ids_to_keep)
Finally, we use the
ConnectionStrategy.connect_cells
function, which will create and store our resulting ConnectivitySet. It will also assign it a name
based on the Strategy name and eventually the pre- and post-synaptic populations connected (if there
are more than one pair).
This function requires for each individual pair of cell, their connection location:
the index of the cell within its
PlacementSet
the index of the morphology branch
the index of the morphology branch point.
Because we are not using morphologies here the second and third indexes should be set to -1
:
for j, pre_position in enumerate(presyn_positions):
# We measure the distance of each postsyn cell with respect to the
# presyn cell
dist = np.linalg.norm(postsyn_positions - pre_position, axis=1)
# We keep only the ids that are within the sphere radius
ids_to_keep = np.where(dist <= self.radius)[0]
nb_connections = len(ids_to_keep)
# We create two connection location array and set their neuron ids.
pre_locs = np.full((nb_connections, 3), -1, dtype=int)
pre_locs[:, 0] = j
post_locs = np.full((nb_connections, 3), -1, dtype=int)
post_locs[:, 0] = ids_to_keep
self.connect_cells(pre_ps, post_ps, pre_locs, post_locs)
You have done it! Congrats! Your final connectome.py should look like this:
import numpy as np
from bsb import ConnectionStrategy, config, types
@config.node
class DistanceConnectivity(ConnectionStrategy):
"""
Connect cells based on the distance between their respective somas.
The algorithm will search for potential targets surrounding the presynaptic cells
in a sphere with a given radius.
"""
radius: float = config.attr(type=types.float(min=0), required=True)
"""Radius of the sphere surrounding the presynaptic cell to filter potential
postsynaptic targets"""
def connect(self, presyn_collection, postsyn_collection):
# For each presynaptic placement set
for pre_ps in presyn_collection.placement:
# Load all presynaptic positions
presyn_positions = pre_ps.load_positions()
# For each postsynaptic placement set
for post_ps in postsyn_collection.placement:
# Load all postsynaptic positions
postsyn_positions = post_ps.load_positions()
# For each presynaptic cell to connect
for j, pre_position in enumerate(presyn_positions):
# We measure the distance of each postsyn cell with respect to the
# presyn cell
dist = np.linalg.norm(postsyn_positions - pre_position, axis=1)
# We keep only the ids that are within the sphere radius
ids_to_keep = np.where(dist <= self.radius)[0]
nb_connections = len(ids_to_keep)
# We create two connection location array and set their
# neuron ids.
pre_locs = np.full((nb_connections, 3), -1, dtype=int)
pre_locs[:, 0] = j
post_locs = np.full((nb_connections, 3), -1, dtype=int)
post_locs[:, 0] = ids_to_keep
self.connect_cells(pre_ps, post_ps, pre_locs, post_locs)
Tip
Comment your code! If not for you (because you are going to forget about it in a month), at least for the other people that will read it afterwards. 😉
Enjoy¶
You have done the hardest part! Now, you should be able to run the reconstruction once again with your brand new component.
bsb compile config_connectome.json --verbosity 3
It is best practice to keep your component code in a subfolder with the same name as
your model. For example, if you are modelling the cerebellum, create a folder called
cerebellum
. Inside place an __init__.py
file, so that Python can import code from
it. Then you best subdivide your code based on component type, e.g. keep connectivity
strategies in a file called connectome.py
. That way, your connectivity components are
available in your model as cerebellum.connectome.MyComponent
. It will also make it
easy to distribute your code as a package!
More advanced component writing¶
If you want to see another example on how to write BSB components, you can take a look at the placement strategy example in this section
Next steps: