Skip to content

Commit

Permalink
Pass along SWC tags during morphologies loading. (#727)
Browse files Browse the repository at this point in the history
* Add tags to the loading of morphologies.
Add config for tags.
Add documentation and test.

* Apply suggestions from code review

Co-authored-by: Robin De Schepper <robin.deschepper93@gmail.com>

* Test tags passed as string or list of strings.

* Update bsb/morphologies/__init__.py

---------

Co-authored-by: Robin De Schepper <robin.deschepper93@gmail.com>
  • Loading branch information
drodarie and Helveg committed Jun 9, 2023
1 parent 53bf5d0 commit e8c1cac
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 12 deletions.
29 changes: 22 additions & 7 deletions bsb/morphologies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,17 +930,21 @@ def simplify(self, *args, optimize=True, **kwargs):
@classmethod
def from_swc(cls, file, branch_class=None, tags=None, meta=None):
"""
Create a Morphology from a file-like object.
Create a Morphology from an SWC file or file-like object.
:param file: path or file-like object to parse.
:param branch_class: Custom branch class
:type branch_class: type
:returns: The parsed morphology, with the SWC tags as a property.
:type branch_class: bsb.morphologies.Branch
:param tags: dictionary mapping morphology label id to its name
:type tags: dict
:param meta: dictionary header containing metadata on morphology
:type meta: dict
:returns: The parsed morphology.
:rtype: bsb.morphologies.Morphology
"""
if isinstance(file, str) or isinstance(file, Path):
with open(str(file), "r") as f:
return cls.from_swc(f, branch_class, meta=meta)
return cls.from_swc(f, branch_class, tags=tags, meta=meta)
if branch_class is None:
branch_class = Branch
return _swc_to_morpho(cls, branch_class, file.read(), tags=tags, meta=meta)
Expand All @@ -960,14 +964,22 @@ def from_swc_data(cls, data, branch_class=None, tags=None, meta=None):
return _swc_data_to_morpho(cls, branch_class, data, tags=tags, meta=meta)

@classmethod
def from_file(cls, path, branch_class=None, meta=None):
def from_file(cls, path, branch_class=None, tags=None, meta=None):
"""
Create a Morphology from a file on the file system through MorphIO.
:param path: path or file-like object to parse.
:param branch_class: Custom branch class
:type branch_class: bsb.morphologies.Branch
:param tags: dictionary mapping morphology label id to its name
:type tags: dict
:param meta: dictionary header containing metadata on morphology
:type meta: dict
"""
if branch_class is None:
branch_class = Branch
if path.endswith("swc"):
return cls.from_swc(path, branch_class, meta=meta)
return cls.from_swc(path, branch_class, tags=tags, meta=meta)
else:
return _import(cls, branch_class, path, meta=meta)

Expand Down Expand Up @@ -1770,7 +1782,10 @@ def _swc_data_to_morpho(cls, branch_cls, data, tags=None, meta=None):
# And the labels
branch_labels = labels[ptr:nptr]
for v in np.unique(branch_tags):
branch_labels.label([tag_map.get(v, f"tag_{v}")], branch_tags == v)
u_tags = tag_map.get(v, f"tag_{v}")
branch_labels.label(
[u_tags] if type(u_tags) == str else u_tags, branch_tags == v
)
ptr = nptr
# Use the views to construct the branch
branch = branch_cls(branch_points, branch_radii, branch_labels)
Expand Down
34 changes: 31 additions & 3 deletions bsb/storage/_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,15 @@ class MorphologyDependencyNode(FilePipelineMixin, FileDependencyNode):
"""

pipeline = config.list(type=MorphologyOperation)
name = config.attr()
name = config.attr(type=str, default=None, required=False)
"""
Name associated to the morphology. If not provided, the program will use the name of the file
in which the morphology is stored.
"""
tags = config.attr(type=types.dict(type=types.or_(str, types.list(str))))
"""
Dictionary mapping SWC tags to sets of morphology labels.
"""

def store_content(self, content, *args, encoding=None, meta=None):
if meta is None:
Expand All @@ -489,9 +497,9 @@ def load_object(self) -> "Morphology":
content, meta = stored.load()
try:
morpho_in = Morphology.from_buffer(content, meta=meta)
except Exception as e:
except Exception as _:
with self.file.provide_locally() as (path, encoding):
morpho_in = Morphology.from_file(path, meta=meta)
morpho_in = Morphology.from_file(path, tags=self.tags, meta=meta)
morpho = self.pipe(morpho_in)
meta["hash"] = self._hash(content)
meta["_stale"] = False
Expand All @@ -504,9 +512,23 @@ def load_object(self) -> "Morphology":
return self.scaffold.morphologies.load(self.get_morphology_name())

def get_morphology_name(self):
"""
Returns morphology name provided by the user or extract it from its file name.
:returns: Morphology name
:rtype: str
"""
return self.name if self.name is not None else _pl.Path(self.file.uri).stem

def store_object(self, morpho, hash_):
"""
Save a morphology into the circuit file under the name of this instance morphology.
:param hash_: Hash key to store as metadata with the morphology
:type hash_: str
:param morpho: Morphology to store
:type morpho: bsb.morphologies.Morphology
"""
self.scaffold.morphologies.save(
self.get_morphology_name(), morpho, meta={"hash": hash_}
)
Expand All @@ -520,6 +542,12 @@ def _hash(self, content):
return md5.hexdigest()

def queue(self, pool):
"""
Add the loading of the current morphology to a job queue.
:param pool: Queue of jobs.
:type pool:bsb.services.pool.JobPool
"""
pool.queue(
lambda scaffold, i=self._config_index: scaffold.configuration.morphologies[
i
Expand Down
26 changes: 24 additions & 2 deletions tests/test_morphologies.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import unittest, os, sys, numpy as np, h5py
import json
import itertools
Expand Down Expand Up @@ -40,13 +41,34 @@ def test_swc_branch_filling(self):

def test_known(self):
# TODO: Check the morphos visually with glover
m = Morphology.from_swc(get_morphology_path("PurkinjeCell.swc"))
m = Morphology.from_file(get_morphology_path("PurkinjeCell.swc"))
self.assertEqual(3834, len(m), "Amount of point on purkinje changed")
self.assertEqual(459, len(m.branches), "Amount of branches on purkinje changed")
self.assertEqual(
42.45157433053635,
np.mean(m.points),
"value of the universe, life and everything changed.",
"value of the universe, life and everything.",
)
for labelset in m.labelsets.values():
for label in labelset:
self.assertTrue(re.match(r"((tag_)?[0-9]+)|(soma)", label) is not None)
tags = {
1: "soma",
16: ["axon", "AIS"],
17: ["axon", "AIS", "K"],
18: ["axon", "axonmyelin"],
19: ["axon", "nodes"],
20: ["dendrites", "basal_dendrites"],
21: ["dendrites", "pf_targets"],
22: ["dendrites", "aa_targets"],
}
m = Morphology.from_file(get_morphology_path("PurkinjeCell.swc"), tags=tags)
all_sets = set()
for value in m.labelsets.values():
all_sets.update(value)
self.assertEqual(
set(np.concatenate([[v] if type(v) == str else v for v in tags.values()])),
all_sets,
)
m = Morphology.from_file(get_morphology_path("GolgiCell.asc"))
self.assertEqual(5105, len(m), "Amount of point on purkinje changed")
Expand Down

0 comments on commit e8c1cac

Please sign in to comment.