sango 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sango/__init__.py +4 -0
- sango/backend/__init__.py +3 -0
- sango/backend/brian/__init__.py +0 -0
- sango/backend/brian/brian.py +292 -0
- sango/backend/brian/registry/base.py +26 -0
- sango/backend/brian/registry/prob.py +23 -0
- sango/backend/fugu/__init__.py +0 -0
- sango/backend/fugu/fugu.py +267 -0
- sango/backend/fugu/registry/base.py +15 -0
- sango/backend/fugu/registry/prob.py +10 -0
- sango/backend/stacs/__init__.py +0 -0
- sango/backend/stacs/registry/base.py +22 -0
- sango/backend/stacs/registry/prob.py +12 -0
- sango/backend/stacs/stacs.py +1000 -0
- sango/core.py +456 -0
- sango/model/__init__.py +5 -0
- sango/model/base.py +53 -0
- sango/model/prob.py +9 -0
- sango/network.py +742 -0
- sango-1.0.0.dist-info/METADATA +230 -0
- sango-1.0.0.dist-info/RECORD +24 -0
- sango-1.0.0.dist-info/WHEEL +5 -0
- sango-1.0.0.dist-info/licenses/LICENCE +28 -0
- sango-1.0.0.dist-info/top_level.txt +1 -0
sango/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import brian2
|
|
8
|
+
except ImportError:
|
|
9
|
+
brian2 = None
|
|
10
|
+
if brian2 is not None:
|
|
11
|
+
from brian2 import NeuronGroup, Synapses, SpikeGeneratorGroup, SpikeMonitor
|
|
12
|
+
from brian2 import ms, defaultclock
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from collections import Counter
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
|
|
20
|
+
# Brian Simulation Backend
|
|
21
|
+
class SimBrian:
|
|
22
|
+
def __init__(self, dsl_net):
|
|
23
|
+
if brian2 is None:
|
|
24
|
+
raise ImportError("brian2 package is required for SimBrian")
|
|
25
|
+
|
|
26
|
+
self.dsl_net = dsl_net
|
|
27
|
+
self.tstep = 1.0*ms
|
|
28
|
+
self.timesteps = None
|
|
29
|
+
|
|
30
|
+
self.node_map = None
|
|
31
|
+
self.spike_list = None
|
|
32
|
+
|
|
33
|
+
self.model_registry = self.import_registry()
|
|
34
|
+
|
|
35
|
+
# Dynamically import the model registry files
|
|
36
|
+
def import_registry(self):
|
|
37
|
+
registry = dict()
|
|
38
|
+
registry_dir = Path(__file__).resolve().parent / 'registry'
|
|
39
|
+
sys.path.insert(0, str(registry_dir.parent))
|
|
40
|
+
for file_path in registry_dir.glob("*.py"):
|
|
41
|
+
module_name = f"registry.{file_path.stem}"
|
|
42
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
43
|
+
module = importlib.util.module_from_spec(spec)
|
|
44
|
+
spec.loader.exec_module(module)
|
|
45
|
+
registry.update(module.model_registry)
|
|
46
|
+
sys.path.pop(0)
|
|
47
|
+
return registry
|
|
48
|
+
|
|
49
|
+
# Convert between dsl model to brian2 model
|
|
50
|
+
def rekey_model(self, data):
|
|
51
|
+
for key, value in self.model_registry[data['model']]['state'].items():
|
|
52
|
+
if value['dsl'] is not None:
|
|
53
|
+
data[key] = data.pop(value['dsl'])
|
|
54
|
+
else:
|
|
55
|
+
data[key] = value['default']
|
|
56
|
+
return data
|
|
57
|
+
|
|
58
|
+
def compile(self, debug=False):
|
|
59
|
+
self.debug = debug
|
|
60
|
+
|
|
61
|
+
# Convert network to brian
|
|
62
|
+
start_time = time.perf_counter()
|
|
63
|
+
self.to_brian()
|
|
64
|
+
end_time = time.perf_counter()
|
|
65
|
+
self.compile_time = end_time - start_time
|
|
66
|
+
if self.debug:
|
|
67
|
+
print(f"Compile time: {self.compile_time}")
|
|
68
|
+
|
|
69
|
+
def run(self, timesteps=10.0, verbose=False):
|
|
70
|
+
self.timesteps = timesteps*self.tstep
|
|
71
|
+
self.verbose = verbose
|
|
72
|
+
|
|
73
|
+
# Run network
|
|
74
|
+
start_time = time.perf_counter()
|
|
75
|
+
self.brian_net.run(self.timesteps)
|
|
76
|
+
end_time = time.perf_counter()
|
|
77
|
+
self.run_time = end_time - start_time
|
|
78
|
+
if self.verbose:
|
|
79
|
+
print(f"Run time: {self.run_time}")
|
|
80
|
+
|
|
81
|
+
def to_brian(self):
|
|
82
|
+
# Get a flattened graph object
|
|
83
|
+
self.dsl_graph = self.dsl_net._topology.to_nx()
|
|
84
|
+
defaultclock.dt = 1.0*ms
|
|
85
|
+
|
|
86
|
+
# Convert to Brian2 network objects
|
|
87
|
+
self.num_nodes = self.dsl_graph.number_of_nodes()
|
|
88
|
+
self.node_index = dict()
|
|
89
|
+
self.node_data = [dict() for _ in range(self.num_nodes)]
|
|
90
|
+
self.edge_data = [dict() for _ in range(self.num_nodes)]
|
|
91
|
+
self.group_count = Counter()
|
|
92
|
+
self.local_index = [0 for _ in range(self.num_nodes)]
|
|
93
|
+
self.edge_set = set()
|
|
94
|
+
self.spike_input = []
|
|
95
|
+
|
|
96
|
+
# Add input model (IN) to the beginning of the group
|
|
97
|
+
self.group_count['IN'] = 0
|
|
98
|
+
|
|
99
|
+
# Global node data
|
|
100
|
+
for n, (node, data) in enumerate(self.dsl_graph.nodes(data=True)):
|
|
101
|
+
self.node_index[node] = n
|
|
102
|
+
self.node_data[n] = self.rekey_model(data)
|
|
103
|
+
self.group_count.update([self.node_data[n]['model']])
|
|
104
|
+
self.local_index[n] = self.group_count[self.node_data[n]['model']] - 1
|
|
105
|
+
if self.model_registry[self.node_data[n]['model']]['graph_type'] == 'input':
|
|
106
|
+
self.spike_input.append(data['times'])
|
|
107
|
+
|
|
108
|
+
# Remove the input model (IN) if counts are zero
|
|
109
|
+
if self.group_count['IN'] == 0:
|
|
110
|
+
del self.group_count['IN']
|
|
111
|
+
|
|
112
|
+
# Get the model insertion order of our counter, and organize node map by group
|
|
113
|
+
self.group_index = {key: index for index, key in enumerate(self.group_count.keys())}
|
|
114
|
+
self.group_sorted_count = [self.group_count[group] for group in self.group_index]
|
|
115
|
+
self.group_offset = [0] + [sum(self.group_sorted_count[:i+1]) for i in range(len(self.group_sorted_count))]
|
|
116
|
+
self.node_map = {key: self.group_offset[self.group_index[self.node_data[n]['model']]] + self.local_index[n]
|
|
117
|
+
for key, n in self.node_index.items()}
|
|
118
|
+
|
|
119
|
+
# Global edge data
|
|
120
|
+
for source, target, data in self.dsl_graph.edges(data=True):
|
|
121
|
+
s = self.node_index[source]
|
|
122
|
+
t = self.node_index[target]
|
|
123
|
+
self.edge_data[s][t] = self.rekey_model(data)
|
|
124
|
+
self.edge_set.add((self.edge_data[s][t]['model'], self.node_data[s]['model'], self.node_data[t]['model']))
|
|
125
|
+
|
|
126
|
+
# Spike generator inputs (sorted)
|
|
127
|
+
if not self.spike_input:
|
|
128
|
+
pass
|
|
129
|
+
else:
|
|
130
|
+
spike_index = []
|
|
131
|
+
spike_times = []
|
|
132
|
+
for i, times in enumerate(self.spike_input):
|
|
133
|
+
for t in times:
|
|
134
|
+
spike_index.append(i)
|
|
135
|
+
spike_times.append(t*ms)
|
|
136
|
+
self.spikegen_times, self.spikegen_index = [list(t) for t in zip(*sorted(zip(spike_times, spike_index)))]
|
|
137
|
+
|
|
138
|
+
# Container for local neuron states (by group)
|
|
139
|
+
self.neuron_states = dict()
|
|
140
|
+
for name in self.group_count.keys():
|
|
141
|
+
self.neuron_states[name] = dict()
|
|
142
|
+
for state in self.model_registry[name]['state']:
|
|
143
|
+
self.neuron_states[name][state] = []
|
|
144
|
+
|
|
145
|
+
# Container for local synapse states (by connection)
|
|
146
|
+
self.synapse_connections = {f"{n}_{s}_{t}": {'i': [], 'j': []} for (n,s,t) in self.edge_set}
|
|
147
|
+
self.synapse_states = dict()
|
|
148
|
+
for name, source, target in self.edge_set:
|
|
149
|
+
full_name = f"{name}_{source}_{target}"
|
|
150
|
+
self.synapse_states[full_name] = dict()
|
|
151
|
+
for state in self.model_registry[name]['state']:
|
|
152
|
+
self.synapse_states[full_name][state] = []
|
|
153
|
+
|
|
154
|
+
# Neurons
|
|
155
|
+
for n, data in enumerate(self.node_data):
|
|
156
|
+
name = data['model']
|
|
157
|
+
for state in self.model_registry[name]['state']:
|
|
158
|
+
self.neuron_states[name][state].append(data[state])
|
|
159
|
+
|
|
160
|
+
# Synapses
|
|
161
|
+
for s in range(self.num_nodes):
|
|
162
|
+
for t, data in self.edge_data[s].items():
|
|
163
|
+
full_name = f"{data['model']}_{self.node_data[s]['model']}_{self.node_data[t]['model']}"
|
|
164
|
+
self.synapse_connections[full_name]['i'].append(self.local_index[s])
|
|
165
|
+
self.synapse_connections[full_name]['j'].append(self.local_index[t])
|
|
166
|
+
for state in self.model_registry[data['model']]['state']:
|
|
167
|
+
if state == 'delay':
|
|
168
|
+
# Brian has a default delay of 0ms (to get to the next timestep)
|
|
169
|
+
self.synapse_states[full_name]['delay'].append((data['delay']-1.0)*ms)
|
|
170
|
+
else:
|
|
171
|
+
self.synapse_states[full_name][state].append(data[state])
|
|
172
|
+
|
|
173
|
+
# Brian Network
|
|
174
|
+
self.brian_net = brian2.Network()
|
|
175
|
+
self.input_groups = dict()
|
|
176
|
+
self.neuron_groups = dict()
|
|
177
|
+
self.synapse_groups = dict()
|
|
178
|
+
self.spike_monitors = dict()
|
|
179
|
+
|
|
180
|
+
# Create input and neuron groups (and their spike monitors)
|
|
181
|
+
for name, count in self.group_count.items():
|
|
182
|
+
# Spike generator group
|
|
183
|
+
if self.model_registry[name]['graph_type'] == 'input':
|
|
184
|
+
self.input_groups[name] = SpikeGeneratorGroup(count, self.spikegen_index,
|
|
185
|
+
self.spikegen_times, sorted=True)
|
|
186
|
+
self.spike_monitors[name] = SpikeMonitor(self.input_groups[name])
|
|
187
|
+
# Regular neuron model group
|
|
188
|
+
elif self.model_registry[name]['graph_type'] == 'neuron':
|
|
189
|
+
self.neuron_groups[name] = NeuronGroup(count, model=self.model_registry[name]['model_eqs'],
|
|
190
|
+
threshold=self.model_registry[name]['threshold'],
|
|
191
|
+
reset=self.model_registry[name]['reset'],
|
|
192
|
+
method=self.model_registry[name]['method'],
|
|
193
|
+
events=dict(self.model_registry[name]['events']))
|
|
194
|
+
# These "run regularly" methods bypass the standard Brian integration step
|
|
195
|
+
if 'run_regularly' in self.model_registry[name]:
|
|
196
|
+
for program in self.model_registry[name]['run_regularly']:
|
|
197
|
+
self.neuron_groups[name].run_regularly(program['eqs'], when=program['when'])
|
|
198
|
+
# These "run on event" methods trigger when a custom event happens
|
|
199
|
+
if 'run_on_event' in self.model_registry[name]:
|
|
200
|
+
for program in self.model_registry[name]['run_on_event']:
|
|
201
|
+
self.neuron_groups[name].run_on_event(program['event'], program['eqs'])
|
|
202
|
+
# Copy over states
|
|
203
|
+
for state in self.model_registry[name]['state']:
|
|
204
|
+
getattr(self.neuron_groups[name], f"{state}")[:] = self.neuron_states[name][state]
|
|
205
|
+
self.spike_monitors[name] = SpikeMonitor(self.neuron_groups[name])
|
|
206
|
+
|
|
207
|
+
# Create synapse groups
|
|
208
|
+
for (name, source, target) in self.edge_set:
|
|
209
|
+
full_name = f"{name}_{source}_{target}"
|
|
210
|
+
if self.model_registry[source]['graph_type'] == 'input':
|
|
211
|
+
self.synapse_groups[full_name] = Synapses(self.input_groups[source],
|
|
212
|
+
self.neuron_groups[target],
|
|
213
|
+
model=self.model_registry[name]['model_eqs'],
|
|
214
|
+
on_pre=self.model_registry[name]['on_pre'])
|
|
215
|
+
else:
|
|
216
|
+
self.synapse_groups[full_name] = Synapses(self.neuron_groups[source],
|
|
217
|
+
self.neuron_groups[target],
|
|
218
|
+
model=self.model_registry[name]['model_eqs'],
|
|
219
|
+
on_pre=self.model_registry[name]['on_pre'])
|
|
220
|
+
# Copy over connections
|
|
221
|
+
self.synapse_groups[full_name].connect(i=self.synapse_connections[full_name]['i'],
|
|
222
|
+
j=self.synapse_connections[full_name]['j'])
|
|
223
|
+
# Copy over states
|
|
224
|
+
for state in self.model_registry[name]['state']:
|
|
225
|
+
getattr(self.synapse_groups[full_name], f"{state}")[:,:] = self.synapse_states[full_name][state]
|
|
226
|
+
|
|
227
|
+
# Add all the objects to the network
|
|
228
|
+
for value in self.input_groups.values():
|
|
229
|
+
self.brian_net.add(value)
|
|
230
|
+
for value in self.neuron_groups.values():
|
|
231
|
+
self.brian_net.add(value)
|
|
232
|
+
for value in self.synapse_groups.values():
|
|
233
|
+
self.brian_net.add(value)
|
|
234
|
+
for value in self.spike_monitors.values():
|
|
235
|
+
self.brian_net.add(value)
|
|
236
|
+
|
|
237
|
+
# This is the scheduling of events needed for the synapse input not be discarded
|
|
238
|
+
# (the default handling of synapses occurs between thresholds and resets)
|
|
239
|
+
self.brian_net.schedule = ['start', 'groups', 'thresholds', 'resets', 'synapses', 'end']
|
|
240
|
+
|
|
241
|
+
# Collect any output from the simulation
|
|
242
|
+
def read_spikes(self):
|
|
243
|
+
self.spike_list = []
|
|
244
|
+
offset = 0
|
|
245
|
+
for name, monitor in self.spike_monitors.items():
|
|
246
|
+
self.spike_list.extend([[] for _ in range(self.group_count[name])])
|
|
247
|
+
for s in range(len(monitor)):
|
|
248
|
+
self.spike_list[offset+monitor.i[s]].append(monitor.t[s]/ms)
|
|
249
|
+
offset += self.group_count[name]
|
|
250
|
+
|
|
251
|
+
return self.spike_list
|
|
252
|
+
|
|
253
|
+
# Return spikes as event list
|
|
254
|
+
def get_spikes(self):
|
|
255
|
+
if self.spike_list is None:
|
|
256
|
+
return self.read_spikes()
|
|
257
|
+
else:
|
|
258
|
+
return self.spike_list
|
|
259
|
+
|
|
260
|
+
def plot_spikes(self, figsize=(8,6), linelengths=0.8, linewidths=1.0,
|
|
261
|
+
color_dict={'LIF': 'C0', 'IN': 'C1'}, tick_names=False):
|
|
262
|
+
if self.spike_list is None:
|
|
263
|
+
self.read_spikes()
|
|
264
|
+
|
|
265
|
+
# Plot the event list information
|
|
266
|
+
plt.figure(figsize=figsize)
|
|
267
|
+
|
|
268
|
+
# We can also color the rows according to population
|
|
269
|
+
if color_dict is None:
|
|
270
|
+
color_dict = {key: f"C{i%10}" for i, key in enumerate(self.group_count.keys())}
|
|
271
|
+
event_color = []
|
|
272
|
+
for name, count in self.group_count.items():
|
|
273
|
+
if name not in color_dict:
|
|
274
|
+
color_dict[name] = f"C{len(color_dict)%10}"
|
|
275
|
+
event_color.extend([color_dict[name]] * count)
|
|
276
|
+
# colored lines (for legend)
|
|
277
|
+
for key in color_dict.keys():
|
|
278
|
+
plt.plot(0,0,'-',color=color_dict[key],linewidth=2.0)
|
|
279
|
+
|
|
280
|
+
# The spike raster is plotted using eventplot
|
|
281
|
+
plt.eventplot(self.spike_list, colors=event_color, lineoffsets=1,
|
|
282
|
+
linelengths=linelengths, linewidths=linewidths)
|
|
283
|
+
|
|
284
|
+
# Tick names (may be too crowded with many neurons)
|
|
285
|
+
if tick_names:
|
|
286
|
+
plt.yticks(list(self.node_map.values()), list(self.node_map.keys()))
|
|
287
|
+
|
|
288
|
+
plt.title('Spike Raster')
|
|
289
|
+
plt.xlabel('Time (ms)')
|
|
290
|
+
plt.ylabel('Neuron (index)')
|
|
291
|
+
plt.tight_layout()
|
|
292
|
+
plt.legend(color_dict.keys())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Base model registry
|
|
2
|
+
model_registry = {'IN': {'graph_type': 'input', 'state': {}},
|
|
3
|
+
'LIF': {'graph_type': 'neuron',
|
|
4
|
+
'model_eqs' : '''v : 1
|
|
5
|
+
v_thresh : 1
|
|
6
|
+
v_reset : 1
|
|
7
|
+
v_bias : 1
|
|
8
|
+
v_leak : 1
|
|
9
|
+
''',
|
|
10
|
+
'method' : 'exact',
|
|
11
|
+
'threshold' : 'v>v_thresh',
|
|
12
|
+
'reset' : 'v=v_reset',
|
|
13
|
+
'events' : {},
|
|
14
|
+
'run_regularly' : [{'eqs': 'v*=(1.0-v_leak)', 'when': 'resets'},
|
|
15
|
+
{'eqs': 'v+=v_bias', 'when': 'groups'}],
|
|
16
|
+
'state': {'v': {'dsl': 'voltage', 'default': 0.0},
|
|
17
|
+
'v_thresh': {'dsl': 'threshold', 'default': 1.0},
|
|
18
|
+
'v_reset': {'dsl': 'reset', 'default': 0.0},
|
|
19
|
+
'v_bias': {'dsl': 'bias', 'default': 0.0},
|
|
20
|
+
'v_leak': {'dsl': 'leak', 'default': 1.0}}},
|
|
21
|
+
'PSP': {'graph_type': 'synapse',
|
|
22
|
+
'model_eqs' : 'weight : 1',
|
|
23
|
+
'on_pre' : 'v+=weight',
|
|
24
|
+
'state': {'delay': {'dsl': 'delay', 'default': 1.0, 'unit': 'ms'},
|
|
25
|
+
'weight': {'dsl': 'weight', 'default': 1.0}}}
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Model registry
|
|
2
|
+
model_registry = {'pLIF': {'graph_type': 'neuron',
|
|
3
|
+
'model_eqs' : '''v : 1
|
|
4
|
+
v_thresh : 1
|
|
5
|
+
v_reset : 1
|
|
6
|
+
v_bias : 1
|
|
7
|
+
v_leak : 1
|
|
8
|
+
p_spike : 1
|
|
9
|
+
''',
|
|
10
|
+
'method' : 'exact',
|
|
11
|
+
'threshold' : '(v>v_thresh) and (rand()<=p_spike)',
|
|
12
|
+
'reset' : '', # probabilistic spiking requires custom event
|
|
13
|
+
'events' : {'pass_thresh': 'v>v_thresh'},
|
|
14
|
+
'run_regularly' : [{'eqs': 'v*=(1.0-v_leak)', 'when': 'resets'},
|
|
15
|
+
{'eqs': 'v+=v_bias', 'when': 'groups'}],
|
|
16
|
+
'run_on_event' : [{'event': 'pass_thresh', 'eqs': 'v=v_reset'}],
|
|
17
|
+
'state': {'v': {'dsl': 'voltage', 'default': 0.0},
|
|
18
|
+
'v_thresh': {'dsl': 'threshold', 'default': 1.0},
|
|
19
|
+
'v_reset': {'dsl': 'reset', 'default': 0.0},
|
|
20
|
+
'v_bias': {'dsl': 'bias', 'default': 0.0},
|
|
21
|
+
'v_leak': {'dsl': 'leak', 'default': 1.0},
|
|
22
|
+
'p_spike': {'dsl': 'prob', 'default': 1.0}}}
|
|
23
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import fugu
|
|
8
|
+
except ImportError:
|
|
9
|
+
fugu = None
|
|
10
|
+
if fugu is not None:
|
|
11
|
+
from fugu import Scaffold
|
|
12
|
+
from fugu.scaffold import PortData, ChannelData
|
|
13
|
+
from fugu.backends import snn_Backend
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
from collections import Counter, defaultdict
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import networkx as nx
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
|
|
22
|
+
class SimFugu:
|
|
23
|
+
def __init__(self, dsl_net):
|
|
24
|
+
if fugu is None:
|
|
25
|
+
raise ImportError("fugu package is required for SimFugu")
|
|
26
|
+
|
|
27
|
+
self.dsl_net = dsl_net
|
|
28
|
+
self.timesteps = None
|
|
29
|
+
|
|
30
|
+
self.node_map = None
|
|
31
|
+
self.spike_list = None
|
|
32
|
+
|
|
33
|
+
self.model_registry = self.import_registry()
|
|
34
|
+
|
|
35
|
+
self.fugu_backend = None
|
|
36
|
+
self.debug = None
|
|
37
|
+
self.backend_args = None
|
|
38
|
+
self.scaffold = None
|
|
39
|
+
|
|
40
|
+
# Dynamically import the model registry files
|
|
41
|
+
def import_registry(self):
|
|
42
|
+
registry = dict()
|
|
43
|
+
registry_dir = Path(__file__).resolve().parent / 'registry'
|
|
44
|
+
sys.path.insert(0, str(registry_dir.parent))
|
|
45
|
+
for file_path in registry_dir.glob("*.py"):
|
|
46
|
+
if file_path.name == "__init__.py":
|
|
47
|
+
continue
|
|
48
|
+
module_name = f"registry.{file_path.stem}"
|
|
49
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
50
|
+
module = importlib.util.module_from_spec(spec)
|
|
51
|
+
spec.loader.exec_module(module)
|
|
52
|
+
registry.update(getattr(module, 'model_registry', {}))
|
|
53
|
+
sys.path.pop(0)
|
|
54
|
+
return registry
|
|
55
|
+
|
|
56
|
+
# Convert between dsl model to Fugu model
|
|
57
|
+
def rekey_model(self, data):
|
|
58
|
+
for key, value in self.model_registry[data['model']]['param'].items():
|
|
59
|
+
if value['dsl'] is not None:
|
|
60
|
+
if value['dsl'] == 'delay':
|
|
61
|
+
data[key] = int(data.pop(value['dsl']))
|
|
62
|
+
else:
|
|
63
|
+
data[key] = data.pop(value['dsl'])
|
|
64
|
+
else:
|
|
65
|
+
data[key] = value['default']
|
|
66
|
+
return data
|
|
67
|
+
|
|
68
|
+
def compile(self, backend='snn', backend_args={'record': 'all'}, debug=False):
|
|
69
|
+
self.debug = debug
|
|
70
|
+
if backend == 'snn':
|
|
71
|
+
self.fugu_backend = snn_Backend()
|
|
72
|
+
else:
|
|
73
|
+
raise NotImplementedError(f"Backend '{backend}' is not supported.")
|
|
74
|
+
self.backend_args = backend_args
|
|
75
|
+
if self.debug:
|
|
76
|
+
self.backend_args['debug_mode'] = True
|
|
77
|
+
|
|
78
|
+
# Convert network to Fugu
|
|
79
|
+
start_time = time.perf_counter()
|
|
80
|
+
self.to_fugu()
|
|
81
|
+
end_time = time.perf_counter()
|
|
82
|
+
self.compile_time = end_time - start_time
|
|
83
|
+
if self.debug:
|
|
84
|
+
print(f"Compile time: {self.compile_time}")
|
|
85
|
+
|
|
86
|
+
# Run the network
|
|
87
|
+
def run(self, timesteps=10.0, verbose=False):
|
|
88
|
+
self.timesteps = int(timesteps)
|
|
89
|
+
self.verbose = verbose
|
|
90
|
+
|
|
91
|
+
# Run network
|
|
92
|
+
start_time = time.perf_counter()
|
|
93
|
+
self.fugu_result = self.fugu_backend.run(n_steps=timesteps)
|
|
94
|
+
end_time = time.perf_counter()
|
|
95
|
+
self.run_time = end_time - start_time
|
|
96
|
+
if self.verbose:
|
|
97
|
+
print(f"Run time: {self.run_time}")
|
|
98
|
+
|
|
99
|
+
# Translates a Sango network into a Fugu Scaffold
|
|
100
|
+
def to_fugu(self):
|
|
101
|
+
# Get a flattened graph object
|
|
102
|
+
self.dsl_graph = self.dsl_net._topology.to_nx()
|
|
103
|
+
|
|
104
|
+
# Convert networkx to scaffold and graph
|
|
105
|
+
self.scaffold = self.to_scaffold(self.dsl_graph)
|
|
106
|
+
|
|
107
|
+
# Compile scaffold in Fugu backend
|
|
108
|
+
self.fugu_backend.compile(self.scaffold, self.backend_args)
|
|
109
|
+
|
|
110
|
+
# Dummy brick object for Fugu scaffold
|
|
111
|
+
class DummyBrick():
|
|
112
|
+
def __init__(self, name='dummy'):
|
|
113
|
+
self.name = name
|
|
114
|
+
self.vector = []
|
|
115
|
+
self.index = 0
|
|
116
|
+
self.is_built = True
|
|
117
|
+
# This object is mainly used for managing Fugu inputs
|
|
118
|
+
def __iter__(self):
|
|
119
|
+
self.index = 0
|
|
120
|
+
return iter(self.vector)
|
|
121
|
+
def __next__(self):
|
|
122
|
+
if self.index < len(self.data):
|
|
123
|
+
result = self.vector[self.index]
|
|
124
|
+
self.index += 1
|
|
125
|
+
return result
|
|
126
|
+
else:
|
|
127
|
+
raise StopIteration
|
|
128
|
+
|
|
129
|
+
# Convert Sango network to Fugu Scaffold
|
|
130
|
+
def to_scaffold(self, net):
|
|
131
|
+
# Set up a dummy scaffold in Fugu
|
|
132
|
+
scaffold = Scaffold()
|
|
133
|
+
scaffold_circuit = nx.DiGraph()
|
|
134
|
+
# Input and main Sango bricks
|
|
135
|
+
input_brick = {'tag': 'input',
|
|
136
|
+
'name': 'InputBrick',
|
|
137
|
+
'brick': self.DummyBrick('input'),
|
|
138
|
+
'layer': 'input',
|
|
139
|
+
'ports': {'output': None},
|
|
140
|
+
'is_built': True}
|
|
141
|
+
sango_brick = {'tag': 'sango',
|
|
142
|
+
'name': 'SangoBrick',
|
|
143
|
+
'brick': self.DummyBrick('sango'),
|
|
144
|
+
'layer': 'output',
|
|
145
|
+
'ports': {'input': None},
|
|
146
|
+
'is_built': True}
|
|
147
|
+
# Connect dummy circuit
|
|
148
|
+
scaffold_circuit.add_node(0, **input_brick)
|
|
149
|
+
scaffold_circuit.add_node(1, **sango_brick)
|
|
150
|
+
scaffold_circuit.add_edge(0, 1, bind={'input': 'output'})
|
|
151
|
+
scaffold.tag_to_name = {'input': 'InputBrick', 'sango': 'SangoBrick'}
|
|
152
|
+
scaffold.brick_to_number = {'InputBrick': 0, 'SangoBrick': 1}
|
|
153
|
+
|
|
154
|
+
# Set up underlying graph
|
|
155
|
+
scaffold_graph = nx.DiGraph()
|
|
156
|
+
self.num_nodes = self.dsl_graph.number_of_nodes()
|
|
157
|
+
self.node_data = [dict() for _ in range(self.num_nodes)]
|
|
158
|
+
self.node_index = dict()
|
|
159
|
+
self.group_count = Counter()
|
|
160
|
+
self.local_index = [0 for _ in range(self.num_nodes)]
|
|
161
|
+
self.group_count['IN'] = 0
|
|
162
|
+
self.spike_input = dict()
|
|
163
|
+
|
|
164
|
+
# Global node data
|
|
165
|
+
for n, (node, data) in enumerate(self.dsl_graph.nodes(data=True)):
|
|
166
|
+
self.node_index[node] = n
|
|
167
|
+
self.node_data[n] = self.rekey_model(data)
|
|
168
|
+
self.group_count.update([self.node_data[n]['model']])
|
|
169
|
+
self.local_index[n] = self.group_count[self.node_data[n]['model']] - 1
|
|
170
|
+
# Add spike times for inputs
|
|
171
|
+
if self.model_registry[self.node_data[n]['model']]['node_class'] == 'InputNeuron':
|
|
172
|
+
self.spike_input[node]=data['times']
|
|
173
|
+
|
|
174
|
+
# Remove the input model (IN) if counts are zero
|
|
175
|
+
if self.group_count['IN'] == 0:
|
|
176
|
+
del self.group_count['IN']
|
|
177
|
+
|
|
178
|
+
# Get the model insertion order of our counter, and organize node map by group
|
|
179
|
+
self.group_index = {key: index for index, key in enumerate(self.group_count.keys())}
|
|
180
|
+
self.group_sorted_count = [self.group_count[group] for group in self.group_index]
|
|
181
|
+
self.group_offset = [0] + [sum(self.group_sorted_count[:i+1]) for i in range(len(self.group_sorted_count))]
|
|
182
|
+
self.node_map = {key: self.group_offset[self.group_index[self.node_data[n]['model']]] + self.local_index[n]
|
|
183
|
+
for key, n in self.node_index.items()}
|
|
184
|
+
|
|
185
|
+
# Attach spike times to input brick
|
|
186
|
+
spike_times = defaultdict(list)
|
|
187
|
+
for n, (neuron, times) in enumerate(self.spike_input.items()):
|
|
188
|
+
for t in times:
|
|
189
|
+
spike_times[t].append(neuron)
|
|
190
|
+
spike_vector = [spike_times[key] for key in range(max(spike_times.keys())+1)]
|
|
191
|
+
scaffold_circuit.nodes[0]['brick'].vector = spike_vector
|
|
192
|
+
scaffold_circuit.nodes[0]['ports']['output'] = PortData(spec=None,
|
|
193
|
+
channels = {'data': ChannelData(spec=None, neurons = list(self.spike_input.keys()))})
|
|
194
|
+
scaffold_circuit.nodes[1]['ports']['input'] = PortData(spec=None,
|
|
195
|
+
channels = {'data': ChannelData(spec=None, neurons = [])})
|
|
196
|
+
scaffold.circuit = scaffold_circuit
|
|
197
|
+
|
|
198
|
+
# Insert nodes by group
|
|
199
|
+
for n, (node, nidx) in enumerate(self.node_map.items()):
|
|
200
|
+
node_data = self.node_data[n]
|
|
201
|
+
if node_data['model'] == 'IN':
|
|
202
|
+
scaffold_graph.add_node(node, neuron_number=nidx,
|
|
203
|
+
brick='InputBrick', **node_data)
|
|
204
|
+
else:
|
|
205
|
+
scaffold_graph.add_node(node, neuron_number=nidx,
|
|
206
|
+
brick='SangoBrick', **node_data)
|
|
207
|
+
|
|
208
|
+
# Global edge data
|
|
209
|
+
for source, target, data in self.dsl_graph.edges(data=True):
|
|
210
|
+
edge_data = self.rekey_model(data)
|
|
211
|
+
scaffold_graph.add_edge(source, target, **edge_data)
|
|
212
|
+
|
|
213
|
+
# Attach the built graph to the scaffold and mark as built
|
|
214
|
+
scaffold.graph = scaffold_graph
|
|
215
|
+
scaffold.is_built = True
|
|
216
|
+
|
|
217
|
+
return scaffold
|
|
218
|
+
|
|
219
|
+
# Collect any output from the simulation
|
|
220
|
+
def read_spikes(self):
|
|
221
|
+
# Reading in Fugu's dataframe results
|
|
222
|
+
spike_dict = defaultdict(list)
|
|
223
|
+
for spike in self.fugu_result.itertuples(index=False):
|
|
224
|
+
spike_dict[int(spike.neuron_number)].append(spike.time)
|
|
225
|
+
self.spike_list = [spike_dict[key] for key in range(len(self.node_map))]
|
|
226
|
+
return self.spike_list
|
|
227
|
+
|
|
228
|
+
# Return spikes as event list
|
|
229
|
+
def get_spikes(self):
|
|
230
|
+
if self.spike_list is None:
|
|
231
|
+
return self.read_spikes()
|
|
232
|
+
else:
|
|
233
|
+
return self.spike_list
|
|
234
|
+
|
|
235
|
+
def plot_spikes(self, figsize=(8,6), linelengths=0.8, linewidths=1.0,
|
|
236
|
+
color_dict={'LIF': 'C0', 'IN': 'C1'}, tick_names=False):
|
|
237
|
+
if self.spike_list is None:
|
|
238
|
+
self.read_spikes()
|
|
239
|
+
|
|
240
|
+
# Plot the event list information
|
|
241
|
+
plt.figure(figsize=figsize)
|
|
242
|
+
|
|
243
|
+
# We can also color the rows according to population
|
|
244
|
+
if color_dict is None:
|
|
245
|
+
color_dict = {key: f"C{i%10}" for i, key in enumerate(self.group_count.keys())}
|
|
246
|
+
event_color = []
|
|
247
|
+
for name, count in self.group_count.items():
|
|
248
|
+
if name not in color_dict:
|
|
249
|
+
color_dict[name] = f"C{len(color_dict)%10}"
|
|
250
|
+
event_color.extend([color_dict[name]] * count)
|
|
251
|
+
# colored lines (for legend)
|
|
252
|
+
for key in color_dict.keys():
|
|
253
|
+
plt.plot(0,0,'-',color=color_dict[key],linewidth=2.0)
|
|
254
|
+
|
|
255
|
+
# The spike raster is plotted using eventplot
|
|
256
|
+
plt.eventplot(self.spike_list, colors=event_color, lineoffsets=1,
|
|
257
|
+
linelengths=linelengths, linewidths=linewidths)
|
|
258
|
+
|
|
259
|
+
# Tick names (may be too crowded with many neurons)
|
|
260
|
+
if tick_names:
|
|
261
|
+
plt.yticks(list(self.node_map.values()), list(self.node_map.keys()))
|
|
262
|
+
|
|
263
|
+
plt.title('Spike Raster')
|
|
264
|
+
plt.xlabel('Time (ms)')
|
|
265
|
+
plt.ylabel('Neuron (index)')
|
|
266
|
+
plt.tight_layout()
|
|
267
|
+
plt.legend(color_dict.keys())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Base model registry
|
|
2
|
+
model_registry = {
|
|
3
|
+
'LIF': {'node_class': 'LIFNeuron',
|
|
4
|
+
'param': {'threshold': {'dsl': 'threshold', 'default': 0.0},
|
|
5
|
+
'reset_voltage': {'dsl': 'reset', 'default': 0.0},
|
|
6
|
+
'decay': {'dsl': 'leak', 'default': 1.0},
|
|
7
|
+
'voltage': {'dsl': 'voltage', 'default': 0.0},
|
|
8
|
+
'bias': {'dsl': 'bias', 'default': 0.0}}},
|
|
9
|
+
'IN': {'node_class': 'InputNeuron',
|
|
10
|
+
'param': {'threshold': {'dsl': None, 'default': 0.1},
|
|
11
|
+
'voltage': {'dsl': None, 'default': 0.0}}},
|
|
12
|
+
'PSP': {'edge_class': 'Synapse',
|
|
13
|
+
'param': {'weight': {'dsl': 'weight', 'default': 1.0},
|
|
14
|
+
'delay': {'dsl': 'delay', 'default': 1.0}}}
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Prob model registry
|
|
2
|
+
model_registry = {
|
|
3
|
+
'pLIF': {'node_class': 'LIFNeuron',
|
|
4
|
+
'param': {'threshold': {'dsl': 'threshold', 'default': 0.0},
|
|
5
|
+
'reset_voltage': {'dsl': 'reset', 'default': 0.0},
|
|
6
|
+
'decay': {'dsl': 'leak', 'default': 1.0},
|
|
7
|
+
'voltage': {'dsl': 'voltage', 'default': 0.0},
|
|
8
|
+
'p': {'dsl': 'prob', 'default': 1.0},
|
|
9
|
+
'bias': {'dsl': 'bias', 'default': 0.0}}}
|
|
10
|
+
}
|
|
File without changes
|