Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support morphology containers #274

Merged
merged 3 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Improvements
- Update simulation validation to conform to the SONATA spec

- ``synapse_replay.source`` and ``.dat`` spike input files are no longer supported
- Support morphology containers
mgeplf marked this conversation as resolved.
Show resolved Hide resolved


Version v3.0.1
Expand Down
41 changes: 33 additions & 8 deletions bluepysnap/morph.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
from pathlib import Path

import morph_tool.transform as transformations
import morphio
import numpy as np
from morphio.mut import Morphology

from bluepysnap.exceptions import BluepySnapError
from bluepysnap.sonata_constants import Node
Expand Down Expand Up @@ -51,8 +51,8 @@ def __init__(self, morph_dir, population, alternate_morphologies=None):
self._alternate_morphologies = alternate_morphologies or {}
self._population = population

def get_morphology_dir(self, extension):
"""Return morphology directory based on a given extension."""
def _get_morphology_base(self, extension):
"""Get morphology base path; this will be a directory unless it's a morphology container."""
if extension == "swc":
if not self._morph_dir:
raise BluepySnapError("'morphologies_dir' is not defined in config")
Expand All @@ -68,16 +68,33 @@ def get_morphology_dir(self, extension):

return morph_dir

def get_morphology_dir(self, extension="swc"):
"""Return morphology directory based on a given extension."""
morph_dir = self._get_morphology_base(extension)

if extension == "h5" and Path(morph_dir).is_file():
raise BluepySnapError(
f"'{morph_dir}' is a morphology container, so a directory does not exist"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the circuit is using a h5 container instead of a directory, I wonder if we want to raise an error anytime that get_morphology_dir or get_filepath are called, regardless of the extension specified as a parameter?
Probably in the medium/long term those functions could be deprecated, but need to check in what use cases they are needed at the moment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean if it's the only alternaive-morphology?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but after checking better it's already good as is, because the container file is retrieved only with extension=h5


return morph_dir

def get_name(self, node_id):
"""Get the morphology name for a `node_id`."""
if not is_node_id(node_id):
raise BluepySnapError("node_id must be a int or a CircuitNodeId")

name = self._population.get(node_id, Node.MORPHOLOGY)
return name

def get_filepath(self, node_id, extension="swc"):
"""Return path to SWC morphology file corresponding to `node_id`.

Args:
node_id (int/CircuitNodeId): could be a int or CircuitNodeId.
extension (str): expected filetype extension of the morph file.
"""
if not is_node_id(node_id):
raise BluepySnapError("node_id must be a int or a CircuitNodeId")
name = self._population.get(node_id, Node.MORPHOLOGY)
name = self.get_name(node_id)

return Path(self.get_morphology_dir(extension), f"{name}.{extension}")

Expand All @@ -90,11 +107,19 @@ def get(self, node_id, transform=False, extension="swc"):
according to `node_id` position in the circuit.
extension (str): expected filetype extension of the morph file.
"""
filepath = self.get_filepath(node_id, extension=extension)
result = Morphology(filepath)
collection = morphio.Collection(
self._get_morphology_base(extension),
[
f".{extension}",
],
)
name = self.get_name(node_id)
result = collection.load(name, mutable=True)

if transform:
T = np.eye(4)
T[:3, :3] = self._population.orientations(node_id) # rotations
T[:3, 3] = self._population.positions(node_id).values # translations
transformations.transform(result, T)

return result.as_immutable()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def __init__(self, *args, **kwargs):
"importlib_resources>=5.0.0",
"jsonschema>=4.0.0,<5.0.0",
"libsonata>=0.1.24,<1.0.0",
"morphio>=3.0.0,<4.0.0",
"morphio>=3.3.5,<4.0.0",
"morph-tool>=2.4.3,<3.0.0",
"numpy>=1.8",
"pandas>=1.0.0",
Expand Down
Binary file added tests/data/morphologies/container-morphs.h5
Binary file not shown.
Binary file modified tests/data/morphologies/morph-B.h5
Binary file not shown.
19 changes: 19 additions & 0 deletions tests/test_morph.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,25 @@ def test_get_morphology(self):
with pytest.raises(BluepySnapError, match="node_id must be a int or a CircuitNodeId"):
self.test_obj.get([0, 1])

def test_get_alternate_morphology_collection(self):
morph_path = TEST_DATA_DIR / "morphologies/container-morphs.h5"
alternate_morphs = {"h5v1": str(morph_path)}

test_obj = test_module.MorphHelper(
None, self.nodes, alternate_morphologies=alternate_morphs
)

node_id = 0

with pytest.raises(BluepySnapError):
test_obj.get_morphology_dir(extension="h5")

with pytest.raises(BluepySnapError):
test_obj.get_filepath(node_id, extension="h5")
Comment on lines +139 to +143
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but the specific reason of BluepySnapError could be checked with match=...


morph_A = test_obj.get(node_id, extension="h5")
assert len(morph_A.points) == 13

def test_get_alternate_morphology(self):
alternate_morphs = {"h5v1": str(self.morph_path)}
test_obj = test_module.MorphHelper(
Expand Down
Loading