'''Module for generating and plotting networks.'''
from trafpy.generator.src import tools
import copy
import networkx as nx
import matplotlib.pyplot as plt
import json
[docs]def gen_arbitrary_network(num_eps,
ep_label=None,
ep_capacity=12500,
num_channels=1,
racks_dict=None,
topology_type=None):
'''Generates an arbitrary network with num_eps nodes labelled as ep_label.
Note that no edges are formed in this network; it is purely for ep name
indexing purposes when using Demand class. This is useful where want to
use the demand class but not necessarily with a carefully crafted networkx
graph that accurately mimics the network you will use for the demands
Args:
num_eps (int): Number of endpoints in network.
ep_label (str,int,float): Endpoint label (e.g. 'server'). All endpoints will have
ep_label appended to the start of their label (e.g. 'server_0', 'server_1', ...).
ep_capacity (int, float): Byte capacity per end point channel.
num_channels (int, float): Number of channels on each link in network.
racks_dict (dict): Mapping of which end points are in which racks. Keys are
rack ids, values are list of end points. If None, assume there is not
clustering/rack system in the network where have different end points
in different clusters/racks.
Returns:
networkx graph: network object
'''
network = nx.Graph()
network.add_nodes_from([node for node in range(num_eps)])
if ep_label is None:
# must be str or not json serialisable
servers = [str(i) for i in range(num_eps)]
else:
servers = [ep_label+'_'+str(i) for i in range(num_eps)]
relabel_mapping = {node: label for node, label in zip(range(num_eps),servers)}
network = nx.relabel_nodes(network, relabel_mapping)
eps = []
for node in list(network.nodes):
try:
if ep_label in node:
eps.append(node)
except TypeError:
# ep_label is None
eps.append(node)
network.graph['endpoints'] = eps
# /= 2 to get max theoretical capacity (number of units which network can transfer per unit time)
max_nw_capacity = (num_eps * ep_capacity * num_channels) / 2
if topology_type is None:
topology_type = 'arbitrary_endpoints_{}_chancap_{}_channels_{}'.format(num_eps, ep_capacity, num_channels)
init_global_network_attrs(network,
max_nw_capacity,
num_channels,
ep_link_capacity=ep_capacity*num_channels,
endpoint_label=ep_label,
node_labels=[ep_label],
racks_dict=racks_dict,
topology_type=topology_type)
return network
[docs]def gen_nsfnet_network(ep_label='server',
rack_label='rack',
N=0,
num_channels=2,
server_to_rack_channel_capacity=1,
rack_to_rack_channel_capacity=10,
show_fig=False):
'''Generates the standard 14-node NSFNET topology (a U.S. core network).
Args:
ep_label (str,int,float): Endpoint label (e.g. 'server'). All endpoints will have
ep_label appended to the start of their label (e.g. 'server_0', 'server_1', ...).
N (int): Number of servers per rack. If 0, assume all nodes in nsfnet
are endpoints
num_channels (int,float): Number of channels on each link in network.
server_to_rack_channel_capacity (int,float): Byte capacity per channel
between servers and ToR switch.
rack_to_rack_channel_capacity (int,float): Byte capacity per channel between racks.
show_fig (bool): Whether or not to plot and show fig. If True, will
display fig.
Returns:
networkx graph: network object
'''
channel_names = gen_channel_names(num_channels)
network = nx.Graph()
node_pair_list = [[0,1],
[0,3],
[0,2],
[1,2],
[1,7],
[3,8],
[3,4],
[3,6],
[4,5],
[4,5],
[5,2],
[5,13],
[5,12],
[6,7],
[7,10],
[8,11],
[8,9],
[9,10],
[9,12],
[10,11],
[10,13],
[11,12]]
if N == 0:
# above nodes are all end points
label = ep_label
else:
# above nodes are ToR switch nodes
label = rack_label
for idx in range(len(node_pair_list)):
node_pair_list[idx][0] = label + '_' + str(node_pair_list[idx][0])
node_pair_list[idx][1] = label + '_' + str(node_pair_list[idx][1])
# add 14 nodes
for edge in node_pair_list:
network.add_edge(*tuple(edge))
if N == 0:
# assume all nodes are servers
racks_dict = None
else:
# each of 14 nodes in NSFNET is a ToR switch
i = 0
racks_dict = {rack: [] for rack in range(14)}
for rack in range(14):
for server in range(N):
racks_dict[rack].append(ep_label+'_'+str(i))
network.add_edge(ep_label+'_'+str(i), rack_label+'_'+str(rack))
i += 1
channel_names = gen_channel_names(num_channels)
edges = [edge for edge in network.edges]
add_edges_capacity_attrs(network, edges, channel_names, rack_to_rack_channel_capacity)
# set gloabl network attrs
network.graph['endpoints'] = get_endpoints(network, ep_label)
# /= 2 to get max theoretical capacity (number of units which network can transfer per unit time)
max_nw_capacity = (len(network.edges) * num_channels * rack_to_rack_channel_capacity) / 2
init_global_network_attrs(network,
max_nw_capacity,
num_channels,
ep_link_capacity=server_to_rack_channel_capacity*num_channels,
endpoint_label=ep_label,
node_labels=[ep_label, rack_label],
topology_type='14_node_nsfnet',
racks_dict=racks_dict)
if show_fig:
plot_network(network, show_fig=True)
return network
[docs]def gen_simple_network(ep_label='server',
num_channels=2,
server_to_rack_channel_capacity=500,
show_fig=False):
'''Generates very simple 5-node topology.
Args:
ep_label (str,int,float): Endpoint label (e.g. 'server'). All endpoints will have
ep_label appended to the start of their label (e.g. 'server_0', 'server_1', ...).
num_channels (int,float): Number of channels on each link in network.
channel_capacity (int,float): Byte capacity per channel.
show_fig (bool): Whether or not to plot and show fig. If True, will
display fig.
Returns:
networkx graph: network object
'''
network = nx.Graph()
network.add_nodes_from([node for node in range(5)])
network.add_edges_from([(0,1),
(0,2),
(1,2),
(2,4),
(4,3),
(3,1)],weight=1)
servers = [ep_label+'_'+str(i) for i in range(5)]
relabel_mapping = {node: label for node, label in zip(range(5),servers)}
network = nx.relabel_nodes(network, relabel_mapping)
channel_names = gen_channel_names(num_channels)
edges = [edge for edge in network.edges]
add_edges_capacity_attrs(network, edges, channel_names, server_to_rack_channel_capacity)
# set gloabl network attrs
network.graph['endpoints'] = get_endpoints(network, ep_label)
# /= 2 to get max theoretical capacity (number of units which network can transfer per unit time)
max_nw_capacity = (len(network.edges) * num_channels * server_to_rack_channel_capacity) / 2
init_global_network_attrs(network,
max_nw_capacity,
num_channels,
ep_link_capacity=server_to_rack_channel_capacity*num_channels,
endpoint_label=ep_label,
node_labels=[ep_label],
topology_type='5_node_simple_network')
if show_fig:
plot_network(network, show_fig=True)
return network
[docs]def get_endpoints(network, ep_label):
'''Gets list of endpoints of network.
Args:
network (networkx graph): Networkx object.
ep_label (str,int,float): Endpoint label (e.g. 'server'). All endpoints will have
ep_label appended to the start of their label (e.g. 'server_0', 'server_1', ...).
Returns:
eps (list): List of endpoints.
'''
eps = []
for node in list(network.nodes):
if ep_label in node:
eps.append(node)
return eps
[docs]def gen_fat_tree(k=4,
L=2,
n=4,
ep_label='server',
rack_label='rack',
edge_label='edge',
aggregate_label='agg',
core_label='core',
num_channels = 2,
server_to_rack_channel_capacity=500,
rack_to_edge_channel_capacity=1000,
edge_to_agg_channel_capacity=1000,
agg_to_core_channel_capacity=2000,
rack_to_core_channel_capacity=2000,
show_fig=False):
'''Generates a perfect fat tree (i.e. all layers have switches with same radix/number of ports).
Top layer is always core (spine) switch layer, bottom layer is always
ToR (leaf) layer.
L must be either 2 (core, ToR) or 4 (core, agg, edge, ToR)
N.B. L=2 is commonly referred to as '2-layer Clos' or 'Clos' or 'spine-leaf' topology
Resource for building (scroll down to summary table with equations):
https://packetpushers.net/demystifying-dcn-topologies-clos-fat-trees-part2/
Another good resource for data centre topologies etc. in general:
https://www.oreilly.com/library/view/bgp-in-the/9781491983416/ch01.html#:~:text=The%20most%20common%20routing%20protocol,single%20data%20center%2C%20as%20well.
Parameters of network:
- number of core (spine) switches = (k/2)^(L/2) (top layer)
- number of edge switches (if L=4) = (k^2)/2
- number of agg switches (if L=4) = (k^2)/2
- number of pods (if L=4) (pod is a group of agg and/or edge switches) = 2*(k/2)^(L-2)
- number of ToR (leaf) switches (racks) = 2*(k/2)^(L-1) (bottom layer)
- number of server-facing ToR 'host' ports = 2*(k/2)^2 (can have multiple servers connected to same host port, & can oversubscribe)
- number of servers = number ToR switches * n
Args:
k (int): Number of ports (links) on each switch (both up and down).
L (int): Number of layers in the fat tree.
n (int): Number of server per rack.
ep_label (str,int,float): Endpoint label (e.g. 'server'). All endpoints will have
ep_label appended to the start of their label (e.g. 'server_0', 'server_1', ...).
edge_label (str,int): Label to assign to edge switch nodes
aggregate_label (str,int): Label to assign to edge switch nodes
core_label (str,int): Label to assign to core switch nodes
num_channels (int, float): Number of channels on each link in network
server_to_edge_channel_capacity (int,float): Byte capacity per channel
edge_to_agg_channel_capacity (int,float): (if L==4) Byte capacity per channel
agg_to_core_channel_capacity (int,float): (if L==4) Byte capacity per channel
rack_to_core_channel_capacity (int,float): (if L==2) Byte capacity per channel
Returns:
networkx graph: network object
'''
if L != 2 and L != 4:
raise Exception('L must be 2 (ToR layer, core layer) or 4 (ToR layer, edge layer, agg layer, core layer), but is {}.'.format(L))
if k % 2 != 0:
raise Exception('k must be even since, in perfect fat tree, have equal number of up and down ports on each switch, but is {}.'.format(k))
channel_names = gen_channel_names(num_channels)
# initialise network nodes
if L == 2:
node_labels = [ep_label, rack_label, core_label]
else:
node_labels = [ep_label, rack_label, edge_label, aggregate_label, core_label]
#num_cores = int((k/2)**(L-1))
#num_cores = int((k/2)**2)
num_cores = int((k/2)**(L/2))
num_aggs = int((k**2)/2)
num_edges = int((k**2)/2)
num_pods = int(2*(k/2)**(L-2))
num_racks = int(2*(k/2)**(L-1))
num_servers = int(num_racks * n)
cores = [core_label+'_'+str(i) for i in range(num_cores)]
aggs = [aggregate_label+'_'+str(i) for i in range(num_aggs)]
edges = [edge_label+'_'+str(i) for i in range(num_edges)]
racks = [rack_label+'_'+str(i) for i in range(num_racks)]
servers = [ep_label+'_'+str(i) for i in range(num_servers)]
# create core and rack layer networks
core_layer = nx.Graph()
rack_layer = nx.Graph()
core_layer.add_nodes_from(cores)
rack_layer.add_nodes_from(racks)
# combine cores and racks into single network
fat_tree_network = nx.compose(core_layer, rack_layer)
if L == 2:
# 2 layers: Core, ToR
# link racks to cores, add link attributes
rack_iterator = iter(racks)
for rack in racks:
core_iterator = iter(cores)
# have k/2 up-ports on each switch
for up_port in range(int(k/2)):
core = next(core_iterator)
fat_tree_network.add_edge(rack, core)
add_edge_capacity_attrs(fat_tree_network,
(rack, core),
channel_names,
rack_to_core_channel_capacity)
else:
# 4 layers: Core, Agg, Edge, ToR. Agg and Edge switches grouped into pods.
# group edges and aggregates into pods
num_pods = int(k)
pods = [[] for i in range(num_pods)]
prev_iter = 0
for pod_iter in range(len(pods)):
curr_iter = int(prev_iter + (k/2))
pods[pod_iter].append(edges[prev_iter:curr_iter])
pods[pod_iter].append(aggs[prev_iter:curr_iter])
prev_iter = curr_iter
# create dict of pod networks
pod_labels = ['pod_'+str(i) for i in range(num_pods)]
pods_dict = {tuple([pod]): nx.Graph() for pod in pod_labels}
for pod_iter in range(num_pods):
key = ('pod_'+str(pod_iter),)
pod_edges = pods[pod_iter][0]
pod_aggs = pods[pod_iter][1]
pods_dict[key].add_nodes_from(pod_edges)
pods_dict[key].add_nodes_from(pod_aggs)
# connect edge and aggregate switches within pod, add link attributes
for pod_edge in pod_edges:
for pod_agg in pod_aggs:
pods_dict[key].add_edge(pod_agg, pod_edge)
add_edge_capacity_attrs(pods_dict[key],
(pod_agg,pod_edge),
channel_names,
edge_to_agg_channel_capacity)
# add pods (agg + edge) layer to fat-tree
pod_networks = list(pods_dict.values())
for pod_iter in range(num_pods):
fat_tree_network = nx.compose(fat_tree_network, pod_networks[pod_iter])
# link aggregate switches in pods to core switches, add link attributes
for pod_iter in range(num_pods):
pod_aggs = pods[pod_iter][1]
core_iterator = iter(cores)
for pod_agg in pod_aggs:
while fat_tree_network.degree[pod_agg] < k:
core = next(core_iterator)
fat_tree_network.add_edge(core, pod_agg)
add_edge_capacity_attrs(fat_tree_network,
(core,pod_agg),
channel_names,
agg_to_core_channel_capacity)
# link edge switches in pods to racks, add link attributes
rack_iterator = iter(racks)
for pod_iter in range(num_pods):
pod_edges = pods[pod_iter][0]
for pod_edge in pod_edges:
while fat_tree_network.degree[pod_edge] < k:
rack = next(rack_iterator)
fat_tree_network.add_edge(pod_edge, rack)
add_edge_capacity_attrs(fat_tree_network,
(pod_edge,rack),
channel_names,
rack_to_edge_channel_capacity)
# link servers to racks, add link attributes
racks_dict = {rack: [] for rack in racks} # track which endpoints in which rack
server_iterator = iter(servers)
for rack in racks:
for _ in range(n):
server = next(server_iterator)
fat_tree_network.add_edge(rack, server)
add_edge_capacity_attrs(fat_tree_network,
(rack, server),
channel_names,
server_to_rack_channel_capacity)
racks_dict[rack].append(server)
# calc total network capacity
# /= 2 to get max theoretical capacity (number of units which network can transfer per unit time)
max_nw_capacity = (num_servers * num_channels * server_to_rack_channel_capacity) / 2
# init global network attrs
fat_tree_network.graph['endpoints'] = servers
init_global_network_attrs(fat_tree_network,
max_nw_capacity,
num_channels,
ep_link_capacity=server_to_rack_channel_capacity*num_channels,
endpoint_label=ep_label,
node_labels=node_labels,
topology_type='fat_tree',
racks_dict=racks_dict)
if show_fig:
plot_network(fat_tree_network, show_fig=True)
return fat_tree_network
[docs]def init_global_network_attrs(network,
max_nw_capacity,
num_channels,
ep_link_capacity,
endpoint_label = 'server',
topology_type='unknown',
node_labels=['server'],
racks_dict=None):
'''Initialises the standard global network attributes of a given network.
Args:
network (obj): NetworkX object.
max_nw_capacity (int/float): Maximum rate at which info can be reliably
transmitted over the network (sum of all link capacities).
num_channels (int): Number of channels on each link in network.
topology_type (str): Label of network topology (e.g. 'fat_tree').
node_labels (list): Label classes assigned to network nodes
(e.g. ['server', 'rack', 'edge']).
racks_dict (dict): Which servers/endpoints are in which rack. If None,
assume do not have rack system where have multiple servers in one
rack.
'''
network.graph['endpoint_label'] = endpoint_label
network.graph['num_channels_per_link'] = num_channels
network.graph['ep_link_capacity'] = ep_link_capacity
network.graph['ep_link_port_capacity'] = ep_link_capacity / 2 # all eps have a src & a dst port
network.graph['max_nw_capacity'] = max_nw_capacity
network.graph['curr_nw_capacity_used'] = 0
network.graph['num_active_connections'] = 0
network.graph['total_connections_blocked'] = 0
network.graph['node_labels'] = node_labels
network.graph['topology_type'] = topology_type
network.graph['channel_names'] = gen_channel_names(num_channels)
# ensure racks dict is str so json serialisable
if racks_dict is not None:
_racks_dict = {}
for key, val in racks_dict.items():
_racks_dict[str(key)] = []
for v in val:
_racks_dict[str(key)].append(str(v))
network.graph['rack_to_ep_dict'] = _racks_dict
else:
network.graph['rack_to_ep_dict'] = None
if racks_dict is not None:
# switch racks_dict keys and values to make hashing easier
ep_to_rack_dict = {}
for key, val in _racks_dict.items():
for v in val:
if v not in ep_to_rack_dict.keys():
ep_to_rack_dict[v] = key
network.graph['ep_to_rack_dict'] = ep_to_rack_dict
else:
network.graph['ep_to_rack_dict'] = None
[docs]def gen_channel_names(num_channels):
'''Generates channel names for channels on each link in network.'''
channels = [channel+1 for channel in range(num_channels)]
channel_names = ['channel_' + str(channel) for channel in channels]
return channel_names
[docs]def add_edge_capacity_attrs(network,
edge,
channel_names,
channel_capacity,
bidirectional_links=True):
'''Adds channels and corresponding max channel bytes to single edge in network.
Args:
network (networkx graph): Network containing edges to whiich attrs will
be added.
edge (tuple): Node-node edge pair.
channel_names (list): List of channel names to add to edge.
channel_capacity (int,float): Capacity to allocate to each channel.
bidirectional_links (bool): If True, each link has capacity split equally
between src and dst port. I.e. all links have a src and dst port
which are treated separately to incoming and outgoing traffic to and
from given node (switch or server).
'''
if bidirectional_links:
attrs = {edge:
{'{}_to_{}_port'.format(edge[0], edge[1]):
{'channels':
{channel: channel_capacity/2 for channel in channel_names},
'max_channel_capacity': channel_capacity/2
},
'{}_to_{}_port'.format(edge[1], edge[0]):
{'channels':
{channel: channel_capacity/2 for channel in channel_names},
'max_channel_capacity': channel_capacity/2
}
}
}
else:
attrs = {edge:
{'channels': {channel: channel_capacity for channel in channel_names},
'max_channel_capacity': channel_capacity}}
nx.set_edge_attributes(network, attrs)
[docs]def add_edges_capacity_attrs(network,
edges,
channel_names,
channel_capacity,
bidirectional_links=True):
'''Adds channels & max channel capacitys to single edge in network.
To access e.g. the edge going from node 0 to node 1 (edge (0, 1)), you
would index the network with network[0][1]
To access e.g. the channel_1 attribute of this particular (0, 1) edge, you
would do network[0][1]['channels']['channel_1']
OR
if bidirectional_links, you do network[0][1]['0_to_1_port']['channels']['channel_1']
or network[0][1]['1_to_0_port']['channels']['channel_1] depending on which direction
of the link you want to access.
Args:
network (networkx graph): Network containing edges to which attrs will
be added.
edges (list): List of node pairs in tuples.
channel_names (list of str): List of channel names to add to edge.
channel_capacity (int, float): Capacity to allocate to each channel.
bidirectional_links (bool): If True, each link has capacity split equally
between src and dst port. I.e. all links have a src and dst port
which are treated separately to incoming and outgoing traffic to and
from given node (switch or server).
'''
if bidirectional_links:
attrs = {edge:
{'{}_to_{}_port'.format(edge[0], edge[1]):
{'channels':
{channel: channel_capacity/2 for channel in channel_names},
'max_channel_capacity': channel_capacity/2
},
'{}_to_{}_port'.format(edge[1], edge[0]):
{'channels':
{channel: channel_capacity/2 for channel in channel_names},
'max_channel_capacity': channel_capacity/2
}
}
for edge in edges}
else:
attrs = {edge:
{'channels':
{channel: channel_capacity for channel in channel_names},
'max_channel_capacity':
channel_capacity
} for edge in edges}
nx.set_edge_attributes(network, attrs)
[docs]def get_node_type_dict(network, node_types=[]):
'''Gets dict where keys are node types, values are list of nodes for each node type in graph.'''
network_nodes = []
for network_node in network.nodes:
network_nodes.append(network_node)
network_nodes_dict = {node_type: [] for node_type in node_types}
for n in network_nodes:
for node_type in node_types:
if node_type in n:
network_nodes_dict[node_type].append(n)
else:
# not this node type
pass
return network_nodes_dict
[docs]def get_fat_tree_positions(net, width_scale=500, height_scale=10):
'''Gets networkx positions of nodes in fat tree network for plotting.'''
pos = {}
node_type_dict = get_node_type_dict(net, net.graph['node_labels'])
node_types = list(node_type_dict.keys())
heights = {} # dict for heigh separation between fat tree layers
widths = {} # dict for width separation between nodes within layers
h = iter([1, 2, 3, 4, 5]) # server, rack, edge, agg, core heights
for node_type in node_types:
heights[node_type] = next(h)
widths[node_type] = 1/(len(node_type_dict[node_type])+1)
idx = 0
for node in node_type_dict[node_type]:
pos[node] = ((idx+1)*widths[node_type]*width_scale,heights[node_type]*height_scale)
idx += 1
return pos
[docs]def init_network_node_positions(net):
'''Initialises network node positions for plotting.'''
if net.graph['topology_type'] == 'fat_tree':
pos = get_fat_tree_positions(net)
else:
pos = nx.nx_agraph.graphviz_layout(net, prog='neato')
return pos
[docs]def plot_network(network,
draw_node_labels=True,
ep_label='server',
network_node_size=2000,
font_size=30,
linewidths=1,
fig_scale=2,
path_to_save=None,
show_fig=False):
'''Plots networkx graph.
Recognises special fat tree network and applies appropriate node positioning,
labelling, colouring etc.
Args:
network (networkx graph): Network object to be plotted.
draw_node_labels (bool): Whether or not to draw node labels on plot.
ep_label (str,int,float): Endpoint label (e.g. 'server'). All endpoints will have
ep_label appended to the start of their label (e.g. 'server_0', 'server_1', ...).
network_node_size (int,float): Size of plotted nodes.
font_size (int,float): Size of of font of plotted labels etc.
linewidths (int,float): Width of edges in network.
fig_scale (int,float): Scaling factor to apply to plotted network.
path_to_save (str): Path to directory (with file name included) in which
to save generated plot. E.g. path_to_save='data/my_plot'
show_fig (bool): Whether or not to plot and show fig. If True, will
return and display fig.
Returns:
matplotlib.figure.Figure: node distribution plotted as a 2d matrix.
'''
net_node_positions = init_network_node_positions(copy.deepcopy(network))
fig = plt.figure(figsize=[15*fig_scale,15*fig_scale])
# add nodes and edges
pos = {}
network_nodes = []
network_nodes_dict = get_node_type_dict(network, network.graph['node_labels'])
for nodes in list(network_nodes_dict.values()):
for network_node in nodes:
pos[network_node] = net_node_positions[network_node]
# network nodes
node_colours = iter(['#25c44d', '#36a0c7', '#e8b017', '#6115a3', '#160e63']) # server, rack, edge, agg, core
for node_type in network.graph['node_labels']:
nx.draw_networkx_nodes(network,
pos,
nodelist=network_nodes_dict[node_type],
node_size=network_node_size,
node_color=next(node_colours),
linewidths=linewidths,
label=node_type)
if draw_node_labels:
# nodes
nx.draw_networkx_labels(network,
pos,
font_size=font_size,
font_color='k',
font_family='sans-serif',
font_weight='normal',
alpha=1.0)
# fibre links
fibre_links = list(network.edges)
nx.draw_networkx_edges(network,
pos,
edgelist=fibre_links,
edge_color='k',
width=3,
label='Fibre link')
if path_to_save is not None:
tools.pickle_data(path_to_save, fig)
if show_fig:
plt.show()
return fig
if __name__ == '__main__':
#network = gen_simple_network()
#network = gen_nsfnet_network()
network = gen_fat_tree(k=3)
plot_network(network, 'figures/graph/',name='network_graph.png',with_labels=True)