• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openbraininstitute / neurodamus / 25438339682

06 May 2026 01:30PM UTC coverage: 91.283% (+0.005%) from 91.278%
25438339682

Pull #517

github

web-flow
Merge 53fe35f34 into 6684bec2f
Pull Request #517: adapt to new libsonata

28 of 33 new or added lines in 3 files covered. (84.85%)

35 existing lines in 1 file now uncovered.

7843 of 8592 relevant lines covered (91.28%)

2.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.7
/neurodamus/node.py
1
# Neurodamus
2
# Copyright 2018 - Blue Brain Project, EPFL
3
from __future__ import annotations
5✔
4

5
from typing import TYPE_CHECKING
5✔
6

7
if TYPE_CHECKING:
8
    from neurodamus.report_parameters import ReportParameters
9

10
import gc
5✔
11
import glob
5✔
12
import itertools
5✔
13
import logging
5✔
14
import math
5✔
15
import os
5✔
16
import shutil
5✔
17
from collections import defaultdict
5✔
18
from contextlib import contextmanager
5✔
19
from os import path as ospath
5✔
20
from pathlib import Path
5✔
21

22
import libsonata
5✔
23

24
# Internal Engine imports
25
from . import (
5✔
26
    allen_point as _allen_point,  # noqa: F401
27
    ngv as _ngv,
28
)
29
from .allen_point import Exp2SynSynapseRuleManager
5✔
30
from .cell_distributor import (
5✔
31
    CellDistributor,
32
    GlobalCellManager,
33
    LoadBalance,
34
    LoadBalanceMode,
35
    VirtualCellPopulation,
36
)
37
from .connection_manager import SynapseRuleManager, edge_node_pop_names
5✔
38
from .core import (
5✔
39
    MPI,
40
    NeuronWrapper as Nd,
41
    SimulationProgress,
42
    mpi_no_errors,
43
    return_neuron_timings,
44
    run_only_rank0,
45
)
46
from .core._engine import EngineBase
5✔
47
from .core._shmutils import SHMUtil
5✔
48
from .core.configuration import (
5✔
49
    CircuitConfig,
50
    ConfigurationError,
51
    Feature,
52
    GlobalConfig,
53
    SimConfig,
54
    _SimConfig,
55
    get_debug_cell_gids,
56
    make_circuit_config,
57
)
58
from .core.coreneuron_configuration import (
5✔
59
    CompartmentMapping,
60
    CoreConfig,
61
)
62
from .core.nodeset import PopulationNodes
5✔
63
from .gap_junction import GapJunctionManager
5✔
64
from .io.sonata_config import ConnectionTypes, RunConfig
5✔
65
from .modification_manager import ModificationManager
5✔
66
from .neuromodulation_manager import NeuroModulationManager
5✔
67
from .replay import MissingSpikesPopulationError, SpikeManager
5✔
68
from .report import ReportManager
5✔
69
from .report_parameters import (
5✔
70
    check_report_parameters,
71
    create_report_parameters,
72
)
73
from .stimulus_manager import StimulusManager
5✔
74
from .target_manager import TargetManager, TargetSpec
5✔
75
from .utils.logging import log_stage, log_verbose
5✔
76
from .utils.memory import DryRunStats, free_event_queues, pool_shrink, print_mem_usage, trim_memory
5✔
77
from .utils.pyutils import cache_errors
5✔
78
from .utils.timeit import TimerManager, timeit
5✔
79
from neurodamus.core.coreneuron_report_config import CoreReportConfig, CoreReportConfigEntry
5✔
80
from neurodamus.core.coreneuron_simulation_config import CoreSimulationConfig
5✔
81
from neurodamus.stimulus_manager import SpatiallyUniformEField
5✔
82
from neurodamus.utils.pyutils import CumulativeError, rmtree
5✔
83

84

85
class METypeEngine(EngineBase):
5✔
86
    CellManagerCls = CellDistributor
5✔
87
    InnerConnectivityCls = SynapseRuleManager
5✔
88
    ConnectionTypes = {
5✔
89
        None: SynapseRuleManager,
90
        ConnectionTypes.Synaptic: SynapseRuleManager,
91
        ConnectionTypes.GapJunction: GapJunctionManager,
92
        ConnectionTypes.NeuroModulation: NeuroModulationManager,
93
        ConnectionTypes.Exp2Syn: Exp2SynSynapseRuleManager,
94
    }
95
    CircuitPrecedence = 0
5✔
96

97

98
class CircuitManager:
5✔
99
    """Holds and manages populations and associated nodes and edges
100

101
    All nodes must have a name or read from sonata pop name
102
    As so, Sonata is preferred when using multiple node files
103
    """
104

105
    def __init__(self):
5✔
106
        self.node_managers = {}  # dict {pop_name -> cell_manager}  # nrn pop is None
4✔
107
        self.virtual_node_managers = {}  # same, but for virtual ones (no cells)
4✔
108
        # dict {(src_pop, dst_pop) -> list[synapse_manager]}
109
        self.edge_managers = defaultdict(list)
4✔
110
        self.alias = {}  # dict {name -> pop_name}
4✔
111
        self.global_manager = GlobalCellManager()
4✔
112
        self.global_target = TargetManager.create_global_target()
4✔
113

114
    def initialized(self):
5✔
115
        return bool(self.node_managers)
4✔
116

117
    def register_node_manager(self, cell_manager):
5✔
118
        pop = cell_manager.population_name
4✔
119
        if pop in self.node_managers:
4✔
120
            raise ConfigurationError(f"Already existing node manager for population {pop}")
×
121
        self.node_managers[pop] = cell_manager
4✔
122
        self.alias[cell_manager.circuit_name] = pop
4✔
123
        self.global_manager.register_manager(cell_manager)
4✔
124
        if cell_manager.is_initialized():
4✔
125
            self.global_target.append_nodeset(cell_manager.local_nodes)
4✔
126

127
    def _new_virtual_node_manager(self, circuit):
5✔
128
        """Instantiate a new virtual node manager explicitly."""
129
        pop = libsonata.NodeStorage(circuit.CellLibraryFile).open_population(circuit.PopulationName)
1✔
130
        virtual_cell_manager = VirtualCellPopulation(circuit.PopulationName, list(range(pop.size)))
1✔
131
        self.virtual_node_managers[circuit.PopulationName] = virtual_cell_manager
1✔
132
        self.global_target.append_nodeset(virtual_cell_manager.local_nodes)
1✔
133
        return virtual_cell_manager
1✔
134

135
    @staticmethod
5✔
136
    def new_node_manager_bare(circuit: CircuitConfig, target_manager, run_conf, **kwargs):
5✔
137
        engine = circuit.Engine or METypeEngine
4✔
138
        CellManagerCls = engine.CellManagerCls or CellDistributor
4✔
139
        return CellManagerCls(circuit, target_manager, run_conf, **kwargs)
4✔
140

141
    def new_node_manager(self, circuit, target_manager, run_conf, *, load_balancer=None, **kwargs):
5✔
142
        if circuit.get("PopulationType") == "virtual":
4✔
143
            return self._new_virtual_node_manager(circuit)
1✔
144
        cell_manager = self.new_node_manager_bare(circuit, target_manager, run_conf, **kwargs)
4✔
145
        cell_manager.load_nodes(load_balancer, **kwargs)
4✔
146
        self.register_node_manager(cell_manager)
4✔
147
        return cell_manager
4✔
148

149
    def get_node_manager(self, name):
5✔
150
        name = self.alias.get(name, name)
3✔
151
        return self.node_managers.get(name)
3✔
152

153
    def has_population(self, pop_name):
5✔
154
        return pop_name in self.node_managers
4✔
155

156
    def unalias_pop_keys(self, source, destination):
5✔
157
        """Un-alias population names"""
158
        return self.alias.get(source, source), self.alias.get(destination, destination)
4✔
159

160
    def get_edge_managers(self, source, destination):
5✔
161
        edge_pop_keys = self.unalias_pop_keys(source, destination)
4✔
162
        return self.edge_managers.get(edge_pop_keys) or []
4✔
163

164
    def get_edge_manager(self, source, destination, conn_type=SynapseRuleManager):
5✔
165
        managers = [
4✔
166
            manager
167
            for manager in self.get_edge_managers(source, destination)
168
            if isinstance(manager, conn_type)
169
        ]
170
        return managers[0] if managers else None
4✔
171

172
    def get_create_edge_manager(
5✔
173
        self, conn_type, source, destination, src_target, manager_args=(), **kw
174
    ):
175
        source, destination = self.unalias_pop_keys(source, destination)
4✔
176
        manager = self.get_edge_manager(source, destination, conn_type)
4✔
177
        if manager:
4✔
178
            return manager
×
179

180
        if not self.has_population(destination):
4✔
181
            raise ConfigurationError("Can't find projection Node population: " + destination)
×
182

183
        src_manager = self.node_managers.get(source) or self.virtual_node_managers.get(source)
4✔
184
        if src_manager is None:  # src manager may not exist -> virtual
4✔
185
            log_verbose("No known population %s. Creating Virtual src for projection", source)
1✔
186
            if conn_type not in {SynapseRuleManager, _ngv.GlioVascularManager}:
1✔
187
                raise ConfigurationError("Custom connections require instantiated source nodes")
×
188
            src_manager = VirtualCellPopulation(source, None, src_target.name)
1✔
189

190
        target_cell_manager = kw["cell_manager"] = self.node_managers[destination]
4✔
191
        kw["src_cell_manager"] = src_manager
4✔
192
        manager = conn_type(*manager_args, **kw)
4✔
193
        self.edge_managers[source, destination].append(manager)
4✔
194
        target_cell_manager.register_connection_manager(manager)
4✔
195
        return manager
4✔
196

197
    def all_node_managers(self):
5✔
198
        return self.node_managers.values()
4✔
199

200
    def all_synapse_managers(self):
5✔
201
        return itertools.chain.from_iterable(self.edge_managers.values())
4✔
202

203
    @staticmethod
5✔
204
    @run_only_rank0
5✔
205
    def write_population_offsets(pop_offsets, alias_pop, virtual_pop_offsets):
5✔
206
        """Write population_offsets where appropriate
207

208
        It is needed for retrieving population offsets for reporting and replay at restore time.
209

210
        Format population name::gid offset::population alias
211
        The virtual population offset is also written for synapse replay in restore.
212
        The data comes from outside because pop_offsets are not initialized
213
        in a restore scenario.
214
        """
215
        # populations_offset is necessary in output_path
216
        output_path = SimConfig.populations_offset_output_path(create=True)
4✔
217

218
        with open(output_path, "w", encoding="utf-8") as f:
4✔
219
            f.writelines(
4✔
220
                "{}::{}::{}\n".format(pop or " ", pop_offsets[pop], alias or " ")
221
                for alias, pop in alias_pop.items()
222
            )
223
            f.writelines(
4✔
224
                "{}::{}::{}\n".format(pop, offset, "virtual")
225
                for pop, offset in virtual_pop_offsets.items()
226
            )
227

228
        # Add a file in save_path too if required
229
        if SimConfig.save:
4✔
230
            save_path = SimConfig.populations_offset_save_path(create=True)
1✔
231
            shutil.copy(output_path, save_path)
1✔
232

233
    def get_population_offsets(self):
5✔
234
        pop_offsets = {
4✔
235
            pop_name: node_manager.local_nodes.offset
236
            for pop_name, node_manager in self.node_managers.items()
237
        }
238
        alias_pop = dict(self.alias)
4✔
239
        return pop_offsets, alias_pop
4✔
240

241
    def get_virtual_population_offsets(self):
5✔
242
        pop_offsets = {
4✔
243
            pop_name: node_manager.local_nodes.offset
244
            for pop_name, node_manager in self.virtual_node_managers.items()
245
        }
246
        return pop_offsets
4✔
247

248
    @classmethod
5✔
249
    def read_population_offsets(cls, file_path=None):
5✔
250
        """Read population offsets from populations_offset.dat"""
251
        pop_offsets = {}
1✔
252
        alias_pop = {}
1✔
253
        virtual_pop_offsets = {}
1✔
254
        with open(file_path or SimConfig.populations_offset_restore_path(), encoding="utf-8") as f:
1✔
255
            for line in f:
1✔
256
                pop, offset, alias = line.strip().split("::")
1✔
257
                pop = pop or None
1✔
258
                alias = alias or None
1✔
259
                if alias == "virtual":
1✔
260
                    virtual_pop_offsets[pop] = int(offset)
×
261
                else:
262
                    pop_offsets[pop] = int(offset)
1✔
263
                    alias_pop[alias] = pop
1✔
264

265
        return pop_offsets, alias_pop, virtual_pop_offsets
1✔
266

267
    def __del__(self):
5✔
268
        """De-init. Edge managers must be destructed first"""
269
        del self.edge_managers
4✔
270
        del self.virtual_node_managers
4✔
271
        del self.node_managers
4✔
272

273

274
class Node:
5✔
275
    """The Node class is the main entity for a distributed Neurodamus execution.
276

277
    Note that this concept of a "Node" differs from both an MPI node, which
278
    refers to a process in a parallel computing environment, and a node in the
279
    circuit graph, which represents an individual element or component within
280
    the simulation's neural network.
281

282
    It serves as the orchestrator for the entire simulation, managing the
283
    parallel execution of the model and distributing the cells across different
284
    computational ranks. As the primary control structure, Node is responsible
285
    for coordinating various components involved in the simulation.
286

287
    Internally, the Node class instantiates and manages parallel structures,
288
    dividing the simulation workload among multiple ranks. With the introduction
289
    of the concept of multiple populations (also known as multi-circuit), the
290
    Node class takes partial responsibility for handling this logic, aided by
291
    the :class:`neurodamus.node.CircuitManager` class (accessible via the
292
    `circuits` property), which manages the different node and edge managers.
293

294
    While many lower-level details of the Node's functionality are encapsulated
295
    within dedicated helper classes, the Node class still exposes an API that
296
    allows advanced users to control and inspect almost every major step of the
297
    simulation. For a standard run, users are encouraged to use the higher-level
298
    `Neurodamus` class instead, which simplifies some of the complexities
299
    handled by Node.
300

301
    The Node class exposes the following public properties:
302

303
    - `circuits`: is a :class:`neurodamus.node.CircuitManager` object,
304
      responsible for managing multiple node and edge managers within the
305
      simulation.
306
    - `target_manager`: is a :class:`neurodamus.target_manager.TargetManager`
307
      object, responsible for managing the targets in the simulation.
308
    - `stimulus_manager`: is a
309
      :class:`neurodamus.stimulus_manager.StimulusManager` object, responsible
310
      for interpreting and instantiating stimulus events.
311
    - `elec_manager`: The electrode manager, which controls the interaction with
312
      simulation electrodes.
313
    - `reports`: A list of Neurodamus Report `hoc` objects, used to generate
314
      simulation reports.
315

316
    Note that, while the Node object owns and manages most of the top-level
317
    objects in the simulation, the management of cell and synapse objects has
318
    been delegated to the `Circuits` class, as these are now handled at a lower
319
    level.
320

321
    Technical note:
322

323
    - The properties exposed by Node are read-only, with most internal
324
      attributes being prefixed with an underscore (`_`). Notable internal
325
      attributes include:
326

327
      `self._sonata_circuits`: The SONATA circuits used by the Node
328
      each represents a node population.
329

330
    These details make the Node class versatile and powerful for advanced users
331
    who need more granular control over the simulation process.
332
    """
333

334
    _default_population = "All"
5✔
335
    """The default population name for e.g. Reports."""
5✔
336

337
    def __init__(
5✔
338
        self, config_path_or_obj: str | libsonata.SimulationConfig, options: dict | None = None
339
    ):
340
        """Creates a neurodamus executor
341

342
        Args:
343
            config_path_or_obj: Path to a SONATA simulation config JSON file,
344
                or a ``libsonata.SimulationConfig`` instance.
345
            options: A dictionary of run options typically coming from cmd line
346
        """
347
        options = options or {}
4✔
348

349
        sim_config_obj, config_file = self._get_simulation_config(config_path_or_obj)
4✔
350
        Nd.init(
4✔
351
            log_filename=sim_config_obj.output.log_file,
352
            log_use_color=options.pop("use_color", True),
353
        )
354

355
        # This is global initialization, happening once, regardless of number of
356
        # cycles
357
        log_stage("Setting up Neurodamus configuration")
4✔
358
        SimConfig.init(sim_config_obj, options, config_file=config_file)
4✔
359

360
        self._pc = Nd.pc
4✔
361
        self._spike_vecs = []
4✔
362
        self._spike_populations = []
4✔
363
        Nd.execute("cvode = new CVode()")
4✔
364

365
        if SimConfig.use_coreneuron:
4✔
366
            # Instantiate the CoreNEURON artificial cell object which is used to fill up
367
            # the empty ranks. This need to be done before the circuit is
368
            # finitialized
369
            CoreConfig.instantiate_artificial_cell()
2✔
370

371
        self._run_conf = SimConfig.run_conf
4✔
372
        self._target_manager = TargetManager(self._run_conf)
4✔
373
        self._target_spec = TargetSpec(self._run_conf.nodeset_name, self._run_conf.population_name)
4✔
374
        if SimConfig.use_neuron or SimConfig.coreneuron_direct_mode:
4✔
375
            self._sonatareport_helper = Nd.SonataReportHelper(Nd.dt, True)  # noqa: FBT003
4✔
376
        self._sonata_circuits = SimConfig.sonata_circuits
4✔
377
        self._dump_cell_state_gids = get_debug_cell_gids(options)
4✔
378
        self._core_replay_file = ""
4✔
379
        self._is_ngv_run = any(
4✔
380
            c.Engine.__name__ == "NGVEngine" for c in self._sonata_circuits.values() if c.Engine
381
        )
382
        self._initial_rss = 0
4✔
383
        self._cycle_i = 0
4✔
384
        self._n_cycles = 1
4✔
385
        self._shm_enabled = False
4✔
386
        self._dry_run_stats = None
4✔
387

388
        self._reset()
4✔
389

390
    @staticmethod
5✔
391
    def _get_simulation_config(config_path_or_obj):
5✔
392
        """Resolve a path or SimulationConfig object into a validated SimulationConfig.
393

394
        Args:
395
            config_path_or_obj: Path to a SONATA simulation config JSON file,
396
                or a ``libsonata.SimulationConfig`` instance.
397

398
        Returns:
399
            Tuple of (SimulationConfig object, config file path or None)
400
        """
401
        if isinstance(config_path_or_obj, str):
4✔
402
            if not config_path_or_obj:
4✔
NEW
403
                raise ConfigurationError("`config_path_or_obj` cannot be empty")
×
404
            if not config_path_or_obj.endswith(".json"):
4✔
NEW
405
                raise ConfigurationError(
×
406
                    "Invalid configuration file format."
407
                    " The configuration file must be a .json file."
408
                )
409
            if not Path(config_path_or_obj).is_file():
4✔
NEW
410
                raise ConfigurationError("Config file not found: " + config_path_or_obj)
×
411
            return libsonata.SimulationConfig.from_file(config_path_or_obj), config_path_or_obj
4✔
412
        if isinstance(config_path_or_obj, libsonata.SimulationConfig):
1✔
413
            return config_path_or_obj, None
1✔
NEW
414
        raise ConfigurationError(
×
415
            f"Invalid config_path_or_obj type: {type(config_path_or_obj)}. "
416
            "Expected a file path (str) or libsonata.SimulationConfig."
417
        )
418

419
    def _reset(self):
5✔
420
        """Resets internal state for a new simulation cycle.
421

422
        Ensures `_run_conf` is a valid dictionary, initializes core attributes,
423
        and registers global targets and cell managers.
424

425
        Note: remember to call Nd.init(...) before to ensure/load neurodamus mods
426
        """
427
        if not self._run_conf or not isinstance(self._run_conf, RunConfig):
4✔
428
            raise ValueError("Invalid `_run_conf`: Must be a dictionary for multi-cycle runs.")
×
429

430
        # Init unconditionally
431
        self._circuits = CircuitManager()
4✔
432
        self._stim_list = None
4✔
433
        self._report_list = None
4✔
434
        self._stim_manager = None
4✔
435
        self._sim_ready = False
4✔
436
        # flag to mark what we already dumped
437
        self._last_cell_state_dump_t = None
4✔
438

439
        self._bbss = Nd.BBSaveState()
4✔
440

441
        # Register the global target and cell manager
442
        self._target_manager.register_target(self._circuits.global_target)
4✔
443
        self._target_manager.register_cell_manager(self._circuits.global_manager)
4✔
444

445
    # public 'read-only' properties - object modification on user responsibility
446
    circuits = property(lambda self: self._circuits)
5✔
447
    target_manager = property(lambda self: self._target_manager)
5✔
448
    stim_manager = property(lambda self: self._stim_manager)
5✔
449
    stims = property(lambda self: self._stim_list)
5✔
450
    reports = property(lambda self: self._report_list)
5✔
451

452
    def all_circuits(self):
5✔
453
        yield from self._sonata_circuits.values()
4✔
454

455
    # -
456
    def load_targets(self):
5✔
457
        """Initialize targets. Nodesets are loaded on demand."""
458
        for circuit in self.all_circuits():
4✔
459
            log_verbose("Loading targets for circuit %s", circuit.name or "(default)")
4✔
460
            self._target_manager.load_targets(circuit)
4✔
461

462
    # -
463
    @mpi_no_errors
5✔
464
    @timeit(name="Compute LB")
5✔
465
    def compute_load_balance(self):
5✔
466
        """In case the user requested load-balance this function instantiates a
467
        CellDistributor to split cells and balance those pieces across the available CPUs.
468
        """
469
        log_stage("Computing Load Balance")
4✔
470
        circuit = None
4✔
471
        for name, circuit in self._sonata_circuits.items():
4✔
472
            if circuit.get("PopulationType") != "virtual":
4✔
473
                logging.info("Activating experimental LB for Sonata circuit '%s'", name)
4✔
474
                break
4✔
475
        if circuit is None:
4✔
476
            logging.warning(
×
477
                "Cannot calculate the load balance because no non-virtual circuit is found"
478
            )
479
            return None
×
480

481
        if not circuit.CellLibraryFile:
4✔
482
            logging.info(" => No circuit for Load Balancing. Skipping... ")
×
483
            return None
×
484

485
        _ = PopulationNodes.offset_freezer()  # Dont offset while in loadbal
4✔
486

487
        # Info about the cells to be distributed
488
        target_spec = TargetSpec(circuit.NodesetName, circuit.PopulationName)
4✔
489
        target = self.target_manager.get_target(target_spec)
4✔
490

491
        # Check / set load balance mode
492
        lb_mode = LoadBalance.select_lb_mode(SimConfig, self._run_conf, target)
4✔
493
        if lb_mode == LoadBalanceMode.RoundRobin:
4✔
494
            return None
4✔
495
        if lb_mode == LoadBalanceMode.Memory:
2✔
496
            logging.info("Load Balancing ENABLED. Mode: Memory")
2✔
497
            return self._memory_mode_load_balancing()
2✔
498

499
        # Build load balancer as per requested options
500
        node_path = circuit.CellLibraryFile
2✔
501
        pop = target_spec.population
2✔
502
        load_balancer = LoadBalance(lb_mode, node_path, pop, self._target_manager)
2✔
503

504
        if load_balancer.valid_load_distribution(target_spec):
2✔
505
            logging.info("Load Balancing done.")
1✔
506
            return load_balancer
1✔
507

508
        logging.info("Could not reuse load balance data. Doing a Full Load-Balance")
2✔
509
        cell_dist = self._circuits.new_node_manager(circuit, self._target_manager, self._run_conf)
2✔
510
        with load_balancer.generate_load_balance(target_spec, cell_dist):
2✔
511
            # Instantiate the circuit cells and synapses to evaluate complexities
512
            cell_dist.finalize()
2✔
513
            self._circuits.global_manager.finalize()
2✔
514
            SimConfig.update_connection_blocks(self._circuits.alias)
2✔
515
            target_manager = self._target_manager
2✔
516
            self._create_synapse_manager(SynapseRuleManager, circuit, target_manager)
2✔
517

518
        # reset since we instantiated with RR distribution
519
        Nd.t = 0.0  # Reset time
2✔
520
        self.clear_model()
2✔
521

522
        return load_balancer
2✔
523

524
    def _memory_mode_load_balancing(self):
5✔
525
        filename = f"allocation_r{MPI.size}_c{SimConfig.modelbuilding_steps}.pkl.gz"
2✔
526

527
        file_exists = ospath.exists(filename)
2✔
528
        MPI.barrier()
2✔
529

530
        self._dry_run_stats = DryRunStats()
2✔
531
        if file_exists:
2✔
532
            alloc = self._dry_run_stats.import_allocation_stats(filename, self._cycle_i)
×
533
        else:
534
            logging.warning("Allocation file not found. Generating on-the-fly.")
2✔
535

536
            self._dry_run_stats.try_import_cell_memory_usage()
2✔
537
            if not self._dry_run_stats.cell_memory_usage.preloaded:
2✔
538
                logging.warning("Cell memory usage file not found. Computing on-the-fly.")
1✔
539
                for circuit in self._sonata_circuits.values():
1✔
540
                    if circuit.get("PopulationType") == "biophysical":
1✔
541
                        cell_distributor = self._circuits.new_node_manager(
1✔
542
                            circuit,
543
                            self._target_manager,
544
                            self._run_conf,
545
                            loader_opts={
546
                                "load_mode": "load_nodes_metype",
547
                                "dry_run_stats": self._dry_run_stats,
548
                            },
549
                        )
550
                        cell_distributor.finalize(dry_run_stats_obj=self._dry_run_stats)
1✔
551

552
                        # Compute synapse memory usage per metype
553
                        self._circuits.global_manager.finalize()
1✔
554
                        SimConfig.update_connection_blocks(self._circuits.alias)
1✔
555
                        self._create_synapse_manager(
1✔
556
                            SynapseRuleManager,
557
                            circuit,
558
                            self._target_manager,
559
                            dry_run_stats=self._dry_run_stats,
560
                            get_conn_stats=True,
561
                        )
562

563
                # include projection edges
564
                for pname, projection in SimConfig.projections.items():
1✔
565
                    self._load_projections(
1✔
566
                        pname, projection, dry_run_stats=self._dry_run_stats, get_conn_stats=True
567
                    )
568

569
                self._dry_run_stats.collect_all_mpi()
1✔
570
                self._dry_run_stats.export_cell_memory_usage()
1✔
571

572
            alloc, _, _ = self._dry_run_stats.distribute_cells_with_validation(
2✔
573
                MPI.size, SimConfig.modelbuilding_steps
574
            )
575

576
            # reset since we instantiated
577
            Nd.t = 0.0  # Reset time
2✔
578
            self.clear_model()
2✔
579

580
        for pop, ranks in alloc.items():
2✔
581
            for rank, gids in ranks.items():
2✔
582
                logging.debug("Population: %s, Rank: %s, Number of GIDs: %s", pop, rank, len(gids))
2✔
583
        return alloc
2✔
584

585
    # -
586
    @mpi_no_errors
5✔
587
    @timeit(name="Cell creation")
5✔
588
    def create_cells(self, load_balance=None):
5✔
589
        """Instantiate and distributes the cells of the network.
590
        Any targets will be updated to know which cells are local to the cpu.
591
        """
592
        if SimConfig.dry_run:
4✔
593
            logging.info("Memory usage after inizialization:")
2✔
594
            print_mem_usage()
2✔
595
            loader_opts = {"dry_run_stats": self._dry_run_stats}
2✔
596
        else:
597
            loader_opts = {}
4✔
598

599
        loader_opts["cycle_i"] = self._cycle_i
4✔
600

601
        # Check dynamic attributes required before loading cells
602
        SimConfig.check_cell_requirements(self.target_manager)
4✔
603

604
        log_stage("LOADING NODES")
4✔
605
        config = SimConfig.cli_options
4✔
606
        if not load_balance:
4✔
607
            logging.info("Load-balance object not present. Continuing Round-Robin...")
4✔
608

609
        for name, circuit in self._sonata_circuits.items():
4✔
610
            log_stage("Circuit %s", name)
4✔
611
            if config.restrict_node_populations and name not in config.restrict_node_populations:
4✔
612
                logging.warning("Skipped node population (restrict_node_populations)")
×
613
                continue
×
614
            self._circuits.new_node_manager(
4✔
615
                circuit,
616
                self._target_manager,
617
                self._run_conf,
618
                load_balancer=load_balance,
619
                loader_opts=loader_opts,
620
            )
621

622
        lfp_weights_file = self._run_conf.lfp_weights_path
4✔
623
        if lfp_weights_file and self._run_conf.enable_reports:
4✔
624
            if SimConfig.use_coreneuron:
1✔
625
                lfp_manager = self._circuits.global_manager._lfp_manager
1✔
626
                cell_managers = self._circuits.global_manager._cell_managers
1✔
627
                population_list = [
1✔
628
                    manager.population_name
629
                    for manager in cell_managers
630
                    if manager.population_name is not None
631
                ]
632
                lfp_manager.load_lfp_config(lfp_weights_file, population_list)
1✔
633
            else:
634
                logging.warning("LFP supported only with CoreNEURON.")
×
635

636
        PopulationNodes.freeze_offsets()  # Dont offset further, could change gids
4✔
637

638
        # Let the cell managers have any final say in the cell objects
639
        log_stage("FINALIZING CIRCUIT CELLS")
4✔
640

641
        for cell_manager in self._circuits.all_node_managers():
4✔
642
            log_stage("Circuit %s", cell_manager.circuit_name or "(default)")
4✔
643
            if SimConfig.dry_run:
4✔
644
                cell_manager.finalize(dry_run_stats_obj=self._dry_run_stats)
2✔
645
            else:
646
                cell_manager.finalize()
4✔
647

648
        # Final bits after we have all cell managers
649
        self._circuits.global_manager.finalize()
4✔
650
        SimConfig.update_connection_blocks(self._circuits.alias)
4✔
651

652
    # -
653
    @mpi_no_errors
5✔
654
    @timeit(name="Synapse creation")
5✔
655
    def create_synapses(self):
5✔
656
        """Create synapses among the cells, handling connections that appear in the config file"""
657
        log_stage("LOADING CIRCUIT CONNECTIVITY")
4✔
658
        target_manager = self._target_manager
4✔
659
        manager_kwa = {
4✔
660
            "load_offsets": self._is_ngv_run,
661
            "dry_run_stats": self._dry_run_stats,
662
        }
663

664
        for circuit in self._sonata_circuits.values():
4✔
665
            Engine = circuit.Engine or METypeEngine
4✔
666
            SynManagerCls = Engine.InnerConnectivityCls
4✔
667
            self._create_synapse_manager(SynManagerCls, circuit, target_manager, **manager_kwa)
4✔
668

669
        MPI.check_no_errors()
4✔
670
        log_stage("Handling projections...")
4✔
671
        for pname, projection in SimConfig.projections.items():
4✔
672
            self._load_projections(pname, projection, **manager_kwa)
4✔
673

674
        if SimConfig.dry_run:
4✔
675
            self.syn_total_memory = self._dry_run_stats.collect_display_syn_counts()
2✔
676
            return
2✔
677

678
        log_stage("Configuring connections...")
4✔
679
        for conn_conf in SimConfig.connections:
4✔
680
            self._process_connection_configure(conn_conf)
2✔
681

682
        logging.info("Done, but waiting for all ranks")
4✔
683

684
    def _create_synapse_manager(self, ctype, conf, *args, **kwargs):
5✔
685
        """Create a synapse manager for intra-circuit connectivity"""
686
        log_stage("Circuit %s", conf.name or "(default)")
4✔
687
        if not conf.get("nrnPath"):
4✔
688
            logging.info(" => No connectivity set as internal. See projections")
3✔
689
            return
3✔
690

691
        if SimConfig.cli_options.restrict_connectivity >= 2:
4✔
692
            logging.warning("Skipped connectivity (restrict_connectivity)")
×
693
            return
×
694
        c_target = TargetSpec(conf.get("NodesetName"), conf.get("PopulationName"))
4✔
695
        if c_target.population is None:
4✔
696
            c_target.population = self._circuits.alias.get(conf.name)
×
697

698
        edge_file, *pop = conf.get("nrnPath").split(":")
4✔
699
        edge_pop = pop[0] if pop else None
4✔
700
        src, dst = edge_node_pop_names(edge_file, edge_pop)
4✔
701

702
        logging.info("Processing edge file %s, pop: %s", edge_file, edge_pop)
4✔
703

704
        if src and dst and src != dst:
4✔
705
            raise ConfigurationError("Inner connectivity with different populations")
×
706

707
        dst = self.circuits.alias.get(dst, dst)
4✔
708
        if dst not in SimConfig.cli_options.restrict_node_populations:
4✔
709
            logging.warning("Skipped connectivity (restrict_node_populations)")
×
710
            return
×
711

712
        manager = self._circuits.get_create_edge_manager(
4✔
713
            ctype, src, dst, c_target, (conf, *args), **kwargs
714
        )
715
        if manager.is_file_open:  # Base connectivity
4✔
716
            manager.create_connections(
4✔
717
                get_conn_stats=kwargs.get("get_conn_stats", SimConfig.dry_run)
718
            )
719

720
    def _process_connection_configure(self, conn_conf):
5✔
721
        source_t = TargetSpec(conn_conf.source, None)
2✔
722
        dest_t = TargetSpec(conn_conf.destination, None)
2✔
723
        source_t.population, dest_t.population = self._circuits.unalias_pop_keys(
2✔
724
            source_t.population, dest_t.population
725
        )
726
        src_target = self.target_manager.get_target(source_t)
2✔
727
        dst_target = self.target_manager.get_target(dest_t)
2✔
728
        # Loop over population pairs
729
        for src_pop in src_target.population_names:
2✔
730
            for dst_pop in dst_target.population_names:
2✔
731
                # Loop over all managers having connections between the populations
732
                for conn_manager in self._circuits.get_edge_managers(src_pop, dst_pop):
2✔
733
                    logging.debug("Using connection manager: %s", conn_manager)
2✔
734
                    conn_manager.configure_connections(conn_conf)
2✔
735

736
    @mpi_no_errors
5✔
737
    def _load_projections(self, pname, projection, **kw):
5✔
738
        """Check for Projection blocks"""
739
        target_manager = self._target_manager
4✔
740
        # None, GapJunctions, NeuroGlial, NeuroModulation...
741
        ptype = projection.get("Type")
4✔
742
        ptype_cls = EngineBase.connection_types.get(ptype)
4✔
743
        source_t = TargetSpec(None, projection.get("Source"))
4✔
744
        dest_t = TargetSpec(None, projection.get("Destination"))
4✔
745

746
        if SimConfig.cli_options.restrict_connectivity >= 1:
4✔
747
            logging.warning("Skipped projections %s->%s (restrict_connectivity)", source_t, dest_t)
×
748
            return
×
749

750
        if not ptype_cls:
4✔
751
            raise RuntimeError(f"No Engine to handle connectivity of type '{ptype}'")
×
752

753
        ppath, *pop_name = projection["Path"].split(":")
4✔
754
        edge_pop_name = pop_name[0] if pop_name else None
4✔
755

756
        logging.info("Processing Edge file: %s", ppath)
4✔
757

758
        # Update the target spec with the actual populations
759
        src_pop, dst_pop = edge_node_pop_names(
4✔
760
            ppath, edge_pop_name, source_t.population, dest_t.population
761
        )
762
        source_t.population, dest_t.population = self._circuits.unalias_pop_keys(src_pop, dst_pop)
4✔
763
        src_target = self.target_manager.get_target(source_t)
4✔
764
        dst_target = self.target_manager.get_target(dest_t)
4✔
765

766
        # If the src_pop is not a known node population, allow creating a Virtual one
767
        src_populations = src_target.population_names or [source_t.population]
4✔
768

769
        for src_pop in src_populations:
4✔
770
            for dst_pop in dst_target.population_names:
4✔
771
                logging.info(" * %s (Type: %s, Src: %s, Dst: %s)", pname, ptype, src_pop, dst_pop)
4✔
772
                conn_manager = self._circuits.get_create_edge_manager(
4✔
773
                    ptype_cls,
774
                    src_pop,
775
                    dst_pop,
776
                    source_t,
777
                    (projection, target_manager),
778
                    **kw,  # args to ptype_cls if creating
779
                )
780
                logging.debug("Using connection manager: %s", conn_manager)
4✔
781
                proj_source = ":".join([ppath, *pop_name])
4✔
782
                conn_manager.open_edge_location(proj_source, projection, src_name=src_pop)
4✔
783
                conn_manager.create_connections(
4✔
784
                    source_t.name,
785
                    dest_t.name,
786
                    get_conn_stats=kw.get("get_conn_stats", SimConfig.dry_run),
787
                )
788

789
    @mpi_no_errors
5✔
790
    @timeit(name="Enable Stimulus")
5✔
791
    def enable_stimulus(self):
5✔
792
        """Iterate over any stimulus defined in the config file given to the simulation
793
        and instantiate them.
794
        This passes the raw text in field/value pairs to a StimulusManager object to interpret the
795
        text and instantiate an actual stimulus object.
796
        """
797
        if Feature.Stimulus not in SimConfig.cli_options.restrict_features:
4✔
798
            logging.warning("Skipped Stimulus (restrict_features)")
1✔
799
            return
1✔
800

801
        log_stage("Stimulus Apply.")
4✔
802

803
        # for each stimulus defined in the config file, request the StimulusManager to
804
        # instantiate
805
        self._stim_manager = StimulusManager(self._target_manager)
4✔
806

807
        for stim in SimConfig.stimuli:
4✔
808
            target_spec = TargetSpec(stim.get("Target"), None)
4✔
809

810
            stim_name = stim["Name"]
4✔
811
            stim_pattern = stim["Pattern"]
4✔
812
            if stim_pattern == "SynapseReplay":
4✔
813
                continue  # Handled by enable_replay
2✔
814
            logging.info(
3✔
815
                " * [STIM] %s (%s): -> %s",
816
                stim_name,
817
                stim_pattern,
818
                target_spec,
819
            )
820
            self._stim_manager.interpret(target_spec, stim)
3✔
821
        if SimConfig.has_extracellular_stimulus:
4✔
822
            logging.info("Inject extracellular stimuli")
2✔
823
            SpatiallyUniformEField.apply_all_stimuli()
2✔
824

825
    # -
826
    @mpi_no_errors
5✔
827
    def enable_replay(self):
5✔
828
        """Activate replay according to config file. Call before connManager.finalize"""
829
        if Feature.Replay not in SimConfig.cli_options.restrict_features:
4✔
830
            logging.warning("Skipped Replay (restrict_features)")
×
831
            return
×
832

833
        log_stage("Handling Replay")
4✔
834

835
        if SimConfig.use_coreneuron and bool(self._core_replay_file):
4✔
836
            logging.info(" -> [REPLAY] Reusing stim file from previous cycle")
×
837
            return
×
838

839
        for stim in SimConfig.stimuli:
4✔
840
            if stim.get("Pattern") != "SynapseReplay":
4✔
841
                continue
3✔
842
            target = stim["Target"]
2✔
843
            source = stim.get("Source")
2✔
844
            stim_name = stim["Name"]
2✔
845

846
            #  - delay: Spike replays are suppressed until a certain time
847
            delay = stim.get("Delay", 0.0)
2✔
848
            logging.info(
2✔
849
                " * [SYN REPLAY] %s -> %s (delay: %d)",
850
                stim_name,
851
                target,
852
                delay,
853
            )
854
            self._enable_replay(source, target, stim, delay=delay)
2✔
855

856
    # -
857
    def _enable_replay(
5✔
858
        self, source, target, stim_conf, tshift=0.0, delay=0.0, connectivity_type=None
859
    ):
860
        ptype_cls = EngineBase.connection_types.get(connectivity_type)
2✔
861
        src_target = self.target_manager.get_target(source)
2✔
862
        dst_target = self.target_manager.get_target(target)
2✔
863

864
        if SimConfig.restore_coreneuron:
2✔
865
            pop_offsets, alias_pop, _virtual_pop_offsets = CircuitManager.read_population_offsets()
×
866

867
        for src_pop in src_target.population_names:
2✔
868
            try:
2✔
869
                log_verbose("Loading replay spikes for population '%s'", src_pop)
2✔
870
                spike_manager = SpikeManager(stim_conf["SpikeFile"], tshift, src_pop)  # Disposable
2✔
871
            except MissingSpikesPopulationError:
2✔
872
                logging.info("  > No replay for src population: '%s'", src_pop)
2✔
873
                continue
2✔
874

875
            for dst_pop in dst_target.population_names:
2✔
876
                src_pop_str, dst_pop_str = src_pop or "(base)", dst_pop or "(base)"
2✔
877

878
                if SimConfig.restore_coreneuron:  # Node and Edges managers not initialized
2✔
879
                    src_pop_offset = (
×
880
                        pop_offsets[src_pop]
881
                        if src_pop in pop_offsets
882
                        else pop_offsets[alias_pop[src_pop]]
883
                    )
884
                else:
885
                    conn_manager = self._circuits.get_edge_manager(src_pop, dst_pop, ptype_cls)
2✔
886
                    if not conn_manager and SimConfig.cli_options.restrict_connectivity >= 1:
2✔
887
                        continue
×
888
                    assert conn_manager, f"Missing edge manager for {src_pop_str} -> {dst_pop_str}"
2✔
889
                    src_pop_offset = conn_manager.src_pop_offset
2✔
890

891
                logging.info(
2✔
892
                    "=> Population pathway %s -> %s. Source offset: %d",
893
                    src_pop_str,
894
                    dst_pop_str,
895
                    src_pop_offset,
896
                )
897
                conn_manager.replay(spike_manager, source, target, delay)
2✔
898

899
    # -
900
    @mpi_no_errors
5✔
901
    @timeit(name="Enable Modifications")
5✔
902
    def enable_modifications(self):
5✔
903
        """Iterate over any Modification blocks read from the config file and apply them to the
904
        network. The steps needed are more complex than NeuronConfigures, so the user should not be
905
        expected to write the hoc directly, but rather access a library of already available mods.
906
        """
907
        # mod_mananger gets destroyed when function returns (not required)
908
        # mod_manager = Nd.ModificationManager(self._target_manager.hoc)
909
        log_stage("Enabling modifications...")
4✔
910

911
        mod_manager = ModificationManager(self._target_manager)
4✔
912
        for mod_info in SimConfig.modifications:
4✔
913
            mod_manager.interpret(mod_info)
2✔
914

915
    def write_and_get_population_offsets(self) -> tuple[dict, dict, dict]:
5✔
916
        """Retrieve population offsets from the circuit or restore them,
917
        write the offsets, and return them.
918

919
        Returns:
920
            tuple[dict, dict, dict]:
921
                - pop_offsets: Mapping of population names to GID offsets.
922
                - alias_pop: Mapping of population aliases to population names.
923
                - virtual_pop_offsets: Mapping of virtual population names to offsets.
924
        """
925
        if self._circuits.initialized():
4✔
926
            pop_offsets, alias_pop = self._circuits.get_population_offsets()
4✔
927
            virtual_pop_offsets = self._circuits.get_virtual_population_offsets()
4✔
928
        else:
929
            # restore way
930
            pop_offsets, alias_pop, virtual_pop_offsets = CircuitManager.read_population_offsets()
1✔
931
        self._circuits.write_population_offsets(
4✔
932
            pop_offsets, alias_pop, virtual_pop_offsets=virtual_pop_offsets
933
        )
934
        return pop_offsets, alias_pop, virtual_pop_offsets
4✔
935

936
    # @mpi_no_errors - not required since theres a call inside before make_comm()
937
    @timeit(name="Enable Reports")
5✔
938
    def enable_reports(self):  # noqa: C901, PLR0912, PLR0915
5✔
939
        """Iterate over reports defined in the config file and instantiate them."""
940
        log_stage("Reports Enabling")
4✔
941

942
        # filter: only the enabled ones
943
        reports_conf = {name: conf for name, conf in SimConfig.reports.items() if conf.enabled}
4✔
944
        self._report_list = []
4✔
945

946
        pop_offsets, alias_pop, _virtual_pop_offsets = self.write_and_get_population_offsets()
4✔
947
        pop_offsets_alias = pop_offsets, alias_pop
4✔
948

949
        if SimConfig.use_coreneuron:
4✔
950
            if SimConfig.restore_coreneuron:
2✔
951
                # we copy it first. We will proceed to modify
952
                # it in update_report_config later in one go
953
                Path(CoreConfig.report_config_file_save).parent.mkdir(parents=True, exist_ok=True)
1✔
954
                shutil.copy(
1✔
955
                    CoreConfig.report_config_file_restore, CoreConfig.report_config_file_save
956
                )
957
            else:
958
                core_report_config = CoreReportConfig()
2✔
959

960
        # necessary for restore: we need to update the various reports tend
961
        # we can do it in one go later
962
        substitutions = defaultdict(dict)
4✔
963
        cumulative_error = CumulativeError()
4✔
964
        for rep_name, rep_conf in reports_conf.items():
4✔
965
            cumulative_error.is_error_appended = False
2✔
966
            target_spec = TargetSpec(rep_conf.cells, None)
2✔
967
            target = self._target_manager.get_target(target_spec)
2✔
968

969
            # Build final config. On errors log, stop only after all reports processed
970
            rep_params = create_report_parameters(
2✔
971
                sim_end=self._run_conf.tstop,
972
                nd_t=Nd.t,
973
                output_root=SimConfig.output_root,
974
                rep_name=rep_name,
975
                rep_conf=rep_conf,
976
                target=target,
977
                buffer_size=SimConfig.report_buffer_size,
978
                cumulative_error=cumulative_error,
979
            )
980
            if cumulative_error.is_error_appended:
2✔
981
                continue
×
982
            check_report_parameters(
2✔
983
                rep_params,
984
                Nd.dt,
985
                lfp_active=self._circuits.global_manager._lfp_manager._lfp_file,
986
                cumulative_error=cumulative_error,
987
            )
988
            if cumulative_error.is_error_appended:
2✔
989
                continue
1✔
990

991
            if SimConfig.restore_coreneuron:
2✔
992
                substitutions[rep_params.name]["end_time"] = rep_params.end
1✔
993
                continue  # we dont even need to initialize reports
1✔
994

995
            # With coreneuron direct mode, enable fast membrane current calculation
996
            # for i_membrane
997
            if (
2✔
998
                SimConfig.coreneuron_direct_mode and "i_membrane" in rep_params.report_on
999
            ) or rep_params.type == libsonata.SimulationConfig.Report.Type.lfp:
1000
                Nd.cvode.use_fast_imem(1)
1✔
1001

1002
            has_gids = len(self._circuits.global_manager.get_final_gids()) > 0
2✔
1003
            if not has_gids:
2✔
1004
                self._report_list.append(None)
×
1005
                continue
×
1006

1007
            report = ReportManager.create(
2✔
1008
                params=rep_params,
1009
                use_coreneuron=SimConfig.use_coreneuron,
1010
                cumulative_error=cumulative_error,
1011
            )
1012
            if cumulative_error.is_error_appended:
2✔
1013
                continue
1✔
1014
            self._set_point_list_in_rep_params(rep_params, cumulative_error=cumulative_error)
2✔
1015
            if cumulative_error.is_error_appended:
2✔
1016
                continue
×
1017

1018
            if SimConfig.use_coreneuron:
2✔
1019
                core_report_config.add_entry(
1✔
1020
                    CoreReportConfigEntry.from_report_params(rep_params=rep_params)
1021
                )
1022

1023
            if (
2✔
1024
                not SimConfig.use_coreneuron
1025
                or rep_params.type == libsonata.SimulationConfig.Report.Type.synapse
1026
            ):
1027
                report.setup(
2✔
1028
                    rep_params=rep_params,
1029
                    global_manager=self._circuits.global_manager,
1030
                    cumulative_error=cumulative_error,
1031
                )
1032
                if cumulative_error.is_error_appended:
2✔
1033
                    continue
1✔
1034

1035
            self._report_list.append(report)
2✔
1036

1037
        if SimConfig.restore_coreneuron:
4✔
1038
            CoreReportConfig.update_file(CoreConfig.report_config_file_save, substitutions)
1✔
1039

1040
        cumulative_error.raise_if_any()
4✔
1041

1042
        MPI.check_no_errors()
4✔
1043

1044
        if not SimConfig.restore_coreneuron:
4✔
1045
            if SimConfig.use_coreneuron:
4✔
1046
                self._finalize_corenrn_reports(core_report_config, pop_offsets_alias)
2✔
1047
            else:
1048
                self._finalize_nrn_reports()
4✔
1049

1050
    def _finalize_corenrn_reports(self, core_report_config, pop_offsets_alias):
5✔
1051
        core_report_config.set_pop_offsets(pop_offsets_alias[0])
2✔
1052
        core_report_config.set_spike_filename(self._run_conf.spikes_file)
2✔
1053
        core_report_config.dump(CoreConfig.report_config_file_save)
2✔
1054

1055
    def _finalize_nrn_reports(self):
5✔
1056
        # once all reports are created, we finalize the communicator for any reports
1057
        self._sonatareport_helper.set_max_buffer_size_hint(SimConfig.report_buffer_size)
4✔
1058
        self._sonatareport_helper.make_comm()
4✔
1059
        self._sonatareport_helper.prepare_datasets()
4✔
1060

1061
    @cache_errors
5✔
1062
    def _set_point_list_in_rep_params(self, rep_params: ReportParameters):
5✔
1063
        """Dispatcher: it helps to retrieve the points of a target and set them in
1064
        the report parameters.
1065

1066
        Returns: The target list of points
1067
        """
1068
        if rep_params.type == libsonata.SimulationConfig.Report.Type.compartment_set:
2✔
1069
            rep_params.points = rep_params.target.get_point_list_from_compartment_set(
×
1070
                cell_manager=self._target_manager._cell_manager,
1071
                compartment_set=self._target_manager.get_compartment_set(
1072
                    rep_params.compartment_set
1073
                ),
1074
            )
1075
        else:
1076
            sections, compartments = rep_params.sections, rep_params.compartments
2✔
1077
            if (
2✔
1078
                rep_params.type == libsonata.SimulationConfig.Report.Type.summation
1079
                and sections == libsonata.SimulationConfig.Report.Sections.soma
1080
            ):
1081
                sections, compartments = (
1✔
1082
                    libsonata.SimulationConfig.Report.Sections.all,
1083
                    libsonata.SimulationConfig.Report.Compartments.all,
1084
                )
1085
            rep_params.points = rep_params.target.get_point_list(
2✔
1086
                cell_manager=self._target_manager._cell_manager,
1087
                section_type=sections,
1088
                compartment_type=compartments,
1089
            )
1090

1091
    # -
1092
    @mpi_no_errors
5✔
1093
    def sim_init(self, corenrn_gen=None, **sim_opts):
5✔
1094
        """Finalize the model and prepare to run simulation.
1095

1096
        After finalizing the model, will eventually write coreneuron config
1097
        and initialize the neuron simulation if applicable.
1098

1099
        Args:
1100
            corenrn_gen: Whether to generate coreneuron config. Default: None (if required)
1101
            sim_opts - override _finalize_model options. E.g. spike_compress
1102
        """
1103
        if self._sim_ready:
4✔
1104
            return self._pc
1✔
1105

1106
        if not len(self._circuits.all_node_managers()):
4✔
1107
            raise RuntimeError("No CellDistributor was initialized. Please create a circuit.")
×
1108

1109
        self._finalize_model(**sim_opts)
4✔
1110

1111
        if corenrn_gen is None:
4✔
1112
            corenrn_gen = SimConfig.use_coreneuron
4✔
1113
        if corenrn_gen:
4✔
1114
            self._coreneuron_configure_datadir(
2✔
1115
                corenrn_restore=False, coreneuron_direct_mode=SimConfig.coreneuron_direct_mode
1116
            )
1117
            self._coreneuron_write_sim_config(corenrn_restore=False)
2✔
1118

1119
        if SimConfig.use_neuron or SimConfig.coreneuron_direct_mode:
4✔
1120
            self._sim_init_neuron()
4✔
1121

1122
        assert not (SimConfig.use_neuron and SimConfig.use_coreneuron)
4✔
1123
        if SimConfig.use_neuron:
4✔
1124
            self.dump_cell_states()
4✔
1125

1126
        self._sim_ready = True
4✔
1127
        return self._pc
4✔
1128

1129
    # -
1130
    @mpi_no_errors
5✔
1131
    @timeit(name="Model Finalized")
5✔
1132
    def _finalize_model(self, spike_compress=3):
5✔
1133
        """Set up simulation run parameters and initialization.
1134

1135
        Handles setup_transfer, spike_compress, _record_spikes, stdinit, timeout
1136
        Args:
1137
            spike_compress: The spike_compress() parameters (tuple or int)
1138
        """
1139
        logging.info("Preparing to run simulation...")
4✔
1140
        for mgr in self._circuits.all_node_managers():
4✔
1141
            mgr.pre_stdinit()
4✔
1142

1143
        is_save_state = SimConfig.save or SimConfig.restore
4✔
1144
        self._pc.setup_transfer()
4✔
1145

1146
        if spike_compress and not is_save_state and not self._is_ngv_run:
4✔
1147
            # multisend 13 is combination of multisend(1) + two_phase(8) + two_intervals(4)
1148
            # to activate set spike_compress=(0, 0, 13)
1149
            if SimConfig.loadbal_mode == LoadBalanceMode.Memory:
3✔
1150
                logging.info("Disabling spike compression for Memory Load Balance")
2✔
1151
                spike_compress = False
2✔
1152
            if not isinstance(spike_compress, tuple):
3✔
1153
                spike_compress = (spike_compress, 1, 0)
3✔
1154
            self._pc.spike_compress(*spike_compress)
3✔
1155

1156
        # LFP calculation requires WholeCell balancing and extracellular mechanism.
1157
        # This is incompatible with efficient caching atm AND Incompatible with
1158
        # mcd & Glut
1159
        if not self._is_ngv_run:
4✔
1160
            Nd.cvode.cache_efficient(1)
3✔
1161
        self._pc.set_maxstep(4)
4✔
1162
        with timeit(name="stdinit"):
4✔
1163
            Nd.stdinit()
4✔
1164

1165
    # -
1166
    def _sim_init_neuron(self):
5✔
1167
        # === Neuron specific init ===
1168
        restore_path = SimConfig.restore
4✔
1169

1170
        # create a spike_id vector which stores the pairs for spikes and timings for
1171
        # every engine
1172
        for cell_manager in self._circuits.all_node_managers():
4✔
1173
            if cell_manager.population_name is not None:
4✔
1174
                self._spike_populations.append(
4✔
1175
                    (cell_manager.population_name, cell_manager.local_nodes.offset)
1176
                )
1177
                self._spike_vecs.append(cell_manager.record_spikes() or (Nd.Vector(), Nd.Vector()))
4✔
1178

1179
        self._pc.timeout(200)  # increase by 10x
4✔
1180

1181
        if restore_path:
4✔
1182
            with timeit(name="restoretime"):
×
1183
                logging.info("Restoring state...")
×
1184
                self._stim_manager.saveStatePreparation(self._bbss)
×
1185
                self._bbss.vector_play_init()
×
1186
                self._restart_events()  # On restore the event queue is cleared
×
1187
                return  # Upon restore sim is ready
×
1188

1189
    # -
1190
    def _restart_events(self):
5✔
1191
        logging.info("Restarting connections events (Replay and Spont Minis)")
×
1192
        for syn_manager in self._circuits.all_synapse_managers():
×
1193
            syn_manager.restart_events()
×
1194

1195
    @contextmanager
5✔
1196
    def _coreneuron_ensure_all_ranks_have_gids(self, corenrn_data):
5✔
1197
        local_gid_count = sum(
2✔
1198
            len(manager.local_nodes) for manager in self._circuits.all_node_managers()
1199
        )
1200
        if local_gid_count > 0:
2✔
1201
            yield
2✔
1202
            return
2✔
1203

1204
        # Create a dummy cell manager with node_pop = None
1205
        # which holds a fake node with a fake population "zzz" to get an unused gid.
1206
        # coreneuron fails if this edge case is reached multiple times as we
1207
        # try to add twice the same gid. pop "zzz" is reserved to be used
1208
        # exclusively for handling cases where no real GIDs are assigned to
1209
        # a rank, ensuring that CoreNeuron does not crash due to missing GIDs.
1210
        log_verbose("Creating fake gid for CoreNeuron")
1✔
1211
        assert not PopulationNodes.get("zzz"), "Population 'zzz' is reserved "
1✔
1212
        "for handling empty GID ranks and should not be used elsewhere."
1✔
1213
        pop_group = PopulationNodes.get("zzz", create=True)
1✔
1214
        fake_gid = pop_group.offset + 1 + MPI.rank
1✔
1215
        # Add the fake cell to a dummy manager
1216
        dummy_cell_manager = CellDistributor(
1✔
1217
            circuit_conf=make_circuit_config({"CellLibraryFile": "<NONE>"}),
1218
            target_manager=self._target_manager,
1219
        )
1220
        dummy_cell_manager.load_artificial_cell(fake_gid, CoreConfig.artificial_cell_object)
1✔
1221
        yield
1✔
1222

1223
        # register_mapping() doesn't work for this artificial cell as somatic attr is
1224
        # missing, so create a dummy mapping file manually, required for reporting
1225
        cur_files = glob.glob(f"{corenrn_data}/*_3.dat")
1✔
1226
        example_mapfile = cur_files[0]
1✔
1227
        with open(example_mapfile, "rb") as f_mapfile:
1✔
1228
            # read the version from the existing mapping file generated by coreneuron
1229
            coredata_version = f_mapfile.readline().rstrip().decode("ascii")
1✔
1230

1231
        mapping_file = Path(corenrn_data, f"{fake_gid}_3.dat")
1✔
1232
        if not mapping_file.is_file():
1✔
1233
            mapping_file.write_text(f"{coredata_version}\n0\n", encoding="utf-8")
1✔
1234

1235
    def _coreneuron_configure_datadir(self, corenrn_restore, coreneuron_direct_mode):
5✔
1236
        """Configures the CoreNEURON data directory and handles shared memory (SHM) setup.
1237

1238
        - Creates the data directory if it doesn't exist.
1239
        - If in direct mode, returns immediately since the default behavior is fine.
1240
        - If restoring, skips the setup.
1241
        - If not restoring, checks if SHM should be enabled based on available memory,
1242
          and sets up symlinks for CoreNEURON's necessary files in SHM.
1243

1244
        Args:
1245
            corenrn_restore (bool): Flag indicating if CoreNEURON is in restore mode.
1246
            coreneuron_direct_mode (bool): Flag indicating if direct mode is enabled.
1247
        """
