├── tests ├── data │ ├── mechanisms_dir │ │ └── .gitkeep │ ├── morphologies │ │ ├── morph-B.asc │ │ ├── morph-B.swc │ │ ├── morph-C.asc │ │ ├── morph-C.swc │ │ ├── morph-D.swc │ │ ├── morph-E.swc │ │ ├── morph-F.swc │ │ ├── morph-G.swc │ │ ├── morph-A.h5 │ │ ├── morph-B.h5 │ │ ├── container-morphs.h5 │ │ └── morph-A.swc │ ├── reporting │ │ ├── lfp_report.h5 │ │ ├── log_spikes.log │ │ ├── spikes.h5 │ │ ├── soma_named.h5 │ │ ├── soma_report.h5 │ │ ├── compartment_named.h5 │ │ └── create_reports.py │ ├── point_neuron_models │ │ └── empty_bio.nml │ ├── biophysical_neuron_models │ │ ├── small_bio-A.hoc │ │ ├── small_bio-B.hoc │ │ ├── small_bio-C.hoc │ │ └── small_bio.hoc │ ├── node_types.csv │ ├── edges.h5 │ ├── nodes.h5 │ ├── node_sets_extra.json │ ├── input_spikes.h5 │ ├── nodes_points.h5 │ ├── edges_single_pop.h5 │ ├── nodes_no_rotation.h5 │ ├── nodes_quaternions.h5 │ ├── nodes_single_pop.h5 │ ├── edges_complete_graph.h5 │ ├── mock_electrodes_file.h5 │ ├── nodes_no_xz_rotation.h5 │ ├── nodes_with_library_large.h5 │ ├── nodes_with_library_small.h5 │ ├── nodes_quaternions_w_missing.h5 │ ├── node_sets_simple.json │ ├── node_sets_file.json │ ├── node_sets.json │ ├── circuit_config.json │ └── simulation_config.json ├── test_version.py ├── test_settings.py ├── test_circuit_ids_types.py ├── test_edge_misc.py ├── test_sonata_constants.py ├── test_cli.py ├── test_edge_population_stats.py ├── test__doctools.py ├── test_input.py ├── test_node_sets.py ├── utils.py ├── test_utils.py ├── test_neuron_models.py ├── test_circuit.py ├── test_partial_config.py ├── test__plotting.py ├── test_simulation.py └── test_full_config.py ├── doc ├── source │ ├── changelog.rst │ ├── _images │ │ └── BlueBrainSNAP.jpg │ ├── api.rst │ ├── simulations.rst │ ├── utilities.rst │ ├── circuits.rst │ ├── index.rst │ ├── notebooks.rst │ ├── conf.py │ └── notebooks │ │ └── 01_circuits.ipynb ├── Makefile └── make.bat ├── MANIFEST.in ├── bluepysnap ├── edges │ ├── __init__.py │ └── edge_population_stats.py ├── nodes │ ├── __init__.py │ └── nodes.py ├── schemas │ ├── __init__.py │ ├── node │ │ ├── virtual.yaml │ │ ├── astrocyte.yaml │ │ ├── vasculature.yaml │ │ └── biophysical.yaml │ ├── edge │ │ ├── neuromodulatory.yaml │ │ ├── synapse_astrocyte.yaml │ │ ├── endfoot.yaml │ │ ├── chemical_virtual.yaml │ │ ├── glialglial.yaml │ │ ├── electrical.yaml │ │ └── chemical.yaml │ ├── definitions │ │ ├── datatypes.yaml │ │ ├── node.yaml │ │ └── simulation_input.yaml │ ├── circuit.yaml │ └── simulation.yaml ├── __init__.py ├── circuit_ids_types.py ├── settings.py ├── bbp.py ├── cli.py ├── input.py ├── neuron_models.py ├── exceptions.py ├── _doctools.py ├── circuit.py ├── sonata_constants.py ├── node_sets.py ├── morph.py ├── simulation.py ├── utils.py └── network.py ├── AUTHORS.txt ├── .gitignore ├── .readthedocs.yml ├── .github └── workflows │ ├── publish-sdist.yml │ └── run-tox.yml ├── tox.ini ├── pyproject.toml ├── setup.py └── README.rst /tests/data/mechanisms_dir/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/morphologies/morph-B.asc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/morphologies/morph-B.swc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/morphologies/morph-C.asc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/morphologies/morph-C.swc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/morphologies/morph-D.swc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/morphologies/morph-E.swc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/morphologies/morph-F.swc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/morphologies/morph-G.swc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/reporting/lfp_report.h5: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/point_neuron_models/empty_bio.nml: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tests/data/biophysical_neuron_models/small_bio-A.hoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/biophysical_neuron_models/small_bio-B.hoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/biophysical_neuron_models/small_bio-C.hoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/biophysical_neuron_models/small_bio.hoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /tests/data/reporting/log_spikes.log: -------------------------------------------------------------------------------- 1 | Log for spikes. 2 | Second line. 3 | -------------------------------------------------------------------------------- /tests/data/node_types.csv: -------------------------------------------------------------------------------- 1 | node_type_id model_processing 2 | 1 perisomatic 3 | -------------------------------------------------------------------------------- /tests/data/edges.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/edges.h5 -------------------------------------------------------------------------------- /tests/data/nodes.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/nodes.h5 -------------------------------------------------------------------------------- /tests/data/node_sets_extra.json: -------------------------------------------------------------------------------- 1 | { 2 | "ExtraLayer2": { 3 | "layer": 2 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/data/input_spikes.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/input_spikes.h5 -------------------------------------------------------------------------------- /tests/data/nodes_points.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/nodes_points.h5 -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYING 2 | include COPYING.LESSER 3 | include README.rst 4 | include LICENSE.txt 5 | -------------------------------------------------------------------------------- /tests/data/edges_single_pop.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/edges_single_pop.h5 -------------------------------------------------------------------------------- /tests/data/nodes_no_rotation.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/nodes_no_rotation.h5 -------------------------------------------------------------------------------- /tests/data/nodes_quaternions.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/nodes_quaternions.h5 -------------------------------------------------------------------------------- /tests/data/nodes_single_pop.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/nodes_single_pop.h5 -------------------------------------------------------------------------------- /tests/data/reporting/spikes.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/reporting/spikes.h5 -------------------------------------------------------------------------------- /tests/data/edges_complete_graph.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/edges_complete_graph.h5 -------------------------------------------------------------------------------- /tests/data/mock_electrodes_file.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/mock_electrodes_file.h5 -------------------------------------------------------------------------------- /tests/data/morphologies/morph-A.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/morphologies/morph-A.h5 -------------------------------------------------------------------------------- /tests/data/morphologies/morph-B.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/morphologies/morph-B.h5 -------------------------------------------------------------------------------- /tests/data/nodes_no_xz_rotation.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/nodes_no_xz_rotation.h5 -------------------------------------------------------------------------------- /tests/data/reporting/soma_named.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/reporting/soma_named.h5 -------------------------------------------------------------------------------- /doc/source/_images/BlueBrainSNAP.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/doc/source/_images/BlueBrainSNAP.jpg -------------------------------------------------------------------------------- /tests/data/reporting/soma_report.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/reporting/soma_report.h5 -------------------------------------------------------------------------------- /tests/data/nodes_with_library_large.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/nodes_with_library_large.h5 -------------------------------------------------------------------------------- /tests/data/nodes_with_library_small.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/nodes_with_library_small.h5 -------------------------------------------------------------------------------- /tests/data/nodes_quaternions_w_missing.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/nodes_quaternions_w_missing.h5 -------------------------------------------------------------------------------- /tests/data/reporting/compartment_named.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/reporting/compartment_named.h5 -------------------------------------------------------------------------------- /tests/data/morphologies/container-morphs.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueBrain/snap/HEAD/tests/data/morphologies/container-morphs.h5 -------------------------------------------------------------------------------- /tests/data/node_sets_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "Layer23": { 3 | "layer": [2,3] 4 | }, 5 | "only_exists_in_simulation": { 6 | "node_id": [0,2] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /bluepysnap/edges/__init__.py: -------------------------------------------------------------------------------- 1 | """Edges and EdgePopulation objects.""" 2 | 3 | from bluepysnap.edges.edge_population import EdgePopulation 4 | from bluepysnap.edges.edges import Edges 5 | -------------------------------------------------------------------------------- /bluepysnap/nodes/__init__.py: -------------------------------------------------------------------------------- 1 | """Nodes and NodePopulation objects.""" 2 | 3 | from bluepysnap.nodes.node_population import NodePopulation 4 | from bluepysnap.nodes.nodes import Nodes 5 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Aleksei Sanin 2 | Andrew Hale 3 | Arseny V. Povolotsky 4 | Benoit Coste 5 | Gianluca Ficarelli 6 | Jean-Denis Courcol 7 | Joni Herttuainen 8 | Mike Gevaert 9 | Thomas Delemontex 10 | -------------------------------------------------------------------------------- /doc/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _api-documentation: 2 | 3 | API Documentation 4 | ================= 5 | 6 | .. automodule:: bluepysnap 7 | 8 | .. toctree:: 9 | 10 | circuits 11 | simulations 12 | utilities 13 | -------------------------------------------------------------------------------- /doc/source/simulations.rst: -------------------------------------------------------------------------------- 1 | Simulations 2 | =========== 3 | 4 | .. currentmodule:: bluepysnap 5 | 6 | .. autosummary:: 7 | :toctree: submodules 8 | 9 | simulation 10 | frame_report 11 | spike_report 12 | -------------------------------------------------------------------------------- /doc/source/utilities.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | ========= 3 | 4 | .. currentmodule:: bluepysnap 5 | 6 | .. autosummary:: 7 | :toctree: submodules 8 | 9 | bbp 10 | exceptions 11 | sonata_constants 12 | circuit_ids 13 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import bluepysnap as test_module 2 | 3 | 4 | def test_version(): 5 | result = test_module.__version__ 6 | 7 | assert isinstance(result, str) 8 | assert len(result) > 0 9 | assert result[0].isdigit() 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/build 2 | build 3 | doc/source/submodules 4 | coverage.xml 5 | .coverage 6 | .eggs 7 | .tox 8 | *.pyc 9 | *.swp 10 | *.egg-info 11 | dist/ 12 | venv/ 13 | .ipynb_checkpoints/ 14 | .idea/ 15 | .vscode 16 | bluepysnap/_version.py 17 | -------------------------------------------------------------------------------- /bluepysnap/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | """Schemas and schema parser.""" 2 | 3 | from bluepysnap.schemas.schemas import ( 4 | validate_circuit_schema, 5 | validate_edges_schema, 6 | validate_nodes_schema, 7 | validate_simulation_schema, 8 | ) 9 | -------------------------------------------------------------------------------- /doc/source/circuits.rst: -------------------------------------------------------------------------------- 1 | Circuits 2 | ======== 3 | 4 | .. currentmodule:: bluepysnap 5 | 6 | .. autosummary:: 7 | :toctree: submodules 8 | :recursive: 9 | 10 | circuit 11 | config 12 | edges 13 | morph 14 | neuron_models 15 | nodes 16 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | :end-before: .. substitutions 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 2 7 | 8 | Home 9 | notebooks 10 | api 11 | changelog 12 | 13 | .. |banner| image:: /_images/BlueBrainSNAP.jpg 14 | .. |circuit| replace:: :class:`.Circuit` 15 | .. |simulation| replace:: :class:`.Simulation` 16 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | sphinx: 8 | fail_on_warning: true 9 | 10 | build: 11 | os: ubuntu-22.04 12 | tools: 13 | python: "3.11" 14 | 15 | python: 16 | install: 17 | - method: pip 18 | path: . 19 | extra_requirements: 20 | - docs 21 | -------------------------------------------------------------------------------- /bluepysnap/schemas/node/virtual.yaml: -------------------------------------------------------------------------------- 1 | title: Nodes - virtual 2 | description: schema for virtual node types 3 | allOf: [{ $ref: "#/$node_file_defs/nodes_file_root" }] 4 | properties: 5 | nodes: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - model_template 13 | - model_type 14 | -------------------------------------------------------------------------------- /tests/data/node_sets_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "double_combined": ["combined", "population_default_L6"], 3 | "Node2_L6_Y": { 4 | "mtype": ["L6_Y"], 5 | "node_id": [30, 20, 20] 6 | }, 7 | "Layer23": { 8 | "layer": [3, 2, 2] 9 | }, 10 | "population_default_L6": { 11 | "population": "default", 12 | "mtype": "L6_Y" 13 | }, 14 | "combined": ["Node2_L6_Y", "Layer23"], 15 | "failing": { 16 | "unknown_property": [0] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bluepysnap/schemas/node/astrocyte.yaml: -------------------------------------------------------------------------------- 1 | title: Nodes - astrocyte 2 | description: schema for astrocyte node types 3 | allOf: [{ $ref: "#/$node_file_defs/nodes_file_root" }] 4 | properties: 5 | nodes: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - x 13 | - y 14 | - z 15 | - radius 16 | - mtype 17 | - morphology 18 | - model_type 19 | - model_template 20 | -------------------------------------------------------------------------------- /bluepysnap/schemas/edge/neuromodulatory.yaml: -------------------------------------------------------------------------------- 1 | title: Edges - neuromodulatory 2 | description: schema for neuromodulatory edge types 3 | allOf: [{ $ref: "#/$edge_file_defs/edges_file_root" }] 4 | properties: 5 | edges: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - afferent_section_id 13 | - afferent_section_pos 14 | - afferent_segment_id 15 | - delay 16 | - neuromod_dtc 17 | - neuromod_strength 18 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest.mock import patch 3 | 4 | import bluepysnap.settings as test_module 5 | 6 | 7 | def test_str2bool(): 8 | assert test_module.str2bool("1") 9 | assert test_module.str2bool("y") 10 | assert test_module.str2bool("YES") 11 | assert not test_module.str2bool("0") 12 | assert not test_module.str2bool("n") 13 | assert not test_module.str2bool("No") 14 | assert not test_module.str2bool(None) 15 | 16 | 17 | def test_STRICT_MODE(): 18 | with patch.dict(os.environ, {"BLUESNAP_STRICT_MODE": "1"}): 19 | test_module.load_env() 20 | assert test_module.STRICT_MODE 21 | -------------------------------------------------------------------------------- /bluepysnap/schemas/edge/synapse_astrocyte.yaml: -------------------------------------------------------------------------------- 1 | title: Edges - synapse_astrocyte 2 | description: schema for synapse_astrocyte edge types 3 | allOf: [{ $ref: "#/$edge_file_defs/edges_file_root" }] 4 | properties: 5 | edges: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - astrocyte_section_id 13 | - astrocyte_segment_id 14 | - astrocyte_segment_offset 15 | - astrocyte_section_pos 16 | - astrocyte_center_x 17 | - astrocyte_center_y 18 | - astrocyte_center_z 19 | - synapse_id 20 | - synapse_population 21 | -------------------------------------------------------------------------------- /.github/workflows/publish-sdist.yml: -------------------------------------------------------------------------------- 1 | name: Publish sdist tarball to PyPi 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish on PyPI 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python 3.11 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.11" 18 | - name: Build a source tarball 19 | run: 20 | python setup.py sdist 21 | 22 | - name: Publish distribution package to PyPI 23 | uses: pypa/gh-action-pypi-publish@release/v1 24 | with: 25 | user: __token__ 26 | password: ${{ secrets.PYPI_PASSWORD }} 27 | -------------------------------------------------------------------------------- /bluepysnap/schemas/edge/endfoot.yaml: -------------------------------------------------------------------------------- 1 | title: Edges - endfoot 2 | description: schema for endfoot edge types 3 | allOf: [{ $ref: "#/$edge_file_defs/edges_file_root" }] 4 | properties: 5 | edges: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - endfoot_id 13 | - endfoot_surface_x 14 | - endfoot_surface_y 15 | - endfoot_surface_z 16 | - vasculature_section_id 17 | - vasculature_segment_id 18 | - astrocyte_section_id 19 | - endfoot_compartment_length 20 | - endfoot_compartment_diameter 21 | - endfoot_compartment_perimeter 22 | -------------------------------------------------------------------------------- /bluepysnap/schemas/node/vasculature.yaml: -------------------------------------------------------------------------------- 1 | title: Nodes - vasculature 2 | description: schema for vasculature node types 3 | allOf: [{ $ref: "#/$node_file_defs/nodes_file_root" }] 4 | properties: 5 | nodes: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - start_x 13 | - start_y 14 | - start_z 15 | - end_x 16 | - end_y 17 | - end_z 18 | - start_diameter 19 | - end_diameter 20 | - start_node 21 | - end_node 22 | - type 23 | - section_id 24 | - segment_id 25 | - model_type 26 | -------------------------------------------------------------------------------- /bluepysnap/schemas/node/biophysical.yaml: -------------------------------------------------------------------------------- 1 | title: Nodes - biophysical 2 | description: schema for biophysical node types 3 | allOf: [{ $ref: "#/$node_file_defs/nodes_file_root" }] 4 | properties: 5 | nodes: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - x 13 | - y 14 | - z 15 | - orientation_w 16 | - orientation_x 17 | - orientation_y 18 | - orientation_z 19 | - morphology 20 | - model_template 21 | - model_type 22 | - morph_class 23 | - etype 24 | - mtype 25 | - synapse_class 26 | - dynamics_params 27 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | .PHONY: clean 18 | clean: 19 | rm -rf $(BUILDDIR)/* 20 | rm -rf $(SOURCEDIR)/submodules/* 21 | 22 | # Catch-all target: route all unknown targets to Sphinx using the new 23 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 24 | %: Makefile 25 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 26 | -------------------------------------------------------------------------------- /tests/test_circuit_ids_types.py: -------------------------------------------------------------------------------- 1 | import bluepysnap.circuit_ids_types as test_module 2 | 3 | 4 | class TestCircuitNodeId: 5 | def setup_method(self): 6 | self.test_obj = test_module.CircuitNodeId("pop", 1) 7 | 8 | def test_init(self): 9 | assert isinstance(self.test_obj, test_module.CircuitNodeId) 10 | assert isinstance(self.test_obj, tuple) 11 | 12 | def test_accessors(self): 13 | assert self.test_obj.population == "pop" 14 | assert self.test_obj.id == 1 15 | 16 | 17 | class TestCircuitEdgeId: 18 | def setup_method(self): 19 | self.test_obj = test_module.CircuitEdgeId("pop", 1) 20 | 21 | def test_init(self): 22 | assert isinstance(self.test_obj, test_module.CircuitEdgeId) 23 | assert isinstance(self.test_obj, tuple) 24 | 25 | def test_accessors(self): 26 | assert self.test_obj.population == "pop" 27 | assert self.test_obj.id == 1 28 | -------------------------------------------------------------------------------- /tests/test_edge_misc.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY, Mock 2 | 3 | import numpy as np 4 | import numpy.testing as npt 5 | import pytest 6 | 7 | import bluepysnap.edges.edge_population as test_module 8 | 9 | 10 | def test_estimate_range_size_1(): 11 | func = lambda x: Mock(ranges=np.zeros(x)) 12 | actual = test_module._estimate_range_size(func, [11, 21, 31], n=5) 13 | npt.assert_equal(actual, 21) 14 | 15 | 16 | def test_estimate_range_size_2(): 17 | func = lambda x: Mock(ranges=[42]) 18 | actual = test_module._estimate_range_size(func, range(10)) 19 | npt.assert_equal(actual, 1) 20 | 21 | 22 | def test_estimate_range_size_3(): 23 | func = lambda x: Mock(ranges=[42]) 24 | actual = test_module._estimate_range_size(func, range(10)) 25 | npt.assert_equal(actual, 1) 26 | 27 | 28 | def test_estimate_range_size_4(): 29 | with pytest.raises(AssertionError): 30 | test_module._estimate_range_size(ANY, []) 31 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /doc/source/notebooks.rst: -------------------------------------------------------------------------------- 1 | Notebooks 2 | ========= 3 | 4 | There is a collection of ``jupyter`` notebooks covering most of the use cases. 5 | 6 | Circuit 7 | ------- 8 | Covers basic circuit usage. 9 | 10 | - :notebooks_source:`01_circuits` 11 | - :notebooks_source:`02_node_populations` 12 | - :notebooks_source:`03_node_properties` 13 | - :notebooks_source:`04_edge_properties` 14 | 15 | Simulation 16 | ---------- 17 | Covers basic simulation usage. 18 | 19 | - :notebooks_source:`05_simulations` 20 | - :notebooks_source:`06_spike_reports` 21 | - :notebooks_source:`07_frame_reports` 22 | 23 | 24 | Advanced 25 | -------- 26 | Covers more advanced use cases. 27 | 28 | - :notebooks_source:`08_nodesets`: How to take full advantage of node sets 29 | - :notebooks_source:`09_node_queries`: Different node queries 30 | - :notebooks_source:`10_edge_queries`: Different edge queries based on node properties 31 | - :notebooks_source:`11_iter_connections`: Efficient querying on large edge collections 32 | -------------------------------------------------------------------------------- /bluepysnap/schemas/edge/chemical_virtual.yaml: -------------------------------------------------------------------------------- 1 | title: Edges - chemical (virtual) 2 | description: schema for virtual chemical edge types 3 | allOf: [{ $ref: "#/$edge_file_defs/edges_file_root" }] 4 | properties: 5 | edges: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - afferent_center_x 13 | - afferent_center_y 14 | - afferent_center_z 15 | - afferent_section_id 16 | - afferent_section_pos 17 | - afferent_section_type 18 | - afferent_segment_id 19 | - afferent_segment_offset 20 | - efferent_section_type 21 | - conductance 22 | - decay_time 23 | - depression_time 24 | - facilitation_time 25 | - u_syn 26 | - n_rrp_vesicles 27 | - syn_type_id 28 | - delay 29 | -------------------------------------------------------------------------------- /.github/workflows/run-tox.yml: -------------------------------------------------------------------------------- 1 | name: Run all tox python3 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip setuptools 26 | pip install tox-gh-actions 27 | - name: Run tox 28 | run: | 29 | tox 30 | - name: Upload to codecov 31 | if: ${{matrix.python-version == '3.11'}} 32 | uses: codecov/codecov-action@v3 33 | with: 34 | fail_ci_if_error: false 35 | files: ./coverage.xml 36 | flags: pytest 37 | name: "bluepysnap-py311" 38 | -------------------------------------------------------------------------------- /tests/data/node_sets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Layer2": { 3 | "layer": 2 4 | }, 5 | "Layer23": { 6 | "layer": [2,3] 7 | }, 8 | "Empty_nodes": { 9 | "node_id": [] 10 | }, 11 | "Node2012": { 12 | "node_id": [2,0,1,2] 13 | }, 14 | "Node12_L6_Y": { 15 | "mtype": "L6_Y", 16 | "node_id": [1,2] 17 | }, 18 | "Node2_L6_Y": { 19 | "mtype": "L6_Y", 20 | "node_id": [2] 21 | }, 22 | "Node0_L6_Y": { 23 | "mtype": "L6_Y", 24 | "node_id": [0] 25 | }, 26 | "Empty_L6_Y": { 27 | "node_id": [], 28 | "mtype": "L6_Y" 29 | }, 30 | "Population_default": { 31 | "population": "default" 32 | }, 33 | "Population_default2": { 34 | "population": "default2" 35 | }, 36 | "Population_default_L6_Y": { 37 | "population": "default", 38 | "mtype": "L6_Y" 39 | }, 40 | "Population_default_L6_Y_Node2": { 41 | "population": "default", 42 | "mtype": "L6_Y", 43 | "node_id": [2] 44 | }, 45 | 46 | "combined_Node0_L6_Y__Node12_L6_Y": ["Node0_L6_Y", "Node12_L6_Y"], 47 | "combined_combined_Node0_L6_Y__Node12_L6_Y__": ["combined_Node0_L6_Y__Node12_L6_Y", "Layer23"] 48 | } 49 | -------------------------------------------------------------------------------- /tests/data/circuit_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "$BASE_DIR": ".", 4 | "$COMPONENT_DIR": "$BASE_DIR", 5 | "$NETWORK_DIR": "./" 6 | }, 7 | "components": { 8 | "biophysical_neuron_models_dir": "$COMPONENT_DIR/biophysical_neuron_models", 9 | "morphologies_dir": "$COMPONENT_DIR/morphologies" 10 | }, 11 | "node_sets_file": "$BASE_DIR/node_sets.json", 12 | "networks": { 13 | "nodes": [ 14 | { 15 | "nodes_file": "$NETWORK_DIR/nodes.h5", 16 | "populations": { 17 | "default": { 18 | "type": "biophysical" 19 | }, 20 | "default2": { 21 | "type": "biophysical", 22 | "spatial_segment_index_dir": "path/to/node/dir" 23 | } 24 | } 25 | } 26 | ], 27 | "edges": [ 28 | { 29 | "edges_file": "$NETWORK_DIR/edges.h5", 30 | "populations": { 31 | "default": { 32 | "type": "chemical" 33 | }, 34 | "default2": { 35 | "type": "chemical", 36 | "spatial_synapse_index_dir": "path/to/edge/dir" 37 | } 38 | } 39 | } 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/test_sonata_constants.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from bluepysnap.exceptions import BluepySnapError 4 | from bluepysnap.sonata_constants import ConstContainer 5 | 6 | 7 | class Container(ConstContainer): 8 | VAR1 = "var1" 9 | 10 | 11 | class SubContainer(Container): 12 | VAR2 = "var2" 13 | 14 | 15 | class SubSubContainer1(SubContainer): 16 | VAR3 = "var3" 17 | 18 | 19 | class SubSubContainer2(SubContainer): 20 | VAR4 = "var4" 21 | 22 | 23 | class ComplexContainer(SubSubContainer1, SubSubContainer2): 24 | VAR5 = "var5" 25 | 26 | 27 | class FailingContainer(SubContainer, int): 28 | VAR6 = "var6" 29 | 30 | 31 | def test_list_keys(): 32 | assert SubSubContainer1.key_set() == {"VAR1", "VAR2", "VAR3"} 33 | assert SubSubContainer2.key_set() == {"VAR1", "VAR2", "VAR4"} 34 | assert ComplexContainer.key_set() == {"VAR1", "VAR2", "VAR3", "VAR4", "VAR5"} 35 | 36 | with pytest.raises(BluepySnapError): 37 | FailingContainer.key_set() 38 | 39 | 40 | def test_get(): 41 | assert SubSubContainer1.get("VAR1") == "var1" 42 | assert SubSubContainer1.get("VAR3") == "var3" 43 | with pytest.raises(BluepySnapError): 44 | SubSubContainer1.get("VAR4") 45 | -------------------------------------------------------------------------------- /bluepysnap/schemas/edge/glialglial.yaml: -------------------------------------------------------------------------------- 1 | title: Edges - glialglial 2 | description: schema for glialglial edge types 3 | allOf: [{ $ref: "#/$edge_file_defs/edges_file_root" }] 4 | properties: 5 | edges: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - afferent_center_x 13 | - afferent_center_y 14 | - afferent_center_z 15 | - afferent_surface_x 16 | - afferent_surface_y 17 | - afferent_surface_z 18 | - afferent_section_id 19 | - afferent_section_pos 20 | - afferent_section_type 21 | - afferent_segment_id 22 | - afferent_segment_offset 23 | - efferent_center_x 24 | - efferent_center_y 25 | - efferent_center_z 26 | - efferent_surface_x 27 | - efferent_surface_y 28 | - efferent_surface_z 29 | - efferent_section_id 30 | - efferent_section_pos 31 | - efferent_section_type 32 | - efferent_segment_id 33 | - efferent_segment_offset 34 | - spine_length 35 | -------------------------------------------------------------------------------- /bluepysnap/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | # 3 | # This file is part of BlueBrain SNAP library 4 | # 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | # 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """Simulation and Neural network Analysis Productivity layer.""" 19 | 20 | from bluepysnap.circuit import Circuit 21 | from bluepysnap.config import Config 22 | from bluepysnap.exceptions import BluepySnapError 23 | from bluepysnap.simulation import Simulation 24 | 25 | try: 26 | from bluepysnap._version import version as __version__ 27 | except ImportError: # pragma: no cover 28 | __version__ = "unknown" 29 | -------------------------------------------------------------------------------- /bluepysnap/circuit_ids_types.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """CircuitIds types.""" 19 | 20 | from collections import namedtuple 21 | 22 | import numpy as np 23 | 24 | # dtypes for the different node and edge ids. We are using np.int64 to avoid the infamous 25 | # https://github.com/numpy/numpy/issues/15084 numpy problem. This type needs to be used for 26 | # all returned node or edge ids. 27 | IDS_DTYPE = np.int64 28 | 29 | CircuitNodeId = namedtuple("CircuitNodeId", ("population", "id")) 30 | CircuitEdgeId = namedtuple("CircuitEdgeId", ("population", "id")) 31 | -------------------------------------------------------------------------------- /bluepysnap/schemas/edge/electrical.yaml: -------------------------------------------------------------------------------- 1 | title: Edges - electrical 2 | description: schema for electrical edge types 3 | allOf: [{ $ref: "#/$edge_file_defs/edges_file_root" }] 4 | properties: 5 | edges: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - afferent_center_x 13 | - afferent_center_y 14 | - afferent_center_z 15 | - afferent_surface_x 16 | - afferent_surface_y 17 | - afferent_surface_z 18 | - afferent_section_id 19 | - afferent_section_pos 20 | - afferent_section_type 21 | - afferent_segment_id 22 | - afferent_segment_offset 23 | - afferent_junction_id 24 | - efferent_center_x 25 | - efferent_center_y 26 | - efferent_center_z 27 | - efferent_surface_x 28 | - efferent_surface_y 29 | - efferent_surface_z 30 | - efferent_section_id 31 | - efferent_section_pos 32 | - efferent_section_type 33 | - efferent_segment_id 34 | - efferent_segment_offset 35 | - efferent_junction_id 36 | - conductance 37 | - spine_length 38 | -------------------------------------------------------------------------------- /bluepysnap/schemas/definitions/datatypes.yaml: -------------------------------------------------------------------------------- 1 | title: Data Types 2 | description: data type definitions for schema validation 3 | $typedefs: 4 | general: 5 | type: object 6 | required: 7 | - datatype 8 | properties: 9 | datatype: 10 | type: string 11 | 12 | float32: 13 | $ref: "#/$typedefs/general" 14 | properties: 15 | datatype: 16 | const: float32 17 | int8: 18 | $ref: "#/$typedefs/general" 19 | properties: 20 | datatype: 21 | const: int8 22 | int16: 23 | $ref: "#/$typedefs/general" 24 | properties: 25 | datatype: 26 | const: int16 27 | int32: 28 | $ref: "#/$typedefs/general" 29 | properties: 30 | datatype: 31 | const: int32 32 | int64: 33 | $ref: "#/$typedefs/general" 34 | properties: 35 | datatype: 36 | const: int64 37 | uint8: 38 | $ref: "#/$typedefs/general" 39 | properties: 40 | datatype: 41 | const: uint8 42 | uint16: 43 | $ref: "#/$typedefs/general" 44 | properties: 45 | datatype: 46 | const: uint16 47 | uint32: 48 | $ref: "#/$typedefs/general" 49 | properties: 50 | datatype: 51 | const: uint32 52 | uint64: 53 | $ref: "#/$typedefs/general" 54 | properties: 55 | datatype: 56 | const: uint64 57 | utf8: 58 | $ref: "#/$typedefs/general" 59 | properties: 60 | datatype: 61 | const: utf-8 62 | -------------------------------------------------------------------------------- /bluepysnap/schemas/edge/chemical.yaml: -------------------------------------------------------------------------------- 1 | title: Edges - chemical 2 | description: schema for chemical edge types 3 | allOf: [{ $ref: "#/$edge_file_defs/edges_file_root" }] 4 | properties: 5 | edges: 6 | patternProperties: 7 | # "" is used as a wild card for population name 8 | "": 9 | properties: 10 | "0": 11 | required: 12 | - afferent_center_x 13 | - afferent_center_y 14 | - afferent_center_z 15 | - afferent_surface_x 16 | - afferent_surface_y 17 | - afferent_surface_z 18 | - afferent_section_id 19 | - afferent_section_pos 20 | - afferent_section_type 21 | - afferent_segment_id 22 | - afferent_segment_offset 23 | - efferent_center_x 24 | - efferent_center_y 25 | - efferent_center_z 26 | - efferent_surface_x 27 | - efferent_surface_y 28 | - efferent_surface_z 29 | - efferent_section_id 30 | - efferent_section_pos 31 | - efferent_section_type 32 | - efferent_segment_id 33 | - efferent_segment_offset 34 | - conductance 35 | - decay_time 36 | - depression_time 37 | - facilitation_time 38 | - u_syn 39 | - n_rrp_vesicles 40 | - spine_length 41 | - syn_type_id 42 | - delay 43 | -------------------------------------------------------------------------------- /bluepysnap/settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """Configuration variables.""" 19 | 20 | import os 21 | 22 | # All possible checks enabled / deprecated methods disallowed 23 | STRICT_MODE = False 24 | 25 | 26 | def str2bool(value): 27 | """Convert environment variable value to bool.""" 28 | if value is None: 29 | return False 30 | else: 31 | return value.lower() in ("y", "yes", "true", "1") 32 | 33 | 34 | def load_env(): 35 | """Load settings from environment variables.""" 36 | # pylint: disable=global-statement 37 | if "BLUESNAP_STRICT_MODE" in os.environ: 38 | global STRICT_MODE 39 | STRICT_MODE = str2bool(os.environ["BLUESNAP_STRICT_MODE"]) 40 | 41 | 42 | load_env() 43 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | import click 4 | from click.testing import CliRunner 5 | 6 | from bluepysnap.cli import cli 7 | 8 | from utils import TEST_DATA_DIR 9 | 10 | 11 | @patch("bluepysnap.schemas.validate_nodes_schema", Mock(return_value=[])) 12 | @patch("bluepysnap.schemas.validate_edges_schema", Mock(return_value=[])) 13 | @patch("bluepysnap.schemas.validate_circuit_schema", Mock(return_value=[])) 14 | def test_cli_validate_circuit_correct(): 15 | runner = CliRunner() 16 | result = runner.invoke(cli, ["validate-circuit", str(TEST_DATA_DIR / "circuit_config.json")]) 17 | assert result.exit_code == 0 18 | assert click.style("No Error: Success.", fg="green") in result.stdout 19 | 20 | 21 | def test_cli_validate_circuit_no_config(): 22 | runner = CliRunner() 23 | result = runner.invoke(cli, ["validate-circuit"]) 24 | assert result.exit_code == 2 25 | assert "Missing argument 'CONFIG_FILE'" in result.stdout 26 | 27 | 28 | def test_cli_validate_simulation_correct(): 29 | runner = CliRunner() 30 | result = runner.invoke( 31 | cli, ["validate-simulation", str(TEST_DATA_DIR / "simulation_config.json")] 32 | ) 33 | assert result.exit_code == 0 34 | assert click.style("No Error: Success.", fg="green") in result.stdout 35 | 36 | 37 | def test_cli_validate_simulation_no_config(): 38 | runner = CliRunner() 39 | result = runner.invoke(cli, ["validate-simulation"]) 40 | assert result.exit_code == 2 41 | assert "Missing argument 'CONFIG_FILE'" in result.stdout 42 | -------------------------------------------------------------------------------- /bluepysnap/schemas/circuit.yaml: -------------------------------------------------------------------------------- 1 | title: SONATA Circuit Config 2 | description: schema for BBP SONATA circuit config 3 | required: 4 | - networks 5 | properties: 6 | version: 7 | type: number 8 | manifest: 9 | type: object 10 | node_sets_file: 11 | type: string 12 | components: 13 | $ref: "#/$defs/components" 14 | networks: 15 | type: object 16 | required: 17 | - nodes 18 | - edges 19 | properties: 20 | nodes: 21 | $ref: "#/$defs/network_array" 22 | items: 23 | required: 24 | - nodes_file 25 | edges: 26 | $ref: "#/$defs/network_array" 27 | items: 28 | required: 29 | - edges_file 30 | $defs: 31 | components: 32 | type: object 33 | properties: 34 | morphologies_dir: 35 | type: string 36 | alternate_morphologies: 37 | type: object 38 | properties: 39 | h5v1: 40 | type: string 41 | neurolucida-asc: 42 | type: string 43 | biophysical_neuron_models_dir: 44 | type: string 45 | vasculature_file: 46 | type: string 47 | vasculature_mesh: 48 | type: string 49 | end_feet_area: 50 | type: string 51 | spine_morphologies_dir: 52 | type: string 53 | network_array: 54 | type: array 55 | minItems: 1 56 | items: 57 | type: object 58 | required: 59 | - populations 60 | properties: 61 | edges_file: 62 | type: string 63 | nodes_file: 64 | type: string 65 | populations: 66 | type: object 67 | minProperties: 1 68 | patternProperties: 69 | # "" is used as a wild card for population name 70 | "": 71 | $ref: "#/$defs/components" 72 | properties: 73 | type: 74 | type: string 75 | -------------------------------------------------------------------------------- /tests/test_edge_population_stats.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import numpy.testing as npt 4 | import pytest 5 | 6 | import bluepysnap.edges.edge_population_stats as test_module 7 | from bluepysnap.exceptions import BluepySnapError 8 | 9 | 10 | class TestStatsHelper: 11 | def setup_method(self): 12 | self.edge_pop = Mock() 13 | self.stats = test_module.StatsHelper(self.edge_pop) 14 | 15 | def test_divergence_by_synapses(self): 16 | self.edge_pop.source.ids.return_value = [1, 2] 17 | self.edge_pop.iter_connections.return_value = [(1, None, 42), (1, None, 43)] 18 | actual = self.stats.divergence("pre", "post", by="synapses") 19 | npt.assert_equal(actual, [85, 0]) 20 | 21 | def test_divergence_by_connections(self): 22 | self.edge_pop.source.ids.return_value = [1, 2] 23 | self.edge_pop.iter_connections.return_value = [(1, None), (1, None)] 24 | actual = self.stats.divergence("pre", "post", by="connections") 25 | npt.assert_equal(actual, [2, 0]) 26 | 27 | def test_divergence_error(self): 28 | pytest.raises(BluepySnapError, self.stats.divergence, "pre", "post", by="err") 29 | 30 | def test_convergence_by_synapses(self): 31 | self.edge_pop.target.ids.return_value = [1, 2] 32 | self.edge_pop.iter_connections.return_value = [(None, 2, 42), (None, 2, 43)] 33 | actual = self.stats.convergence("pre", "post", by="synapses") 34 | npt.assert_equal(actual, [0, 85]) 35 | 36 | def test_convergence_by_connections(self): 37 | self.edge_pop.target.ids.return_value = [1, 2] 38 | self.edge_pop.iter_connections.return_value = [(None, 2), (None, 2)] 39 | actual = self.stats.convergence("pre", "post", by="connections") 40 | npt.assert_equal(actual, [0, 2]) 41 | 42 | def test_convergence_error(self): 43 | pytest.raises(BluepySnapError, self.stats.convergence, "pre", "post", by="err") 44 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [base] 2 | name = bluepysnap 3 | 4 | [tox] 5 | envlist = 6 | lint 7 | py{39,310,311,312} 8 | 9 | ignore_basepython_conflict = true 10 | 11 | [testenv] 12 | basepython=python3.11,python3.10 13 | deps = 14 | pytest 15 | pytest-cov 16 | 17 | extras = 18 | plots 19 | 20 | usedevelop = true 21 | setenv = 22 | MPLBACKEND = Agg 23 | commands = pytest tests --cov={[base]name} --cov-report term-missing --cov-fail-under=100 --cov-report=xml {posargs} 24 | 25 | [testenv:lint] 26 | deps = 27 | black 28 | isort 29 | pycodestyle 30 | pydocstyle 31 | pylint 32 | commands = 33 | black --check . 34 | isort --check {[base]name} tests setup.py doc/source/conf.py 35 | pycodestyle {[base]name} 36 | pydocstyle {[base]name} 37 | pylint -j4 {[base]name} tests 38 | 39 | [testenv:format] 40 | deps = 41 | black 42 | isort 43 | commands = 44 | black . 45 | isort {[base]name} tests setup.py doc/source/conf.py 46 | 47 | [testenv:docs] 48 | changedir = doc 49 | extras = docs 50 | commands = 51 | make clean 52 | make html SPHINXOPTS=-W 53 | allowlist_externals = make 54 | 55 | # E203: whitespace before ':' 56 | # E501: line too long (handled by black) 57 | # E731: do not assign a lambda expression, use a def 58 | # W503: line break after binary operator 59 | # W504: line break before binary operator 60 | [pycodestyle] 61 | ignore = E203,E501,E731,W503,W504 62 | 63 | [pydocstyle] 64 | # ignore the following 65 | # - D413: no blank line after last section 66 | # - D202: no blank line after docstring (handled by black, causes an issue if first line is '#pylint:disable') 67 | add-ignore = D413,D202 68 | convention = google 69 | 70 | [gh-actions] 71 | python = 72 | 3.8: py38 73 | 3.9: py39 74 | 3.10: py310 75 | 3.11: py311, lint, docs 76 | 77 | [pytest] 78 | filterwarnings = 79 | # ignoring the warning about Simulation node sets overwriting Circuit node sets in tests 80 | ignore:Simulation node sets overwrite:RuntimeWarning:bluepysnap 81 | -------------------------------------------------------------------------------- /tests/data/simulation_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "$OUTPUT_DIR": "./reporting", 4 | "$INPUT_DIR": "./" 5 | }, 6 | "run": { 7 | "tstop": 1000.0, 8 | "dt": 0.01, 9 | "spike_threshold": -15, 10 | "random_seed": 42 11 | }, 12 | "target_simulator":"CORENEURON", 13 | "network": "$INPUT_DIR/circuit_config.json", 14 | "conditions": { 15 | "celsius": 34.0, 16 | "v_init": -80, 17 | "other": "something" 18 | }, 19 | "node_sets_file": "$INPUT_DIR/node_sets_simple.json", 20 | "mechanisms_dir": "../shared_components_mechanisms", 21 | "inputs": { 22 | "current_clamp_1": { 23 | "input_type": "current_clamp", 24 | "module": "linear", 25 | "node_set": "Layer23", 26 | "amp_start": 190.0, 27 | "delay": 100.0, 28 | "duration": 800.0 29 | }, 30 | "spikes_1":{ 31 | "input_type": "spikes", 32 | "module": "synapse_replay", 33 | "delay": 800, 34 | "duration": 100, 35 | "node_set": "Layer23", 36 | "source": "Layer23", 37 | "spike_file": "input_spikes.h5" 38 | } 39 | }, 40 | 41 | "output":{ 42 | "output_dir": "$OUTPUT_DIR", 43 | "log_file": "log_spikes.log", 44 | "spikes_file": "spikes.h5", 45 | "spikes_sort_order": "by_time" 46 | }, 47 | 48 | "reports": { 49 | "soma_report": { 50 | "cells": "Layer23", 51 | "variable_name": "m", 52 | "sections": "soma", 53 | "type": "compartment", 54 | "file_name": "soma_report", 55 | "start_time": 0, 56 | "end_time": 1000.0, 57 | "dt": 0.01, 58 | "enabled": true 59 | }, 60 | "section_report": { 61 | "cells": "Layer23", 62 | "variable_name": "m", 63 | "sections": "all", 64 | "type": "compartment", 65 | "start_time": 0.2, 66 | "end_time": 0.8, 67 | "dt": 0.02, 68 | "file_name": "compartment_named" 69 | }, 70 | "lfp_report": { 71 | "cells": "Layer23", 72 | "variable_name": "v", 73 | "type": "lfp", 74 | "start_time": 0.2, 75 | "end_time": 0.8, 76 | "dt": 0.02 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/test__doctools.py: -------------------------------------------------------------------------------- 1 | import bluepysnap._doctools as test_module 2 | 3 | 4 | class TestClass: 5 | __test__ = False 6 | 7 | def __init__(self): 8 | """TestClass""" 9 | self.foo_val = 0 10 | self.bar_val = 1 11 | 12 | def foo(self): 13 | """foo function for TestClass""" 14 | return self.foo_val 15 | 16 | def bar(self): 17 | """bar function for TestClass 18 | 19 | Returns: 20 | TestClass: return a TestClass 21 | """ 22 | return self.bar_val 23 | 24 | def foo_bar(self): 25 | pass 26 | 27 | 28 | class TestClassA( 29 | TestClass, 30 | metaclass=test_module.DocSubstitutionMeta, 31 | source_word="TestClass", 32 | target_word="TestClassA", 33 | ): 34 | """New class with changed docstrings.""" 35 | 36 | 37 | class TestClassB( 38 | TestClass, 39 | metaclass=test_module.DocSubstitutionMeta, 40 | source_word="TestClass", 41 | target_word="TestClassB", 42 | ): 43 | """New class with changed docstrings.""" 44 | 45 | 46 | class TestClassC(TestClassA): 47 | def bar(self): 48 | """Is overrode correctly""" 49 | return 42 50 | 51 | 52 | def test_DocSubstitutionMeta(): 53 | default = TestClass() 54 | tested = TestClassA() 55 | assert tested.foo.__doc__ == default.foo.__doc__.replace("TestClass", "TestClassA") 56 | expected = default.bar.__doc__.replace("TestClass", "TestClassA") 57 | assert tested.bar.__doc__ == expected 58 | assert tested.foo_bar.__doc__ is None 59 | assert tested.__dict__ == default.__dict__ 60 | 61 | # do not override the mother class docstrings 62 | tested = TestClassB() 63 | assert tested.foo.__doc__ == default.foo.__doc__.replace("TestClass", "TestClassB") 64 | expected = default.bar.__doc__.replace("TestClass", "TestClassB") 65 | assert tested.bar.__doc__ == expected 66 | assert tested.foo_bar.__doc__ is None 67 | assert tested.__dict__ == default.__dict__ 68 | 69 | # I can inherit from a class above 70 | tested = TestClassC() 71 | assert tested.foo.__doc__ == TestClassA.foo.__doc__ 72 | assert tested.bar.__doc__ == "Is overrode correctly" 73 | assert tested.bar() == 42 74 | -------------------------------------------------------------------------------- /tests/test_input.py: -------------------------------------------------------------------------------- 1 | import libsonata 2 | import numpy.testing as npt 3 | import pytest 4 | 5 | import bluepysnap.input as test_module 6 | from bluepysnap.exceptions import BluepySnapError 7 | 8 | from utils import TEST_DATA_DIR 9 | 10 | 11 | class TestSynapseReplay: 12 | def setup_method(self): 13 | simulation = libsonata.SimulationConfig.from_file(TEST_DATA_DIR / "simulation_config.json") 14 | self.test_obj = test_module.SynapseReplay(simulation.input("spikes_1")) 15 | 16 | def test_all(self): 17 | snap_attrs = {a for a in dir(self.test_obj) if not a.startswith("_")} 18 | libsonata_attrs = {a for a in dir(self.test_obj._instance) if not a.startswith("_")} 19 | 20 | # check that wrapped instance's public methods are available in the object 21 | assert snap_attrs.symmetric_difference(libsonata_attrs) == {"reader"} 22 | assert isinstance(self.test_obj.reader, libsonata.SpikeReader) 23 | 24 | for a in libsonata_attrs: 25 | assert getattr(self.test_obj, a) == getattr(self.test_obj._instance, a) 26 | 27 | npt.assert_almost_equal(self.test_obj.reader["default"].get(), [[0, 10.775]]) 28 | 29 | def test_no_such_attribute(self): 30 | """Check that the attribute error is raised from the wrapped libsonata object.""" 31 | with pytest.raises(AttributeError, match="libsonata._libsonata.SynapseReplay"): 32 | self.test_obj.no_such_attribute 33 | 34 | 35 | def test_get_simulation_inputs(): 36 | simulation = libsonata.SimulationConfig.from_file(TEST_DATA_DIR / "simulation_config.json") 37 | inputs = test_module.get_simulation_inputs(simulation) 38 | 39 | assert isinstance(inputs, dict) 40 | assert inputs.keys() == {"spikes_1", "current_clamp_1"} 41 | 42 | assert isinstance(inputs["spikes_1"], test_module.SynapseReplay) 43 | 44 | try: 45 | Linear = libsonata._libsonata.Linear 46 | except AttributeError: 47 | from libsonata._libsonata import SimulationConfig 48 | 49 | Linear = SimulationConfig.Linear 50 | 51 | assert isinstance(inputs["current_clamp_1"], Linear) 52 | 53 | with pytest.raises(BluepySnapError, match="Unexpected type for 'simulation': str"): 54 | test_module.get_simulation_inputs("fail_me") 55 | -------------------------------------------------------------------------------- /bluepysnap/bbp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """BBP cell / synapse attribute namespace.""" 19 | 20 | from bluepysnap.sonata_constants import DYNAMICS_PREFIX, Edge, Node 21 | 22 | NODE_TYPES = {"biophysical", "virtual", "astrocyte", "single_compartment"} 23 | EDGE_TYPES = {"chemical", "electrical", "synapse_astrocyte", "endfoot"} 24 | 25 | 26 | class Cell(Node): 27 | """Cell property names.""" 28 | 29 | ME_COMBO = "me_combo" #: 30 | MTYPE = "mtype" #: 31 | ETYPE = "etype" #: 32 | LAYER = "layer" #: 33 | REGION = "region" #: 34 | SYNAPSE_CLASS = "synapse_class" #: 35 | HOLDING_CURRENT = DYNAMICS_PREFIX + "holding_current" #: 36 | THRESHOLD_CURRENT = DYNAMICS_PREFIX + "threshold_current" #: 37 | INPUT_RESISTANCE = DYNAMICS_PREFIX + "input_resistance" #: 38 | 39 | 40 | class Synapse(Edge): 41 | """Synapse property names.""" 42 | 43 | PRE_GID = Edge.SOURCE_NODE_ID #: 44 | POST_GID = Edge.TARGET_NODE_ID #: 45 | 46 | D_SYN = "depression_time" #: 47 | DTC = "decay_time" #: 48 | F_SYN = "facilitation_time" #: 49 | G_SYNX = "conductance" #: 50 | NRRP = "NRRP" #: 51 | TYPE = "syn_type_id" #: 52 | U_SYN = "u_syn" #: 53 | SPINE_LENGTH = "spine_length" #: 54 | 55 | PRE_SEGMENT_ID = "efferent_segment_id" #: 56 | PRE_SEGMENT_OFFSET = "efferent_segment_offset" #: 57 | PRE_MORPH_ID = "efferent_morphology_id" #: 58 | 59 | POST_SEGMENT_ID = "afferent_segment_id" #: 60 | POST_SEGMENT_OFFSET = "afferent_segment_offset" #: 61 | POST_BRANCH_TYPE = "afferent_section_type" #: 62 | -------------------------------------------------------------------------------- /bluepysnap/cli.py: -------------------------------------------------------------------------------- 1 | """The project's command line launcher.""" 2 | 3 | import logging 4 | 5 | import click 6 | 7 | from bluepysnap import circuit_validation, simulation_validation 8 | 9 | CLICK_EXISTING_FILE = click.Path(exists=True, file_okay=True, dir_okay=False) 10 | 11 | 12 | @click.group() 13 | @click.version_option() 14 | @click.option("-v", "--verbose", count=True) 15 | def cli(verbose): 16 | """The CLI object.""" 17 | logging.basicConfig( 18 | level=(logging.WARNING, logging.INFO, logging.DEBUG)[min(verbose, 2)], 19 | format="%(asctime)s %(levelname)-8s %(message)s", 20 | datefmt="%Y-%m-%d %H:%M:%S", 21 | ) 22 | 23 | 24 | @cli.command() 25 | @click.argument("config_file", type=CLICK_EXISTING_FILE) 26 | @click.option( 27 | "--skip-slow/--no-skip-slow", 28 | default=True, 29 | help=( 30 | "Skip slow checks; checking all edges refer to existing node ids, " 31 | "edge indices are correct, etc" 32 | ), 33 | ) 34 | @click.option("--only-errors", is_flag=True, help="Only print fatal errors (ignore warnings)") 35 | @click.option( 36 | "--ignore-datatype-errors", 37 | is_flag=True, 38 | help="Ignore errors related to mismatch of datatypes: ie: float64 used instead of float32", 39 | ) 40 | def validate_circuit(config_file, skip_slow, only_errors, ignore_datatype_errors): 41 | """Validate Sonata circuit based on config file. 42 | 43 | Args: 44 | config_file (str): path to Sonata circuit config file 45 | skip_slow (bool): skip slow tests 46 | only_errors (bool): only print fatal errors 47 | ignore_datatype_errors (bool): ignore checks related to datatypes 48 | """ 49 | circuit_validation.validate( 50 | config_file, skip_slow, only_errors, ignore_datatype_errors=ignore_datatype_errors 51 | ) 52 | 53 | 54 | @cli.command() 55 | @click.argument("config_file", type=CLICK_EXISTING_FILE) 56 | @click.option( 57 | "--ignore-datatype-errors", 58 | is_flag=True, 59 | help="Ignore errors related to mismatch of datatypes: ie: float64 used instead of float32", 60 | ) 61 | def validate_simulation(config_file, ignore_datatype_errors): 62 | """Validate Sonata simulation based on config file. 63 | 64 | Args: 65 | config_file (str): path to Sonata simulation config file 66 | ignore_datatype_errors (bool): ignore checks related to datatypes 67 | """ 68 | simulation_validation.validate(config_file, ignore_datatype_errors=ignore_datatype_errors) 69 | -------------------------------------------------------------------------------- /tests/data/morphologies/morph-A.swc: -------------------------------------------------------------------------------- 1 | # index type X Y Z radius parent 2 | 1 1 -0.320000 1.000000 0.000000 0.725000 -1 3 | 2 1 -0.320000 0.900000 0.000000 0.820000 1 4 | 3 1 -0.320000 0.800000 0.000000 0.820000 2 5 | 4 1 -0.320000 0.700000 0.000000 0.820000 3 6 | 5 1 -0.320000 0.600000 0.000000 0.820000 4 7 | 6 1 -0.320000 0.500000 0.000000 0.820000 5 8 | 7 1 -0.320000 0.400000 0.000000 0.820000 6 9 | 8 1 -0.320000 0.300000 0.000000 0.820000 7 10 | 9 1 -0.320000 0.200000 0.000000 0.820000 8 11 | 10 1 -0.320000 0.100000 0.000000 0.820000 9 12 | 11 1 -0.320000 0.000000 0.000000 0.820000 10 13 | 12 1 -0.320000 -0.100000 0.000000 0.820000 11 14 | 13 1 -0.320000 -0.200000 0.000000 0.820000 12 15 | 14 1 -0.320000 -0.300000 0.000000 0.820000 13 16 | 15 1 -0.320000 -0.400000 0.000000 0.820000 14 17 | 16 1 -0.320000 -0.500000 0.000000 0.820000 15 18 | 17 1 -0.320000 -0.600000 0.000000 0.820000 16 19 | 18 1 -0.320000 -0.700000 0.000000 0.820000 17 20 | 19 1 -0.320000 -0.800000 0.000000 0.820000 18 21 | 20 1 -0.320000 -0.900000 0.000000 0.820000 19 22 | 21 1 -0.320000 -1.000000 0.000000 0.430000 20 23 | 22 2 0.000000 5.000000 0.000000 1.000000 1 24 | 23 2 2.000000 9.000000 0.000000 1.000000 22 25 | 24 2 0.000000 13.000000 0.000000 1.000000 23 26 | 25 2 2.000000 13.000000 0.000000 1.000000 24 27 | 26 2 4.000000 13.000000 0.000000 1.000000 25 28 | 27 3 3.000000 -4.000000 0.000000 1.000000 1 29 | 28 3 3.000000 -6.000000 0.000000 1.000000 27 30 | 29 3 3.000000 -8.000000 0.000000 1.000000 28 31 | 30 3 3.000000 -10.000000 0.000000 1.000000 29 32 | 31 3 0.000000 -10.000000 0.000000 1.000000 30 33 | 32 3 6.000000 -10.000000 0.000000 1.000000 30 34 | 35 | # Created by MorphIO v2.0.4 36 | -------------------------------------------------------------------------------- /bluepysnap/input.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | """Simulation input access.""" 18 | import libsonata 19 | 20 | from bluepysnap.exceptions import BluepySnapError 21 | 22 | 23 | class SynapseReplay: 24 | """Wrapper class for libsonata.SynapseReplay to provide the reader as a property.""" 25 | 26 | def __init__(self, instance): 27 | """Wrap libsonata SynapseReplay object. 28 | 29 | Args: 30 | instance (libsonata.SynapseReplay): instance to wrap 31 | """ 32 | self._instance = instance 33 | 34 | def __dir__(self): 35 | """Provide wrapped SynapseReplay instance's public attributes in dir.""" 36 | public_attrs_instance = {attr for attr in dir(self._instance) if not attr.startswith("_")} 37 | return list(set(super().__dir__()) | public_attrs_instance) 38 | 39 | def __getattr__(self, name): 40 | """Retrieve attributes from the wrapped object.""" 41 | return getattr(self._instance, name) 42 | 43 | @property 44 | def reader(self): 45 | """Return a spike reader object for the instance.""" 46 | return libsonata.SpikeReader(self.spike_file) 47 | 48 | 49 | def get_simulation_inputs(simulation): 50 | """Get simulation inputs as a dictionary. 51 | 52 | Args: 53 | simulation (libsonata.SimulationConfig): libsonata Simulation instance 54 | 55 | Returns: 56 | dict: inputs with input names as keys and corresponding objects as values 57 | """ 58 | 59 | def _get_input(name): 60 | """Helper function to wrap certain objects.""" 61 | item = simulation.input(name) 62 | 63 | if item.module.name == "synapse_replay": 64 | return SynapseReplay(item) 65 | return item 66 | 67 | if isinstance(simulation, libsonata.SimulationConfig): 68 | return {name: _get_input(name) for name in simulation.list_input_names} 69 | 70 | raise BluepySnapError(f"Unexpected type for 'simulation': {simulation.__class__.__name__}") 71 | -------------------------------------------------------------------------------- /bluepysnap/neuron_models.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """Neuron models access.""" 19 | 20 | from pathlib import Path 21 | 22 | from bluepysnap.exceptions import BluepySnapError 23 | from bluepysnap.sonata_constants import Node 24 | from bluepysnap.utils import is_node_id 25 | 26 | 27 | class NeuronModelsHelper: 28 | """Collection of neuron models related methods.""" 29 | 30 | def __init__(self, properties, population): 31 | """Constructor. 32 | 33 | Args: 34 | properties (libsonata.PopulationProperties): properties of the population 35 | population (NodePopulation): NodePopulation object used to query the nodes. 36 | 37 | Returns: 38 | NeuronModelsHelper: A NeuronModelsHelper object. 39 | """ 40 | # all nodes from a population must have the same model type 41 | if properties.type != "biophysical": 42 | raise BluepySnapError("Neuron models can be only in biophysical node population.") 43 | 44 | self._properties = properties 45 | self._population = population 46 | 47 | def get_filepath(self, node_id): 48 | """Return path to model file corresponding to `node_id`. 49 | 50 | Args: 51 | node_id (int|CircuitNodeId): node id 52 | 53 | Returns: 54 | Path: path to the model file of neuron 55 | """ 56 | if not is_node_id(node_id): 57 | raise BluepySnapError("node_id must be a int or a CircuitNodeId") 58 | node = self._population.get(node_id, [Node.MODEL_TYPE, Node.MODEL_TEMPLATE]) 59 | models_dir = self._properties.biophysical_neuron_models_dir 60 | 61 | template = node[Node.MODEL_TEMPLATE] 62 | assert ":" in template, "Format of 'model_template' must be :." 63 | schema, resource = template.split(":", 1) 64 | resource = Path(resource).with_suffix(f".{schema}") 65 | if resource.is_absolute(): 66 | return resource 67 | return Path(models_dir, resource) 68 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = [ 4 | 'py38', 5 | ] 6 | include = '(bluepysnap|tests)\/.*\.py|doc\/source\/conf\.py|setup\.py' 7 | 8 | [tool.isort] 9 | profile = 'black' 10 | line_length = 100 11 | known_local_folder = [ 12 | 'utils', 13 | 'test_module', 14 | ] 15 | 16 | [tool.coverage.run] 17 | omit = [ 18 | 'bluepysnap/_version.py', 19 | ] 20 | 21 | [tool.pylint.main] 22 | # Files or directories to be skipped. They should be base names, not paths. 23 | ignore = ["CVS", "_version.py"] 24 | 25 | [tool.pylint.messages_control] 26 | disable = [ 27 | 'fixme', 28 | 'invalid-name', 29 | 'len-as-condition', 30 | 'no-else-return', 31 | 'import-outside-toplevel', 32 | 'bad-mcs-classmethod-argument', 33 | 'unnecessary-lambda-assignment', 34 | ] 35 | 36 | [tool.pylint.format] 37 | # Regexp for a line that is allowed to be longer than the limit. 38 | ignore-long-lines='.*https?://' 39 | # Maximum number of characters on a single line. 40 | max-line-length = 100 41 | 42 | [tool.pylint.design] 43 | # Maximum number of arguments for function / method 44 | max-args = 8 45 | 46 | # Argument names that match this expression will be ignored. Default to name 47 | # with leading underscore 48 | ignored-argument-names = '_.*' 49 | 50 | # Maximum number of locals for function / method body 51 | max-locals = 15 52 | 53 | # Maximum number of return / yield for function / method body 54 | max-returns = 6 55 | 56 | # Maximum number of branch for function / method body 57 | max-branches = 12 58 | 59 | # Maximum number of statements in function / method body 60 | max-statements = 50 61 | 62 | # Maximum number of parents for a class (see R0901). 63 | max-parents = 7 64 | 65 | # Maximum number of attributes for a class (see R0902). 66 | max-attributes = 40 67 | 68 | # Minimum number of public methods for a class (see R0903). 69 | min-public-methods = 0 70 | 71 | # Maximum number of public methods for a class (see R0904). 72 | max-public-methods = 60 73 | 74 | # checks for similarities and duplicated code. This computation may be 75 | # memory / CPU intensive, so you should disable it if you experiment some 76 | # problems. 77 | [tool.pylint.similarities] 78 | # Minimum lines number of a similarity. 79 | min-similarity-lines = 25 80 | 81 | # Ignore comments when computing similarities. 82 | ignore-comments = 'yes' 83 | 84 | # Ignore docstrings when computing similarities. 85 | ignore-docstrings = 'yes' 86 | 87 | [tool.pylint.typecheck] 88 | extension-pkg-whitelist = [ 89 | 'libsonata', 90 | ] 91 | ignored-modules = [ 92 | 'numpy', 93 | ] 94 | 95 | [tool.pylint.tests] 96 | disable = [ 97 | 'all', 98 | ] 99 | enable = [ 100 | 'unused-import', 101 | 'unused-variable', 102 | 'unused-argument', 103 | ] 104 | -------------------------------------------------------------------------------- /bluepysnap/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | """Exceptions used throughout the library.""" 18 | 19 | 20 | class BluepySnapError(Exception): 21 | """Base SNAP exception.""" 22 | 23 | 24 | class BluepySnapDeprecationError(Exception): 25 | """SNAP deprecation exception.""" 26 | 27 | 28 | class BluepySnapDeprecationWarning(DeprecationWarning): 29 | """SNAP deprecation warning.""" 30 | 31 | 32 | class BluepySnapValidationError: 33 | """Error used for reporting of validation errors.""" 34 | 35 | FATAL = "FATAL" 36 | WARNING = "WARNING" 37 | INFO = "INFO" 38 | 39 | def __init__(self, level, message=None): 40 | """Error. 41 | 42 | Args: 43 | level (str): error level 44 | message (str|None): message 45 | """ 46 | self.level = level 47 | self.message = message 48 | 49 | def __str__(self): 50 | """Returns only message by default.""" 51 | return str(self.message) 52 | 53 | __repr__ = __str__ 54 | 55 | def __eq__(self, other): 56 | """Two errors are equal if inherit from Error and their level, message are equal.""" 57 | if not isinstance(other, BluepySnapValidationError): 58 | return False 59 | return self.level == other.level and self.message == other.message 60 | 61 | def __hash__(self): 62 | """Hash. Errors with the same level and message give the same hash.""" 63 | return hash(self.level) ^ hash(self.message) 64 | 65 | @classmethod 66 | def warning(cls, message): 67 | """Shortcut for a warning. 68 | 69 | Args: 70 | message (str): text message 71 | 72 | Returns: 73 | Error: Error with level WARNING 74 | """ 75 | return cls(cls.WARNING, message) 76 | 77 | @classmethod 78 | def fatal(cls, message): 79 | """Shortcut for a fatal error. 80 | 81 | Args: 82 | message (str): text message 83 | 84 | Returns: 85 | Error: Error with level FATAL 86 | """ 87 | return cls(cls.FATAL, message) 88 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | import importlib 10 | import subprocess 11 | 12 | # -- Project information ----------------------------------------------------- 13 | 14 | project = "Blue Brain SNAP" 15 | author = "Blue Brain Project, EPFL" 16 | 17 | release = importlib.metadata.distribution("bluepysnap").version 18 | version = release 19 | 20 | # -- General configuration --------------------------------------------------- 21 | 22 | # Add any Sphinx extension module names here, as strings. They can be 23 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 24 | # ones. 25 | extensions = [ 26 | "sphinx.ext.autodoc", 27 | "sphinx.ext.autosummary", 28 | "sphinx.ext.autosectionlabel", 29 | "sphinx.ext.extlinks", 30 | "sphinx.ext.intersphinx", 31 | "sphinx.ext.napoleon", 32 | ] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ["_templates"] 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | # This pattern also affects html_static_path and html_extra_path. 40 | exclude_patterns = [] 41 | 42 | # -- Options for HTML output ------------------------------------------------- 43 | 44 | # The theme to use for HTML and HTML Help pages. See the documentation for 45 | # a list of builtin themes. 46 | html_theme = "sphinx-bluebrain-theme" 47 | 48 | # Add any paths that contain custom static files (such as style sheets) here, 49 | # relative to this directory. They are copied after the builtin static files, 50 | # so a file named "default.css" will overwrite the builtin "default.css". 51 | # html_static_path = ['_static'] 52 | 53 | # ensure a useful title is used 54 | html_title = "Blue Brain SNAP" 55 | 56 | # hide source links 57 | html_show_sourcelink = False 58 | 59 | # set the theme settings 60 | html_theme_options = { 61 | "repo_url": "https://github.com/BlueBrain/snap/", 62 | "repo_name": "BlueBrain/snap", 63 | } 64 | 65 | # autodoc settings 66 | autodoc_default_options = { 67 | "members": True, 68 | } 69 | 70 | autoclass_content = "both" 71 | 72 | autodoc_mock_imports = ["libsonata"] 73 | 74 | # autosummary settings 75 | autosummary_generate = True 76 | 77 | suppress_warnings = [ 78 | "autosectionlabel.*", 79 | ] 80 | 81 | # generate the link to the notebooks on GitHub 82 | _base_url = "https://github.com/BlueBrain/snap" 83 | _git_commit = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() 84 | extlinks = { 85 | "notebooks_source": ( 86 | f"{_base_url}/blob/{_git_commit}/doc/source/notebooks/%s.ipynb", 87 | "Notebook: %s", 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2019, EPFL/Blue Brain Project 4 | 5 | # This file is part of BlueBrain SNAP library 6 | 7 | # This library is free software; you can redistribute it and/or modify it under 8 | # the terms of the GNU Lesser General Public License version 3.0 as published 9 | # by the Free Software Foundation. 10 | 11 | # This library is distributed in the hope that it will be useful, but WITHOUT 12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | # details. 15 | 16 | # You should have received a copy of the GNU Lesser General Public License 17 | # along with this library; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | from setuptools import find_packages, setup 21 | from setuptools.command.egg_info import egg_info 22 | 23 | 24 | # setuptools_scm forcibly includes all files under version control into the sdist 25 | # See https://github.com/pypa/setuptools_scm/issues/190 26 | # Workaround taken from: 27 | # https://github.com/raiden-network/raiden/commit/3fb837e8b6e6343f65d99055459cb440e1a938ff 28 | class EggInfo(egg_info): 29 | def __init__(self, *args, **kwargs): 30 | egg_info.__init__(self, *args, **kwargs) 31 | try: 32 | import setuptools_scm.integration 33 | 34 | setuptools_scm.integration.find_files = lambda _: [] 35 | except ImportError: 36 | pass 37 | 38 | 39 | with open("README.rst") as f: 40 | README = f.read() 41 | 42 | setup( 43 | name="bluepysnap", 44 | python_requires=">=3.8", 45 | install_requires=[ 46 | "brain-indexer>=3.0.0", 47 | "cached_property>=1.0", 48 | "h5py>=3.0.1,<4.0.0", 49 | "importlib_resources>=5.0.0", 50 | "jsonschema>=4.0.0,<5.0.0", 51 | "libsonata>=0.1.24,<1.0.0", 52 | "morphio>=3.3.5,<4.0.0", 53 | "morph-tool>=2.4.3,<3.0.0", 54 | "numpy>=1.8", 55 | "pandas>=1.0.0", 56 | "click>=7.0", 57 | "more-itertools>=8.2.0", 58 | ], 59 | extras_require={ 60 | "docs": ["sphinx", "sphinx-bluebrain-theme"], 61 | "plots": ["matplotlib>=3.0.0"], 62 | }, 63 | packages=find_packages(), 64 | package_data={ 65 | "bluepysnap": ["schemas/*.yaml", "schemas/*/*.yaml"], 66 | }, 67 | use_scm_version={ 68 | "write_to": "bluepysnap/_version.py", 69 | }, 70 | setup_requires=[ 71 | "setuptools_scm", 72 | ], 73 | cmdclass={ 74 | "egg_info": EggInfo, 75 | }, 76 | entry_points=""" 77 | [console_scripts] 78 | bluepysnap=bluepysnap.cli:cli 79 | """, 80 | author="Blue Brain Project, EPFL", 81 | description="Simulation and Neural network Analysis Productivity layer", 82 | long_description=README, 83 | long_description_content_type="text/x-rst", 84 | license="LGPLv3", 85 | url="https://github.com/BlueBrain/snap", 86 | keywords=["SONATA", "BlueBrainProject"], 87 | classifiers=[ 88 | "Development Status :: 3 - Alpha", 89 | "Programming Language :: Python :: 3.8", 90 | "Programming Language :: Python :: 3.9", 91 | "Programming Language :: Python :: 3.10", 92 | "Programming Language :: Python :: 3.11", 93 | "Operating System :: POSIX", 94 | "Topic :: Scientific/Engineering", 95 | "Topic :: Utilities", 96 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 97 | ], 98 | ) 99 | -------------------------------------------------------------------------------- /bluepysnap/_doctools.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | """Module containing tools related to the documentation and docstrings.""" 18 | import abc 19 | import functools 20 | import inspect 21 | import types 22 | 23 | 24 | def _word_swapper(doc, source_word, target_word): 25 | """Swap a word with another in a docstring.""" 26 | if doc is None: 27 | return doc 28 | if source_word is None or target_word is None: 29 | return doc 30 | return doc.replace(source_word, target_word) 31 | 32 | 33 | def _copy_func(f): 34 | """Based on http://stackoverflow.com/a/6528148/190597 (Glenn Maynard).""" 35 | g = types.FunctionType( 36 | f.__code__, f.__globals__, name=f.__name__, argdefs=f.__defaults__, closure=f.__closure__ 37 | ) 38 | g = functools.update_wrapper(g, f) 39 | g.__kwdefaults__ = f.__kwdefaults__ 40 | return g 41 | 42 | 43 | class DocSubstitutionMeta(type): 44 | """Tool to update an inherited class documentation. 45 | 46 | Notes: 47 | Using a metaclass is better than decorator to do that due to the returned type being 48 | incorrect when using a wrapper. Ex with CircuitNodeIds: 49 | type(CircuitNodeIds) 50 | .Wrapped'> 51 | when with metaclass: 52 | type(CircuitNodeIds) 53 | 54 | It works well with Sphinx also. 55 | """ 56 | 57 | def __new__(mcs, name, parents, attrs, source_word=None, target_word=None): 58 | """Define the new class to return.""" 59 | original_attrs = attrs.copy() 60 | for parent in parents: 61 | # skip classmethod with isfunction if I use also ismethod as a predicate I can have the 62 | # classmethod docstring changed but then the cls argument is not automatically skipped. 63 | for fun_name, fun_value in inspect.getmembers(parent, predicate=inspect.isfunction): 64 | # skip abstract methods. This is fine because we must override them anyway 65 | try: 66 | if fun_name in parent.__abstractmethods__: 67 | continue 68 | except AttributeError: 69 | pass 70 | # skip overrode functions 71 | if fun_name in original_attrs: 72 | continue 73 | # skip special methods 74 | if fun_name.startswith("__"): 75 | continue 76 | changed_fun = _copy_func(fun_value) 77 | changed_fun.__doc__ = _word_swapper(changed_fun.__doc__, source_word, target_word) 78 | attrs[fun_name] = changed_fun 79 | # create the class 80 | obj = super(DocSubstitutionMeta, mcs).__new__(mcs, name, parents, attrs) 81 | return obj 82 | 83 | 84 | class AbstractDocSubstitutionMeta(abc.ABCMeta, DocSubstitutionMeta): 85 | """Mixin class to use with abstract classes. 86 | 87 | It solves the metaclass conflict. 88 | """ 89 | -------------------------------------------------------------------------------- /tests/test_node_sets.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | import libsonata 5 | import pytest 6 | from numpy.testing import assert_array_equal 7 | 8 | import bluepysnap.node_sets as test_module 9 | from bluepysnap.exceptions import BluepySnapError 10 | 11 | from utils import TEST_DATA_DIR 12 | 13 | 14 | class TestNodeSet: 15 | def setup_method(self): 16 | self.test_node_sets = test_module.NodeSets.from_file( 17 | str(TEST_DATA_DIR / "node_sets_file.json") 18 | ) 19 | self.test_pop = libsonata.NodeStorage(str(TEST_DATA_DIR / "nodes.h5")).open_population( 20 | "default" 21 | ) 22 | 23 | def test_get_ids(self): 24 | assert_array_equal(self.test_node_sets["Node2_L6_Y"].get_ids(self.test_pop), []) 25 | assert_array_equal(self.test_node_sets["double_combined"].get_ids(self.test_pop), [0, 1, 2]) 26 | 27 | node_set = self.test_node_sets["failing"] 28 | 29 | with pytest.raises(BluepySnapError, match="No such attribute"): 30 | node_set.get_ids(self.test_pop) 31 | 32 | assert node_set.get_ids(self.test_pop, raise_missing_property=False) == [] 33 | 34 | 35 | class TestNodeSets: 36 | def setup_method(self): 37 | self.test_obj = test_module.NodeSets.from_file(str(TEST_DATA_DIR / "node_sets_file.json")) 38 | self.test_pop = libsonata.NodeStorage(str(TEST_DATA_DIR / "nodes.h5")).open_population( 39 | "default" 40 | ) 41 | 42 | def test_init(self): 43 | assert self.test_obj.content == { 44 | "double_combined": ["combined", "population_default_L6"], 45 | "Node2_L6_Y": {"mtype": ["L6_Y"], "node_id": [30, 20, 20]}, 46 | "Layer23": {"layer": [3, 2, 2]}, 47 | "population_default_L6": {"population": "default", "mtype": "L6_Y"}, 48 | "combined": ["Node2_L6_Y", "Layer23"], 49 | "failing": {"unknown_property": [0]}, 50 | } 51 | 52 | def test_from_string(self): 53 | res = test_module.NodeSets.from_string(json.dumps(self.test_obj.content)) 54 | assert res.content == self.test_obj.content 55 | 56 | def test_from_dict(self): 57 | res = test_module.NodeSets.from_dict(self.test_obj.content) 58 | assert res.content == self.test_obj.content 59 | 60 | def test_update(self): 61 | # update all keys 62 | tested = test_module.NodeSets.from_file(str(TEST_DATA_DIR / "node_sets_file.json")) 63 | res = tested.update(tested) 64 | 65 | # should return all keys as replaced 66 | assert res == {*self.test_obj} 67 | assert tested.content == self.test_obj.content 68 | 69 | # actually add a new node set 70 | additional = {"test": {"test_property": ["test_value"]}} 71 | res = tested.update(test_module.NodeSets.from_dict(additional)) 72 | expected_content = {**self.test_obj.content, **additional} 73 | 74 | # None of the keys should be replaced 75 | assert res == set() 76 | assert tested.content == expected_content 77 | 78 | with pytest.raises( 79 | BluepySnapError, match=re.escape("Unexpected type: 'str' (expected: 'NodeSets')") 80 | ): 81 | tested.update("") 82 | 83 | def test_contains(self): 84 | assert "Layer23" in self.test_obj 85 | assert "find_me_you_will_not" not in self.test_obj 86 | with pytest.raises(BluepySnapError, match="Unexpected type"): 87 | 42 in self.test_obj 88 | 89 | def test_getitem(self): 90 | assert isinstance(self.test_obj["Layer23"], test_module.NodeSet) 91 | 92 | with pytest.raises(BluepySnapError, match="Undefined node set:"): 93 | self.test_obj["no-such-node-set"] 94 | 95 | def test_iter(self): 96 | expected = set(json.loads((TEST_DATA_DIR / "node_sets_file.json").read_text())) 97 | assert set(self.test_obj) == expected 98 | -------------------------------------------------------------------------------- /bluepysnap/circuit.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """Access to circuit data.""" 19 | import logging 20 | from pathlib import Path 21 | 22 | from cached_property import cached_property 23 | 24 | from bluepysnap.config import CircuitConfig, CircuitConfigStatus 25 | from bluepysnap.edges import Edges 26 | from bluepysnap.exceptions import BluepySnapError 27 | from bluepysnap.node_sets import NodeSets 28 | from bluepysnap.nodes import Nodes 29 | 30 | L = logging.getLogger(__name__) 31 | 32 | 33 | class Circuit: 34 | """Access to circuit data.""" 35 | 36 | def __init__(self, config): 37 | """Initializes a circuit object from a SONATA config file. 38 | 39 | Args: 40 | config (str): Path to a SONATA config file. 41 | 42 | Returns: 43 | Circuit: A Circuit object. 44 | """ 45 | self._circuit_config_path = str(Path(config).absolute()) 46 | self._config = CircuitConfig.from_config(config) 47 | 48 | if self.partial_config: 49 | L.info( 50 | "Loaded PARTIAL circuit config. Functionality may be limited. " 51 | "It is up to the user to be diligent when accessing properties." 52 | ) 53 | 54 | @property 55 | def to_libsonata(self): 56 | """Libsonata instance of the circuit.""" 57 | return self._config.to_libsonata 58 | 59 | @property 60 | def config(self): 61 | """Network config dictionary.""" 62 | return self._config.to_dict() 63 | 64 | def get_node_population_config(self, name): 65 | """Get node population configuration.""" 66 | try: 67 | return self._config.node_populations[name] 68 | except KeyError as e: 69 | raise BluepySnapError(f"Population config not found for node population: {name}") from e 70 | 71 | def get_edge_population_config(self, name): 72 | """Get edge population configuration.""" 73 | try: 74 | return self._config.edge_populations[name] 75 | except KeyError as e: 76 | raise BluepySnapError(f"Population config not found for edge population: {name}") from e 77 | 78 | @cached_property 79 | def node_sets(self): 80 | """Returns the NodeSets object bound to the circuit.""" 81 | path = self.to_libsonata.node_sets_path 82 | return NodeSets.from_file(path) if path else NodeSets.from_dict({}) 83 | 84 | @cached_property 85 | def nodes(self): 86 | """Access to node population(s). See :py:class:`~bluepysnap.nodes.Nodes`.""" 87 | return Nodes(self) 88 | 89 | @cached_property 90 | def edges(self): 91 | """Access to edge population(s). See :py:class:`~bluepysnap.edges.Edges`.""" 92 | return Edges(self) 93 | 94 | @cached_property 95 | def partial_config(self): 96 | """Check partiality of the config.""" 97 | return self._config.status == CircuitConfigStatus.partial 98 | 99 | def __getstate__(self): 100 | """Make Circuits pickle-able, without storing state of caches.""" 101 | return self._circuit_config_path 102 | 103 | def __setstate__(self, state): 104 | """Load from pickle state.""" 105 | self.__init__(state) 106 | -------------------------------------------------------------------------------- /bluepysnap/edges/edge_population_stats.py: -------------------------------------------------------------------------------- 1 | """EdgePopulation stats helper.""" 2 | 3 | import numpy as np 4 | 5 | from bluepysnap.exceptions import BluepySnapError 6 | 7 | 8 | class StatsHelper: 9 | """EdgePopulation stats helper.""" 10 | 11 | def __init__(self, edge_population): 12 | """Initialize StatsHelper with an EdgePopulation instance.""" 13 | self._edge_population = edge_population 14 | 15 | def divergence(self, source, target, by, sample=None): 16 | """`source` -> `target` divergence. 17 | 18 | Calculate the divergence based on number of `"connections"` or `"synapses"` each `source` 19 | cell shares with the cells specified in `target`. 20 | 21 | * `connections`: number of unique target cells each source cell shares a connection with 22 | * `synapses`: number of unique synapses between a source cell and its target cells 23 | 24 | Args: 25 | source (int/CircuitNodeId/CircuitNodeIds/sequence/str/mapping/None): source nodes 26 | target (int/CircuitNodeId/CircuitNodeIds/sequence/str/mapping/None): target nodes 27 | by (str): 'synapses' or 'connections' 28 | sample (int): if specified, sample size for source group 29 | 30 | Returns: 31 | Array with synapse / connection count per each cell from `source` sample 32 | (taking into account only connections to cells in `target`). 33 | """ 34 | by_alternatives = {"synapses", "connections"} 35 | if by not in by_alternatives: 36 | raise BluepySnapError(f"`by` should be one of {by_alternatives}; got: {by}") 37 | 38 | source_sample = self._edge_population.source.ids(source, sample=sample) 39 | 40 | result = {id_: 0 for id_ in source_sample} 41 | if by == "synapses": 42 | connections = self._edge_population.iter_connections( 43 | source_sample, target, return_synapse_count=True 44 | ) 45 | for pre_gid, _, synapse_count in connections: 46 | result[pre_gid] += synapse_count 47 | else: 48 | connections = self._edge_population.iter_connections(source_sample, target) 49 | for pre_gid, _ in connections: 50 | result[pre_gid] += 1 51 | 52 | return np.array(list(result.values())) 53 | 54 | def convergence(self, source, target, by=None, sample=None): 55 | """`source` -> `target` convergence. 56 | 57 | Calculate the convergence based on number of `"connections"` or `"synapses"` each `target` 58 | cell shares with the cells specified in `source`. 59 | 60 | * `connections`: number of unique source cells each target cell shares a connection with 61 | * `synapses`: number of unique synapses between a target cell and its source cells 62 | 63 | Args: 64 | source (int/CircuitNodeId/CircuitNodeIds/sequence/str/mapping/None): source nodes 65 | target (int/CircuitNodeId/CircuitNodeIds/sequence/str/mapping/None): target nodes 66 | by (str): 'synapses' or 'connections' 67 | sample (int): if specified, sample size for target group 68 | 69 | Returns: 70 | Array with synapse / connection count per each cell from `target` sample 71 | (taking into account only connections from cells in `source`). 72 | """ 73 | by_alternatives = {"synapses", "connections"} 74 | if by not in by_alternatives: 75 | raise BluepySnapError(f"`by` should be one of {by_alternatives}; got: {by}") 76 | 77 | target_sample = self._edge_population.target.ids(target, sample=sample) 78 | 79 | result = {id_: 0 for id_ in target_sample} 80 | if by == "synapses": 81 | connections = self._edge_population.iter_connections( 82 | source, target_sample, return_synapse_count=True 83 | ) 84 | for _, post_gid, synapse_count in connections: 85 | result[post_gid] += synapse_count 86 | else: 87 | connections = self._edge_population.iter_connections(source, target_sample) 88 | for _, post_gid in connections: 89 | result[post_gid] += 1 90 | 91 | return np.array(list(result.values())) 92 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | """Module providing utility functions for the tests""" 2 | 3 | import json 4 | import pickle 5 | import shutil 6 | import tempfile 7 | from contextlib import contextmanager 8 | from pathlib import Path 9 | 10 | import numpy.testing as npt 11 | 12 | from bluepysnap.circuit import Circuit 13 | from bluepysnap.nodes import NodePopulation, Nodes 14 | 15 | TEST_DIR = Path(__file__).resolve().parent 16 | TEST_DATA_DIR = TEST_DIR / "data" 17 | 18 | # Pickle size tests often fail when run locally. At the moment, the only thing affecting the 19 | # pickled size is the path length. This is to estimate a safe offset for the size limit 20 | # based on the pickled size of the path of the test data directory. 21 | PICKLED_SIZE_ADJUSTMENT = len(pickle.dumps(str(TEST_DATA_DIR.absolute()))) 22 | 23 | 24 | @contextmanager 25 | def setup_tempdir(cleanup=True): 26 | temp_dir = Path(tempfile.mkdtemp()).resolve() 27 | try: 28 | yield temp_dir 29 | finally: 30 | if cleanup: 31 | shutil.rmtree(temp_dir) 32 | 33 | 34 | @contextmanager 35 | def copy_test_data(config="circuit_config.json"): 36 | """Copies test/data circuit to a temp directory. 37 | 38 | We don't all data every time but considering this is a copy into a temp dir, it should be fine. 39 | Returns: 40 | yields a path to the copy of the config file 41 | """ 42 | with setup_tempdir() as tmp_dir: 43 | # shutil.copytree expects the target to not exist 44 | if tmp_dir.exists(): 45 | tmp_dir.rmdir() 46 | shutil.copytree(str(TEST_DATA_DIR), tmp_dir) 47 | copied_path = Path(tmp_dir) 48 | yield copied_path, copied_path / config 49 | 50 | 51 | @contextmanager 52 | def copy_config(config="circuit_config.json"): 53 | """Copies config to a temp directory. 54 | 55 | Returns: 56 | yields a path to the copy of the config file 57 | """ 58 | with setup_tempdir() as tmp_dir: 59 | output = Path(tmp_dir, config) 60 | shutil.copy(str(TEST_DATA_DIR / config), str(output)) 61 | yield output 62 | 63 | 64 | @contextmanager 65 | def edit_config(config_path): 66 | """Context manager within which you can edit a circuit config. Edits are saved on the context 67 | manager leave. 68 | 69 | Args: 70 | config_path (Path): path to config 71 | 72 | Returns: 73 | Yields a json dict instance of the config_path. This instance will be saved as the config. 74 | """ 75 | with config_path.open("r") as f: 76 | config = json.load(f) 77 | try: 78 | yield config 79 | finally: 80 | with config_path.open("w") as f: 81 | f.write(json.dumps(config)) 82 | 83 | 84 | def create_node_population(filepath, pop_name, circuit=None, node_sets=None, pop_type=None): 85 | """Creates a node population. 86 | Args: 87 | filepath (str): path to the node file. 88 | pop_name (str): population name inside the file. 89 | circuit (Mock/Circuit): either a real circuit or a Mock containing the nodes. 90 | node_sets: (Mock/NodeSets): either a real node_sets or a mocked node_sets. 91 | pop_type (str): optional population type. 92 | Returns: 93 | NodePopulation: return a node population. 94 | """ 95 | node_pop_config = { 96 | "nodes_file": filepath, 97 | "node_types_file": None, 98 | "populations": {pop_name: {}}, 99 | } 100 | 101 | if pop_type is not None: 102 | node_pop_config["populations"][pop_name]["type"] = pop_type 103 | 104 | with copy_test_data() as (_, config_path): 105 | with edit_config(config_path) as config: 106 | config["networks"]["nodes"] = [node_pop_config] 107 | 108 | if circuit is None: 109 | circuit = Circuit(config_path) 110 | 111 | if node_sets is not None: 112 | circuit.node_sets = node_sets 113 | 114 | node_pop = NodePopulation(circuit, pop_name) 115 | circuit.nodes = Nodes(circuit) 116 | return node_pop 117 | 118 | 119 | def assert_array_equal_strict(x, y): 120 | # With numpy >= 1.22.4 it would be possible to specify strict=True. 121 | # The strict parameter ensures that the array data types match. 122 | npt.assert_array_equal(x, y) 123 | assert x.dtype == y.dtype 124 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import numpy.testing as npt 3 | import pytest 4 | 5 | import bluepysnap.utils as test_module 6 | from bluepysnap.circuit_ids_types import CircuitEdgeId, CircuitNodeId 7 | from bluepysnap.exceptions import ( 8 | BluepySnapDeprecationError, 9 | BluepySnapDeprecationWarning, 10 | BluepySnapError, 11 | ) 12 | from bluepysnap.sonata_constants import DYNAMICS_PREFIX 13 | 14 | from utils import TEST_DATA_DIR 15 | 16 | 17 | def test_load_json(): 18 | actual = test_module.load_json(str(TEST_DATA_DIR / "circuit_config.json")) 19 | assert actual["manifest"]["$BASE_DIR"] == "." 20 | 21 | 22 | def test_is_iterable(): 23 | assert test_module.is_iterable([12, 13]) 24 | assert test_module.is_iterable(np.asarray([12, 13])) 25 | assert not test_module.is_iterable(12) 26 | assert not test_module.is_iterable("abc") 27 | 28 | 29 | def test_is_node_id(): 30 | assert test_module.is_node_id(1) 31 | assert test_module.is_node_id(np.int32(1)) 32 | assert test_module.is_node_id(np.uint32(1)) 33 | assert test_module.is_node_id(np.int64(1)) 34 | assert test_module.is_node_id(np.uint64(1)) 35 | assert test_module.is_node_id(CircuitNodeId("default", 1)) 36 | 37 | assert not test_module.is_node_id([1]) 38 | assert not test_module.is_node_id(np.array([1], dtype=np.int64)) 39 | assert not test_module.is_node_id(CircuitEdgeId("default", 1)) 40 | 41 | 42 | def test_ensure_list(): 43 | assert test_module.ensure_list(1) == [1] 44 | assert test_module.ensure_list([1]) == [1] 45 | assert test_module.ensure_list(iter([1])) == [1] 46 | assert test_module.ensure_list((2, 1)) == [2, 1] 47 | assert test_module.ensure_list("abc") == ["abc"] 48 | 49 | 50 | def test_ensure_ids(): 51 | res = test_module.ensure_ids(np.array([1, 2, 3], dtype=np.uint64)) 52 | npt.assert_equal(res, np.array([1, 2, 3], dtype=test_module.IDS_DTYPE)) 53 | npt.assert_equal(res.dtype, test_module.IDS_DTYPE) 54 | 55 | 56 | def test_add_dynamic_prefix(): 57 | assert test_module.add_dynamic_prefix(["a", "b"]) == [ 58 | DYNAMICS_PREFIX + "a", 59 | DYNAMICS_PREFIX + "b", 60 | ] 61 | 62 | 63 | def test_euler2mat(): 64 | pi2 = 0.5 * np.pi 65 | actual = test_module.euler2mat( 66 | [0.0, pi2], # rotation_angle_z 67 | [pi2, 0.0], # rotation_angle_y 68 | [pi2, pi2], # rotation_angle_x 69 | ) 70 | expected = np.array( 71 | [ 72 | [ 73 | [0.0, 0.0, 1.0], 74 | [1.0, 0.0, 0.0], 75 | [0.0, 1.0, 0.0], 76 | ], 77 | [ 78 | [0.0, -1.0, 0.0], 79 | [0.0, 0.0, -1.0], 80 | [1.0, 0.0, 0.0], 81 | ], 82 | ] 83 | ) 84 | npt.assert_almost_equal(actual, expected) 85 | 86 | with pytest.raises(BluepySnapError): 87 | test_module.euler2mat([pi2, pi2], [pi2, pi2], [pi2]) # ax|y|z not of same size 88 | 89 | 90 | def test_quaternion2mat(): 91 | actual = test_module.quaternion2mat( 92 | [1, 1, 1], 93 | [ 94 | 1, 95 | 0, 96 | 0, 97 | ], 98 | [0, 1, 0], 99 | [0, 0, 1], 100 | ) 101 | expected = np.array( 102 | [ 103 | [ 104 | [1.0, 0.0, 0.0], 105 | [0.0, 0.0, -1.0], 106 | [0.0, 1.0, 0.0], 107 | ], 108 | [ 109 | [0.0, 0.0, 1.0], 110 | [0.0, 1.0, 0.0], 111 | [-1.0, 0.0, 0.0], 112 | ], 113 | [ 114 | [0.0, -1.0, 0.0], 115 | [1.0, 0.0, 0.0], 116 | [0.0, 0.0, 1.0], 117 | ], 118 | ] 119 | ) 120 | npt.assert_almost_equal(actual, expected) 121 | 122 | 123 | class TestDeprecate: 124 | def test_fail(self): 125 | with pytest.raises(BluepySnapDeprecationError): 126 | test_module.Deprecate.fail("something") 127 | 128 | def test_warning(self): 129 | import warnings 130 | 131 | with warnings.catch_warnings(record=True) as w: 132 | test_module.Deprecate.warn("something") 133 | 134 | # Verify some things 135 | assert len(w) == 1 136 | assert issubclass(w[-1].category, BluepySnapDeprecationWarning) 137 | assert "something" in str(w[-1].message) 138 | -------------------------------------------------------------------------------- /tests/test_neuron_models.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from unittest.mock import Mock 3 | 4 | import h5py 5 | import libsonata 6 | import numpy as np 7 | import pytest 8 | 9 | import bluepysnap.neuron_models as test_module 10 | from bluepysnap.circuit import Circuit, CircuitConfig 11 | from bluepysnap.circuit_ids_types import CircuitNodeId 12 | from bluepysnap.exceptions import BluepySnapError 13 | from bluepysnap.sonata_constants import Node 14 | 15 | from utils import TEST_DATA_DIR, copy_test_data, create_node_population, edit_config 16 | 17 | 18 | def test_invalid_model_type(): 19 | """test that model type, other than 'biophysical' throws an error""" 20 | with copy_test_data() as (_, config_copy_path): 21 | config = CircuitConfig.from_config(config_copy_path).to_dict() 22 | nodes_file = config["networks"]["nodes"][0]["nodes_file"] 23 | with h5py.File(nodes_file, "r+") as h5f: 24 | grp_name = "nodes/default/0/model_type" 25 | data = h5f[grp_name][:] 26 | del h5f[grp_name] 27 | h5f.create_dataset( 28 | grp_name, data=["virtual"] * data.shape[0], dtype=h5py.string_dtype() 29 | ) 30 | nodes = create_node_population(nodes_file, "default") 31 | with pytest.raises(BluepySnapError) as e: 32 | test_module.NeuronModelsHelper(Mock(), nodes) 33 | assert "biophysical node population" in e.value.args[0] 34 | 35 | 36 | def test_get_invalid_node_id(): 37 | nodes = create_node_population(str(TEST_DATA_DIR / "nodes.h5"), "default") 38 | test_obj = test_module.NeuronModelsHelper(nodes._properties, nodes) 39 | 40 | with pytest.raises(BluepySnapError) as e: 41 | test_obj.get_filepath("1") 42 | assert "node_id must be a int or a CircuitNodeId" in e.value.args[0] 43 | 44 | 45 | def test_get_filepath_biophysical(): 46 | nodes = create_node_population(str(TEST_DATA_DIR / "nodes.h5"), "default") 47 | test_obj = test_module.NeuronModelsHelper(nodes._properties, nodes) 48 | 49 | node_id = 0 50 | assert nodes.get(node_id, properties=Node.MODEL_TEMPLATE) == "hoc:small_bio-A" 51 | actual = test_obj.get_filepath(node_id) 52 | expected = Path(nodes._properties.biophysical_neuron_models_dir, "small_bio-A.hoc") 53 | assert actual == expected 54 | 55 | actual = test_obj.get_filepath(np.int64(node_id)) 56 | assert actual == expected 57 | actual = test_obj.get_filepath(np.uint64(node_id)) 58 | assert actual == expected 59 | actual = test_obj.get_filepath(np.int32(node_id)) 60 | assert actual == expected 61 | actual = test_obj.get_filepath(np.uint32(node_id)) 62 | assert actual == expected 63 | 64 | node_id = CircuitNodeId("default", 0) 65 | assert nodes.get(node_id, properties=Node.MODEL_TEMPLATE) == "hoc:small_bio-A" 66 | actual = test_obj.get_filepath(node_id) 67 | assert actual == expected 68 | 69 | node_id = CircuitNodeId("default", 2) 70 | assert nodes.get(node_id, properties=Node.MODEL_TEMPLATE) == "hoc:small_bio-C" 71 | actual = test_obj.get_filepath(node_id) 72 | expected = Path(nodes._properties.biophysical_neuron_models_dir, "small_bio-C.hoc") 73 | assert actual == expected 74 | 75 | 76 | def test_absolute_biophysical_dir(): 77 | with copy_test_data() as (circuit_dir, circuit_config): 78 | neuron_dir = circuit_dir / "biophysical_neuron_models" 79 | with h5py.File(str(circuit_dir / "nodes.h5"), "r+") as h5: 80 | template = [t.decode().split(":") for t in h5["nodes/default/0/model_template"]] 81 | template = [t[0] + ":" + str(neuron_dir / t[1]) for t in template] 82 | h5["nodes/default/0/model_template"][...] = template 83 | 84 | nodes = Circuit(circuit_config).nodes["default"] 85 | test_obj = test_module.NeuronModelsHelper(nodes._properties, nodes) 86 | 87 | for i, t in enumerate(template): 88 | assert str(test_obj.get_filepath(i)) == ".".join(t.split(":")[::-1]) 89 | 90 | 91 | def test_no_biophysical_dir(): 92 | with copy_test_data() as (_, circuit_config): 93 | with edit_config(circuit_config) as config: 94 | del config["components"]["biophysical_neuron_models_dir"] 95 | 96 | with pytest.raises( 97 | libsonata.SonataError, 98 | match="Node population .* is defined as 'biophysical' but does not define 'biophysical_neuron_models_dir'", 99 | ): 100 | Circuit(circuit_config).nodes["default"] 101 | -------------------------------------------------------------------------------- /tests/test_circuit.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | 4 | import pandas as pd 5 | import pytest 6 | from libsonata import SonataError 7 | 8 | import bluepysnap.circuit as test_module 9 | from bluepysnap.edges import EdgePopulation, Edges 10 | from bluepysnap.exceptions import BluepySnapError 11 | from bluepysnap.nodes import NodePopulation, Nodes 12 | 13 | from utils import PICKLED_SIZE_ADJUSTMENT, TEST_DATA_DIR, copy_test_data, edit_config 14 | 15 | 16 | def test_all(): 17 | circuit = test_module.Circuit(str(TEST_DATA_DIR / "circuit_config.json")) 18 | assert circuit.config["networks"]["nodes"][0] == { 19 | "nodes_file": str(TEST_DATA_DIR / "nodes.h5"), 20 | "populations": { 21 | "default": {"type": "biophysical"}, 22 | "default2": {"type": "biophysical", "spatial_segment_index_dir": "path/to/node/dir"}, 23 | }, 24 | } 25 | assert isinstance(circuit.nodes, Nodes) 26 | assert isinstance(circuit.edges, Edges) 27 | assert list(circuit.edges) == ["default", "default2"] 28 | assert isinstance(circuit.edges["default"], EdgePopulation) 29 | assert isinstance(circuit.edges["default2"], EdgePopulation) 30 | assert sorted(list(circuit.nodes)) == ["default", "default2"] 31 | assert isinstance(circuit.nodes["default"], NodePopulation) 32 | assert isinstance(circuit.nodes["default2"], NodePopulation) 33 | assert sorted(circuit.node_sets) == sorted( 34 | json.loads((TEST_DATA_DIR / "node_sets.json").read_text()) 35 | ) 36 | 37 | fake_pop = "fake" 38 | with pytest.raises( 39 | BluepySnapError, match=f"Population config not found for node population: {fake_pop}" 40 | ): 41 | circuit.get_node_population_config(fake_pop) 42 | with pytest.raises( 43 | BluepySnapError, match=f"Population config not found for edge population: {fake_pop}" 44 | ): 45 | circuit.get_edge_population_config(fake_pop) 46 | 47 | 48 | def test_duplicate_node_populations(): 49 | with copy_test_data() as (_, config_path): 50 | with edit_config(config_path) as config: 51 | config["networks"]["nodes"].append(config["networks"]["nodes"][0]) 52 | 53 | match = "Duplicate population|Population default is declared twice" 54 | with pytest.raises(SonataError, match=match): 55 | test_module.Circuit(config_path) 56 | 57 | 58 | def test_duplicate_edge_populations(): 59 | with copy_test_data() as (_, config_path): 60 | with edit_config(config_path) as config: 61 | config["networks"]["edges"].append(config["networks"]["edges"][0]) 62 | 63 | match = "Duplicate population|Population default is declared twice" 64 | with pytest.raises(SonataError, match=match): 65 | test_module.Circuit(config_path) 66 | 67 | 68 | def test_no_node_set(): 69 | with copy_test_data() as (_, config_path): 70 | with edit_config(config_path) as config: 71 | config.pop("node_sets_file") 72 | circuit = test_module.Circuit(config_path) 73 | assert circuit.node_sets.content == {} 74 | 75 | 76 | def test_integration(): 77 | circuit = test_module.Circuit(str(TEST_DATA_DIR / "circuit_config.json")) 78 | node_ids = circuit.nodes.ids({"mtype": ["L6_Y", "L2_X"]}) 79 | edge_ids = circuit.edges.afferent_edges(node_ids) 80 | edge_props = circuit.edges.get(edge_ids, properties=["syn_weight", "delay"]) 81 | edge_reduced = edge_ids.limit(2) 82 | edge_props = pd.concat(df for _, df in edge_props) 83 | edge_props_reduced = edge_props.loc[edge_reduced] 84 | assert edge_props_reduced["syn_weight"].tolist() == [1, 1] 85 | 86 | 87 | def test_pickle(tmp_path): 88 | circuit = test_module.Circuit(TEST_DATA_DIR / "circuit_config.json") 89 | 90 | pickle_path = tmp_path / "pickle.pkl" 91 | with open(pickle_path, "wb") as fd: 92 | pickle.dump(circuit, fd) 93 | 94 | with open(pickle_path, "rb") as fd: 95 | circuit = pickle.load(fd) 96 | 97 | assert pickle_path.stat().st_size < 60 + PICKLED_SIZE_ADJUSTMENT 98 | assert list(circuit.edges) == ["default", "default2"] 99 | 100 | 101 | def test_empty_manifest(tmp_path): 102 | path = tmp_path / "circuit_config.json" 103 | with open(path, "w") as fd: 104 | json.dump( 105 | { 106 | "manifest": {}, 107 | "networks": { 108 | "nodes": {}, 109 | "edges": {}, 110 | }, 111 | }, 112 | fd, 113 | ) 114 | 115 | test_module.Circuit(path) 116 | -------------------------------------------------------------------------------- /bluepysnap/schemas/definitions/node.yaml: -------------------------------------------------------------------------------- 1 | title: Node file definitions 2 | description: schema definitions that apply to all node files 3 | $node_file_defs: 4 | nodes_file_root: 5 | required: 6 | - nodes 7 | properties: 8 | nodes: 9 | minProperties: 1 10 | type: object 11 | patternProperties: 12 | # "" is used as a wild card for population name 13 | "": 14 | type: object 15 | required: 16 | - "0" 17 | - node_type_id 18 | properties: 19 | node_type_id: 20 | $ref: "#/$typedefs/int64" 21 | "0": 22 | type: object 23 | properties: 24 | dynamics_params: 25 | type: object 26 | required: 27 | - threshold_current 28 | - holding_current 29 | properties: 30 | AIS_scaler: 31 | $ref: "#/$typedefs/float32" 32 | holding_current: 33 | $ref: "#/$typedefs/float32" 34 | input_resistance: 35 | $ref: "#/$typedefs/float32" 36 | threshold_current: 37 | $ref: "#/$typedefs/float32" 38 | 39 | x: 40 | $ref: "#/$typedefs/float32" 41 | y: 42 | $ref: "#/$typedefs/float32" 43 | z: 44 | $ref: "#/$typedefs/float32" 45 | 46 | orientation_w: 47 | $ref: "#/$typedefs/float32" 48 | orientation_x: 49 | $ref: "#/$typedefs/float32" 50 | orientation_y: 51 | $ref: "#/$typedefs/float32" 52 | orientation_z: 53 | $ref: "#/$typedefs/float32" 54 | 55 | rotation_angle_xaxis: 56 | $ref: "#/$typedefs/float32" 57 | rotation_angle_yaxis: 58 | $ref: "#/$typedefs/float32" 59 | rotation_angle_zaxis: 60 | $ref: "#/$typedefs/float32" 61 | 62 | etype: 63 | $ref: "#/$typedefs/utf8" 64 | exc-mini_frequency: 65 | $ref: "#/$typedefs/float32" 66 | hemisphere: 67 | $ref: "#/$typedefs/utf8" 68 | inh-mini_frequency: 69 | $ref: "#/$typedefs/float32" 70 | layer: 71 | $ref: "#/$typedefs/utf8" 72 | me_combo: 73 | $ref: "#/$typedefs/utf8" 74 | model_template: 75 | $ref: "#/$typedefs/utf8" 76 | model_type: 77 | $ref: "#/$typedefs/utf8" 78 | morphology: 79 | $ref: "#/$typedefs/utf8" 80 | morph_class: 81 | $ref: "#/$typedefs/utf8" 82 | mtype: 83 | $ref: "#/$typedefs/utf8" 84 | radius: 85 | $ref: "#/$typedefs/float32" 86 | region: 87 | $ref: "#/$typedefs/utf8" 88 | section_id: 89 | $ref: "#/$typedefs/uint32" 90 | segment_id: 91 | $ref: "#/$typedefs/uint32" 92 | synapse_class: 93 | $ref: "#/$typedefs/utf8" 94 | type: 95 | $ref: "#/$typedefs/int32" 96 | 97 | "@library": 98 | type: object 99 | patternProperties: 100 | # "" is used as a wild card for field name 101 | "": 102 | $ref: "#/$typedefs/utf8" 103 | 104 | # model_type: vasculature 105 | start_x: 106 | $ref: "#/$typedefs/float32" 107 | start_y: 108 | $ref: "#/$typedefs/float32" 109 | start_z: 110 | $ref: "#/$typedefs/float32" 111 | end_x: 112 | $ref: "#/$typedefs/float32" 113 | end_y: 114 | $ref: "#/$typedefs/float32" 115 | end_z: 116 | $ref: "#/$typedefs/float32" 117 | start_diameter: 118 | $ref: "#/$typedefs/float32" 119 | end_diameter: 120 | $ref: "#/$typedefs/float32" 121 | start_node: 122 | $ref: "#/$typedefs/uint64" 123 | end_node: 124 | $ref: "#/$typedefs/uint64" 125 | -------------------------------------------------------------------------------- /bluepysnap/sonata_constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | """Module including the sonata node and edge namespaces.""" 18 | from bluepysnap.exceptions import BluepySnapError 19 | 20 | DYNAMICS_PREFIX = "@dynamics:" 21 | DEFAULT_NODE_TYPE = "biophysical" 22 | DEFAULT_EDGE_TYPE = "chemical" 23 | 24 | 25 | class ConstContainer: 26 | """Constant Container for snap. 27 | 28 | Notes: 29 | Allows the creation of hierarchical subclasses such as: 30 | 31 | .. code-block:: pycon 32 | 33 | >>> class Container(ConstContainer): 34 | >>> VAR1 = "var1" 35 | >>> class SubContainer(Container): 36 | >>> VAR2 = "var2" 37 | >>> class SubSubContainer(SubContainer): 38 | >>> VAR3 = "var3" 39 | >>> print(SubSubContainer.key_set()) # To know the accessible variable names 40 | {"VAR1", "VAR2", "VAR3"} 41 | >>> for v in SubSubContainer.key_set(): # To get the variable (names, values) 42 | >>> print(v, getattr(SubSubContainer, v)) 43 | """ 44 | 45 | @classmethod 46 | def key_set(cls): 47 | """List all constant members of the class.""" 48 | all_keys = set() 49 | for base in cls.__bases__: 50 | if base is object: 51 | continue 52 | try: 53 | all_keys.update(base.key_set()) 54 | except AttributeError as e: 55 | raise BluepySnapError( 56 | "Container classes must derive from classes implementing key_set method" 57 | ) from e 58 | all_keys.update( 59 | name 60 | for name in vars(cls) 61 | if not name.startswith("_") and name not in ["key_set", "get"] 62 | ) 63 | return all_keys 64 | 65 | @classmethod 66 | def get(cls, const_name): 67 | """Get a constant from a string name.""" 68 | try: 69 | res = getattr(cls, const_name) 70 | except AttributeError as e: 71 | raise BluepySnapError("{cls} does not have a '{const_name}' member") from e 72 | return res 73 | 74 | 75 | class Node(ConstContainer): 76 | """Node property names.""" 77 | 78 | X = "x" #: 79 | Y = "y" #: 80 | Z = "z" #: 81 | 82 | ORIENTATION_W = "orientation_w" #: 83 | ORIENTATION_X = "orientation_x" #: 84 | ORIENTATION_Y = "orientation_y" #: 85 | ORIENTATION_Z = "orientation_z" #: 86 | 87 | ROTATION_ANGLE_X = "rotation_angle_xaxis" #: 88 | ROTATION_ANGLE_Y = "rotation_angle_yaxis" #: 89 | ROTATION_ANGLE_Z = "rotation_angle_zaxis" #: 90 | 91 | MORPHOLOGY = "morphology" #: 92 | 93 | RECENTER = "recenter" #: 94 | 95 | MODEL_TYPE = "model_type" #: 96 | MODEL_TEMPLATE = "model_template" #: 97 | 98 | 99 | class Edge(ConstContainer): 100 | """Edge property names.""" 101 | 102 | SOURCE_NODE_ID = "@source_node" #: 103 | TARGET_NODE_ID = "@target_node" #: 104 | 105 | AXONAL_DELAY = "delay" #: 106 | SYN_WEIGHT = "syn_weight" #: 107 | 108 | POST_SECTION_ID = "afferent_section_id" #: 109 | POST_SECTION_POS = "afferent_section_pos" #: 110 | PRE_SECTION_ID = "efferent_section_id" #: 111 | PRE_SECTION_POS = "efferent_section_pos" #: 112 | 113 | # postsynaptic touch position (in the center of the segment) 114 | POST_X_CENTER = "afferent_center_x" #: 115 | POST_Y_CENTER = "afferent_center_y" #: 116 | POST_Z_CENTER = "afferent_center_z" #: 117 | 118 | # postsynaptic touch position (on the segment surface) 119 | POST_X_SURFACE = "afferent_surface_x" #: 120 | POST_Y_SURFACE = "afferent_surface_y" #: 121 | POST_Z_SURFACE = "afferent_surface_z" #: 122 | 123 | # presynaptic touch position (in the center of the segment) 124 | PRE_X_CENTER = "efferent_center_x" #: 125 | PRE_Y_CENTER = "efferent_center_y" #: 126 | PRE_Z_CENTER = "efferent_center_z" #: 127 | 128 | # presynaptic touch position (on the segment surface) 129 | PRE_X_SURFACE = "efferent_surface_x" #: 130 | PRE_Y_SURFACE = "efferent_surface_y" #: 131 | PRE_Z_SURFACE = "efferent_surface_z" #: 132 | -------------------------------------------------------------------------------- /bluepysnap/node_sets.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """Access to node set data. 19 | 20 | For more information see: 21 | https://github.com/AllenInstitute/sonata/blob/master/docs/SONATA_DEVELOPER_GUIDE.md#node-sets-file 22 | """ 23 | 24 | import copy 25 | import json 26 | 27 | import libsonata 28 | 29 | from bluepysnap import utils 30 | from bluepysnap.exceptions import BluepySnapError 31 | 32 | 33 | class NodeSet: 34 | """Access to single node set.""" 35 | 36 | def __init__(self, node_sets, name): 37 | """Initializes a single node set object. 38 | 39 | Args: 40 | node_sets (libsonata.NodeSets): libsonata NodeSets instance. 41 | name (str): name of the node set. 42 | 43 | Returns: 44 | NodeSet: A NodeSet object. 45 | """ 46 | self._node_sets = node_sets 47 | self._name = name 48 | 49 | def get_ids(self, population, raise_missing_property=True): 50 | """Get the resolved node set as ids.""" 51 | try: 52 | return self._node_sets.materialize(self._name, population).flatten() 53 | except libsonata.SonataError as e: 54 | if not raise_missing_property and "No such attribute" in e.args[0]: 55 | return [] 56 | raise BluepySnapError(*e.args) from e 57 | 58 | 59 | class NodeSets: 60 | """Access to node sets data.""" 61 | 62 | def __init__(self, content, instance): 63 | """Initializes a node set object from a node sets file. 64 | 65 | Args: 66 | content (dict): Node sets as a dictionary. 67 | instance (libsonata.NodeSets): ``libsonata`` node sets instance. 68 | 69 | Returns: 70 | NodeSets: A NodeSets object. 71 | """ 72 | self._content = content 73 | self._instance = instance 74 | 75 | @classmethod 76 | def from_file(cls, filepath): 77 | """Create NodeSets instance from a file.""" 78 | content = utils.load_json(filepath) 79 | instance = libsonata.NodeSets.from_file(filepath) 80 | return cls(content, instance) 81 | 82 | @classmethod 83 | def from_string(cls, content): 84 | """Create NodeSets instance from a JSON string.""" 85 | instance = libsonata.NodeSets(content) 86 | content = json.loads(content) 87 | return cls(content, instance) 88 | 89 | @classmethod 90 | def from_dict(cls, content): 91 | """Create NodeSets instance from a dict.""" 92 | return cls.from_string(json.dumps(content)) 93 | 94 | @property 95 | def content(self): 96 | """Access (a copy of) the node sets contents.""" 97 | return copy.deepcopy(self._content) 98 | 99 | @property 100 | def to_libsonata(self): 101 | """Libsonata instance of the NodeSets.""" 102 | return self._instance 103 | 104 | def update(self, node_sets): 105 | """Update the contents of the node set. 106 | 107 | Args: 108 | node_sets (bluepysnap.NodeSets): The node set to extend this node set with. 109 | 110 | Returns: 111 | set: Names of any overwritten node sets. 112 | """ 113 | if isinstance(node_sets, NodeSets): 114 | overwritten = self._instance.update(node_sets.to_libsonata) 115 | self._content.update(node_sets.content) 116 | return overwritten 117 | 118 | raise BluepySnapError( 119 | f"Unexpected type: '{type(node_sets).__name__}' " 120 | f"(expected: '{self.__class__.__name__}')" 121 | ) 122 | 123 | def __contains__(self, name): 124 | """Check if node set exists.""" 125 | if isinstance(name, str): 126 | return name in self._instance.names 127 | 128 | raise BluepySnapError(f"Unexpected type: '{type(name).__name__}' (expected: 'str')") 129 | 130 | def __getitem__(self, name): 131 | """Return a node set instance for the given node set name.""" 132 | if name not in self: 133 | raise BluepySnapError(f"Undefined node set: '{name}'") 134 | return NodeSet(self._instance, name) 135 | 136 | def __iter__(self): 137 | """Iter through the different node sets names.""" 138 | return iter(self._instance.names) 139 | -------------------------------------------------------------------------------- /bluepysnap/morph.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """Morphology access.""" 19 | 20 | from pathlib import Path 21 | 22 | import morph_tool.transform as transformations 23 | import morphio 24 | import numpy as np 25 | 26 | from bluepysnap.exceptions import BluepySnapError 27 | from bluepysnap.sonata_constants import Node 28 | from bluepysnap.utils import is_node_id 29 | 30 | EXTENSIONS_MAPPING = { 31 | "asc": "neurolucida-asc", 32 | "h5": "h5v1", 33 | } 34 | 35 | 36 | class MorphHelper: 37 | """Collection of morphology-related methods.""" 38 | 39 | def __init__(self, morph_dir, population, alternate_morphologies=None): 40 | """Initializes a MorphHelper object from a directory path and a NodePopulation object. 41 | 42 | Args: 43 | morph_dir (str): Path to the directory containing the node morphologies. 44 | population (NodePopulation): NodePopulation object used to query the nodes. 45 | alternate_morphologies (dict): Dictionary containing paths to alternate morphologies. 46 | 47 | Returns: 48 | MorphHelper: A MorphHelper object. 49 | """ 50 | self._morph_dir = morph_dir or "" 51 | self._alternate_morphologies = alternate_morphologies or {} 52 | self._population = population 53 | 54 | def _get_morphology_base(self, extension): 55 | """Get morphology base path; this will be a directory unless it's a morphology container.""" 56 | if extension == "swc": 57 | if not self._morph_dir: 58 | raise BluepySnapError("'morphologies_dir' is not defined in config") 59 | return self._morph_dir 60 | 61 | alternate_key = EXTENSIONS_MAPPING.get(extension) 62 | if not alternate_key: 63 | raise BluepySnapError(f"Unsupported extension: {extension}") 64 | 65 | morph_dir = self._alternate_morphologies.get(alternate_key) 66 | if not morph_dir: 67 | raise BluepySnapError(f"'{alternate_key}' is not defined in 'alternate_morphologies'") 68 | 69 | return morph_dir 70 | 71 | def get_morphology_dir(self, extension="swc"): 72 | """Return morphology directory based on a given extension.""" 73 | morph_dir = self._get_morphology_base(extension) 74 | 75 | if extension == "h5" and Path(morph_dir).is_file(): 76 | raise BluepySnapError( 77 | f"'{morph_dir}' is a morphology container, so a directory does not exist" 78 | ) 79 | 80 | return morph_dir 81 | 82 | def get_name(self, node_id): 83 | """Get the morphology name for a `node_id`.""" 84 | if not is_node_id(node_id): 85 | raise BluepySnapError("node_id must be a int or a CircuitNodeId") 86 | 87 | name = self._population.get(node_id, Node.MORPHOLOGY) 88 | return name 89 | 90 | def get_filepath(self, node_id, extension="swc"): 91 | """Return path to SWC morphology file corresponding to `node_id`. 92 | 93 | Args: 94 | node_id (int/CircuitNodeId): could be a int or CircuitNodeId. 95 | extension (str): expected filetype extension of the morph file. 96 | """ 97 | name = self.get_name(node_id) 98 | 99 | return Path(self.get_morphology_dir(extension), f"{name}.{extension}") 100 | 101 | def get(self, node_id, transform=False, extension="swc"): 102 | """Return MorphIO morphology object corresponding to `node_id`. 103 | 104 | Args: 105 | node_id (int/CircuitNodeId): could be a single int or a CircuitNodeId. 106 | transform (bool): If `transform` is True, rotate and translate morphology points 107 | according to `node_id` position in the circuit. 108 | extension (str): expected filetype extension of the morph file. 109 | """ 110 | collection = morphio.Collection( 111 | self._get_morphology_base(extension), 112 | [ 113 | f".{extension}", 114 | ], 115 | ) 116 | name = self.get_name(node_id) 117 | result = collection.load(name, mutable=True) 118 | 119 | if transform: 120 | T = np.eye(4) 121 | T[:3, :3] = self._population.orientations(node_id) # rotations 122 | T[:3, 3] = self._population.positions(node_id).values # translations 123 | transformations.transform(result, T) 124 | 125 | return result.as_immutable() 126 | -------------------------------------------------------------------------------- /tests/test_partial_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import tempfile 4 | 5 | import pytest 6 | 7 | import bluepysnap.circuit as test_module 8 | from bluepysnap.exceptions import BluepySnapError 9 | from bluepysnap.sonata_constants import Edge, Node 10 | 11 | from utils import TEST_DATA_DIR 12 | 13 | 14 | def test_partial_circuit_config_minimal(): 15 | config = { 16 | "metadata": {"status": "partial"}, 17 | "networks": { 18 | "nodes": [ 19 | { 20 | # missing `nodes_file` rises libsonata exception 21 | "nodes_file": str(TEST_DATA_DIR / "nodes.h5"), 22 | "populations": { 23 | "default": {}, 24 | }, 25 | } 26 | ], 27 | "edges": [ 28 | { 29 | # missing `edges_file` rises libsonata exception 30 | "edges_file": str(TEST_DATA_DIR / "edges.h5"), 31 | "populations": { 32 | "default": {}, 33 | }, 34 | } 35 | ], 36 | }, 37 | } 38 | with tempfile.NamedTemporaryFile(mode="w+") as config_file: 39 | config_file.write(json.dumps(config)) 40 | config_file.flush() 41 | circuit = test_module.Circuit(config_file.name) 42 | 43 | assert circuit.get_node_population_config("default") 44 | assert circuit.nodes 45 | assert circuit.nodes["default"].type == "biophysical" 46 | assert circuit.nodes["default"].size == 3 47 | assert circuit.nodes["default"].source_in_edges() == {"default"} 48 | assert circuit.nodes["default"].target_in_edges() == {"default"} 49 | assert circuit.nodes["default"].config is not None 50 | assert circuit.nodes["default"].property_names is not None 51 | assert circuit.nodes["default"].container_property_names(Node) is not None 52 | assert circuit.nodes["default"].container_property_names(Edge) == [] 53 | assert circuit.nodes["default"].property_values("layer") == {2, 6} 54 | assert circuit.nodes["default"].property_dtypes is not None 55 | assert list(circuit.nodes["default"].ids()) == [0, 1, 2] 56 | assert circuit.nodes["default"].get() is not None 57 | assert circuit.nodes["default"].positions() is not None 58 | assert circuit.nodes["default"].orientations() is not None 59 | assert circuit.nodes["default"].count() == 3 60 | assert circuit.nodes["default"].morph is not None 61 | assert circuit.nodes["default"].models is not None 62 | assert circuit.nodes["default"].h5_filepath == str(TEST_DATA_DIR / "nodes.h5") 63 | assert circuit.nodes["default"]._properties.spatial_segment_index_dir == "" 64 | 65 | assert circuit.nodes.population_names == ["default"] 66 | assert list(circuit.nodes.values()) 67 | 68 | assert [item.id for item in circuit.nodes.ids()] == [0, 1, 2] 69 | 70 | assert circuit.get_edge_population_config("default") 71 | assert circuit.edges 72 | assert circuit.edges["default"].type == "chemical" 73 | assert circuit.edges["default"].size == 4 74 | assert circuit.edges["default"].source is not None 75 | assert circuit.edges["default"].target is not None 76 | assert circuit.edges["default"].config is not None 77 | assert circuit.edges["default"].property_names is not None 78 | assert circuit.edges["default"].property_dtypes is not None 79 | assert circuit.edges["default"].container_property_names(Node) == [] 80 | assert circuit.edges["default"].container_property_names(Edge) is not None 81 | assert list(circuit.edges["default"].ids()) == [0, 1, 2, 3] 82 | assert list(circuit.edges["default"].get([1, 2], None)) == [1, 2] 83 | assert circuit.edges["default"].positions([1, 2], "afferent", "center") is not None 84 | assert list(circuit.edges["default"].afferent_nodes(None)) == [0, 2] 85 | assert list(circuit.edges["default"].efferent_nodes(None)) == [0, 1] 86 | assert list(circuit.edges["default"].pathway_edges(0)) == [1, 2] 87 | assert list(circuit.edges["default"].afferent_edges(0)) == [0] 88 | assert list(circuit.edges["default"].efferent_edges(0)) == [1, 2] 89 | assert circuit.edges["default"].iter_connections() is not None 90 | assert circuit.edges["default"].h5_filepath == str(TEST_DATA_DIR / "edges.h5") 91 | assert circuit.nodes["default"]._properties.spatial_segment_index_dir == "" 92 | 93 | assert circuit.edges.population_names == ["default"] 94 | assert list(circuit.edges.values()) 95 | 96 | assert [item.id for item in circuit.edges.ids()] == [0, 1, 2, 3] 97 | 98 | 99 | def test_partial_circuit_config_log(caplog): 100 | caplog.set_level(logging.INFO) 101 | 102 | config = {"metadata": {"status": "partial"}} 103 | 104 | with tempfile.NamedTemporaryFile(mode="w+") as config_file: 105 | config_file.write(json.dumps(config)) 106 | config_file.flush() 107 | test_module.Circuit(config_file.name) 108 | 109 | assert "Loaded PARTIAL circuit config" in caplog.text 110 | 111 | 112 | def test_partial_circuit_config_empty(): 113 | config = {"metadata": {"status": "partial"}} 114 | with tempfile.NamedTemporaryFile(mode="w+") as config_file: 115 | config_file.write(json.dumps(config)) 116 | config_file.flush() 117 | circuit = test_module.Circuit(config_file.name) 118 | 119 | with pytest.raises(BluepySnapError): 120 | assert circuit.get_node_population_config("default") 121 | with pytest.raises(BluepySnapError): 122 | assert circuit.get_edge_population_config("default") 123 | with pytest.raises(BluepySnapError): 124 | circuit.nodes.ids() 125 | with pytest.raises(BluepySnapError): 126 | circuit.edges.ids() 127 | -------------------------------------------------------------------------------- /tests/data/reporting/create_reports.py: -------------------------------------------------------------------------------- 1 | """Taken from the libsonata lib.""" 2 | 3 | import h5py 4 | import numpy as np 5 | 6 | string_dtype = h5py.special_dtype(vlen=str) 7 | 8 | 9 | def write_spikes(filepath): 10 | population_names = ["default", "default2"] 11 | timestamps_base = (0.3, 0.1, 0.2, 1.3, 0.7) 12 | node_ids_base = (1, 2, 0, 0, 2) 13 | 14 | sorting_type = h5py.enum_dtype({"none": 0, "by_id": 1, "by_time": 2}) 15 | 16 | with h5py.File(filepath, "w") as h5f: 17 | h5f.create_group("spikes") 18 | gpop_spikes = h5f.create_group("/spikes/" + population_names[0]) 19 | gpop_spikes.attrs.create("sorting", data=2, dtype=sorting_type) 20 | timestamps, node_ids = zip(*sorted(zip(timestamps_base, node_ids_base))) 21 | dtimestamps = gpop_spikes.create_dataset("timestamps", data=timestamps, dtype=np.double) 22 | dtimestamps.attrs.create("units", data="ms", dtype=string_dtype) 23 | gpop_spikes.create_dataset("node_ids", data=node_ids, dtype=np.uint64) 24 | 25 | gpop_spikes2 = h5f.create_group("/spikes/" + population_names[1]) 26 | gpop_spikes2.attrs.create("sorting", data=1, dtype=sorting_type) 27 | node_ids, timestamps = zip(*sorted(zip(node_ids_base, timestamps_base))) 28 | dtimestamps2 = gpop_spikes2.create_dataset("timestamps", data=timestamps, dtype=np.double) 29 | dtimestamps2.attrs.create("units", data="ms", dtype=string_dtype) 30 | gpop_spikes2.create_dataset("node_ids", data=node_ids, dtype=np.uint64) 31 | 32 | 33 | def write_soma_report(filepath): 34 | population_names = ["default", "default2"] 35 | node_ids = np.arange(0, 3) 36 | index_pointers = np.arange(0, 4) 37 | element_ids = np.zeros(3) 38 | times = (0.0, 1.0, 0.1) 39 | data = [node_ids + j * 0.1 for j in range(10)] 40 | with h5py.File(filepath, "w") as h5f: 41 | h5f.create_group("report") 42 | gpop_all = h5f.create_group("/report/" + population_names[0]) 43 | ddata = gpop_all.create_dataset("data", data=data, dtype=np.float32) 44 | ddata.attrs.create("units", data="mV", dtype=string_dtype) 45 | gmapping = h5f.create_group("/report/" + population_names[0] + "/mapping") 46 | 47 | dnodes = gmapping.create_dataset("node_ids", data=node_ids, dtype=np.uint64) 48 | dnodes.attrs.create("sorted", data=True, dtype=np.uint8) 49 | gmapping.create_dataset("index_pointers", data=index_pointers, dtype=np.uint64) 50 | gmapping.create_dataset("element_ids", data=element_ids, dtype=np.uint32) 51 | dtimes = gmapping.create_dataset("time", data=times, dtype=np.double) 52 | dtimes.attrs.create("units", data="ms", dtype=string_dtype) 53 | 54 | gpop_soma2 = h5f.create_group("/report/" + population_names[1]) 55 | ddata = gpop_soma2.create_dataset("data", data=data, dtype=np.float32) 56 | ddata.attrs.create("units", data="mV", dtype=string_dtype) 57 | gmapping = h5f.create_group("/report/" + population_names[1] + "/mapping") 58 | 59 | dnodes = gmapping.create_dataset("node_ids", data=node_ids, dtype=np.uint64) 60 | dnodes.attrs.create("sorted", data=True, dtype=np.uint8) 61 | gmapping.create_dataset("index_pointers", data=index_pointers, dtype=np.uint64) 62 | gmapping.create_dataset("element_ids", data=element_ids, dtype=np.uint32) 63 | dtimes = gmapping.create_dataset("time", data=times, dtype=np.double) 64 | dtimes.attrs.create("units", data="ms", dtype=string_dtype) 65 | 66 | 67 | def write_element_report(filepath): 68 | population_names = ["default", "default2"] 69 | node_ids = np.arange(0, 3) 70 | index_pointers = np.arange(0, 8, 2) 71 | index_pointers[-1] = index_pointers[-1] + 1 72 | element_ids = np.array([0, 1] * 3 + [1]) 73 | 74 | times = (0.0, 1, 0.1) 75 | 76 | with h5py.File(filepath, "w") as h5f: 77 | h5f.create_group("report") 78 | gpop_element = h5f.create_group("/report/" + population_names[0]) 79 | d1 = np.array([np.arange(7) + j * 0.1 for j in range(10)]) 80 | ddata = gpop_element.create_dataset("data", data=d1, dtype=np.float32) 81 | ddata.attrs.create("units", data="mV", dtype=string_dtype) 82 | gmapping = h5f.create_group("/report/" + population_names[0] + "/mapping") 83 | 84 | dnodes = gmapping.create_dataset("node_ids", data=node_ids, dtype=np.uint64) 85 | dnodes.attrs.create("sorted", data=True, dtype=np.uint8) 86 | gmapping.create_dataset("index_pointers", data=index_pointers, dtype=np.uint64) 87 | gmapping.create_dataset("element_ids", data=element_ids, dtype=np.uint32) 88 | dtimes = gmapping.create_dataset("time", data=times, dtype=np.double) 89 | dtimes.attrs.create("units", data="ms", dtype=string_dtype) 90 | 91 | gpop_element2 = h5f.create_group("/report/" + population_names[1]) 92 | d1 = np.array([np.arange(7) + j * 0.1 for j in range(10)]) 93 | ddata = gpop_element2.create_dataset("data", data=d1, dtype=np.float32) 94 | ddata.attrs.create("units", data="mR", dtype=string_dtype) 95 | gmapping = h5f.create_group("/report/" + population_names[1] + "/mapping") 96 | 97 | dnodes = gmapping.create_dataset("node_ids", data=node_ids, dtype=np.uint64) 98 | dnodes.attrs.create("sorted", data=True, dtype=np.uint8) 99 | gmapping.create_dataset("index_pointers", data=index_pointers, dtype=np.uint64) 100 | gmapping.create_dataset("element_ids", data=element_ids, dtype=np.uint32) 101 | dtimes = gmapping.create_dataset("time", data=times, dtype=np.double) 102 | dtimes.attrs.create("units", data="mR", dtype=string_dtype) 103 | 104 | 105 | if __name__ == "__main__": 106 | write_spikes("spikes.h5") 107 | write_soma_report("soma_report.h5") 108 | write_element_report("compartment_named.h5") 109 | -------------------------------------------------------------------------------- /tests/test__plotting.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest.mock import Mock, patch 3 | 4 | import pytest 5 | 6 | import bluepysnap._plotting as test_module 7 | from bluepysnap.exceptions import BluepySnapError 8 | from bluepysnap.simulation import Simulation 9 | 10 | from utils import TEST_DATA_DIR 11 | 12 | # NOTE: The tests here are primarily to make sure all the code is covered and deprecation warnings, 13 | # etc. are raised. They don't ensure nor really test the correctness of the functionality. 14 | 15 | 16 | def test__get_pyplot(): 17 | with patch.dict(sys.modules, {"matplotlib.pyplot": None}): 18 | with pytest.raises(ImportError): 19 | test_module._get_pyplot() 20 | 21 | import matplotlib.pyplot 22 | 23 | plt_test = test_module._get_pyplot() 24 | assert plt_test is matplotlib.pyplot 25 | 26 | 27 | def _get_filtered_spike_report(): 28 | return Simulation(TEST_DATA_DIR / "simulation_config.json").spikes.filter() 29 | 30 | 31 | def _get_filtered_frame_report(): 32 | return Simulation(TEST_DATA_DIR / "simulation_config.json").reports["soma_report"].filter() 33 | 34 | 35 | def test_spikes_firing_rate_histogram(): 36 | with pytest.raises(BluepySnapError, match="Invalid time_binsize"): 37 | test_module.spikes_firing_rate_histogram(filtered_report=None, time_binsize=0) 38 | 39 | filtered_report = _get_filtered_spike_report() 40 | ax = test_module.spikes_firing_rate_histogram(filtered_report) 41 | assert ax.xaxis.label.get_text() == "Time [ms]" 42 | assert ax.yaxis.label.get_text() == "PSTH [Hz]" 43 | 44 | ax.xaxis.label.set_text("Fake X") 45 | ax.yaxis.label.set_text("Fake Y") 46 | 47 | ax = test_module.spikes_firing_rate_histogram(filtered_report, ax=ax) 48 | assert ax.xaxis.label.get_text() == "Fake X" 49 | assert ax.yaxis.label.get_text() == "Fake Y" 50 | 51 | 52 | def test_spike_raster(): 53 | filtered_report = _get_filtered_spike_report() 54 | 55 | test_module.spike_raster(filtered_report) 56 | test_module.spike_raster(filtered_report, y_axis="y") 57 | 58 | ax = test_module.spike_raster(filtered_report, y_axis="mtype") 59 | 60 | assert ax.xaxis.label.get_text() == "Time [ms]" 61 | assert ax.yaxis.label.get_text() == "mtype" 62 | 63 | ax.xaxis.label.set_text("Fake X") 64 | ax.yaxis.label.set_text("Fake Y") 65 | 66 | ax = test_module.spike_raster(filtered_report, y_axis="mtype", ax=ax) 67 | assert ax.xaxis.label.get_text() == "Fake X" 68 | assert ax.yaxis.label.get_text() == "Fake Y" 69 | 70 | # Have error raised in node_population get 71 | filtered_report.spike_report["default"].nodes.get = Mock( 72 | side_effect=BluepySnapError("Fake error") 73 | ) 74 | test_module.spike_raster(filtered_report, y_axis="mtype") 75 | 76 | 77 | def test_spikes_isi(): 78 | with pytest.raises(BluepySnapError, match="Invalid binsize"): 79 | test_module.spikes_isi(filtered_report=None, binsize=0) 80 | 81 | filtered_report = _get_filtered_spike_report() 82 | 83 | ax = test_module.spikes_isi(filtered_report) 84 | assert ax.xaxis.label.get_text() == "Interspike interval [ms]" 85 | assert ax.yaxis.label.get_text() == "Bin weight" 86 | 87 | ax = test_module.spikes_isi(filtered_report, use_frequency=True, binsize=42) 88 | assert ax.xaxis.label.get_text() == "Frequency [Hz]" 89 | assert ax.yaxis.label.get_text() == "Bin weight" 90 | 91 | ax.xaxis.label.set_text("Fake X") 92 | ax.yaxis.label.set_text("Fake Y") 93 | ax = test_module.spikes_isi(filtered_report, use_frequency=True, binsize=42, ax=ax) 94 | assert ax.xaxis.label.get_text() == "Fake X" 95 | assert ax.yaxis.label.get_text() == "Fake Y" 96 | 97 | with patch.object(test_module.np, "concatenate", Mock(return_value=[])): 98 | with pytest.raises(BluepySnapError, match="No data to display"): 99 | test_module.spikes_isi(filtered_report) 100 | 101 | 102 | def test_spikes_firing_animation(tmp_path): 103 | with pytest.raises(BluepySnapError, match="Fake is not a valid axis"): 104 | test_module.spikes_firing_animation(filtered_report=None, x_axis="Fake") 105 | 106 | with pytest.raises(BluepySnapError, match="Fake is not a valid axis"): 107 | test_module.spikes_firing_animation(filtered_report=None, y_axis="Fake") 108 | 109 | filtered_report = _get_filtered_spike_report() 110 | anim, ax = test_module.spikes_firing_animation(filtered_report, dt=0.2) 111 | assert ax.title.get_text() == "time = 0.1ms" 112 | 113 | # convert to video to have `update_animation` called 114 | anim.save(tmp_path / "test.gif") 115 | 116 | ax.title.set_text("Fake Title") 117 | anim, ax = test_module.spikes_firing_animation(filtered_report, dt=0.2, ax=ax) 118 | assert ax.title.get_text() == "Fake Title" 119 | anim.save(tmp_path / "test.gif") 120 | 121 | # Have error raised in node_population get 122 | filtered_report.spike_report["default"].nodes.get = Mock( 123 | side_effect=BluepySnapError("Fake error") 124 | ) 125 | 126 | anim, _ = test_module.spikes_firing_animation(filtered_report, dt=0.2) 127 | anim.save(tmp_path / "test.gif") 128 | 129 | 130 | def test_frame_trace(): 131 | with pytest.raises(BluepySnapError, match="Unknown plot_type Fake."): 132 | test_module.frame_trace(filtered_report=None, plot_type="Fake", ax="also fake") 133 | 134 | filtered_report = _get_filtered_frame_report() 135 | test_module.frame_trace(filtered_report) 136 | ax = test_module.frame_trace(filtered_report, plot_type="all") 137 | 138 | assert ax.xaxis.label.get_text() == "Time [ms]" 139 | assert ax.yaxis.label.get_text() == "Voltage [mV]" 140 | 141 | ax.xaxis.label.set_text("Fake X") 142 | ax.yaxis.label.set_text("Fake Y") 143 | 144 | ax = test_module.frame_trace(filtered_report, plot_type="all", ax=ax) 145 | 146 | assert ax.xaxis.label.get_text() == "Fake X" 147 | assert ax.yaxis.label.get_text() == "Fake Y" 148 | -------------------------------------------------------------------------------- /bluepysnap/simulation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | """Simulation access.""" 18 | 19 | import warnings 20 | from pathlib import Path 21 | 22 | from cached_property import cached_property 23 | 24 | from bluepysnap.config import SimulationConfig 25 | from bluepysnap.exceptions import BluepySnapError 26 | from bluepysnap.input import get_simulation_inputs 27 | from bluepysnap.node_sets import NodeSets 28 | 29 | 30 | def _collect_frame_reports(sim): 31 | """Collect the different frame reports.""" 32 | res = {} 33 | for name in sim.to_libsonata.list_report_names: 34 | report = sim.to_libsonata.report(name) 35 | report_type = report.sections.name 36 | if report_type == "all" or report.type.name == "lfp": 37 | from bluepysnap.frame_report import CompartmentReport 38 | 39 | cls = CompartmentReport 40 | elif report_type == "soma": 41 | from bluepysnap.frame_report import SomaReport 42 | 43 | cls = SomaReport 44 | else: 45 | raise BluepySnapError(f"Report {name}: format {report_type} not yet supported.") 46 | 47 | res[name] = cls(sim, name) 48 | return res 49 | 50 | 51 | def _warn_on_overwritten_node_sets(overwritten, print_max=10): 52 | """Helper function to warn and print overwritten nodesets.""" 53 | if (n := len(overwritten)) > 0: 54 | names = ", ".join(list(overwritten)[:print_max]) + (", ..." if n > print_max else "") 55 | warnings.warn( 56 | f"Simulation node sets overwrite {n} node set(s) in Circuit node sets: {names}", 57 | RuntimeWarning, 58 | ) 59 | 60 | 61 | class Simulation: 62 | """Access to Simulation data.""" 63 | 64 | def __init__(self, config): 65 | """Initializes a simulation object from a SONATA simulation config file. 66 | 67 | Args: 68 | config (str): Path to a SONATA simulation config file. 69 | 70 | Returns: 71 | Simulation: A Simulation object. 72 | """ 73 | self._simulation_config_path = str(Path(config).absolute()) 74 | self._config = SimulationConfig.from_config(config) 75 | 76 | @property 77 | def to_libsonata(self): 78 | """Libsonata instance of the config.""" 79 | return self._config.to_libsonata 80 | 81 | @property 82 | def config(self): 83 | """Simulation config dictionary.""" 84 | return self._config.to_dict() 85 | 86 | @cached_property 87 | def circuit(self): 88 | """Access to the circuit used for the simulation.""" 89 | from bluepysnap.circuit import Circuit 90 | 91 | if not Path(self.to_libsonata.network).is_file(): 92 | raise BluepySnapError(f"'network' file not found: {self.to_libsonata.network}") 93 | return Circuit(self.to_libsonata.network) 94 | 95 | @property 96 | def output(self): 97 | """Access the output section.""" 98 | return self.to_libsonata.output 99 | 100 | @property 101 | def inputs(self): 102 | """Access the inputs section.""" 103 | return get_simulation_inputs(self.to_libsonata) 104 | 105 | @property 106 | def run(self): 107 | """Access to the complete run dictionary for this simulation.""" 108 | return self.to_libsonata.run 109 | 110 | @property 111 | def time_start(self): 112 | """Returns the starting time of the simulation.""" 113 | return 0 114 | 115 | @property 116 | def time_stop(self): 117 | """Returns the stopping time of the simulation.""" 118 | return self.run.tstop 119 | 120 | @property 121 | def dt(self): 122 | """Returns the frequency of reporting in milliseconds.""" 123 | return self.run.dt 124 | 125 | @property 126 | def time_units(self): 127 | """Returns the times unit for this simulation.""" 128 | # Assuming ms at the simulation level 129 | return "ms" 130 | 131 | @property 132 | def conditions(self): 133 | """Access to the conditions dictionary for this simulation.""" 134 | return getattr(self.to_libsonata, "conditions", None) 135 | 136 | @property 137 | def simulator(self): 138 | """Returns the targeted simulator.""" 139 | target_simulator = getattr(self.to_libsonata, "target_simulator", None) 140 | return target_simulator.name if target_simulator is not None else None 141 | 142 | @cached_property 143 | def node_sets(self): 144 | """Returns the NodeSets object bound to the simulation.""" 145 | try: 146 | path = self.circuit.to_libsonata.node_sets_path 147 | except BluepySnapError: # Error raised if circuit can not be instantiated 148 | path = "" 149 | 150 | node_sets = NodeSets.from_file(path) if path else NodeSets.from_dict({}) 151 | 152 | if self.to_libsonata.node_sets_file != path: 153 | overwritten = node_sets.update(NodeSets.from_file(self.to_libsonata.node_sets_file)) 154 | _warn_on_overwritten_node_sets(overwritten) 155 | 156 | return node_sets 157 | 158 | @cached_property 159 | def spikes(self): 160 | """Access to the SpikeReport.""" 161 | from bluepysnap.spike_report import SpikeReport 162 | 163 | return SpikeReport(self) 164 | 165 | @cached_property 166 | def reports(self): 167 | """Access all available FrameReports. 168 | 169 | Notes: 170 | Supported FrameReports are soma and compartment reports. 171 | """ 172 | return _collect_frame_reports(self) 173 | 174 | def __getstate__(self): 175 | """Make Simulations pickle-able, without storing state of caches.""" 176 | return self._simulation_config_path 177 | 178 | def __setstate__(self, state): 179 | """Load from pickle state.""" 180 | self.__init__(state) 181 | -------------------------------------------------------------------------------- /bluepysnap/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """Miscellaneous utilities.""" 19 | 20 | import json 21 | import warnings 22 | from collections.abc import Iterable 23 | 24 | import click 25 | import numpy as np 26 | 27 | from bluepysnap.circuit_ids_types import IDS_DTYPE, CircuitEdgeId, CircuitNodeId 28 | from bluepysnap.exceptions import ( 29 | BluepySnapDeprecationError, 30 | BluepySnapDeprecationWarning, 31 | BluepySnapError, 32 | BluepySnapValidationError, 33 | ) 34 | from bluepysnap.sonata_constants import DYNAMICS_PREFIX 35 | 36 | 37 | class Deprecate: 38 | """Class for the deprecations in BluepySnap.""" 39 | 40 | @staticmethod 41 | def fail(msg=""): 42 | """Raise a deprecation exception.""" 43 | raise BluepySnapDeprecationError(msg) 44 | 45 | @staticmethod 46 | def warn(msg=""): 47 | """Issue a deprecation warning.""" 48 | warnings.warn(msg, BluepySnapDeprecationWarning) 49 | 50 | 51 | def load_json(filepath): 52 | """Load JSON from file.""" 53 | with open(filepath, encoding="utf-8") as f: 54 | return json.load(f) 55 | 56 | 57 | def is_node_id(node_id): 58 | """Check if node_id can be a valid node id. 59 | 60 | Returns: 61 | bool: true is node_id is a int, np.integer or CircuitNodeId. False for anything else. 62 | """ 63 | return isinstance(node_id, (int, np.integer, CircuitNodeId)) 64 | 65 | 66 | def is_iterable(v): 67 | """Check if `v` is any iterable (strings are considered scalar and CircuitNode/EdgeId also).""" 68 | return isinstance(v, Iterable) and not isinstance(v, (str, CircuitNodeId, CircuitEdgeId)) 69 | 70 | 71 | def ensure_list(v): 72 | """Convert iterable / wrap scalar into list (strings are considered scalar).""" 73 | if is_iterable(v): 74 | return list(v) 75 | else: 76 | return [v] 77 | 78 | 79 | def ensure_ids(a): 80 | """Convert a numpy array dtype into IDS_DTYPE. 81 | 82 | This function is here due to the https://github.com/numpy/numpy/issues/15084 numpy issue. 83 | It is quite unsafe to the use uint64 for the ids due to this problem where : 84 | numpy.uint64 + int --> float64 85 | numpy.uint64 += int --> float64 86 | 87 | This function needs to be used everywhere node_ids or edge_ids are returned. 88 | """ 89 | return np.asarray(a, IDS_DTYPE) 90 | 91 | 92 | def add_dynamic_prefix(properties): 93 | """Add the dynamic prefix to a list of properties.""" 94 | return [DYNAMICS_PREFIX + name for name in list(properties)] 95 | 96 | 97 | def euler2mat(az, ay, ax): 98 | """Build 3x3 rotation matrices from az, ay, ax rotation angles (in that order). 99 | 100 | Args: 101 | az: rotation angles around Z (Nx1 NumPy array; radians) 102 | ay: rotation angles around Y (Nx1 NumPy array; radians) 103 | ax: rotation angles around X (Nx1 NumPy array; radians) 104 | 105 | Returns: 106 | List with Nx3x3 rotation matrices corresponding to each of N angle triplets. 107 | 108 | See Also: 109 | https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix (R = X1 * Y2 * Z3) 110 | """ 111 | if len(az) != len(ay) or len(az) != len(ax): 112 | raise BluepySnapError("All angles must have the same length.") 113 | c1, s1 = np.cos(ax), np.sin(ax) 114 | c2, s2 = np.cos(ay), np.sin(ay) 115 | c3, s3 = np.cos(az), np.sin(az) 116 | 117 | mm = np.array( 118 | [ 119 | [c2 * c3, -c2 * s3, s2], 120 | [c1 * s3 + c3 * s1 * s2, c1 * c3 - s1 * s2 * s3, -c2 * s1], 121 | [s1 * s3 - c1 * c3 * s2, c3 * s1 + c1 * s2 * s3, c1 * c2], 122 | ] 123 | ) 124 | 125 | return [mm[..., i] for i in range(len(az))] 126 | 127 | 128 | def quaternion2mat(aqw, aqx, aqy, aqz): 129 | """Build 3x3 rotation matrices from quaternions. 130 | 131 | Args: 132 | aqw: w component of quaternions (Nx1 NumPy array; float) 133 | aqx: x component of quaternions (Nx1 NumPy array; float) 134 | aqy: y component of quaternions (Nx1 NumPy array; float) 135 | aqz: z component of quaternions (Nx1 NumPy array; float) 136 | 137 | Returns: 138 | List with Nx3x3 rotation matrices corresponding to each of N quaternions. 139 | 140 | See Also: 141 | https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation 142 | """ 143 | 144 | def normalize_quaternions(qs): 145 | """Normalize a bunch of quaternions along axis==1. 146 | 147 | Args: 148 | qs: quaternions (Nx4 NumPy array; float) 149 | 150 | Returns: 151 | numpy array of normalized quaternions 152 | """ 153 | return qs / np.sqrt(np.einsum("...i,...i", qs, qs)).reshape(-1, 1) 154 | 155 | aq = np.dstack([np.asarray(aqw), np.asarray(aqx), np.asarray(aqy), np.asarray(aqz)])[0] 156 | aq = normalize_quaternions(aq) 157 | 158 | w = aq[:, 0] 159 | x = aq[:, 1] 160 | y = aq[:, 2] 161 | z = aq[:, 3] 162 | 163 | mm = np.array( 164 | [ 165 | [w * w + x * x - y * y - z * z, 2 * x * y - 2 * w * z, 2 * w * y + 2 * x * z], 166 | [2 * w * z + 2 * x * y, w * w - x * x + y * y - z * z, 2 * y * z - 2 * w * x], 167 | [2 * x * z - 2 * w * y, 2 * w * x + 2 * y * z, w * w - x * x - y * y + z * z], 168 | ] 169 | ) 170 | 171 | return [mm[..., i] for i in range(len(aq))] 172 | 173 | 174 | def print_validation_errors(errors): 175 | """Some fancy errors printing.""" 176 | colors = { 177 | BluepySnapValidationError.WARNING: "yellow", 178 | BluepySnapValidationError.FATAL: "red", 179 | BluepySnapValidationError.INFO: "green", 180 | } 181 | 182 | if not errors: 183 | print(click.style("No Error: Success.", fg=colors[BluepySnapValidationError.INFO])) 184 | 185 | for error in errors: 186 | print(click.style(error.level + ": ", fg=colors[error.level]) + str(error)) 187 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. warning:: 2 | The Blue Brain Project concluded in December 2024, so development has ceased under the BlueBrain GitHub organization. 3 | Future development will take place at: https://github.com/openbraininstitute/snap 4 | 5 | |banner| 6 | 7 | |build_status| |license| |coverage| |docs| |DOI| 8 | 9 | Blue Brain SNAP 10 | =============== 11 | 12 | Blue Brain Simulation and Neural network Analysis Productivity layer (Blue Brain SNAP). 13 | 14 | Blue Brain SNAP is a Python library for accessing BlueBrain circuit models represented in 15 | `SONATA `__ format. 16 | 17 | Installation 18 | ------------ 19 | 20 | Blue Brain SNAP can be installed using ``pip``:: 21 | 22 | pip install bluepysnap 23 | 24 | Usage 25 | ----- 26 | 27 | For a full in-depth usage quide, there's a series of jupyter notebooks available in `doc/source/notebooks `_ subfolder. 28 | 29 | There are two main interface classes provided by Blue Brain SNAP: 30 | 31 | |circuit| corresponds to the *static* structure of a neural network, that is: 32 | 33 | - node positions and properties, 34 | - edge positions and properties, and, 35 | - detailed morphologies. 36 | 37 | |simulation| corresponds to the *dynamic* data for a neural network simulation, including: 38 | 39 | - spike reports, 40 | - soma reports, and, 41 | - compartment reports. 42 | 43 | Most of Blue Brain SNAP methods return `pandas `__ Series or DataFrames, 44 | indexed in a way to facilitate combining data from different sources (that is, by node or edge IDs). 45 | 46 | Among other dependencies, Blue Brain SNAP relies on Blue Brain Project provided libraries: 47 | 48 | - `libsonata `__, for accessing SONATA files 49 | - `MorphIO `__, for accessing detailed morphologies 50 | 51 | Tools 52 | ----- 53 | 54 | Circuit Validation 55 | ~~~~~~~~~~~~~~~~~~ 56 | 57 | Blue Brain SNAP provides a SONATA circuit validator for verifying circuits. 58 | 59 | The validation includes: 60 | 61 | - integrity of the circuit config file. 62 | - existence of the different node/edges files and ``components`` directories. 63 | - presence of the "sonata required" field for node/edges files. 64 | - the correctness of the edge to node population/ids bindings. 65 | - existence of the morphology files for the nodes. 66 | 67 | This functionality is provided by either the cli function: 68 | 69 | .. code-block:: shell 70 | 71 | bluepysnap validate-circuit my/circuit/path/circuit_config.json 72 | 73 | 74 | Or a python free function: 75 | 76 | .. code-block:: python3 77 | 78 | from bluepysnap.circuit_validation import validate 79 | errors = validate("my/circuit/path/circuit_config.json") 80 | 81 | 82 | Simulation Validation 83 | ~~~~~~~~~~~~~~~~~~~~~ 84 | 85 | Similarly to circuit validation, Blue Brain SNAP also provides a SONATA simulation validator for verifying simulation configs. 86 | 87 | Currently, the validator verifies that: 88 | 89 | - all the mandatory fields are present in the config file 90 | - all the properties in the `simulation config specification `__ have correct data types and accepted values 91 | - paths specified in the config exist 92 | - node sets specified in the config exist 93 | - input spike file's node IDs are found in the ``source`` node set 94 | - electrodes file's node IDs are found in the simulation's ``node_set`` (if set) or in non-virtual populations 95 | - neurodamus helpers and variables exist (requires ``neurodamus`` to be available in the environment) 96 | 97 | This functionality is provided by either the cli function: 98 | 99 | .. code-block:: shell 100 | 101 | bluepysnap validate-simulation my/circuit/path/simulation_config.json 102 | 103 | 104 | Or a python free function: 105 | 106 | .. code-block:: python3 107 | 108 | from bluepysnap.simulation_validation import validate 109 | errors = validate("my/circuit/path/simulation_config.json") 110 | 111 | 112 | Acknowledgements 113 | ---------------- 114 | 115 | The development of this software was supported by funding to the Blue Brain Project, a research center of the École polytechnique fédérale de Lausanne (EPFL), from the Swiss government’s ETH Board of the Swiss Federal Institutes of Technology. 116 | 117 | This project/research has received funding from the European Union’s Horizon 2020 Framework Programme for Research and Innovation under the Specific Grant Agreement No. 785907 (Human Brain Project SGA2). 118 | 119 | The Blue Brain Project would like to thank `Dr Eilif Muller `_, the author of the precursor to Blue Brain SNAP, for his invaluable insights and contributions 120 | 121 | License 122 | ------- 123 | 124 | Blue Brain SNAP is licensed under the terms of the GNU Lesser General Public License version 3, 125 | unless noted otherwise, for example, external dependencies. 126 | Refer to `COPYING.LESSER `__ and 127 | `COPYING `__ for details. 128 | 129 | Copyright (c) 2019-2024 Blue Brain Project/EPFL 130 | 131 | This program is free software: you can redistribute it and/or modify 132 | it under the terms of the GNU Lesser General Public License version 3 133 | as published by the Free Software Foundation. 134 | 135 | This program is distributed in the hope that it will be useful, 136 | but WITHOUT ANY WARRANTY; without even the implied warranty of 137 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 138 | GNU Lesser General Public License for more details. 139 | 140 | You should have received a copy of the GNU Lesser General Public License 141 | along with this program. If not, see . 142 | 143 | 144 | .. |build_status| image:: https://github.com/BlueBrain/snap/actions/workflows/run-tox.yml/badge.svg 145 | :alt: Build Status 146 | 147 | .. |license| image:: https://img.shields.io/pypi/l/bluepysnap 148 | :target: https://github.com/BlueBrain/snap/blob/master/COPYING.LESSER 149 | :alt: License 150 | 151 | .. |coverage| image:: https://codecov.io/github/BlueBrain/snap/coverage.svg?branch=master 152 | :target: https://codecov.io/github/BlueBrain/snap?branch=master 153 | :alt: codecov.io 154 | 155 | .. |docs| image:: https://readthedocs.org/projects/bluebrainsnap/badge/?version=latest 156 | :target: https://bluebrainsnap.readthedocs.io/ 157 | :alt: documentation status 158 | 159 | .. |DOI| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.8026852.svg 160 | :target: https://doi.org/10.5281/zenodo.8026852 161 | :alt: DOI 162 | 163 | .. substitutions 164 | .. |banner| image:: doc/source/_images/BlueBrainSNAP.jpg 165 | .. |circuit| replace:: **Circuit** 166 | .. |simulation| replace:: **Simulation** 167 | -------------------------------------------------------------------------------- /bluepysnap/schemas/simulation.yaml: -------------------------------------------------------------------------------- 1 | title: SONATA Simulation Config 2 | description: schema for BBP SONATA simulation config 3 | required: 4 | - run 5 | properties: 6 | version: 7 | type: number 8 | manifest: 9 | type: object 10 | network: 11 | type: string 12 | target_simulator: 13 | type: string 14 | enum: 15 | - "CORENEURON" 16 | - "NEURON" 17 | node_sets_file: 18 | type: string 19 | node_set: 20 | type: string 21 | run: 22 | type: object 23 | required: 24 | - tstop 25 | - dt 26 | - random_seed 27 | properties: 28 | tstop: 29 | type: number 30 | dt: 31 | $ref: "#/$simulation_defs/positive_float" 32 | random_seed: 33 | $ref: "#/$simulation_defs/non_negative_integer" 34 | spike_threshold: 35 | type: integer 36 | integration_method: 37 | type: string 38 | enum: 39 | - "0" 40 | - "1" 41 | - "2" 42 | stimulus_seed: 43 | $ref: "#/$simulation_defs/non_negative_integer" 44 | ionchannel_seed: 45 | $ref: "#/$simulation_defs/non_negative_integer" 46 | minis_seed: 47 | $ref: "#/$simulation_defs/non_negative_integer" 48 | synapse_seed: 49 | $ref: "#/$simulation_defs/non_negative_integer" 50 | electrodes_file: 51 | type: string 52 | output: 53 | type: object 54 | properties: 55 | output_dir: 56 | type: string 57 | log_file: 58 | type: string 59 | spikes_file: 60 | type: string 61 | spikes_sort_order: 62 | type: string 63 | enum: 64 | - "by_id" 65 | - "by_time" 66 | - "none" 67 | conditions: 68 | type: object 69 | properties: 70 | celsius: 71 | type: number 72 | v_init: 73 | type: number 74 | spike_location: 75 | type: string 76 | enum: 77 | - "AIS" 78 | - "soma" 79 | extracellular_calcium: 80 | type: number 81 | randomize_gaba_rise_time: 82 | type: boolean 83 | mechanisms: 84 | type: object 85 | patternProperties: 86 | # "" is used as a wild card for suffix names of mod files 87 | "": 88 | type: object 89 | modifications: 90 | type: object 91 | patternProperties: 92 | # "" is used as a wild card for modification names 93 | "": 94 | type: object 95 | required: 96 | - node_set 97 | - type 98 | properties: 99 | node_set: 100 | type: string 101 | type: 102 | type: string 103 | enum: 104 | - "ConfigureAllSections" 105 | - "TTX" 106 | section_configure: 107 | type: string 108 | # if type == "ConfigureAllSections", section_configure is mandatory 109 | if: 110 | properties: 111 | type: 112 | const: "ConfigureAllSections" 113 | # if type is not required here, too, we get an error that 'section_configure' is required if type is not specified 114 | required: 115 | - type 116 | then: 117 | required: 118 | - section_configure 119 | inputs: 120 | type: object 121 | patternProperties: 122 | # "" is used as a wild card for input name 123 | "": 124 | allOf: 125 | - $ref: "#/$input_defs/modules/linear" 126 | - $ref: "#/$input_defs/modules/relative_linear" 127 | - $ref: "#/$input_defs/modules/pulse" 128 | - $ref: "#/$input_defs/modules/subthreshold" 129 | - $ref: "#/$input_defs/modules/hyperpolarizing" 130 | - $ref: "#/$input_defs/modules/synapse_replay" 131 | - $ref: "#/$input_defs/modules/seclamp" 132 | - $ref: "#/$input_defs/modules/noise" 133 | - $ref: "#/$input_defs/modules/shot_noise" 134 | - $ref: "#/$input_defs/modules/absolute_shot_noise" 135 | - $ref: "#/$input_defs/modules/relative_shot_noise" 136 | - $ref: "#/$input_defs/modules/ornstein_uhlenbeck" 137 | - $ref: "#/$input_defs/modules/relative_ornstein_uhlenbeck" 138 | reports: 139 | type: object 140 | patternProperties: 141 | # "" is used as a wild card for report name 142 | "": 143 | required: 144 | - type 145 | - variable_name 146 | - dt 147 | - start_time 148 | - end_time 149 | properties: 150 | cells: 151 | type: string 152 | sections: 153 | type: string 154 | enum: 155 | - "all" 156 | - "apic" 157 | - "axon" 158 | - "dend" 159 | - "soma" 160 | type: 161 | type: string 162 | enum: 163 | - "compartment" 164 | - "lfp" 165 | - "summation" 166 | - "synapse" 167 | scaling: 168 | type: string 169 | enum: 170 | - "area" 171 | - "none" 172 | compartments: 173 | type: string 174 | enum: 175 | - "all" 176 | - "center" 177 | variable_name: 178 | type: string 179 | unit: 180 | type: string 181 | dt: 182 | $ref: "#/$simulation_defs/positive_float" 183 | start_time: 184 | type: number 185 | end_time: 186 | type: number 187 | file_name: 188 | type: string 189 | enabled: 190 | type: boolean 191 | connection_overrides: 192 | type: array 193 | items: 194 | type: object 195 | required: 196 | - name 197 | - source 198 | - target 199 | properties: 200 | name: 201 | type: string 202 | source: 203 | type: string 204 | target: 205 | type: string 206 | weight: 207 | type: number 208 | spont_minis: 209 | type: number 210 | synapse_configure: 211 | type: string 212 | modoverride: 213 | type: string 214 | synapse_delay_override: 215 | type: number 216 | delay: 217 | type: number 218 | neuromodulation_dtc: 219 | type: number 220 | neuromodulation_strength: 221 | type: number 222 | metadata: 223 | type: object 224 | beta_features: 225 | type: object 226 | # require "run: electrodes_file" if any of the reports is of type "lfp" 227 | if: 228 | # need to require reports here, otherwise 'electrodes_file' is required if no reports defined 229 | required: 230 | - reports 231 | properties: 232 | reports: 233 | patternProperties: 234 | "": 235 | properties: 236 | type: 237 | const: "lfp" 238 | then: 239 | properties: 240 | run: 241 | required: 242 | - electrodes_file 243 | $simulation_defs: 244 | non_negative_integer: 245 | type: integer 246 | minimum: 0 247 | positive_float: 248 | type: number 249 | exclusiveMinimum: 0 250 | -------------------------------------------------------------------------------- /tests/test_simulation.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pickle 4 | import warnings 5 | 6 | import libsonata 7 | import pytest 8 | 9 | import bluepysnap.simulation as test_module 10 | from bluepysnap.exceptions import BluepySnapError 11 | from bluepysnap.frame_report import ( 12 | CompartmentReport, 13 | PopulationCompartmentReport, 14 | PopulationSomaReport, 15 | SomaReport, 16 | ) 17 | from bluepysnap.node_sets import NodeSets 18 | from bluepysnap.spike_report import PopulationSpikeReport, SpikeReport 19 | 20 | from utils import PICKLED_SIZE_ADJUSTMENT, TEST_DATA_DIR, copy_test_data, edit_config 21 | 22 | try: 23 | Run = libsonata._libsonata.Run 24 | Conditions = libsonata._libsonata.Conditions 25 | except AttributeError: 26 | from libsonata._libsonata import SimulationConfig 27 | 28 | Run = SimulationConfig.Run 29 | Conditions = SimulationConfig.Conditions 30 | 31 | 32 | def test__warn_on_overwritten_node_sets(): 33 | # No warnings if no overwritten 34 | with warnings.catch_warnings(): 35 | warnings.simplefilter("error") 36 | test_module._warn_on_overwritten_node_sets(set()) 37 | 38 | with pytest.warns(RuntimeWarning, match="Simulation node sets overwrite 3 .*: a, b, c"): 39 | test_module._warn_on_overwritten_node_sets(["a", "b", "c"]) 40 | 41 | with pytest.warns(RuntimeWarning, match=r"Simulation node sets overwrite 3 .*: a, b, \.\.\."): 42 | test_module._warn_on_overwritten_node_sets(["a", "b", "c"], print_max=2) 43 | 44 | 45 | def test_all(): 46 | simulation = test_module.Simulation(str(TEST_DATA_DIR / "simulation_config.json")) 47 | assert simulation.config["network"] == str(TEST_DATA_DIR / "circuit_config.json") 48 | assert set(simulation.circuit.nodes) == {"default", "default2"} 49 | assert set(simulation.circuit.edges) == {"default", "default2"} 50 | 51 | assert isinstance(simulation.run, Run) 52 | 53 | assert simulation.run.tstop == 1000.0 54 | assert simulation.run.dt == 0.01 55 | assert simulation.run.spike_threshold == -15 56 | assert simulation.run.random_seed == 42 57 | assert simulation.time_start == 0.0 58 | assert simulation.time_stop == 1000.0 59 | assert simulation.dt == 0.01 60 | assert simulation.time_units == "ms" 61 | 62 | assert simulation.simulator == "CORENEURON" 63 | assert isinstance(simulation.conditions, Conditions) 64 | assert simulation.conditions.celsius == 34.0 65 | assert simulation.conditions.v_init == -80 66 | 67 | with pytest.warns(RuntimeWarning, match="Simulation node sets overwrite 1 .* Layer23"): 68 | assert isinstance(simulation.node_sets, NodeSets) 69 | 70 | expected_content = { 71 | **json.loads((TEST_DATA_DIR / "node_sets.json").read_text()), 72 | "Layer23": {"layer": [2, 3]}, 73 | "only_exists_in_simulation": {"node_id": [0, 2]}, 74 | } 75 | assert simulation.node_sets.content == expected_content 76 | 77 | assert isinstance(simulation.inputs, dict) 78 | assert isinstance(simulation.spikes, SpikeReport) 79 | assert isinstance(simulation.spikes["default"], PopulationSpikeReport) 80 | 81 | assert set(simulation.reports) == {"soma_report", "section_report", "lfp_report"} 82 | assert isinstance(simulation.reports["soma_report"], SomaReport) 83 | assert isinstance(simulation.reports["section_report"], CompartmentReport) 84 | assert isinstance(simulation.reports["lfp_report"], CompartmentReport) 85 | 86 | rep = simulation.reports["soma_report"] 87 | assert set(rep.population_names) == {"default", "default2"} 88 | assert isinstance(rep["default"], PopulationSomaReport) 89 | 90 | rep = simulation.reports["section_report"] 91 | assert set(rep.population_names) == {"default", "default2"} 92 | assert isinstance(rep["default"], PopulationCompartmentReport) 93 | 94 | 95 | def test_no_warning_when_shared_node_sets_path(): 96 | with copy_test_data(config="simulation_config.json") as (_, config_path): 97 | circuit = test_module.Simulation(config_path).circuit 98 | circuit_node_sets_path = circuit.to_libsonata.node_sets_path 99 | 100 | # set simulation node set path = circuit node set path 101 | with edit_config(config_path) as config: 102 | config["node_sets_file"] = circuit_node_sets_path 103 | 104 | # Should not raise 105 | with warnings.catch_warnings(): 106 | warnings.simplefilter("error") 107 | test_module.Simulation(config_path).node_sets 108 | 109 | # if node_sets_file is not defined, libsonata should use the same path as the circuit 110 | with edit_config(config_path) as config: 111 | config.pop("node_sets_file") 112 | 113 | # Should not raise either 114 | with warnings.catch_warnings(): 115 | warnings.simplefilter("error") 116 | test_module.Simulation(config_path).node_sets 117 | 118 | 119 | def test_unknown_report(): 120 | with copy_test_data(config="simulation_config.json") as (_, config_path): 121 | with edit_config(config_path) as config: 122 | config["reports"]["soma_report"]["sections"] = "unknown" 123 | 124 | with pytest.raises(libsonata.SonataError, match="Invalid value.*for key 'sections'"): 125 | test_module.Simulation(config_path) 126 | 127 | 128 | def test_nonimplemented_report(): 129 | with copy_test_data(config="simulation_config.json") as (_, config_path): 130 | with edit_config(config_path) as config: 131 | config["reports"]["soma_report"]["sections"] = "axon" 132 | 133 | with pytest.raises( 134 | BluepySnapError, match="Report soma_report: format axon not yet supported" 135 | ): 136 | test_module.Simulation(config_path).reports 137 | 138 | 139 | def test_network_file_not_found(): 140 | with copy_test_data(config="simulation_config.json") as (_, config_path): 141 | with edit_config(config_path) as config: 142 | config.pop("network") 143 | 144 | os.remove(config_path.parent / "circuit_config.json") 145 | simulation = test_module.Simulation(config_path) 146 | 147 | with pytest.raises(BluepySnapError, match="'network' file not found"): 148 | simulation.circuit 149 | 150 | 151 | def test_no_node_set(): 152 | with copy_test_data(config="simulation_config.json") as (_, config_path): 153 | with edit_config(config_path) as config: 154 | config.pop("node_sets_file") 155 | # remove circuit config to prevent libsonata from fetching the path from there 156 | os.remove(config_path.parent / "circuit_config.json") 157 | 158 | simulation = test_module.Simulation(config_path) 159 | assert simulation.node_sets.content == {} 160 | 161 | 162 | def test_pickle(tmp_path): 163 | pickle_path = tmp_path / "pickle.pkl" 164 | simulation = test_module.Simulation(TEST_DATA_DIR / "simulation_config.json") 165 | 166 | # trigger some cached properties, to makes sure they aren't being pickeld 167 | simulation.circuit 168 | 169 | with open(pickle_path, "wb") as fd: 170 | pickle.dump(simulation, fd) 171 | 172 | with open(pickle_path, "rb") as fd: 173 | simulation = pickle.load(fd) 174 | 175 | assert pickle_path.stat().st_size < 70 + PICKLED_SIZE_ADJUSTMENT 176 | assert simulation.dt == 0.01 177 | -------------------------------------------------------------------------------- /bluepysnap/network.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2021, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | """Module containing the Abstract classes for the Network.""" 18 | import abc 19 | 20 | import numpy as np 21 | import pandas as pd 22 | from cached_property import cached_property 23 | 24 | from bluepysnap import utils 25 | from bluepysnap.exceptions import BluepySnapError 26 | 27 | 28 | class NetworkObject(abc.ABC): 29 | """Abstract class for the top level NetworkObjects accessor.""" 30 | 31 | _population_class = None 32 | 33 | def __init__(self, circuit): 34 | """Initialize the top level NetworkObjects accessor.""" 35 | self._circuit = circuit 36 | 37 | def _get_populations(self, cls): 38 | """Collects the different NetworkObjectPopulation and returns them as a dict.""" 39 | return {name: cls(self._circuit, name) for name in self.population_names} 40 | 41 | @cached_property 42 | def _populations(self): 43 | """Cached population dictionary.""" 44 | return self._get_populations(self._population_class) 45 | 46 | @property 47 | @abc.abstractmethod 48 | def population_names(self): 49 | """Should define all sorted NetworkObjects population names from the Circuit.""" 50 | 51 | def keys(self): 52 | """Returns iterator on the NetworkObjectPopulation names. 53 | 54 | Made to simulate the behavior of a dict.keys(). 55 | """ 56 | return (name for name in self.population_names) 57 | 58 | def values(self): 59 | """Returns iterator on the NetworkObjectPopulations. 60 | 61 | Made to simulate the behavior of a dict.values(). 62 | """ 63 | return (self[name] for name in self.population_names) 64 | 65 | def items(self): 66 | """Returns iterator on the tuples (population name, NetworkObjectPopulations). 67 | 68 | Made to simulate the behavior of a dict.items(). 69 | """ 70 | return ((name, self[name]) for name in self.population_names) 71 | 72 | def __getitem__(self, population_name): 73 | """Access the NetworkObjectPopulation corresponding to the population 'population_name'.""" 74 | try: 75 | return self._populations[population_name] 76 | except KeyError as e: 77 | raise BluepySnapError(f"{population_name} not a {self.__class__} population.") from e 78 | 79 | def __iter__(self): 80 | """Allows iteration over the different NetworkObjectPopulation.""" 81 | return iter(self.keys()) 82 | 83 | @cached_property 84 | def size(self): 85 | """Total number of NetworkObject inside the circuit.""" 86 | return sum(pop.size for pop in self.values()) 87 | 88 | @cached_property 89 | def property_names(self): 90 | """Returns all the NetworkObject properties present inside the circuit.""" 91 | return set(prop for pop in self.values() for prop in pop.property_names) 92 | 93 | def _get_ids_from_pop(self, fun_to_apply, returned_ids_cls, sample=None, limit=None): 94 | """Get CircuitIds of class 'returned_ids_cls' for all populations using 'fun_to_apply'. 95 | 96 | Args: 97 | fun_to_apply (function): A function that returns the list of IDs for each population 98 | and the population containing these IDs. 99 | returned_ids_cls (CircuitNodeIds/CircuitEdgeIds): the class for the CircuitIds. 100 | sample (int): If specified, randomly choose ``sample`` number of 101 | IDs from the match result. If the size of the sample is greater than 102 | the size of all the NetworkObjectPopulation then all ids are taken and shuffled. 103 | limit (int): If specified, return the first ``limit`` number of 104 | IDs from the match result. If limit is greater than the size of all the population 105 | then all IDs are returned. 106 | 107 | Returns: 108 | CircuitNodeIds/CircuitEdgeIds: containing the IDs and the populations. 109 | """ 110 | if not self.population_names: 111 | raise BluepySnapError("Cannot create CircuitIds for empty population.") 112 | 113 | str_type = f" 0: 151 | pop_properties = properties_set & pop.property_names 152 | # Since the columns are passed as Series, index cannot be specified directly. 153 | # However, it's a bit more performant than converting the Series to numpy arrays. 154 | pop_df = pd.DataFrame({prop: pop.get(pop_ids, prop) for prop in pop_properties}) 155 | pop_df.index = global_pop_ids.index 156 | 157 | # Sort the columns in the given order 158 | yield name, pop_df[[p for p in properties if p in pop_properties]] 159 | 160 | @abc.abstractmethod 161 | def __getstate__(self): 162 | """Make pickle-able, without storing state of caches.""" 163 | 164 | @abc.abstractmethod 165 | def __setstate__(self, state): 166 | """Load from pickle state.""" 167 | -------------------------------------------------------------------------------- /doc/source/notebooks/01_circuits.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Circuits\n", 8 | "## Introduction\n", 9 | "In this tutorial we cover how to load a SONATA circuit using BlueBrain SNAP and access its properties." 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "## Downloading a circuit\n", 17 | "\n", 18 | "As a preliminary step, we download a Sonata circuit. You can learn more about the scientific aspects of this circuit in this [preprint](https://www.biorxiv.org/content/10.1101/2022.02.28.482273)." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "# This step might take some minutes (large file size)\n", 28 | "\n", 29 | "from urllib.request import urlretrieve\n", 30 | "from pathlib import Path\n", 31 | "from zipfile import ZipFile\n", 32 | "\n", 33 | "url = \"https://zenodo.org/record/6259750/files/thalamus_microcircuit.zip?download=1\"\n", 34 | "extract_dir=\".\"\n", 35 | "\n", 36 | "circuit_path = Path('./sonata')\n", 37 | "if not circuit_path.exists():\n", 38 | " zip_path, _ = urlretrieve(url)\n", 39 | " with ZipFile(zip_path, \"r\") as f:\n", 40 | " f.extractall(extract_dir)" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "We start by importing the `bluepysnap` package:" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 2, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "import bluepysnap" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "## Loading\n", 64 | "In order to load the circuit data, we need the path to the file." 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 3, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "circuit_path = \"sonata/circuit_sonata.json\"\n", 74 | "circuit = bluepysnap.Circuit(circuit_path)" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "## Properties\n", 82 | "Circuits provide access to four properties:\n", 83 | "1. Configuration: `circuit.config`\n", 84 | "2. Node populations: `circuit.nodes`\n", 85 | "3. Edge populations: `circuit.edges`\n", 86 | "4. Node sets: `circuit.node_sets`" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 4, 92 | "metadata": {}, 93 | "outputs": [ 94 | { 95 | "data": { 96 | "text/plain": [ 97 | "{'version': 2,\n", 98 | " 'node_sets_file': '/gpfs/bbp.cscs.ch/home/herttuai/snap/doc/source/notebooks/sonata/networks/nodes/node_sets.json',\n", 99 | " 'networks': {'nodes': [{'nodes_file': '/gpfs/bbp.cscs.ch/home/herttuai/snap/doc/source/notebooks/sonata/networks/nodes/thalamus_neurons/nodes.h5',\n", 100 | " 'populations': {'thalamus_neurons': {'type': 'biophysical',\n", 101 | " 'biophysical_neuron_models_dir': '/gpfs/bbp.cscs.ch/project/proj82/home/iavarone/modelmanagement/20191105/memodels/hoc',\n", 102 | " 'alternate_morphologies': {'neurolucida-asc': '/gpfs/bbp.cscs.ch/project/proj82/home/iavarone/morphology_release/20191031/ascii',\n", 103 | " 'h5v1': '/gpfs/bbp.cscs.ch/project/proj82/home/iavarone/morphology_release/20191031/h5'}}}},\n", 104 | " {'nodes_file': '/gpfs/bbp.cscs.ch/home/herttuai/snap/doc/source/notebooks/sonata/networks/nodes/CorticoThalamic_projections/nodes.h5',\n", 105 | " 'populations': {'CorticoThalamic_projections': {'type': 'virtual'}}},\n", 106 | " {'nodes_file': '/gpfs/bbp.cscs.ch/home/herttuai/snap/doc/source/notebooks/sonata/networks/nodes/MedialLemniscus_projections/nodes.h5',\n", 107 | " 'populations': {'MedialLemniscus_projections': {'type': 'virtual'}}}],\n", 108 | " 'edges': [{'edges_file': '/gpfs/bbp.cscs.ch/home/herttuai/snap/doc/source/notebooks/sonata/networks/edges/thalamus_neurons__thalamus_neurons__chemical/edges.h5',\n", 109 | " 'populations': {'thalamus_neurons__thalamus_neurons__chemical': {'type': 'chemical'}}},\n", 110 | " {'edges_file': '/gpfs/bbp.cscs.ch/home/herttuai/snap/doc/source/notebooks/sonata/networks/edges/thalamus_neurons__thalamus_neurons__electrical_synapse/edges.h5',\n", 111 | " 'populations': {'thalamus_neurons__thalamus_neurons__electrical_synapse': {'type': 'electrical'}}},\n", 112 | " {'edges_file': '/gpfs/bbp.cscs.ch/home/herttuai/snap/doc/source/notebooks/sonata/networks/edges/MedialLemniscus_projections__thalamus_neurons__chemical/edges.h5',\n", 113 | " 'populations': {'MedialLemniscus_projections__thalamus_neurons__chemical': {'type': 'chemical'}}},\n", 114 | " {'edges_file': '/gpfs/bbp.cscs.ch/home/herttuai/snap/doc/source/notebooks/sonata/networks/edges/CorticoThalamic_projections__thalamus_neurons__chemical/edges.h5',\n", 115 | " 'populations': {'CorticoThalamic_projections__thalamus_neurons__chemical': {'type': 'chemical'}}}]}}" 116 | ] 117 | }, 118 | "execution_count": 4, 119 | "metadata": {}, 120 | "output_type": "execute_result" 121 | } 122 | ], 123 | "source": [ 124 | "circuit.config" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 5, 130 | "metadata": {}, 131 | "outputs": [ 132 | { 133 | "data": { 134 | "text/plain": [ 135 | "" 136 | ] 137 | }, 138 | "execution_count": 5, 139 | "metadata": {}, 140 | "output_type": "execute_result" 141 | } 142 | ], 143 | "source": [ 144 | "circuit.nodes" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 6, 150 | "metadata": {}, 151 | "outputs": [ 152 | { 153 | "data": { 154 | "text/plain": [ 155 | "" 156 | ] 157 | }, 158 | "execution_count": 6, 159 | "metadata": {}, 160 | "output_type": "execute_result" 161 | } 162 | ], 163 | "source": [ 164 | "circuit.edges" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 7, 170 | "metadata": {}, 171 | "outputs": [ 172 | { 173 | "data": { 174 | "text/plain": [ 175 | "" 176 | ] 177 | }, 178 | "execution_count": 7, 179 | "metadata": {}, 180 | "output_type": "execute_result" 181 | } 182 | ], 183 | "source": [ 184 | "circuit.node_sets" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "## Conclusion\n", 192 | "Now that we can load circuits and inspect their properties, the following lessons will delve deeper into\n", 193 | "what these contain." 194 | ] 195 | } 196 | ], 197 | "metadata": { 198 | "kernelspec": { 199 | "display_name": "Python 3", 200 | "language": "python", 201 | "name": "python3" 202 | }, 203 | "language_info": { 204 | "codemirror_mode": { 205 | "name": "ipython", 206 | "version": 3 207 | }, 208 | "file_extension": ".py", 209 | "mimetype": "text/x-python", 210 | "name": "python", 211 | "nbconvert_exporter": "python", 212 | "pygments_lexer": "ipython3", 213 | "version": "3.7.4" 214 | } 215 | }, 216 | "nbformat": 4, 217 | "nbformat_minor": 4 218 | } 219 | -------------------------------------------------------------------------------- /bluepysnap/nodes/nodes.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, EPFL/Blue Brain Project 2 | 3 | # This file is part of BlueBrain SNAP library 4 | 5 | # This library is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License version 3.0 as published 7 | # by the Free Software Foundation. 8 | 9 | # This library is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | 14 | # You should have received a copy of the GNU Lesser General Public License 15 | # along with this library; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | """Nodes access.""" 19 | 20 | import numpy as np 21 | 22 | from bluepysnap._doctools import AbstractDocSubstitutionMeta 23 | from bluepysnap.circuit_ids import CircuitNodeIds 24 | from bluepysnap.exceptions import BluepySnapError 25 | from bluepysnap.network import NetworkObject 26 | from bluepysnap.nodes.node_population import NodePopulation 27 | 28 | 29 | class Nodes( 30 | NetworkObject, 31 | metaclass=AbstractDocSubstitutionMeta, 32 | source_word="NetworkObject", 33 | target_word="Node", 34 | ): 35 | """The top level Nodes accessor.""" 36 | 37 | _population_class = NodePopulation 38 | 39 | def __init__(self, circuit): # pylint: disable=useless-super-delegation 40 | """Initialize the top level Nodes accessor.""" 41 | super().__init__(circuit) 42 | 43 | @property 44 | def population_names(self): 45 | """Defines all sorted node population names from the Circuit.""" 46 | return sorted(self._circuit.to_libsonata.node_populations) 47 | 48 | def property_values(self, prop): 49 | """Returns all the values for a given Nodes property.""" 50 | return set( 51 | value 52 | for pop in self.values() 53 | if prop in pop.property_names 54 | for value in pop.property_values(prop) 55 | ) 56 | 57 | def ids(self, group=None, sample=None, limit=None): 58 | """Returns the CircuitNodeIds corresponding to the nodes from ``group``. 59 | 60 | Args: 61 | group (CircuitNodeId/CircuitNodeIds/int/sequence/str/mapping/None): Which IDs will be 62 | returned depends on the type of the ``group`` argument: 63 | 64 | - ``CircuitNodeId``: return the ID in a CircuitNodeIds object if it belongs to 65 | the circuit. 66 | - ``CircuitNodeIds``: return the IDs in a CircuitNodeIds object if they belong to 67 | the circuit. 68 | - ``int``: if the node ID is present in all populations, returns a CircuitNodeIds 69 | object containing the corresponding node ID for all populations. 70 | - ``sequence``: if all the values contained in the sequence are present in all 71 | populations, returns a CircuitNodeIds object containing the corresponding node 72 | IDs for all populations. 73 | - ``str``: use a node set name as input. Returns a CircuitNodeIds object containing 74 | nodes selected by the node set. 75 | - ``mapping``: Returns a CircuitNodeIds object containing nodes matching a 76 | properties filter. 77 | - ``None``: return all node IDs of the circuit in a CircuitNodeIds object. 78 | sample (int): If specified, randomly choose ``sample`` number of 79 | IDs from the match result. If the size of the sample is greater than 80 | the size of all the NodePopulations then all ids are taken and shuffled. 81 | limit (int): If specified, return the first ``limit`` number of 82 | IDs from the match result. If limit is greater than the size of all the populations, 83 | all node IDs are returned. 84 | 85 | Returns: 86 | CircuitNodeIds: returns a CircuitNodeIds containing all the node IDs and the 87 | corresponding populations. All the explicitly requested IDs must be present inside 88 | the circuit. 89 | 90 | Raises: 91 | BluepySnapError: when a population from a CircuitNodeIds is not present in the circuit. 92 | BluepySnapError: when an id query via a int, sequence, or CircuitNodeIds is not present 93 | in the circuit. 94 | 95 | Examples: 96 | The available group parameter values (example with 2 node populations pop1 and pop2): 97 | 98 | >>> nodes = circuit.nodes 99 | >>> nodes.ids(group=None) # returns all CircuitNodeIds from the circuit 100 | >>> node_ids = CircuitNodeIds.from_arrays(["pop1", "pop2"], [1, 3]) 101 | >>> nodes.ids(group=node_ids) # returns ID 1 from pop1 and ID 3 from pop2 102 | >>> nodes.ids(group=0) # returns CircuitNodeIds 0 from pop1 and pop2 103 | >>> nodes.ids(group=[0, 1]) # returns CircuitNodeIds 0 and 1 from pop1 and pop2 104 | >>> nodes.ids(group="node_set_name") # returns CircuitNodeIds matching node set 105 | >>> nodes.ids(group={Node.LAYER: 2}) # returns CircuitNodeIds matching layer==2 106 | >>> nodes.ids(group={Node.LAYER: [2, 3]}) # returns CircuitNodeIds with layer in [2,3] 107 | >>> nodes.ids(group={Node.X: (0, 1)}) # returns CircuitNodeIds with 0 < x < 1 108 | >>> # returns CircuitNodeIds matching one of the queries inside the 'or' list 109 | >>> nodes.ids(group={'$or': [{ Node.LAYER: [2, 3]}, 110 | >>> { Node.X: (0, 1), Node.MTYPE: 'L1_SLAC' }]}) 111 | >>> # returns CircuitNodeIds matching all the queries inside the 'and' list 112 | >>> nodes.ids(group={'$and': [{ Node.LAYER: [2, 3]}, 113 | >>> { Node.X: (0, 1), Node.MTYPE: 'L1_SLAC' }]}) 114 | """ 115 | if isinstance(group, CircuitNodeIds): 116 | diff = np.setdiff1d(group.get_populations(unique=True), self.population_names) 117 | if diff.size != 0: 118 | raise BluepySnapError(f"Population {diff} does not exist in the circuit.") 119 | 120 | fun = lambda x: (x.ids(group, raise_missing_property=False), x.name) 121 | return self._get_ids_from_pop(fun, CircuitNodeIds, sample=sample, limit=limit) 122 | 123 | def get(self, group=None, properties=None): # pylint: disable=arguments-differ 124 | """Node properties by iterating populations. 125 | 126 | Args: 127 | group (CircuitNodeIds/int/sequence/str/mapping/None): Which nodes will have their 128 | properties returned depends on the type of the ``group`` argument: 129 | See :py:class:`~bluepysnap.nodes.Nodes.ids`. 130 | 131 | properties (str/list): If specified, return only the properties in the list. 132 | Otherwise return all properties. 133 | 134 | Returns: 135 | generator: yields tuples of ``(, pandas.DataFrame)``: 136 | - DataFrame indexed by CircuitNodeIds containing the properties from ``properties``. 137 | 138 | Notes: 139 | The NodePopulation.property_names function will give you all the usable properties 140 | for the `properties` argument. 141 | """ 142 | if properties is None: 143 | # not strictly needed, but ensure that the properties are always in the same order 144 | properties = sorted(self.property_names) 145 | return super().get(group, properties) 146 | 147 | def __getstate__(self): 148 | """Make Nodes pickle-able, without storing state of caches.""" 149 | return self._circuit 150 | 151 | def __setstate__(self, state): 152 | """Load from pickle state.""" 153 | self.__init__(state) 154 | -------------------------------------------------------------------------------- /bluepysnap/schemas/definitions/simulation_input.yaml: -------------------------------------------------------------------------------- 1 | title: SONATA Simulation Input definitions 2 | description: schemas for different rules based on the input module 3 | $input_defs: 4 | base: 5 | type: object 6 | required: 7 | - module 8 | - input_type 9 | - delay 10 | - duration 11 | - node_set 12 | properties: 13 | module: 14 | type: string 15 | enum: 16 | - "linear" 17 | - "relative_linear" 18 | - "pulse" 19 | - "subthreshold" 20 | - "hyperpolarizing" 21 | - "synapse_replay" 22 | - "seclamp" 23 | - "noise" 24 | - "shot_noise" 25 | - "relative_shot_noise" 26 | - "absolute_shot_noise" 27 | - "ornstein_uhlenbeck" 28 | - "relative_ornstein_uhlenbeck" 29 | input_type: 30 | type: string 31 | enum: 32 | - "spikes" 33 | - "extracellular_stimulation" 34 | - "current_clamp" 35 | - "voltage_clamp" 36 | - "conductance" 37 | delay: 38 | type: number 39 | duration: 40 | type: number 41 | node_set: 42 | type: string 43 | amp_cv: 44 | type: number 45 | amp_end: 46 | type: number 47 | amp_mean: 48 | type: number 49 | amp_start: 50 | type: number 51 | amp_var: 52 | type: number 53 | decay_time: 54 | type: number 55 | dt: 56 | $ref: "#/$simulation_defs/positive_float" 57 | frequency: 58 | type: number 59 | mean: 60 | type: number 61 | mean_percent: 62 | type: number 63 | percent_end: 64 | type: number 65 | percent_less: 66 | type: integer 67 | percent_start: 68 | type: number 69 | random_seed: 70 | $ref: "#/$simulation_defs/non_negative_integer" 71 | rate: 72 | type: number 73 | reversal: 74 | type: number 75 | rise_time: 76 | type: number 77 | sd_percent: 78 | type: number 79 | series_resistance: 80 | type: number 81 | sigma: 82 | type: number 83 | spike_file: 84 | type: string 85 | tau: 86 | type: number 87 | variance: 88 | type: number 89 | voltage: 90 | type: number 91 | width: 92 | type: number 93 | modules: 94 | linear: 95 | $ref: "#/$input_defs/base" 96 | if: 97 | properties: 98 | module: 99 | const: "linear" 100 | required: 101 | - module 102 | then: 103 | properties: 104 | input_type: 105 | const: "current_clamp" 106 | required: 107 | - amp_start 108 | relative_linear: 109 | $ref: "#/$input_defs/base" 110 | if: 111 | properties: 112 | module: 113 | const: "relative_linear" 114 | required: 115 | - module 116 | then: 117 | properties: 118 | input_type: 119 | const: "current_clamp" 120 | required: 121 | - percent_start 122 | pulse: 123 | $ref: "#/$input_defs/base" 124 | if: 125 | properties: 126 | module: 127 | const: "pulse" 128 | required: 129 | - module 130 | then: 131 | properties: 132 | input_type: 133 | const: "current_clamp" 134 | required: 135 | - amp_start 136 | - width 137 | - frequency 138 | subthreshold: 139 | $ref: "#/$input_defs/base" 140 | if: 141 | properties: 142 | module: 143 | const: "subthreshold" 144 | required: 145 | - module 146 | then: 147 | properties: 148 | input_type: 149 | const: "current_clamp" 150 | required: 151 | - percent_less 152 | hyperpolarizing: 153 | $ref: "#/$input_defs/base" 154 | if: 155 | properties: 156 | module: 157 | const: "hyperpolarizing" 158 | required: 159 | - module 160 | then: 161 | properties: 162 | input_type: 163 | const: "current_clamp" 164 | synapse_replay: 165 | $ref: "#/$input_defs/base" 166 | if: 167 | properties: 168 | module: 169 | const: "synapse_replay" 170 | required: 171 | - module 172 | then: 173 | properties: 174 | input_type: 175 | const: "spikes" 176 | required: 177 | - spike_file 178 | seclamp: 179 | $ref: "#/$input_defs/base" 180 | if: 181 | properties: 182 | module: 183 | const: "seclamp" 184 | required: 185 | - module 186 | then: 187 | properties: 188 | input_type: 189 | const: "voltage_clamp" 190 | required: 191 | - voltage 192 | noise: 193 | $ref: "#/$input_defs/base" 194 | if: 195 | properties: 196 | module: 197 | const: "noise" 198 | required: 199 | - module 200 | then: 201 | properties: 202 | input_type: 203 | const: "current_clamp" 204 | oneOf: 205 | - required: [mean] 206 | - required: [mean_percent] 207 | messages: 208 | oneOf: "either 'mean' or 'mean_percent' is required (not both)" 209 | shot_noise: 210 | $ref: "#/$input_defs/base" 211 | if: 212 | properties: 213 | module: 214 | const: "shot_noise" 215 | required: 216 | - module 217 | then: 218 | properties: 219 | input_type: 220 | enum: 221 | - "current_clamp" 222 | - "conductance" 223 | required: 224 | - rise_time 225 | - decay_time 226 | - rate 227 | - amp_mean 228 | - amp_var 229 | absolute_shot_noise: 230 | $ref: "#/$input_defs/base" 231 | if: 232 | properties: 233 | module: 234 | const: "absolute_shot_noise" 235 | required: 236 | - module 237 | then: 238 | properties: 239 | input_type: 240 | enum: 241 | - "current_clamp" 242 | - "conductance" 243 | required: 244 | - rise_time 245 | - decay_time 246 | - amp_cv 247 | - mean 248 | - sigma 249 | relative_shot_noise: 250 | $ref: "#/$input_defs/base" 251 | if: 252 | properties: 253 | module: 254 | const: "relative_shot_noise" 255 | required: 256 | - module 257 | then: 258 | properties: 259 | input_type: 260 | enum: 261 | - "current_clamp" 262 | - "conductance" 263 | required: 264 | - rise_time 265 | - decay_time 266 | - amp_cv 267 | - mean_percent 268 | - sd_percent 269 | ornstein_uhlenbeck: 270 | $ref: "#/$input_defs/base" 271 | if: 272 | properties: 273 | module: 274 | const: "ornstein_uhlenbeck" 275 | required: 276 | - module 277 | then: 278 | properties: 279 | input_type: 280 | enum: 281 | - "current_clamp" 282 | - "conductance" 283 | required: 284 | - tau 285 | - mean 286 | - sigma 287 | relative_ornstein_uhlenbeck: 288 | $ref: "#/$input_defs/base" 289 | if: 290 | properties: 291 | module: 292 | const: "relative_ornstein_uhlenbeck" 293 | required: 294 | - module 295 | then: 296 | properties: 297 | input_type: 298 | enum: 299 | - "current_clamp" 300 | - "conductance" 301 | required: 302 | - tau 303 | - mean_percent 304 | - sd_percent 305 | -------------------------------------------------------------------------------- /tests/test_full_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | import bluepysnap.config as test_module 7 | from bluepysnap.exceptions import BluepySnapError 8 | 9 | from utils import TEST_DATA_DIR, copy_config, edit_config 10 | 11 | 12 | def parse(path): 13 | with open(path) as fd: 14 | config = json.load(fd) 15 | 16 | return test_module.Parser.parse(config, str(Path(path).parent)) 17 | 18 | 19 | def test_parse(): 20 | actual = parse(str(TEST_DATA_DIR / "circuit_config.json")) 21 | 22 | # check double resolution and '.' works: $COMPONENT_DIR -> $BASE_DIR -> '.' 23 | assert actual["components"]["morphologies_dir"] == str(TEST_DATA_DIR / "morphologies") 24 | 25 | # check resolution and './' works: $NETWORK_DIR -> './' 26 | assert actual["networks"]["nodes"][0]["nodes_file"] == str(TEST_DATA_DIR / "nodes.h5") 27 | 28 | # check resolution of '../' works: $PARENT --> '../' 29 | with copy_config() as config_path: 30 | with edit_config(config_path) as config: 31 | config["manifest"]["$PARENT"] = "../" 32 | config["components"]["other"] = "$PARENT/other" 33 | 34 | actual = parse(config_path) 35 | assert actual["components"]["other"] == str(Path(config_path.parent / "../other").resolve()) 36 | 37 | # check resolution of '../' works in a path outside manifest 38 | with copy_config() as config_path: 39 | with edit_config(config_path) as config: 40 | config["components"]["other"] = "../other" 41 | 42 | actual = parse(config_path) 43 | assert actual["components"]["other"] == str(Path(config_path.parent / "../other").resolve()) 44 | 45 | # check resolution without manifest of '../' works in a path outside 46 | # i.e. : self.manifest contains the configdir even if manifest is not here 47 | with copy_config() as config_path: 48 | with edit_config(config_path) as config: 49 | for k in list(config): 50 | config.pop(k) 51 | config["something"] = "../other" 52 | actual = parse(config_path) 53 | assert actual["something"] == str(Path(config_path.parent / "../other").resolve()) 54 | 55 | # check resolution with multiple slashes 56 | with copy_config() as config_path: 57 | with edit_config(config_path) as config: 58 | config["something"] = "$COMPONENT_DIR/something////else" 59 | actual = parse(config_path) 60 | assert actual["something"] == str(Path(config_path.parent) / "something" / "else") 61 | 62 | # check resolution with $ in a middle of the words 63 | with copy_config() as config_path: 64 | with edit_config(config_path) as config: 65 | config["something"] = "$COMPONENT_DIR/somet$hing/else" 66 | actual = parse(config_path) 67 | assert actual["something"] == str(Path(config_path.parent) / "somet$hing" / "else") 68 | 69 | # check resolution with relative path without "." in the manifest 70 | with copy_config() as config_path: 71 | with edit_config(config_path) as config: 72 | config["manifest"]["$NOPOINT"] = "nopoint" 73 | config["components"]["other"] = "$NOPOINT/other" 74 | actual = parse(config_path) 75 | assert actual["components"]["other"] == str(Path(config_path.parent) / "nopoint" / "other") 76 | 77 | # check resolution for non path objects 78 | with copy_config() as config_path: 79 | with edit_config(config_path) as config: 80 | for k in list(config): 81 | config.pop(k) 82 | config["string"] = "string" 83 | config["int"] = 1 84 | config["double"] = 0.2 85 | # just to check because we use starting with '.' as a special case 86 | config["tricky_double"] = 0.2 87 | config["path"] = "./path" 88 | 89 | actual = parse(config_path) 90 | 91 | # string 92 | assert actual["string"] == "string" 93 | # int 94 | assert actual["int"] == 1 95 | # double 96 | assert actual["double"] == 0.2 97 | assert actual["tricky_double"] == 0.2 98 | # path 99 | assert actual["path"] == str(Path(config_path.parent / "./path").resolve()) 100 | 101 | 102 | def test_bad_manifest(): 103 | # 2 anchors would result in the absolute path of the last one : misleading 104 | with copy_config() as config_path: 105 | with edit_config(config_path) as config: 106 | config["manifest"]["$COMPONENT_DIR"] = "$BASE_DIR/$NETWORK_DIR" 107 | with pytest.raises(BluepySnapError): 108 | parse(config_path) 109 | 110 | # same but not in the manifest 111 | with copy_config() as config_path: 112 | with edit_config(config_path) as config: 113 | config["components"]["other"] = "$COMPONENT_DIR/$BASE_DIR" 114 | with pytest.raises(BluepySnapError): 115 | parse(config_path) 116 | 117 | # manifest value not a string 118 | with copy_config() as config_path: 119 | with edit_config(config_path) as config: 120 | config["manifest"]["$COMPONENT_DIR"] = 42 121 | with pytest.raises(BluepySnapError): 122 | parse(config_path) 123 | 124 | # relative path with an anchor in the middle is not allowed this breaks the purpose of the 125 | # anchors (they are not just generic placeholders) 126 | with copy_config() as config_path: 127 | with edit_config(config_path) as config: 128 | config["components"]["other"] = "something/$COMPONENT_DIR/" 129 | with pytest.raises(BluepySnapError): 130 | parse(config_path) 131 | 132 | # abs path with an anchor in the middle is not allowed 133 | with copy_config() as config_path: 134 | with edit_config(config_path) as config: 135 | config["components"]["other"] = "/something/$COMPONENT_DIR/" 136 | with pytest.raises(BluepySnapError): 137 | parse(config_path) 138 | 139 | # unknown anchor 140 | with copy_config() as config_path: 141 | with edit_config(config_path) as config: 142 | config["components"]["other"] = "$UNKNOWN/something/" 143 | with pytest.raises(KeyError): 144 | parse(config_path) 145 | 146 | 147 | def test_simulation_config(): 148 | actual = test_module.SimulationConfig.from_config( 149 | str(TEST_DATA_DIR / "simulation_config.json") 150 | ).to_dict() 151 | assert actual["target_simulator"] == "CORENEURON" 152 | assert actual["network"] == str(Path(TEST_DATA_DIR / "circuit_config.json").resolve()) 153 | assert actual["mechanisms_dir"] == str( 154 | Path(TEST_DATA_DIR / "../shared_components_mechanisms").resolve() 155 | ) 156 | assert actual["conditions"]["celsius"] == 34.0 157 | assert actual["conditions"]["v_init"] == -80 158 | 159 | 160 | def test__resolve_population_configs(): 161 | node = { 162 | "nodes_file": "nodes_path", 163 | "populations": {"node_pop": {"changed_node": "changed", "added_this": "node_test"}}, 164 | } 165 | 166 | edge = { 167 | "edges_file": "edges_path", 168 | "populations": {"edge_pop": {"changed_edge": "changed", "added_this": "edge_test"}}, 169 | } 170 | 171 | config = { 172 | "components": { 173 | "unchanged": True, 174 | "changed_node": "unchanged", 175 | "changed_edge": "unchanged", 176 | }, 177 | "networks": { 178 | "nodes": [node], 179 | "edges": [edge], 180 | }, 181 | } 182 | expected_node_pop = { 183 | "unchanged": True, 184 | "changed_node": "changed", 185 | "changed_edge": "unchanged", 186 | "added_this": "node_test", 187 | "nodes_file": "nodes_path", 188 | } 189 | expected_edge_pop = { 190 | "unchanged": True, 191 | "changed_node": "unchanged", 192 | "changed_edge": "changed", 193 | "added_this": "edge_test", 194 | "edges_file": "edges_path", 195 | } 196 | expected = { 197 | "nodes": {"node_pop": expected_node_pop}, 198 | "edges": {"edge_pop": expected_edge_pop}, 199 | } 200 | res = test_module.CircuitConfig._resolve_population_configs(config) 201 | 202 | assert res == expected 203 | --------------------------------------------------------------------------------