superneuroabm 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.
- superneuroabm/__init__.py +3 -0
- superneuroabm/component_base_config.yaml +129 -0
- superneuroabm/io/__init__.py +3 -0
- superneuroabm/io/nx.py +425 -0
- superneuroabm/io/synthetic_networks.py +770 -0
- superneuroabm/model.py +689 -0
- superneuroabm/step_functions/soma/izh.py +86 -0
- superneuroabm/step_functions/soma/lif.py +98 -0
- superneuroabm/step_functions/soma/lif_soma_adaptive_thr.py +111 -0
- superneuroabm/step_functions/synapse/single_exp.py +71 -0
- superneuroabm/step_functions/synapse/stdp/Low_resolution_synapse.py +117 -0
- superneuroabm/step_functions/synapse/stdp/Three-bit_exp_pair_wise.py +130 -0
- superneuroabm/step_functions/synapse/stdp/Three_bit_exp_pair_wise.py +133 -0
- superneuroabm/step_functions/synapse/stdp/exp_pair_wise_stdp.py +119 -0
- superneuroabm/step_functions/synapse/stdp/learning_rule_selector.py +72 -0
- superneuroabm/step_functions/synapse/util.py +49 -0
- superneuroabm/util.py +38 -0
- superneuroabm-1.0.0.dist-info/METADATA +100 -0
- superneuroabm-1.0.0.dist-info/RECORD +22 -0
- superneuroabm-1.0.0.dist-info/WHEEL +5 -0
- superneuroabm-1.0.0.dist-info/licenses/LICENSE +28 -0
- superneuroabm-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Synthetic network generation for weak scaling tests.
|
|
3
|
+
|
|
4
|
+
This module provides functions to generate spiking neural networks that are
|
|
5
|
+
optimized for METIS partitioning, ensuring:
|
|
6
|
+
1. Balanced agent distribution across workers
|
|
7
|
+
2. Minimal cross-worker communication
|
|
8
|
+
3. Similar computational load per worker
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import networkx as nx
|
|
12
|
+
import numpy as np
|
|
13
|
+
from typing import Optional, Dict, Tuple, List
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_clustered_network(
|
|
17
|
+
num_clusters: int,
|
|
18
|
+
neurons_per_cluster: int,
|
|
19
|
+
intra_cluster_prob: float = 0.3,
|
|
20
|
+
inter_cluster_prob: float = 0.01,
|
|
21
|
+
external_input_prob: float = 0.2,
|
|
22
|
+
soma_breed: str = "lif_soma",
|
|
23
|
+
soma_config: str = "config_0",
|
|
24
|
+
synapse_breed: str = "single_exp_synapse",
|
|
25
|
+
synapse_config: str = "no_learning_config_0",
|
|
26
|
+
excitatory_ratio: float = 0.8,
|
|
27
|
+
weight_exc: float = 14.0,
|
|
28
|
+
weight_inh: float = -10.0,
|
|
29
|
+
seed: Optional[int] = None
|
|
30
|
+
) -> nx.DiGraph:
|
|
31
|
+
"""
|
|
32
|
+
Generate a clustered spiking neural network optimized for METIS partitioning.
|
|
33
|
+
|
|
34
|
+
This creates multiple clusters with high intra-cluster connectivity and
|
|
35
|
+
low inter-cluster connectivity, which METIS can efficiently partition.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
num_clusters: Number of clusters (should match number of workers)
|
|
39
|
+
neurons_per_cluster: Number of neurons in each cluster
|
|
40
|
+
intra_cluster_prob: Connection probability within cluster (default: 0.3)
|
|
41
|
+
inter_cluster_prob: Connection probability between clusters (default: 0.01)
|
|
42
|
+
external_input_prob: Probability of external input per neuron (default: 0.2)
|
|
43
|
+
soma_breed: Neuron type ("lif_soma" or "izh_soma")
|
|
44
|
+
soma_config: Configuration name for somas
|
|
45
|
+
synapse_breed: Synapse type
|
|
46
|
+
synapse_config: Configuration name for synapses
|
|
47
|
+
excitatory_ratio: Ratio of excitatory neurons (default: 0.8)
|
|
48
|
+
weight_exc: Weight for excitatory synapses (default: 14.0)
|
|
49
|
+
weight_inh: Weight for inhibitory synapses (default: -10.0)
|
|
50
|
+
seed: Random seed for reproducibility
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
NetworkX DiGraph with neuron and synapse attributes
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> # Create network for 4 workers with 1000 neurons each
|
|
57
|
+
>>> graph = generate_clustered_network(
|
|
58
|
+
... num_clusters=4,
|
|
59
|
+
... neurons_per_cluster=1000,
|
|
60
|
+
... seed=42
|
|
61
|
+
... )
|
|
62
|
+
>>> # Total neurons: 4000, optimized for 4 workers
|
|
63
|
+
"""
|
|
64
|
+
if seed is not None:
|
|
65
|
+
np.random.seed(seed)
|
|
66
|
+
|
|
67
|
+
graph = nx.DiGraph()
|
|
68
|
+
total_neurons = num_clusters * neurons_per_cluster
|
|
69
|
+
|
|
70
|
+
print(f"[SyntheticNet] Generating clustered network:")
|
|
71
|
+
print(f" - Clusters: {num_clusters}")
|
|
72
|
+
print(f" - Neurons per cluster: {neurons_per_cluster}")
|
|
73
|
+
print(f" - Total neurons: {total_neurons}")
|
|
74
|
+
print(f" - Intra-cluster p: {intra_cluster_prob}")
|
|
75
|
+
print(f" - Inter-cluster p: {inter_cluster_prob}")
|
|
76
|
+
|
|
77
|
+
# Create neurons organized by cluster
|
|
78
|
+
neuron_ids = []
|
|
79
|
+
for cluster_id in range(num_clusters):
|
|
80
|
+
cluster_neurons = []
|
|
81
|
+
for i in range(neurons_per_cluster):
|
|
82
|
+
neuron_id = cluster_id * neurons_per_cluster + i
|
|
83
|
+
|
|
84
|
+
# Determine if excitatory or inhibitory
|
|
85
|
+
is_excitatory = i < int(neurons_per_cluster * excitatory_ratio)
|
|
86
|
+
|
|
87
|
+
# Add neuron node
|
|
88
|
+
graph.add_node(
|
|
89
|
+
neuron_id,
|
|
90
|
+
soma_breed=soma_breed,
|
|
91
|
+
config=soma_config,
|
|
92
|
+
cluster=cluster_id, # Store cluster ID for analysis
|
|
93
|
+
type="excitatory" if is_excitatory else "inhibitory",
|
|
94
|
+
tags=[f"cluster_{cluster_id}"]
|
|
95
|
+
)
|
|
96
|
+
cluster_neurons.append(neuron_id)
|
|
97
|
+
|
|
98
|
+
neuron_ids.append(cluster_neurons)
|
|
99
|
+
|
|
100
|
+
# Add intra-cluster connections
|
|
101
|
+
intra_edges = 0
|
|
102
|
+
for cluster_id, cluster_neurons in enumerate(neuron_ids):
|
|
103
|
+
for pre in cluster_neurons:
|
|
104
|
+
for post in cluster_neurons:
|
|
105
|
+
if pre != post and np.random.random() < intra_cluster_prob:
|
|
106
|
+
# Get neuron types
|
|
107
|
+
pre_type = graph.nodes[pre]["type"]
|
|
108
|
+
weight = weight_exc if pre_type == "excitatory" else weight_inh
|
|
109
|
+
|
|
110
|
+
graph.add_edge(
|
|
111
|
+
pre,
|
|
112
|
+
post,
|
|
113
|
+
synapse_breed=synapse_breed,
|
|
114
|
+
config=synapse_config,
|
|
115
|
+
overrides={"hyperparameters": {"weight": weight}},
|
|
116
|
+
connection_type="intra_cluster"
|
|
117
|
+
)
|
|
118
|
+
intra_edges += 1
|
|
119
|
+
|
|
120
|
+
# Add inter-cluster connections
|
|
121
|
+
inter_edges = 0
|
|
122
|
+
for cluster_i in range(num_clusters):
|
|
123
|
+
for cluster_j in range(num_clusters):
|
|
124
|
+
if cluster_i != cluster_j:
|
|
125
|
+
for pre in neuron_ids[cluster_i]:
|
|
126
|
+
for post in neuron_ids[cluster_j]:
|
|
127
|
+
if np.random.random() < inter_cluster_prob:
|
|
128
|
+
pre_type = graph.nodes[pre]["type"]
|
|
129
|
+
weight = weight_exc if pre_type == "excitatory" else weight_inh
|
|
130
|
+
|
|
131
|
+
graph.add_edge(
|
|
132
|
+
pre,
|
|
133
|
+
post,
|
|
134
|
+
synapse_breed=synapse_breed,
|
|
135
|
+
config=synapse_config,
|
|
136
|
+
overrides={"hyperparameters": {"weight": weight}},
|
|
137
|
+
connection_type="inter_cluster"
|
|
138
|
+
)
|
|
139
|
+
inter_edges += 1
|
|
140
|
+
|
|
141
|
+
# Add external inputs
|
|
142
|
+
external_inputs = 0
|
|
143
|
+
for cluster_id, cluster_neurons in enumerate(neuron_ids):
|
|
144
|
+
for post in cluster_neurons:
|
|
145
|
+
if np.random.random() < external_input_prob:
|
|
146
|
+
graph.add_edge(
|
|
147
|
+
-1, # External input
|
|
148
|
+
post,
|
|
149
|
+
synapse_breed=synapse_breed,
|
|
150
|
+
config=synapse_config,
|
|
151
|
+
overrides={"hyperparameters": {"weight": weight_exc}},
|
|
152
|
+
connection_type="external",
|
|
153
|
+
tags=["input_synapse"],
|
|
154
|
+
cluster=cluster_id # Assign to post-synaptic neuron's cluster
|
|
155
|
+
)
|
|
156
|
+
external_inputs += 1
|
|
157
|
+
|
|
158
|
+
# Print statistics
|
|
159
|
+
total_edges = intra_edges + inter_edges
|
|
160
|
+
theoretical_edge_cut = inter_edges / total_edges if total_edges > 0 else 0
|
|
161
|
+
|
|
162
|
+
print(f"[SyntheticNet] Network statistics:")
|
|
163
|
+
print(f" - Total edges: {total_edges}")
|
|
164
|
+
print(f" - Intra-cluster edges: {intra_edges} ({100*intra_edges/total_edges:.1f}%)")
|
|
165
|
+
print(f" - Inter-cluster edges: {inter_edges} ({100*inter_edges/total_edges:.1f}%)")
|
|
166
|
+
print(f" - External inputs: {external_inputs}")
|
|
167
|
+
print(f" - Theoretical edge cut (with perfect partition): {theoretical_edge_cut:.4f}")
|
|
168
|
+
|
|
169
|
+
return graph
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def generate_clustered_network_constant_comm(
|
|
173
|
+
num_clusters: int,
|
|
174
|
+
neurons_per_cluster: int,
|
|
175
|
+
intra_cluster_prob: Optional[float] = None,
|
|
176
|
+
intra_cluster_degree: Optional[int] = None,
|
|
177
|
+
cross_cluster_edges: int = 2000,
|
|
178
|
+
num_neighbor_clusters: Optional[int] = None,
|
|
179
|
+
external_input_prob: float = 0.2,
|
|
180
|
+
soma_breed: str = "lif_soma",
|
|
181
|
+
soma_config: str = "config_0",
|
|
182
|
+
synapse_breed: str = "single_exp_synapse",
|
|
183
|
+
synapse_config: str = "no_learning_config_0",
|
|
184
|
+
excitatory_ratio: float = 0.8,
|
|
185
|
+
weight_exc: float = 14.0,
|
|
186
|
+
weight_inh: float = -10.0,
|
|
187
|
+
seed: Optional[int] = None
|
|
188
|
+
) -> nx.DiGraph:
|
|
189
|
+
"""
|
|
190
|
+
Generate a clustered network for PROPER WEAK SCALING with constant per-worker work.
|
|
191
|
+
|
|
192
|
+
This function creates networks suitable for weak scaling tests where:
|
|
193
|
+
1. Per-worker workload remains constant as workers scale (linear scaling)
|
|
194
|
+
2. Per-worker communication remains constant (truly constant communication)
|
|
195
|
+
3. Per-worker contextualization overhead remains constant
|
|
196
|
+
|
|
197
|
+
IMPORTANT: For proper weak scaling, use intra_cluster_degree (NOT intra_cluster_prob):
|
|
198
|
+
- intra_cluster_degree: Each neuron connects to a FIXED number of neurons (O(n) edges per worker)
|
|
199
|
+
- intra_cluster_prob: Each neuron connects with probability p (O(n²) edges per worker - NOT weak scaling!)
|
|
200
|
+
|
|
201
|
+
CRITICAL: Use num_neighbor_clusters to control contextualization overhead:
|
|
202
|
+
- If None (default), each cluster connects to ALL other clusters (NOT TRUE weak scaling!)
|
|
203
|
+
- If specified, each cluster forms BIDIRECTIONAL pairs with K neighbors
|
|
204
|
+
- Recommended: Set to 1 for minimal constant cross-cluster communication
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
num_clusters: Number of clusters (should match number of workers)
|
|
208
|
+
neurons_per_cluster: Number of neurons in each cluster
|
|
209
|
+
intra_cluster_prob: Connection probability within cluster (creates O(n²) edges - NOT for weak scaling!)
|
|
210
|
+
intra_cluster_degree: Average degree per neuron (creates O(n) edges - PROPER weak scaling!)
|
|
211
|
+
cross_cluster_edges: Edges in EACH direction for bidirectional pairs (default: 2000)
|
|
212
|
+
num_neighbor_clusters: Number of bidirectional neighbor pairs (None = all neighbors)
|
|
213
|
+
external_input_prob: Probability of external input per neuron (default: 0.2)
|
|
214
|
+
soma_breed: Neuron type ("lif_soma" or "izh_soma")
|
|
215
|
+
soma_config: Configuration name for somas
|
|
216
|
+
synapse_breed: Synapse type
|
|
217
|
+
synapse_config: Configuration name for synapses
|
|
218
|
+
excitatory_ratio: Ratio of excitatory neurons (default: 0.8)
|
|
219
|
+
weight_exc: Weight for excitatory synapses (default: 14.0)
|
|
220
|
+
weight_inh: Weight for inhibitory synapses (default: -10.0)
|
|
221
|
+
seed: Random seed for reproducibility
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
NetworkX DiGraph with neuron and synapse attributes
|
|
225
|
+
|
|
226
|
+
Example (PROPER weak scaling):
|
|
227
|
+
>>> # Create network with constant degree (linear edge count)
|
|
228
|
+
>>> graph = generate_clustered_network_constant_comm(
|
|
229
|
+
... num_clusters=4,
|
|
230
|
+
... neurons_per_cluster=10000,
|
|
231
|
+
... intra_cluster_degree=10, # Each neuron connects to 10 others
|
|
232
|
+
... cross_cluster_edges=2000,
|
|
233
|
+
... seed=42
|
|
234
|
+
... )
|
|
235
|
+
>>> # Per worker: 10,000 neurons × 10 degree = 100,000 edges (linear!)
|
|
236
|
+
"""
|
|
237
|
+
# Validate parameters
|
|
238
|
+
if intra_cluster_prob is None and intra_cluster_degree is None:
|
|
239
|
+
raise ValueError("Must specify either intra_cluster_prob or intra_cluster_degree")
|
|
240
|
+
if intra_cluster_prob is not None and intra_cluster_degree is not None:
|
|
241
|
+
raise ValueError("Cannot specify both intra_cluster_prob and intra_cluster_degree")
|
|
242
|
+
|
|
243
|
+
# Set default for num_neighbor_clusters if not specified
|
|
244
|
+
if num_neighbor_clusters is None:
|
|
245
|
+
# Default: connect to all other clusters (backward compatibility but NOT true weak scaling)
|
|
246
|
+
num_neighbor_clusters = num_clusters - 1 if num_clusters > 1 else 0
|
|
247
|
+
print(f"[Warning] num_neighbor_clusters not specified, using all-to-all ({num_neighbor_clusters} neighbors). For TRUE weak scaling, set num_neighbor_clusters=1.")
|
|
248
|
+
else:
|
|
249
|
+
# Validate num_neighbor_clusters
|
|
250
|
+
max_neighbors = num_clusters - 1
|
|
251
|
+
if num_neighbor_clusters > max_neighbors:
|
|
252
|
+
print(f"[Warning] num_neighbor_clusters ({num_neighbor_clusters}) > max possible ({max_neighbors}). Using {max_neighbors}.")
|
|
253
|
+
num_neighbor_clusters = max_neighbors
|
|
254
|
+
|
|
255
|
+
if seed is not None:
|
|
256
|
+
np.random.seed(seed)
|
|
257
|
+
|
|
258
|
+
graph = nx.DiGraph()
|
|
259
|
+
total_neurons = num_clusters * neurons_per_cluster
|
|
260
|
+
|
|
261
|
+
print(f"[SyntheticNet] Generating clustered network for WEAK SCALING:")
|
|
262
|
+
print(f" - Clusters: {num_clusters}")
|
|
263
|
+
print(f" - Neurons per cluster: {neurons_per_cluster}")
|
|
264
|
+
print(f" - Total neurons: {total_neurons}")
|
|
265
|
+
|
|
266
|
+
if intra_cluster_degree is not None:
|
|
267
|
+
print(f" - Intra-cluster degree: {intra_cluster_degree} edges/neuron (O(n) edges - PROPER weak scaling!)")
|
|
268
|
+
expected_intra_edges = num_clusters * neurons_per_cluster * intra_cluster_degree
|
|
269
|
+
print(f" - Expected intra-cluster edges: {expected_intra_edges:,}")
|
|
270
|
+
else:
|
|
271
|
+
print(f" - Intra-cluster prob: {intra_cluster_prob} (O(n²) edges - NOT proper weak scaling!)")
|
|
272
|
+
expected_intra_edges = int(num_clusters * neurons_per_cluster * (neurons_per_cluster - 1) * intra_cluster_prob)
|
|
273
|
+
print(f" - Expected intra-cluster edges: {expected_intra_edges:,}")
|
|
274
|
+
|
|
275
|
+
print(f" - Neighbor clusters per worker: {num_neighbor_clusters} (DIRECTED RING)")
|
|
276
|
+
print(f" - Cross-cluster edges per neighbor: {cross_cluster_edges}")
|
|
277
|
+
# With directed ring, each cluster sends cross_cluster_edges to K neighbors
|
|
278
|
+
total_cross_edges_per_worker = cross_cluster_edges * num_neighbor_clusters if num_clusters > 1 else 0
|
|
279
|
+
print(f" - Total cross-cluster edges per worker (outgoing): {total_cross_edges_per_worker} (constant!)")
|
|
280
|
+
if num_clusters > 1:
|
|
281
|
+
print(f" - Each worker also receives {total_cross_edges_per_worker} edges from {num_neighbor_clusters} sender(s)")
|
|
282
|
+
|
|
283
|
+
# Create neurons organized by cluster
|
|
284
|
+
neuron_ids = []
|
|
285
|
+
for cluster_id in range(num_clusters):
|
|
286
|
+
cluster_neurons = []
|
|
287
|
+
for i in range(neurons_per_cluster):
|
|
288
|
+
neuron_id = cluster_id * neurons_per_cluster + i
|
|
289
|
+
|
|
290
|
+
# Determine if excitatory or inhibitory
|
|
291
|
+
is_excitatory = i < int(neurons_per_cluster * excitatory_ratio)
|
|
292
|
+
|
|
293
|
+
# Add neuron node
|
|
294
|
+
graph.add_node(
|
|
295
|
+
neuron_id,
|
|
296
|
+
soma_breed=soma_breed,
|
|
297
|
+
config=soma_config,
|
|
298
|
+
cluster=cluster_id,
|
|
299
|
+
type="excitatory" if is_excitatory else "inhibitory",
|
|
300
|
+
tags=[f"cluster_{cluster_id}"]
|
|
301
|
+
)
|
|
302
|
+
cluster_neurons.append(neuron_id)
|
|
303
|
+
|
|
304
|
+
neuron_ids.append(cluster_neurons)
|
|
305
|
+
|
|
306
|
+
# Add intra-cluster connections
|
|
307
|
+
intra_edges = 0
|
|
308
|
+
for cluster_id, cluster_neurons in enumerate(neuron_ids):
|
|
309
|
+
if intra_cluster_degree is not None:
|
|
310
|
+
# CONSTANT DEGREE approach (proper weak scaling!)
|
|
311
|
+
# Each neuron connects to exactly intra_cluster_degree random targets
|
|
312
|
+
for pre in cluster_neurons:
|
|
313
|
+
# Sample random targets (excluding self)
|
|
314
|
+
possible_targets = [n for n in cluster_neurons if n != pre]
|
|
315
|
+
num_targets = min(intra_cluster_degree, len(possible_targets))
|
|
316
|
+
|
|
317
|
+
if num_targets > 0:
|
|
318
|
+
targets = np.random.choice(possible_targets, size=num_targets, replace=False)
|
|
319
|
+
|
|
320
|
+
for post in targets:
|
|
321
|
+
pre_type = graph.nodes[pre]["type"]
|
|
322
|
+
weight = weight_exc if pre_type == "excitatory" else weight_inh
|
|
323
|
+
|
|
324
|
+
graph.add_edge(
|
|
325
|
+
pre,
|
|
326
|
+
post,
|
|
327
|
+
synapse_breed=synapse_breed,
|
|
328
|
+
config=synapse_config,
|
|
329
|
+
overrides={"hyperparameters": {"weight": weight}},
|
|
330
|
+
connection_type="intra_cluster",
|
|
331
|
+
cluster=cluster_id
|
|
332
|
+
)
|
|
333
|
+
intra_edges += 1
|
|
334
|
+
else:
|
|
335
|
+
# PROBABILITY approach (creates O(n²) edges - NOT proper weak scaling!)
|
|
336
|
+
for pre in cluster_neurons:
|
|
337
|
+
for post in cluster_neurons:
|
|
338
|
+
if pre != post and np.random.random() < intra_cluster_prob:
|
|
339
|
+
pre_type = graph.nodes[pre]["type"]
|
|
340
|
+
weight = weight_exc if pre_type == "excitatory" else weight_inh
|
|
341
|
+
|
|
342
|
+
graph.add_edge(
|
|
343
|
+
pre,
|
|
344
|
+
post,
|
|
345
|
+
synapse_breed=synapse_breed,
|
|
346
|
+
config=synapse_config,
|
|
347
|
+
overrides={"hyperparameters": {"weight": weight}},
|
|
348
|
+
connection_type="intra_cluster",
|
|
349
|
+
cluster=cluster_id
|
|
350
|
+
)
|
|
351
|
+
intra_edges += 1
|
|
352
|
+
|
|
353
|
+
# Add inter-cluster connections with CONSTANT per-worker communication
|
|
354
|
+
# Using BIDIRECTIONAL PAIRS topology: each cluster pairs with K neighbors bidirectionally
|
|
355
|
+
# This ensures each cluster has exactly K unique communication partners (not 2K)
|
|
356
|
+
inter_edges = 0
|
|
357
|
+
for cluster_i in range(num_clusters):
|
|
358
|
+
if num_clusters == 1 or num_neighbor_clusters == 0:
|
|
359
|
+
continue # No other clusters to connect to
|
|
360
|
+
|
|
361
|
+
# Select K neighbor clusters using DIRECTED RING topology
|
|
362
|
+
# Strategy: cluster i connects to next K clusters in ring (unidirectional)
|
|
363
|
+
# For K=1: 0→1→2→3→0 (directed ring, keeps network connected)
|
|
364
|
+
# Each cluster sends to K neighbors and receives from K neighbors
|
|
365
|
+
# With K=1: each cluster has 2 unique communication partners (send-to, receive-from)
|
|
366
|
+
# - 2 workers: same neighbor for send/receive → 1 unique partner
|
|
367
|
+
# - 4+ workers: different neighbors for send/receive → 2 unique partners (constant!)
|
|
368
|
+
|
|
369
|
+
target_clusters = []
|
|
370
|
+
for offset in range(1, num_neighbor_clusters + 1):
|
|
371
|
+
cluster_j = (cluster_i + offset) % num_clusters
|
|
372
|
+
target_clusters.append(cluster_j)
|
|
373
|
+
|
|
374
|
+
# Send cross_cluster_edges to each of the K target clusters
|
|
375
|
+
for cluster_j in target_clusters:
|
|
376
|
+
edges_to_add = cross_cluster_edges
|
|
377
|
+
max_possible = neurons_per_cluster * neurons_per_cluster
|
|
378
|
+
edges_to_add = min(edges_to_add, max_possible)
|
|
379
|
+
|
|
380
|
+
all_possible_edges = [
|
|
381
|
+
(pre, post)
|
|
382
|
+
for pre in neuron_ids[cluster_i]
|
|
383
|
+
for post in neuron_ids[cluster_j]
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
if len(all_possible_edges) > 0:
|
|
387
|
+
selected_edges = np.random.choice(
|
|
388
|
+
len(all_possible_edges),
|
|
389
|
+
size=min(edges_to_add, len(all_possible_edges)),
|
|
390
|
+
replace=False
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
for edge_idx in selected_edges:
|
|
394
|
+
pre, post = all_possible_edges[edge_idx]
|
|
395
|
+
pre_type = graph.nodes[pre]["type"]
|
|
396
|
+
weight = weight_exc if pre_type == "excitatory" else weight_inh
|
|
397
|
+
|
|
398
|
+
graph.add_edge(
|
|
399
|
+
pre,
|
|
400
|
+
post,
|
|
401
|
+
synapse_breed=synapse_breed,
|
|
402
|
+
config=synapse_config,
|
|
403
|
+
overrides={"hyperparameters": {"weight": weight}},
|
|
404
|
+
connection_type="inter_cluster",
|
|
405
|
+
cluster=cluster_i
|
|
406
|
+
)
|
|
407
|
+
inter_edges += 1
|
|
408
|
+
|
|
409
|
+
# Add external inputs
|
|
410
|
+
external_inputs = 0
|
|
411
|
+
for cluster_id, cluster_neurons in enumerate(neuron_ids):
|
|
412
|
+
for post in cluster_neurons:
|
|
413
|
+
if np.random.random() < external_input_prob:
|
|
414
|
+
graph.add_edge(
|
|
415
|
+
-1, # External input
|
|
416
|
+
post,
|
|
417
|
+
synapse_breed=synapse_breed,
|
|
418
|
+
config=synapse_config,
|
|
419
|
+
overrides={"hyperparameters": {"weight": weight_exc}},
|
|
420
|
+
connection_type="external",
|
|
421
|
+
tags=["input_synapse"],
|
|
422
|
+
cluster=cluster_id # Assign to post-synaptic neuron's cluster
|
|
423
|
+
)
|
|
424
|
+
external_inputs += 1
|
|
425
|
+
|
|
426
|
+
# Print statistics
|
|
427
|
+
total_edges = intra_edges + inter_edges
|
|
428
|
+
theoretical_edge_cut = inter_edges / total_edges if total_edges > 0 else 0
|
|
429
|
+
|
|
430
|
+
# Calculate total agent count (neurons + synapses)
|
|
431
|
+
total_neurons = num_clusters * neurons_per_cluster
|
|
432
|
+
total_synapses = total_edges + external_inputs # All edges become synapse agents
|
|
433
|
+
total_agents = total_neurons + total_synapses
|
|
434
|
+
agents_per_worker = total_agents / num_clusters if num_clusters > 0 else 0
|
|
435
|
+
|
|
436
|
+
print(f"[SyntheticNet] Network statistics:")
|
|
437
|
+
print(f" - Total neurons: {total_neurons:,}")
|
|
438
|
+
print(f" - Total edges: {total_edges:,}")
|
|
439
|
+
print(f" - Intra-cluster: {intra_edges:,} ({100*intra_edges/total_edges:.1f}%)")
|
|
440
|
+
print(f" - Inter-cluster: {inter_edges:,} ({100*inter_edges/total_edges:.1f}%)")
|
|
441
|
+
print(f" - External inputs: {external_inputs:,}")
|
|
442
|
+
print(f" - Theoretical edge cut (with perfect partition): {theoretical_edge_cut:.4f}")
|
|
443
|
+
print(f"\n[SyntheticNet] AGENT COUNT (for weak scaling verification):")
|
|
444
|
+
print(f" - Total agents: {total_agents:,} ({total_neurons:,} neurons + {total_synapses:,} synapses)")
|
|
445
|
+
print(f" - Agents per worker: {agents_per_worker:,.0f}")
|
|
446
|
+
print(f" - Scaling: {agents_per_worker:,.0f} agents/worker × {num_clusters} workers = {total_agents:,} total")
|
|
447
|
+
|
|
448
|
+
return graph
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def generate_grid_network(
|
|
452
|
+
grid_size: Tuple[int, int],
|
|
453
|
+
connection_radius: int = 1,
|
|
454
|
+
connection_prob: float = 0.5,
|
|
455
|
+
external_input_prob: float = 0.1,
|
|
456
|
+
soma_breed: str = "lif_soma",
|
|
457
|
+
soma_config: str = "config_0",
|
|
458
|
+
synapse_breed: str = "single_exp_synapse",
|
|
459
|
+
synapse_config: str = "no_learning_config_0",
|
|
460
|
+
excitatory_ratio: float = 0.8,
|
|
461
|
+
weight_exc: float = 14.0,
|
|
462
|
+
weight_inh: float = -10.0,
|
|
463
|
+
seed: Optional[int] = None
|
|
464
|
+
) -> nx.DiGraph:
|
|
465
|
+
"""
|
|
466
|
+
Generate a grid-structured network with local connectivity.
|
|
467
|
+
|
|
468
|
+
This creates a 2D grid of neurons where each neuron connects primarily
|
|
469
|
+
to its neighbors within a given radius. This topology is naturally
|
|
470
|
+
partitionable and METIS-friendly.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
grid_size: (rows, cols) dimensions of the grid
|
|
474
|
+
connection_radius: Euclidean distance for connectivity (default: 1)
|
|
475
|
+
connection_prob: Probability of connection within radius (default: 0.5)
|
|
476
|
+
external_input_prob: Probability of external input per neuron
|
|
477
|
+
soma_breed: Neuron type
|
|
478
|
+
soma_config: Configuration name for somas
|
|
479
|
+
synapse_breed: Synapse type
|
|
480
|
+
synapse_config: Configuration name for synapses
|
|
481
|
+
excitatory_ratio: Ratio of excitatory neurons
|
|
482
|
+
weight_exc: Weight for excitatory synapses
|
|
483
|
+
weight_inh: Weight for inhibitory synapses
|
|
484
|
+
seed: Random seed for reproducibility
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
NetworkX DiGraph with grid structure
|
|
488
|
+
|
|
489
|
+
Example:
|
|
490
|
+
>>> # Create 100x100 grid (10,000 neurons)
|
|
491
|
+
>>> graph = generate_grid_network(
|
|
492
|
+
... grid_size=(100, 100),
|
|
493
|
+
... connection_radius=2,
|
|
494
|
+
... seed=42
|
|
495
|
+
... )
|
|
496
|
+
"""
|
|
497
|
+
if seed is not None:
|
|
498
|
+
np.random.seed(seed)
|
|
499
|
+
|
|
500
|
+
rows, cols = grid_size
|
|
501
|
+
total_neurons = rows * cols
|
|
502
|
+
graph = nx.DiGraph()
|
|
503
|
+
|
|
504
|
+
print(f"[SyntheticNet] Generating grid network:")
|
|
505
|
+
print(f" - Grid size: {rows}x{cols} ({total_neurons} neurons)")
|
|
506
|
+
print(f" - Connection radius: {connection_radius}")
|
|
507
|
+
print(f" - Connection probability: {connection_prob}")
|
|
508
|
+
|
|
509
|
+
# Create neurons in grid layout
|
|
510
|
+
neuron_positions = {}
|
|
511
|
+
for i in range(rows):
|
|
512
|
+
for j in range(cols):
|
|
513
|
+
neuron_id = i * cols + j
|
|
514
|
+
is_excitatory = neuron_id < int(total_neurons * excitatory_ratio)
|
|
515
|
+
|
|
516
|
+
graph.add_node(
|
|
517
|
+
neuron_id,
|
|
518
|
+
soma_breed=soma_breed,
|
|
519
|
+
config=soma_config,
|
|
520
|
+
position=(i, j),
|
|
521
|
+
type="excitatory" if is_excitatory else "inhibitory"
|
|
522
|
+
)
|
|
523
|
+
neuron_positions[neuron_id] = (i, j)
|
|
524
|
+
|
|
525
|
+
# Add connections based on distance
|
|
526
|
+
edge_count = 0
|
|
527
|
+
for pre_id, (pre_i, pre_j) in neuron_positions.items():
|
|
528
|
+
for post_id, (post_i, post_j) in neuron_positions.items():
|
|
529
|
+
if pre_id != post_id:
|
|
530
|
+
# Calculate Euclidean distance
|
|
531
|
+
dist = np.sqrt((pre_i - post_i)**2 + (pre_j - post_j)**2)
|
|
532
|
+
|
|
533
|
+
if dist <= connection_radius and np.random.random() < connection_prob:
|
|
534
|
+
pre_type = graph.nodes[pre_id]["type"]
|
|
535
|
+
weight = weight_exc if pre_type == "excitatory" else weight_inh
|
|
536
|
+
|
|
537
|
+
graph.add_edge(
|
|
538
|
+
pre_id,
|
|
539
|
+
post_id,
|
|
540
|
+
synapse_breed=synapse_breed,
|
|
541
|
+
config=synapse_config,
|
|
542
|
+
overrides={"hyperparameters": {"weight": weight}}
|
|
543
|
+
)
|
|
544
|
+
edge_count += 1
|
|
545
|
+
|
|
546
|
+
# Add external inputs
|
|
547
|
+
external_inputs = 0
|
|
548
|
+
for neuron_id in neuron_positions.keys():
|
|
549
|
+
if np.random.random() < external_input_prob:
|
|
550
|
+
graph.add_edge(
|
|
551
|
+
-1, # External input
|
|
552
|
+
neuron_id,
|
|
553
|
+
synapse_breed=synapse_breed,
|
|
554
|
+
config=synapse_config,
|
|
555
|
+
overrides={"hyperparameters": {"weight": weight_exc}},
|
|
556
|
+
tags=["input_synapse"]
|
|
557
|
+
)
|
|
558
|
+
external_inputs += 1
|
|
559
|
+
|
|
560
|
+
print(f"[SyntheticNet] Network statistics:")
|
|
561
|
+
print(f" - Total edges: {edge_count}")
|
|
562
|
+
print(f" - External inputs: {external_inputs}")
|
|
563
|
+
print(f" - Avg degree: {2*edge_count/total_neurons:.2f}")
|
|
564
|
+
|
|
565
|
+
return graph
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def generate_ring_of_clusters(
|
|
569
|
+
num_clusters: int,
|
|
570
|
+
neurons_per_cluster: int,
|
|
571
|
+
intra_cluster_prob: float = 0.3,
|
|
572
|
+
adjacent_cluster_prob: float = 0.05,
|
|
573
|
+
external_input_prob: float = 0.2,
|
|
574
|
+
soma_breed: str = "lif_soma",
|
|
575
|
+
soma_config: str = "config_0",
|
|
576
|
+
synapse_breed: str = "single_exp_synapse",
|
|
577
|
+
synapse_config: str = "no_learning_config_0",
|
|
578
|
+
excitatory_ratio: float = 0.8,
|
|
579
|
+
weight_exc: float = 14.0,
|
|
580
|
+
weight_inh: float = -10.0,
|
|
581
|
+
seed: Optional[int] = None
|
|
582
|
+
) -> nx.DiGraph:
|
|
583
|
+
"""
|
|
584
|
+
Generate a ring of clusters network.
|
|
585
|
+
|
|
586
|
+
This creates clusters arranged in a ring topology, where each cluster
|
|
587
|
+
connects primarily to itself and its immediate neighbors in the ring.
|
|
588
|
+
This provides a balanced structure with predictable cross-cluster
|
|
589
|
+
communication patterns.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
num_clusters: Number of clusters in the ring
|
|
593
|
+
neurons_per_cluster: Number of neurons per cluster
|
|
594
|
+
intra_cluster_prob: Connection probability within cluster
|
|
595
|
+
adjacent_cluster_prob: Connection probability to adjacent clusters
|
|
596
|
+
external_input_prob: Probability of external input per neuron
|
|
597
|
+
soma_breed: Neuron type
|
|
598
|
+
soma_config: Configuration name for somas
|
|
599
|
+
synapse_breed: Synapse type
|
|
600
|
+
synapse_config: Configuration name for synapses
|
|
601
|
+
excitatory_ratio: Ratio of excitatory neurons
|
|
602
|
+
weight_exc: Weight for excitatory synapses
|
|
603
|
+
weight_inh: Weight for inhibitory synapses
|
|
604
|
+
seed: Random seed for reproducibility
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
NetworkX DiGraph with ring-of-clusters structure
|
|
608
|
+
|
|
609
|
+
Example:
|
|
610
|
+
>>> # Create ring of 8 clusters with 500 neurons each
|
|
611
|
+
>>> graph = generate_ring_of_clusters(
|
|
612
|
+
... num_clusters=8,
|
|
613
|
+
... neurons_per_cluster=500,
|
|
614
|
+
... seed=42
|
|
615
|
+
... )
|
|
616
|
+
"""
|
|
617
|
+
if seed is not None:
|
|
618
|
+
np.random.seed(seed)
|
|
619
|
+
|
|
620
|
+
graph = nx.DiGraph()
|
|
621
|
+
total_neurons = num_clusters * neurons_per_cluster
|
|
622
|
+
|
|
623
|
+
print(f"[SyntheticNet] Generating ring-of-clusters network:")
|
|
624
|
+
print(f" - Clusters: {num_clusters}")
|
|
625
|
+
print(f" - Neurons per cluster: {neurons_per_cluster}")
|
|
626
|
+
print(f" - Total neurons: {total_neurons}")
|
|
627
|
+
|
|
628
|
+
# Create neurons organized by cluster
|
|
629
|
+
neuron_ids = []
|
|
630
|
+
for cluster_id in range(num_clusters):
|
|
631
|
+
cluster_neurons = []
|
|
632
|
+
for i in range(neurons_per_cluster):
|
|
633
|
+
neuron_id = cluster_id * neurons_per_cluster + i
|
|
634
|
+
is_excitatory = i < int(neurons_per_cluster * excitatory_ratio)
|
|
635
|
+
|
|
636
|
+
graph.add_node(
|
|
637
|
+
neuron_id,
|
|
638
|
+
soma_breed=soma_breed,
|
|
639
|
+
config=soma_config,
|
|
640
|
+
cluster=cluster_id,
|
|
641
|
+
type="excitatory" if is_excitatory else "inhibitory",
|
|
642
|
+
tags=[f"cluster_{cluster_id}"]
|
|
643
|
+
)
|
|
644
|
+
cluster_neurons.append(neuron_id)
|
|
645
|
+
|
|
646
|
+
neuron_ids.append(cluster_neurons)
|
|
647
|
+
|
|
648
|
+
# Add intra-cluster connections
|
|
649
|
+
intra_edges = 0
|
|
650
|
+
for cluster_neurons in neuron_ids:
|
|
651
|
+
for pre in cluster_neurons:
|
|
652
|
+
for post in cluster_neurons:
|
|
653
|
+
if pre != post and np.random.random() < intra_cluster_prob:
|
|
654
|
+
pre_type = graph.nodes[pre]["type"]
|
|
655
|
+
weight = weight_exc if pre_type == "excitatory" else weight_inh
|
|
656
|
+
|
|
657
|
+
graph.add_edge(
|
|
658
|
+
pre, post,
|
|
659
|
+
synapse_breed=synapse_breed,
|
|
660
|
+
config=synapse_config,
|
|
661
|
+
overrides={"hyperparameters": {"weight": weight}}
|
|
662
|
+
)
|
|
663
|
+
intra_edges += 1
|
|
664
|
+
|
|
665
|
+
# Add connections to adjacent clusters in ring
|
|
666
|
+
inter_edges = 0
|
|
667
|
+
for cluster_id in range(num_clusters):
|
|
668
|
+
# Connect to next cluster in ring
|
|
669
|
+
next_cluster = (cluster_id + 1) % num_clusters
|
|
670
|
+
|
|
671
|
+
for pre in neuron_ids[cluster_id]:
|
|
672
|
+
for post in neuron_ids[next_cluster]:
|
|
673
|
+
if np.random.random() < adjacent_cluster_prob:
|
|
674
|
+
pre_type = graph.nodes[pre]["type"]
|
|
675
|
+
weight = weight_exc if pre_type == "excitatory" else weight_inh
|
|
676
|
+
|
|
677
|
+
graph.add_edge(
|
|
678
|
+
pre, post,
|
|
679
|
+
synapse_breed=synapse_breed,
|
|
680
|
+
config=synapse_config,
|
|
681
|
+
overrides={"hyperparameters": {"weight": weight}}
|
|
682
|
+
)
|
|
683
|
+
inter_edges += 1
|
|
684
|
+
|
|
685
|
+
# Add external inputs
|
|
686
|
+
external_inputs = 0
|
|
687
|
+
for cluster_neurons in neuron_ids:
|
|
688
|
+
for post in cluster_neurons:
|
|
689
|
+
if np.random.random() < external_input_prob:
|
|
690
|
+
graph.add_edge(
|
|
691
|
+
-1, # External input
|
|
692
|
+
post,
|
|
693
|
+
synapse_breed=synapse_breed,
|
|
694
|
+
config=synapse_config,
|
|
695
|
+
overrides={"hyperparameters": {"weight": weight_exc}},
|
|
696
|
+
tags=["input_synapse"]
|
|
697
|
+
)
|
|
698
|
+
external_inputs += 1
|
|
699
|
+
|
|
700
|
+
total_edges = intra_edges + inter_edges
|
|
701
|
+
print(f"[SyntheticNet] Network statistics:")
|
|
702
|
+
print(f" - Total edges: {total_edges}")
|
|
703
|
+
print(f" - Intra-cluster edges: {intra_edges}")
|
|
704
|
+
print(f" - Inter-cluster edges: {inter_edges}")
|
|
705
|
+
print(f" - External inputs: {external_inputs}")
|
|
706
|
+
|
|
707
|
+
return graph
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def analyze_network_partition(
|
|
711
|
+
graph: nx.DiGraph,
|
|
712
|
+
partition_dict: Dict[int, int]
|
|
713
|
+
) -> Dict[str, any]:
|
|
714
|
+
"""
|
|
715
|
+
Analyze network partition quality.
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
graph: NetworkX graph
|
|
719
|
+
partition_dict: Mapping from node_id to worker_rank
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
Dictionary with partition statistics
|
|
723
|
+
"""
|
|
724
|
+
num_workers = max(partition_dict.values()) + 1
|
|
725
|
+
|
|
726
|
+
# Count nodes per worker
|
|
727
|
+
nodes_per_worker = [0] * num_workers
|
|
728
|
+
for rank in partition_dict.values():
|
|
729
|
+
nodes_per_worker[rank] += 1
|
|
730
|
+
|
|
731
|
+
# Count edges within and between workers
|
|
732
|
+
intra_worker_edges = 0
|
|
733
|
+
inter_worker_edges = 0
|
|
734
|
+
edges_per_worker = [0] * num_workers
|
|
735
|
+
|
|
736
|
+
for u, v in graph.edges():
|
|
737
|
+
if u in partition_dict and v in partition_dict:
|
|
738
|
+
u_rank = partition_dict[u]
|
|
739
|
+
v_rank = partition_dict[v]
|
|
740
|
+
|
|
741
|
+
if u_rank == v_rank:
|
|
742
|
+
intra_worker_edges += 1
|
|
743
|
+
edges_per_worker[u_rank] += 1
|
|
744
|
+
else:
|
|
745
|
+
inter_worker_edges += 1
|
|
746
|
+
|
|
747
|
+
total_edges = intra_worker_edges + inter_worker_edges
|
|
748
|
+
edge_cut_ratio = inter_worker_edges / total_edges if total_edges > 0 else 0
|
|
749
|
+
|
|
750
|
+
# Calculate balance metrics
|
|
751
|
+
avg_nodes = np.mean(nodes_per_worker)
|
|
752
|
+
std_nodes = np.std(nodes_per_worker)
|
|
753
|
+
avg_edges = np.mean(edges_per_worker)
|
|
754
|
+
std_edges = np.std(edges_per_worker)
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
"num_workers": num_workers,
|
|
758
|
+
"nodes_per_worker": nodes_per_worker,
|
|
759
|
+
"avg_nodes": avg_nodes,
|
|
760
|
+
"std_nodes": std_nodes,
|
|
761
|
+
"node_imbalance": std_nodes / avg_nodes if avg_nodes > 0 else 0,
|
|
762
|
+
"edges_per_worker": edges_per_worker,
|
|
763
|
+
"avg_edges": avg_edges,
|
|
764
|
+
"std_edges": std_edges,
|
|
765
|
+
"edge_imbalance": std_edges / avg_edges if avg_edges > 0 else 0,
|
|
766
|
+
"total_edges": total_edges,
|
|
767
|
+
"intra_worker_edges": intra_worker_edges,
|
|
768
|
+
"inter_worker_edges": inter_worker_edges,
|
|
769
|
+
"edge_cut_ratio": edge_cut_ratio
|
|
770
|
+
}
|