Skip to content

poc: add BrianExporter and BrianImporter skeleton#80

Draft
Sanchit2662 wants to merge 1 commit intobrian-team:masterfrom
Sanchit2662:poc/brian-exporter-importer
Draft

poc: add BrianExporter and BrianImporter skeleton#80
Sanchit2662 wants to merge 1 commit intobrian-team:masterfrom
Sanchit2662:poc/brian-exporter-importer

Conversation

@Sanchit2662
Copy link
Copy Markdown

@Sanchit2662 Sanchit2662 commented Mar 29, 2026

What this is

I'm applying for GSoC 2026 to work on serialization/deserialization for Brian2 models, results, and input data. This is a proof-of-concept showing I understand the existing brian2tools codebase and have a concrete approach to the problem.

This is the brian2tools half of the PoC. The other half lives in the brian2 repo and demonstrates portable state checkpointing using _full_state()/_restore_from_full_state(): brian-team/brian2#1799


What I built

brian2tools/baseexport/exporter.py
BrianExporter.serialize(net, 'file.brian')

I built this by wrapping the existing collect_*() functions from collector.py using the same COLLECTOR_MAP pattern I found in BaseExporter.network_run() (device.py:151). While reading through the code I identified three gaps that BaseExporter deliberately omits because it operates before the simulation runs:

  • _json_safe() : converts Quantity objects in collector output to JSON-safe dicts. The concrete problem: collect_Equations() (collector.py:212) stores eqs.unit as a raw Quantity, which breaks json.dumps(). I serialize it as {'__type__': 'quantity', 'value': ..., 'dim': [7-element SI exponent tuple]} using Dimension._dims directly.

  • _collect_state() : captures actual state variable values via var.get_value() after the run. Since BaseExporter intercepts code generation before the simulation, it never sees the values.

  • _collect_connectivity() : captures _synaptic_pre / _synaptic_post arrays. I noticed that collect_Synapses() (collector.py:565) only stores the connect() arguments condition string, p, n — not the resulting index arrays. For probabilistic connections (p=0.1), re-running that condition on load gives different synapses every time. Storing the arrays fixes this.

brian2tools/baseimport/importer.py
BrianImporter.load('file.brian') → (net, namespace)

I mapped each field in the collect_*() output back to the corresponding Brian2 constructor. For example, _reconstruct_neurongroup() pulls threshold/reset/refractory from ng_dict['events']['spike'] (the structure from collect_Events() at collector.py:225), and _reconstruct_synapses() resolves source/target using collect_SpikeSource()'s Subgroup dict form. Connectivity is restored via syn.connect(i=arr, j=arr) — not by re-running the original condition. State variables are restored after connect() so DynamicArrayVariable sizes are correct first.

11 tests in tests/test_poc_exporter.py covering archive structure, Quantity serialization, state capture, connectivity arrays, and full NeuronGroup/Synapses round-trips.


What this isn't

  • Not a full implementation monitors, PoissonGroup, SpikeGeneratorGroup, SpatialNeuron reconstruction are planned but out of scope here
  • Not a device-mode integration, I call it explicitly after net.run(), not via set_device()
  • Not production-ready

Questions for reviewers

  1. I'm using var.owner.name != obj.name to filter out variables owned by other objects (clocks etc.) — is there a more idiomatic way to do this check?

  2. collect_Synapses() at collector.py:629 drops non-scalar delays. Should I capture them the same way I capture _synaptic_pre / _synaptic_post, or is there a better hook?

  3. For Synapses reconstruction I use str(obj.equations) as the model string. Is there a more reliable canonical form I should use instead?

Adds a minimal working proof-of-concept for network serialization to
a portable .brian ZIP archive (model.json + arrays.npz).

BrianExporter.serialize() wraps the existing collect_*() functions from
baseexport/collector.py and fills three gaps that BaseExporter omits:
- converts Quantity objects in collector output to JSON-safe dicts
- captures state variable values after net.run() via var.get_value()
- captures _synaptic_pre/_synaptic_post arrays for exact connectivity
  restoration (critical for probabilistic connections)

BrianImporter.load() reconstructs NeuronGroups and Synapses from the
archive, restoring connectivity via syn.connect(i=, j=) and state
variables via var.set_value() after connect().

11 tests in tests/test_poc_exporter.py cover archive structure,
Quantity serialization, state capture, connectivity arrays, and
NeuronGroup/Synapses round-trips.

Signed-off-by: Sanchit2662 <sanchit2662@gmail.com>
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.

1 participant