Namespace Issues with PoissonGroup

Problem

I’m encountering a persistent KeyError when trying to use a state variable from one NeuronGroup to dynamically modulate the rates of a PoissonGroup. I’ve tried providing the necessary objects in the namespace both at object creation and at runtime, but the error persists, and I’m struggling to understand what I’m missing.
I want to avoid using a TimedArray, since my simulations are long and if I pre-allocate a TimedArray I run out of memory. Any help would be appreciated!

Minimal code to reproduce the problem

#!/usr/bin/env python
from brian2 import *
import matplotlib.pyplot as plt

# 1. --- Parameters ---
duration = 10*second
track_length = 100*cm
speed = 20*cm/second
N_inputs = 50

# 2. --- Define the Brian2 Objects ---

# This group provides the dynamic variables
animal_eqs = '''
pos = (speed * t) % track_length : meter (constant over dt)
is_on : 1 (constant)
'''
animal = NeuronGroup(1, animal_eqs, name='animal')

# The rates string links to 'pos' and 'is_on'
rates_eq = '''
is_on * (200*Hz * (1 + sin(2*pi*i/N_inputs + 2*pi*pos/track_length))/2) + 2*Hz
'''

# Create a dedicated namespace that points to the other objects
input_namespace = {
    'animal': animal,
    'N_inputs': N_inputs
}

# Provide this namespace during the object's creation
inputs = PoissonGroup(N_inputs, rates=rates_eq,
                      namespace=input_namespace, name='inputs')

# 3. --- Set up Monitors and Network ---
spike_mon = SpikeMonitor(inputs)
pos_mon = StateMonitor(animal, 'pos', record=True)
net = Network(collect())


# 4. --- Run the Simulation in Phases ---
print("Running Phase 1: Spatial input is OFF...")
animal.is_on = 0.
net.run(duration/2, namespace=locals())

print("Running Phase 2: Spatial input is ON...")
animal.is_on = 1.
net.run(duration/2, namespace=locals())


# 5. --- Plot the Results ---
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 7), sharex=True,
                               gridspec_kw={'height_ratios': [3, 1]})

ax1.plot(spike_mon.t/second, spike_mon.i, '.k', ms=3)
ax1.axvline(5.0, ls='--', color='red', lw=2, label='is_on = 1')
ax1.set_title("Input Neuron Firing (Gated by 'is_on')")
ax1.set_ylabel("Neuron Index")
ax1.legend()
ax1.grid(axis='y', alpha=0.2)

ax2.plot(pos_mon.t/second, pos_mon.pos[0]/cm)
ax2.set_title("Animal Position on Track")
ax2.set_xlabel("Time (s)")
ax2.set_ylabel("Position (cm)")

plt.tight_layout()
plt.show()

Full traceback of error

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/core/network.py:1003, in Network.before_run(self, run_namespace)
   1002 try:
-> 1003     obj.before_run(run_namespace)
   1004 except Exception as ex:

File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/input/poissongroup.py:132, in PoissonGroup.before_run(self, run_namespace)
    131 identifiers = get_identifiers(expr)
--> 132 variables = self.resolve_all(
    133     identifiers, run_namespace, user_identifiers=identifiers
    134 )
    135 unit = parse_expression_dimensions(rates_var.expr, variables)

File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/groups/group.py:801, in Group.resolve_all(self, identifiers, run_namespace, user_identifiers, additional_variables)
    800 for identifier in identifiers:
--> 801     resolved[identifier] = self._resolve(
    802         identifier,
    803         user_identifier=identifier in user_identifiers,
    804         additional_variables=additional_variables,
    805         run_namespace=run_namespace,
    806     )
    807 return resolved

File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/groups/group.py:756, in Group._resolve(self, identifier, run_namespace, user_identifier, additional_variables)
    754 # We did not find the name internally, try to resolve it in the external
    755 # namespace
--> 756 return self._resolve_external(identifier, run_namespace=run_namespace)

File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/groups/group.py:890, in Group._resolve_external(self, identifier, run_namespace, user_identifier, internal_variable)
    889             error_msg = f'The identifier "{identifier}" could not be resolved.'
--> 890         raise KeyError(error_msg)
    892 elif len(matches) > 1:
    893     # Possibly, all matches refer to the same object

KeyError: 'The identifier "pos" could not be resolved.'

The above exception was the direct cause of the following exception:

BrianObjectException                      Traceback (most recent call last)
Cell In[2], line 44
     42 print("Running Phase 1: Spatial input is OFF...")
     43 animal.is_on = 0.
---> 44 net.run(duration/2, namespace=locals())
     46 print("Running Phase 2: Spatial input is ON...")
     47 animal.is_on = 1.

File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/core/base.py:346, in device_override.<locals>.device_override_decorator.<locals>.device_override_decorated_function(*args, **kwds)
    344     return getattr(curdev, name)(*args, **kwds)
    345 else:
--> 346     return func(*args, **kwds)

File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/units/fundamentalunits.py:2652, in check_units.<locals>.do_check_units.<locals>.new_f(*args, **kwds)
   2642             error_message = (
   2643                 f"Function '{f.__name__}' "
   2644                 "expected a quantity with unit "
   2645                 f"{unit} for argument '{k}' but got "
   2646                 f"'{value}'"
   2647             )
   2648             raise DimensionMismatchError(
   2649                 error_message, get_dimensions(newkeyset[k])
   2650             )
-> 2652 result = f(*args, **kwds)
   2653 if "result" in au:
   2654     if isinstance(au["result"], Callable) and au["result"] not in (
   2655         bool,
   2656         np.bool_,
   2657     ):

File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/core/network.py:1141, in Network.run(self, duration, report, report_period, namespace, profile, level)
   1138 if namespace is None:
   1139     namespace = get_local_namespace(level=level + 3)
-> 1141 self.before_run(namespace)
   1143 if len(all_objects) == 0:
   1144     return  # TODO: raise an error? warning?

File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/core/base.py:346, in device_override.<locals>.device_override_decorator.<locals>.device_override_decorated_function(*args, **kwds)
    344     return getattr(curdev, name)(*args, **kwds)
    345 else:
--> 346     return func(*args, **kwds)

File ~/miniconda3/envs/brian/lib/python3.13/site-packages/brian2/core/network.py:1005, in Network.before_run(self, run_namespace)
   1003             obj.before_run(run_namespace)
   1004         except Exception as ex:
-> 1005             raise BrianObjectException(
   1006                 "An error occurred when preparing an object.", obj
   1007             ) from ex
   1009 # Check that no object has been run as part of another network before
   1010 for obj in all_objects:

BrianObjectException: Error encountered with object named 'inputs'.
Object was created here (most recent call only, full details in debug log):
  File '<ipython-input-2-c4adc19fee79>', line 32, in <module>
    inputs = PoissonGroup(N_inputs, rates=rates_eq,

An error occurred when preparing an object. (See above for original error message and traceback.)

Environment info:

  • Python 3.13.2

  • Brian2 2.8.0

  • Linux OS

Hi @flmuk ,

What seems to be the problem is how Brian2 resolves external variables for a PoissonGroup. When you provide the rates as a string expression, Brian2 needs to know exactly where to find every variable in that string (like pos and is_on) at the moment it prepares the simulation.

Your initial approach of providing a namespace {'animal': animal} was on the right track, but Brian2 doesn’t automatically look inside the animal object to find pos. It’s looking for a top-level variable named pos in the namespace and can’t find it, which causes the KeyError.

The correct way to handle this is to make the link explicit by creating a namespace that maps the variable names directly to the specific variable objects within the animal group. This gives the PoissonGroup a direct and efficient pointer to the values it needs, without you having to use a memory-intensive TimedArray.

Here is the corrected and working version of your code:

#!/usr/bin/env python
from brian2 import *
import matplotlib.pyplot as plt

# 1. --- Parameters ---
duration = 10*second
track_length = 100*cm
speed = 20*cm/second
N_inputs = 50

# 2. --- Define the Brian2 Objects ---

# This group provides the dynamic variables
animal_eqs = '''
pos = (speed * t) % track_length : meter (constant over dt)
is_on : 1 (constant)
'''
animal = NeuronGroup(1, animal_eqs, name='animal')

# The rates string remains the same
rates_eq = '''
is_on * (200*Hz * (1 + sin(2*pi*i/N_inputs + 2*pi*pos/track_length))/2) + 2*Hz
'''

# **THE FIX: Create a namespace that maps names directly to the variable objects**
input_namespace = {
    'pos': animal.variables['pos'],
    'is_on': animal.variables['is_on'],
    'N_inputs': N_inputs,
    'track_length': track_length
}

# Provide this correctly structured namespace during the object's creation
inputs = PoissonGroup(N_inputs, rates=rates_eq,
                      namespace=input_namespace, name='inputs')

# 3. --- Set up Monitors and Network ---
spike_mon = SpikeMonitor(inputs)
pos_mon = StateMonitor(animal, 'pos', record=True)
net = Network(collect())


# 4. --- Run the Simulation in Phases ---
print("Running Phase 1: Spatial input is OFF...")
animal.is_on = 0.
net.run(duration/2) # No runtime namespace is needed now

print("Running Phase 2: Spatial input is ON...")
animal.is_on = 1.
net.run(duration/2) # No runtime namespace is needed now


# 5. --- Plot the Results ---
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 7), sharex=True,
                               gridspec_kw={'height_ratios': [3, 1]})

ax1.plot(spike_mon.t/second, spike_mon.i, '.k', ms=3)
ax1.axvline(5.0, ls='--', color='red', lw=2, label='is_on = 1')
ax1.set_title("Input Neuron Firing (Gated by 'is_on')")
ax1.set_ylabel("Neuron Index")
ax1.legend()
ax1.grid(axis='y', alpha=0.2)

ax2.plot(pos_mon.t/second, pos_mon.pos[0]/cm)
ax2.set_title("Animal Position on Track")
ax2.set_xlabel("Time (s)")
ax2.set_ylabel("Position (cm)")

plt.tight_layout()
plt.show()

By explicitly mapping 'pos': animal.variables['pos'], you’re telling the PoissonGroup exactly where to look for the pos variable, which resolves the error at the compilation stage.

Hope this helps :grin:

Hi everyone (just came back from holiday and only now going through old posts…). The way Brian objects interact with variables not defined as part of their own equations is sometimes non-trivial, and I think we could do a better job explaining it. The namespace can only be used for two specific things: references to constant values and to user-defined functions. In both cases, specifying things via namespace is not necessary if the variable/function is “visible” from where the run function is called. So in principle, you could use your is_on variable defined outside any group as it is:

# 4. --- Run the Simulation in Phases ---
print("Running Phase 1: Spatial input is OFF...")
is_on = 0.
net.run(duration/2)

print("Running Phase 2: Spatial input is ON...")
is_on = 1.
net.run(duration/2)

This would work without any namespace definition, because the default namespace is picking up variables from the point where net.run is called (it is only necessary to use it when you e.g. structure your code with functions that don’t have direct access to the value, or if the values are stored in a dictionary or similar cases). The problem is that this would work for is_on, since it is a constant, but it does not work for pos which is a function of time. You are correct that this has to be declared within a group, but you could also declare it as part of the PoissonGroup rate itself:

rates_eq = '''
is_on * (200*Hz * (1 + sin(2*pi*i/N_inputs + 2*pi*((speed * t) % track_length)/track_length))/2) + 2*Hz
'''

(I replaced pos by its definition). This should work, but is not as convenient as what you had in mind – for example, you cannot reuse the pos variable anywhere else, or record it with a monitor. The best solution for your problem is therefore probably to not use PoissonGroup at all, but rather write an equivalent NeuronGroup (as we mention in passing in the documentation: Input stimuli — Brian 2 2.9.0.post70 documentation) .

# This group provides both the dynamic variables and the spikes
animal_rate_eqs = '''
pos = (speed * t) % track_length : meter (constant over dt)
is_on : 1 (constant)
rates = is_on * (200*Hz * (1 + sin(2*pi*i/N_inputs + 2*pi*pos/track_length))/2) + 2*Hz : Hz
'''
animal_input = NeuronGroup(N_inputs, animal_eqs,
                           threshold="rand()<rates*dt", reset="",
                           name='animal_input')
# ...
print("Running Phase 1: Spatial input is OFF...")
animal_input.is_on = 0.
net.run(duration/2)
# ...

Now, this is slightly more inefficient compared to your solution, since it calculates the position for each of the inputs, instead of once. I don’t think this will make a noticeable difference, but if you prefer to keep the animal and input groups separate, you can do this as well, replacing the PoissonGroup by a NeuronGroup as described earlier and then use “linked variables” to connect the two groups.

Hopefully that gives you an idea of how to proceed!

1 Like

Yes, thank you very much!

1 Like