Implement SONATA condition modifications#63
Conversation
- Add support for all 5 SONATA modification types: ttx, configure_all_sections,
section_list, section, and compartment_set
- Replace hardcoded MechanismConditions with generic mechanisms dict in Conditions
- Parse modifications from libsonata via SonataSimulationConfig.get_modifications()
- Apply modifications automatically during CircuitSimulation.instantiate_gids()
- Handle empty reports dict gracefully (libsonata returns None for reports: {})
- Add 40 unit tests for modifications with 100% coverage on modifications.py
- Require libsonata>=0.1.34 for snake_case modification type enums
- Add example notebook demonstrating all 5 modification types
bluecellulab/circuit_simulation.py
Outdated
| raise BluecellulabError( | ||
| "instantiate_gids() is called twice on the " | ||
| "same CircuitSimumation, this is not supported") | ||
| "same CircuitSimumation, this is not supported" |
There was a problem hiding this comment.
Thanks! Fixed in the latest commit.
| """Base class for all modification types.""" | ||
|
|
||
| name: str | ||
| type: str |
There was a problem hiding this comment.
Small thought: since the subclass already implies the modification kind, having a free type: str here means the instance can technically end up inconsistent with its class (e.g. a ModificationTTX whose type is "section"). Maybe we could make type fixed per subclass (or not part of the constructor) so it can’t drift?
There was a problem hiding this comment.
Good idea. I removed the free type: str from ModificationBase and explicitly defined it as a fixed Literal in each subclass (e.g., type: Literal["ttx"] = "ttx").
|
|
||
| def modification_from_libsonata(mod) -> ModificationBase: | ||
| """Convert a libsonata modification object to a BlueCelluLab dataclass.""" | ||
| type_name = mod.type.name # e.g. "ttx", "configure_all_sections", etc. |
There was a problem hiding this comment.
Maybe worth normalizing to lowercase before matching?
There was a problem hiding this comment.
Yes, thanks. I updated to use mod.type.name.lower().
| n_sections = 0 | ||
| for cell_id in target_cell_ids: | ||
| cell = cells[cell_id] | ||
| cell_applied = 0 |
There was a problem hiding this comment.
cell_applied reads a bit like a boolean but it actually counts how many sections in the cell were affected. Maybe something like sections_applied / sections_modified would make the later > 0 check clearer?
There was a problem hiding this comment.
Renamed cell_applied to sections_applied in all functions.
| section_configs: dict[str, tuple[str, set[str]]] = {} | ||
| for sec_name in section_names: | ||
| escaped = re.escape(sec_name) | ||
| config_str = re.sub(escaped + r"\.", "sec.", mod.section_configure) |
There was a problem hiding this comment.
I might be misunderstanding, but it looks like we replace
There was a problem hiding this comment.
I updated the code to only support one section per modification, as per the SONATA spec. Can you please check again?
|
|
||
| # Parse section_configure — bare format: "attr = value" | ||
| # Prefix with "seg." to make it executable | ||
| config_str = re.sub(r"(\b\w+)\s*=", r"seg.\1 =", mod.section_configure) |
There was a problem hiding this comment.
Small question: we build config_str with the regex rewrite but then overwrite it below by prefixing each full statement. Is the first transformation intentionally unused?
There was a problem hiding this comment.
Removed the confusing section_configs dict loop and the unused regex. Since the SONATA spec requires section modifications to reference only one section, the multi-section was not needed.
| pre_gids = None | ||
|
|
||
| # if pre_spike_trains take int as key then convert to CellId | ||
| normalized_pre_spike_trains: dict[CellId, Iterable] | None = None |
| "======================================================================\n", | ||
| "\n", | ||
| "Cell CellId(population_name='NodeA', id=0):\n", | ||
| "soma[0] cm = {soma.cm}\n", |
There was a problem hiding this comment.
small bug: the variable here and below were not resolved, did you run the cell with f-string?
There was a problem hiding this comment.
It was from an old run. I ran again, and I can see the values now.
| "# Simulation WITHOUT modifications (baseline)\n", | ||
| "baseline_config = Path(\"sim_quick_scx_sonata_multicircuit\") / \"simulation_config_noinput.json\"\n", | ||
| "sim_baseline = CircuitSimulation(baseline_config)\n", | ||
| "sim_baseline.instantiate_gids(all_cell_ids, add_stimuli=False, add_synapses=False)\n", |
There was a problem hiding this comment.
why setting add_stimuli to false?
There was a problem hiding this comment.
To check changes in baseline potentials.
Summary
Implement SONATA condition modifications in BlueCelluLab, supporting all five modification types defined in the SONATA-extension specification.
Changes
New: SONATA Modifications (bluecellulab/simulation/modifications.py)
ttxTTXDynamicsSwitchconfigure_all_sectionssection_listsectiondend[0])compartment_setModifications are:
conditions.modificationsof the SONATA simulation configcreation, but before synapses/stimuli (matching neurodamus ordering)
Graceful Skipping & Logging
Modifications skip sections/cells that don't match rather than failing:
section_configuresyntax raisesValueErrorNew: Generic Mechanism Handling
MechanismConditionsfields (minis_single_vesicle,init_depleted) with a genericmechanisms: dictin theConditionsdataclass.set_global_condition_parameters()now iterates the dict and sets NEURON globals dynamically, supporting any mechanism parameter without code changes.This matches neurodamus behavior and is essential for heterogeneous node sets.
Files Changed
bluecellulab/circuit/config/sections.py— Modification dataclasses, generic mechanisms dictbluecellulab/circuit/config/definition.py— get_modifications() in SimulationConfig protocolbluecellulab/simulation/neuron_globals.py— Generic mechanism globals applicationlibsonata>=0.1.34Example
New notebook examples/2-sonata-network/sonata-modifications.ipynb demonstrates all modification types.
Bug Fix: Empty Reports Dict
get_report_entries() now handles
None(returned by libsonata/bluepysnap for"reports": {}) gracefully instead of raisingValueError.Dependencies
libsonata>=0.1.34(adds snake_case modification type enums)