sinabs 3.0.4.dev25__py3-none-any.whl → 3.1.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.
Files changed (48) hide show
  1. sinabs/activation/reset_mechanism.py +3 -3
  2. sinabs/activation/surrogate_gradient_fn.py +4 -4
  3. sinabs/backend/dynapcnn/__init__.py +5 -4
  4. sinabs/backend/dynapcnn/chip_factory.py +33 -61
  5. sinabs/backend/dynapcnn/chips/dynapcnn.py +182 -86
  6. sinabs/backend/dynapcnn/chips/speck2e.py +6 -5
  7. sinabs/backend/dynapcnn/chips/speck2f.py +6 -5
  8. sinabs/backend/dynapcnn/config_builder.py +39 -59
  9. sinabs/backend/dynapcnn/connectivity_specs.py +48 -0
  10. sinabs/backend/dynapcnn/discretize.py +91 -155
  11. sinabs/backend/dynapcnn/dvs_layer.py +59 -101
  12. sinabs/backend/dynapcnn/dynapcnn_layer.py +185 -119
  13. sinabs/backend/dynapcnn/dynapcnn_layer_utils.py +335 -0
  14. sinabs/backend/dynapcnn/dynapcnn_network.py +602 -325
  15. sinabs/backend/dynapcnn/dynapcnnnetwork_module.py +370 -0
  16. sinabs/backend/dynapcnn/exceptions.py +122 -3
  17. sinabs/backend/dynapcnn/io.py +51 -91
  18. sinabs/backend/dynapcnn/mapping.py +111 -75
  19. sinabs/backend/dynapcnn/nir_graph_extractor.py +877 -0
  20. sinabs/backend/dynapcnn/sinabs_edges_handler.py +1024 -0
  21. sinabs/backend/dynapcnn/utils.py +214 -459
  22. sinabs/backend/dynapcnn/weight_rescaling_methods.py +53 -0
  23. sinabs/conversion.py +2 -2
  24. sinabs/from_torch.py +23 -1
  25. sinabs/hooks.py +38 -41
  26. sinabs/layers/alif.py +16 -16
  27. sinabs/layers/crop2d.py +2 -2
  28. sinabs/layers/exp_leak.py +1 -1
  29. sinabs/layers/iaf.py +11 -11
  30. sinabs/layers/lif.py +9 -9
  31. sinabs/layers/neuromorphic_relu.py +9 -8
  32. sinabs/layers/pool2d.py +5 -5
  33. sinabs/layers/quantize.py +1 -1
  34. sinabs/layers/stateful_layer.py +10 -7
  35. sinabs/layers/to_spike.py +9 -9
  36. sinabs/network.py +14 -12
  37. sinabs/synopcounter.py +10 -7
  38. sinabs/utils.py +155 -7
  39. sinabs/validate_memory_speck.py +0 -5
  40. {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/METADATA +2 -1
  41. sinabs-3.1.0.dist-info/RECORD +65 -0
  42. {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/licenses/AUTHORS +1 -0
  43. sinabs-3.1.0.dist-info/pbr.json +1 -0
  44. sinabs-3.0.4.dev25.dist-info/RECORD +0 -59
  45. sinabs-3.0.4.dev25.dist-info/pbr.json +0 -1
  46. {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/WHEEL +0 -0
  47. {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/licenses/LICENSE +0 -0
  48. {sinabs-3.0.4.dev25.dist-info → sinabs-3.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1024 @@
1
+ """
2
+ Implements the pre-processing of edges into blocks of nodes (modules) for future
3
+ creation of DynapcnnLayer objects.
4
+ """
5
+
6
+ from typing import Deque, Dict, List, Optional, Set, Tuple, Type, Union
7
+
8
+ from torch import Size, nn
9
+
10
+ from sinabs.layers import SumPool2d
11
+
12
+ from .connectivity_specs import VALID_SINABS_EDGE_TYPES
13
+ from .crop2d import Crop2d
14
+ from .dvs_layer import DVSLayer
15
+ from .exceptions import (
16
+ InvalidEdge,
17
+ InvalidGraphStructure,
18
+ default_invalid_structure_string,
19
+ )
20
+ from .flipdims import FlipDims
21
+ from .utils import Edge, merge_bn
22
+
23
+
24
+ def remap_edges_after_drop(
25
+ dropped_node: int, source_of_dropped_node: int, edges: Set[Edge]
26
+ ) -> Set[Edge]:
27
+ """Creates a new set of edges from `edges`. All edges where `dropped_node`
28
+ is the source node will be used to generate a new edge where
29
+ `source_of_dropped_node` becomes the source node (the target is kept).
30
+
31
+ Args:
32
+ dropped_node (int):
33
+ source_of_dropped_node (int):
34
+ edges (set): tuples describing the connections between layers in
35
+ `spiking_model`.
36
+
37
+ Returns:
38
+ New set of edges with `source_of_dropped_node` as the source node where
39
+ `dropped_node` used to be.
40
+ """
41
+ remapped_edges = set()
42
+
43
+ for src, tgt in edges:
44
+ if src == dropped_node:
45
+ remapped_edges.add((source_of_dropped_node, tgt))
46
+
47
+ return remapped_edges
48
+
49
+
50
+ def handle_batchnorm_nodes(
51
+ edges: Set[Edge],
52
+ indx_2_module_map: Dict[int, nn.Module],
53
+ name_2_indx_map: Dict[str, int],
54
+ ) -> None:
55
+ """Merges `BatchNorm2d`/`BatchNorm1d` layers into `Conv2d`/`Linear` ones.
56
+ The batch norm nodes will be removed from the graph (by updating all
57
+ variables passed as arguments in-place) after their properties are used to
58
+ re-scale the weights of the convolutional/linear layers associated with
59
+ batch normalization via the `weight-batchnorm` edges found in the original
60
+ graph.
61
+
62
+ Args:
63
+ edges (set): tuples describing the connections between layers in
64
+ `spiking_model`.
65
+ indx_2_module_map (dict): the mapping between a node (`key` as an `int`)
66
+ and its module (`value` as a `nn.Module`).
67
+ name_2_indx_map (dict): Map from node names to unique indices.
68
+ """
69
+
70
+ # Gather indexes of the BatchNorm2d/BatchNorm1d nodes.
71
+ bnorm_nodes = {
72
+ index
73
+ for index, module in indx_2_module_map.items()
74
+ if isinstance(module, (nn.BatchNorm2d, nn.BatchNorm1d))
75
+ }
76
+
77
+ if len(bnorm_nodes) == 0:
78
+ # There are no edges with batch norm - nothing to do here.
79
+ return
80
+
81
+ # Find weight-bnorm edges.
82
+ weight_bnorm_edges = {
83
+ (src, tgt)
84
+ for (src, tgt) in edges
85
+ if (
86
+ isinstance(indx_2_module_map[src], nn.Conv2d)
87
+ and isinstance(indx_2_module_map[tgt], nn.BatchNorm2d)
88
+ )
89
+ or (
90
+ isinstance(indx_2_module_map[src], nn.Linear)
91
+ and isinstance(indx_2_module_map[tgt], nn.BatchNorm1d)
92
+ )
93
+ }
94
+
95
+ # Merge conv/linear and bnorm layers using 'weight-bnorm' edges.
96
+ for edge in weight_bnorm_edges:
97
+ bnorm = indx_2_module_map[edge[1]]
98
+ weight = indx_2_module_map[edge[0]]
99
+
100
+ # merge and update weight node.
101
+ indx_2_module_map[edge[0]] = merge_bn(weight, bnorm)
102
+
103
+ # Point weight nodes to the targets of their respective batch norm nodes.
104
+ new_edges = set()
105
+ for weight_id, bnorm_id in weight_bnorm_edges:
106
+ new_edges.update(
107
+ remap_edges_after_drop(
108
+ dropped_node=bnorm_id, source_of_dropped_node=weight_id, edges=edges
109
+ )
110
+ )
111
+ # Remove all edges to and from a batch norm node and replace with new edges
112
+ bnorm_edges = {e for e in edges if bnorm_nodes.intersection(e)}
113
+ edges.difference_update(bnorm_edges)
114
+ edges.update(new_edges)
115
+
116
+ # Remove references to the bnorm node.
117
+ for idx in bnorm_nodes:
118
+ indx_2_module_map.pop(idx)
119
+
120
+ for name in [name for name, indx in name_2_indx_map.items() if indx in bnorm_nodes]:
121
+ name_2_indx_map.pop(name)
122
+
123
+
124
+ def fix_dvs_module_edges(
125
+ edges: Set[Edge],
126
+ indx_2_module_map: Dict[int, nn.Module],
127
+ name_2_indx_map: Dict[str, int],
128
+ entry_nodes: Set[Edge],
129
+ ) -> None:
130
+ """All arguments are modified in-place to fix wrong node extractions from
131
+ NIRtorch when a DVSLayer istance is the first layer in the network.
132
+ Modifies `edges` to re-structure the edges related witht the DVSLayer
133
+ instance. The DVSLayer's forward method feeds data in the sequence
134
+ 'DVS -> DVS.pool -> DVS.crop -> DVS.flip', so we remove edges involving
135
+ these nodes (that are internaly implementend in the DVSLayer) from the
136
+ graph and point the node of DVSLayer to the node where it should send its
137
+ output to. This is also removes a self-recurrent node with edge '(FlipDims,
138
+ FlipDims)' that is wrongly extracted.
139
+ Modifies `indx_2_module_map` and `name_2_indx_map` to remove the internal
140
+ DVSLayer nodes (Crop2d, FlipDims and DVSLayer's pooling) since these should
141
+ not be independent nodes in the graph.
142
+ Modifies `entry_nodes` such that the DVSLayer becomes the only entry node
143
+ of the graph.
144
+
145
+ This fix is to imply there's something odd with the extracted adges for the
146
+ forward pass implemented by the DVSLayer. For now this function is fixing
147
+ these edges to have them representing the information flow through this
148
+ layer as **it should be** but the graph tracing of NIR should be looked
149
+ into to solve the root problem.
150
+
151
+ Args:
152
+ edges (set): tuples describing the connections between layers in
153
+ `spiking_model`.
154
+ indx_2_module_map (dict): the mapping between a node (`key` as an `int`)
155
+ and its module (`value` as a `nn.Module`).
156
+ name_2_indx_map (dict): Map from node names to unique indices.
157
+ entry_nodes (set): IDs of nodes acting as entry points for the network
158
+ (i.e., receiving external input).
159
+ """
160
+
161
+ # spot nodes (ie, modules) used in a DVSLayer instance's forward pass (including the DVSLayer node itself).
162
+ dvslayer_nodes = {
163
+ index: module
164
+ for index, module in indx_2_module_map.items()
165
+ if any(
166
+ isinstance(module, dvs_node) for dvs_node in (DVSLayer, Crop2d, FlipDims)
167
+ )
168
+ }
169
+
170
+ if len(dvslayer_nodes) <= 1:
171
+ # No module within the DVSLayer instance appears as an independent node - nothing to do here.
172
+ return
173
+
174
+ # TODO - a `SumPool2d` is also a node that's used inside a DVSLayer instance. In what follows we try to find it
175
+ # by looking for pooling nodes that appear in a (pool, crop) edge - the assumption being that if the pooling is
176
+ # inputing into a crop layer than the pool is inside the DVSLayer instance. It feels like a hacky way to do it
177
+ # so we should revise this.
178
+ dvslayer_nodes.update(
179
+ {
180
+ edge[0]: indx_2_module_map[edge[0]]
181
+ for edge in edges
182
+ if isinstance(indx_2_module_map[edge[0]], SumPool2d)
183
+ and isinstance(indx_2_module_map[edge[1]], Crop2d)
184
+ }
185
+ )
186
+
187
+ # NIR is extracting an edge (FlipDims, FlipDims) from the DVSLayer: remove self-recurrent nodes from the graph.
188
+ for edge in [
189
+ (src, tgt)
190
+ for (src, tgt) in edges
191
+ if (src == tgt and isinstance(indx_2_module_map[src], FlipDims))
192
+ ]:
193
+ edges.remove(edge)
194
+
195
+ # Since NIR is not extracting the edges for the DVSLayer correctly, remove all edges involving the DVS.
196
+ for edge in [
197
+ (src, tgt)
198
+ for (src, tgt) in edges
199
+ if (src in dvslayer_nodes or tgt in dvslayer_nodes)
200
+ ]:
201
+ edges.remove(edge)
202
+
203
+ # Get node's indexes based on the module type - just for validation.
204
+ dvs_node = [
205
+ key for key, value in dvslayer_nodes.items() if isinstance(value, DVSLayer)
206
+ ]
207
+ dvs_pool_node = [
208
+ key for key, value in dvslayer_nodes.items() if isinstance(value, SumPool2d)
209
+ ]
210
+ dvs_crop_node = [
211
+ key for key, value in dvslayer_nodes.items() if isinstance(value, Crop2d)
212
+ ]
213
+ dvs_flip_node = [
214
+ key for key, value in dvslayer_nodes.items() if isinstance(value, FlipDims)
215
+ ]
216
+
217
+ if any(
218
+ len(node) > 1
219
+ for node in [dvs_node, dvs_pool_node, dvs_crop_node, dvs_flip_node]
220
+ ):
221
+ raise ValueError(
222
+ f"Internal DVS nodes should be single instances but multiple have been found: dvs_node: {len(dvs_node)} dvs_pool_node: {len(dvs_pool_node)} dvs_crop_node: {len(dvs_crop_node)} dvs_flip_node: {len(dvs_flip_node)}"
223
+ )
224
+
225
+ # Remove dvs_pool, dvs_crop and dvs_flip nodes from `indx_2_module_map` (these operate within the DVS, not as independent nodes of the final graph).
226
+ indx_2_module_map.pop(dvs_pool_node[-1])
227
+ indx_2_module_map.pop(dvs_crop_node[-1])
228
+ indx_2_module_map.pop(dvs_flip_node[-1])
229
+
230
+ # Remove internal DVS modules from name/index map.
231
+ # Iterate over copy to prevent iterable from changing size.
232
+ n2i_map_copy = {k: v for k, v in name_2_indx_map.items()}
233
+ for name, index in n2i_map_copy.items():
234
+ if index in [dvs_pool_node[-1], dvs_crop_node[-1], dvs_flip_node[-1]]:
235
+ name_2_indx_map.pop(name)
236
+
237
+ dvs_node = dvs_node[0]
238
+ if edges:
239
+ # Add edges from 'dvs' node to the entry point of the graph.
240
+ all_sources, all_targets = zip(*edges)
241
+ local_entry_nodes = set(all_sources) - set(all_targets)
242
+ edges.update({(dvs_node, node) for node in local_entry_nodes})
243
+
244
+ # DVS becomes the only entry node of the graph.
245
+ entry_nodes.clear()
246
+ entry_nodes.add(dvs_node)
247
+
248
+
249
+ def collect_dynapcnn_layer_info(
250
+ indx_2_module_map: Dict[int, nn.Module],
251
+ edges: Set[Edge],
252
+ nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
253
+ entry_nodes: Set[int],
254
+ ) -> Tuple[Dict[int, Dict], Union[Dict, None]]:
255
+ """Collect information to construct DynapcnnLayer instances.
256
+
257
+ Validate and sort edges based on the type of nodes they connect.
258
+ Iterate over edges in order of their type. For each neuron->weight edge
259
+ generate a new dict to collect information for the corresponding dynapcnn
260
+ layer. Then add pooling based on neuron->pooling type edges. Collect
261
+ additional pooling from pooling->pooling type edges. Finally set layer
262
+ destinations based on neuron/pooling->weight type of edges.
263
+
264
+ Args:
265
+ indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
266
+ associated module as `value`.
267
+ edges (set of tuples): Represent connections between two nodes in
268
+ computational graph.
269
+ nodes_io_shapes (dict): Map from node ID to dict containing node's in-
270
+ and output shapes.
271
+ entry_nodes (set of int): IDs of nodes that receive external input.
272
+
273
+ Returns:
274
+ A tuple with a dictionary where each 'key' is the index of a future
275
+ 'DynapcnnLayer' and 'value' is a dictionary, with keys 'conv',
276
+ 'neuron', and 'destinations', containing corresponding node ids and
277
+ modules required to build the layer, and if a DVSLayer is part of the
278
+ network, another dictionary containing the layer itself and its
279
+ destination indices. If not, None.
280
+ """
281
+
282
+ # Sort edges by edge type (type of layers they connect)
283
+ edges_by_type: Dict[str, Set[Edge]] = sort_edges_by_type(
284
+ edges=edges, indx_2_module_map=indx_2_module_map
285
+ )
286
+ edge_counts_by_type = {t: len(e) for t, e in edges_by_type.items()}
287
+
288
+ # Dict to collect information for each future dynapcnn layer
289
+ dynapcnn_layer_info = dict()
290
+ # Map node IDs to dynapcnn layer ID
291
+ node_2_layer_map = dict()
292
+
293
+ # Each weight->neuron connection instantiates a new, unique dynapcnn layer
294
+ weight_neuron_edges = edges_by_type.get("weight-neuron", set())
295
+ while weight_neuron_edges:
296
+ edge = weight_neuron_edges.pop()
297
+ init_new_dynapcnnlayer_entry(
298
+ dynapcnn_layer_info,
299
+ edge,
300
+ indx_2_module_map,
301
+ nodes_io_shapes,
302
+ node_2_layer_map,
303
+ entry_nodes,
304
+ )
305
+
306
+ # Process all edges related to DVS layer
307
+ dvs_layer_info = dvs_setup(
308
+ edges_by_type, indx_2_module_map, node_2_layer_map, nodes_io_shapes
309
+ )
310
+
311
+ # Process all edges connecting two dynapcnn layers that do not include pooling
312
+ neuron_weight_edges = edges_by_type.get("neuron-weight", set())
313
+ while neuron_weight_edges:
314
+ edge = neuron_weight_edges.pop()
315
+ set_neuron_layer_destination(
316
+ dynapcnn_layer_info,
317
+ edge,
318
+ node_2_layer_map,
319
+ nodes_io_shapes,
320
+ indx_2_module_map,
321
+ )
322
+
323
+ # Add pooling based on neuron->pooling connections
324
+ pooling_pooling_edges = edges_by_type.get("pooling-pooling", set())
325
+ neuron_pooling_edges = edges_by_type.get("neuron-pooling", set())
326
+ while neuron_pooling_edges:
327
+ edge = neuron_pooling_edges.pop()
328
+ # Search pooling-pooling edges for chains of pooling and add to existing entry
329
+ pooling_chains, edges_used = trace_paths(edge[1], pooling_pooling_edges)
330
+ add_pooling_to_entry(
331
+ dynapcnn_layer_info,
332
+ edge,
333
+ pooling_chains,
334
+ indx_2_module_map,
335
+ node_2_layer_map,
336
+ )
337
+ # Remove handled pooling-pooling edges
338
+ pooling_pooling_edges.difference_update(edges_used)
339
+
340
+ # After adding pooling make sure all pooling-pooling edges have been handled
341
+ if len(pooling_pooling_edges) > 0:
342
+ unmatched_layers = {edge[0] for edge in pooling_pooling_edges}
343
+ raise InvalidGraphStructure(
344
+ f"Pooling layers {unmatched_layers} could not be assigned to a "
345
+ "DynapCNN layer. This is likely due to an unsupported SNN "
346
+ "architecture. Pooling layers must always be preceded by a "
347
+ "spiking layer (`IAFSqueeze`), another pooling layer, or"
348
+ "DVS input"
349
+ )
350
+
351
+ # Add all edges connecting pooling to a new dynapcnn layer
352
+ pooling_weight_edges = edges_by_type.get("pooling-weight", set())
353
+ while pooling_weight_edges:
354
+ edge = pooling_weight_edges.pop()
355
+ set_pooling_layer_destination(
356
+ dynapcnn_layer_info,
357
+ edge,
358
+ node_2_layer_map,
359
+ nodes_io_shapes,
360
+ indx_2_module_map,
361
+ )
362
+
363
+ # Make sure we have taken care of all edges
364
+ assert all(len(edges) == 0 for edges in edges_by_type.values())
365
+
366
+ # Set minimal destination entries for layers without child nodes, to act as network outputs
367
+ set_exit_destinations(dynapcnn_layer_info)
368
+
369
+ # Assert formal correctness of layer info
370
+ verify_layer_info(dynapcnn_layer_info, edge_counts_by_type)
371
+
372
+ return dynapcnn_layer_info, dvs_layer_info
373
+
374
+
375
+ def get_valid_edge_type(
376
+ edge: Edge,
377
+ layers: Dict[int, nn.Module],
378
+ valid_edge_ids: Dict[Tuple[Type, Type], int],
379
+ ) -> int:
380
+ """Checks if the modules each node in 'edge' represent are a valid
381
+ connection between a sinabs network to be loaded on Speck and return the
382
+ edge type.
383
+
384
+ Args:
385
+ edge (tuple of two int): The edge whose type is to be inferred.
386
+ layers (Dict): Dict with node IDs as keys and layer instances as values.
387
+ valid_edge_ids: Dict with valid edge-types (tuples of Types) as keys
388
+ and edge-type-ID as value
389
+
390
+ Returns:
391
+ The edge type specified in 'valid_edges_map' ('None' if edge is not valid).
392
+ """
393
+ source_type = type(layers[edge[0]])
394
+ target_type = type(layers[edge[1]])
395
+
396
+ return valid_edge_ids.get((source_type, target_type), None)
397
+
398
+
399
+ def sort_edges_by_type(
400
+ edges: Set[Edge], indx_2_module_map: Dict[int, Type]
401
+ ) -> Dict[str, Set[Edge]]:
402
+ """Sort edges by the type of nodes they connect
403
+
404
+ Args:
405
+ edges (set of tuples): Represent connections between two nodes in
406
+ computational graph.
407
+ indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
408
+ associated module as `value`.
409
+
410
+ Returns:
411
+ Dictionary with possible keys "weight-neuron", "neuron-weight",
412
+ "neuron-pooling", "pooling-pooling", and "pooling-weight".
413
+ Values are sets of edges corresponding to these types.
414
+ """
415
+ edges_by_type: Dict[str, Set[Edge]] = dict()
416
+
417
+ for edge in edges:
418
+ edge_type = get_valid_edge_type(
419
+ edge, indx_2_module_map, VALID_SINABS_EDGE_TYPES
420
+ )
421
+
422
+ # Validate edge type
423
+ if edge_type is None:
424
+ raise InvalidEdge(
425
+ edge, type(indx_2_module_map[edge[0]]), type(indx_2_module_map[edge[1]])
426
+ )
427
+
428
+ if edge_type in edges_by_type:
429
+ edges_by_type[edge_type].add(edge)
430
+ else:
431
+ edges_by_type[edge_type] = {edge}
432
+
433
+ return edges_by_type
434
+
435
+
436
+ def init_new_dynapcnnlayer_entry(
437
+ dynapcnn_layer_info: Dict[int, Dict[int, Dict]],
438
+ edge: Edge,
439
+ indx_2_module_map: Dict[int, nn.Module],
440
+ nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
441
+ node_2_layer_map: Dict[int, int],
442
+ entry_nodes: Set[int],
443
+ ) -> None:
444
+ """Initiate dict to hold information for new dynapcnn layer based on a
445
+ "weight->neuron" edge. Change `dynapcnn_layer_info` in-place.
446
+
447
+ Args:
448
+ dynapcnn_layer_info: Dict with one entry for each future dynapcnn
449
+ layer. Key is unique dynapcnn layer ID, value is dict with nodes of
450
+ the layer will be updated in-place.
451
+ edge: Tuple of 2 integers, indicating edge between two nodes in graph.
452
+ Edge source has to be within an existing entry of
453
+ `dynapcnn_layer_info`.
454
+ indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
455
+ associated module as `value`.
456
+ nodes_io_shapes (dict): Map from node ID to dict containing node's in-
457
+ and output shapes.
458
+ node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
459
+ assigned to. Will be updated in-place.
460
+ entry_nodes (set of int): IDs of nodes that receive external input.
461
+ """
462
+ # Make sure there are no existing entries holding any of the modules connected by `edge`
463
+ assert edge[0] not in node_2_layer_map
464
+ assert edge[1] not in node_2_layer_map
465
+
466
+ # Take current length of the dict as new, unique ID
467
+ layer_id = len(dynapcnn_layer_info)
468
+ assert layer_id not in dynapcnn_layer_info
469
+
470
+ dynapcnn_layer_info[layer_id] = {
471
+ "input_shape": nodes_io_shapes[edge[0]]["input"],
472
+ "conv": {
473
+ "module": indx_2_module_map[edge[0]],
474
+ "node_id": edge[0],
475
+ },
476
+ "neuron": {
477
+ "module": indx_2_module_map[edge[1]],
478
+ "node_id": edge[1],
479
+ },
480
+ # This will be used later to account for average pooling in preceding layers
481
+ "rescale_factors": set(),
482
+ "is_entry_node": edge[0] in entry_nodes,
483
+ # Will be populated by `set_[pooling/neuron]_layer_destination`
484
+ "destinations": [],
485
+ }
486
+ node_2_layer_map[edge[0]] = layer_id
487
+ node_2_layer_map[edge[1]] = layer_id
488
+
489
+
490
+ def add_pooling_to_entry(
491
+ dynapcnn_layer_info: Dict[int, Dict],
492
+ edge: Edge,
493
+ pooling_chains: List[Deque[int]],
494
+ indx_2_module_map: Dict[int, nn.Module],
495
+ node_2_layer_map: Dict[int, int],
496
+ ) -> None:
497
+ """Add or extend destination information with pooling for existing entry in
498
+ `dynapcnn_layer_info`.
499
+
500
+ Correct entry is identified by existing neuron node. Destination information
501
+ is a dict containing list of IDs and list of modules for each chains of
502
+ pooling nodes.
503
+
504
+ Args:
505
+ dynapcnn_layer_info: Dict with one entry for each future DynapCNN layer.
506
+ Key is unique DynapCNN layer ID, value is dict with nodes of the
507
+ layer. Will be updated in-place.
508
+ edge: Tuple of 2 integers, indicating edge between a neuron node and the
509
+ pooling node that starts all provided `pooling_chains`. Edge source
510
+ has to be a neuron node within an existing entry of
511
+ `dynapcnn_layer_info`, i.e. it has to have been processed already.
512
+ pooling_chains: List of deque of int. All sequences ("chains") of
513
+ connected pooling nodes, starting from edge[1].
514
+ indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
515
+ associated module as `value`.
516
+ node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
517
+ assigned to. Will be updated in-place.
518
+ """
519
+ # Find layer containing edge[0]
520
+ try:
521
+ layer_idx = node_2_layer_map[edge[0]]
522
+ except KeyError:
523
+ neuron_layer = indx_2_module_map[edge[0]]
524
+ raise InvalidGraphStructure(
525
+ f"Spiking layer {neuron_layer} cannot be assigned to a DynapCNN layer. "
526
+ "This is likely due to an unsupported SNN architecture. Spiking "
527
+ "layers have to be preceded by a weight layer (`nn.Conv2d` or "
528
+ "`nn.Linear`)."
529
+ )
530
+ # Make sure all pooling chains start with expected node
531
+ assert all(chain[0] == edge[1] for chain in pooling_chains)
532
+
533
+ # Keep track of all nodes that have been added
534
+ new_nodes = set()
535
+
536
+ # For each pooling chain initialize new destination
537
+ layer_info = dynapcnn_layer_info[layer_idx]
538
+ for chain in pooling_chains:
539
+ layer_info["destinations"].append(
540
+ {
541
+ "pooling_ids": chain,
542
+ "pooling_modules": [indx_2_module_map[idx] for idx in chain],
543
+ # Setting `destination_layer` to `None` allows for this layer
544
+ # to act as network exit point if not destination is added later
545
+ "destination_layer": None,
546
+ }
547
+ )
548
+ new_nodes.update(set(chain))
549
+
550
+ for node in new_nodes:
551
+ # Make sure new pooling nodes have not been used elsewhere
552
+ assert node not in node_2_layer_map
553
+ node_2_layer_map[node] = layer_idx
554
+
555
+
556
+ def dvs_setup(
557
+ edges_by_type: Dict[str, Set[Edge]],
558
+ indx_2_module_map: Dict[int, nn.Module],
559
+ node_2_layer_map: Dict[int, int],
560
+ nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
561
+ ) -> Union[None, Dict]:
562
+ """Generate dict containing information to set up DVS layer.
563
+
564
+ Args:
565
+ edges_by_type (dict of sets of edges): Keys are edge types (str),
566
+ values are sets of edges.
567
+ indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
568
+ associated module as `value`.
569
+ node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
570
+ assigned to. Will be updated in-place.
571
+ nodes_io_shapes (dict): Map from node ID to dict containing node's in-
572
+ and output shapes
573
+
574
+ Returns:
575
+ Dictionary containing information about the DVSLayer.
576
+ """
577
+ # Process all outgoing edges of a DVSLayer
578
+ dvs_weight_edges = edges_by_type.get("dvs-weight", set())
579
+ dvs_pooling_edges = edges_by_type.get("dvs-pooling", set())
580
+
581
+ # Process all dvs->weight edges connecting the DVS camera to a dynapcnn layer.
582
+ if dvs_weight_edges:
583
+ if dvs_pooling_edges:
584
+ raise InvalidGraphStructure(
585
+ "DVS layer has destinations with and without pooling. Unlike "
586
+ "with CNN layers, pooling of the DVS has to be the same for "
587
+ "all destinations."
588
+ )
589
+ return init_dvs_entry(
590
+ dvs_weight_edges,
591
+ indx_2_module_map,
592
+ node_2_layer_map,
593
+ nodes_io_shapes,
594
+ )
595
+
596
+ # Process dvs->pooling edges adding pooling to a DVS Layer
597
+ elif dvs_pooling_edges:
598
+ # Make sure there is exactly one dvs->pooling edge
599
+ if len(dvs_pooling_edges) > 1:
600
+ raise InvalidGraphStructure(
601
+ "DVSLayer has connects to multiple pooling layers. Unlike "
602
+ "with CNN layers, pooling of the DVS has to be the same for "
603
+ "all destinations, therefore the DVSLayer can connect to at "
604
+ "most one pooling layer."
605
+ )
606
+ dvs_pooling_edge = dvs_pooling_edges.pop()
607
+ # Find pooling-weight edges that connect DVS layer to dynapcnn layers.
608
+ pooling_weight_edges = edges_by_type.get("pooling-weight", set())
609
+ dvs_pooling_weight_edges = find_edges_by_source(
610
+ pooling_weight_edges, dvs_pooling_edge[1]
611
+ )
612
+ # Remove handled pooling-weight edges
613
+ pooling_weight_edges.difference_update(dvs_pooling_weight_edges)
614
+
615
+ return init_dvs_entry_with_pooling(
616
+ dvs_pooling_edge,
617
+ dvs_pooling_weight_edges,
618
+ indx_2_module_map,
619
+ node_2_layer_map,
620
+ nodes_io_shapes,
621
+ )
622
+ else:
623
+ # If no edges related to DVS have been found return None
624
+ return
625
+
626
+
627
+ def init_dvs_entry(
628
+ dvs_weight_edges: Set[Edge],
629
+ indx_2_module_map: Dict[int, nn.Module],
630
+ node_2_layer_map: Dict[int, int],
631
+ nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
632
+ ) -> Dict:
633
+ """Initiate dict to hold information for a DVS Layer configuration
634
+ based on "dvs-weight" edges.
635
+
636
+ Args:
637
+ dvs_weight_edges: Set of edges between two nodes in graph.
638
+ Edge source has to be a DVSLayer and the same for all edges.
639
+ Edge target has to be within an existing entry of
640
+ `dynapcnn_layer_info`.
641
+ indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
642
+ associated module as `value`.
643
+ node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
644
+ assigned to. Will be updated in-place.
645
+ nodes_io_shapes (dict): Map from node ID to dict containing node's in-
646
+ and output shapes
647
+
648
+ Returns:
649
+ Dictionary containing information about the DVSLayer.
650
+ """
651
+
652
+ # Pick any of the edges in set to get the DVS node ID. Should be same for all.
653
+ dvs_node_id = next(dvs_weight_edges.__iter__())[0]
654
+
655
+ # This should never fail
656
+ if not all(edge[0] == dvs_node_id for edge in dvs_weight_edges):
657
+ raise InvalidGraphStructure(
658
+ "The provided network seems to consist of multiple DVS layers. "
659
+ "This is not supported."
660
+ )
661
+ assert isinstance(
662
+ (dvs_layer := indx_2_module_map[dvs_node_id]), DVSLayer
663
+ ), f"Source node in edges {dvs_weight_edges} is of type {type(dvs_layer)} (it should be a DVSLayer instance)."
664
+
665
+ # Initialize dvs config dict
666
+ dvs_layer_info = {
667
+ "node_id": dvs_node_id,
668
+ "input_shape": nodes_io_shapes[dvs_node_id]["input"],
669
+ "module": dvs_layer,
670
+ "pooling": None,
671
+ }
672
+ node_2_layer_map[dvs_node_id] = "dvs"
673
+
674
+ # Find destination layer indices
675
+ destinations = []
676
+ while dvs_weight_edges:
677
+ edge = dvs_weight_edges.pop()
678
+ try:
679
+ destination_layer_idx = node_2_layer_map[edge[1]]
680
+ except KeyError:
681
+ weight_layer = indx_2_module_map[edge[1]]
682
+ raise InvalidGraphStructure(
683
+ f"Weight layer {weight_layer} cannot be assigned to a DynapCNN layer. "
684
+ "This is likely due to an unsupported SNN architecture. Weight "
685
+ "layers have to be followed by a spiking layer (`sl.IAFSqueeze`)."
686
+ )
687
+
688
+ # Update entry for DVS with new destination.
689
+ assert destination_layer_idx not in destinations
690
+ destinations.append(destination_layer_idx)
691
+
692
+ if destinations:
693
+ dvs_layer_info["destinations"] = destinations
694
+ else:
695
+ dvs_layer_info["destinations"] = None
696
+
697
+ return dvs_layer_info
698
+
699
+
700
+ def init_dvs_entry_with_pooling(
701
+ dvs_pooling_edge: Edge,
702
+ pooling_weight_edges: Set[Edge],
703
+ indx_2_module_map: Dict[int, nn.Module],
704
+ node_2_layer_map: Dict[int, int],
705
+ nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
706
+ ) -> Dict:
707
+ """Initiate dict to hold information for a DVS Layer configuration with additional pooling
708
+
709
+ Args:
710
+ dvs_pooling_edge: Edge from DVSLayer to pooling layer.
711
+ pooling_weight_edges: Set of edges between pooling layer and weight
712
+ layer. Edge source has to be the target of `dvs_pooling_edge`.
713
+ Edge targets have to be within an existing entry of
714
+ `dynapcnn_layer_info`.
715
+ indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
716
+ associated module as `value`.
717
+ node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
718
+ assigned to. Will be updated in-place.
719
+ nodes_io_shapes (dict): Map from node ID to dict containing node's in-
720
+ and output shapes
721
+
722
+ Returns:
723
+ Dictionary containing information about the DVSLayer.
724
+ """
725
+
726
+ dvs_node_id, pooling_id = dvs_pooling_edge
727
+
728
+ # This should never fail
729
+ assert all(edge[0] == pooling_id for edge in pooling_weight_edges)
730
+ assert isinstance(
731
+ (dvs_layer := indx_2_module_map[dvs_node_id]), DVSLayer
732
+ ), f"Source node in edge {dvs_pooling_edge} is of type {type(dvs_layer)} (it should be a DVSLayer instance)."
733
+
734
+ # Initialize dvs config dict
735
+ dvs_layer_info = {
736
+ "node_id": dvs_node_id,
737
+ "input_shape": nodes_io_shapes[dvs_node_id]["input"],
738
+ "module": dvs_layer,
739
+ "pooling": {"module": indx_2_module_map[pooling_id], "node_id": pooling_id},
740
+ }
741
+ node_2_layer_map[dvs_node_id] = "dvs"
742
+
743
+ # Find destination layer indices
744
+ destinations = []
745
+ for edge in pooling_weight_edges:
746
+ try:
747
+ destination_layer_idx = node_2_layer_map[edge[1]]
748
+ except KeyError:
749
+ weight_layer = indx_2_module_map[edge[1]]
750
+ raise InvalidGraphStructure(
751
+ f"Weight layer {weight_layer} cannot be assigned to a DynapCNN layer. "
752
+ "This is likely due to an unsupported SNN architecture. Weight "
753
+ "layers have to be followed by a spiking layer (`sl.IAFSqueeze`)."
754
+ )
755
+
756
+ # Update entry for DVS with new destination.
757
+ assert destination_layer_idx not in destinations
758
+ destinations.append(destination_layer_idx)
759
+
760
+ if destinations:
761
+ dvs_layer_info["destinations"] = destinations
762
+ else:
763
+ dvs_layer_info["destinations"] = None
764
+
765
+ return dvs_layer_info
766
+
767
+
768
+ def set_exit_destinations(dynapcnn_layer: Dict) -> None:
769
+ """Set minimal destination entries for layers that don't have any.
770
+
771
+ This ensures that the forward methods of the resulting DynapcnnLayer
772
+ instances return an output, letting these layers act as exit points
773
+ of the network.
774
+ The destination layer will be `None`, and no pooling applied.
775
+
776
+ Args:
777
+ dynapcnn_layer_info: Dict with one entry for each future dynapcnn layer.
778
+ Key is unique dynapcnn layer ID, value is dict with nodes of the
779
+ layer. Will be updated in-place.
780
+ """
781
+ for layer_info in dynapcnn_layer.values():
782
+ if not (destinations := layer_info["destinations"]):
783
+ # Add `None` destination to empty destination lists
784
+ destinations.append(
785
+ {
786
+ "pooling_ids": [],
787
+ "pooling_modules": [],
788
+ "destination_layer": None,
789
+ }
790
+ )
791
+
792
+
793
+ def set_neuron_layer_destination(
794
+ dynapcnn_layer_info: Dict[int, Dict],
795
+ edge: Edge,
796
+ node_2_layer_map: Dict[int, int],
797
+ nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
798
+ indx_2_module_map: Dict[int, nn.Module],
799
+ ) -> None:
800
+ """Set destination layer without pooling for existing entry in `dynapcnn_layer_info`.
801
+
802
+ Args:
803
+ dynapcnn_layer_info: Dict with one entry for each future DynapCNN layer.
804
+ Key is unique DynapCNN layer ID, value is dict with nodes of the
805
+ layer. Will be updated in-place.
806
+ edge: Tuple of 2 integers, indicating edge between two nodes in graph.
807
+ Edge source has to be a neuron layer within an existing entry of
808
+ `dynapcnn_layer_info`. Edge target has to be the weight layer of
809
+ another DynapCNN layer.
810
+ node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
811
+ assigned to. Will be updated in-place.
812
+ nodes_io_shapes (dict): Map from node ID to dict containing node's in-
813
+ and output shapes
814
+ indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
815
+ associated module as `value`
816
+ """
817
+ # Make sure both source (neuron layer) and target (weight layer) have been previously processed
818
+ try:
819
+ source_layer_idx = node_2_layer_map[edge[0]]
820
+ except KeyError:
821
+ neuron_layer = indx_2_module_map[edge[0]]
822
+ raise InvalidGraphStructure(
823
+ f"Spiking layer {neuron_layer} cannot be assigned to a DynapCNN layer. "
824
+ "This is likely due to an unsupported SNN architecture. Spiking "
825
+ "layers have to be preceded by a weight layer (`nn.Conv2d` or "
826
+ "`nn.Linear`)."
827
+ )
828
+ try:
829
+ destination_layer_idx = node_2_layer_map[edge[1]]
830
+ except KeyError:
831
+ weight_layer = indx_2_module_map[edge[1]]
832
+ raise InvalidGraphStructure(
833
+ f"Weight layer {weight_layer} cannot be assigned to a DynapCNN layer. "
834
+ "This is likely due to an unsupported SNN architecture. Weight "
835
+ "layers have to be followed by a spiking layer (`IAFSqueeze`)."
836
+ )
837
+
838
+ # Add new destination
839
+ output_shape = nodes_io_shapes[edge[0]]["output"]
840
+ layer_info = dynapcnn_layer_info[source_layer_idx]
841
+ layer_info["destinations"].append(
842
+ {
843
+ "pooling_ids": [],
844
+ "pooling_modules": [],
845
+ "destination_layer": destination_layer_idx,
846
+ "output_shape": output_shape,
847
+ }
848
+ )
849
+
850
+
851
+ def set_pooling_layer_destination(
852
+ dynapcnn_layer_info: Dict[int, Dict],
853
+ edge: Edge,
854
+ node_2_layer_map: Dict[int, int],
855
+ nodes_io_shapes: Dict[int, Dict[str, Tuple[Size, Size]]],
856
+ indx_2_module_map: Dict[int, nn.Module],
857
+ ) -> None:
858
+ """Set destination layer with pooling for existing entry in `dynapcnn_layer_info`.
859
+
860
+ Args:
861
+ dynapcnn_layer_info: Dict with one entry for each future DynapCNN layer.
862
+ Key is unique DynapCNN layer ID, value is dict with nodes of the
863
+ layer. Will be updated in-place.
864
+ edge: Tuple of 2 integers, indicating edge between two nodes in graph.
865
+ Edge source has to be a pooling layer that is at the end of at
866
+ least one pooling chain within an existing entry of
867
+ `dynapcnn_layer_info`. Edge target has to be a weight layer within
868
+ an existing entry of `dynapcnn_layer_info`.
869
+ node_2_layer_map (dict): Maps each node ID to the ID of the layer it is
870
+ assigned to. Will be updated in-place.
871
+ nodes_io_shapes (dict): Map from node ID to dict containing node's in-
872
+ and output shapes
873
+ indx_2_module_map (dict): Maps node IDs of the graph as `key` to their
874
+ associated module as `value`
875
+ """
876
+ # Make sure both source (pooling layer) and target (weight layer) have been previously processed
877
+ try:
878
+ source_layer_idx = node_2_layer_map[edge[0]]
879
+ except KeyError:
880
+ poolin_layer = indx_2_module_map[edge[0]]
881
+ raise InvalidGraphStructure(
882
+ f"Layer {poolin_layer} cannot be assigned to a DynapCNN layer. "
883
+ "This is likely due to an unsupported SNN architecture. Pooling "
884
+ "layers have to be preceded by a spiking layer (`IAFSqueeze`), "
885
+ "another pooling layer, or DVS input"
886
+ )
887
+ try:
888
+ destination_layer_idx = node_2_layer_map[edge[1]]
889
+ except KeyError:
890
+ weight_layer = indx_2_module_map[edge[1]]
891
+ raise InvalidGraphStructure(
892
+ f"Weight layer {weight_layer} cannot be assigned to a DynapCNN layer. "
893
+ "This is likely due to an unsupported SNN architecture. Weight "
894
+ "layers have to be preceded by a spiking layer (`IAFSqueeze`), "
895
+ "another pooling layer, or DVS input"
896
+ )
897
+
898
+ # Find current source node within destinations
899
+ layer_info = dynapcnn_layer_info[source_layer_idx]
900
+ matched = False
901
+ for destination in layer_info["destinations"]:
902
+ if destination["pooling_ids"][-1] == edge[0]:
903
+ if destination["destination_layer"] is not None:
904
+ # Destination is already linked to a postsynaptic layer. This happens when
905
+ # pooling nodes have outgoing edges to different weight layer.
906
+ # Copy the destination
907
+ # TODO: Add unit test for this case
908
+ destination = {k: v for k, v in destination.items()}
909
+ layer_info["destinations"].append(destination)
910
+ matched = True
911
+ break
912
+ if not matched:
913
+ pooling_layer = indx_2_module_map[edge[0]]
914
+ raise InvalidGraphStructure(
915
+ f"Layer {pooling_layer} cannot be assigned to a DynapCNN layer. "
916
+ "This is likely due to an unsupported SNN architecture. Pooling "
917
+ "layers have to be preceded by a spiking layer (`IAFSqueeze`), "
918
+ "another pooling layer, or DVS input"
919
+ )
920
+
921
+ # Set destination layer within destination dict that holds current source node
922
+ destination["destination_layer"] = destination_layer_idx
923
+ output_shape = nodes_io_shapes[edge[0]]["output"]
924
+ destination["output_shape"] = output_shape
925
+
926
+
927
+ def trace_paths(node: int, remaining_edges: Set[Edge]) -> List[Deque[int]]:
928
+ """Trace any path of collected edges through the graph.
929
+
930
+ Start with `node`, and recursively look for paths of connected nodes
931
+ within `remaining edges.`
932
+
933
+ Args:
934
+ node (int): ID of current node.
935
+ remaining_edges: Set of remaining edges still to be searched.
936
+
937
+ Returns:
938
+ List of deque of int, all paths of connected edges starting from `node`
939
+ and a set of edges that are part of the returned paths.
940
+ """
941
+ paths = []
942
+ processed_edges = set()
943
+ for src, tgt in remaining_edges:
944
+ if src == node:
945
+ processed_edges.add((src, tgt))
946
+ # For each edge with `node` as source, find subsequent pooling nodes recursively
947
+ new_remaining = remaining_edges.difference({(src, tgt)})
948
+ branches, new_processed = trace_paths(tgt, new_remaining)
949
+ # Make sure no edge was processed twice
950
+ assert len(processed_edges.intersection(new_processed)) == 0
951
+
952
+ # Keep track of newly processed edges
953
+ processed_edges.update(new_processed)
954
+
955
+ # Collect all branching paths of pooling, inserting src at beginning
956
+ for branch in branches:
957
+ branch.appendleft(src)
958
+ paths.append(branch)
959
+
960
+ if not paths:
961
+ # End of recursion: instantiate a deque only with node
962
+ paths = [Deque([node])]
963
+
964
+ return paths, processed_edges
965
+
966
+
967
+ def find_edges_by_source(edges: Set[Edge], source: int) -> Set[Edge]:
968
+ """Utility function to find all edges with a given source node.
969
+
970
+ Args:
971
+ edges: Set of `Edge` instances to be searched.
972
+ source (int): Node ID that returned edges should have as source.
973
+
974
+ Returns:
975
+ All sets from `edges` that have `source` as source.
976
+ """
977
+ return {(src, tgt) for (src, tgt) in edges if src == source}
978
+
979
+
980
+ def verify_layer_info(
981
+ dynapcnn_layer_info: Dict[int, Dict], edge_counts: Optional[Dict[str, int]] = None
982
+ ):
983
+ """Verify that `dynapcnn_layer_info` matches formal requirements.
984
+
985
+ Every layer needs to have at least a `conv`, `neuron`, and `destinations`
986
+ entry.
987
+ If `edge_counts` is provided, also make sure that number of layer matches
988
+ numbers of edges.
989
+
990
+ Args:
991
+ dynapcnn_layer_info: Dict with information to construct and connect
992
+ DynapcnnLayer instances
993
+ edge_counts: Optional Dict with edge counts for each edge type. If not
994
+ `None`, will be used to do further verifications on
995
+ `dynapcnn_layer_info`.
996
+
997
+ Raises:
998
+ InvalidGraphStructure: if any verification fails.
999
+ """
1000
+
1001
+ # Make sure that each dynapcnn layer has at least a weight layer and a neuron layer
1002
+ for idx, info in dynapcnn_layer_info.items():
1003
+ if not "conv" in info:
1004
+ raise InvalidGraphStructure(
1005
+ f"DynapCNN layer {idx} has no weight assigned. "
1006
+ + default_invalid_structure_string
1007
+ )
1008
+ if not "neuron" in info:
1009
+ raise InvalidGraphStructure(
1010
+ f"DynapCNN layer {idx} has no spiking layer assigned. "
1011
+ + default_invalid_structure_string
1012
+ )
1013
+ if not "destinations" in info:
1014
+ raise InvalidGraphStructure(
1015
+ f"DynapCNN layer {idx} has no destination info assigned. "
1016
+ + default_invalid_structure_string
1017
+ )
1018
+ if edge_counts is not None:
1019
+ # Make sure there are as many layers as edges from weight to neuron
1020
+ if edge_counts.get("weight-neuron", 0) - len(dynapcnn_layer_info) > 0:
1021
+ raise InvalidGraphStructure(
1022
+ "Not all weight-to-neuron edges have been processed. "
1023
+ + default_invalid_structure_string
1024
+ )