/ generate_network.py
generate_network.py
1 #! /usr/bin/env python3 2 3 import matplotlib.pyplot as plt 4 import networkx as nx 5 import random, math 6 import json 7 import sys, os 8 import string 9 import typer 10 from enum import Enum 11 12 13 # Enums & Consts 14 15 # To add a new node type, add appropriate entries to the nodeType and nodeTypeSwitch 16 class nodeType(Enum): 17 DESKTOP = "desktop" # waku desktop config 18 MOBILE = "mobile" # waku mobile config 19 20 nodeTypeSwitch = { 21 nodeType.DESKTOP : "rpc-admin = true\nkeep-alive = true\n", 22 nodeType.MOBILE : "rpc-admin = true\nkeep-alive = true\n" 23 } 24 25 26 # To add a new network type, add appropriate entries to the networkType and networkTypeSwitch 27 # the networkTypeSwitch is placed before generate_network(): fwd declaration mismatch with typer/python :/ 28 class networkType(Enum): 29 CONFIGMODEL = "configmodel" 30 SCALEFREE = "scalefree" # power law 31 NEWMANWATTSSTROGATZ = "newmanwattsstrogatz" # mesh, smallworld 32 BARBELL = "barbell" # partition 33 BALANCEDTREE = "balancedtree" # committees? 34 STAR = "star" # spof 35 36 37 NW_DATA_FNAME = "network_data.json" 38 NODE_PREFIX = "waku" 39 SUBNET_PREFIX = "subnetwork" 40 41 42 ### I/O related fns ############################################################## 43 44 # Dump to a json file 45 def write_json(dirname, json_dump): 46 fname = os.path.join(dirname, NW_DATA_FNAME) 47 with open(fname, "w") as f: 48 json.dump(json_dump, f, indent=2) 49 50 51 def write_toml(dirname, node_name, toml): 52 fname = os.path.join(dirname, f"{node_name}.toml") 53 with open(fname, "w") as f: 54 f.write(toml) 55 56 57 # Draw the network and output the image to a file; does not account for subnets yet 58 def draw(dirname, H): 59 nx.draw(H, pos=nx.kamada_kawai_layout(H), with_labels=True) 60 fname = os.path.join(dirname, NW_DATA_FNAME) 61 plt.savefig(f"{os.path.splitext(fname)[0]}.png", format="png") 62 plt.show() 63 64 65 # Has trouble with non-integer/non-hashable keys 66 def read_json(fname): 67 with open(fname) as f: 68 jdata = json.load(f) 69 return nx.node_link_graph(jdata) 70 71 72 # check if the required dir can be created 73 def exists_or_nonempty(dirname): 74 if not os.path.exists(dirname): 75 return False 76 elif not os.path.isfile(dirname) and os.listdir(dirname): 77 print(f"{dirname}: exists and not empty") 78 return True 79 elif os.path.isfile(dirname): 80 print(f"{dirname}: exists but not a directory") 81 return True 82 else: 83 return False 84 85 86 ### topics related fns ############################################################# 87 88 # Generate a random string of upper case chars 89 def generate_random_string(n): 90 return "".join(random.choice(string.ascii_uppercase) for _ in range(n)) 91 92 93 # Generate the topics - topic followed by random UC chars - Eg, topic_XY" 94 def generate_topics(num_topics): 95 topic_len = int(math.log(num_topics)/math.log(26)) + 1 # base is 26 - upper case letters 96 topics = {i: f"topic_{generate_random_string(topic_len)}" for i in range(num_topics)} 97 return topics 98 99 100 # Get a random sub-list of topics 101 def get_random_sublist(topics): 102 n = len(topics) 103 lo = random.randint(0, n - 1) 104 hi = random.randint(lo + 1, n) 105 sublist = [] 106 for i in range(lo, hi): 107 sublist.append(topics[i]) 108 return sublist 109 110 111 ### network processing related fns ################################################# 112 113 # Network Types 114 def generate_config_model(n): 115 #degrees = nx.random_powerlaw_tree_sequence(n, tries=10000) 116 degrees = [random.randint(1, n) for i in range(n)] 117 if (sum(degrees)) % 2 != 0: # adjust the degree to be even 118 degrees[-1] += 1 119 return nx.configuration_model(degrees) # generate the graph 120 121 122 def generate_scalefree_graph(n): 123 return nx.scale_free_graph(n) 124 125 126 # n must be larger than k=D=3 127 def generate_newmanwattsstrogatz_graph(n): 128 return nx.newman_watts_strogatz_graph(n, 3, 0.5) 129 130 131 def generate_barbell_graph(n): 132 return nx.barbell_graph(int(n/2), 1) 133 134 135 def generate_balanced_tree(n, fanout=3): 136 height = int(math.log(n)/math.log(fanout)) 137 return nx.balanced_tree(fanout, height) 138 139 140 def generate_star_graph(n): 141 return nx.star_graph(n) 142 143 144 networkTypeSwitch = { 145 networkType.CONFIGMODEL : generate_config_model, 146 networkType.SCALEFREE : generate_scalefree_graph, 147 networkType.NEWMANWATTSSTROGATZ : generate_newmanwattsstrogatz_graph, 148 networkType.BARBELL : generate_barbell_graph, 149 networkType.BALANCEDTREE: generate_balanced_tree, 150 networkType.STAR : generate_star_graph 151 } 152 153 154 # Generate the network from nw type 155 def generate_network(n, network_type): 156 return postprocess_network(networkTypeSwitch.get(network_type)(n)) 157 158 159 # Label the generated network with prefix 160 def postprocess_network(G): 161 G = nx.Graph(G) # prune out parallel/multi edges 162 G.remove_edges_from(nx.selfloop_edges(G)) # remove the self-loops 163 mapping = {i: f"{NODE_PREFIX}_{i}" for i in range(len(G))} 164 return nx.relabel_nodes(G, mapping) # label the nodes 165 166 167 def generate_subnets(G, num_subnets): 168 n = len(G.nodes) 169 if num_subnets == n: # if num_subnets == size of the network 170 return {f"{NODE_PREFIX}_{i}": f"{SUBNET_PREFIX}_{i}" for i in range(n)} 171 172 lst = list(range(n)) 173 random.shuffle(lst) 174 offsets = sorted(random.sample(range(0, n), num_subnets - 1)) 175 offsets.append(n-1) 176 177 start = 0 178 subnets = {} 179 subnet_id = 0 180 for end in offsets: 181 for i in range(start, end+1): 182 subnets[f"{NODE_PREFIX}_{lst[i]}"] = f"{SUBNET_PREFIX}_{subnet_id}" 183 start = end 184 subnet_id += 1 185 return subnets 186 187 188 ### file format related fns ########################################################### 189 #Generate per node toml configs 190 def generate_toml(topics, node_type=nodeType.DESKTOP): 191 topic_str = " ".join(get_random_sublist(topics)) # space separated topics 192 return f"{nodeTypeSwitch.get(node_type)}topics = \"{topic_str}\"\n" 193 194 195 # Generates network-wide json and per-node toml and writes them 196 def generate_and_write_files(dirname, num_topics, num_subnets, G): 197 topics = generate_topics(num_topics) 198 subnets = generate_subnets(G, num_subnets) 199 json_dump = {} 200 for node in G.nodes: 201 write_toml(dirname, node, generate_toml(topics)) # per node toml 202 json_dump[node] = {} 203 json_dump[node]["static-nodes"] = [] 204 for edge in G.edges(node): 205 json_dump[node]["static-nodes"].append(edge[1]) 206 json_dump[node][SUBNET_PREFIX] = subnets[node] 207 write_json(dirname, json_dump) # network wide json 208 209 210 ### the main ########################################################################## 211 def main( 212 dirname: str = "WakuNetwork", num_nodes: int = 4, num_topics: int = 1, 213 network_type: networkType = networkType.NEWMANWATTSSTROGATZ.value, 214 node_type: nodeType = nodeType.DESKTOP.value, 215 num_subnets: int = -1, 216 num_partitions: int = 1): 217 218 # sanity checks 219 if num_partitions > 1: 220 raise ValueError(f"--num-partitions {num_partitions}, Sorry, we do not yet support partitions") 221 if num_subnets > num_nodes: 222 raise ValueError(f"num_subnets must be <= num_nodes: num_subnets={num_subnets}, num_nodes={num_nodes}") 223 if num_subnets == -1: 224 num_subnets = num_nodes 225 226 # Generate the network 227 G = generate_network(num_nodes, network_type) 228 229 # Refuse to overwrite non-empty dirs 230 if exists_or_nonempty(dirname) : 231 sys.exit(1) 232 os.makedirs(dirname, exist_ok=True) 233 234 # Generate file format specific data structs and write the files; optionally, draw the network 235 generate_and_write_files(dirname, num_topics, num_subnets, G) 236 #draw(dirname, G) 237 238 239 if __name__ == "__main__": 240 typer.run(main)