Skip to content

Commit

Permalink
Changed Simulation and Reports to take more advantage of libsonata (#165
Browse files Browse the repository at this point in the history
)

* Have simulation.time_start always return 0
  • Loading branch information
joni-herttuainen committed Sep 2, 2022
1 parent 62d2e80 commit 9bb210f
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 53 deletions.
14 changes: 8 additions & 6 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ Improvements
- Add black & isort to handle formatting
- Use libsonata to provide more of the functionality
- parsing config files
- accessing data
- accessing data in Circuit and Simulation
- Circuit validation changed to be more config-driven
- it now only validates objects defined in the circuit configuration file

Removed
~~~~~~~
- non-BBP Sonata circuit validation
- NodeStorage & EdgeStorage classes
- point_neuron support
Breaking Changes
~~~~~~~~~~~~~~~~
- Simulation
- `run` and `condition` properties return libsonata classes instead of dictionaries
- non-BBP Sonata circuit validation was removed
- The NodeStorage & EdgeStorage classes were removed
- point_neuron is no longer supported


Version v0.13.1
Expand Down
25 changes: 15 additions & 10 deletions bluepysnap/frame_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

L = logging.getLogger(__name__)

FORMAT_TO_EXT = {"ASCII": ".txt", "HDF5": ".h5", "BIN": ".bbp"}
REPORT_EXTENSION = ".h5"


def _collect_population_reports(frame_report, cls):
Expand All @@ -39,9 +39,9 @@ def _collect_population_reports(frame_report, cls):


def _get_reader(reader_report, cls):
path = reader_report.simulation.config["output"]["output_dir"]
ext = FORMAT_TO_EXT[reader_report.config.get("format", "HDF5")]
file_name = reader_report.config.get("file_name", reader_report.name) + ext
path = reader_report.simulation.output.output_dir
# TODO: change to use reader_report.to_libsonata.file_name when fixed in libsonata
file_name = reader_report.config.get("file_name", reader_report.name) + REPORT_EXTENSION
path = str(Path(path, file_name))
return cls(path)

Expand Down Expand Up @@ -204,26 +204,31 @@ def _frame_reader(self):
"""Access to the compartment report reader."""
return _get_reader(self, ElementReportReader)

@property
def to_libsonata(self):
"""Access libsonata instance of the report."""
return self.simulation.to_libsonata.report(self.name)

@property
def config(self):
"""Access the report config."""
return self._simulation.config["reports"][self.name]
return self.simulation.config["reports"][self.name]

@property
def time_start(self):
"""Returns the starting time of the report."""
return self.config.get("start_time", self._simulation.time_start)
return self.to_libsonata.start_time

@property
def time_stop(self):
"""Returns the stopping time of the report."""
return self.config.get("end_time", self._simulation.time_stop)
return self.to_libsonata.end_time

@property
def dt(self):
"""Returns the frequency of reporting in milliseconds."""
dt = self.config.get("dt", self._simulation.dt)
if dt != self._simulation.dt:
dt = self.to_libsonata.dt
if dt != self.simulation.dt:
L.warning("dt from the report differs from the global simulation dt.")
return dt

Expand All @@ -246,7 +251,7 @@ def data_units(self):
@property
def node_set(self):
"""Returns the node set for the report."""
return self.simulation.node_sets[self.config["cells"]]
return self.simulation.node_sets[self.to_libsonata.cells]

@property
def simulation(self):
Expand Down
41 changes: 27 additions & 14 deletions bluepysnap/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Simulation access."""

from pathlib import Path

from cached_property import cached_property

from bluepysnap.config import SimulationConfig
Expand All @@ -26,8 +28,9 @@
def _collect_frame_reports(sim):
"""Collect the different frame reports."""
res = {}
for name, report in sim.config["reports"].items():
report_type = report.get("sections", "soma")
for name in sim.to_libsonata.list_report_names:
report = sim.to_libsonata.report(name)
report_type = report.sections.name
if report_type == "soma":
from bluepysnap.frame_report import SomaReport

Expand Down Expand Up @@ -57,6 +60,11 @@ def __init__(self, config):
"""
self._config = SimulationConfig.from_config(config)

@property
def to_libsonata(self):
"""Libsonata instance of the config."""
return self._config.to_libsonata

@property
def config(self):
"""Simulation config dictionary."""
Expand All @@ -67,29 +75,34 @@ def circuit(self):
"""Access to the circuit used for the simulation."""
from bluepysnap.circuit import Circuit

if "network" not in self.config:
raise BluepySnapError("No 'network' set in the simulation/global config file.")
return Circuit(self.config["network"])
if not Path(self.to_libsonata.network).is_file():
raise BluepySnapError(f"'network' file not found: {self.to_libsonata.network}")
return Circuit(self.to_libsonata.network)

@property
def output(self):
"""Access the output section."""
return self.to_libsonata.output

@property
def run(self):
"""Access to the complete run dictionary for this simulation."""
return self.config["run"]
return self.to_libsonata.run

@property
def time_start(self):
"""Returns the starting time of the simulation."""
return self.run.get("tstart", 0)
return 0

@property
def time_stop(self):
"""Returns the stopping time of the simulation."""
return self.run["tstop"]
return self.run.tstop

@property
def dt(self):
"""Returns the frequency of reporting in milliseconds."""
return self.run.get("dt", None)
return self.run.dt

@property
def time_units(self):
Expand All @@ -100,19 +113,19 @@ def time_units(self):
@property
def conditions(self):
"""Access to the conditions dictionary for this simulation."""
return self.config.get("conditions", {})
return getattr(self.to_libsonata, "conditions", None)

@property
def simulator(self):
"""Returns the targeted simulator."""
return self.config.get("target_simulator")
target_simulator = getattr(self.to_libsonata, "target_simulator", None)
return target_simulator.name if target_simulator is not None else None

@cached_property
def node_sets(self):
"""Returns the NodeSets object bound to the simulation."""
if "node_sets_file" in self.config:
return NodeSets(self.config["node_sets_file"])
return {}
node_sets_file = self.to_libsonata.node_sets_file
return NodeSets(node_sets_file) if node_sets_file else {}

@cached_property
def spikes(self):
Expand Down
12 changes: 6 additions & 6 deletions bluepysnap/spike_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
from bluepysnap.utils import IDS_DTYPE


def _get_reader(spike_report):
path = str(Path(spike_report.config["output_dir"]) / spike_report.config["spikes_file"])
def _get_reader(config):
path = str(Path(config.output_dir) / config.spikes_file)
return SpikeReader(path)


Expand All @@ -55,7 +55,7 @@ def __init__(self, spike_report, population_name):
PopulationSpikeReport: A PopulationSpikeReport object.
"""
self.spike_report = spike_report
self._spike_population = _get_reader(self.spike_report)[population_name]
self._spike_population = _get_reader(self.spike_report.config)[population_name]
self._population_name = population_name

@property
Expand Down Expand Up @@ -195,7 +195,7 @@ def __init__(self, simulation):
@property
def config(self):
"""Access to the spike 'output' config part."""
return self._simulation.config["output"]
return self._simulation.output

@property
def time_start(self):
Expand Down Expand Up @@ -225,15 +225,15 @@ def simulation(self):
@contextmanager
def log(self):
"""Context manager for the spike log file."""
path = Path(self.config["output_dir"]) / self.config["log_file"]
path = Path(self.config.output_dir) / self.config.log_file
if not path.exists():
raise BluepySnapError("Cannot find the log file for the spike report.")
yield path.open("r", encoding="utf-8")

@cached_property
def _spike_reader(self):
"""Access to the libsonata SpikeReader."""
return _get_reader(self)
return _get_reader(self.config)

@cached_property
def population_names(self):
Expand Down
1 change: 0 additions & 1 deletion tests/data/simulation_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"tstop": 1000.0,
"dt": 0.01,
"spike_threshold": -15,
"nsteps_block": 10000,
"random_seed": 42
},
"target_simulator":"CORENEURON",
Expand Down
24 changes: 14 additions & 10 deletions tests/test_simulation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os

import libsonata
import pytest
Expand All @@ -23,20 +24,20 @@ def test_all():
assert set(simulation.circuit.nodes) == {"default", "default2"}
assert set(simulation.circuit.edges) == {"default", "default2"}

assert simulation.run == {
"tstop": 1000.0,
"dt": 0.01,
"spike_threshold": -15,
"nsteps_block": 10000,
"random_seed": 42,
}
assert isinstance(simulation.run, libsonata._libsonata.Run)
assert simulation.run.tstop == 1000.0
assert simulation.run.dt == 0.01
assert simulation.run.spike_threshold == -15
assert simulation.run.random_seed == 42
assert simulation.time_start == 0.0
assert simulation.time_stop == 1000.0
assert simulation.dt == 0.01
assert simulation.time_units == "ms"

assert simulation.simulator == "CORENEURON"
assert simulation.conditions == {"celsius": 34.0, "v_init": -80, "other": "something"}
assert isinstance(simulation.conditions, libsonata._libsonata.Conditions)
assert simulation.conditions.celsius == 34.0
assert simulation.conditions.v_init == -80

assert simulation.node_sets.resolved == {"Layer23": {"layer": [2, 3]}}
assert isinstance(simulation.spikes, SpikeReport)
Expand Down Expand Up @@ -75,21 +76,24 @@ def test_nonimplemented_report():
test_module.Simulation(config_path).reports


def test_no_network_config():
def test_network_file_not_found():
with copy_test_data(config="simulation_config.json") as (_, config_path):
with edit_config(config_path) as config:
config.pop("network")

os.remove(config_path.parent / "circuit_config.json")
simulation = test_module.Simulation(config_path)

with pytest.raises(BluepySnapError, match="No 'network' set"):
with pytest.raises(BluepySnapError, match="'network' file not found"):
simulation.circuit


def test_no_node_set():
with copy_test_data(config="simulation_config.json") as (_, config_path):
with edit_config(config_path) as config:
config.pop("node_sets_file")
# remove circuit config to prevent libsonata from fetching the path from there
os.remove(config_path.parent / "circuit_config.json")

simulation = test_module.Simulation(config_path)
assert simulation.node_sets == {}
12 changes: 6 additions & 6 deletions tests/test_spike_report.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from unittest.mock import patch

import libsonata
import numpy as np
import numpy.testing as npt
import pandas as pd
Expand Down Expand Up @@ -33,12 +34,11 @@ def setup(self):
self.test_obj = test_module.SpikeReport(self.simulation)

def test_config(self):
assert self.test_obj.config == {
"output_dir": str(TEST_DATA_DIR / "reporting"),
"log_file": "log_spikes.log",
"spikes_file": "spikes.h5",
"spikes_sort_order": "by_time",
}
assert isinstance(self.test_obj.config, libsonata._libsonata.Output)
assert self.test_obj.config.output_dir == str(TEST_DATA_DIR / "reporting")
assert self.test_obj.config.log_file == "log_spikes.log"
assert self.test_obj.config.spikes_file == "spikes.h5"
assert self.test_obj.config.spikes_sort_order.name == "by_time"

def test_time_start(self):
assert self.test_obj.time_start == 0.0
Expand Down

0 comments on commit 9bb210f

Please sign in to comment.