1248
        corenrn_datadir = SimConfig.coreneuron_datadir_path(create=True)
2✔
1249
        if coreneuron_direct_mode:
2✔
1250
            return
1✔
1251
        corenrn_datadir_shm = SHMUtil.get_datadir_shm(corenrn_datadir)
2✔
1252

1253
        # Clean-up any previous simulations in the same output directory
1254
        if self._cycle_i == 0 and corenrn_datadir_shm:
2✔
1255
            rmtree(corenrn_datadir_shm)
1✔
1256

1257
        # Ensure that we have a folder in /dev/shm (i.e., 'SHMDIR' ENV variable)
1258
        if SimConfig.cli_options.enable_shm and not corenrn_datadir_shm:
2✔
1259
            logging.warning("Unknown SHM directory for model file transfer in CoreNEURON.")
×
1260
        # Try to configure the /dev/shm folder as the output directory for the files
1261
        elif (
2✔
1262
            self._cycle_i == 0
1263
            and not corenrn_restore
1264
            and (SimConfig.cli_options.enable_shm and SimConfig.delete_corenrn_data)
1265
        ):
1266
            # Check for the available memory in /dev/shm and estimate the RSS by multiplying
1267
            # the number of cycles in the multi-step model build with an approximate
1268
            # factor
1269
            mem_avail = SHMUtil.get_mem_avail()
1✔
1270
            shm_avail = SHMUtil.get_shm_avail()
1✔
1271
            initial_rss = self._initial_rss
1✔
1272
            current_rss = SHMUtil.get_node_rss()
1✔
1273
            factor = SHMUtil.get_shm_factor()
1✔
1274
            rss_diff = (current_rss - initial_rss) if initial_rss < current_rss else current_rss
1✔
1275
            # 'rss_diff' prevents <0 estimates
1276
            rss_req = int(rss_diff * self._n_cycles * factor)
1✔
1277

1278
            # Sync condition value with all ranks to ensure that all of them can use
1279
            # /dev/shm
1280
            shm_possible = (rss_req < shm_avail) and (rss_req < mem_avail)
1✔
1281
            if MPI.allreduce(int(shm_possible), MPI.SUM) == MPI.size:
1✔
1282
                logging.info("SHM file transfer mode for CoreNEURON enabled")
1✔
1283

1284
                # Create SHM folder and links to GPFS for the global data structures
1285
                os.makedirs(corenrn_datadir_shm, exist_ok=True)
1✔
1286

1287
                # Important: These three files must be available on every node, as they are shared
1288
                #            across all of the processes. The trick here is to fool NEURON into
1289
                #            thinking that the files are written in /dev/shm, but they are actually
1290
                #            written on GPFS. The workflow is identical, meaning that rank 0 writes
1291
                #            the content and every other rank reads it afterwards in CoreNEURON.
1292
                for filename in ("bbcore_mech.dat", "files.dat", "globals.dat"):
1✔
1293
                    path = os.path.join(corenrn_datadir, filename)
1✔
1294
                    path_shm = os.path.join(corenrn_datadir_shm, filename)
1✔
1295

1296
                    try:
1✔
1297
                        os.close(os.open(path, os.O_CREAT))
1✔
1298
                        os.symlink(path, path_shm)
1✔
1299
                    except FileExistsError:
×
1300
                        pass  # Ignore if other process has already created it
×
1301

1302
                # Update the flag to confirm the configuration
1303
                self._shm_enabled = True
1✔
1304
            else:
1305
                logging.warning(
×
1306
                    "Unable to utilize SHM for model file transfer in CoreNEURON. "
1307
                    "Increase the number of nodes to reduce the memory footprint "
1308
                    "(Current use node: %d MB / SHM Limit: %d MB / Mem. Limit: %d MB)",
1309
                    (rss_req >> 20),
1310
                    (shm_avail >> 20),
1311
                    (mem_avail >> 20),
1312
                )
1313
        _SimConfig.coreneuron_datadir = (
2✔
1314
            corenrn_datadir if not self._shm_enabled else corenrn_datadir_shm
1315
        )
1316

1317
    # -
1318
    @timeit(name="corewrite")
5✔
1319
    def _coreneuron_write_sim_config(self, corenrn_restore):
5✔
1320
        log_stage("Dataset generation for CoreNEURON")
2✔
1321

1322
        if not corenrn_restore:
2✔
1323
            CompartmentMapping(self._circuits.global_manager).register_mapping()
2✔
1324
            if not SimConfig.coreneuron_direct_mode:
2✔
1325
                with self._coreneuron_ensure_all_ranks_have_gids(CoreConfig.datadir):
2✔
1326
                    self._pc.nrnbbcore_write(CoreConfig.datadir)
2✔
1327
                    MPI.barrier()  # wait for all ranks to finish corenrn data generation
2✔
1328

1329
        prcellgid = self._dump_cell_state_gids[0] if self._dump_cell_state_gids else -1
2✔
1330
        if self._dump_cell_state_gids and len(self._dump_cell_state_gids) > 1:
2✔
1331
            logging.warning(
1✔
1332
                "Multiple cell state GIDs provided. Using the first one: %d",
1333
                self._dump_cell_state_gids[0],
1334
            )
1335

1336
        core_simulation_config = CoreSimulationConfig(
2✔
1337
            outpath=CoreConfig.output_root,
1338
            datpath=CoreConfig.datadir,
1339
            tstop=Nd.tstop,
1340
            dt=Nd.dt,
1341
            prcellgid=prcellgid,
1342
            celsius=getattr(SimConfig, "celsius", 34.0),
1343
            voltage=getattr(SimConfig, "v_init", -65.0),
1344
            cell_permute=int(SimConfig.cell_permute),
1345
            pattern=self._core_replay_file or None,
1346
            seed=SimConfig.rng_info.getGlobalSeed(),
1347
            model_stats=int(SimConfig.cli_options.model_stats),
1348
            report_conf=CoreConfig.report_config_file_save
1349
            if self._run_conf.enable_reports
1350
            else None,
1351
            mpi=int(os.environ.get("NEURON_INIT_MPI", "1")),
1352
        )
1353
        core_simulation_config.dump(CoreConfig.sim_config_file)
2✔
1354
        # Wait for rank0 to write the sim config file
1355
        MPI.barrier()
2✔
1356
        logging.info(" => Dataset written to '%s'", CoreConfig.datadir)
2✔
1357

1358
    # -
1359
    def run_all(self):
5✔
1360
        """Run the whole simulation according to the simulation config file"""
1361
        if not self._sim_ready:
3✔
1362
            self.sim_init()
×
1363

1364
        timings = None
3✔
1365
        if SimConfig.use_neuron:
3✔
1366
            timings = self._run_neuron()
3✔
1367
            self.sonata_spikes()
3✔
1368
        if SimConfig.use_coreneuron:
3✔
1369
            print_mem_usage()
2✔
1370
            if not SimConfig.coreneuron_direct_mode:
2✔
1371
                self.clear_model(avoid_clearing_queues=False)
2✔
1372
            self._run_coreneuron()
2✔
1373
            if SimConfig.coreneuron_direct_mode:
2✔
1374
                self.sonata_spikes()
1✔
1375
        return timings
3✔
1376

1377
    # -
1378
    @return_neuron_timings
5✔
1379
    def _run_neuron(self):
5✔
1380
        if MPI.rank == 0:
3✔
1381
            _ = SimulationProgress()
3✔
1382
        self.solve()
3✔
1383
        logging.info("Simulation finished.")
3✔
1384

1385
    @staticmethod
5✔
1386
    def _run_coreneuron():
5✔
1387
        logging.info("Launching simulation with CoreNEURON")
2✔
1388
        CoreConfig.psolve_core(
2✔
1389
            SimConfig.coreneuron_direct_mode,
1390
        )
1391

1392
    def _sim_event_handlers(self, tstart, tstop):
5✔
1393
        """Create handlers for "in-simulation" events, like activating delayed
1394
        connections, execute Save-State, etc
1395
        """
1396
        events = defaultdict(list)  # each key (time) points to a list of handlers
3✔
1397

1398
        if SimConfig.save:
3✔
1399
            tsave = SimConfig.tstop  # Consider 0 as the end too!
×
1400
            save_f = self._create_save_handler()
×
1401
            events[tsave].append(save_f)
×
1402

1403
        event_list = [(t, events[t]) for t in sorted(events)]
3✔
1404
        return event_list
3✔
1405

1406
    # -
1407
    def _create_save_handler(self):
5✔
1408
        @timeit(name="savetime")
×
1409
        def save_f():
×
1410
            """Function that saves the current simulation state:
1411
            syncs MPI, saves stimuli, flushes reports, clears the model,
1412
            and logs progress.
1413
            """
1414
            logging.info("Saving State... (t=%f)", SimConfig.tstop)
×
1415
            MPI.barrier()
×
1416
            self._stim_manager.saveStatePreparation(self._bbss)
×
1417
            log_verbose("SaveState Initialization Done")
×
1418

1419
            # If event at the end of the sim we can actually clearModel()
1420
            # before savestate()
1421
            log_verbose("Clearing model prior to final save")
×
1422
            self._sonatareport_helper.flush()
×
1423

1424
            self.clear_model()
×
1425
            logging.info(" => Save done successfully")
×
1426

1427
        return save_f
×
1428

1429
    # -
1430
    @mpi_no_errors
5✔
1431
    @timeit(name="psolve")
5✔
1432
    def solve(self, tstop=None):
5✔
1433
        """Call solver with a given stop time (default: whole interval).
1434
        Be sure to have sim_init()'d the simulation beforehand
1435
        """
1436
        if not self._sim_ready:
3✔
1437
            raise ConfigurationError("Initialize simulation first")
×
1438

1439
        tstart = Nd.t
3✔
1440
        tstop = tstop or Nd.tstop
3✔
1441
        event_list = self._sim_event_handlers(tstart, tstop)
3✔
1442

1443
        # NOTE: _psolve_loop is called among events in order to eventually split long
1444
        # simulation blocks, where one or more report flush(es) can happen. It is a simplified
1445
        # design relatively to the original version where the report checkpoint would not happen
1446
        # before the checkpoint timeout (25ms default). However there shouldn't be almost any
1447
        # performance penalty since the simulation is already halted between events.
1448

1449
        logging.info("Running simulation until t=%d ms", tstop)
3✔
1450
        t = tstart  # default if there are no events
3✔
1451
        for t, events in event_list:
3✔
1452
            self._psolve_loop(t)
×
1453
            for event in events:
×
1454
                event()
×
1455
            self.dump_cell_states()
×
1456
        # Run until the end
1457
        if t < tstop:
3✔
1458
            self._psolve_loop(tstop)
3✔
1459
            self.dump_cell_states()
3✔
1460

1461
        # Final flush
1462
        self._sonatareport_helper.flush()
3✔
1463

1464
    # psolve_loop: There was an issue where MPI collective routines for reporting and spike exchange
1465
    # are mixed such that some cpus are blocked waiting to complete reporting while others to
1466
    # finish spike exchange. As a work-around, periodically halt simulation and flush reports
1467
    # Default is 25 ms / cycle
1468
    def _psolve_loop(self, tstop):
5✔
1469
        cur_t = round(Nd.t, 2)  # fp innnacuracies could lead to infinitesimal loops
3✔
1470
        buffer_t = SimConfig.buffer_time
3✔
1471
        for _ in range(math.ceil((tstop - cur_t) / buffer_t)):
3✔
1472
            next_flush = min(tstop, cur_t + buffer_t)
3✔
1473
            self._pc.psolve(next_flush)
3✔
1474
            cur_t = next_flush
3✔
1475
        Nd.t = cur_t
3✔
1476

1477
    # -
1478
    @mpi_no_errors
5✔
1479
    def clear_model(self, avoid_creating_objs=False, avoid_clearing_queues=True):
5✔
1480
        """Clears appropriate lists and other stored references.
1481
        For use with intrinsic load balancing. After creating and evaluating the network using
1482
        round robin distribution, we want to clear the cells and synapses in order to have a
1483
        clean slate on which to instantiate the balanced cells.
1484
        """
1485
        logging.info("Clearing model")
3✔
1486
        self._pc.gid_clear()
3✔
1487
        self._target_manager.clear_simulation_data()
3✔
1488

1489
        if not avoid_creating_objs and SimConfig.use_neuron and self._sonatareport_helper:
3✔
1490
            self._sonatareport_helper.clear()
2✔
1491

1492
        # Reset vars
1493
        self._reset()
3✔
1494

1495
        # Clear BBSaveState
1496
        self._bbss.ignore()
3✔
1497

1498
        # Shrink ArrayPools holding mechanism's data in NEURON
1499
        pool_shrink()
3✔
1500

1501
        # Free event queues in NEURON
1502
        if not avoid_clearing_queues:
3✔
1503
            free_event_queues()
2✔
1504

1505
        # Garbage collect all Python objects without references
1506
        gc.collect()
3✔
1507

1508
        # Finally call malloc_trim to return all the freed pages back to the OS
1509
        trim_memory()
3✔
1510
        print_mem_usage()
3✔
1511

1512
    # -------------------------------------------------------------------------
1513
    #  output
1514
    # -------------------------------------------------------------------------
1515

1516
    def sonata_spikes(self):
5✔
1517
        """Write the spike events that occured on each node into a single output SONATA file."""
1518
        output_root = SimConfig.output_root_path(create=True)
3✔
1519
        if hasattr(self._sonatareport_helper, "create_spikefile"):
3✔
1520
            # Write spike report for multiple populations if exist
1521
            spike_path = self._run_conf.spikes_file
3✔
1522

1523
            # Get only the spike file name
1524
            file_name = spike_path.split("/")[-1] if spike_path is not None else "out.h5"
3✔
1525

1526
            # create a sonata spike file
1527
            self._sonatareport_helper.create_spikefile(output_root, file_name)
3✔
1528
            # write spikes per population
1529
            for (population, population_offset), (spikevec, idvec) in zip(
3✔
1530
                self._spike_populations, self._spike_vecs, strict=True
1531
            ):
1532
                extra_args = (
3✔
1533
                    (population, population_offset) if population else ("All", population_offset)
1534
                )
1535
                self._sonatareport_helper.add_spikes_population(spikevec, idvec, *extra_args)
3✔
1536
            # write all spike populations
1537
            self._sonatareport_helper.write_spike_populations()
3✔
1538
            # close the spike file
1539
            self._sonatareport_helper.close_spikefile()
3✔
1540
        else:
1541
            # fallback: write spike report with one single population "ALL"
1542
            logging.warning(
×
1543
                "Writing spike reports with multiple populations is not supported. "
1544
                "If needed, please update to a newer version of neurodamus."
1545
            )
1546
            population = self._target_spec.population or "All"
×
1547
            extra_args = (population,)
×
1548
            self._sonatareport_helper.write_spikes(spikevec, idvec, output_root, *extra_args)
×
1549

1550
    def dump_cell_states(self):
5✔
1551
        """Dump the _pr_cell_gid cell state if not already done
1552

1553
        We assume that the parallel context is ready. Thus, This function should
1554
        not be called if coreNeuron is employed and we are not at t=0.0.
1555
        """
1556
        assert SimConfig.use_neuron, "This function can work only with Neuron. Use sim.conf to "
4✔
1557
        "instruct coreNeuron to dump a cell state instead."
4✔
1558
        if not self._dump_cell_state_gids:
4✔
1559
            return
4✔
1560
        if self._last_cell_state_dump_t == Nd.t:  # avoid duplicating output
1✔
1561
            return
×
1562

1563
        for i in self._dump_cell_state_gids:
1✔
1564
            log_verbose("Dumping info about cell %d", i)
1✔
1565

1566
            self._pc.prcellstate(i, f"py_Neuron_t{Nd.t}")
1✔
1567

1568
        self._last_cell_state_dump_t = Nd.t
1✔
1569

1570
    @staticmethod
5✔
1571
    @run_only_rank0
5✔
1572
    def coreneuron_cleanup():
5✔
1573
        """Clean coreneuron save files after running"""
1574
        data_folder = Path(CoreConfig.datadir)
2✔
1575
        logging.info("Deleting intermediate data in %s", data_folder)
2✔
1576
        assert data_folder.is_dir(), "Data folder must be a directory"
2✔
1577
        if data_folder.is_symlink():
2✔
1578
            # in restore, coreneuron data is a symbolic link
1579
            data_folder.unlink()
1✔
1580
        else:
1581
            rmtree(data_folder)
2✔
1582

1583
        build_path = Path(SimConfig.build_path())
2✔
1584
        if build_path.exists():
2✔
1585
            shutil.rmtree(build_path)
2✔
1586

1587
        sim_conf = Path(CoreConfig.sim_config_file)
2✔
1588
        assert not sim_conf.exists()
2✔
1589

1590
        report_file = Path(CoreConfig.report_config_file_save)
2✔
1591
        assert not report_file.exists()
2✔
1592

1593
    def cleanup(self):
5✔
1594
        """Have the compute nodes wrap up tasks before exiting."""
1595
        # MemUsage constructor will do MPI communications
1596
        print_mem_usage()
3✔
1597

1598
        # Coreneuron runs clear the model before starting
1599
        if not SimConfig.use_coreneuron or SimConfig.simulate_model is False:
3✔
1600
            self.clear_model(avoid_creating_objs=True)
3✔
1601

1602
        if SimConfig.delete_corenrn_data and not SimConfig.save and not SimConfig.dry_run:
3✔
1603
            with timeit(name="Delete corenrn data"):
2✔
1604
                self.coreneuron_cleanup()
2✔
1605
                MPI.barrier()
2✔
1606

1607
    @staticmethod
5✔
1608
    @run_only_rank0
5✔
1609
    def move_dumpcellstates_to_output_root():
5✔
1610
        """Check for .corenrn or .nrn files in the current directory
1611
        and move them to CoreConfig.output_root_path(create=True).
1612
        """
1613
        current_dir = Path.cwd()
3✔
1614
        output_root = Path(SimConfig.output_root_path(create=True))
3✔
1615

1616
        # Iterate through files in the current directory
1617
        for file in current_dir.iterdir():
3✔
1618
            if file.suffix in {".corenrn", ".nrn", ".nrndat"}:
3✔
1619
                shutil.move(str(file), output_root / file.name)
1✔
1620
                logging.info("Moved %s to %s", file.name, output_root)
1✔
1621

1622

1623
class Neurodamus(Node):
5✔
1624
    """A high level interface to Neurodamus"""
1625

1626
    def __init__(
5✔
1627
        self, config_path_or_obj: str | libsonata.SimulationConfig, logging_level=None, **user_opts
1628
    ):
1629
        """Creates and initializes a neurodamus run node
1630

1631
        As part of Initiazation it calls:
1632
         * load_targets
1633
         * compute_load_balance
1634
         * Build the circuit (cells, synapses, GJs)
1635
         * Add stimulus & replays
1636
         * Activate reports if requested
1637

1638
        Args:
1639
            config_path_or_obj: Path to a SONATA simulation config JSON file,
1640
                or a ``libsonata.SimulationConfig`` instance.
1641
            logging_level: (int) Redefine the global logging level.
1642
                0 - Only warnings / errors
1643
                1 - Info messages (default)
1644
                2 - Verbose
1645
                3 - Debug messages
1646
            user_opts: Options to Neurodamus overriding the simulation config file
1647
        """
1648
        enable_reports = not user_opts.pop("disable_reports", False)
4✔
1649
        if logging_level is not None:
4✔
1650
            GlobalConfig.verbosity = logging_level
1✔
1651

1652
        Node.__init__(self, config_path_or_obj, user_opts)
4✔
1653
        # Use the run_conf dict to avoid passing it around
1654
        self._run_conf.enable_reports = enable_reports
4✔
1655

1656
        if SimConfig.dry_run:
4✔
1657
            if self._is_ngv_run:
2✔
1658
                raise Exception("Dry run not available for ngv circuit")
1✔
1659
            self._dry_run_stats = DryRunStats()
2✔
1660
            self._dry_run_stats.try_import_cell_memory_usage()
2✔
1661
            if not self._dry_run_stats.cell_memory_usage.preloaded:
2✔
1662
                self.load_targets()
2✔
1663
                self.create_cells()
2✔
1664
                self.create_synapses()
2✔
1665
            return
2✔
1666

1667
        if SimConfig.restore_coreneuron:
4✔
1668
            self._coreneuron_restore()
1✔
1669
        elif SimConfig.build_model:
4✔
1670
            self._instantiate_simulation()
4✔
1671

1672
        # Remove .SUCCESS file if exists
1673
        self._success_file = SimConfig.config_file + ".SUCCESS" if SimConfig.config_file else None
4✔
1674
        if self._success_file:
4✔
1675
            self._remove_file(self._success_file)
4✔
1676

1677
    # -
1678
    def _build_single_model(self):
5✔
1679
        """Construct the model for a single cycle.
1680

1681
        This process includes:
1682
        - Computing load balance across ranks.
1683
        - Building the circuit by creating cells and applying configurations.
1684
        - Establishing synaptic connections.
1685
        - Enabling replay mechanisms if applicable.
1686
        """
1687
        log_stage("================ CALCULATING LOAD BALANCE ================")
4✔
1688
        load_bal = self.compute_load_balance()
4✔
1689
        print_mem_usage()
4✔
1690

1691
        log_stage("==================== BUILDING CIRCUIT ====================")
4✔
1692
        self.create_cells(load_bal)
4✔
1693
        print_mem_usage()
4✔
1694

1695
        # Create connections
1696
        self.create_synapses()
4✔
1697
        print_mem_usage()
4✔
1698

1699
        log_stage("================ INSTANTIATING SIMULATION ================")
4✔
1700
        # Apply replay
1701
        self.enable_replay()
4✔
1702
        print_mem_usage()
4✔
1703

1704
        self.init()
4✔
1705

1706
    # -
1707
    def init(self):
5✔
1708
        """Explicitly initialize, allowing users to make last changes before simulation"""
1709
        if self._sim_ready:
4✔
1710
            logging.warning("Simulation already initialized. Skip second init")
×
1711
            return
×
1712

1713
        log_stage("Creating connections in the simulator")
4✔
1714
        base_seed = self._run_conf.base_seed  # base seed for synapse RNG
4✔
1715
        for syn_manager in self._circuits.all_synapse_managers():
4✔
1716
            syn_manager.finalize(base_seed)
4✔
1717
        print_mem_usage()
4✔
1718

1719
        self.enable_stimulus()
4✔
1720
        print_mem_usage()
4✔
1721
        self.enable_modifications()
4✔
1722

1723
        if self._run_conf.enable_reports:
4✔
1724
            self.enable_reports()
4✔
1725
        print_mem_usage()
4✔
1726

1727
        self.sim_init()
4✔
1728
        assert self._sim_ready, "sim_init should have set this"
4✔
1729

1730
    @staticmethod
5✔
1731
    def _merge_filesdat(ncycles):
5✔
1732
        log_stage("Generating merged CoreNeuron files.dat")
1✔
1733
        coreneuron_datadir = CoreConfig.datadir
1✔
1734
        cn_entries = []
1✔
1735
        for i in range(ncycles):
1✔
1736
            log_verbose(f"files_{i}.dat")
1✔
1737
            filename = ospath.join(coreneuron_datadir, f"files_{i}.dat")
1✔
1738
            with open(filename, encoding="utf-8") as fd:
1✔
1739
                first_line = fd.readline()
1✔
1740
                nlines = int(fd.readline())
1✔
1741
                for _ in range(nlines):
1✔
1742
                    line = fd.readline()
1✔
1743
                    cn_entries.append(line)
1✔
1744

