Skip to content

Refactor GID handling and synapse seeding to align with Neurodamus#73

Open
ilkilic wants to merge 12 commits intomainfrom
neurodamus-gid-alignment
Open

Refactor GID handling and synapse seeding to align with Neurodamus#73
ilkilic wants to merge 12 commits intomainfrom
neurodamus-gid-alignment

Conversation

@ilkilic
Copy link
Copy Markdown
Collaborator

@ilkilic ilkilic commented Mar 20, 2026

Refactored GID handling and synapse seeding to make simulations deterministic and consistent with Neurodamus. Global GIDs are now computed from SONATA node populations (1-based with 1000 block offsets). This introduces a breaking change: post_gid is now required when creating synapses.

@ilkilic ilkilic self-assigned this Mar 24, 2026
@ilkilic ilkilic requested a review from darshanmandge March 24, 2026 16:08
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 89.92806% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
bluecellulab/circuit_simulation.py 85.96% 8 Missing ⚠️
...ab/circuit/circuit_access/sonata_circuit_access.py 66.66% 4 Missing ⚠️
bluecellulab/synapse/synapse_types.py 90.00% 1 Missing ⚠️
tests/test_circuit_simulation_mpi.py 97.95% 1 Missing ⚠️
Files with missing lines Coverage Δ
bluecellulab/cell/core.py 78.96% <100.00%> (+78.96%) ⬆️
bluecellulab/circuit/circuit_access/definition.py 94.54% <100.00%> (+94.54%) ⬆️
bluecellulab/circuit/gid_resolver.py 100.00% <100.00%> (ø)
bluecellulab/connection.py 100.00% <100.00%> (+100.00%) ⬆️
bluecellulab/synapse/synapse_factory.py 90.80% <ø> (+90.80%) ⬆️
tests/test_synapse/test_synapse_factory.py 100.00% <100.00%> (+67.21%) ⬆️
bluecellulab/synapse/synapse_types.py 83.02% <90.00%> (+83.02%) ⬆️
tests/test_circuit_simulation_mpi.py 98.48% <97.95%> (+81.15%) ⬆️
...ab/circuit/circuit_access/sonata_circuit_access.py 96.06% <66.66%> (+96.06%) ⬆️
bluecellulab/circuit_simulation.py 84.23% <85.96%> (+84.23%) ⬆️

... and 110 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

if (
user_pre_spike_trains is not None
and pre_gid in user_pre_spike_trains
user_pre_spike_trains is not None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is overindented, no?

Comment on lines +22 to +23
def global_gid(self, pop: str, local_id: int) -> int:
return int(self.pop_offset[pop]) + int(local_id) + 1 # 1-based indexing to mirror Neurodamus synapse seeding implementation
Copy link
Copy Markdown

@WeinaJi WeinaJi Mar 25, 2026

Choose a reason for hiding this comment

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

I understand the intention of +1 here to match the Neurodamus synapse seeding. However, the name global_gid makes me confused that the gid is now 1-based. Neurodamus is using 0-based gids, the +1 is only for synapse seeding.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ah good point, I will move the +1 out of global_gid and apply it only in the seeding.

rng_settings = RNGSettings.get_instance()
if rng_settings.mode == "Random123":
self.randseed1 = self.post_cell_id.id + 250
self.randseed1 = self.post_gid + 250
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Continuing my previous comment about global_gid. If it is only used here, can we use instead self.randseed1 = self.post_gid (0-based) + 251 ?
And what is the different between self.post_cell_id.id and self.post_gid ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe we don't need the new self.post_gid , but derive from self.post_cell_id.id ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have added the +1 here, thank you!

post_cell_id.id is the node id from sonata (local to a population), while post_gid is the globally unique id following the Neurodamus convention.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Then could you just pass the population offset instead of post_gid into this function?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I would prefer to keep the gid computation higher up where the namespace is available and just pass the final value down. We don’t really have the population offset here and the synapse would still need to rebuild the gid. I also refactored a bit to avoid passing post_gid through a long chain and keep it on the cell.

Copy link
Copy Markdown
Collaborator

@AurelienJaquier AurelienJaquier left a comment

Choose a reason for hiding this comment

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

looks good to me

Comment on lines +312 to +313
s = node_pop.size
out[str(pop_name)] = int(s() if callable(s) else s)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is node_pop not always a libsonata NodePopulation ? Why do you need to detect if it is callable ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Right, NodePopulation.size is a cached property. I have droped the check

def _build_gid_namespace(self) -> GidNamespace:
sizes = self.circuit_access.node_population_sizes()
pops_sorted = sorted(sizes.keys())
max_raw = {p: max(0, int(n) - 1) for p, n in sizes.items()}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One more question, sorry.
max_raw here is either 0 or the population size?

But in Neurodamus we use the max gid of the loaded cells
https://github.com/openbraininstitute/neurodamus/blob/e5abd33ad2cc5a4450c2d5190e32afc11ee385d8/neurodamus/core/nodeset.py#L231
For virtual population, it is the whole population size
https://github.com/openbraininstitute/neurodamus/blob/e5abd33ad2cc5a4450c2d5190e32afc11ee385d8/neurodamus/node.py#L130

The offset would be different between BlueCellulab and Neurodamus if they don't load the full population.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Great catch! I have aligned it now so we use the max gid from the loaded cells and the full population size for virtual populations. Let me know if anything still looks off, 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