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.

@@ -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
- maxtime: float,
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 = maxtime / 100
110
+ self.progress_dt = tstop / 100
111
111
  self.init_progress_callback()
112
112
 
113
- neuron.h.tstop = maxtime
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 = 0.0
154
+ neuron.h.t = forward_skip_value
156
155
 
157
156
  if self.pc is not None:
158
157
  for cell in self.cells: