Synaptic Weight Conversion From Brian1 to Brian2

Description of problem

I am trying to reproduce the model from Klaus Wimmer’s 2015 paper (Sensory integration dynamics in a hierarchical network explains choice probabilities in cortical area MT) in Brian2, and all of the Brian1 code is publicly available on ModelDB here: ModelDB: Hierarchical network model of perceptual decision making (Wimmer et al 2015)
When trying to convert the synapse weights from Brian1 to Brian2, I end up with the following error: ‘The identifier “max” could not be resolved.’

Minimal code to reproduce problem

Wimmer’s Brian1 code is:

C_SE_SE = Connection(sensoryE, sensoryE, 'xe', delay=True, max_delay=1.5 * ms)
C_SE_SE.connect_random(sensoryE1, sensoryE1, sparseness=p, weight=lambda:w_p * gEE/gLeakE * max(0.0, 1.0 + 0.5 * randn()), delay=dE) 

My Brian2 translation is

C_SE1_SE1 = Synapses(sensoryE1, sensoryE1, 'w: siemens', on_pre='ye+=w') 
C_SE1_SE1.connect(p=0.2) 
#C_SE1_SE1.w=max(0.0, w_p*gEE/gLeakE*(1.0 + 0.5 * randn()))*nS
C_SE1_SE1.w='w_p*gEE/gLeakE*max(0.0, 1.0 + 0.5 * randn())*nS' # This is the string that throws an error 
C_SE1_SE1.delay='randn()*1.5*ms'

What you have already tried

If C_SE1_SE1.w='w_p*gEE/gLeakE*max(0.0, 1.0 + 0.5 * randn())*nS' isn’t a string, then the code runs fine but all of the synaptic weights have the same value. However, I’m pretty sure the line above is a function.

Full traceback of error (if relevant)

Traceback (most recent call last)
Cell In[7], line 5
      3 C_SE1_SE1.connect(p=0.2) #I think you need to separate these guys into different groups as the weights are different. 
      4 #C_SE1_SE1.w=max(0.0, w_p*gEE/gLeakE*(1.0 + 0.5 * randn()))*nS
----> 5 C_SE1_SE1.w='w_p*gEE/gLeakE*max(0.0, 1.0 + 0.5 * randn())*nS'
      6 C_SE1_SE1.delay='randn()*1.5*ms'
      7 #This works without putting anything into randn, but it doesn't when you turn it into a string? 

File ~/Library/Python/3.11/lib/python/site-packages/brian2/groups/group.py:420, in VariableOwner.__setattr__(self, name, val, level)
    418         raise TypeError(f'Variable {name} is read-only.')
    419     # Make the call X.var = ... equivalent to X.var[:] = ...
--> 420     var.get_addressable_value_with_unit(name, self).set_item(slice(None),
    421                                                              val,
    422                                                              level=level+1)
    423 elif len(name) and name[-1]=='_' and name[:-1] in self.variables:
    424     # no unit checking
    425     var = self.variables[name[:-1]]

File ~/Library/Python/3.11/lib/python/site-packages/brian2/core/variables.py:873, in VariableView.set_item(self, item, value, level, namespace)
    870 # Both index and values are strings, use a single code object do deal
    871 # with this situation
    872 if isinstance(value, str) and isinstance(item, str):
--> 873     self.set_with_expression_conditional(item, value,
    874                                          check_units=check_units,
    875                                          run_namespace=namespace)
    876 elif isinstance(item, str):
    877     try:

File ~/Library/Python/3.11/lib/python/site-packages/brian2/core/base.py:293, in device_override.<locals>.device_override_decorator.<locals>.device_override_decorated_function(*args, **kwds)
    291     return getattr(curdev, name)(*args, **kwds)
    292 else:
--> 293     return func(*args, **kwds)

File ~/Library/Python/3.11/lib/python/site-packages/brian2/core/variables.py:1027, in VariableView.set_with_expression_conditional(self, cond, code, run_namespace, check_units)
   1025 from brian2.devices.device import get_device
   1026 device = get_device()
-> 1027 codeobj = create_runner_codeobj(self.group,
   1028                                 {'condition': abstract_code_cond,
   1029                                  'statement': abstract_code},
   1030                                 'group_variable_set_conditional',
   1031                                 additional_variables=variables,
   1032                                 check_units=check_units,
   1033                                 run_namespace=run_namespace,
   1034                                 codeobj_class=device.code_object_class(fallback_pref='codegen.string_expression_target'))
   1035 codeobj()

File ~/Library/Python/3.11/lib/python/site-packages/brian2/codegen/codeobject.py:352, in create_runner_codeobj(group, code, template_name, run_namespace, user_code, variable_indices, name, check_units, needed_variables, additional_variables, template_kwds, override_conditional_write, codeobj_class)
    349     needed_variables = []
    350 # Resolve all variables (variables used in the code and variables needed by
    351 # the template)
--> 352 variables = group.resolve_all(sorted(identifiers | set(needed_variables) | set(template_variables)),
    353                               # template variables are not known to the user:
    354                               user_identifiers=user_identifiers,
    355                               additional_variables=additional_variables,
    356                               run_namespace=run_namespace)
    357 # We raise this error only now, because there is some non-obvious code path
    358 # where Jinja tries to get a Synapse's "name" attribute via syn['name'],
    359 # which then triggers the use of the `group_get_indices` template which does
    360 # not exist for standalone. Putting the check for template == None here
    361 # means we will first raise an error about the unknown identifier which will
    362 # then make Jinja try syn.name
    363 if template is None:

File ~/Library/Python/3.11/lib/python/site-packages/brian2/groups/group.py:731, in Group.resolve_all(self, identifiers, run_namespace, user_identifiers, additional_variables)
    729 resolved = {}
    730 for identifier in identifiers:
--> 731     resolved[identifier] = self._resolve(identifier,
    732                                          user_identifier=identifier in user_identifiers,
    733                                          additional_variables=additional_variables,
    734                                          run_namespace=run_namespace)
    735 return resolved

File ~/Library/Python/3.11/lib/python/site-packages/brian2/groups/group.py:691, in Group._resolve(self, identifier, run_namespace, user_identifier, additional_variables)
    687     return resolved_internal
    689 # We did not find the name internally, try to resolve it in the external
    690 # namespace
--> 691 return self._resolve_external(identifier, run_namespace=run_namespace)

File ~/Library/Python/3.11/lib/python/site-packages/brian2/groups/group.py:814, in Group._resolve_external(self, identifier, run_namespace, user_identifier, internal_variable)
    812         else:
    813             error_msg = f'The identifier "{identifier}" could not be resolved.'
--> 814         raise KeyError(error_msg)
    816 elif len(matches) > 1:
    817     # Possibly, all matches refer to the same object
    818     first_obj = matches[0][1]

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

Hi @luol3. Your basic approach is correct: in Brian 1, you’d specify synaptic weights that are calculated for each synapse via a function (or a lambda expression, which is the same thing), whereas in Brian 2, you’d use a string expression (Synapses (Brian 1 –> 2 conversion) — Brian 2 2.5.1 documentation).

These two often look very similar. One difference though is that in the Brian 1 method you can refer to arbitrary Python functions, while in Brian 2 you are restricted to the functions provided here: Functions — Brian 2 2.5.1 documentation (this is because Brian 2 will not necessarily execute this code in Python, but it might be C++, etc.). As you can see, this list does not include the max function. Luckily, the max function in this code is actually used to do clipping (the weights should not become negative), and Brian 2 comes with a dedicated function for that purpose: clip. Therefore, the following string should work:

C_SE1_SE1.w='w_p*gEE/gLeakE*clip(1.0 + 0.5 * randn(), 0.0, inf)*nS'

A quick explanation why it seemingly works if you don’t put things into the string, but leads to a single value for all synapses: the expression provided in the lambda (Brian 1) or in the string (Brian 2), gets evaluated for each of the synapses individually, therefore rand() gives a different random number each time. When you instead call the same expression directly from Python, as you did in your commented line, randn() will return a single number. This means that Brian will receive something like C_SE1_SE1.w = 1.294, i.e. a single value for all the weights.

Hope that helps!
Marcel

1 Like

Thank you for your help, @mstimberg. That line of code worked.

Later in Wimmer’s code (ModelDB: Hierarchical network model of perceptual decision making (Wimmer et al 2015)), there is a line that displays a connection (C_DE_DI_AMPA = Connection(decisionE, decisionI, ‘gea’, weight = gEI_AMPA / gLeakI, delay = d)) that seems to have no connectivity properties and I’m not sure how to convert that into Brian2.

My Brian 1 knowledge is a bit rusty by now, but I think if you defined a weight directly in the Connection constructor, this meant that it should connect things with all-to-all connectivity. In Brian 2, this means you’ll have to call C_DE_DI_AMPA.connect().

Just to be sure, I looked it up and I was correct :wink: : Connections — Brian 1.4.4 documentation

1 Like