Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0796e48
feat: start to add AdapterController
filimarc Jul 15, 2025
68bc534
feat: start to add BasicSimulationListener
Jul 16, 2025
4ea9a42
Merge branch 'main' into feature/sim-controllers
filimarc Jul 17, 2025
570b136
Merge branch 'main' into feature/sim-controllers
filimarc Aug 26, 2025
558188c
fix: adapting run NeuronAdapter method
filimarc Aug 26, 2025
f77edec
fix: clean BasicSimulationListeners
filimarc Aug 27, 2025
b280929
Merge branch 'main' into feature/sim-controllers
filimarc Aug 27, 2025
814f9e0
fix: check if listener already selected as controller
filimarc Aug 27, 2025
e380d88
fix: update nest adapter to new system
filimarc Aug 27, 2025
dcad6d2
fix: create a B listener during prep
filimarc Aug 28, 2025
9472064
feat: add progress bar for simulation (only-neuron)
Aug 29, 2025
81a5752
fix: change back to collect at the end of simulate | Update Arbor Ada…
Aug 29, 2025
f92377f
test: add test for AdapterController
filimarc Sep 1, 2025
a140130
feat: Add logic to free memory when data are flushed for Arbor
Sep 2, 2025
0613aca
test: Add test for async ending | remove AdapterProgress
Sep 2, 2025
d4e62f4
chore: lint
Sep 2, 2025
0b40df2
test: add test for two AdapterControllers
Sep 2, 2025
5198e0f
fix: remove the rank() == 0 in Adapters'run
filimarc Sep 4, 2025
3ab3f12
fix: substitute tdqm for progress bar
Sep 4, 2025
074a881
refactor: change to isinstance control during controller registration
Sep 5, 2025
142ccd6
fix: Implement changes and generalisations
Sep 17, 2025
65be85d
feat: give option to set a BasicSimulationListener as default
Sep 17, 2025
288a031
lint: do lint
Sep 17, 2025
6e2a5d8
fix: fix issues with the new option | clean some stuff
filimarc Sep 18, 2025
2dc1941
test: add test for incorrect usage
filimarc Sep 18, 2025
80a36a0
docs: add page for simulation-controllers | use report for progress bar
filimarc Sep 18, 2025
1947b35
fix: refactor progress()
filimarc Sep 18, 2025
9181f61
docs: small changes
filimarc Sep 18, 2025
3d7db46
Merge branch 'next' into feature/sim-controllers
filimarc Sep 18, 2025
ee9a901
docs: update options page for simulation_report
Sep 18, 2025
179ce89
docs: small updates
Sep 19, 2025
40d8c8d
fix: changes and lint
filimarc Oct 6, 2025
96c8473
style: clean get_next_checkpoint()
filimarc Oct 6, 2025
bae97fd
fix: correct set min default
filimarc Oct 6, 2025
b4a5ae2
fix: clean and insert tqdm as progress bar
filimarc Oct 7, 2025
ea8f51f
fix: progress bar now is sync with current_checkpoint
filimarc Oct 8, 2025
a8bbf76
fix: correctly retrieve simulator time for progress | do not use report
filimarc Oct 8, 2025
880732b
fix: correctly retrieve simulator time for progress
filimarc Oct 8, 2025
22f3c95
docs: change controller example in doc
Oct 10, 2025
79104fc
docs: rechange controller example in doc
Oct 10, 2025
48ac68a
Merge branch 'next' into feature/sim-controllers
filimarc Oct 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions packages/bsb-arbor/bsb_arbor/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,13 @@ def prepare(self, simulation: "ArborSimulation") -> ArborSimulationData:
report("MPI processes:", context.ranks, level=2)
report("Threads per process:", context.threads, level=2)
recipe = self.get_recipe(simulation, simdata)
self._duration = simulation.duration
# Gap junctions are required for domain decomposition
self.domain = arbor.partition_load_balance(recipe, context)
self.gids = set(it.chain.from_iterable(g.gids for g in self.domain.groups))
simdata.arbor_sim = arbor.simulation(recipe, context, self.domain)
self.prepare_samples(simulation, simdata)
self.implement_components(simulation)
self.load_controllers(simulation)
report("prepared simulation", level=1)
return simdata
except Exception:
Expand All @@ -422,10 +424,6 @@ def prepare(self, simulation: "ArborSimulation") -> ArborSimulationData:
def get_gid_manager(self, simulation, simdata):
return GIDManager(simulation, simdata)