1745
        cnfilename = ospath.join(coreneuron_datadir, "files.dat")
1✔
1746
        with open(cnfilename, "w", encoding="utf-8") as cnfile:
1✔
1747
            cnfile.write(first_line)
1✔
1748
            cnfile.write(str(len(cn_entries)) + "\n")
1✔
1749
            cnfile.writelines(cn_entries)
1✔
1750

1751
        logging.info(" => %s files merged successfully", ncycles)
1✔
1752

1753
    def _coreneuron_restore(self):
5✔
1754
        """Restore CoreNEURON simulation state.
1755

1756
        This method sets up the CoreNEURON environment for restoring a simulation:
1757
        - load targets
1758
        - enable replay
1759
        - enable reports (this writes also report.conf)
1760
        - write sim.conf
1761
        - set and link coreneuron_datadir to the old restore one
1762
        """
1763
        log_stage(" =============== CORENEURON RESTORE ===============")
1✔
1764
        self.load_targets()
1✔
1765
        self.enable_replay()
1✔
1766
        if self._run_conf.enable_reports:
1✔
1767
            self.enable_reports()
1✔
1768

1769
        self._coreneuron_write_sim_config(corenrn_restore=True)
1✔
1770
        self._setup_coreneuron_datadir_from_restore()
1✔
1771

1772
        self._sim_ready = True
1✔
1773

1774
    @run_only_rank0
5✔
1775
    def _setup_coreneuron_datadir_from_restore(self):
5✔
1776
        """Configure the environment for restoring CoreNEURON.
1777

1778
        This involves:
1779
        - setting the coreneuron_datadir
1780
        - writing the sim.conf
1781
        - linking the old coreneuron_datadir to the new one
1782
        (in save_path or output_root)
1783
        """
1784
        self._coreneuron_configure_datadir(
1✔
1785
            corenrn_restore=True, coreneuron_direct_mode=SimConfig.coreneuron_direct_mode
1786
        )
1787

1788
        # handle coreneuron_input movements
1789
        src_datadir = Path(SimConfig.coreneuron_datadir_restore_path())
1✔
1790
        dst_datadir = Path(SimConfig.coreneuron_datadir_path())
1✔
1791
        # Check if source directory exists
1792
        if not src_datadir.exists():
1✔
1793
            raise FileNotFoundError(
×
1794
                f"Coreneuron input directory in `{src_datadir}` does not exist!"
1795
            )
1796

1797
        # If the source exists,
1798
        # remove the destination directory or symlink (if it exists)
1799
        if dst_datadir.exists():
1✔
1800
            if dst_datadir.is_symlink():
1✔
1801
                # Remove the symlink
1802
                dst_datadir.unlink()
×
1803
            else:
1804
                # Remove the folder if it's not a symlink
1805
                shutil.rmtree(dst_datadir)
1✔
1806

1807
        dst_datadir.symlink_to(src_datadir)
1✔
1808

1809
    def compute_n_cycles(self):
5✔
1810
        """Determine the number of model-building cycles
1811

1812
        It is based on configuration and system constraints.
1813
        """
1814
        n_cycles = SimConfig.modelbuilding_steps
4✔
1815
        # No multi-cycle. Trivial result, this is always possible
1816
        if n_cycles == 1:
4✔
1817
            return n_cycles
4✔
1818

1819
        target = self._target_manager.get_target(self._target_spec)
1✔
1820
        target_name = self._target_spec.name
1✔
1821
        max_cell_count = target.max_gid_count_per_population()
1✔
1822
        logging.info(
1✔
1823
            "Simulation target: %s, Max cell count per population: %d", target_name, max_cell_count
1824
        )
1825

1826
        if SimConfig.use_coreneuron and max_cell_count / n_cycles < MPI.size and max_cell_count > 0:
1✔
1827
            # coreneuron with no. ranks >> no. cells
1828
            # need to assign fake gids to artificial cells in empty threads
1829
            # during module building fake gids start from max_gid + 1
1830
            # currently not support engine plugin where target is loaded later
1831
            # We can always have only 1 cycle. coreneuron throws an error if a
1832
            # rank does not have cells during a cycle. There is a way to prevent
1833
            # this for unbalanced multi-populations but if more than one cycle
1834
            # happens on a rank without instantiating cells another error raises.
1835
            # Thus, the number of cycles should be rounded down; on the safe side
1836
            max_num_cycles = int(max_cell_count / MPI.size) or 1
1✔
1837
            if n_cycles > max_num_cycles:
1✔
1838
                logging.warning(
1✔
1839
                    "Your simulation is using multi-cycle without enough cells.\n"
1840
                    "  => Number of cycles has been automatically set to the max: %d",
1841
                    max_num_cycles,
1842
                )
1843
                n_cycles = max_num_cycles
1✔
1844
        return n_cycles
1✔
1845

1846
    def _build_model(self):
5✔
1847
        """Build the model
1848

1849
        Internally it calls _build_single_model, over multiple
1850
        cycles if necessary.
1851

1852
        Note: only relevant for coreNeuron
1853
        """
1854
        self._n_cycles = self.compute_n_cycles()
4✔
1855

1856
        # Without multi-cycle, it's a trivial model build.
1857
        # sub_targets is False
1858
        if self._n_cycles == 1:
4✔
1859
            self._build_single_model()
4✔
1860
            return
4✔
1861

1862
        logging.info("MULTI-CYCLE RUN: %s Cycles", self._n_cycles)
1✔
1863
        target = self._target_manager.get_target(self._target_spec)
1✔
1864
        TimerManager.archive(archive_name="Before Cycle Loop")
1✔
1865

1866
        PopulationNodes.freeze_offsets()
1✔
1867

1868
        if SimConfig.loadbal_mode != LoadBalanceMode.Memory:
1✔
1869
            sub_targets = target.generate_subtargets(self._n_cycles)
1✔
1870

1871
        for cycle_i in range(self._n_cycles):
1✔
1872
            logging.info("")
1✔
1873
            logging.info("-" * 60)
1✔
1874
            log_stage(f"==> CYCLE {cycle_i + 1} (OUT OF {self._n_cycles})")
1✔
1875
            logging.info("-" * 60)
1✔
1876

1877
            self.clear_model()
1✔
1878

1879
            if SimConfig.loadbal_mode != LoadBalanceMode.Memory:
1✔
1880
                for cur_target in sub_targets[cycle_i]:
1✔
1881
                    self._target_manager.register_target(cur_target)
1✔
1882
                    pop = next(iter(cur_target.population_names))
1✔
1883
                    for circuit in self._sonata_circuits.values():
1✔
1884
                        tmp_target_spec = TargetSpec(circuit.NodesetName, circuit.PopulationName)
1✔
1885
                        if tmp_target_spec.population == pop:
1✔
1886
                            tmp_target_spec.name = cur_target.name
1✔
1887
                            circuit.NodesetName = tmp_target_spec.name
1✔
1888
                            circuit.PopulationName = tmp_target_spec.population
1✔
1889

1890
            self._cycle_i = cycle_i
1✔
1891
            self._build_single_model()
1✔
1892

1893
            # Move generated files aside (to be merged later)
1894
            if MPI.rank == 0:
1✔
1895
                base_filesdat = ospath.join(CoreConfig.datadir, "files")
1✔
1896
                os.rename(base_filesdat + ".dat", base_filesdat + f"_{cycle_i}.dat")
1✔
1897
            # Archive timers for this cycle
1898
            TimerManager.archive(archive_name=f"Cycle Run {cycle_i + 1:d}")
1✔
1899

1900
        if MPI.rank == 0:
1✔
1901
            self._merge_filesdat(self._n_cycles)
1✔
1902

1903
    # -
1904
    def _instantiate_simulation(self):
5✔
1905
        """Initialize the simulation
1906

1907
        - load targets
1908
        - check connections
1909
        - build the model
1910
        """
1911
        # Keep the initial RSS for the SHM file transfer calculations
1912
        self._initial_rss = SHMUtil.get_node_rss()
4✔
1913
        print_mem_usage()
4✔
1914

1915
        self.load_targets()
4✔
1916

1917
        # Check connection block configuration and raise warnings for overriding
1918
        # parameters
1919
        SimConfig.check_connections_configure(self._target_manager)
4✔
1920

1921
        self._build_model()
4✔
1922

1923
    # -
1924
    @timeit(name="finished Run")
5✔
1925
    def run(self, cleanup=True):
5✔
1926
        """Prepares and launches the simulation according to the loaded config.
1927
        If '--only-build-model' option is set, simulation is skipped.
1928

1929
        Args:
1930
            cleanup (bool): Free up the model and intermediate files [default: true]
1931
                Rationale is: the high-level run() method it's typically for a
1932
                one shot simulation so we should cleanup. If not it can be set to False
1933
        """
1934
        if SimConfig.dry_run:
3✔
1935
            log_stage("============= DRY RUN (SKIP SIMULATION) =============")
2✔
1936
            if not self._dry_run_stats.cell_memory_usage.preloaded:
2✔
1937
                self._dry_run_stats.collect_all_mpi()
2✔
1938
                self._dry_run_stats.export_cell_memory_usage()
2✔
1939
            self._dry_run_stats.estimate_cell_memory()
2✔
1940
            self._dry_run_stats.display_total()
2✔
1941
            self._dry_run_stats.display_node_suggestions()
2✔
1942
            ranks = self._dry_run_stats.get_num_target_ranks(SimConfig.num_target_ranks)
2✔
1943
            try:
2✔
1944
                self._dry_run_stats.distribute_cells_with_validation(
2✔
1945
                    ranks, SimConfig.modelbuilding_steps
1946
                )
1947
            except RuntimeError:
×
1948
                logging.exception("Dry run failed")
×
1949
            return
2✔
1950

1951
        if not SimConfig.simulate_model:
3✔
1952
            self.sim_init()
1✔
1953
            log_stage("======== [SKIPPED] SIMULATION (MODEL BUILD ONLY) ========")
1✔
1954
        elif not SimConfig.build_model:
3✔
1955
            log_stage("============= SIMULATION (SKIP MODEL BUILD) =============")
1✔
1956
            # coreneuron needs the report file created
1957
            self._run_coreneuron()
1✔
1958
        else:
1959
            log_stage("======================= SIMULATION =======================")
3✔
1960
            self.run_all()
3✔
1961

1962
        # Create SUCCESS file if the simulation finishes successfully
1963
        if self._success_file:
3✔
1964
            self._touch_file(self._success_file)
3✔
1965
            logging.info("Finished! Creating .SUCCESS file: '%s'", self._success_file)
3✔
1966
        else:
NEW
1967
            logging.info(
×
1968
                "Finished! Skipping .SUCCESS file creation:"
1969
                " config was passed as a libsonata.SimulationConfig object (no file path)"
1970
            )
1971

1972
        # Save seclamp holding currents for gap junction user corrections
1973
        if (
3✔
1974
            gj_target_pop := SimConfig.beta_features.get("gapjunction_target_population")
1975
        ) and SimConfig.beta_features.get("procedure_type") == "find_holding_current":
1976
            gj_manager = self._circuits.get_edge_manager(
1✔
1977
                gj_target_pop, gj_target_pop, GapJunctionManager
1978
            )
1979
            gj_manager.save_seclamp()
1✔
1980

1981
        self.move_dumpcellstates_to_output_root()
3✔
1982

1983
        if cleanup:
3✔
1984
            self.cleanup()
3✔
1985

1986
    @staticmethod
5✔
1987
    @run_only_rank0
5✔
1988
    def _remove_file(file_name):
5✔
1989
        import contextlib
4✔
1990

1991
        with contextlib.suppress(FileNotFoundError):
4✔
1992
            os.remove(file_name)
4✔
1993

1994
    @staticmethod
5✔
1995
    @run_only_rank0
5✔
1996
    def _touch_file(file_name):
5✔
1997
        with open(file_name, "a", encoding="utf-8"):
3✔
1998
            os.utime(file_name, None)
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc