Morphologies

Morphologies are the 3D representation of a cell. A morphology consists of head-to-tail connected branches, and branches consist of a series of points with radii. Points can be labeled and can have multiple user-defined properties per point.

../_images/morphology.png
../_images/morphology_dark.png
  1. The root branch, shaped like a soma because of its radii.

  2. A child branch of the root branch.

  3. Another child branch of the root branch.

Network configurations can contain a morphologies key to define the morphologies that should be processed and assigned to cells. See Adding morphologies for a guide on the possibilities. Morphologies can be stored in a network in the MorphologyRepository.

Parsing morphologies

A morphology file can be parsed with parse_morphology_file, if you already have the content of a file you can pass that directly into parse_morphology_content:

from bsb import parse_morphology_file

# Import a morphology from a file
morpho = parse_morphology_file("data/neuron_A.swc")

Important

The default parser only supports SWC files. Use the morphio parser for ASC files.

There are many different formats and even multiple conventions per format for parsing morphologies. To support these diverse approaches the framework provides configurable Morphology parsers. You can pass the type of parser and additional arguments:

from bsb import parse_morphology_file

morpho = parse_morphology_file("./my_file.swc", parser="morphio", flags=["no_duplicates"])

Once we have our Morphology object we can save it in Storage; storages and networks have a morphologies attribute that links to a MorphologyRepository that can save and load morphologies:

from bsb import Storage

# Store it in a MorphologyRepository to use it later.
store = Storage("hdf5", "morphologies.hdf5")
store.morphologies.save("my_morphology", morpho)

Constructing morphologies

Create your branches, attach them in a parent-child relationship, and provide the roots to the Morphology constructor:

from bsb import Branch, Morphology
import numpy as np

root = Branch(
  # XYZ
  points=np.array([
    [0, 1, 2],
    [0, 1, 2],
    [0, 1, 2],
  ]),
  # radii
  radii=np.array([1, 1, 1]),
)
child_branch = Branch(
  points=np.array([
    [2, 3, 4],
    [2, 3, 4],
    [2, 3, 4],
  ]),
  radii=np.array([1, 1, 1]),
)
root.attach_child(child_branch)
m = Morphology([root])

Basic use

Morphologies and branches contain spatial data in the points and radii attributes. Points can be individually labelled with arbitrary strings, and additional properties for each point can be assigned to morphologies/branches:

import numpy as np

from bsb import from_storage

# Load the morphology from the Scaffold object
scaffold = from_storage("morphologies.hdf5")
morpho = scaffold.morphologies.load("my_morphology")

Once loaded we can do transformations, label or assign properties on the morphology:

# Take a branch
special_branch = morpho.branches[3]
# Assign some labels to the whole branch
special_branch.label(["axon", "special"])
# Assign labels only to the first quarter of the branch
first_quarter = np.arange(len(special_branch)) < len(special_branch) / 4
special_branch.label(["initial_segment"], first_quarter)
# Assign random data as the `random_data` property to the branch
special_branch.set_properties(random_data=np.random.random(len(special_branch)))
print(f"Random data for each point: {special_branch.random_data}")

Once you are done with the morphology you can save it again:

scaffold.morphologies.save("processed_morphology", morpho)

Note

You can assign as many labels as you like (2^64 combinations max 😇)! Labels’ll cost you almost no memory or disk space! You can also add as many properties as you like, but they’ll cost you memory and disk space per point on the morphology.

Labels

Branches or points can be labelled, and pieces of the morphology can be selected by their label. Labels are also useful targets to insert biophysical mechanisms into parts of the cell later on in simulation.

import numpy as np

from bsb import from_storage

# Load the morphology from the Scaffold object
scaffold = from_storage("morphologies.hdf5")
morpho = scaffold.morphologies.load("my_morphology")
# Filter branches
big_branches = [b for b in morpho.branches if np.any(b.radii > 2)]
for b in big_branches:
    # Label all points on the branch as a `big_branch` point
    b.label(["big_branch"])
    if b.is_terminal:
        # Label the last point on terminal branches as a `tip`
        b.label(["tip"], [-1])

scaffold.morphologies.save("labelled_morphology", morpho)

Properties

Branches and morphologies can be given additional properties. The basic properties are x, y, z, radii and labels. You can pass additional properties to the properties argument of the Branch constructor. They will be automatically joined on the morphology.

Subtree transformations

A Subtree is a (sub)set of a morphology defined by a set of roots and all of its downstream branches (i.e. the branches emanating from a set of roots). A subtree with roots equal to the roots of the morphology is equal to the entire morphology, and all transformations valid on a subtree are also valid morphology transformations.

Creating subtrees

Subtrees can be selected using label(s) on the morphology.

../_images/tuft_select.png
../_images/tuft_select_dark.png
axon = morpho.subtree("axon")
# Multiple labels can be given
hybrid = morpho.subtree("proximal", "distal")

Warning

Branches will be selected as soon as they have one or more points labelled with a selected label.

Selections will always include all the branches emanating (downtree) from the selection as well:

../_images/emanating.png
../_images/emanating_dark.png
tuft = morpho.subtree("dendritic_piece")

Translation

axon.translate([24, 100, 0])

Centering

Subtrees may center themselves so that the point (0, 0, 0) becomes the geometric mean of the roots.

../_images/center.png
../_images/center_dark.png

Rotation

Subtrees may be rotated around a singular point, by giving a Rotation (and a center, by default 0):

../_images/rotate_tree.png
../_images/rotate_tree_dark.png
from scipy.spatial.transform import Rotation

r = Rotation.from_euler("xy", [90, 90], degrees=True)
dendrites.rotate(r)
../_images/rotate_dend.png
../_images/rotate_dend_dark.png
dendrite.rotate(r)

Note that this creates a gap, because we are rotating around the center, root-rotation might be preferred here.

Root-rotation

Subtrees may be root-rotated around each respective root in the tree:

../_images/root_rotate_dend.png
../_images/root_rotate_dend_dark.png
dendrite.root_rotate(r)
../_images/root_rotate_tree.png
../_images/root_rotate_tree_dark.png
dendrites.root_rotate(r)

Additionally, you can root-rotate from a point of the subtree instead of its root. In this case, points starting from the point selected will be rotated.

To do so, set the downstream_of parameter with the index of the point of your interest.

# rotate all points after the second point in the subtree
# i.e.: points at index 0 and 1 will not be rotated.
dendrites.root_rotate(r, downstream_of=2)

Note

This feature can only be applied to subtrees with a single root

Gap closing

Subtree gaps between parent and child branches can be closed:

../_images/close_gaps.png
../_images/close_gaps_dark.png
dendrites.close_gaps()

Note

The gaps between any subtree branch and its parent will be closed, even if the parent is not part of the subtree. This means that gaps of roots of a subtree may be closed as well. Gaps _between_ roots are never collapsed.

See also

Collapsing

Collapsing

Collapse the roots of a subtree onto a single point, by default the origin.

../_images/collapse.png
../_images/collapse_dark.png
roots.collapse()

Call chaining

Calls to any of the above functions can be chained together:

dendrites.close_gaps().center().rotate(r)

Advanced features

Morphology preloading

Reading the morphology data from the repository takes time. Usually morphologies are passed around in the framework as StoredMorphologies. These objects have a load method to load the Morphology object from storage and a get_meta method to return the metadata.

Morphology selectors

The most common way of telling the framework which morphologies to use is through MorphologySelectors. Currently you can select morphologies by_name or from_neuromorpho:

"morphologies": [
  {
    "select": "by_name",
    "names": ["my_morpho_1", "all_other_*"]
  },
  {
    "select": "from_neuromorpho",
    "names": ["H17-03-013-11-08-04_692297214_m", "cell010_GroundTruth"]
  }
]

If you want to make your own selector, you should implement the validate and pick methods.

validate can be used to assert that all the required morphologies and metadata are present, while pick needs to return True/False to include a morphology in the selection. Both methods are handed StoredMorphology objects. Only load morphologies if it is impossible to determine the outcome from the metadata alone.

The following example creates a morphology selector selects morphologies based on the presence of a user defined metadata "size":

from bsb import config, MorphologySelector

@config.node
class MySizeSelector(MorphologySelector, classmap_entry="by_size"):
  min_size = config.attr(type=float, default=20)
  max_size = config.attr(type=float, default=50)

  def validate(self, morphos):
    if not all("size" in m.get_meta() for m in morphos):
      raise Exception("Missing size metadata for the size selector")

  def pick(self, morpho):
    meta = morpho.get_meta()
    return meta["size"] > self.min_size and meta["size"] < self.max_size

After installing your morphology selector as a plugin, you can use by_size as selector:

{
  "cell_type_A": {
    "spatial": {
      "morphologies": [
        {
          "select": "by_size",
          "min_size": 35
        }
      ]
    }
  }
}
network.cell_types.cell_type_A.spatial.morphologies = [MySizeSelector(min_size=35)]

Morphology metadata

Currently unspecified, up to the Storage and MorphologyRepository support to return a dictionary of available metadata from get_meta.

Morphology distributors

A MorphologyDistributor is a special type of Distributor that is called after positions have been generated by a PlacementStrategy to assign morphologies, and optionally rotations.

The distribute method is called with the partitions, the indicators for the cell type and the positions; the method has to return a MorphologySet or a tuple together with a RotationSet.

Warning

The rotations returned by a morphology distributor may be overruled when a RotationDistributor is defined for the same placement block.

Distributor configuration

Each placement block may contain a DistributorsNode, which can specify the morphology and/or rotation distributors, and any other property distributor:

{
  "placement": {
    "placement_A": {
      "strategy": "bsb.placement.RandomPlacement",
      "cell_types": ["cell_A"],
      "partitions": ["layer_A"],
      "distribute": {
        "morphologies": {
          "strategy": "roundrobin"
        }
      }
    }
  }
}
from bsb import RoundRobinMorphologies

network.placement.placement_A.distribute.morphologies = RoundRobinMorphologies()

Distributor interface

The generic interface has a single function: distribute(positions, context). The context contains .partitions and .indicator for additional placement context. The distributor must return a dataset of N floats, where N is the number of positions you’ve been given, so that it can be stored as an additional property on the cell type.

The morphology distributors have a slightly different interface, and receive an additional morphologies argument: distribute(positions, morphologies, context). The morphologies are a list of StoredMorphology, that the user has configured to use for the cell type under consideration and that the distributor should consider the input, or template morphologies for the operation.

The morphology distributor is supposed to return an array of N integers, where each integer refers to an index in the list of morphologies. e.g.: if there are 3 morphologies, putting a 0 on the n-th index means that cell N will be assigned morphology 0 (which is the first morphology in the list). 1 and 2 refer to the 2nd and 3rd morphology, and returning any other values would be an error.

If you need to break out of the morphologies that were handed to you, morphology distributors are also allowed to return their own MorphologySet. Since you are free to pass any list of morphology loaders to create a morphology set, you can put and assign any morphology you like.

Tip

MorphologySets work on StoredMorphologies! This means that it is your job to save the morphologies into your network first, and to use the returned values of the save operation as input to the morphology set:

def distribute(self, positions, morphologies, context):
  # We're ignoring what is given, and make our own morphologies
  morphologies = [Morphology(...) for p in positions]
  # If we pass the `morphologies` to the `MorphologySet`, we create an error.
  # So we save the morphologies, and use the stored morphologies instead.
  loaders = [
    self.scaffold.morphologies.save(f"morpho_{i}", m)
    for i, m in enumerate(morphologies)
  ]
  return MorphologySet(loaders, np.arange(len(loaders)))

This is cumbersome, so if you plan on generating new morphologies, use a morphology generator instead.

Finally, each morphology distributor is allowed to return an additional argument to assign rotations to each cell as well. The return value must be a RotationSet.

Warning

The rotations returned from a morphology distributor may be ignored and replaced by the values of the rotation distributor, if the user configures one.

The following example creates a distributor that selects smaller morphologies the closer the position is to the top of the partition:

import numpy as np
from scipy.stats.distributions import norm

from bsb import MorphologyDistributor


class SmallerTopMorphologies(MorphologyDistributor, classmap_entry="small_top"):
    def distribute(self, positions, morphologies, context):
        # Get the maximum Y coordinate of all the partitions boundaries
        top_of_layers = np.maximum([p.data.mdc[1] for p in context.partitions])
        depths = top_of_layers - positions[:, 1]
        # Get all the heights of the morphologies, by peeking into the morphology metadata
        msizes = [
            loader.get_meta()["mdc"][1] - loader.get_meta()["ldc"][1]
            for loader in morphologies
        ]
        # Pick deeper positions for bigger morphologies.
        weights = np.column_stack(
            [norm(loc=size, scale=20).pdf(depths) for size in msizes]
        )
        # The columns are the morphology ids, so make an arr from 0 to n morphologies.
        picker = np.arange(weights.shape[1])
        # An array to store the picked weights
        picked = np.empty(weights.shape[0], dtype=int)
        rng = np.default_rng()
        for i, p in enumerate(weights):
            # Pick a value from 0 to n, based on the weights.
            picked[i] = rng.choice(picker, p=p)
        # Return the picked morphologies for each position.
        return picked

Then, after installing your distributor as a plugin, you can use small_top:

{
  "placement": {
    "placement_A": {
      "strategy": "bsb.placement.RandomPlacement",
      "cell_types": ["cell_A"],
      "partitions": ["layer_A"],
      "distribute": {
        "morphologies": {
          "strategy": "small_top"
        }
      }
    }
  }
}
network.placement.placement_A.distribute.morphologies = SmallerTopMorphologies()

Morphology generators

Continuing on the morphology distributor, one can also make a specialized generator of morphologies. The generator takes the same arguments as a distributor, but returns a list of Morphology objects, and the morphology indices to make use of them. It can also return rotations as a 3rd return value.

This example is a morphology generator that generates a simple stick that drops down to the origin for each position:

import numpy as np

from bsb import Branch, Morphology, MorphologyGenerator


class TouchTheBottomMorphologies(MorphologyGenerator, classmap_entry="touchdown"):
    def generate(self, positions, morphologies, context):
        return [
            Morphology([Branch([pos, [pos[1], 0, pos[2]]], [1, 1])])
            for pos in positions
        ], np.arange(len(positions))

Then, after installing your generator as a plugin, you can use touchdown:

{
  "placement": {
    "placement_A": {
      "strategy": "bsb.placement.RandomPlacement",
      "cell_types": ["cell_A"],
      "partitions": ["layer_A"],
      "distribute": {
        "morphologies": {
          "strategy": "touchdown"
        }
      }
    }
  }
}
network.placement.placement_A.distribute.morphologies = TouchTheBottomMorphologies()

MorphologySets

MorphologySets are the result of distributors assigning morphologies to placed cells. They consist of a list of StoredMorphologies, a vector of indices referring to these stored morphologies and a vector of rotations. You can use iter_morphologies to iterate over each morphology.

ps = network.get_placement_set("my_detailed_neurons")
positions = ps.load_positions()
morphology_set = ps.load_morphologies()
rotations = ps.load_rotations()
cache = morphology_set.iter_morphologies(cache=True)
for pos, morpho, rot in zip(positions, cache, rotations):
  morpho.rotate(rot)