sango 1.0.0__tar.gz
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-1.0.0/LICENCE +28 -0
- sango-1.0.0/PKG-INFO +230 -0
- sango-1.0.0/README.md +213 -0
- sango-1.0.0/pyproject.toml +31 -0
- sango-1.0.0/setup.cfg +4 -0
- sango-1.0.0/src/sango/__init__.py +4 -0
- sango-1.0.0/src/sango/backend/__init__.py +3 -0
- sango-1.0.0/src/sango/backend/brian/__init__.py +0 -0
- sango-1.0.0/src/sango/backend/brian/brian.py +292 -0
- sango-1.0.0/src/sango/backend/brian/registry/base.py +26 -0
- sango-1.0.0/src/sango/backend/brian/registry/prob.py +23 -0
- sango-1.0.0/src/sango/backend/fugu/__init__.py +0 -0
- sango-1.0.0/src/sango/backend/fugu/fugu.py +267 -0
- sango-1.0.0/src/sango/backend/fugu/registry/base.py +15 -0
- sango-1.0.0/src/sango/backend/fugu/registry/prob.py +10 -0
- sango-1.0.0/src/sango/backend/stacs/__init__.py +0 -0
- sango-1.0.0/src/sango/backend/stacs/registry/base.py +22 -0
- sango-1.0.0/src/sango/backend/stacs/registry/prob.py +12 -0
- sango-1.0.0/src/sango/backend/stacs/stacs.py +1000 -0
- sango-1.0.0/src/sango/core.py +456 -0
- sango-1.0.0/src/sango/model/__init__.py +5 -0
- sango-1.0.0/src/sango/model/base.py +53 -0
- sango-1.0.0/src/sango/model/prob.py +9 -0
- sango-1.0.0/src/sango/network.py +742 -0
- sango-1.0.0/src/sango.egg-info/PKG-INFO +230 -0
- sango-1.0.0/src/sango.egg-info/SOURCES.txt +27 -0
- sango-1.0.0/src/sango.egg-info/dependency_links.txt +1 -0
- sango-1.0.0/src/sango.egg-info/requires.txt +2 -0
- sango-1.0.0/src/sango.egg-info/top_level.txt +1 -0
sango-1.0.0/LICENCE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, National Technology & Engineering Solutions of Sandia, LLC (NTESS)
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
sango-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sango
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Compositional Spiking Neural Network Domain Specific Language
|
|
5
|
+
Author-email: Felix Wang <felwang@sandia.gov>
|
|
6
|
+
Project-URL: Repository, https://github.com/sandialabs/Sango
|
|
7
|
+
Classifier: Intended Audience :: Science/Research
|
|
8
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENCE
|
|
14
|
+
Requires-Dist: numpy>=1.19.3
|
|
15
|
+
Requires-Dist: networkx>=2.4
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Compositional Spiking Neural Network Domain Specific Language
|
|
19
|
+
|
|
20
|
+
Sango is a compositional spiking neural network domain specific language that is implemented as an internal DSL within Python, introducing a number of high level classes for compositionally constructing networks, and enabling flexible code reuse.
|
|
21
|
+
|
|
22
|
+
The name Sango is inspired by the Japanese word for coral, a common habitat for pufferfish (see the [Fugu](https://github.com/sandialabs/Fugu) project), and admits the backronym Structured Abstractions for Network Group Organization.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
The Sango package requires Python 3.9 or above (for more advanced dataclass support). There are also two main dependencies that are currently required: NumPy and NetworkX. The features used by these external packages are fairly generic, so versioning should not be an issue. Installation proceeds as with any standard python package.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
git clone https://github.com/sandialabs/Sango.git
|
|
30
|
+
cd Sango
|
|
31
|
+
python -m pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## General Structure
|
|
35
|
+
|
|
36
|
+
At the core of Sango is the structural organization of networks analogous to a directory listing of folders (networks themselves), files (topological components), and their contents (nodes and edges). Additionally, alias classes (analogous to symlinks) provide a degree of indirection and introduce reference dependencies that allows for the procedural construction of complex network topologies.
|
|
37
|
+
|
|
38
|
+
The main classes that accomplish this are:
|
|
39
|
+
- `Network`: the main container that is built out of high-level topological components (including other networks)
|
|
40
|
+
- `NodeGroup`: a group of instantiated nodes sharing the same node model (e.g. a population of neurons)
|
|
41
|
+
- `EdgeGroup`: a group of instantiated edges between two sets of nodes (e.g. a projection of synapses)
|
|
42
|
+
- `NodePort`: an alias class pointing to set of (external) nodes (e.g. placeholder nodes for network inputs)
|
|
43
|
+
- `NodeList`: an alias class with a list of (pointers to) nodes (e.g. a collection of nodes for network outputs)
|
|
44
|
+
|
|
45
|
+
## Basic Syntax
|
|
46
|
+
|
|
47
|
+
The construction of a network proceeds through creating network objects and adding topological components to them. Syntactically, these topological components are assigned as attributes to a Network object instance and accessed through dot notation. Additionally, lists of any of these classes may be assigned and are indexed through bracket notation.
|
|
48
|
+
|
|
49
|
+
The individual nodes and edges within their groups are also indexed through bracket notation. This combination of dot and bracket notation determines the "path name" of instantiated nodes, and the pair of source and target "path names" identify instantiated edges.
|
|
50
|
+
```python
|
|
51
|
+
net = Network() # instantiate network
|
|
52
|
+
net.layer = [NodeGroup(LIF(), 32), # add a list of node groups
|
|
53
|
+
NodeGroup(LIF(), 10)]
|
|
54
|
+
net.dense = EdgeGroup(net.layer[0], # add an edge group connecting
|
|
55
|
+
net.layer[1], # the node groups
|
|
56
|
+
PSP(), edges=...)
|
|
57
|
+
net.inp = NodePort() # add an input port (not linked)
|
|
58
|
+
net.out = NodeList(net.layer[1][::2]) # add an output list (slicing)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
After a network topology has been defined, it can be "built" accordingly. This process is performed iteratively and recursively for any child networks within the topology to resolve any dependencies introduced through placeholder node ports (implementing a topological sort). The path structure through the network topology is also "flattened" to generate the full path names for the instantiated nodes. More complex example networks are provided in the notebooks.
|
|
62
|
+
```python
|
|
63
|
+
net.build() # build network topology
|
|
64
|
+
print(net) # print the topological components
|
|
65
|
+
|
|
66
|
+
# Network Topology:
|
|
67
|
+
# (node) layer[0]
|
|
68
|
+
# (node) layer[1]
|
|
69
|
+
# (edge) dense: (node) layer[0] -> (node) layer[1]
|
|
70
|
+
# (port) inp <- (no link)
|
|
71
|
+
# (list) out
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Networks
|
|
75
|
+
|
|
76
|
+
It is possible and encouraged to develop subclasses of network for organization and code reuse. This allows you more easily to use networks in the construction of nested structures.
|
|
77
|
+
|
|
78
|
+
The definition of a network is split into two main methods:
|
|
79
|
+
- `__init__()`: this is where node ports and any relevant network parameters are provided
|
|
80
|
+
- `build()`: this is where topological components are assigned (potentially procedurally)
|
|
81
|
+
|
|
82
|
+
The use of node ports additionally allows for variably sized networks that are determined at build time. During the automated iterative and recursive build process, any dependencies that were generated by unsized node ports in the network initialization method are resolved before the build method is called.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
class Linear(Network):
|
|
86
|
+
def __init__(self, size=64):
|
|
87
|
+
super().__init__() # base class initialization (required)
|
|
88
|
+
self.size = size # user-defined network parameters
|
|
89
|
+
self.inp = NodePort() # unsized node port (generates dependency)
|
|
90
|
+
self.ctrl = NodePort(1) # sized node port (no dependency)
|
|
91
|
+
|
|
92
|
+
def build(self):
|
|
93
|
+
# Layers (using supplied parameters)
|
|
94
|
+
self.layer = NodeGroup(LIF(), self.size)
|
|
95
|
+
|
|
96
|
+
# Procedurally generating edges (using resolved port information)
|
|
97
|
+
edges = [(i,j) for i,j in itertools.product(
|
|
98
|
+
range(self.inp.size),range(self.layer.size))]
|
|
99
|
+
|
|
100
|
+
# Connections (using node port placeholder)
|
|
101
|
+
self.dense = EdgeGroup(self.inp, self.layer, PSP(), edges=edges)
|
|
102
|
+
|
|
103
|
+
return
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Port size resolution is generally performed by binding/linking a node port with a sized node group/list (from elsewhere in the network topology), or by manually specifying its size. In the case that node port dependencies cannot be resolved (e.g. for cyclic structures), the user will need to provide some additional information to break the dependency chain.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
net = Network() # instantiate network
|
|
110
|
+
net.inp = NodeGroup(IN(), 12) # add an input node group
|
|
111
|
+
net.ff = [Linear(32), # add a list of networks
|
|
112
|
+
Linear(10)]
|
|
113
|
+
# bind topological components to ports (using dot notation)
|
|
114
|
+
net.bind(net.inp, net.ff[0].inp)
|
|
115
|
+
net.bind(net.ff[0].layer, net.ff[1].inp)
|
|
116
|
+
net.build() # build network topology
|
|
117
|
+
print(net) # print the topological components
|
|
118
|
+
|
|
119
|
+
# Network Topology:
|
|
120
|
+
# (node) inp
|
|
121
|
+
# (port) ff[0].inp <- (node) inp
|
|
122
|
+
# (node) ff[0].layer
|
|
123
|
+
# (edge) ff[0].dense: (port) ff[0].inp <- (node) inp -> (node) ff[0].layer
|
|
124
|
+
# (port) ff[1].inp <- (node) ff[0].layer
|
|
125
|
+
# (node) ff[1].layer
|
|
126
|
+
# (edge) ff[1].dense: (port) ff[1].inp <- (node) ff[0].layer -> (node) ff[1].layer
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Nodes and Edges
|
|
130
|
+
The various node and edge models (LIF and PSP in the example, respectively) are defined as Python dataclasses, where each attribute may be assigned a default value for initialization of state variables (e.g. as ints, floats). The use of tuples indicate shared parameters with respect to a node group.
|
|
131
|
+
|
|
132
|
+
When a node/edge group is defined, a node/edge model instance is provided (allowing it to have different defaults than in the class definition by providing keyword arguments). Data within a node/edge group are stored as numpy arrays to take advantage of its shared memory property (so that changing parameters at either the group or individual node/edge level will be reflected coherently). These can be accessed using node/edge model attribute name at the group level.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
@dataclass
|
|
136
|
+
class LIF(Neuron):
|
|
137
|
+
model: str = 'LIF' # model name
|
|
138
|
+
voltage: float = 0.0 # individual parameter
|
|
139
|
+
threshold: float = 1.0
|
|
140
|
+
reset: float = 0.0, # shared parameter (note: this is for illustration, the
|
|
141
|
+
leak: float = 1.0 # basic LIF model provided by Sango has
|
|
142
|
+
# 'reset' as an individual parameter)
|
|
143
|
+
|
|
144
|
+
res = NodeGroup(LIF(threshold=0.9), # model parameter default
|
|
145
|
+
size=3, # node group size
|
|
146
|
+
voltage=0.6, # bulk parameter assignment
|
|
147
|
+
leak=[0.5, 0.4, 0.3]) # individual parameter assignment
|
|
148
|
+
res.reset = 0.1 # shared parameter assignment
|
|
149
|
+
res[1].voltage = 0.8 # individual node parameter assignment
|
|
150
|
+
|
|
151
|
+
print(res.model) # ['LIF']
|
|
152
|
+
print(res.voltage) # [0.6 0.8 0.6]
|
|
153
|
+
print(res.threshold) # [0.9 0.9 0.9]
|
|
154
|
+
print(res.reset) # [(0.1,)]
|
|
155
|
+
print(res.leak) # [0.5 0.4 0.3]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
@dataclass
|
|
160
|
+
class PSP(Synapse):
|
|
161
|
+
model: str = 'PSP'
|
|
162
|
+
delay: float = 1.0
|
|
163
|
+
weight: float = 1.0
|
|
164
|
+
|
|
165
|
+
rr = EdgeGroup(res, res, PSP(delay=2.0), # model parameter default
|
|
166
|
+
edges=[(0,1), (1,2), (2,0)], # list of directed tuples (source, target)
|
|
167
|
+
weight=[1.0, 2.0, 3.0]) # individual parameter assignment
|
|
168
|
+
rr[(1,2)].delay = 3.0 # individual edge parameter assignment (by tuple)
|
|
169
|
+
rr[0].weight = 1.5 # individual edge parameter assignment (by index)
|
|
170
|
+
|
|
171
|
+
print(rr.delay) # [2.0 3.0 2.0]
|
|
172
|
+
print(rr.weight) # [1.5 2.0 3.0]
|
|
173
|
+
|
|
174
|
+
# Edge groups have a mapping from directed tuples to linear indexes
|
|
175
|
+
print(rr.edge_map) # {(0, 1): 0, (1, 2): 1, (2, 0): 2}
|
|
176
|
+
print(rr.target_index) # [1, 2, 0]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Node groups require a node model, and are preferably also instantiated with a size (as opposed to incrementally adding nodes). By default, if no size is provided, a single node will be instantiated.
|
|
180
|
+
|
|
181
|
+
Edge groups require an edge model and source/target nodes (which may be node groups, node lists, or node ports), and are preferably also instantiated with a list of edges. This is a list of tuples of pairs of source/target indexes that are local with respect to the source/target sets of nodes, respectively. By default, if no edge list is provided, an edge with the source/target pair of (0,0) will be instantiated.
|
|
182
|
+
|
|
183
|
+
## Simulation
|
|
184
|
+
In addition to defining networks, it is often useful to simulate the constructed network. There is currently some simulation support provided through the Brian 2 and STACS spiking neural network simulators (installed separately). Here, Sango may be thought of as the "frontend" interface to the "backend" simulator (or potentially neuromorphic hardware platform). This decoupling between frontend and backend is important for maintaining flexibility in the high-level network descriptions and for portability to different low-level network implementations.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
# Import the desired simulator backend
|
|
188
|
+
from sango.backend import SimBrian
|
|
189
|
+
|
|
190
|
+
sim = SimBrian(net) # Pass the built network to the backend translation layer
|
|
191
|
+
sim.compile() # Convert the network onto the backend execution model
|
|
192
|
+
sim.run(10.0) # Simulate the network (arguments may be backend-specific)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Custom user inputs into a network may be provided through input node models. In particular, there is a simple spike generator input model that takes a list of spike times (which specifies when the node emits spikes). This can be wrapped in a node group to provide a list of input nodes, and further wrapped in a network for compositional reuse.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
class Input(Network):
|
|
199
|
+
def __init__(self, spike_times):
|
|
200
|
+
super().__init__()
|
|
201
|
+
self.spike_times = spike_times # number of nodes x lists of times
|
|
202
|
+
|
|
203
|
+
def build(self):
|
|
204
|
+
# Spike generator
|
|
205
|
+
self.spikegen = NodeGroup(IN(), len(self.spike_times), times=self.spike_times)
|
|
206
|
+
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
# Define spiking inputs as a list of lists
|
|
210
|
+
input_vec = [[2, 4, 5], # 0,1,1,0,1,0,0 (represented as bit strings,
|
|
211
|
+
[0, 1, 4, 6]] # 1,0,1,0,0,1,1 least significant bit first)
|
|
212
|
+
|
|
213
|
+
net = Network() # instantiate network
|
|
214
|
+
net.inp = Input(spike_times=input_vec) # add an input network
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Simulation outputs are similarly provided as a spike list of times (per node in the network). The mapping between a node in Sango and the corresponding node in the backend is provided through a "node map" which is generated during the compilation process. There are also some convenience functions to plot the resulting spike raster. Additional features, such as recording state variables, depend on the backend that is used.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
spike_list = sim.get_spikes() # Get the spike list for each node
|
|
221
|
+
node_index = sim.node_map['inp.spikegen[1]'] # Find the index of a node by name
|
|
222
|
+
inp1_spike = spike_list[node_index] # Extract the node spike times
|
|
223
|
+
|
|
224
|
+
# Plot the spike raster (this uses matplotlib's eventplot)
|
|
225
|
+
sim.plot_spikes(tick_names=True)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Sango currently provides a method for converting its network object into a NetworkX directed graph for ease of translation. Nodes and edges are simply identified by their flattened "path name", and any associated data are provided as additional attributes. While the network descriptions in Sango are fairly flexible and open-ended, it does not prescribe node/edge model dynamics or how those computations should be implemented. Their translation with respect to a backend simulator is mediated through a "model registry" which provides the necessary information for mapping models. This is also intended to provide a degree of extensibility for custom user models.
|
|
229
|
+
|
|
230
|
+
There is existing backend support for these basic node and edge models: LIF (leaky integrate-and-fire neuron model), PSP (post-synaptic potential synapse model), and IN (simple spike generator input model). There is also support for probabilistic spiking: pLIF (probabilistic LIF neuron model, which is used in Fugu). Support for additional model types may require updating the model registry.
|
sango-1.0.0/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Compositional Spiking Neural Network Domain Specific Language
|
|
2
|
+
|
|
3
|
+
Sango is a compositional spiking neural network domain specific language that is implemented as an internal DSL within Python, introducing a number of high level classes for compositionally constructing networks, and enabling flexible code reuse.
|
|
4
|
+
|
|
5
|
+
The name Sango is inspired by the Japanese word for coral, a common habitat for pufferfish (see the [Fugu](https://github.com/sandialabs/Fugu) project), and admits the backronym Structured Abstractions for Network Group Organization.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
The Sango package requires Python 3.9 or above (for more advanced dataclass support). There are also two main dependencies that are currently required: NumPy and NetworkX. The features used by these external packages are fairly generic, so versioning should not be an issue. Installation proceeds as with any standard python package.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
git clone https://github.com/sandialabs/Sango.git
|
|
13
|
+
cd Sango
|
|
14
|
+
python -m pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## General Structure
|
|
18
|
+
|
|
19
|
+
At the core of Sango is the structural organization of networks analogous to a directory listing of folders (networks themselves), files (topological components), and their contents (nodes and edges). Additionally, alias classes (analogous to symlinks) provide a degree of indirection and introduce reference dependencies that allows for the procedural construction of complex network topologies.
|
|
20
|
+
|
|
21
|
+
The main classes that accomplish this are:
|
|
22
|
+
- `Network`: the main container that is built out of high-level topological components (including other networks)
|
|
23
|
+
- `NodeGroup`: a group of instantiated nodes sharing the same node model (e.g. a population of neurons)
|
|
24
|
+
- `EdgeGroup`: a group of instantiated edges between two sets of nodes (e.g. a projection of synapses)
|
|
25
|
+
- `NodePort`: an alias class pointing to set of (external) nodes (e.g. placeholder nodes for network inputs)
|
|
26
|
+
- `NodeList`: an alias class with a list of (pointers to) nodes (e.g. a collection of nodes for network outputs)
|
|
27
|
+
|
|
28
|
+
## Basic Syntax
|
|
29
|
+
|
|
30
|
+
The construction of a network proceeds through creating network objects and adding topological components to them. Syntactically, these topological components are assigned as attributes to a Network object instance and accessed through dot notation. Additionally, lists of any of these classes may be assigned and are indexed through bracket notation.
|
|
31
|
+
|
|
32
|
+
The individual nodes and edges within their groups are also indexed through bracket notation. This combination of dot and bracket notation determines the "path name" of instantiated nodes, and the pair of source and target "path names" identify instantiated edges.
|
|
33
|
+
```python
|
|
34
|
+
net = Network() # instantiate network
|
|
35
|
+
net.layer = [NodeGroup(LIF(), 32), # add a list of node groups
|
|
36
|
+
NodeGroup(LIF(), 10)]
|
|
37
|
+
net.dense = EdgeGroup(net.layer[0], # add an edge group connecting
|
|
38
|
+
net.layer[1], # the node groups
|
|
39
|
+
PSP(), edges=...)
|
|
40
|
+
net.inp = NodePort() # add an input port (not linked)
|
|
41
|
+
net.out = NodeList(net.layer[1][::2]) # add an output list (slicing)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
After a network topology has been defined, it can be "built" accordingly. This process is performed iteratively and recursively for any child networks within the topology to resolve any dependencies introduced through placeholder node ports (implementing a topological sort). The path structure through the network topology is also "flattened" to generate the full path names for the instantiated nodes. More complex example networks are provided in the notebooks.
|
|
45
|
+
```python
|
|
46
|
+
net.build() # build network topology
|
|
47
|
+
print(net) # print the topological components
|
|
48
|
+
|
|
49
|
+
# Network Topology:
|
|
50
|
+
# (node) layer[0]
|
|
51
|
+
# (node) layer[1]
|
|
52
|
+
# (edge) dense: (node) layer[0] -> (node) layer[1]
|
|
53
|
+
# (port) inp <- (no link)
|
|
54
|
+
# (list) out
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Networks
|
|
58
|
+
|
|
59
|
+
It is possible and encouraged to develop subclasses of network for organization and code reuse. This allows you more easily to use networks in the construction of nested structures.
|
|
60
|
+
|
|
61
|
+
The definition of a network is split into two main methods:
|
|
62
|
+
- `__init__()`: this is where node ports and any relevant network parameters are provided
|
|
63
|
+
- `build()`: this is where topological components are assigned (potentially procedurally)
|
|
64
|
+
|
|
65
|
+
The use of node ports additionally allows for variably sized networks that are determined at build time. During the automated iterative and recursive build process, any dependencies that were generated by unsized node ports in the network initialization method are resolved before the build method is called.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
class Linear(Network):
|
|
69
|
+
def __init__(self, size=64):
|
|
70
|
+
super().__init__() # base class initialization (required)
|
|
71
|
+
self.size = size # user-defined network parameters
|
|
72
|
+
self.inp = NodePort() # unsized node port (generates dependency)
|
|
73
|
+
self.ctrl = NodePort(1) # sized node port (no dependency)
|
|
74
|
+
|
|
75
|
+
def build(self):
|
|
76
|
+
# Layers (using supplied parameters)
|
|
77
|
+
self.layer = NodeGroup(LIF(), self.size)
|
|
78
|
+
|
|
79
|
+
# Procedurally generating edges (using resolved port information)
|
|
80
|
+
edges = [(i,j) for i,j in itertools.product(
|
|
81
|
+
range(self.inp.size),range(self.layer.size))]
|
|
82
|
+
|
|
83
|
+
# Connections (using node port placeholder)
|
|
84
|
+
self.dense = EdgeGroup(self.inp, self.layer, PSP(), edges=edges)
|
|
85
|
+
|
|
86
|
+
return
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Port size resolution is generally performed by binding/linking a node port with a sized node group/list (from elsewhere in the network topology), or by manually specifying its size. In the case that node port dependencies cannot be resolved (e.g. for cyclic structures), the user will need to provide some additional information to break the dependency chain.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
net = Network() # instantiate network
|
|
93
|
+
net.inp = NodeGroup(IN(), 12) # add an input node group
|
|
94
|
+
net.ff = [Linear(32), # add a list of networks
|
|
95
|
+
Linear(10)]
|
|
96
|
+
# bind topological components to ports (using dot notation)
|
|
97
|
+
net.bind(net.inp, net.ff[0].inp)
|
|
98
|
+
net.bind(net.ff[0].layer, net.ff[1].inp)
|
|
99
|
+
net.build() # build network topology
|
|
100
|
+
print(net) # print the topological components
|
|
101
|
+
|
|
102
|
+
# Network Topology:
|
|
103
|
+
# (node) inp
|
|
104
|
+
# (port) ff[0].inp <- (node) inp
|
|
105
|
+
# (node) ff[0].layer
|
|
106
|
+
# (edge) ff[0].dense: (port) ff[0].inp <- (node) inp -> (node) ff[0].layer
|
|
107
|
+
# (port) ff[1].inp <- (node) ff[0].layer
|
|
108
|
+
# (node) ff[1].layer
|
|
109
|
+
# (edge) ff[1].dense: (port) ff[1].inp <- (node) ff[0].layer -> (node) ff[1].layer
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Nodes and Edges
|
|
113
|
+
The various node and edge models (LIF and PSP in the example, respectively) are defined as Python dataclasses, where each attribute may be assigned a default value for initialization of state variables (e.g. as ints, floats). The use of tuples indicate shared parameters with respect to a node group.
|
|
114
|
+
|
|
115
|
+
When a node/edge group is defined, a node/edge model instance is provided (allowing it to have different defaults than in the class definition by providing keyword arguments). Data within a node/edge group are stored as numpy arrays to take advantage of its shared memory property (so that changing parameters at either the group or individual node/edge level will be reflected coherently). These can be accessed using node/edge model attribute name at the group level.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
@dataclass
|
|
119
|
+
class LIF(Neuron):
|
|
120
|
+
model: str = 'LIF' # model name
|
|
121
|
+
voltage: float = 0.0 # individual parameter
|
|
122
|
+
threshold: float = 1.0
|
|
123
|
+
reset: float = 0.0, # shared parameter (note: this is for illustration, the
|
|
124
|
+
leak: float = 1.0 # basic LIF model provided by Sango has
|
|
125
|
+
# 'reset' as an individual parameter)
|
|
126
|
+
|
|
127
|
+
res = NodeGroup(LIF(threshold=0.9), # model parameter default
|
|
128
|
+
size=3, # node group size
|
|
129
|
+
voltage=0.6, # bulk parameter assignment
|
|
130
|
+
leak=[0.5, 0.4, 0.3]) # individual parameter assignment
|
|
131
|
+
res.reset = 0.1 # shared parameter assignment
|
|
132
|
+
res[1].voltage = 0.8 # individual node parameter assignment
|
|
133
|
+
|
|
134
|
+
print(res.model) # ['LIF']
|
|
135
|
+
print(res.voltage) # [0.6 0.8 0.6]
|
|
136
|
+
print(res.threshold) # [0.9 0.9 0.9]
|
|
137
|
+
print(res.reset) # [(0.1,)]
|
|
138
|
+
print(res.leak) # [0.5 0.4 0.3]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
@dataclass
|
|
143
|
+
class PSP(Synapse):
|
|
144
|
+
model: str = 'PSP'
|
|
145
|
+
delay: float = 1.0
|
|
146
|
+
weight: float = 1.0
|
|
147
|
+
|
|
148
|
+
rr = EdgeGroup(res, res, PSP(delay=2.0), # model parameter default
|
|
149
|
+
edges=[(0,1), (1,2), (2,0)], # list of directed tuples (source, target)
|
|
150
|
+
weight=[1.0, 2.0, 3.0]) # individual parameter assignment
|
|
151
|
+
rr[(1,2)].delay = 3.0 # individual edge parameter assignment (by tuple)
|
|
152
|
+
rr[0].weight = 1.5 # individual edge parameter assignment (by index)
|
|
153
|
+
|
|
154
|
+
print(rr.delay) # [2.0 3.0 2.0]
|
|
155
|
+
print(rr.weight) # [1.5 2.0 3.0]
|
|
156
|
+
|
|
157
|
+
# Edge groups have a mapping from directed tuples to linear indexes
|
|
158
|
+
print(rr.edge_map) # {(0, 1): 0, (1, 2): 1, (2, 0): 2}
|
|
159
|
+
print(rr.target_index) # [1, 2, 0]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Node groups require a node model, and are preferably also instantiated with a size (as opposed to incrementally adding nodes). By default, if no size is provided, a single node will be instantiated.
|
|
163
|
+
|
|
164
|
+
Edge groups require an edge model and source/target nodes (which may be node groups, node lists, or node ports), and are preferably also instantiated with a list of edges. This is a list of tuples of pairs of source/target indexes that are local with respect to the source/target sets of nodes, respectively. By default, if no edge list is provided, an edge with the source/target pair of (0,0) will be instantiated.
|
|
165
|
+
|
|
166
|
+
## Simulation
|
|
167
|
+
In addition to defining networks, it is often useful to simulate the constructed network. There is currently some simulation support provided through the Brian 2 and STACS spiking neural network simulators (installed separately). Here, Sango may be thought of as the "frontend" interface to the "backend" simulator (or potentially neuromorphic hardware platform). This decoupling between frontend and backend is important for maintaining flexibility in the high-level network descriptions and for portability to different low-level network implementations.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# Import the desired simulator backend
|
|
171
|
+
from sango.backend import SimBrian
|
|
172
|
+
|
|
173
|
+
sim = SimBrian(net) # Pass the built network to the backend translation layer
|
|
174
|
+
sim.compile() # Convert the network onto the backend execution model
|
|
175
|
+
sim.run(10.0) # Simulate the network (arguments may be backend-specific)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Custom user inputs into a network may be provided through input node models. In particular, there is a simple spike generator input model that takes a list of spike times (which specifies when the node emits spikes). This can be wrapped in a node group to provide a list of input nodes, and further wrapped in a network for compositional reuse.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
class Input(Network):
|
|
182
|
+
def __init__(self, spike_times):
|
|
183
|
+
super().__init__()
|
|
184
|
+
self.spike_times = spike_times # number of nodes x lists of times
|
|
185
|
+
|
|
186
|
+
def build(self):
|
|
187
|
+
# Spike generator
|
|
188
|
+
self.spikegen = NodeGroup(IN(), len(self.spike_times), times=self.spike_times)
|
|
189
|
+
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
# Define spiking inputs as a list of lists
|
|
193
|
+
input_vec = [[2, 4, 5], # 0,1,1,0,1,0,0 (represented as bit strings,
|
|
194
|
+
[0, 1, 4, 6]] # 1,0,1,0,0,1,1 least significant bit first)
|
|
195
|
+
|
|
196
|
+
net = Network() # instantiate network
|
|
197
|
+
net.inp = Input(spike_times=input_vec) # add an input network
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Simulation outputs are similarly provided as a spike list of times (per node in the network). The mapping between a node in Sango and the corresponding node in the backend is provided through a "node map" which is generated during the compilation process. There are also some convenience functions to plot the resulting spike raster. Additional features, such as recording state variables, depend on the backend that is used.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
spike_list = sim.get_spikes() # Get the spike list for each node
|
|
204
|
+
node_index = sim.node_map['inp.spikegen[1]'] # Find the index of a node by name
|
|
205
|
+
inp1_spike = spike_list[node_index] # Extract the node spike times
|
|
206
|
+
|
|
207
|
+
# Plot the spike raster (this uses matplotlib's eventplot)
|
|
208
|
+
sim.plot_spikes(tick_names=True)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Sango currently provides a method for converting its network object into a NetworkX directed graph for ease of translation. Nodes and edges are simply identified by their flattened "path name", and any associated data are provided as additional attributes. While the network descriptions in Sango are fairly flexible and open-ended, it does not prescribe node/edge model dynamics or how those computations should be implemented. Their translation with respect to a backend simulator is mediated through a "model registry" which provides the necessary information for mapping models. This is also intended to provide a degree of extensibility for custom user models.
|
|
212
|
+
|
|
213
|
+
There is existing backend support for these basic node and edge models: LIF (leaky integrate-and-fire neuron model), PSP (post-synaptic potential synapse model), and IN (simple spike generator input model). There is also support for probabilistic spiking: pLIF (probabilistic LIF neuron model, which is used in Fugu). Support for additional model types may require updating the model registry.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sango"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Compositional Spiking Neural Network Domain Specific Language"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Felix Wang", email = "felwang@sandia.gov" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy>=1.19.3",
|
|
17
|
+
"networkx>=2.4"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/sandialabs/Sango"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
sango-1.0.0/setup.cfg
ADDED
|
File without changes
|