Skip to content

Implement SONATA condition modifications#63

Open
darshanmandge wants to merge 9 commits intomainfrom
condtions_modifications
Open

Implement SONATA condition modifications#63
darshanmandge wants to merge 9 commits intomainfrom
condtions_modifications

Conversation

@darshanmandge
Copy link
Collaborator

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)

Type Description
ttx Block Na channels via TTXDynamicsSwitch
configure_all_sections Apply a statement to all sections of target cells
section_list Apply to a named section list (somatic, basal, apical, axonal)
section Apply to specific named sections (e.g. dend[0])
compartment_set Apply to segments defined by a compartment set file

Modifications are:

Graceful Skipping & Logging

Modifications skip sections/cells that don't match rather than failing:

  • Sections missing a referenced attribute are silently skipped
  • Zero-match cases log a WARNING
  • Invalid section_configure syntax raises ValueError

New: Generic Mechanism Handling

  • Replaced the hardcoded MechanismConditions fields (minis_single_vesicle, init_depleted) with a generic mechanisms: dict in the Conditions dataclass.
  • 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

Example

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 raising ValueError.

Dependencies

  • Requires libsonata>=0.1.34 (adds snake_case modification type enums)

- 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
@darshanmandge darshanmandge self-assigned this Feb 16, 2026
@codecov
Copy link

codecov bot commented Feb 16, 2026

Codecov Report

❌ Patch coverage is 97.78052% with 18 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
bluecellulab/circuit_simulation.py 73.77% 16 Missing ⚠️
bluecellulab/circuit/config/sections.py 97.77% 1 Missing ⚠️
...ellulab/circuit/config/sonata_simulation_config.py 94.44% 1 Missing ⚠️
Files with missing lines Coverage Δ
bluecellulab/circuit/config/definition.py 100.00% <100.00%> (ø)
bluecellulab/simulation/modifications.py 100.00% <100.00%> (ø)
bluecellulab/simulation/neuron_globals.py 96.29% <100.00%> (+0.46%) ⬆️
tests/test_circuit/test_simulation_config.py 100.00% <100.00%> (ø)
tests/test_simulation/test_modifications.py 100.00% <100.00%> (ø)
tests/test_simulation/test_neuron_globals.py 100.00% <100.00%> (ø)
bluecellulab/circuit/config/sections.py 93.07% <97.77%> (+3.42%) ⬆️
...ellulab/circuit/config/sonata_simulation_config.py 97.40% <94.44%> (-0.46%) ⬇️
bluecellulab/circuit_simulation.py 85.20% <73.77%> (-0.19%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

raise BluecellulabError(
"instantiate_gids() is called twice on the "
"same CircuitSimumation, this is not supported")
"same CircuitSimumation, this is not supported"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: CircuitSimulation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Fixed in the latest commit.

"""Base class for all modification types."""

name: str
type: str
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth normalizing to lowercase before matching?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed cell_applied to sections_applied in all functions.


# Extract list name from section_configure: e.g. "apical.gbar = 0" -> "apical"
# The format is "<list_name>.attr = value [; <list_name>.attr = value ...]"
match = re.match(r"^(\w+)\.", mod.section_configure)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the SONATA spec it looks like a section_list entry should reference a single list. Since this only inspects the first prefix, could a snippet that accidentally mixes lists (e.g. apical, basal) pass here and only fail later during exec? Would it be worth checking all statements use the same prefix and raising a clearer config error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could a snippet that accidentally mixes lists (e.g. apical, basal)

Just to understand what you meant: you meant checking for entries such as apical.basal.gbar_NaTg =2 or apicalbasal.gbar_NaTg =2 or something else?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant semicolon separated statements using different lists, e.g. apical.gbar_NaTg = 0.0; basal.cm = 1. Right now we only rewrite the first prefix so the basal. part would remain and likely fail during exec. Maybe we should either support mixed prefixes or detect them and raise a clearer error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing that out. The SONATA documentation notes that the section_lists modification has the same section_list per modification entry in section_configure. So, this "apical.gbar_NaTg = 0.0; apical.cm = 1" is acceptable. The code will now raise an error for mixed prefixes for your example, apical.gbar_NaTg = 0.0; basal.cm = 1 as intended.

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be misunderstanding, but it looks like we replace

. with sec. in the full string rather than isolating the statements for that section. If we references multiple sections (e.g. apic[10], dend[3]), would we still execute the other statements when applying to one section?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much clearer, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants