From 129cba22746b0f4e9e7c923f3bf59ba2b16fb2f2 Mon Sep 17 00:00:00 2001 From: filimarc Date: Fri, 16 Jan 2026 14:59:28 +0100 Subject: [PATCH 1/3] fix: make flush method to save in file --- packages/bsb-arbor/bsb_arbor/adapter.py | 16 ++++---- .../bsb-core/bsb/cli/commands/_commands.py | 2 +- packages/bsb-core/bsb/core.py | 4 +- packages/bsb-core/bsb/simulation/adapter.py | 10 ++--- packages/bsb-core/bsb/simulation/results.py | 40 ++++++++++++------- packages/bsb-nest/bsb_nest/adapter.py | 8 ++-- packages/bsb-neuron/bsb_neuron/adapter.py | 13 ++++-- 7 files changed, 57 insertions(+), 36 deletions(-) diff --git a/packages/bsb-arbor/bsb_arbor/adapter.py b/packages/bsb-arbor/bsb_arbor/adapter.py index 72d874ef..1d5480a2 100644 --- a/packages/bsb-arbor/bsb_arbor/adapter.py +++ b/packages/bsb-arbor/bsb_arbor/adapter.py @@ -25,11 +25,11 @@ class ArborSimulationData(SimulationData): Container class for simulation data. """ - def __init__(self, simulation): + def __init__(self, simulation, filename): """ Container class for simulation data. """ - super().__init__(simulation) + super().__init__(simulation, filename) self.arbor_sim: arbor.simulation = None @@ -234,7 +234,9 @@ def __iter__(self): :yield: Each GID in the population's ranges """ - yield from itertools.chain.from_iterable(range(r[0], r[1]) for r in self._ranges) + yield from itertools.chain.from_iterable( + range(r[0], r[1]) for r in self._ranges + ) class GIDManager: @@ -375,11 +377,11 @@ def __init__(self, comm=None): super().__init__(comm) self.simdata: dict[ArborSimulation, ArborSimulationData] = {} - def prepare(self, simulation: "ArborSimulation") -> ArborSimulationData: + def prepare(self, simulation: "ArborSimulation", filename) -> ArborSimulationData: """ Prepares the arbor simulation engine with the given simulation. """ - simdata = self._create_simdata(simulation) + simdata = self._create_simdata(simulation, filename) try: context = arbor.context(arbor.proc_allocation(threads=simulation.threads)) if self.comm.get_size() > 1: @@ -466,8 +468,8 @@ def get_recipe(self, simulation, simdata=None): self._cache_devices(simulation, simdata) return ArborRecipe(simulation, simdata) - def _create_simdata(self, simulation): - self.simdata[simulation] = simdata = ArborSimulationData(simulation) + def _create_simdata(self, simulation, filename): + self.simdata[simulation] = simdata = ArborSimulationData(simulation, filename) self._assign_chunks(simulation, simdata) return simdata diff --git a/packages/bsb-core/bsb/cli/commands/_commands.py b/packages/bsb-core/bsb/cli/commands/_commands.py index cc9c402a..345c5e28 100644 --- a/packages/bsb-core/bsb/cli/commands/_commands.py +++ b/packages/bsb-core/bsb/cli/commands/_commands.py @@ -229,7 +229,7 @@ def handler(self, context): level=0, ) try: - result = network.run_simulation(sim_name) + result = network.run_simulation(sim_name, root / f"{uuid4()}.nio") except NodeNotFoundError as e: append = ", " if len(network.simulations) else "" append += ", ".join(f"'{name}'" for name in extra_simulations) diff --git a/packages/bsb-core/bsb/core.py b/packages/bsb-core/bsb/core.py index 9a1125f8..caf8ec25 100644 --- a/packages/bsb-core/bsb/core.py +++ b/packages/bsb-core/bsb/core.py @@ -449,7 +449,7 @@ def run_pipelines(self, fail_fast=True, pipelines=None): pool.schedule(pipelines) pool.execute() - def run_simulation(self, simulation_name: str): + def run_simulation(self, simulation_name: str, output_filename: str = None): """ Run a simulation starting from the default single-instance adapter. @@ -460,7 +460,7 @@ def run_simulation(self, simulation_name: str): adapter = get_simulation_adapter( simulation.simulator, comm=self._comm.get_communicator() ) - return adapter.simulate(simulation)[0] + return adapter.simulate(simulation, filename=output_filename)[0] def get_simulation(self, sim_name: str) -> Simulation: """ diff --git a/packages/bsb-core/bsb/simulation/adapter.py b/packages/bsb-core/bsb/simulation/adapter.py index b31e28a6..1815d756 100644 --- a/packages/bsb-core/bsb/simulation/adapter.py +++ b/packages/bsb-core/bsb/simulation/adapter.py @@ -66,7 +66,7 @@ def use_bar(self): class SimulationData: - def __init__(self, simulation: "Simulation", result=None): + def __init__(self, simulation: "Simulation", result=None, filename=None): self.chunks = None self.populations = dict() self.placement: dict[CellModel, PlacementSet] = { @@ -75,7 +75,7 @@ def __init__(self, simulation: "Simulation", result=None): self.connections = dict() self.devices = dict() if result is None: - result = SimulationResult(simulation) + result = SimulationResult(simulation, filename=filename) self.result: SimulationResult = result @@ -92,7 +92,7 @@ def __init__(self, comm=None): self._duration = None self.current_checkpoint = 0 - def simulate(self, *simulations, post_prepare=None): + def simulate(self, *simulations, post_prepare=None, filename=None): """ Simulate the given simulations. @@ -113,7 +113,7 @@ def simulate(self, *simulations, post_prepare=None): self._controllers.append(listener) for simulation in simulations: - data = self.prepare(simulation) + data = self.prepare(simulation, filename) alldata.append(data) for hook in simulation.post_prepare: hook(self, simulation, data) @@ -123,7 +123,7 @@ def simulate(self, *simulations, post_prepare=None): return self.collect(results) @abc.abstractmethod - def prepare(self, simulation): # pragma: nocover + def prepare(self, simulation, filename): # pragma: nocover """ Reset the simulation backend and prepare for the given simulation. diff --git a/packages/bsb-core/bsb/simulation/results.py b/packages/bsb-core/bsb/simulation/results.py index 1547d22e..f5422902 100644 --- a/packages/bsb-core/bsb/simulation/results.py +++ b/packages/bsb-core/bsb/simulation/results.py @@ -9,22 +9,27 @@ class SimulationResult: - def __init__(self, simulation): - from neo import Block + def __init__(self, simulation, filename=None): + from neo import Block, io tree = simulation.__tree__() with contextlib.suppress(KeyError): del tree["post_prepare"] - self.block = Block(name=simulation.name, config=tree) - self.recorders = [] - - @property - def spiketrains(self): - return self.block.segments[0].spiketrains + if filename: + self.filename = filename + self.name = simulation.name + io = io.NixIO(filename, mode="rw") + io.write(Block(name=self.name, nix_name=self.name, config=tree)) + for i, nixblock in enumerate(io.nix_file.blocks): + if self.name == nixblock.name: + self.block_id = i + io.close() + else: + self.block = Block( + name=simulation.name, nix_name=simulation.name, config=tree + ) - @property - def analogsignals(self): - return self.block.segments[0].analogsignals + self.recorders = [] def add(self, recorder): self.recorders.append(recorder) @@ -36,21 +41,28 @@ def create_recorder(self, flush: typing.Callable[["neo.core.Segment"], None]): return recorder def flush(self): - from neo import Segment + from neo import Segment, io segment = Segment() - self.block.segments.append(segment) for recorder in self.recorders: try: recorder.flush(segment) except Exception: traceback.print_exc() warn("Recorder errored out!") + if hasattr(self, "filename"): + io = io.NixIO(self.filename, mode="rw") + block = io.nix_file.blocks[self.block_id] + io._write_segment(segment, block) + io.close() + else: + self.block.segments.append(segment) def write(self, filename, mode): from neo import io - io.NixIO(filename, mode=mode).write(self.block) + if hasattr(self, "block"): + io.NixIO(filename, mode=mode).write(self.block) class SimulationRecorder: diff --git a/packages/bsb-nest/bsb_nest/adapter.py b/packages/bsb-nest/bsb_nest/adapter.py index 7accd0ca..aeb16bf3 100644 --- a/packages/bsb-nest/bsb_nest/adapter.py +++ b/packages/bsb-nest/bsb_nest/adapter.py @@ -58,7 +58,7 @@ def simulate(self, *simulations, post_prepare=None): finally: self.reset_kernel() - def prepare(self, simulation): + def prepare(self, simulation, filename): """ Prepare the simulation environment in NEST. @@ -80,7 +80,7 @@ def prepare(self, simulation): :rtype: bsb.simulation.adapter.SimulationData """ self.simdata[simulation] = SimulationData( - simulation, result=NestResult(simulation) + simulation, result=NestResult(simulation, filename) ) try: report("Installing NEST modules...", level=2) @@ -186,7 +186,9 @@ def connect_neurons(self, simulation): ) ) except Exception as e: - raise NestConnectError(f"{connection_model} error during connect.") from e + raise NestConnectError( + f"{connection_model} error during connect." + ) from e def set_settings(self, simulation: "NestSimulation"): nest.set_verbosity(simulation.verbosity) diff --git a/packages/bsb-neuron/bsb_neuron/adapter.py b/packages/bsb-neuron/bsb_neuron/adapter.py index d00ec34f..3ddde80f 100644 --- a/packages/bsb-neuron/bsb_neuron/adapter.py +++ b/packages/bsb-neuron/bsb_neuron/adapter.py @@ -70,7 +70,7 @@ def engine(self): return engine - def prepare(self, simulation): + def prepare(self, simulation, filename): """ Prepare the simulation environment and data structures for running a NEURON simulation. @@ -90,7 +90,8 @@ def prepare(self, simulation): """ self.simdata[simulation] = NeuronSimulationData( - simulation, result=NeuronResult(simulation) + simulation, + result=NeuronResult(simulation, filename=filename), ) try: report("Preparing simulation", level=2) @@ -192,7 +193,9 @@ def _map_transceivers(self, simulation, simdata): offset = 0 transmap = {} - pre_types = set(cs.pre_type for cs in simulation.get_connectivity_sets().values()) + pre_types = set( + cs.pre_type for cs in simulation.get_connectivity_sets().values() + ) for pre_type in sorted(pre_types, key=lambda pre_type: pre_type.name): data = [] for _cm, cs in simulation.get_connectivity_sets().items(): @@ -209,7 +212,9 @@ def _map_transceivers(self, simulation, simdata): continue # Now look up which transmitters are on our chunks - pre_t, _ = cs.load_connections().from_(simdata.chunks).as_globals().all() + pre_t, _ = ( + cs.load_connections().from_(simdata.chunks).as_globals().all() + ) our_cm_transmitters = np.unique(pre_t[:, :2], axis=0) # Look up the local ids of those transmitters pre_lc, _ = cs.load_connections().from_(simdata.chunks).all() From 98f2e066431f1adbe60ebb3ec0521619bc240d64 Mon Sep 17 00:00:00 2001 From: filimarc Date: Mon, 19 Jan 2026 11:59:34 +0100 Subject: [PATCH 2/3] fix: filename options and sim tests --- examples/nest-simulation/tests/test_examples.py | 4 ++-- examples/neuron-simulation/tests/test_examples.py | 4 ++-- packages/bsb-arbor/bsb_arbor/adapter.py | 8 ++++---- packages/bsb-core/bsb/simulation/adapter.py | 2 +- packages/bsb-nest/bsb_nest/adapter.py | 12 ++++++------ packages/bsb-neuron/bsb_neuron/adapter.py | 10 +++------- 6 files changed, 18 insertions(+), 22 deletions(-) diff --git a/examples/nest-simulation/tests/test_examples.py b/examples/nest-simulation/tests/test_examples.py index f7c65953..28fe210f 100644 --- a/examples/nest-simulation/tests/test_examples.py +++ b/examples/nest-simulation/tests/test_examples.py @@ -62,7 +62,7 @@ def test_json_example(self): self.scaffold.compile() self._test_scaffold_results() results = self.scaffold.run_simulation("basal_activity") - self._test_simulation_results(results.spiketrains) + self._test_simulation_results(results.block.segments[0].spiketrains) def test_yaml_example(self): self.cfg = parse_configuration_file( @@ -72,7 +72,7 @@ def test_yaml_example(self): self.scaffold.compile() self._test_scaffold_results() results = self.scaffold.run_simulation("basal_activity") - self._test_simulation_results(results.spiketrains) + self._test_simulation_results(results.block.segments[0].spiketrains) def test_python_example(self): import scripts.guide_nest # noqa: F401 diff --git a/examples/neuron-simulation/tests/test_examples.py b/examples/neuron-simulation/tests/test_examples.py index 8ecd2208..0cefa8d1 100644 --- a/examples/neuron-simulation/tests/test_examples.py +++ b/examples/neuron-simulation/tests/test_examples.py @@ -67,7 +67,7 @@ def test_json_example(self): self.scaffold.compile() self._test_scaffold_results() results = self.scaffold.run_simulation("neuronsim") - self._test_simulation_results(results.analogsignals) + self._test_simulation_results(results.block.segments[0].analogsignals) def test_yaml_example(self): self.cfg = parse_configuration_file( @@ -77,7 +77,7 @@ def test_yaml_example(self): self.scaffold.compile() self._test_scaffold_results() results = self.scaffold.run_simulation("neuronsim") - self._test_simulation_results(results.analogsignals) + self._test_simulation_results(results.block.segments[0].analogsignals) def test_python_example(self): import scripts.guide_neuron # noqa: F401 diff --git a/packages/bsb-arbor/bsb_arbor/adapter.py b/packages/bsb-arbor/bsb_arbor/adapter.py index 1d5480a2..db77c602 100644 --- a/packages/bsb-arbor/bsb_arbor/adapter.py +++ b/packages/bsb-arbor/bsb_arbor/adapter.py @@ -234,9 +234,7 @@ def __iter__(self): :yield: Each GID in the population's ranges """ - yield from itertools.chain.from_iterable( - range(r[0], r[1]) for r in self._ranges - ) + yield from itertools.chain.from_iterable(range(r[0], r[1]) for r in self._ranges) class GIDManager: @@ -377,7 +375,9 @@ def __init__(self, comm=None): super().__init__(comm) self.simdata: dict[ArborSimulation, ArborSimulationData] = {} - def prepare(self, simulation: "ArborSimulation", filename) -> ArborSimulationData: + def prepare( + self, simulation: "ArborSimulation", filename=None + ) -> ArborSimulationData: """ Prepares the arbor simulation engine with the given simulation. """ diff --git a/packages/bsb-core/bsb/simulation/adapter.py b/packages/bsb-core/bsb/simulation/adapter.py index 1815d756..b365c303 100644 --- a/packages/bsb-core/bsb/simulation/adapter.py +++ b/packages/bsb-core/bsb/simulation/adapter.py @@ -123,7 +123,7 @@ def simulate(self, *simulations, post_prepare=None, filename=None): return self.collect(results) @abc.abstractmethod - def prepare(self, simulation, filename): # pragma: nocover + def prepare(self, simulation, filename=None): # pragma: nocover """ Reset the simulation backend and prepare for the given simulation. diff --git a/packages/bsb-nest/bsb_nest/adapter.py b/packages/bsb-nest/bsb_nest/adapter.py index aeb16bf3..13109a13 100644 --- a/packages/bsb-nest/bsb_nest/adapter.py +++ b/packages/bsb-nest/bsb_nest/adapter.py @@ -51,14 +51,16 @@ def __init__(self, comm=None): self.loaded_modules = set() self._prev_chkpoint = 0 - def simulate(self, *simulations, post_prepare=None): + def simulate(self, *simulations, post_prepare=None, filename=None): try: self.reset_kernel() - return super().simulate(*simulations, post_prepare=post_prepare) + return super().simulate( + *simulations, post_prepare=post_prepare, filename=filename + ) finally: self.reset_kernel() - def prepare(self, simulation, filename): + def prepare(self, simulation, filename=None): """ Prepare the simulation environment in NEST. @@ -186,9 +188,7 @@ def connect_neurons(self, simulation): ) ) except Exception as e: - raise NestConnectError( - f"{connection_model} error during connect." - ) from e + raise NestConnectError(f"{connection_model} error during connect.") from e def set_settings(self, simulation: "NestSimulation"): nest.set_verbosity(simulation.verbosity) diff --git a/packages/bsb-neuron/bsb_neuron/adapter.py b/packages/bsb-neuron/bsb_neuron/adapter.py index 3ddde80f..8402ca50 100644 --- a/packages/bsb-neuron/bsb_neuron/adapter.py +++ b/packages/bsb-neuron/bsb_neuron/adapter.py @@ -70,7 +70,7 @@ def engine(self): return engine - def prepare(self, simulation, filename): + def prepare(self, simulation, filename=None): """ Prepare the simulation environment and data structures for running a NEURON simulation. @@ -193,9 +193,7 @@ def _map_transceivers(self, simulation, simdata): offset = 0 transmap = {} - pre_types = set( - cs.pre_type for cs in simulation.get_connectivity_sets().values() - ) + pre_types = set(cs.pre_type for cs in simulation.get_connectivity_sets().values()) for pre_type in sorted(pre_types, key=lambda pre_type: pre_type.name): data = [] for _cm, cs in simulation.get_connectivity_sets().items(): @@ -212,9 +210,7 @@ def _map_transceivers(self, simulation, simdata): continue # Now look up which transmitters are on our chunks - pre_t, _ = ( - cs.load_connections().from_(simdata.chunks).as_globals().all() - ) + pre_t, _ = cs.load_connections().from_(simdata.chunks).as_globals().all() our_cm_transmitters = np.unique(pre_t[:, :2], axis=0) # Look up the local ids of those transmitters pre_lc, _ = cs.load_connections().from_(simdata.chunks).all() From 3ae47466ea4bb6b881d1d1480640f785951c0bb7 Mon Sep 17 00:00:00 2001 From: filimarc Date: Tue, 20 Jan 2026 10:34:58 +0100 Subject: [PATCH 3/3] fix: backward compatibility --- packages/bsb-arbor/bsb_arbor/adapter.py | 2 +- packages/bsb-core/bsb/cli/commands/_commands.py | 6 +++--- packages/bsb-core/bsb/simulation/results.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/bsb-arbor/bsb_arbor/adapter.py b/packages/bsb-arbor/bsb_arbor/adapter.py index db77c602..b64021cb 100644 --- a/packages/bsb-arbor/bsb_arbor/adapter.py +++ b/packages/bsb-arbor/bsb_arbor/adapter.py @@ -29,7 +29,7 @@ def __init__(self, simulation, filename): """ Container class for simulation data. """ - super().__init__(simulation, filename) + super().__init__(simulation, filename=filename) self.arbor_sim: arbor.simulation = None diff --git a/packages/bsb-core/bsb/cli/commands/_commands.py b/packages/bsb-core/bsb/cli/commands/_commands.py index 345c5e28..c3a6779f 100644 --- a/packages/bsb-core/bsb/cli/commands/_commands.py +++ b/packages/bsb-core/bsb/cli/commands/_commands.py @@ -229,13 +229,13 @@ def handler(self, context): level=0, ) try: - result = network.run_simulation(sim_name, root / f"{uuid4()}.nio") + network.run_simulation(sim_name, output_filename=root / f"{uuid4()}.nio") except NodeNotFoundError as e: append = ", " if len(network.simulations) else "" append += ", ".join(f"'{name}'" for name in extra_simulations) errr.wrap(type(e), e, append=append) - else: - result.write(root / f"{uuid4()}.nio", "ow") + # else: + # result.write(root / f"{uuid4()}.nio", "ow") def get_options(self): return { diff --git a/packages/bsb-core/bsb/simulation/results.py b/packages/bsb-core/bsb/simulation/results.py index f5422902..fbf68025 100644 --- a/packages/bsb-core/bsb/simulation/results.py +++ b/packages/bsb-core/bsb/simulation/results.py @@ -31,6 +31,16 @@ def __init__(self, simulation, filename=None): self.recorders = [] + @property + def analogsignals(self): + if hasattr(self, "block"): + return self.block.segments[0].analogsignals + + @property + def spiketrains(self): + if hasattr(self, "block"): + return self.block.segments[0].spiketrains + def add(self, recorder): self.recorders.append(recorder)