Skip to content

Commit

Permalink
Introduce NeuronModelsHelper, an analog of MorphHelper (#123)
Browse files Browse the repository at this point in the history
It is used to access neuron models
  • Loading branch information
asanin-epfl committed Feb 10, 2021
1 parent 601aacc commit fd42846
Show file tree
Hide file tree
Showing 14 changed files with 204 additions and 6 deletions.
8 changes: 4 additions & 4 deletions bluepysnap/morph.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

"""Morphology access."""

import os
from pathlib import Path

import numpy as np
import neurom as nm
Expand Down Expand Up @@ -61,10 +61,10 @@ def get_filepath(self, node_id):
Args:
node_id (int/CircuitNodeId): could be a int or CircuitNodeId.
"""
if not isinstance(node_id, (int, CircuitNodeId)):
raise BluepySnapError("node_id must be a int or a CircuitNodeId")
name = self._population.get(node_id, Node.MORPHOLOGY)
if isinstance(node_id, (int, CircuitNodeId)):
return os.path.join(self._morph_dir, "%s.swc" % name)
raise BluepySnapError("node_id must be a int or a CircuitNodeId")
return Path(self._morph_dir, f"{name}.swc")

def get(self, node_id, transform=False):
"""Return NeuroM morphology object corresponding to `node_id`.
Expand Down
76 changes: 76 additions & 0 deletions bluepysnap/neuron_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright (c) 2019, EPFL/Blue Brain Project

# This file is part of BlueBrain SNAP library <https://github.com/BlueBrain/snap>

# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License version 3.0 as published
# by the Free Software Foundation.

# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.

# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""Neuron models access."""

from pathlib import Path

from bluepysnap.circuit_ids import CircuitNodeId
from bluepysnap.exceptions import BluepySnapError
from bluepysnap.sonata_constants import Node


class NeuronModelsHelper:
"""Collection of neuron models related methods."""

def __init__(self, config_components, population):
"""Constructor.
Args:
config_components (dict): 'components' part of circuit's config
population (NodePopulation): NodePopulation object used to query the nodes.
Returns:
NeuronModelsHelper: A NeuronModelsHelper object.
"""
# all nodes from a population must have the same model type
if population.get(0, Node.MODEL_TYPE) not in {"biophysical", "point_neuron"}:
raise BluepySnapError(
"Neuron models can be only in biophysical or point node population.")

self._config_components = config_components
self._population = population

def get_filepath(self, node_id):
"""Return path to model file corresponding to `node_id`.
Args:
node_id (int|CircuitNodeId): node id
Returns:
Path: path to the model file of neuron
"""
if not isinstance(node_id, (int, CircuitNodeId)):
raise BluepySnapError("node_id must be a int or a CircuitNodeId")
node = self._population.get(node_id, [Node.MODEL_TYPE, Node.MODEL_TEMPLATE])
if node[Node.MODEL_TYPE] == "biophysical":
models_dir = self._config_components.get("biophysical_neuron_models_dir")
if models_dir is None:
raise BluepySnapError(
"Missing 'biophysical_neuron_models_dir' in Sonata config")
else:
models_dir = self._config_components.get("point_neuron_models_dir")
if models_dir is None:
raise BluepySnapError("Missing 'point_neuron_models_dir' in Sonata config")

template = node[Node.MODEL_TEMPLATE]
assert ':' in template, "Format of 'model_template' must be <schema>:<resource>."
schema, resource = template.split(':', 1)
resource = Path(resource).with_suffix(f'.{schema}')
if resource.is_absolute():
return resource
return Path(models_dir, resource)
6 changes: 6 additions & 0 deletions bluepysnap/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,3 +734,9 @@ def morph(self):
self._node_storage.circuit.config['components']['morphologies_dir'],
self
)

@cached_property
def models(self):
"""Access to node neuron models."""
from bluepysnap.neuron_models import NeuronModelsHelper
return NeuronModelsHelper(self._node_storage.circuit.config['components'], self)
Empty file.
Empty file.
Empty file.
Binary file modified tests/data/nodes.h5
Binary file not shown.
Binary file added tests/data/nodes_points.h5
Binary file not shown.
1 change: 1 addition & 0 deletions tests/data/point_neuron_models/empty_bio.nml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions tests/test_edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ def create_edge_population(filepath, pop_name):
'edge_types_file': None,
}
circuit = Mock()
circuit.config = {}
create_node_population(str(TEST_DATA_DIR / 'nodes.h5'), "default", circuit=circuit,
node_sets=NodeSets(str(TEST_DATA_DIR / 'node_sets.json')))
storage = test_module.EdgeStorage(config, circuit)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_morph.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def test_get_filepath(self):
node_id = 0
assert self.nodes.get(node_id, properties="morphology") == "morph-A"
actual = self.test_obj.get_filepath(node_id)
expected = str(self.morph_path / 'morph-A.swc')
expected = self.morph_path / 'morph-A.swc'
assert actual == expected
node_id = CircuitNodeId("default", 0)
assert self.nodes.get(node_id, properties="morphology") == "morph-A"
Expand Down
105 changes: 105 additions & 0 deletions tests/test_neuron_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from pathlib import Path
import h5py
import bluepysnap.neuron_models as test_module
from bluepysnap.circuit import Circuit, Config
from bluepysnap.circuit_ids import CircuitNodeId
from bluepysnap.sonata_constants import Node
from bluepysnap.exceptions import BluepySnapError

import pytest

from utils import TEST_DATA_DIR, copy_circuit, copy_config, edit_config, create_node_population


def test_invalid_model_type():
"""test that model type, other than 'biophysical' or 'point_neuron', throws an error"""
with copy_circuit() as (circuit_copy_path, config_copy_path):
config = Config(config_copy_path).resolve()
nodes_file = config["networks"]["nodes"][0]["nodes_file"]
with h5py.File(nodes_file, 'r+') as h5f:
grp_name = 'nodes/default/0/model_type'
data = h5f[grp_name][:]
del h5f[grp_name]
h5f.create_dataset(grp_name,
data=["virtual"] * data.shape[0],
dtype=h5py.string_dtype())
nodes = create_node_population(nodes_file, "default")
with pytest.raises(BluepySnapError) as e:
test_module.NeuronModelsHelper({}, nodes)
assert 'biophysical or point node population' in e.value.args[0]


def test_get_invalid_node_id():
nodes = create_node_population(str(TEST_DATA_DIR / 'nodes.h5'), "default")
config = Config(TEST_DATA_DIR / 'circuit_config.json').resolve()
test_obj = test_module.NeuronModelsHelper(config['components'], nodes)

with pytest.raises(BluepySnapError) as e:
test_obj.get_filepath('1')
assert "node_id must be a int or a CircuitNodeId" in e.value.args[0]


def test_get_filepath_biophysical():
nodes = create_node_population(str(TEST_DATA_DIR / 'nodes.h5'), "default")
config = Config(TEST_DATA_DIR / 'circuit_config.json').resolve()
test_obj = test_module.NeuronModelsHelper(config['components'], nodes)

node_id = 0
assert nodes.get(node_id, properties=Node.MODEL_TEMPLATE) == "hoc:small_bio-A"
actual = test_obj.get_filepath(node_id)
expected = Path(config['components']['biophysical_neuron_models_dir'], 'small_bio-A.hoc')
assert actual == expected

node_id = CircuitNodeId('default', 0)
assert nodes.get(node_id, properties=Node.MODEL_TEMPLATE) == "hoc:small_bio-A"
actual = test_obj.get_filepath(node_id)
assert actual == expected

node_id = CircuitNodeId('default', 2)
assert nodes.get(node_id, properties=Node.MODEL_TEMPLATE) == "hoc:small_bio-C"
actual = test_obj.get_filepath(node_id)
expected = Path(config['components']['biophysical_neuron_models_dir'], 'small_bio-C.hoc')
assert actual == expected


def test_no_biophysical_dir():
nodes = create_node_population(str(TEST_DATA_DIR / 'nodes.h5'), "default")
config = Config(TEST_DATA_DIR / 'circuit_config.json').resolve()
del config['components']['biophysical_neuron_models_dir']
test_obj = test_module.NeuronModelsHelper(config['components'], nodes)

with pytest.raises(BluepySnapError) as e:
test_obj.get_filepath(0)
assert "Missing 'biophysical_neuron_models_dir'" in e.value.args[0]


def test_get_filepath_point():
with copy_config() as config_copy_path:
with edit_config(config_copy_path) as config:
config['components']['point_neuron_models_dir'] = "$COMPONENT_DIR/point_neuron_models"

circuit = Circuit(config_copy_path)
nodes = create_node_population(str(TEST_DATA_DIR / 'nodes_points.h5'), "default", circuit)
test_obj = test_module.NeuronModelsHelper(circuit.config['components'], nodes)

node_id = 0
assert nodes.get(node_id, properties=Node.MODEL_TEMPLATE) == "nml:empty_bio"
actual = test_obj.get_filepath(node_id)
expected = Path(circuit.config['components']['point_neuron_models_dir'], 'empty_bio.nml')
assert actual == expected

node_id = 1
assert nodes.get(node_id, properties=Node.MODEL_TEMPLATE) == "nml:/abs/path/empty_bio"
actual = test_obj.get_filepath(node_id)
expected = Path('/abs/path/empty_bio.nml')
assert actual == expected


def test_no_point_dir():
nodes = create_node_population(str(TEST_DATA_DIR / 'nodes_points.h5'), "default")
config = Config(TEST_DATA_DIR / 'circuit_config.json').resolve()
test_obj = test_module.NeuronModelsHelper(config['components'], nodes)

with pytest.raises(BluepySnapError) as e:
test_obj.get_filepath(0)
assert "Missing 'point_neuron_models_dir'" in e.value.args[0]
8 changes: 8 additions & 0 deletions tests/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ def _mock_edge(name, source, target):
return edges

circuit = Mock()
circuit.config = {}
circuit.edges = {"edge1": _mock_edge('edge1', "default", "nodeother"),
"edge2": _mock_edge('edge2', "nodeother", "default"),
"edge3": _mock_edge('edge3', "default", "nodeother")}
Expand Down Expand Up @@ -869,3 +870,10 @@ def test_morph(self):
}
}
assert isinstance(self.test_obj.morph, MorphHelper)

def test_models(self):
from bluepysnap.neuron_models import NeuronModelsHelper
self.test_obj._node_storage.circuit.config = {
'components': {}
}
assert isinstance(self.test_obj.models, NeuronModelsHelper)
3 changes: 2 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ def create_node_population(filepath, pop_name, circuit=None, node_sets=None):
}
if circuit is None:
circuit = mock.Mock()
circuit.config = {}
circuit.config.update({"networks": {"nodes": [config]}})
if node_sets is not None:
circuit.node_sets = node_sets
node_pop = NodeStorage(config, circuit).population(pop_name)
circuit.config = {"networks": {"nodes": [config]}}
circuit.nodes = Nodes(circuit)
return node_pop

0 comments on commit fd42846

Please sign in to comment.