Skip to content

Commit

Permalink
Access population config from the populations (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
joni-herttuainen committed Feb 8, 2023
1 parent 12092bd commit 8fc7888
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 22 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

Version v1.0.1
--------------

New Features
~~~~~~~~~~~~
- Access the population configs for node/edge populations via population_config property

Version v1.0.0
--------------

Expand Down
15 changes: 15 additions & 0 deletions bluepysnap/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from bluepysnap.config import CircuitConfig
from bluepysnap.edges import Edges
from bluepysnap.exceptions import BluepySnapError
from bluepysnap.node_sets import NodeSets
from bluepysnap.nodes import Nodes

Expand Down Expand Up @@ -49,6 +50,20 @@ def config(self):
"""Network config dictionary."""
return self._config.to_dict()

def get_node_population_config(self, name):
"""Get node population configuration."""
try:
return self._config.node_populations[name]
except KeyError as e:
raise BluepySnapError(f"Population config not found for node population: {name}") from e

def get_edge_population_config(self, name):
"""Get edge population configuration."""
try:
return self._config.edge_populations[name]
except KeyError as e:
raise BluepySnapError(f"Population config not found for edge population: {name}") from e

@cached_property
def node_sets(self):
"""Returns the NodeSets object bound to the circuit."""
Expand Down
45 changes: 45 additions & 0 deletions bluepysnap/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

"""SONATA network config parsing."""

import copy
import json
from collections.abc import Iterable, Mapping
from pathlib import Path
Expand Down Expand Up @@ -163,11 +164,55 @@ def to_dict(self):
class CircuitConfig(Config):
"""Handle CircuitConfig."""

def __init__(self, *args):
"""Initializes circuit config."""
super().__init__(*args)
self._populations = self._resolve_population_configs(self.to_dict())

@classmethod
def from_config(cls, config_path):
"""Instantiate the config class from circuit configuration."""
return cls(config_path, libsonata.CircuitConfig)

@property
def node_populations(self):
"""Access node population configs."""
return self._populations["nodes"]

@property
def edge_populations(self):
"""Access edge population configs."""
return self._populations["edges"]

@staticmethod
def _resolve_population_configs(config):
"""Resolves population configs for the node and edge populations."""
networks = config.get("networks") or {}
components = config.get("components") or {}

def resolve_network_populations(element_type):
populations = {}
type_file_key = f"{element_type}_file"
element_dicts = networks.get(element_type) or []

for elem_dict in element_dicts:
elem_pops = elem_dict.get("populations") or {}

for pop, cfg in elem_pops.items():
populations[pop] = copy.deepcopy(components)
populations[pop].update(cfg or {})

# add h5 filepath for simpler access
if type_file_key in elem_dict:
populations[pop][type_file_key] = elem_dict[type_file_key]

return populations

return {
"nodes": resolve_network_populations("nodes"),
"edges": resolve_network_populations("edges"),
}


class SimulationConfig(Config):
"""Handle SimulationConfig."""
Expand Down
18 changes: 13 additions & 5 deletions bluepysnap/edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ def _dynamics_params_names(self):
def _topology_property_names(self):
return {Edge.SOURCE_NODE_ID, Edge.TARGET_NODE_ID}

@property
def population_config(self):
"""Access the configuration for the population.
This configuration is extended with
* 'components' of the circuit config
* 'edges_file': the path the h5 file containing the population.
"""
return self._circuit.get_edge_population_config(self.name)

@property
def property_names(self):
"""Set of available edge properties.
Expand Down Expand Up @@ -450,6 +460,7 @@ def pair_edges(self, source_node_id, target_node_id, properties=None):

def _iter_connections(self, source_node_ids, target_node_ids, unique_node_ids, shuffle):
"""Iterate through `source_node_ids` -> `target_node_ids` connections."""

# pylint: disable=too-many-branches,too-many-locals
def _optimal_direction():
"""Choose between source and target node IDs for iterating."""
Expand Down Expand Up @@ -563,13 +574,10 @@ def iter_connections(
omit_edge_count = lambda x: x[:2]
return map(omit_edge_count, it)

@cached_property
@property
def h5_filepath(self):
"""Get the H5 edges file associated with population."""
for edge_conf in self._circuit.config["networks"]["edges"]:
if self.name in edge_conf["populations"]:
return edge_conf["edges_file"]
raise BluepySnapError(f"h5_filepath not found for population '{self.name}'")
return self.population_config["edges_file"]


class Edges(
Expand Down
17 changes: 12 additions & 5 deletions bluepysnap/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ def target_in_edges(self):
edge.name for edge in self._circuit.edges.values() if self.name == edge.target.name
)

@property
def population_config(self):
"""Access the configuration for the population.
This configuration is extended with
* 'components' of the circuit config
* 'nodes_file': the path the h5 file containing the population.
"""
return self._circuit.get_node_population_config(self.name)

@property
def property_names(self):
"""Set of available node properties.
Expand Down Expand Up @@ -568,13 +578,10 @@ def models(self):

return NeuronModelsHelper(self._properties, self)

@cached_property
@property
def h5_filepath(self):
"""Get the H5 nodes file associated with population."""
for node_conf in self._circuit.config["networks"]["nodes"]:
if self.name in node_conf["populations"]:
return node_conf["nodes_file"]
raise BluepySnapError(f"h5_filepath not found for population '{self.name}'")
return self.population_config["nodes_file"]


class Nodes(
Expand Down
11 changes: 11 additions & 0 deletions tests/test_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import bluepysnap.circuit as test_module
from bluepysnap.edges import EdgePopulation, Edges
from bluepysnap.exceptions import BluepySnapError
from bluepysnap.nodes import NodePopulation, Nodes

from utils import TEST_DATA_DIR, copy_test_data, edit_config, skip_if_libsonata_0_1_16
Expand All @@ -28,6 +29,16 @@ def test_all():
json.loads((TEST_DATA_DIR / "node_sets.json").read_text())
)

fake_pop = "fake"
with pytest.raises(
BluepySnapError, match=f"Population config not found for node population: {fake_pop}"
):
circuit.get_node_population_config(fake_pop)
with pytest.raises(
BluepySnapError, match=f"Population config not found for edge population: {fake_pop}"
):
circuit.get_edge_population_config(fake_pop)


@skip_if_libsonata_0_1_16
def test_duplicate_node_populations():
Expand Down
45 changes: 45 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,48 @@ def test_simulation_config():
)
assert actual["conditions"]["celsius"] == 34.0
assert actual["conditions"]["v_init"] == -80


def test__resolve_population_configs():
node = {
"nodes_file": "nodes_path",
"populations": {"node_pop": {"changed_node": "changed", "added_this": "node_test"}},
}

edge = {
"edges_file": "edges_path",
"populations": {"edge_pop": {"changed_edge": "changed", "added_this": "edge_test"}},
}

config = {
"components": {
"unchanged": True,
"changed_node": "unchanged",
"changed_edge": "unchanged",
},
"networks": {
"nodes": [node],
"edges": [edge],
},
}
expected_node_pop = {
"unchanged": True,
"changed_node": "changed",
"changed_edge": "unchanged",
"added_this": "node_test",
"nodes_file": "nodes_path",
}
expected_edge_pop = {
"unchanged": True,
"changed_node": "unchanged",
"changed_edge": "changed",
"added_this": "edge_test",
"edges_file": "edges_path",
}
expected = {
"nodes": {"node_pop": expected_node_pop},
"edges": {"edge_pop": expected_edge_pop},
}
res = test_module.CircuitConfig._resolve_population_configs(config)

assert res == expected
6 changes: 0 additions & 6 deletions tests/test_edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,6 @@ def test_efferent_edges(self):
)

def test_pair_edges(self):

# no connection between 0 and 2
assert self.test_obj.pair_edges(0, 2, None) == CircuitEdgeIds.from_arrays([], [])
actual = self.test_obj.pair_edges(0, 2, [Synapse.AXONAL_DELAY])
Expand Down Expand Up @@ -1394,8 +1393,3 @@ def test_iter_connection_unique(self):

def test_h5_filepath_from_config(self):
assert self.test_obj.h5_filepath == str(TEST_DATA_DIR / "edges.h5")

def test_no_h5_filepath(self):
with pytest.raises(BluepySnapError, match="h5_filepath not found for population"):
self.test_obj.name = "fake"
self.test_obj.h5_filepath
5 changes: 0 additions & 5 deletions tests/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,8 +996,3 @@ def test_models(self):

def test_h5_filepath_from_config(self):
assert self.test_obj.h5_filepath == str(TEST_DATA_DIR / "nodes.h5")

def test_no_h5_filepath(self):
with pytest.raises(BluepySnapError, match="h5_filepath not found for population"):
self.test_obj.name = "fake"
self.test_obj.h5_filepath
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ max-line-length = 100
[pydocstyle]
# ignore the following
# - D413: no blank line after last section
add-ignore = D413
# - D202: no blank line after docstring (handled by black, causes an issue if first line is '#pylint:disable')
add-ignore = D413,D202
convention = google

[gh-actions]
Expand Down

0 comments on commit 8fc7888

Please sign in to comment.