Skip to content

Bug Report: Network.restore() fails with KeyError when multiple models are created and restored in a loop; spurious "unused object" warnings #1814

Description

@Jasmine969

Summary

Two related issues occur when creating and restoring Brian2 networks in a for loop:

  1. KeyError on restore: Network.restore() fails because auto-generated clock names increment across iterations despite calling start_scope(), causing a mismatch with the names stored in the file.
  2. Spurious "unused object" warning: Objects that were added to a Network via collect() still trigger the warning "The object 'G' is getting deleted, but was never included in a network" when they are garbage-collected between iterations.

Each case works perfectly when run independently in a separate Python process.

Issue 1: KeyError on Network.restore()

Minimal reproducing scenario

The store files are pre-generated once:

from brian2 import *


def store_net(store_path):
    start_scope()

    G = NeuronGroup(10, 'dv/dt = -v / (10*ms) : 1', name='G', method='euler')
    G.run_regularly('v += 0.1', dt=1 * ms)  # This creates an auto-named clock

    net = Network(collect())
    net.run(1 * second)
    net.store(filename=store_path)


# Pre-generate store files
for i in range(5):
    store_net(f'./test_store_{i}.dat')

Then, restoring them in a loop triggers the error:

from brian2 import *


def restore_net(store_path):
    start_scope()
    G2 = NeuronGroup(10, 'dv/dt = -v / (10*ms) : 1', name='G', method='euler')
    G2.run_regularly('v += 0.1', dt=1 * ms)

    net2 = Network(collect())
    net2.restore(filename=store_path)  # Fails on later iterations


# Run in a loop
for i in range(5):
    print(f'--- Iteration {i} ---')
    restore_net(f'./test_store_{i}.dat')

Observed behavior

  • Iteration 0: Succeeds. The auto-generated clock is named G_run_regularly_clock.
  • Iteration 1: The clock is now auto-named G_run_regularly_clock_1 due to the global name registry retaining previous entries despite start_scope(). This no longer matches the name stored in the file (G_run_regularly_clock), causing:
File "brian2/core/network.py", line 716, in restore
    clock._restore_from_full_state(state[clock.name])
KeyError: 'G_run_regularly_clock_1'

Expected behavior

start_scope() should fully reset the internal name registry so that auto-generated names are identical across iterations. Consequently, Network.restore() should succeed because the clock names in the newly built network match those in the stored file.

Root cause analysis

Brian2 auto-generates unique names for objects (including Clock instances created internally by run_regularly()). The uniqueness is ensured by appending a suffix (_1, _2, ...) via a global instance counter. However, start_scope() does not fully reset these counters.

As a result:

Image

When Network.restore() looks up the state dictionary using the current (incremented) clock name, the key does not exist, causing the KeyError.

The relevant code path is in brian2/core/network.py:

        for clock in clocks:
            clock._restore_from_full_state(state[clock.name])

Issue 2: Spurious "unused object" warning

Minimal reproducing scenario

Even when run_regularly() is removed (so no KeyError occurs), a spurious warning appears on every iteration after the first:

from brian2 import *


def restore_batch(store_path):
    start_scope()
    G2 = NeuronGroup(10, 'dv/dt = -v / (10*ms) : 1', name='G', method='euler')

    net2 = Network(collect())
    net2.restore(filename=store_path)


for i in range(5):
    print(f'--- Iteration {i} ---')
    restore_batch(f'./test_store_{i}.dat')

Observed behavior

On each iteration (except the last), the following warning is emitted:

WARNING    The object 'G' is getting deleted, but was never included in a network.

This is incorrect — G2 was included in the network via Network(collect()).

Expected behavior

No warning should be emitted, since the object was properly added to a Network.

Root cause analysis

When start_scope() is called at the beginning of the next iteration, or when the previous iteration's local variables go out of scope, the NeuronGroup from the previous iteration gets garbage-collected. During finalization, Brian2 checks whether the object was ever added to a network. However, this check appears to fail — likely because start_scope() clears the global BrianObject registry before the garbage collector runs, causing Brian2 to lose track of the object's network membership.

Workaround (for both issues)

Run each case in an isolated subprocess using multiprocessing:

import multiprocessing as mp

def worker(store_path, result_queue):
    from brian2 import start_scope
    start_scope()
    # rebuild network and restore...
    result_queue.put(some_result)

for i in range(5):
    q = mp.Queue()
    p = mp.Process(target=worker, args=(f'./test_store_{i}.dat', q))
    p.start()
    p.join()
    result = q.get()

This works but is cumbersome and adds significant overhead.

Environment

Brian2 version: 2.9.0
Python version: 3.10
OS: Windows 10

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions