Summary
Two related issues occur when creating and restoring Brian2 networks in a for loop:
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.
- 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:
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
Summary
Two related issues occur when creating and restoring Brian2 networks in a
forloop:KeyErroron restore:Network.restore()fails because auto-generated clock names increment across iterations despite callingstart_scope(), causing a mismatch with the names stored in the file.Networkviacollect()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:
KeyErroronNetwork.restore()Minimal reproducing scenario
The store files are pre-generated once:
Then, restoring them in a loop triggers the error:
Observed behavior
G_run_regularly_clock.G_run_regularly_clock_1due 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: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
Clockinstances created internally byrun_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:
When
Network.restore()looks up the state dictionary using the current (incremented) clock name, the key does not exist, causing theKeyError.The relevant code path is in
brian2/core/network.py: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:
Observed behavior
On each iteration (except the last), the following warning is emitted:
This is incorrect —
G2was included in the network viaNetwork(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, theNeuronGroupfrom 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 becausestart_scope()clears the globalBrianObjectregistry 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:
This works but is cumbersome and adds significant overhead.
Environment
Brian2 version: 2.9.0
Python version: 3.10
OS: Windows 10