def prepare_samples(self, simulation, simdata):
for device in simulation.devices.values():
device.prepare_samples(simdata, comm=self.comm)

def run(self, *simulations):
if len(simulations) != 1:
raise RuntimeError(
Expand All @@ -446,14 +444,19 @@ def run(self, *simulations):

start = time.time()
report("running simulation", level=1)
arbor_sim.run(simulation.duration * U.ms, dt=simulation.resolution * U.ms)
report(f"completed simulation. {time.time() - start:.2f}s", level=1)

for t, checkpoint_controllers in self.get_next_checkpoint():
arbor_sim.run(t * U.ms, dt=simulation.resolution * U.ms)
self.execute_checkpoints(checkpoint_controllers)
report(f"Completed simulation. {time.time() - start:.2f}s", level=1)
if simulation.profiling and arbor.config()["profiling"]:
report("printing profiler summary", level=2)
report(arbor.profiler_summary(), level=1)
return [simdata.result]
finally:
results = [self.simdata[sim].result for sim in simulations]
del self.simdata[simulation]
return results

def get_recipe(self, simulation, simdata=None):
if simdata is None:
Expand Down
2 changes: 1 addition & 1 deletion packages/bsb-arbor/bsb_arbor/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __boot__(self):
def register_probe_id(self, gid, tag):
self._probe_ids.append((gid, tag))

def prepare_samples(self, simdata, comm):
def implement(self, adapter, simulation, simdata):
self._handles = [
self.sample(simdata.arbor_sim, probe_id) for probe_id in self._probe_ids
]
Expand Down
11 changes: 3 additions & 8 deletions packages/bsb-arbor/bsb_arbor/devices/probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,10 @@ def validate_specifics(self):
f"`{self.probe_type}` is not a valid probe type for `{self.name}`"
)

def implement(self, target):
probe_args = ("where", "mechanism", "ion", "state")
kwargs = dict((k, getattr(self, k)) for k in probe_args if hasattr(self, k))
return [getattr(arbor, self.get_probe_name())(**kwargs)]

def prepare_samples(self, sim, comm):
super().prepare_samples(sim, comm)
def implement(self, adapter, simulation, simdata):
super().implement(adapter, simulation, simdata)
for probe_id, handle in zip(self._probe_ids, self._handles, strict=False):
self.adapter.result.add(ProbeRecorder(self, sim, probe_id, handle))
self.adapter.result.add(ProbeRecorder(self, simdata, probe_id, handle))


class ProbeRecorder:
Expand Down
6 changes: 3 additions & 3 deletions packages/bsb-arbor/bsb_arbor/devices/spike_recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ class SpikeRecorder(ArborDevice, classmap_entry="spike_recorder"):
def boot(self):
self._gids = set()

def prepare_samples(self, simdata, comm):
super().prepare_samples(simdata, comm)
if not comm.get_rank():
def implement(self, adapter, simulation, simdata):
super().implement(adapter, simulation, simdata)
if not adapter.comm.get_rank():

def record_device_spikes(segment):
spiketrain = list()
Expand Down
2 changes: 1 addition & 1 deletion packages/bsb-core/bsb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ def __dir__():
import bsb.voxels

AdapterError: type["bsb.exceptions.AdapterError"]
AdapterProgress: type["bsb.simulation.adapter.AdapterProgress"]
AfterConnectivityHook: type["bsb.postprocessing.AfterConnectivityHook"]
AfterPlacementHook: type["bsb.postprocessing.AfterPlacementHook"]
AllToAll: type["bsb.connectivity.general.AllToAll"]
Expand Down Expand Up @@ -229,6 +228,7 @@ def __dir__():
FixedIndegree: type["bsb.connectivity.general.FixedIndegree"]
FixedOutdegree: type["bsb.connectivity.general.FixedOutdegree"]
FixedPositions: type["bsb.placement.strategy.FixedPositions"]
FixedStepProgressController: type["bsb.simulation.adapter.FixedStepProgressController"]
FractionFilter: type["bsb.simulation.targetting.FractionFilter"]
GatewayError: type["bsb.exceptions.GatewayError"]
GeneratedMorphology: type["bsb.storage.interfaces.GeneratedMorphology"]
Expand Down
23 changes: 23 additions & 0 deletions packages/bsb-core/bsb/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ def get_default(self):
return "network_configuration.json"


class SimulationProgress(
BsbOption,
name="sim_console_progress",
cli=("scp", "sim_console_progress"),
project=("sim_console_progress",),
script=("sim_console_progress",),
env=("BSB_SIM_PROGRESS",),
):
"""
Activate reports during simulations, set the time steps of the report.
"""

def setter(self, value):
return float(value)

def getter(self, value):
return float(value)


class ProfilingOption(
BsbOption,
name="profiling",
Expand Down Expand Up @@ -157,6 +176,10 @@ def config():
return ConfigOption


def sim_console_progress():
return SimulationProgress


def profiling():
return ProfilingOption

Expand Down
3 changes: 2 additions & 1 deletion packages/bsb-core/bsb/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_default(self):
import contextlib
import functools

from ._options import ProfilingOption, VerbosityOption
from ._options import ProfilingOption, SimulationProgress, VerbosityOption

# Store the module magic for unpolluted namespace copy
_module_magic = globals().copy()
Expand Down Expand Up @@ -388,6 +388,7 @@ def __delattr__(self, attr):

register_option("verbosity", VerbosityOption())
register_option("profiling", ProfilingOption())
register_option("sim_console_progress", SimulationProgress())

# Static public API
__all__ = [
Expand Down
135 changes: 102 additions & 33 deletions packages/bsb-core/bsb/simulation/adapter.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,68 @@
import abc
import itertools
import types
import os
import sys
import typing
from contextlib import ExitStack
from time import time

import numpy as np
from tqdm import tqdm

from bsb import AttributeMissingError, SimulationResult, options

from ..services.mpi import MPIService
from .results import SimulationResult

if typing.TYPE_CHECKING:
from ..storage import PlacementSet
from .cell import CellModel
from .simulation import Simulation


class AdapterProgress:
def __init__(self, duration):
self._duration = duration
class FixedStepProgressController:
def __init__(self, simulations, adapter, step=1):
self._status = 0
self._adapter = adapter
self._start = self._last_tick = time()
self._ticks = 0

def tick(self, step):
"""
Report simulation progress.
"""
self._step = step
self._sim_name = [sim._name for sim in simulations]
self._use_tty = os.isatty(sys.stdout.fileno()) and sum(os.get_terminal_size())

def silent():
self._status = self._adapter.current_checkpoint

if self._use_tty:
if not self._adapter.comm.get_rank():
self.run_checkpoint = self.use_bar
else:
self.run_checkpoint = silent
else:
if self._adapter.comm.get_rank():
self.run_checkpoint = silent

def get_next_checkpoint(self):
return self._status + self._step

def run_checkpoint(self):
now = time()
self._status = self._adapter.current_checkpoint
tic = now - self._last_tick
self._ticks += 1
el = now - self._start
progress = types.SimpleNamespace(
progression=step, duration=self._duration, time=time(), tick=tic, elapsed=el
)
el_time = now - self._start
duration = self._adapter._duration
msg = f"Simulation {self._sim_name} | progress: {self._status:.2f} "
msg += f"elapsed: {el_time:.2f}s - last step time: {tic:.2f}s - "
msg += f"exectuted: {(self._status / duration) * 100:.2f}%"
print(msg)
self._last_tick = now
return progress

def steps(self, step=1):
steps = itertools.chain(np.arange(0, self._duration, step), (self._duration,))
a, b = itertools.tee(steps)
next(b, None)
yield from zip(a, b, strict=False)

def complete(self):
return
def use_bar(self):
if not self._status:
self._progress_bar = tqdm(total=self._adapter._duration)
self._progress_bar.update(self._adapter.current_checkpoint - self._status)
elif self._adapter.current_checkpoint == self._adapter._duration:
self._progress_bar.update(self._adapter.current_checkpoint - self._status)
self._progress_bar.close()
else:
self._progress_bar.update(self._adapter.current_checkpoint - self._status)
self._status = self._adapter.current_checkpoint


class SimulationData:
Expand All @@ -66,9 +85,12 @@ def __init__(self, comm=None):
:param comm: The mpi4py MPI communicator to use. Only nodes in the communicator
will participate in the simulation. The first node will idle as the main node.
"""
self._progress_listeners = []

self.simdata: dict[Simulation, SimulationData] = dict()
self.comm = MPIService(comm)
self._controllers = []
self._duration = None
self.current_checkpoint = 0

def simulate(self, *simulations, post_prepare=None):
"""
Expand All @@ -84,6 +106,12 @@ def simulate(self, *simulations, post_prepare=None):
for simulation in simulations:
context.enter_context(simulation.scaffold.storage.read_only())
alldata = []
if options.sim_console_progress:
listener = FixedStepProgressController(
simulations, self, options.sim_console_progress
)
self._controllers.append(listener)

for simulation in simulations:
data = self.prepare(simulation)
alldata.append(data)
Expand Down Expand Up @@ -118,6 +146,30 @@ def run(self, *simulations):
"""
pass

def get_next_checkpoint(self):
while self.current_checkpoint < self._duration:
checkpoints = [
controller.get_next_checkpoint() for controller in self._controllers
]
# Filter out invalid "regressive" checkpoints,
# and default to the end of the simulation
chkp_noregressive = [
checkpoint
for checkpoint in checkpoints
if checkpoint > self.current_checkpoint
]
self.current_checkpoint = min(chkp_noregressive, default=self._duration)
participants = [
self._controllers[i]
for i, checkpoint in enumerate(checkpoints)
if checkpoint == self.current_checkpoint
]
yield (self.current_checkpoint, participants)

def execute_checkpoints(self, controllers):
for controller in controllers:
controller.run_checkpoint()

def collect(self, results):
"""
Collect the output the simulations that completed.
Expand All @@ -129,8 +181,25 @@ def collect(self, results):
result.flush()
return results

def add_progress_listener(self, listener):
self._progress_listeners.append(listener)


__all__ = ["AdapterProgress", "SimulationData", "SimulatorAdapter"]
def implement_components(self, simulation):
simdata = self.simdata[simulation]
for component in simulation.get_components():
component.implement(self, simulation, simdata)

def load_controllers(self, simulation):
for component in simulation.get_components():
if hasattr(component, "get_next_checkpoint"):
if hasattr(component, "run_checkpoint"):
self._controllers.append(component)
else:
raise AttributeMissingError(
f"Device {component.name} is configured to be a controller "
f"but run_checkpoint is not defined"
)


__all__ = [
"FixedStepProgressController",
"SimulationData",
"SimulatorAdapter",
]
5 changes: 5 additions & 0 deletions packages/bsb-core/bsb/simulation/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ def simulation(self):
def __str__(self):
return f"'{self.name}'"

def implement(self, adapter, simulation, simdata):
"""Method called when simulation is being set up. Can be used for components to
set themselves up and store the context they need to operate."""
pass


__all__ = ["SimulationComponent"]
8 changes: 8 additions & 0 deletions packages/bsb-core/bsb/simulation/simulation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import itertools
import typing

from .. import config
Expand Down Expand Up @@ -84,5 +85,12 @@ def get_connectivity_sets(
for model in sorted(self.connection_models.values())
}

def get_components(self):
return itertools.chain(
self.cell_models.values(),
self.connection_models.values(),
self.devices.values(),
)


__all__ = ["ProgressEvent", "Simulation"]
1 change: 1 addition & 0 deletions packages/bsb-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ verbosity = "bsb._options:verbosity"
sudo = "bsb._options:sudo"
version = "bsb._options:version"
config = "bsb._options:config"
sim_console_progress= "bsb._options:sim_console_progress"
profiling = "bsb._options:profiling"
debug_pool = "bsb._options:debug_pool"

Expand Down
Loading
Loading