bluecellulab 2.6.61__py3-none-any.whl → 2.6.63__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bluecellulab might be problematic. Click here for more details.
- bluecellulab/analysis/analysis.py +48 -24
- bluecellulab/circuit/config/definition.py +8 -0
- bluecellulab/circuit/config/sonata_simulation_config.py +14 -1
- bluecellulab/circuit/simulation_access.py +29 -7
- bluecellulab/circuit_simulation.py +40 -94
- bluecellulab/reports/__init__.py +0 -0
- bluecellulab/reports/manager.py +78 -0
- bluecellulab/reports/utils.py +156 -0
- bluecellulab/reports/writers/__init__.py +25 -0
- bluecellulab/reports/writers/base_writer.py +30 -0
- bluecellulab/reports/writers/compartment.py +196 -0
- bluecellulab/reports/writers/spikes.py +61 -0
- bluecellulab/simulation/report.py +0 -264
- bluecellulab/simulation/simulation.py +5 -6
- bluecellulab/validation/validation.py +65 -30
- {bluecellulab-2.6.61.dist-info → bluecellulab-2.6.63.dist-info}/METADATA +1 -1
- {bluecellulab-2.6.61.dist-info → bluecellulab-2.6.63.dist-info}/RECORD +21 -14
- {bluecellulab-2.6.61.dist-info → bluecellulab-2.6.63.dist-info}/WHEEL +0 -0
- {bluecellulab-2.6.61.dist-info → bluecellulab-2.6.63.dist-info}/licenses/AUTHORS.txt +0 -0
- {bluecellulab-2.6.61.dist-info → bluecellulab-2.6.63.dist-info}/licenses/LICENSE +0 -0
- {bluecellulab-2.6.61.dist-info → bluecellulab-2.6.63.dist-info}/top_level.txt +0 -0
|
@@ -4,9 +4,11 @@ try:
|
|
|
4
4
|
except ImportError:
|
|
5
5
|
efel = None
|
|
6
6
|
from itertools import islice
|
|
7
|
+
from itertools import repeat
|
|
7
8
|
import logging
|
|
8
9
|
from matplotlib.collections import LineCollection
|
|
9
10
|
import matplotlib.pyplot as plt
|
|
11
|
+
from multiprocessing import Pool
|
|
10
12
|
import neuron
|
|
11
13
|
import numpy as np
|
|
12
14
|
import pathlib
|
|
@@ -40,7 +42,8 @@ def compute_plot_iv_curve(cell,
|
|
|
40
42
|
show_figure=True,
|
|
41
43
|
save_figure=False,
|
|
42
44
|
output_dir="./",
|
|
43
|
-
output_fname="iv_curve.pdf"
|
|
45
|
+
output_fname="iv_curve.pdf",
|
|
46
|
+
n_processes=None):
|
|
44
47
|
"""Compute and plot the Current-Voltage (I-V) curve for a given cell by
|
|
45
48
|
injecting a range of currents.
|
|
46
49
|
|
|
@@ -72,6 +75,9 @@ def compute_plot_iv_curve(cell,
|
|
|
72
75
|
save_figure (bool): Whether to save the figure. Default is False.
|
|
73
76
|
output_dir (str): The directory to save the figure if save_figure is True. Default is "./".
|
|
74
77
|
output_fname (str): The filename to save the figure as if save_figure is True. Default is "iv_curve.png".
|
|
78
|
+
n_processes (int, optional): The number of processes to use for parallel execution.
|
|
79
|
+
If None or if it is higher than the number of steps,
|
|
80
|
+
it will use the number of steps as the number of processes.
|
|
75
81
|
|
|
76
82
|
Returns:
|
|
77
83
|
tuple: A tuple containing:
|
|
@@ -95,15 +101,22 @@ def compute_plot_iv_curve(cell,
|
|
|
95
101
|
for amp in list_amp
|
|
96
102
|
]
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
if n_processes is None or n_processes > len(steps):
|
|
105
|
+
n_processes = len(steps)
|
|
106
|
+
with Pool(n_processes) as p:
|
|
107
|
+
recordings = p.starmap(
|
|
108
|
+
run_stimulus,
|
|
109
|
+
zip(
|
|
110
|
+
repeat(cell.template_params),
|
|
111
|
+
steps,
|
|
112
|
+
repeat(injecting_section),
|
|
113
|
+
repeat(injecting_segment),
|
|
114
|
+
repeat(True), # cvode
|
|
115
|
+
repeat(True), # add_hypamp
|
|
116
|
+
repeat(recording_section),
|
|
117
|
+
repeat(recording_segment),
|
|
118
|
+
)
|
|
119
|
+
)
|
|
107
120
|
|
|
108
121
|
steady_states = []
|
|
109
122
|
# compute steady state response
|
|
@@ -148,7 +161,8 @@ def compute_plot_fi_curve(cell,
|
|
|
148
161
|
show_figure=True,
|
|
149
162
|
save_figure=False,
|
|
150
163
|
output_dir="./",
|
|
151
|
-
output_fname="fi_curve.pdf"
|
|
164
|
+
output_fname="fi_curve.pdf",
|
|
165
|
+
n_processes=None):
|
|
152
166
|
"""Compute and plot the Frequency-Current (F-I) curve for a given cell by
|
|
153
167
|
injecting a range of currents.
|
|
154
168
|
|
|
@@ -182,6 +196,9 @@ def compute_plot_fi_curve(cell,
|
|
|
182
196
|
save_figure (bool): Whether to save the figure. Default is False.
|
|
183
197
|
output_dir (str): The directory to save the figure if save_figure is True. Default is "./".
|
|
184
198
|
output_fname (str): The filename to save the figure as if save_figure is True. Default is "iv_curve.png".
|
|
199
|
+
n_processes (int, optional): The number of processes to use for parallel execution.
|
|
200
|
+
If None or if it is higher than the number of steps,
|
|
201
|
+
it will use the number of steps as the number of processes.
|
|
185
202
|
|
|
186
203
|
Returns:
|
|
187
204
|
tuple: A tuple containing:
|
|
@@ -201,19 +218,26 @@ def compute_plot_fi_curve(cell,
|
|
|
201
218
|
for amp in list_amp
|
|
202
219
|
]
|
|
203
220
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
if n_processes is None or n_processes > len(steps):
|
|
222
|
+
n_processes = len(steps)
|
|
223
|
+
with Pool(n_processes) as p:
|
|
224
|
+
recordings = p.starmap(
|
|
225
|
+
run_stimulus,
|
|
226
|
+
zip(
|
|
227
|
+
repeat(cell.template_params),
|
|
228
|
+
steps,
|
|
229
|
+
repeat(injecting_section),
|
|
230
|
+
repeat(injecting_segment),
|
|
231
|
+
repeat(True), # cvode
|
|
232
|
+
repeat(True), # add_hypamp
|
|
233
|
+
repeat(recording_section),
|
|
234
|
+
repeat(recording_segment),
|
|
235
|
+
repeat(True), # enable_spike_detection
|
|
236
|
+
repeat(threshold_voltage), # threshold_spike_detection
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
spike_count = [len(recording.spike) for recording in recordings]
|
|
217
241
|
|
|
218
242
|
plot_fi_curve(list_amp,
|
|
219
243
|
spike_count,
|
|
@@ -68,6 +68,14 @@ class SimulationConfig(Protocol):
|
|
|
68
68
|
def spike_location(self) -> str:
|
|
69
69
|
raise NotImplementedError
|
|
70
70
|
|
|
71
|
+
@property
|
|
72
|
+
def tstart(self) -> Optional[float]:
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def tstop(self) -> Optional[float]:
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
|
|
71
79
|
@property
|
|
72
80
|
def duration(self) -> Optional[float]:
|
|
73
81
|
raise NotImplementedError
|
|
@@ -17,6 +17,7 @@ import json
|
|
|
17
17
|
import logging
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Optional
|
|
20
|
+
import warnings
|
|
20
21
|
|
|
21
22
|
from bluecellulab.circuit.config.sections import Conditions, ConnectionOverrides
|
|
22
23
|
from bluecellulab.stimulus.circuit_stimulus_definitions import Stimulus
|
|
@@ -160,9 +161,21 @@ class SonataSimulationConfig:
|
|
|
160
161
|
return self.impl.conditions.spike_location.name
|
|
161
162
|
|
|
162
163
|
@property
|
|
163
|
-
def
|
|
164
|
+
def tstart(self) -> Optional[float]:
|
|
165
|
+
return self.impl.config.get("run", {}).get("tstart", 0.0)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def tstop(self) -> float:
|
|
164
169
|
return self.impl.run.tstop
|
|
165
170
|
|
|
171
|
+
@property
|
|
172
|
+
def duration(self) -> Optional[float]:
|
|
173
|
+
warnings.warn(
|
|
174
|
+
"`duration` is deprecated. Use `tstop` instead.",
|
|
175
|
+
DeprecationWarning
|
|
176
|
+
)
|
|
177
|
+
return self.tstop
|
|
178
|
+
|
|
166
179
|
@property
|
|
167
180
|
def dt(self) -> float:
|
|
168
181
|
return self.impl.run.dt
|
|
@@ -172,16 +172,38 @@ class SonataSimulationAccess:
|
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
def get_synapse_replay_spikes(f_name: str) -> dict:
|
|
175
|
-
"""Read the .h5 file containing the spike replays.
|
|
175
|
+
"""Read the .h5 file containing the spike replays.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
f_name: Path to SONATA .h5 spike file.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dictionary mapping node_id to np.array of spike times.
|
|
182
|
+
"""
|
|
183
|
+
all_spikes = []
|
|
176
184
|
with h5py.File(f_name, 'r') as f:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
node_ids = f['/spikes/All/node_ids'][:]
|
|
185
|
+
if "spikes" not in f:
|
|
186
|
+
raise ValueError("spike file is missing required 'spikes' group.")
|
|
180
187
|
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
for population in f["spikes"]:
|
|
189
|
+
pop_group = f["spikes"][population]
|
|
190
|
+
timestamps = pop_group["timestamps"][:]
|
|
191
|
+
node_ids = pop_group["node_ids"][:]
|
|
192
|
+
|
|
193
|
+
pop_spikes = pd.DataFrame({"t": timestamps, "node_id": node_ids})
|
|
194
|
+
pop_spikes = pop_spikes.astype({"node_id": int})
|
|
195
|
+
all_spikes.append(pop_spikes)
|
|
196
|
+
|
|
197
|
+
if not all_spikes:
|
|
198
|
+
return {}
|
|
199
|
+
|
|
200
|
+
spikes = pd.concat(all_spikes, ignore_index=True)
|
|
183
201
|
|
|
184
202
|
if (spikes["t"] < 0).any():
|
|
185
203
|
logger.warning("Found negative spike times... Clipping them to 0")
|
|
186
204
|
spikes["t"].clip(lower=0., inplace=True)
|
|
187
|
-
|
|
205
|
+
|
|
206
|
+
# Group spikes by node_id and ensure spike times are sorted in ascending order.
|
|
207
|
+
# This is critical because NEURON's VecStim requires monotonically increasing times per train.
|
|
208
|
+
grouped = spikes.groupby("node_id")["t"]
|
|
209
|
+
return {k: np.sort(np.asarray(v.values)) for k, v in grouped}
|
|
@@ -17,12 +17,11 @@ simulations."""
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
from collections.abc import Iterable
|
|
20
|
-
import os
|
|
21
20
|
from pathlib import Path
|
|
22
21
|
from typing import Optional
|
|
23
22
|
import logging
|
|
24
23
|
|
|
25
|
-
from
|
|
24
|
+
from bluecellulab.reports.utils import configure_all_reports
|
|
26
25
|
import neuron
|
|
27
26
|
import numpy as np
|
|
28
27
|
import pandas as pd
|
|
@@ -47,7 +46,6 @@ from bluecellulab.circuit.simulation_access import BluepySimulationAccess, Simul
|
|
|
47
46
|
from bluecellulab.importer import load_mod_files
|
|
48
47
|
from bluecellulab.rngsettings import RNGSettings
|
|
49
48
|
from bluecellulab.simulation.neuron_globals import NeuronGlobals
|
|
50
|
-
from bluecellulab.simulation.report import configure_all_reports, write_compartment_report, write_sonata_spikes
|
|
51
49
|
from bluecellulab.stimulus.circuit_stimulus_definitions import Noise, OrnsteinUhlenbeck, RelativeOrnsteinUhlenbeck, RelativeShotNoise, ShotNoise
|
|
52
50
|
import bluecellulab.stimulus.circuit_stimulus_definitions as circuit_stimulus_definitions
|
|
53
51
|
from bluecellulab.exceptions import BluecellulabError
|
|
@@ -638,15 +636,32 @@ class CircuitSimulation:
|
|
|
638
636
|
will not be exactly reproduced.
|
|
639
637
|
"""
|
|
640
638
|
if t_stop is None:
|
|
641
|
-
|
|
642
|
-
if
|
|
639
|
+
t_stop = self.circuit_access.config.tstop
|
|
640
|
+
if t_stop is None: # type narrowing
|
|
643
641
|
t_stop = 0.0
|
|
644
|
-
else:
|
|
645
|
-
t_stop = duration
|
|
646
642
|
if dt is None:
|
|
647
643
|
dt = self.circuit_access.config.dt
|
|
648
|
-
|
|
649
|
-
|
|
644
|
+
|
|
645
|
+
config_forward_skip_value = self.circuit_access.config.forward_skip # legacy
|
|
646
|
+
config_tstart = self.circuit_access.config.tstart or 0.0 # SONATA
|
|
647
|
+
# Determine effective skip value and flag
|
|
648
|
+
if forward_skip_value is not None:
|
|
649
|
+
# User explicitly provided value → use it
|
|
650
|
+
effective_skip_value = forward_skip_value
|
|
651
|
+
effective_skip = forward_skip
|
|
652
|
+
elif config_forward_skip_value is not None:
|
|
653
|
+
# Use legacy config if available
|
|
654
|
+
effective_skip_value = config_forward_skip_value
|
|
655
|
+
effective_skip = forward_skip
|
|
656
|
+
elif config_tstart > 0.0:
|
|
657
|
+
# Use SONATA tstart *only* if no other skip value was provided
|
|
658
|
+
effective_skip_value = config_tstart
|
|
659
|
+
effective_skip = True
|
|
660
|
+
else:
|
|
661
|
+
# No skip
|
|
662
|
+
effective_skip_value = None
|
|
663
|
+
effective_skip = False
|
|
664
|
+
|
|
650
665
|
if celsius is None:
|
|
651
666
|
celsius = self.circuit_access.config.celsius
|
|
652
667
|
NeuronGlobals.get_instance().temperature = celsius
|
|
@@ -664,15 +679,13 @@ class CircuitSimulation:
|
|
|
664
679
|
"simulations")
|
|
665
680
|
|
|
666
681
|
sim.run(
|
|
667
|
-
t_stop,
|
|
682
|
+
tstop=t_stop,
|
|
668
683
|
cvode=cvode,
|
|
669
684
|
dt=dt,
|
|
670
|
-
forward_skip=
|
|
671
|
-
forward_skip_value=
|
|
685
|
+
forward_skip=effective_skip,
|
|
686
|
+
forward_skip_value=effective_skip_value,
|
|
672
687
|
show_progress=show_progress)
|
|
673
688
|
|
|
674
|
-
self.write_reports()
|
|
675
|
-
|
|
676
689
|
def get_mainsim_voltage_trace(
|
|
677
690
|
self, cell_id: int | tuple[str, int], t_start=None, t_stop=None, t_step=None
|
|
678
691
|
) -> np.ndarray:
|
|
@@ -713,23 +726,31 @@ class CircuitSimulation:
|
|
|
713
726
|
first_key = next(iter(self.cells))
|
|
714
727
|
return self.cells[first_key].get_time()
|
|
715
728
|
|
|
716
|
-
def get_time_trace(self, t_step=None) -> np.ndarray:
|
|
729
|
+
def get_time_trace(self, t_start=None, t_stop=None, t_step=None) -> np.ndarray:
|
|
717
730
|
"""Get the time vector for the recordings, negative times removed.
|
|
718
731
|
|
|
719
732
|
Parameters
|
|
720
733
|
-----------
|
|
721
|
-
|
|
722
|
-
equals
|
|
734
|
+
t_start, t_stop: time range of interest.
|
|
735
|
+
t_step: time step (multiple of report dt; equals dt by default)
|
|
723
736
|
|
|
724
737
|
Returns:
|
|
725
|
-
|
|
738
|
+
1D np.ndarray representing time points.
|
|
726
739
|
"""
|
|
727
740
|
time = self.get_time()
|
|
728
|
-
time = time[
|
|
741
|
+
time = time[time >= 0.0]
|
|
742
|
+
|
|
743
|
+
if t_start is None or t_start < 0:
|
|
744
|
+
t_start = 0
|
|
745
|
+
if t_stop is None:
|
|
746
|
+
t_stop = np.inf
|
|
747
|
+
|
|
748
|
+
time = time[(time >= t_start) & (time <= t_stop)]
|
|
729
749
|
|
|
730
750
|
if t_step is not None:
|
|
731
751
|
ratio = t_step / self.dt
|
|
732
752
|
time = _sample_array(time, ratio)
|
|
753
|
+
|
|
733
754
|
return time
|
|
734
755
|
|
|
735
756
|
def get_voltage_trace(
|
|
@@ -806,78 +827,3 @@ class CircuitSimulation:
|
|
|
806
827
|
record_dt=cell_kwargs['record_dt'],
|
|
807
828
|
template_format=cell_kwargs['template_format'],
|
|
808
829
|
emodel_properties=cell_kwargs['emodel_properties'])
|
|
809
|
-
|
|
810
|
-
def write_reports(self):
|
|
811
|
-
"""Write all reports defined in the simulation config."""
|
|
812
|
-
report_entries = self.circuit_access.config.get_report_entries()
|
|
813
|
-
|
|
814
|
-
for report_name, report_cfg in report_entries.items():
|
|
815
|
-
report_type = report_cfg.get("type", "compartment")
|
|
816
|
-
section = report_cfg.get("sections")
|
|
817
|
-
|
|
818
|
-
if report_type != "compartment":
|
|
819
|
-
raise NotImplementedError(f"Report type '{report_type}' is not supported.")
|
|
820
|
-
|
|
821
|
-
output_path = self.circuit_access.config.report_file_path(report_cfg, report_name)
|
|
822
|
-
if section == "compartment_set":
|
|
823
|
-
if report_cfg.get("cells") is not None:
|
|
824
|
-
raise ValueError(
|
|
825
|
-
"Report config error: 'cells' must not be set when using 'compartment_set' sections."
|
|
826
|
-
)
|
|
827
|
-
compartment_sets = self.circuit_access.config.get_compartment_sets()
|
|
828
|
-
write_compartment_report(
|
|
829
|
-
report_name=report_name,
|
|
830
|
-
output_path=output_path,
|
|
831
|
-
cells=self.cells,
|
|
832
|
-
report_cfg=report_cfg,
|
|
833
|
-
source_sets=compartment_sets,
|
|
834
|
-
source_type="compartment_set",
|
|
835
|
-
sim_dt=self.dt,
|
|
836
|
-
)
|
|
837
|
-
|
|
838
|
-
else:
|
|
839
|
-
node_sets = self.circuit_access.config.get_node_sets()
|
|
840
|
-
if report_cfg.get("compartments") not in ("center", "all"):
|
|
841
|
-
raise ValueError(
|
|
842
|
-
f"Unsupported 'compartments' value '{report_cfg.get('compartments')}' "
|
|
843
|
-
"for node-based section recording (must be 'center' or 'all')."
|
|
844
|
-
)
|
|
845
|
-
write_compartment_report(
|
|
846
|
-
report_name=report_name,
|
|
847
|
-
output_path=output_path,
|
|
848
|
-
cells=self.cells,
|
|
849
|
-
report_cfg=report_cfg,
|
|
850
|
-
source_sets=node_sets,
|
|
851
|
-
source_type="node_set",
|
|
852
|
-
sim_dt=self.dt,
|
|
853
|
-
)
|
|
854
|
-
|
|
855
|
-
self.write_spike_report()
|
|
856
|
-
|
|
857
|
-
def write_spike_report(self):
|
|
858
|
-
"""Collect and write in-memory recorded spike times to a SONATA HDF5
|
|
859
|
-
file, grouped by population as required by the SONATA specification."""
|
|
860
|
-
output_path = self.circuit_access.config.spikes_file_path
|
|
861
|
-
|
|
862
|
-
if os.path.exists(output_path):
|
|
863
|
-
os.remove(output_path)
|
|
864
|
-
|
|
865
|
-
# Group spikes per population
|
|
866
|
-
spikes_by_population = defaultdict(dict)
|
|
867
|
-
for gid, cell in self.cells.items():
|
|
868
|
-
pop = getattr(gid, 'population_name', None)
|
|
869
|
-
if pop is None:
|
|
870
|
-
continue
|
|
871
|
-
try:
|
|
872
|
-
cell_spikes = cell.get_recorded_spikes(location=self.spike_location, threshold=self.spike_threshold)
|
|
873
|
-
if cell_spikes is not None:
|
|
874
|
-
spikes_by_population[pop][gid.id] = list(cell_spikes)
|
|
875
|
-
except AttributeError:
|
|
876
|
-
continue
|
|
877
|
-
|
|
878
|
-
# Ensure we at least create empty groups for all known populations
|
|
879
|
-
all_populations = set(getattr(gid, 'population_name', None) for gid in self.cells.keys())
|
|
880
|
-
|
|
881
|
-
for pop in all_populations:
|
|
882
|
-
spikes = spikes_by_population.get(pop, {}) # May be empty
|
|
883
|
-
write_sonata_spikes(output_path, spikes, pop)
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Copyright 2025 Open Brain Institute
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Optional, Dict
|
|
16
|
+
from bluecellulab.reports.writers import get_writer
|
|
17
|
+
from bluecellulab.reports.utils import extract_spikes_from_cells # helper you already have / write
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReportManager:
|
|
21
|
+
"""Orchestrates writing all requested SONATA reports."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, config, sim_dt: float):
|
|
24
|
+
self.cfg = config
|
|
25
|
+
self.dt = sim_dt
|
|
26
|
+
|
|
27
|
+
def write_all(
|
|
28
|
+
self,
|
|
29
|
+
cells_or_traces: Dict,
|
|
30
|
+
spikes_by_pop: Optional[Dict[str, Dict[int, list]]] = None,
|
|
31
|
+
):
|
|
32
|
+
"""Write all configured reports (compartment and spike) in SONATA
|
|
33
|
+
format.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
cells_or_traces : dict
|
|
38
|
+
A dictionary mapping (population, gid) to either:
|
|
39
|
+
- Cell objects with recorded data (used in single-process simulations), or
|
|
40
|
+
- Precomputed trace dictionaries, e.g., {"voltage": ndarray}, typically gathered across ranks in parallel runs.
|
|
41
|
+
|
|
42
|
+
spikes_by_pop : dict, optional
|
|
43
|
+
A precomputed dictionary of spike times by population.
|
|
44
|
+
If not provided, spike times are extracted from `cells_or_traces`.
|
|
45
|
+
|
|
46
|
+
Notes
|
|
47
|
+
-----
|
|
48
|
+
In parallel simulations, you must gather all traces and spikes to rank 0 and pass them here.
|
|
49
|
+
"""
|
|
50
|
+
self._write_voltage_reports(cells_or_traces)
|
|
51
|
+
self._write_spike_report(spikes_by_pop or extract_spikes_from_cells(cells_or_traces, location=self.cfg.spike_location, threshold=self.cfg.spike_threshold))
|
|
52
|
+
|
|
53
|
+
def _write_voltage_reports(self, cells_or_traces):
|
|
54
|
+
for name, rcfg in self.cfg.get_report_entries().items():
|
|
55
|
+
if rcfg.get("type") != "compartment":
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
section = rcfg.get("sections")
|
|
59
|
+
if section == "compartment_set":
|
|
60
|
+
if rcfg.get("cells") is not None:
|
|
61
|
+
raise ValueError("'cells' may not be set with 'compartment_set'")
|
|
62
|
+
src_sets, src_type = self.cfg.get_compartment_sets(), "compartment_set"
|
|
63
|
+
else:
|
|
64
|
+
if rcfg.get("compartments") not in ("center", "all"):
|
|
65
|
+
raise ValueError("invalid 'compartments' value")
|
|
66
|
+
src_sets, src_type = self.cfg.get_node_sets(), "node_set"
|
|
67
|
+
|
|
68
|
+
rcfg["_source_sets"] = src_sets
|
|
69
|
+
rcfg["_source_type"] = src_type
|
|
70
|
+
|
|
71
|
+
out_path = self.cfg.report_file_path(rcfg, name)
|
|
72
|
+
writer = get_writer("compartment")(rcfg, out_path, self.dt)
|
|
73
|
+
writer.write(cells_or_traces, self.cfg.tstart)
|
|
74
|
+
|
|
75
|
+
def _write_spike_report(self, spikes_by_pop):
|
|
76
|
+
out_path = self.cfg.spikes_file_path
|
|
77
|
+
writer = get_writer("spikes")({}, out_path, self.dt)
|
|
78
|
+
writer.write(spikes_by_pop)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Copyright 2025 Open Brain Institute
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Report class of bluecellulab."""
|
|
15
|
+
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Dict, Any, List
|
|
19
|
+
|
|
20
|
+
from bluecellulab.tools import resolve_segments, resolve_source_nodes
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _configure_recording(cell, report_cfg, source, source_type, report_name):
|
|
26
|
+
"""Configure recording of a variable on a single cell.
|
|
27
|
+
|
|
28
|
+
This function sets up the recording of the specified variable (e.g., membrane voltage)
|
|
29
|
+
in the target cell, for each resolved segment.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
cell : Any
|
|
34
|
+
The cell object on which to configure recordings.
|
|
35
|
+
|
|
36
|
+
report_cfg : dict
|
|
37
|
+
The configuration dictionary for this report.
|
|
38
|
+
|
|
39
|
+
source : dict
|
|
40
|
+
The source definition specifying nodes or compartments.
|
|
41
|
+
|
|
42
|
+
source_type : str
|
|
43
|
+
Either "node_set" or "compartment_set".
|
|
44
|
+
|
|
45
|
+
report_name : str
|
|
46
|
+
The name of the report (used in logging).
|
|
47
|
+
"""
|
|
48
|
+
variable = report_cfg.get("variable_name", "v")
|
|
49
|
+
|
|
50
|
+
node_id = cell.cell_id
|
|
51
|
+
compartment_nodes = source.get("compartment_set") if source_type == "compartment_set" else None
|
|
52
|
+
|
|
53
|
+
targets = resolve_segments(cell, report_cfg, node_id, compartment_nodes, source_type)
|
|
54
|
+
for sec, sec_name, seg in targets:
|
|
55
|
+
try:
|
|
56
|
+
cell.add_variable_recording(variable=variable, section=sec, segx=seg)
|
|
57
|
+
except AttributeError:
|
|
58
|
+
logger.warning(f"Recording for variable '{variable}' is not implemented in Cell.")
|
|
59
|
+
return
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.warning(
|
|
62
|
+
f"Failed to record '{variable}' at {sec_name}({seg}) on GID {node_id} for report '{report_name}': {e}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def configure_all_reports(cells, simulation_config):
|
|
67
|
+
"""Configure recordings for all reports defined in the simulation
|
|
68
|
+
configuration.
|
|
69
|
+
|
|
70
|
+
This iterates through all report entries, resolves source nodes or compartments,
|
|
71
|
+
and configures the corresponding recordings on each cell.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
cells : dict
|
|
76
|
+
Mapping from (population, gid) → Cell object.
|
|
77
|
+
|
|
78
|
+
simulation_config : Any
|
|
79
|
+
Simulation configuration object providing report entries,
|
|
80
|
+
node sets, and compartment sets.
|
|
81
|
+
"""
|
|
82
|
+
report_entries = simulation_config.get_report_entries()
|
|
83
|
+
|
|
84
|
+
for report_name, report_cfg in report_entries.items():
|
|
85
|
+
report_type = report_cfg.get("type", "compartment")
|
|
86
|
+
section = report_cfg.get("sections", "soma")
|
|
87
|
+
|
|
88
|
+
if report_type != "compartment":
|
|
89
|
+
raise NotImplementedError(f"Report type '{report_type}' is not supported.")
|
|
90
|
+
|
|
91
|
+
if section == "compartment_set":
|
|
92
|
+
source_type = "compartment_set"
|
|
93
|
+
source_sets = simulation_config.get_compartment_sets()
|
|
94
|
+
source_name = report_cfg.get("compartments")
|
|
95
|
+
if not source_name:
|
|
96
|
+
logger.warning(f"Report '{report_name}' does not specify a node set in 'compartments' for {source_type}.")
|
|
97
|
+
continue
|
|
98
|
+
else:
|
|
99
|
+
source_type = "node_set"
|
|
100
|
+
source_sets = simulation_config.get_node_sets()
|
|
101
|
+
source_name = report_cfg.get("cells")
|
|
102
|
+
if not source_name:
|
|
103
|
+
logger.warning(f"Report '{report_name}' does not specify a node set in 'cells' for {source_type}.")
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
source = source_sets.get(source_name)
|
|
107
|
+
if not source:
|
|
108
|
+
logger.warning(f"{source_type.title()} '{source_name}' not found for report '{report_name}', skipping recording.")
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
population = source["population"]
|
|
112
|
+
node_ids, _ = resolve_source_nodes(source, source_type, cells, population)
|
|
113
|
+
|
|
114
|
+
for node_id in node_ids:
|
|
115
|
+
cell = cells.get((population, node_id))
|
|
116
|
+
if not cell:
|
|
117
|
+
continue
|
|
118
|
+
_configure_recording(cell, report_cfg, source, source_type, report_name)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def extract_spikes_from_cells(
|
|
122
|
+
cells: Dict[Any, Any],
|
|
123
|
+
location: str = "soma",
|
|
124
|
+
threshold: float = -20.0,
|
|
125
|
+
) -> Dict[str, Dict[int, list]]:
|
|
126
|
+
"""Extract spike times from recorded cells, grouped by population.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
cells : dict
|
|
131
|
+
Mapping from (population, gid) → Cell object, or similar.
|
|
132
|
+
|
|
133
|
+
location : str
|
|
134
|
+
Recording location passed to Cell.get_recorded_spikes().
|
|
135
|
+
|
|
136
|
+
threshold : float
|
|
137
|
+
Voltage threshold (mV) used for spike detection.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
spikes_by_population : dict
|
|
142
|
+
{population → {gid_int → [spike_times_ms]}}
|
|
143
|
+
"""
|
|
144
|
+
spikes_by_pop: defaultdict[str, Dict[int, List[float]]] = defaultdict(dict)
|
|
145
|
+
|
|
146
|
+
for key, cell in cells.items():
|
|
147
|
+
if isinstance(key, tuple):
|
|
148
|
+
pop, gid = key
|
|
149
|
+
else:
|
|
150
|
+
raise ValueError(f"Cell key {key} is not a (population, gid) tuple.")
|
|
151
|
+
|
|
152
|
+
times = cell.get_recorded_spikes(location=location, threshold=threshold)
|
|
153
|
+
if times is not None and len(times) > 0:
|
|
154
|
+
spikes_by_pop[pop][gid] = list(times)
|
|
155
|
+
|
|
156
|
+
return dict(spikes_by_pop)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright 2025 Open Brain Institute
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from .compartment import CompartmentReportWriter
|
|
16
|
+
from .spikes import SpikeReportWriter
|
|
17
|
+
|
|
18
|
+
REGISTRY = {
|
|
19
|
+
"compartment": CompartmentReportWriter,
|
|
20
|
+
"spikes": SpikeReportWriter,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_writer(report_type):
|
|
25
|
+
return REGISTRY[report_type]
|