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
|
@@ -0,0 +1,30 @@
|
|
|
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 abc import ABC, abstractmethod
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseReportWriter(ABC):
|
|
21
|
+
"""Abstract interface for every report writer."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, report_cfg: Dict[str, Any], output_path: Path, sim_dt: float):
|
|
24
|
+
self.cfg = report_cfg
|
|
25
|
+
self.output_path = Path(output_path)
|
|
26
|
+
self.sim_dt = sim_dt
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def write(self, data: Dict):
|
|
30
|
+
"""Write one report to disk."""
|
|
@@ -0,0 +1,196 @@
|
|
|
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 pathlib import Path
|
|
16
|
+
import numpy as np
|
|
17
|
+
import h5py
|
|
18
|
+
from typing import Dict, List
|
|
19
|
+
from .base_writer import BaseReportWriter
|
|
20
|
+
from bluecellulab.reports.utils import (
|
|
21
|
+
resolve_source_nodes,
|
|
22
|
+
resolve_segments,
|
|
23
|
+
)
|
|
24
|
+
import logging
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CompartmentReportWriter(BaseReportWriter):
|
|
30
|
+
"""Writes SONATA compartment (voltage) reports."""
|
|
31
|
+
|
|
32
|
+
def write(self, cells: Dict, tstart=0):
|
|
33
|
+
report_name = self.cfg.get("name", "unnamed")
|
|
34
|
+
# section = self.cfg.get("sections")
|
|
35
|
+
variable = self.cfg.get("variable_name", "v")
|
|
36
|
+
|
|
37
|
+
source_sets = self.cfg["_source_sets"]
|
|
38
|
+
source_type = self.cfg["_source_type"]
|
|
39
|
+
src_name = self.cfg.get("cells") if source_type == "node_set" else self.cfg.get("compartments")
|
|
40
|
+
src = source_sets.get(src_name)
|
|
41
|
+
if not src:
|
|
42
|
+
logger.warning(f"{source_type.title()} '{src_name}' not found – skipping '{report_name}'.")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
population = src["population"]
|
|
46
|
+
node_ids, comp_nodes = resolve_source_nodes(src, source_type, cells, population)
|
|
47
|
+
|
|
48
|
+
data_matrix: List[np.ndarray] = []
|
|
49
|
+
node_id_list: List[int] = []
|
|
50
|
+
idx_ptr: List[int] = [0]
|
|
51
|
+
elem_ids: List[int] = []
|
|
52
|
+
|
|
53
|
+
for nid in node_ids:
|
|
54
|
+
cell = cells.get((population, nid)) or cells.get(f"{population}_{nid}")
|
|
55
|
+
if cell is None:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
if isinstance(cell, dict):
|
|
59
|
+
# No section/segment structure to resolve for traces
|
|
60
|
+
trace = np.asarray(cell["voltage"], dtype=np.float32)
|
|
61
|
+
data_matrix.append(trace)
|
|
62
|
+
node_id_list.append(nid)
|
|
63
|
+
elem_ids.append(len(elem_ids))
|
|
64
|
+
idx_ptr.append(idx_ptr[-1] + 1)
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
targets = resolve_segments(cell, self.cfg, nid, comp_nodes, source_type)
|
|
68
|
+
for sec, sec_name, seg in targets:
|
|
69
|
+
try:
|
|
70
|
+
if hasattr(cell, "get_variable_recording"):
|
|
71
|
+
trace = cell.get_variable_recording(variable=variable, section=sec, segx=seg)
|
|
72
|
+
else:
|
|
73
|
+
trace = np.asarray(cell["voltage"], dtype=np.float32)
|
|
74
|
+
data_matrix.append(trace)
|
|
75
|
+
node_id_list.append(nid)
|
|
76
|
+
elem_ids.append(len(elem_ids))
|
|
77
|
+
idx_ptr.append(idx_ptr[-1] + 1)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.warning(f"Failed recording {nid}:{sec_name}@{seg}: {e}")
|
|
80
|
+
|
|
81
|
+
if not data_matrix:
|
|
82
|
+
logger.warning(f"No data for report '{report_name}'.")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
self._write_sonata_report_file(
|
|
86
|
+
self.output_path,
|
|
87
|
+
population,
|
|
88
|
+
data_matrix,
|
|
89
|
+
node_id_list,
|
|
90
|
+
idx_ptr,
|
|
91
|
+
elem_ids,
|
|
92
|
+
self.cfg,
|
|
93
|
+
self.sim_dt,
|
|
94
|
+
tstart
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _write_sonata_report_file(
|
|
98
|
+
self,
|
|
99
|
+
output_path,
|
|
100
|
+
population,
|
|
101
|
+
data_matrix,
|
|
102
|
+
recorded_node_ids,
|
|
103
|
+
index_pointers,
|
|
104
|
+
element_ids,
|
|
105
|
+
report_cfg,
|
|
106
|
+
sim_dt,
|
|
107
|
+
tstart
|
|
108
|
+
):
|
|
109
|
+
"""Write a SONATA HDF5 report file containing time series data.
|
|
110
|
+
|
|
111
|
+
This function downsamples the data if needed, prepares metadata arrays,
|
|
112
|
+
and writes the report in SONATA format to the specified HDF5 file.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
output_path : str or Path
|
|
117
|
+
Destination path of the report file.
|
|
118
|
+
|
|
119
|
+
population : str
|
|
120
|
+
Name of the population being recorded.
|
|
121
|
+
|
|
122
|
+
data_matrix : list of ndarray
|
|
123
|
+
List of arrays containing recorded time series per element.
|
|
124
|
+
|
|
125
|
+
recorded_node_ids : list of int
|
|
126
|
+
Node IDs corresponding to the recorded traces.
|
|
127
|
+
|
|
128
|
+
index_pointers : list of int
|
|
129
|
+
Index pointers mapping node IDs to data.
|
|
130
|
+
|
|
131
|
+
element_ids : list of int
|
|
132
|
+
Element IDs (e.g., segment IDs) corresponding to each trace.
|
|
133
|
+
|
|
134
|
+
report_cfg : dict
|
|
135
|
+
Report configuration specifying time window and variable name.
|
|
136
|
+
|
|
137
|
+
sim_dt : float
|
|
138
|
+
Simulation timestep (ms).
|
|
139
|
+
|
|
140
|
+
tstart : float
|
|
141
|
+
Simulation start time (ms).
|
|
142
|
+
"""
|
|
143
|
+
start_time = float(report_cfg.get("start_time", 0.0))
|
|
144
|
+
end_time = float(report_cfg.get("end_time", 0.0))
|
|
145
|
+
dt_report = float(report_cfg.get("dt", sim_dt))
|
|
146
|
+
|
|
147
|
+
# Clamp dt_report if finer than simuldation dt
|
|
148
|
+
if dt_report < sim_dt:
|
|
149
|
+
logger.warning(
|
|
150
|
+
f"Requested report dt={dt_report} ms is finer than simulation dt={sim_dt} ms. "
|
|
151
|
+
f"Clamping report dt to {sim_dt} ms."
|
|
152
|
+
)
|
|
153
|
+
dt_report = sim_dt
|
|
154
|
+
|
|
155
|
+
step = int(round(dt_report / sim_dt))
|
|
156
|
+
if not np.isclose(step * sim_dt, dt_report, atol=1e-9):
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"dt_report={dt_report} is not an integer multiple of dt_data={sim_dt}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Downsample the data if needed
|
|
162
|
+
# Compute start and end indices in the original data
|
|
163
|
+
start_index = int(round((start_time - tstart) / sim_dt))
|
|
164
|
+
end_index = int(round((end_time - tstart) / sim_dt)) + 1 # inclusive
|
|
165
|
+
|
|
166
|
+
# Now slice and downsample
|
|
167
|
+
data_matrix_downsampled = [
|
|
168
|
+
trace[start_index:end_index:step] for trace in data_matrix
|
|
169
|
+
]
|
|
170
|
+
data_array = np.stack(data_matrix_downsampled, axis=1).astype(np.float32)
|
|
171
|
+
|
|
172
|
+
# Prepare metadata arrays
|
|
173
|
+
node_ids_arr = np.array(recorded_node_ids, dtype=np.uint64)
|
|
174
|
+
index_ptr_arr = np.array(index_pointers, dtype=np.uint64)
|
|
175
|
+
element_ids_arr = np.array(element_ids, dtype=np.uint32)
|
|
176
|
+
time_array = np.array([start_time, end_time, dt_report], dtype=np.float64)
|
|
177
|
+
|
|
178
|
+
# Ensure output directory exists
|
|
179
|
+
output_path = Path(output_path)
|
|
180
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
|
|
182
|
+
# Write to HDF5
|
|
183
|
+
with h5py.File(output_path, "w") as f:
|
|
184
|
+
grp = f.require_group(f"/report/{population}")
|
|
185
|
+
data_ds = grp.create_dataset("data", data=data_array.astype(np.float32))
|
|
186
|
+
|
|
187
|
+
variable = report_cfg.get("variable_name", "v")
|
|
188
|
+
if variable == "v":
|
|
189
|
+
data_ds.attrs["units"] = "mV"
|
|
190
|
+
|
|
191
|
+
mapping = grp.require_group("mapping")
|
|
192
|
+
mapping.create_dataset("node_ids", data=node_ids_arr)
|
|
193
|
+
mapping.create_dataset("index_pointers", data=index_ptr_arr)
|
|
194
|
+
mapping.create_dataset("element_ids", data=element_ids_arr)
|
|
195
|
+
time_ds = mapping.create_dataset("time", data=time_array)
|
|
196
|
+
time_ds.attrs["units"] = "ms"
|
|
@@ -0,0 +1,61 @@
|
|
|
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 Dict, List
|
|
16
|
+
from bluecellulab.reports.writers.base_writer import BaseReportWriter
|
|
17
|
+
import logging
|
|
18
|
+
import numpy as np
|
|
19
|
+
import h5py
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SpikeReportWriter(BaseReportWriter):
|
|
25
|
+
"""Writes SONATA spike report from pop→gid→times mapping."""
|
|
26
|
+
|
|
27
|
+
def write(self, spikes_by_pop: Dict[str, Dict[int, list]]):
|
|
28
|
+
if self.output_path.exists():
|
|
29
|
+
self.output_path.unlink()
|
|
30
|
+
|
|
31
|
+
self.output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
for pop, gid_map in spikes_by_pop.items():
|
|
34
|
+
all_node_ids: List[int] = []
|
|
35
|
+
all_timestamps: List[float] = []
|
|
36
|
+
for node_id, times in gid_map.items():
|
|
37
|
+
all_node_ids.extend([node_id] * len(times))
|
|
38
|
+
all_timestamps.extend(times)
|
|
39
|
+
|
|
40
|
+
if not all_timestamps:
|
|
41
|
+
logger.warning(f"No spikes to write for population '{pop}'.")
|
|
42
|
+
|
|
43
|
+
# Sort by time for consistency
|
|
44
|
+
sorted_indices = np.argsort(all_timestamps)
|
|
45
|
+
node_ids_sorted = np.array(all_node_ids, dtype=np.uint64)[sorted_indices]
|
|
46
|
+
timestamps_sorted = np.array(all_timestamps, dtype=np.float64)[sorted_indices]
|
|
47
|
+
|
|
48
|
+
with h5py.File(self.output_path, 'a') as f:
|
|
49
|
+
spikes_group = f.require_group("spikes")
|
|
50
|
+
if pop in spikes_group:
|
|
51
|
+
logger.warning(f"Overwriting existing group for population '{pop}' in {self.output_path}.")
|
|
52
|
+
del spikes_group[pop]
|
|
53
|
+
|
|
54
|
+
group = spikes_group.create_group(pop)
|
|
55
|
+
sorting_enum = h5py.enum_dtype({'none': 0, 'by_id': 1, 'by_time': 2}, basetype='u1')
|
|
56
|
+
group.attrs.create("sorting", 2, dtype=sorting_enum) # 2 = by_time
|
|
57
|
+
|
|
58
|
+
timestamps_ds = group.create_dataset("timestamps", data=timestamps_sorted)
|
|
59
|
+
group.create_dataset("node_ids", data=node_ids_sorted)
|
|
60
|
+
|
|
61
|
+
timestamps_ds.attrs["units"] = "ms" # SONATA-required
|
|
@@ -1,264 +0,0 @@
|
|
|
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
|
-
import logging
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
import h5py
|
|
19
|
-
from typing import List
|
|
20
|
-
import numpy as np
|
|
21
|
-
import os
|
|
22
|
-
|
|
23
|
-
from bluecellulab.tools import resolve_segments, resolve_source_nodes
|
|
24
|
-
from bluecellulab.cell.cell_dict import CellDict
|
|
25
|
-
|
|
26
|
-
logger = logging.getLogger(__name__)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _configure_recording(cell, report_cfg, source, source_type, report_name):
|
|
30
|
-
variable = report_cfg.get("variable_name", "v")
|
|
31
|
-
|
|
32
|
-
node_id = cell.cell_id
|
|
33
|
-
compartment_nodes = source.get("compartment_set") if source_type == "compartment_set" else None
|
|
34
|
-
|
|
35
|
-
targets = resolve_segments(cell, report_cfg, node_id, compartment_nodes, source_type)
|
|
36
|
-
for sec, sec_name, seg in targets:
|
|
37
|
-
try:
|
|
38
|
-
cell.add_variable_recording(variable=variable, section=sec, segx=seg)
|
|
39
|
-
except AttributeError:
|
|
40
|
-
logger.warning(f"Recording for variable '{variable}' is not implemented in Cell.")
|
|
41
|
-
return
|
|
42
|
-
except Exception as e:
|
|
43
|
-
logger.warning(
|
|
44
|
-
f"Failed to record '{variable}' at {sec_name}({seg}) on GID {node_id} for report '{report_name}': {e}"
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def configure_all_reports(cells, simulation_config):
|
|
49
|
-
report_entries = simulation_config.get_report_entries()
|
|
50
|
-
|
|
51
|
-
for report_name, report_cfg in report_entries.items():
|
|
52
|
-
report_type = report_cfg.get("type", "compartment")
|
|
53
|
-
section = report_cfg.get("sections", "soma")
|
|
54
|
-
|
|
55
|
-
if report_type != "compartment":
|
|
56
|
-
raise NotImplementedError(f"Report type '{report_type}' is not supported.")
|
|
57
|
-
|
|
58
|
-
if section == "compartment_set":
|
|
59
|
-
source_type = "compartment_set"
|
|
60
|
-
source_sets = simulation_config.get_compartment_sets()
|
|
61
|
-
source_name = report_cfg.get("compartments")
|
|
62
|
-
if not source_name:
|
|
63
|
-
logger.warning(f"Report '{report_name}' does not specify a node set in 'compartments' for {source_type}.")
|
|
64
|
-
continue
|
|
65
|
-
else:
|
|
66
|
-
source_type = "node_set"
|
|
67
|
-
source_sets = simulation_config.get_node_sets()
|
|
68
|
-
source_name = report_cfg.get("cells")
|
|
69
|
-
if not source_name:
|
|
70
|
-
logger.warning(f"Report '{report_name}' does not specify a node set in 'cells' for {source_type}.")
|
|
71
|
-
continue
|
|
72
|
-
|
|
73
|
-
source = source_sets.get(source_name)
|
|
74
|
-
if not source:
|
|
75
|
-
logger.warning(f"{source_type.title()} '{source_name}' not found for report '{report_name}', skipping recording.")
|
|
76
|
-
continue
|
|
77
|
-
|
|
78
|
-
population = source["population"]
|
|
79
|
-
node_ids, _ = resolve_source_nodes(source, source_type, cells, population)
|
|
80
|
-
|
|
81
|
-
for node_id in node_ids:
|
|
82
|
-
cell = cells.get((population, node_id))
|
|
83
|
-
if not cell:
|
|
84
|
-
continue
|
|
85
|
-
_configure_recording(cell, report_cfg, source, source_type, report_name)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def write_compartment_report(
|
|
89
|
-
report_name: str,
|
|
90
|
-
output_path: str,
|
|
91
|
-
cells: CellDict,
|
|
92
|
-
report_cfg: dict,
|
|
93
|
-
source_sets: dict,
|
|
94
|
-
source_type: str,
|
|
95
|
-
sim_dt: float
|
|
96
|
-
):
|
|
97
|
-
"""Write a SONATA-compatible compartment report to an HDF5 file.
|
|
98
|
-
|
|
99
|
-
This function collects time series data (e.g., membrane voltage, ion currents)
|
|
100
|
-
from a group of cells defined by either a node set or a compartment set, and
|
|
101
|
-
writes the data to a SONATA-style report file.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
output_path (str): Path to the output HDF5 file.
|
|
105
|
-
cells (CellDict): Mapping of (population, node_id) to cell objects that
|
|
106
|
-
provide access to pre-recorded variable traces.
|
|
107
|
-
report_cfg (dict): Configuration for the report. Must include:
|
|
108
|
-
- "variable_name": Name of the variable to report (e.g., "v", "ica", "ina").
|
|
109
|
-
- "start_time", "end_time", "dt": Timing parameters.
|
|
110
|
-
- "cells" or "compartments": Name of the node or compartment set.
|
|
111
|
-
source_sets (dict): Dictionary of either node sets or compartment sets.
|
|
112
|
-
source_type (str): Either "node_set" or "compartment_set".
|
|
113
|
-
sim_dt (float): Simulation time step used for the recorded data.
|
|
114
|
-
|
|
115
|
-
Raises:
|
|
116
|
-
ValueError: If the specified source set is not found.
|
|
117
|
-
|
|
118
|
-
Notes:
|
|
119
|
-
- Currently supports only variables explicitly handled in Cell.get_variable_recording().
|
|
120
|
-
- Cells without recordings for the requested variable will be skipped.
|
|
121
|
-
"""
|
|
122
|
-
source_name = report_cfg.get("cells") if source_type == "node_set" else report_cfg.get("compartments")
|
|
123
|
-
source = source_sets.get(source_name)
|
|
124
|
-
if not source:
|
|
125
|
-
logger.warning(f"{source_type.title()} '{source_name}' not found for report '{report_name}', skipping write.")
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
population = source["population"]
|
|
129
|
-
|
|
130
|
-
node_ids, compartment_nodes = resolve_source_nodes(source, source_type, cells, population)
|
|
131
|
-
|
|
132
|
-
data_matrix: List[np.ndarray] = []
|
|
133
|
-
recorded_node_ids: List[int] = []
|
|
134
|
-
index_pointers: List[int] = [0]
|
|
135
|
-
element_ids: List[int] = []
|
|
136
|
-
|
|
137
|
-
for node_id in node_ids:
|
|
138
|
-
try:
|
|
139
|
-
cell = cells[(population, node_id)]
|
|
140
|
-
except KeyError:
|
|
141
|
-
continue
|
|
142
|
-
if not cell:
|
|
143
|
-
continue
|
|
144
|
-
|
|
145
|
-
targets = resolve_segments(cell, report_cfg, node_id, compartment_nodes, source_type)
|
|
146
|
-
for sec, sec_name, seg in targets:
|
|
147
|
-
try:
|
|
148
|
-
variable = report_cfg.get("variable_name", "v")
|
|
149
|
-
trace = cell.get_variable_recording(variable=variable, section=sec, segx=seg)
|
|
150
|
-
data_matrix.append(trace)
|
|
151
|
-
recorded_node_ids.append(node_id)
|
|
152
|
-
element_ids.append(len(element_ids))
|
|
153
|
-
index_pointers.append(index_pointers[-1] + 1)
|
|
154
|
-
except Exception as e:
|
|
155
|
-
logger.warning(f"Failed recording: GID {node_id} sec {sec_name} seg {seg}: {e}")
|
|
156
|
-
|
|
157
|
-
if not data_matrix:
|
|
158
|
-
logger.warning(f"No data recorded for report '{source_name}'. Skipping write.")
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
write_sonata_report_file(
|
|
162
|
-
output_path, population, data_matrix, recorded_node_ids, index_pointers, element_ids, report_cfg, sim_dt
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def write_sonata_report_file(
|
|
167
|
-
output_path,
|
|
168
|
-
population,
|
|
169
|
-
data_matrix,
|
|
170
|
-
recorded_node_ids,
|
|
171
|
-
index_pointers,
|
|
172
|
-
element_ids,
|
|
173
|
-
report_cfg,
|
|
174
|
-
sim_dt
|
|
175
|
-
):
|
|
176
|
-
start_time = float(report_cfg.get("start_time", 0.0))
|
|
177
|
-
end_time = float(report_cfg.get("end_time", 0.0))
|
|
178
|
-
dt_report = float(report_cfg.get("dt", sim_dt))
|
|
179
|
-
|
|
180
|
-
# Clamp dt_report if finer than simuldation dt
|
|
181
|
-
if dt_report < sim_dt:
|
|
182
|
-
logger.warning(
|
|
183
|
-
f"Requested report dt={dt_report} ms is finer than simulation dt={sim_dt} ms. "
|
|
184
|
-
f"Clamping report dt to {sim_dt} ms."
|
|
185
|
-
)
|
|
186
|
-
dt_report = sim_dt
|
|
187
|
-
|
|
188
|
-
step = int(round(dt_report / sim_dt))
|
|
189
|
-
if not np.isclose(step * sim_dt, dt_report, atol=1e-9):
|
|
190
|
-
raise ValueError(
|
|
191
|
-
f"dt_report={dt_report} is not an integer multiple of dt_data={sim_dt}"
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
# Downsample the data if needed
|
|
195
|
-
# Compute start and end indices in the original data
|
|
196
|
-
start_index = int(round(start_time / sim_dt))
|
|
197
|
-
end_index = int(round(end_time / sim_dt)) + 1 # inclusive
|
|
198
|
-
|
|
199
|
-
# Now slice and downsample
|
|
200
|
-
data_matrix_downsampled = [
|
|
201
|
-
trace[start_index:end_index:step] for trace in data_matrix
|
|
202
|
-
]
|
|
203
|
-
data_array = np.stack(data_matrix_downsampled, axis=1).astype(np.float32)
|
|
204
|
-
|
|
205
|
-
# Prepare metadata arrays
|
|
206
|
-
node_ids_arr = np.array(recorded_node_ids, dtype=np.uint64)
|
|
207
|
-
index_ptr_arr = np.array(index_pointers, dtype=np.uint64)
|
|
208
|
-
element_ids_arr = np.array(element_ids, dtype=np.uint32)
|
|
209
|
-
time_array = np.array([start_time, end_time, dt_report], dtype=np.float64)
|
|
210
|
-
|
|
211
|
-
# Ensure output directory exists
|
|
212
|
-
output_path = Path(output_path)
|
|
213
|
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
214
|
-
|
|
215
|
-
# Write to HDF5
|
|
216
|
-
with h5py.File(output_path, "w") as f:
|
|
217
|
-
grp = f.require_group(f"/report/{population}")
|
|
218
|
-
data_ds = grp.create_dataset("data", data=data_array.astype(np.float32))
|
|
219
|
-
|
|
220
|
-
variable = report_cfg.get("variable_name", "v")
|
|
221
|
-
if variable == "v":
|
|
222
|
-
data_ds.attrs["units"] = "mV"
|
|
223
|
-
|
|
224
|
-
mapping = grp.require_group("mapping")
|
|
225
|
-
mapping.create_dataset("node_ids", data=node_ids_arr)
|
|
226
|
-
mapping.create_dataset("index_pointers", data=index_ptr_arr)
|
|
227
|
-
mapping.create_dataset("element_ids", data=element_ids_arr)
|
|
228
|
-
time_ds = mapping.create_dataset("time", data=time_array)
|
|
229
|
-
time_ds.attrs["units"] = "ms"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def write_sonata_spikes(f_name: str, spikes_dict: dict[int, np.ndarray], population: str):
|
|
233
|
-
"""Write a SONATA spike group to a spike file from {node_id: [t1, t2,
|
|
234
|
-
...]}."""
|
|
235
|
-
all_node_ids: List[int] = []
|
|
236
|
-
all_timestamps: List[float] = []
|
|
237
|
-
|
|
238
|
-
for node_id, times in spikes_dict.items():
|
|
239
|
-
all_node_ids.extend([node_id] * len(times))
|
|
240
|
-
all_timestamps.extend(times)
|
|
241
|
-
|
|
242
|
-
if not all_timestamps:
|
|
243
|
-
logger.warning(f"No spikes to write for population '{population}'.")
|
|
244
|
-
|
|
245
|
-
# Sort by time for consistency
|
|
246
|
-
sorted_indices = np.argsort(all_timestamps)
|
|
247
|
-
node_ids_sorted = np.array(all_node_ids, dtype=np.uint64)[sorted_indices]
|
|
248
|
-
timestamps_sorted = np.array(all_timestamps, dtype=np.float64)[sorted_indices]
|
|
249
|
-
|
|
250
|
-
os.makedirs(os.path.dirname(f_name), exist_ok=True)
|
|
251
|
-
with h5py.File(f_name, 'a') as f: # 'a' to allow multiple writes
|
|
252
|
-
spikes_group = f.require_group("spikes")
|
|
253
|
-
if population in spikes_group:
|
|
254
|
-
logger.warning(f"Overwriting existing group for population '{population}' in {f_name}.")
|
|
255
|
-
del spikes_group[population]
|
|
256
|
-
|
|
257
|
-
group = spikes_group.create_group(population)
|
|
258
|
-
sorting_enum = h5py.enum_dtype({'none': 0, 'by_id': 1, 'by_time': 2}, basetype='u1')
|
|
259
|
-
group.attrs.create("sorting", 2, dtype=sorting_enum) # 2 = by_time
|
|
260
|
-
|
|
261
|
-
timestamps_ds = group.create_dataset("timestamps", data=timestamps_sorted)
|
|
262
|
-
group.create_dataset("node_ids", data=node_ids_sorted)
|
|
263
|
-
|
|
264
|
-
timestamps_ds.attrs["units"] = "ms" # SONATA-required
|
|
@@ -88,7 +88,7 @@ class Simulation:
|
|
|
88
88
|
|
|
89
89
|
def run(
|
|
90
90
|
self,
|
|
91
|
-
|
|
91
|
+
tstop: float,
|
|
92
92
|
cvode=True,
|
|
93
93
|
cvode_minstep=None,
|
|
94
94
|
cvode_maxstep=None,
|
|
@@ -107,10 +107,10 @@ class Simulation:
|
|
|
107
107
|
show_progress = bluecellulab.VERBOSE_LEVEL > 1
|
|
108
108
|
|
|
109
109
|
if show_progress:
|
|
110
|
-
self.progress_dt =
|
|
110
|
+
self.progress_dt = tstop / 100
|
|
111
111
|
self.init_progress_callback()
|
|
112
112
|
|
|
113
|
-
neuron.h.tstop =
|
|
113
|
+
neuron.h.tstop = tstop
|
|
114
114
|
|
|
115
115
|
cvode_old_status = neuron.h.cvode_active()
|
|
116
116
|
if cvode:
|
|
@@ -138,10 +138,9 @@ class Simulation:
|
|
|
138
138
|
# initialized heavily influence the random number generator
|
|
139
139
|
# e.g. finitialize() + step() != run()
|
|
140
140
|
|
|
141
|
-
logger.debug(f'Running a simulation until {maxtime} ms ...')
|
|
142
|
-
|
|
143
141
|
self.init_callbacks()
|
|
144
142
|
|
|
143
|
+
logger.debug(f'Running a simulation until {tstop} ms ...')
|
|
145
144
|
neuron.h.stdinit()
|
|
146
145
|
|
|
147
146
|
if forward_skip:
|
|
@@ -152,7 +151,7 @@ class Simulation:
|
|
|
152
151
|
for _ in range(0, 10):
|
|
153
152
|
neuron.h.fadvance()
|
|
154
153
|
neuron.h.dt = save_dt
|
|
155
|
-
neuron.h.t =
|
|
154
|
+
neuron.h.t = forward_skip_value
|
|
156
155
|
|
|
157
156
|
if self.pc is not None:
|
|
158
157
|
for cell in self.cells:
|