diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 96fdd2b1..bd6749c3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 Version v3.0.1 diff --git a/bluepysnap/morph.py b/bluepysnap/morph.py index 0f200c90..7d3864a9 100644 --- a/bluepysnap/morph.py +++ b/bluepysnap/morph.py @@ -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 @@ -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") @@ -68,6 +68,25 @@ 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" + ) + + 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`. @@ -75,9 +94,7 @@ def get_filepath(self, node_id, extension="swc"): 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}") @@ -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() diff --git a/setup.py b/setup.py index b29f1c19..e658074d 100644 --- a/setup.py +++ b/setup.py @@ -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", diff --git a/tests/data/morphologies/container-morphs.h5 b/tests/data/morphologies/container-morphs.h5 new file mode 100644 index 00000000..967c33fc Binary files /dev/null and b/tests/data/morphologies/container-morphs.h5 differ diff --git a/tests/data/morphologies/morph-B.h5 b/tests/data/morphologies/morph-B.h5 index e69de29b..6b65c705 100644 Binary files a/tests/data/morphologies/morph-B.h5 and b/tests/data/morphologies/morph-B.h5 differ diff --git a/tests/test_morph.py b/tests/test_morph.py index 5ede964e..6182ef5b 100644 --- a/tests/test_morph.py +++ b/tests/test_morph.py @@ -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") + + 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(