From 3cf023acf4e9add8775c903ff58da93d67b301df Mon Sep 17 00:00:00 2001 From: danilobenozzo Date: Tue, 25 Feb 2025 19:07:44 +0100 Subject: [PATCH 01/14] spike recorder device --- bsb_neuron/adapter.py | 17 +++++++++++++++- bsb_neuron/devices/__init__.py | 1 + bsb_neuron/devices/spike_recorder.py | 30 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 bsb_neuron/devices/spike_recorder.py diff --git a/bsb_neuron/adapter.py b/bsb_neuron/adapter.py index dc9c929..aae75f4 100644 --- a/bsb_neuron/adapter.py +++ b/bsb_neuron/adapter.py @@ -15,7 +15,7 @@ SimulatorAdapter, report, ) -from neo import AnalogSignal +from neo import AnalogSignal, SpikeTrain if typing.TYPE_CHECKING: from bsb import Simulation @@ -31,6 +31,21 @@ def __init__(self, simulation: "Simulation", result=None): class NeuronResult(SimulationResult): + + def record_spike(self, time_vect, gid_vect, **annotations): + def flush(segment): + if "units" not in annotations.keys(): + annotations["units"] = "ms" + segment.spiketrains.append( + SpikeTrain( + time_vect, + waveforms=gid_vect, + **annotations, + ) + ) + + self.create_recorder(flush) + def record(self, obj, **annotations): from patch import p from quantities import ms diff --git a/bsb_neuron/devices/__init__.py b/bsb_neuron/devices/__init__.py index ce2852e..8ec3122 100644 --- a/bsb_neuron/devices/__init__.py +++ b/bsb_neuron/devices/__init__.py @@ -4,3 +4,4 @@ from .synapse_recorder import SynapseRecorder from .voltage_clamp import VoltageClamp from .voltage_recorder import VoltageRecorder +from .spike_recorder import SpikeRecorder diff --git a/bsb_neuron/devices/spike_recorder.py b/bsb_neuron/devices/spike_recorder.py new file mode 100644 index 0000000..800b391 --- /dev/null +++ b/bsb_neuron/devices/spike_recorder.py @@ -0,0 +1,30 @@ +from bsb import LocationTargetting, config +from patch import p + +from ..device import NeuronDevice + + +@config.node +class SpikeRecorder(NeuronDevice, classmap_entry="spike_recorder"): + locations = config.attr(type=LocationTargetting, default={"strategy": "soma"}) + + def implement(self, adapter, simulation, simdata): + for model, pop in self.targetting.get_targets( + adapter, simulation, simdata + ).items(): + for target in pop: + for location in self.locations.get_locations(target): + self._add_spike_recorder( + simdata.result, + target, + location, + adapter.next_gid, + name=self.name, + cell_type=target.cell_model.name, + cell_id=target.id, + ) + adapter.next_gid += 1 + + def _add_spike_recorder(self, results, cell, location, gid, **annotations): + gid = cell.insert_transmitter(gid, location).gid + results.record_spike(p.parallel.spike_record(gids=gid), **annotations) From c7613a824c7a2a02e3cea118c05462de716a0b45 Mon Sep 17 00:00:00 2001 From: filimarc Date: Wed, 26 Feb 2025 11:07:43 +0100 Subject: [PATCH 02/14] fix: fix implementation of SpikeRecorder | handle cells of the same population in the same SpikeTrain --- bsb_neuron/adapter.py | 9 ++++-- bsb_neuron/devices/__init__.py | 2 +- bsb_neuron/devices/spike_recorder.py | 42 +++++++++++++++++++--------- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/bsb_neuron/adapter.py b/bsb_neuron/adapter.py index aae75f4..11d30df 100644 --- a/bsb_neuron/adapter.py +++ b/bsb_neuron/adapter.py @@ -32,14 +32,17 @@ def __init__(self, simulation: "Simulation", result=None): class NeuronResult(SimulationResult): - def record_spike(self, time_vect, gid_vect, **annotations): + def record_spike(self, time_vect, id_vect, cell_model_id, **annotations): def flush(segment): if "units" not in annotations.keys(): annotations["units"] = "ms" + # time_vect = np.array(time_vect) + # list_id = np.full(len(time_vect), cell_id) segment.spiketrains.append( SpikeTrain( - time_vect, - waveforms=gid_vect, + np.array(time_vect), + gids=np.array(id_vect), + senders=np.array([cell_model_id[gid] for gid in id_vect]), **annotations, ) ) diff --git a/bsb_neuron/devices/__init__.py b/bsb_neuron/devices/__init__.py index 8ec3122..8923c49 100644 --- a/bsb_neuron/devices/__init__.py +++ b/bsb_neuron/devices/__init__.py @@ -1,7 +1,7 @@ from .current_clamp import CurrentClamp from .ion_recorder import IonRecorder from .spike_generator import SpikeGenerator +from .spike_recorder import SpikeRecorder from .synapse_recorder import SynapseRecorder from .voltage_clamp import VoltageClamp from .voltage_recorder import VoltageRecorder -from .spike_recorder import SpikeRecorder diff --git a/bsb_neuron/devices/spike_recorder.py b/bsb_neuron/devices/spike_recorder.py index 800b391..7591072 100644 --- a/bsb_neuron/devices/spike_recorder.py +++ b/bsb_neuron/devices/spike_recorder.py @@ -1,3 +1,4 @@ +import numpy as np from bsb import LocationTargetting, config from patch import p @@ -12,19 +13,34 @@ def implement(self, adapter, simulation, simdata): for model, pop in self.targetting.get_targets( adapter, simulation, simdata ).items(): + spike_times = p.parallel._interpreter.Vector() + neuron_gids = p.parallel._interpreter.Vector() + gids_to_cell = {} for target in pop: - for location in self.locations.get_locations(target): - self._add_spike_recorder( - simdata.result, - target, - location, - adapter.next_gid, - name=self.name, - cell_type=target.cell_model.name, - cell_id=target.id, - ) + print(f"select Cell {target.id} from {model}") + for location in self.locations: + print(f"> processing location {location}") + # Insert a NetCon (if not already present) and retrieve its gid + gid = target.insert_transmitter(adapter.next_gid, (0, 0)).gid + gids_to_cell[gid] = target.id adapter.next_gid += 1 + # Call record_spike() method on selected gid using common spike_times and neuron_gids Vector for + # cells in the same population + spike_times, neuron_gids = p.parallel.spike_record( + gid, spike_times, neuron_gids + ) + # Record a SpikeTrain obj for every model + self._add_spike_recorder( + simdata.result, + spike_times, + neuron_gids, + gids_to_cell, + device=self.name, + t_stop=simulation.duration, + cell_type=target.cell_model.name, + cell_id=target.id, + pop_size=len(pop), + ) - def _add_spike_recorder(self, results, cell, location, gid, **annotations): - gid = cell.insert_transmitter(gid, location).gid - results.record_spike(p.parallel.spike_record(gids=gid), **annotations) + def _add_spike_recorder(self, results, spike_times, gids, cell_dict, **annotations): + results.record_spike(spike_times, gids, cell_dict, **annotations) From 5161d1eef43fdbe66277e3449bc071ca6ebd910d Mon Sep 17 00:00:00 2001 From: filimarc Date: Wed, 26 Feb 2025 11:52:25 +0100 Subject: [PATCH 03/14] fix: let implement to iterate to all the locations provided --- bsb_neuron/adapter.py | 2 -- bsb_neuron/devices/spike_recorder.py | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/bsb_neuron/adapter.py b/bsb_neuron/adapter.py index 11d30df..c276af4 100644 --- a/bsb_neuron/adapter.py +++ b/bsb_neuron/adapter.py @@ -36,8 +36,6 @@ def record_spike(self, time_vect, id_vect, cell_model_id, **annotations): def flush(segment): if "units" not in annotations.keys(): annotations["units"] = "ms" - # time_vect = np.array(time_vect) - # list_id = np.full(len(time_vect), cell_id) segment.spiketrains.append( SpikeTrain( np.array(time_vect), diff --git a/bsb_neuron/devices/spike_recorder.py b/bsb_neuron/devices/spike_recorder.py index 7591072..443247d 100644 --- a/bsb_neuron/devices/spike_recorder.py +++ b/bsb_neuron/devices/spike_recorder.py @@ -17,11 +17,14 @@ def implement(self, adapter, simulation, simdata): neuron_gids = p.parallel._interpreter.Vector() gids_to_cell = {} for target in pop: - print(f"select Cell {target.id} from {model}") - for location in self.locations: - print(f"> processing location {location}") + locations = [ + k + for k, v in target.locations.items() + if v in self.locations.get_locations(target) + ] + for location in locations: # Insert a NetCon (if not already present) and retrieve its gid - gid = target.insert_transmitter(adapter.next_gid, (0, 0)).gid + gid = target.insert_transmitter(adapter.next_gid, location).gid gids_to_cell[gid] = target.id adapter.next_gid += 1 # Call record_spike() method on selected gid using common spike_times and neuron_gids Vector for From ead58cee5bb4815dc00e714c7cda8e257111661a Mon Sep 17 00:00:00 2001 From: filimarc Date: Wed, 26 Feb 2025 16:43:13 +0100 Subject: [PATCH 04/14] refactor: handling of locations --- bsb_neuron/devices/spike_recorder.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bsb_neuron/devices/spike_recorder.py b/bsb_neuron/devices/spike_recorder.py index 443247d..8e36b00 100644 --- a/bsb_neuron/devices/spike_recorder.py +++ b/bsb_neuron/devices/spike_recorder.py @@ -18,11 +18,10 @@ def implement(self, adapter, simulation, simdata): gids_to_cell = {} for target in pop: locations = [ - k - for k, v in target.locations.items() - if v in self.locations.get_locations(target) + location._loc for location in self.locations.get_locations(target) ] for location in locations: + print(f"> Processing Location {location} for cell {target.id}") # Insert a NetCon (if not already present) and retrieve its gid gid = target.insert_transmitter(adapter.next_gid, location).gid gids_to_cell[gid] = target.id From ca3553b7076e1c5ea99754289f8ae51bcb5fa1f7 Mon Sep 17 00:00:00 2001 From: filimarc Date: Thu, 27 Feb 2025 18:38:32 +0100 Subject: [PATCH 05/14] fix: add check if NetCon is already present --- bsb_neuron/devices/spike_recorder.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bsb_neuron/devices/spike_recorder.py b/bsb_neuron/devices/spike_recorder.py index 8e36b00..95d56c2 100644 --- a/bsb_neuron/devices/spike_recorder.py +++ b/bsb_neuron/devices/spike_recorder.py @@ -21,11 +21,15 @@ def implement(self, adapter, simulation, simdata): location._loc for location in self.locations.get_locations(target) ] for location in locations: - print(f"> Processing Location {location} for cell {target.id}") # Insert a NetCon (if not already present) and retrieve its gid - gid = target.insert_transmitter(adapter.next_gid, location).gid + la = target.get_location(location) + if hasattr(la.section, "_transmitter"): + gid = la.section._transmitter.gid + else: + gid = target.insert_transmitter(adapter.next_gid, location).gid + adapter.next_gid += 1 gids_to_cell[gid] = target.id - adapter.next_gid += 1 + # Call record_spike() method on selected gid using common spike_times and neuron_gids Vector for # cells in the same population spike_times, neuron_gids = p.parallel.spike_record( From 08f4df7bd31d9cdc8517ae7b3013d8d4d7fa9827 Mon Sep 17 00:00:00 2001 From: filimarc Date: Fri, 28 Feb 2025 14:56:08 +0100 Subject: [PATCH 06/14] test: add unittest for spike_recorder fix empty SpikeTrain allocation --- tests/test_devices.py | 125 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tests/test_devices.py diff --git a/tests/test_devices.py b/tests/test_devices.py new file mode 100644 index 0000000..57d8660 --- /dev/null +++ b/tests/test_devices.py @@ -0,0 +1,125 @@ +import importlib +import unittest +from copy import copy + +import numpy as np +from bsb.services import MPI +from bsb.simulation import get_simulation_adapter +from bsb_test import ( + ConfigFixture, + MorphologiesFixture, + NetworkFixture, + NumpyTestCase, + RandomStorageFixture, +) +from patch import p + +from bsb_neuron.cell import ArborizedModel +from bsb_neuron.connection import TransceiverModel + + +def neuron_installed(): + return importlib.util.find_spec("neuron") + + +@unittest.skipIf(not neuron_installed(), "NEURON is not installed") +class TestSpikeRecorder( + RandomStorageFixture, + ConfigFixture, + NetworkFixture, + MorphologiesFixture, + NumpyTestCase, + unittest.TestCase, + config="complete", + morpho_filters=["3branch"], + engine_name="hdf5", +): + + def setUp(self): + super().setUp() + p.parallel.gid_clear() + self.network.network.chunk_size = [10, 10, 10] + for ct in self.network.cell_types.values(): + ct.spatial.morphologies = ["3branch"] + hh_soma = { + "cable_types": { + "soma": { + "cable": {"Ra": 10, "cm": 1}, + "mechanisms": {"pas": {}, "hh": {}}, + }, + "dendrite": { + "cable": {"Ra": 2, "cm": 5}, + "mechanisms": {"pas": {}, "hh": {}}, + }, + }, + "synapse_types": {"ExpSyn": {}}, + } + self.network.simulations.add( + "test", + simulator="neuron", + duration=50, + resolution=0.1, + temperature=32, + cell_models=dict( + A=ArborizedModel(model=hh_soma), + B=ArborizedModel(model=hh_soma), + C=ArborizedModel(model=hh_soma), + ), + connection_models=dict( + A_to_B=TransceiverModel(synapses=[dict(synapse="ExpSyn")]), + B_to_C=TransceiverModel(synapses=[dict(synapse="ExpSyn")]), + ), + devices=dict( + spike_detector=dict( + device="spike_recorder", + targetting={ + "strategy": "by_id", + "ids": {"A": [0], "B": [2], "C": [1, 3]}, + }, + ), + first_current=dict( + device="current_clamp", + targetting={ + "strategy": "by_id", + "ids": {"A": [0], "C": [1]}, + }, + locations={"strategy": "soma"}, + before=5, + amplitude=50, + duration=1, + ), + second_current=dict( + device="current_clamp", + targetting={ + "strategy": "cell_model", + "cell_models": {"C"}, + }, + locations={"strategy": "soma"}, + before=35, + amplitude=50, + duration=1, + ), + ), + ) + self.network.compile() + + def test_simple_stimulus(self): + "Test that spike_recorder correctly records stimulus" + + result = self.network.run_simulation("test") + self.assertEqual( + len(result.spiketrains), + 3, + "No event should be recorded for B cells but a SpikeTrain should still be allocated", + ) + control_data = [ + ["A", [0], np.array([5.1], dtype=np.float64)], + ["B", [], np.array([], dtype=np.float64)], + ["C", [1, 1, 3], np.array([5.1, 35.1, 35.1], dtype=np.float64)], + ] + for elem, spike_train in enumerate(result.spiketrains): + self.assertEqual(control_data[elem][0], spike_train.annotations["cell_type"]) + self.assertEqual( + control_data[elem][1], list(spike_train.annotations["senders"]) + ) + self.assertClose(control_data[elem][2], spike_train.magnitude) From 90e5f2181d37c1f7a275a72b51d52b88b5b29be6 Mon Sep 17 00:00:00 2001 From: filimarc Date: Fri, 28 Feb 2025 14:56:48 +0100 Subject: [PATCH 07/14] fix: empy SpikeTrain allocation --- bsb_neuron/adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bsb_neuron/adapter.py b/bsb_neuron/adapter.py index c276af4..4fab139 100644 --- a/bsb_neuron/adapter.py +++ b/bsb_neuron/adapter.py @@ -38,8 +38,8 @@ def flush(segment): annotations["units"] = "ms" segment.spiketrains.append( SpikeTrain( - np.array(time_vect), - gids=np.array(id_vect), + np.array(time_vect) if len(time_vect) > 0 else [], + gids=np.array(id_vect) if len(id_vect) > 0 else [], senders=np.array([cell_model_id[gid] for gid in id_vect]), **annotations, ) From 7ae17e57fc9fa33938fa3b4df9d492a7f0e09dd9 Mon Sep 17 00:00:00 2001 From: filimarc Date: Fri, 28 Feb 2025 15:49:14 +0100 Subject: [PATCH 08/14] fix: add delay to NetCons --- bsb_neuron/devices/spike_recorder.py | 4 +++- tests/test_devices.py | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/bsb_neuron/devices/spike_recorder.py b/bsb_neuron/devices/spike_recorder.py index 95d56c2..ab00cbd 100644 --- a/bsb_neuron/devices/spike_recorder.py +++ b/bsb_neuron/devices/spike_recorder.py @@ -26,7 +26,9 @@ def implement(self, adapter, simulation, simdata): if hasattr(la.section, "_transmitter"): gid = la.section._transmitter.gid else: - gid = target.insert_transmitter(adapter.next_gid, location).gid + gid = target.insert_transmitter( + adapter.next_gid, location, delay=1, weight=0.0004 + ).gid adapter.next_gid += 1 gids_to_cell[gid] = target.id diff --git a/tests/test_devices.py b/tests/test_devices.py index 57d8660..521a522 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -47,7 +47,7 @@ def setUp(self): "cable": {"Ra": 10, "cm": 1}, "mechanisms": {"pas": {}, "hh": {}}, }, - "dendrite": { + "dendrites": { "cable": {"Ra": 2, "cm": 5}, "mechanisms": {"pas": {}, "hh": {}}, }, @@ -66,8 +66,12 @@ def setUp(self): C=ArborizedModel(model=hh_soma), ), connection_models=dict( - A_to_B=TransceiverModel(synapses=[dict(synapse="ExpSyn")]), - B_to_C=TransceiverModel(synapses=[dict(synapse="ExpSyn")]), + A_to_B=TransceiverModel( + synapses=[dict(synapse="ExpSyn", weight=0.001, delay=1)] + ), + B_to_C=TransceiverModel( + synapses=[dict(synapse="ExpSyn", weight=0.001, delay=1)] + ), ), devices=dict( spike_detector=dict( From 256cae8824798f16e0664bd1a83e53d1762db4a2 Mon Sep 17 00:00:00 2001 From: filimarc Date: Sat, 1 Mar 2025 16:48:18 +0100 Subject: [PATCH 09/14] fix: fix test for mpi --- tests/test_devices.py | 53 +++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/tests/test_devices.py b/tests/test_devices.py index 521a522..f114392 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -66,9 +66,6 @@ def setUp(self): C=ArborizedModel(model=hh_soma), ), connection_models=dict( - A_to_B=TransceiverModel( - synapses=[dict(synapse="ExpSyn", weight=0.001, delay=1)] - ), B_to_C=TransceiverModel( synapses=[dict(synapse="ExpSyn", weight=0.001, delay=1)] ), @@ -77,15 +74,15 @@ def setUp(self): spike_detector=dict( device="spike_recorder", targetting={ - "strategy": "by_id", - "ids": {"A": [0], "B": [2], "C": [1, 3]}, + "strategy": "cell_model", + "cell_models": ["A", "B", "C"], }, ), first_current=dict( device="current_clamp", targetting={ - "strategy": "by_id", - "ids": {"A": [0], "C": [1]}, + "strategy": "cell_model", + "cell_models": ["A", "C"], }, locations={"strategy": "soma"}, before=5, @@ -96,7 +93,7 @@ def setUp(self): device="current_clamp", targetting={ "strategy": "cell_model", - "cell_models": {"C"}, + "cell_models": ["C"], }, locations={"strategy": "soma"}, before=35, @@ -110,17 +107,45 @@ def setUp(self): def test_simple_stimulus(self): "Test that spike_recorder correctly records stimulus" - result = self.network.run_simulation("test") + # result = self.network.run_simulation("test") + sim = self.network.simulations.test + adapter = get_simulation_adapter(sim.simulator) + simdata = adapter.prepare(sim) + results = adapter.run(sim) + result = adapter.collect(sim, simdata, results[0]) self.assertEqual( len(result.spiketrains), 3, "No event should be recorded for B cells but a SpikeTrain should still be allocated", ) - control_data = [ - ["A", [0], np.array([5.1], dtype=np.float64)], - ["B", [], np.array([], dtype=np.float64)], - ["C", [1, 1, 3], np.array([5.1, 35.1, 35.1], dtype=np.float64)], - ] + # control_data = [ + # ["A", [0], np.array([5.1], dtype=np.float64)], + # ["B", [], np.array([], dtype=np.float64)], + # ["C", [1, 1, 3], np.array([5.1, 35.1, 35.1], dtype=np.float64)], + # ] + control_data = [] + for cm in sim.cell_models: + appo = [] + pop = [cell.id for cell in simdata.populations[sim.cell_models[cm]]] + pop_len = len(pop) + appo.append(cm) + if cm == "A": + appo.append(list(pop)) + appo.append(np.full(pop_len, [5.1], dtype=np.float64)) + if cm == "B": + appo.append([]) + appo.append(np.array([], dtype=np.float64)) + if cm == "C": + appo.append([*pop, *pop]) + tot_times = np.concatenate( + ( + np.full(pop_len, [5.1], dtype=np.float64), + np.full(pop_len, [35.1], dtype=np.float64), + ) + ) + appo.append(tot_times) + control_data.append(appo) + for elem, spike_train in enumerate(result.spiketrains): self.assertEqual(control_data[elem][0], spike_train.annotations["cell_type"]) self.assertEqual( From 25ce7b9a24e2e0d8ec16d88ed1c89d9564cde4db Mon Sep 17 00:00:00 2001 From: filimarc Date: Mon, 3 Mar 2025 14:13:58 +0100 Subject: [PATCH 10/14] fix: update black 25 and isort 6 --- .github/workflows/black.yml | 2 +- .github/workflows/isort.yml | 2 +- .pre-commit-config.yaml | 4 ++-- pyproject.toml | 4 ++-- tests/test_devices.py | 8 +------- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 0b86fd4..ea8f99b 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -11,4 +11,4 @@ jobs: - uses: psf/black@stable with: options: "--check --verbose" - version: "24.1.1" + version: "25.1.0" diff --git a/.github/workflows/isort.yml b/.github/workflows/isort.yml index 6e61454..e1a2c71 100644 --- a/.github/workflows/isort.yml +++ b/.github/workflows/isort.yml @@ -17,4 +17,4 @@ jobs: sudo apt install openmpi-bin libopenmpi-dev # Install dependencies for proper 1st/2nd/3rd party import sorting - run: pip install -e .[parallel] - - uses: isort/isort-action@v1.1.0 + - uses: isort/isort-action@master diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33153b1..76525ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,11 +3,11 @@ default_install_hook_types: - commit-msg repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.1.1 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 6.0.0 hooks: - id: isort name: isort (python) diff --git a/pyproject.toml b/pyproject.toml index cf13240..53c9319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,8 +34,8 @@ dev = [ "build~=1.0", "twine~=4.0", "pre-commit~=3.5", - "black~=24.1.1", - "isort~=5.12", + "black~=25.1.0", + "isort~=6.0", "snakeviz~=2.1", "bump-my-version~=0.24" ] diff --git a/tests/test_devices.py b/tests/test_devices.py index f114392..a9d097e 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -105,9 +105,8 @@ def setUp(self): self.network.compile() def test_simple_stimulus(self): - "Test that spike_recorder correctly records stimulus" + """Test that spike_recorder correctly records stimulus""" - # result = self.network.run_simulation("test") sim = self.network.simulations.test adapter = get_simulation_adapter(sim.simulator) simdata = adapter.prepare(sim) @@ -118,11 +117,6 @@ def test_simple_stimulus(self): 3, "No event should be recorded for B cells but a SpikeTrain should still be allocated", ) - # control_data = [ - # ["A", [0], np.array([5.1], dtype=np.float64)], - # ["B", [], np.array([], dtype=np.float64)], - # ["C", [1, 1, 3], np.array([5.1, 35.1, 35.1], dtype=np.float64)], - # ] control_data = [] for cm in sim.cell_models: appo = [] From bfe8e0357ff6bb5e54ad3e63e78a6d69eabd5239 Mon Sep 17 00:00:00 2001 From: filimarc Date: Tue, 11 Mar 2025 14:41:26 +0100 Subject: [PATCH 11/14] refactor: Now also location info are stored| Add option to merge the spiketrains --- bsb_neuron/adapter.py | 28 +++++-- bsb_neuron/devices/spike_recorder.py | 115 ++++++++++++++++++--------- tests/test_devices.py | 41 +++++++++- 3 files changed, 136 insertions(+), 48 deletions(-) diff --git a/bsb_neuron/adapter.py b/bsb_neuron/adapter.py index 4fab139..7435d8c 100644 --- a/bsb_neuron/adapter.py +++ b/bsb_neuron/adapter.py @@ -32,18 +32,30 @@ def __init__(self, simulation: "Simulation", result=None): class NeuronResult(SimulationResult): - def record_spike(self, time_vect, id_vect, cell_model_id, **annotations): + def record_spike( + self, time_vect, id_vect, cell_model_id, loc_label_id, locs_ids, **annotations + ): def flush(segment): if "units" not in annotations.keys(): annotations["units"] = "ms" - segment.spiketrains.append( - SpikeTrain( - np.array(time_vect) if len(time_vect) > 0 else [], - gids=np.array(id_vect) if len(id_vect) > 0 else [], - senders=np.array([cell_model_id[gid] for gid in id_vect]), - **annotations, + if cell_model_id: + segment.spiketrains.append( + SpikeTrain( + np.array(time_vect) if len(time_vect) > 0 else [], + gids=np.array(id_vect) if len(id_vect) > 0 else [], + senders=np.array([cell_model_id[gid] for gid in id_vect]), + labels=np.array([loc_label_id[gid] for gid in id_vect]), + loc=np.array([locs_ids[gid] for gid in id_vect]), + **annotations, + ) + ) + else: + segment.spiketrains.append( + SpikeTrain( + np.array(time_vect) if len(time_vect) > 0 else [], + **annotations, + ) ) - ) self.create_recorder(flush) diff --git a/bsb_neuron/devices/spike_recorder.py b/bsb_neuron/devices/spike_recorder.py index ab00cbd..1de6788 100644 --- a/bsb_neuron/devices/spike_recorder.py +++ b/bsb_neuron/devices/spike_recorder.py @@ -8,47 +8,84 @@ @config.node class SpikeRecorder(NeuronDevice, classmap_entry="spike_recorder"): locations = config.attr(type=LocationTargetting, default={"strategy": "soma"}) + join_population = config.attr(type=bool, default=False) + + def check_netcon(self, adapter, target, location): + # Insert a NetCon (if not already present) and retrieve its gid + if hasattr(location.section, "_transmitter"): + gid = location.section._transmitter.gid + else: + gid = target.insert_transmitter( + adapter.next_gid, location._loc, delay=1, weight=0.0004 + ).gid + adapter.next_gid += 1 + return gid def implement(self, adapter, simulation, simdata): for model, pop in self.targetting.get_targets( adapter, simulation, simdata ).items(): - spike_times = p.parallel._interpreter.Vector() - neuron_gids = p.parallel._interpreter.Vector() - gids_to_cell = {} - for target in pop: - locations = [ - location._loc for location in self.locations.get_locations(target) - ] - for location in locations: - # Insert a NetCon (if not already present) and retrieve its gid - la = target.get_location(location) - if hasattr(la.section, "_transmitter"): - gid = la.section._transmitter.gid - else: - gid = target.insert_transmitter( - adapter.next_gid, location, delay=1, weight=0.0004 - ).gid - adapter.next_gid += 1 - gids_to_cell[gid] = target.id - - # Call record_spike() method on selected gid using common spike_times and neuron_gids Vector for - # cells in the same population - spike_times, neuron_gids = p.parallel.spike_record( - gid, spike_times, neuron_gids - ) - # Record a SpikeTrain obj for every model - self._add_spike_recorder( - simdata.result, - spike_times, - neuron_gids, - gids_to_cell, - device=self.name, - t_stop=simulation.duration, - cell_type=target.cell_model.name, - cell_id=target.id, - pop_size=len(pop), - ) - - def _add_spike_recorder(self, results, spike_times, gids, cell_dict, **annotations): - results.record_spike(spike_times, gids, cell_dict, **annotations) + if self.join_population: + spike_times = p.parallel._interpreter.Vector() + neuron_gids = p.parallel._interpreter.Vector() + gids_to_cell = {} + gids_to_labels = {} + gids_to_locs = {} + for target in pop: + for location in self.locations.get_locations(target): + + gid = self.check_netcon(adapter, target, location) + # Call record_spike() method on selected gid using common spike_times and neuron_gids Vector for + # cells in the same population + gids_to_cell[gid] = target.id + gids_to_labels[gid] = location.section.labels + gids_to_locs[gid] = location._loc + spike_times, neuron_gids = p.parallel.spike_record( + gid, spike_times, neuron_gids + ) + # If join_population is selected Record a SpikeTrain obj for every model + self._add_spike_recorder( + simdata.result, + spike_times, + neuron_gids, + gids_to_cell, + gids_to_labels, + gids_to_locs, + device=self.name, + t_stop=simulation.duration, + cell_type=target.cell_model.name, + pop_size=len(pop), + ) + else: # We are splitting the outputs + for target in pop: + for location in self.locations.get_locations(target): + gid = self.check_netcon(adapter, target, location) + + # Call record_spike() method on selected gid, it will build a SpikeTrain obj for every location + spike_times, neuron_gids = p.parallel.spike_record(gid) + self._add_spike_recorder( + simdata.result, + spike_times, + neuron_gids, + device=self.name, + t_stop=simulation.duration, + cell_type=target.cell_model.name, + cell_id=target.id, + labels=location.section.labels, + loc=location._loc, + pop_size=len(pop), + ) + + def _add_spike_recorder( + self, + results, + spike_times, + gids, + cell_dict=None, + labels_dict=None, + locs_dict=None, + **annotations + ): + results.record_spike( + spike_times, gids, cell_dict, labels_dict, locs_dict, **annotations + ) diff --git a/tests/test_devices.py b/tests/test_devices.py index a9d097e..2185245 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -105,8 +105,47 @@ def setUp(self): self.network.compile() def test_simple_stimulus(self): - """Test that spike_recorder correctly records stimulus""" + sim = self.network.simulations.test + adapter = get_simulation_adapter(sim.simulator) + simdata = adapter.prepare(sim) + results = adapter.run(sim) + result = adapter.collect(sim, simdata, results[0]) + pop_lenghts = [] + for cm in sim.cell_models: + pop = simdata.populations[sim.cell_models[cm]] + pop_lenghts.append(len(pop)) + + for index, spk in enumerate(result.spiketrains[: pop_lenghts[0] : 1]): + self.assertEqual(spk.annotations["cell_type"], "A") + self.assertEqual(spk.annotations["cell_id"], index) + self.assertClose( + spk.magnitude, + np.full(pop_lenghts[0], 5.1, dtype=np.float64), + f"SpikeTrains for cell A do not match!", + ) + second_interval = pop_lenghts[0] + pop_lenghts[1] + for index, spk in enumerate( + result.spiketrains[pop_lenghts[0] : second_interval : 1] + ): + self.assertEqual(spk.annotations["cell_type"], "B") + self.assertEqual(spk.annotations["cell_id"], index) + self.assertClose( + spk.magnitude, np.array([]), f"SpikeTrains for cell B should be empty!" + ) + for index, spk in enumerate(result.spiketrains[second_interval::1]): + self.assertEqual(spk.annotations["cell_type"], "C") + self.assertEqual(spk.annotations["cell_id"], index) + self.assertClose( + spk.magnitude, + np.full((pop_lenghts[0], 2), [5.1, 35.1], dtype=np.float64), + f"SpikeTrains for cell C do not match!", + ) + def test_join_population(self): + """Test that spike_recorder correctly records stimulus and stores a spiketrain for every cell population""" + cfg = self.network.configuration + cfg.simulations.test.devices.spike_detector.join_population = True + self.network.storage.store_active_config(cfg) sim = self.network.simulations.test adapter = get_simulation_adapter(sim.simulator) simdata = adapter.prepare(sim) From 8c253fc588aaa566d10bd3b284d89262c20136b7 Mon Sep 17 00:00:00 2001 From: filimarc Date: Tue, 11 Mar 2025 15:02:42 +0100 Subject: [PATCH 12/14] fix: tests for mpi --- bsb_neuron/devices/spike_recorder.py | 11 +++++++++++ tests/test_devices.py | 12 +++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/bsb_neuron/devices/spike_recorder.py b/bsb_neuron/devices/spike_recorder.py index 1de6788..e6df9fd 100644 --- a/bsb_neuron/devices/spike_recorder.py +++ b/bsb_neuron/devices/spike_recorder.py @@ -7,6 +7,17 @@ @config.node class SpikeRecorder(NeuronDevice, classmap_entry="spike_recorder"): + """ + Device to record the spike events in selected locations. + + :param location: The LocationTargetting chosen to select location on cells, default selects "soma". + :type LocatioTargetting: ~bsb.simulation.targetting.LocationTargetting + + :param join_population: If set to True, a SpikeTrain object will be created for each cell population; if set to False, + a SpikeTrain will be stored for each individual location. The default value is False. + :type bool + """ + locations = config.attr(type=LocationTargetting, default={"strategy": "soma"}) join_population = config.attr(type=bool, default=False) diff --git a/tests/test_devices.py b/tests/test_devices.py index 2185245..a3f1cb0 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -111,11 +111,13 @@ def test_simple_stimulus(self): results = adapter.run(sim) result = adapter.collect(sim, simdata, results[0]) pop_lenghts = [] + ids = [] for cm in sim.cell_models: - pop = simdata.populations[sim.cell_models[cm]] + pop = [cell.id for cell in simdata.populations[sim.cell_models[cm]]] pop_lenghts.append(len(pop)) + ids.append(pop) - for index, spk in enumerate(result.spiketrains[: pop_lenghts[0] : 1]): + for index, spk in zip(ids[0], result.spiketrains[: pop_lenghts[0] : 1]): self.assertEqual(spk.annotations["cell_type"], "A") self.assertEqual(spk.annotations["cell_id"], index) self.assertClose( @@ -124,15 +126,15 @@ def test_simple_stimulus(self): f"SpikeTrains for cell A do not match!", ) second_interval = pop_lenghts[0] + pop_lenghts[1] - for index, spk in enumerate( - result.spiketrains[pop_lenghts[0] : second_interval : 1] + for index, spk in zip( + ids[1], result.spiketrains[pop_lenghts[0] : second_interval : 1] ): self.assertEqual(spk.annotations["cell_type"], "B") self.assertEqual(spk.annotations["cell_id"], index) self.assertClose( spk.magnitude, np.array([]), f"SpikeTrains for cell B should be empty!" ) - for index, spk in enumerate(result.spiketrains[second_interval::1]): + for index, spk in zip(ids[2], result.spiketrains[second_interval::1]): self.assertEqual(spk.annotations["cell_type"], "C") self.assertEqual(spk.annotations["cell_id"], index) self.assertClose( From 108bb0b8d156d41737356effedc7bc37de06bae6 Mon Sep 17 00:00:00 2001 From: filimarc Date: Wed, 12 Mar 2025 14:13:31 +0100 Subject: [PATCH 13/14] refactor: use array_annotation for senders --- bsb_neuron/adapter.py | 4 +++- tests/test_devices.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bsb_neuron/adapter.py b/bsb_neuron/adapter.py index 7435d8c..337a7de 100644 --- a/bsb_neuron/adapter.py +++ b/bsb_neuron/adapter.py @@ -43,7 +43,9 @@ def flush(segment): SpikeTrain( np.array(time_vect) if len(time_vect) > 0 else [], gids=np.array(id_vect) if len(id_vect) > 0 else [], - senders=np.array([cell_model_id[gid] for gid in id_vect]), + array_annotations={ + "senders": np.array([cell_model_id[gid] for gid in id_vect]) + }, labels=np.array([loc_label_id[gid] for gid in id_vect]), loc=np.array([locs_ids[gid] for gid in id_vect]), **annotations, diff --git a/tests/test_devices.py b/tests/test_devices.py index a3f1cb0..6d8babe 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -184,6 +184,6 @@ def test_join_population(self): for elem, spike_train in enumerate(result.spiketrains): self.assertEqual(control_data[elem][0], spike_train.annotations["cell_type"]) self.assertEqual( - control_data[elem][1], list(spike_train.annotations["senders"]) + control_data[elem][1], list(spike_train.array_annotations["senders"]) ) self.assertClose(control_data[elem][2], spike_train.magnitude) From 2c7e8bfa4ad624e55e78c328f63f0ed5942811f2 Mon Sep 17 00:00:00 2001 From: filimarc Date: Mon, 19 May 2025 12:55:33 +0200 Subject: [PATCH 14/14] refactor: change to arborize and patch util function fix tests --- bsb_neuron/devices/spike_recorder.py | 20 +++++--------------- tests/test_devices.py | 4 ++-- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/bsb_neuron/devices/spike_recorder.py b/bsb_neuron/devices/spike_recorder.py index e6df9fd..b4a4325 100644 --- a/bsb_neuron/devices/spike_recorder.py +++ b/bsb_neuron/devices/spike_recorder.py @@ -1,6 +1,7 @@ import numpy as np from bsb import LocationTargetting, config from patch import p +from patch.objects import Vector from ..device import NeuronDevice @@ -21,31 +22,20 @@ class SpikeRecorder(NeuronDevice, classmap_entry="spike_recorder"): locations = config.attr(type=LocationTargetting, default={"strategy": "soma"}) join_population = config.attr(type=bool, default=False) - def check_netcon(self, adapter, target, location): - # Insert a NetCon (if not already present) and retrieve its gid - if hasattr(location.section, "_transmitter"): - gid = location.section._transmitter.gid - else: - gid = target.insert_transmitter( - adapter.next_gid, location._loc, delay=1, weight=0.0004 - ).gid - adapter.next_gid += 1 - return gid - def implement(self, adapter, simulation, simdata): for model, pop in self.targetting.get_targets( adapter, simulation, simdata ).items(): if self.join_population: - spike_times = p.parallel._interpreter.Vector() - neuron_gids = p.parallel._interpreter.Vector() + spike_times = p.parallel.Vector + neuron_gids = p.parallel.Vector gids_to_cell = {} gids_to_labels = {} gids_to_locs = {} for target in pop: for location in self.locations.get_locations(target): - gid = self.check_netcon(adapter, target, location) + gid = target.check_netcon(location, adapter) # Call record_spike() method on selected gid using common spike_times and neuron_gids Vector for # cells in the same population gids_to_cell[gid] = target.id @@ -70,7 +60,7 @@ def implement(self, adapter, simulation, simdata): else: # We are splitting the outputs for target in pop: for location in self.locations.get_locations(target): - gid = self.check_netcon(adapter, target, location) + gid = target.check_netcon(location, adapter) # Call record_spike() method on selected gid, it will build a SpikeTrain obj for every location spike_times, neuron_gids = p.parallel.spike_record(gid) diff --git a/tests/test_devices.py b/tests/test_devices.py index 6d8babe..87853ae 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -109,7 +109,7 @@ def test_simple_stimulus(self): adapter = get_simulation_adapter(sim.simulator) simdata = adapter.prepare(sim) results = adapter.run(sim) - result = adapter.collect(sim, simdata, results[0]) + result = adapter.collect(results)[0] pop_lenghts = [] ids = [] for cm in sim.cell_models: @@ -152,7 +152,7 @@ def test_join_population(self): adapter = get_simulation_adapter(sim.simulator) simdata = adapter.prepare(sim) results = adapter.run(sim) - result = adapter.collect(sim, simdata, results[0]) + result = adapter.collect(results)[0] self.assertEqual( len(result.spiketrains), 3,