In January this year, I came across a coding challenge by John Cricket to build a network modeling tool. The challenge aims to create a tool that loads a network described in a CSV file and some traffic data, then models the traffic flowing over the network to determine the utilization of each link within the network. In addition, it also models the worst-case failure to see how the network will behave.
The challenge is open to using any programming language and has a set of steps to follow to build the tool. I decided to create the tool in Python and in this article, I'd like to have a walkthrough of how I built the tool.
Step 1
The challenge assumes that we have this network:
The network is represented like this:
Our first task is to have a CSV file for the network and load that file into a suitable data structure. You can download the csv file here. Let's work on this first step.
First, create a file, main.py
, and import the pandas
module to read the CSV file:
"""The main entry point."""
import pandas as pd
df = pd.read_csv("network.csv")
(rows, cols) = df.shape
We also need to know how many rows and columns are there, this information will be useful shortly.
Let's create a folder, graph
. Then create a file inside, graph.py
. We'll use an adjacent matrix to represent our graph network and assume that each link is bi-directional to make it easy:
"""Graph class used to represent nodes as an adjacent matrix."""
class Graph:
def __init__(self):
self.graph = []
self.vertices = []
self.vertices_no = 0
def add_vertex(self, v: str) -> None:
"""Adds a node to the graph.
Args:
v: str. A node to add.
"""
if v in self.vertices:
print("Vertex ", v, " already exists")
else:
self.vertices.append(v)
self.vertices_no += 1
if self.vertices_no > 1:
for vertex in self.graph:
vertex.append(0)
temp = []
for i in range(self.vertices_no):
temp.append(0)
self.graph.append(temp)
def add_edge(self, v1: str, v2: str, weight: int) -> None:
"""Adds an edge betweeen two nodes.
Args:
v1: str. The start node.
v2: str. The end node.
weight: int. The weight between the two nodes.
"""
if v1 not in self.vertices:
print("Vertex ", v1, " does not exist.")
elif v2 not in self.vertices:
print("Vertex ", v2, " does not exist.")
else:
index1 = self.vertices.index(v1)
index2 = self.vertices.index(v2)
self.graph[index1][index2] = weight
self.graph[index2][index1] = weight
Inside the Graph
class, we have initialized a graph and vertices list. The graph will contain the entire graph network while the vertices list will contain nodes of the graph. In the beginning, the number of vertices is 0 and it will increase every time we add a node/vertex. Notice that I'm using the word node and vertex interchangerbly.
We have an add_vertex
method. The method takes a node as an argument and checks if the node already exists in the vertices list. If it exists, it will print out that the node already exists. If the node does not exist, it will be added to the vertices list and increase the vertices_no
by 1.
Next, it checks if there is more than one node by checking the vertices_no
. If there is more than one node, the code loops through each row and adds a new column for the new node initializing it to 0
.
temp
creates a new list with a length equal to the current number of nodes and initializes all elements to 0. The list is added to the graph as a new expansion of the adjacent matrix to include the new node.
The add_edge
method adds an edge between two nodes. It takes in the two nodes and the weight between them. Before adding the weight, it checks if both nodes exist in the vertices list. If both nodes exist, it gets the indices of both nodes and updates the adjacency matrix. Since the graph is undirected, it updates both [index1][index2]
and [index2][index1]
.
Let's go back to our main.py and add the following code.
...
nodes_list = []
for i in range(rows):
start = df.iat[i, 1]
end = df.iat[i, 2]
nodes_list.append(start)
nodes_list.append(end)
This iterates over each row in the data frame and gets the values in the second and third columns, representing the start and end nodes. The nodes are appended to the nodes_list
.
Create another folder, utils
and create a file inside, utils.py
. Add the following code to utils.py
:
from functools import reduce
def unique(nodes: list[str]) -> list[str]:
"""Returns a list with unique elemets.
Args:
nodes: List[str]. Graph nodes.
"""
unique_values = reduce(lambda x, y: x + [y] if y not in x else x, nodes, [])
return unique_values
The unique
function returns a list of unique elements from the input list.
In our main.py
file, add the following code:
from graph.graph import Graph
from utils.utils import unique
...
graph = Graph()
unique_nodes_list = unique(nodes_list)
for node in unique_nodes_list:
graph.add_vertex(node)
for i in range(rows):
start_node = df.iat[i, 1]
end_node = df.iat[i, 2]
weight = df.iat[i, 4]
graph.add_edge(start_node, end_node, weight)
This imports the Graph
class and the unique
function. We pass the nodes_list
to the unique function and get the unique_nodes_list
. Then, the code loops through the unique nodes and passes each node to the add_vertex
method.
Next, the code loops through each data frame row to collect the start_node
, end_node
, and weight
. We pass these arguments to the add_edge
method.
We have successfully implemented the first step of loading the CSV file into a suitable data structure. In part 2, we will be writing tests for the methods in the Graph
class.
All the code for this first step can be found on GitHub.
Please Like, Comment, or Share if you found this article helpful.