Fundemantal of AI - Project
Fundemantal of AI - Project
Fundemantal of AI - Project
Artificial Intelligence
Fall 2023
[Solutions to this assignment must be submitted via CANVAS prior to midnight on the due date. These
dates and times vary depending on the milestone to be submitted. Submissions up to one day late will be
penalized 10% and a further 10% will be applied for the next day late. Submissions will not be accepted if
more than two days later than the due date.]
This project may be undertaken in pairs or individually. If working in a pair, state the names of the two
people undertaking the project and the contributions that each has made. Only ONE submission should
be made per group.
You will make use of the Networkx environment that you should be familiar with from Project 1.
Purpose: To gain a thorough understanding of the working of an agent that maximizes flow through a
network. Maximizing network flow ensures that the network’s resources are fully utilized according to
their capacities, and this reduces time to move commodities through the network. Numerous real-world
applications benefit from maximizing flow such as maximizing data throughput in a computer network,
maximizing the flow of oil through a network of pipelines, etc. Figures 1 A and B below provide a
concrete example of a network flow problem. The source node is S, and the sink (destination) node is T.
V 1/3
2/6
1/5 T
4/8
3/3 Flow into T before optimization = 1+4+4=9
S
W 2/9 4/5
2/2
4/5
U Z
2/3
S
W 3/9 5/5
2/2
4/5
U Z
2/3
Figures 1 A and B
Each link has a capacity (e.g., maximum number of barrels of oil that can flow per hour) and an actual
flow. The actual flow in the above figure for each edge is indicated in red while the capacity is given in
black. Figure 1 A represents a sub optimal flow as we can see that the flow across most of the edges do
not utilize their full capacity. The solution in Figure 1 B utilizes the full capacity of the network and is
thus an optimal solution.
One constraint that must be enforced is the law of conservation: the sum of the outflows from a node
cannot exceed the sum of inflows into that same node. Thus, for example, the sum of the inflows into
node w must be equal to the sum of the outflows from node w.
In practice, on a large network it may take too long to obtain the optimal solution and therefore we will
use an extended version of the Hill Climbing (HC) algorithm (see Tutorial 3 for details) to construct a
solution. We saw in the lectures that hill climbing can return high quality solutions that avoid getting
stuck in local minima and return solutions that are as close to the global (optimal) solution as possible.
The solution that you generate must also satisfy the conservation law across all nodes in the network.
Hill Climbing requires an initial solution to start off with and this solution can be generated randomly
(refer Tutorial 4 for code for this purpose). Of course, you need to ensure that your start solution is
valid: all links must obey the law of conservation and all edge values must not exceed their assigned
capacities. Edge capacities can be assigned at random to be an integer between 1 and 10.
Once an initial solution is in place, HC needs to generate in each iteration a neighbor of the solution that
existed in the previous iteration until no uphill moves are possible (i.e., when the value of the heuristic
function is 0 for all child nodes of the current node).
Jot down any questions/doubts that you may have and feel free to ask me questions in class or in
person. Together with your partner, work out a strategy before you start coding the solution in Python.
Given the limited timeframe for the project, some simplifications have been assumed.
Environment Description: The environment is a network that is specified in the form of a graph
structure as represented in Figures 1 A and B (simply a sample, not the entire network). The graph that
you will use to represent the network will consist of 30 nodes, with two different average connectivity
values (see below).
Your task in this project is to implement the following requirements. The project has two milestones,
with Part A representing design of algorithms and Part B consisting of algorithm implementation and
experimentation. The milestones have different deadlines as shown below.
Part A
Produce pseudo code versions of algorithms needed to construct:
a) A heuristic function suitable for a hill climbing solution for the network resource problem.
b) A successor function – a successor function specifies how a node is expanded into its child
nodes. Your successor function should contain code that (1) generates paths and (2)
removes redundant paths.
c) The hill climbing algorithm itself takes as parameters: source and sink nodes and calls the
two functions mentioned in a) and b) above.
Part B
Using the pseudo code that you produced in Part A above to implement a Python program to maximize
the flow. For all of the requirements below use 0 and 29 for source and sink nodes respectively. Your
Python program should meet the following requirements:
R1: Visualize your solution for the graph given in Figure 1 B, showing the capacities and the actual flow
along each edge that is returned by executing your HC algorithm. This is an important test case for your
code and will require you to hard code your graph G. Run your HC code for the graph in Figure 1 A and
present its answer to the total flow into the sink node T. Verify that the value returned is the same value
given in Figure 1 B. (30 marks)
R2: Run your HC code on a graph G containing 30 nodes and an average connectivity of 3 (connectivity
parameter of 0.1). Work out the total flow value tf into node T and compare it with the total flow value
tf_net returned by the NetworkX version of the hill climbing algorithm. Present both values, tf and
tf_net. (10 marks)
Notes:
1. Produce ONE pdf document that contains pseudo code, actual Python code and the answers to the
questions. Do NOT bury answers to questions as comments in your code. If you are unsure how to
produce an original pdf from your Google Co-lab notebook, refer to this tutorial on Youtube: Bing
Videos. Do NOT simply use the print option as this will result in an image. If you submit an image
version of a pdf it will not be graded.
2. If you finish Part A before its deadline, start working on Part B immediately afterwards.
min_increase ∞
FOR each edge(u, v) in path:
unused_capacity capacity.egde(u, v) – flow.edge(u, v)
IF unsued_capacity < min_increase:
min_increase = unused_capacity
PROJECT 2 - PART B
STUDENT NAME: SAMIR TARDA
Source Code:
https://colab.research.google.com/drive/1QZx-ix7yf-ZwE7hggSrbaHtgHdBsczJt?usp=sharing
#Part B - R1
import networkx as nx
import random
import matplotlib.pyplot as plt
# 1. GRAPH INITIALIZATION
source = "S"
sink = "T"
G = nx.DiGraph()
G.add_edges_from([
("S", "V", {'flow': 2}), ("S", "V",{'capacity': 6}),
("S", "W", {'flow': 3}), ("S", "W",{'capacity': 3}),
("S","U", {'flow': 4}), ("S","U",{'capacity': 5}),
("V","W", {'flow': 1}), ("V","W",{'capacity': 5}),
("V","T", {'flow': 1}), ("V","T",{'capacity': 3}),
("W","Z", {'flow': 2}), ("W","Z",{'capacity': 9}),
("W","T", {'flow': 4}), ("W","T",{'capacity': 8}),
("U","W", {'flow': 2}), ("U","W",{'capacity': 2}),
("U","Z", {'flow': 2}), ("U","Z",{'capacity': 3}),
("Z","T", {'flow': 4}), ("Z","T",{'capacity': 5})
])
# 2. GRAPH VISUALIZATION
links = [(u, v) for (u, v, d) in G.edges(data=True)]
pos = nx.nx_pydot.graphviz_layout(G)
nx.draw_networkx_nodes(G, pos, node_size=1200, node_color='lightblue',␣
↪linewidths=0.05) # draw nodes
1
capacity_data = nx.get_edge_attributes(G, 'capacity')
edge_labels = {(u, v): f"{edge_data[(u, v)]}/{capacity_data[(u, v)]}" for u, v␣
↪in edge_data}
plt.show()
return valid_paths
# 5.1 Updating the flow along each edge and calculating the total flow of all␣
↪edges
2
# - to maximize the flow from the source to the sink in graph G.
def hill_climbing(source, sink, G):
while True:
successors = generate_successors(source, sink, G)
if not successors:
break
increase_value = compute_heuristic(selected_path, G)
for u, v in selected_path:
increase_flow(G, u, v, increase_value)
<ipython-input-1-3e8b6b54804c>:25: DeprecationWarning:
nx.nx_pydot.graphviz_layout depends on the pydot package, which hasknown issues
and is not actively maintained. Consider usingnx.nx_agraph.graphviz_layout
instead.
See https://github.com/networkx/networkx/issues/5723
pos = nx.nx_pydot.graphviz_layout(G)
3
Total Flow Value from S to T: 14
plt.show()
<ipython-input-2-976b31985a69>:3: DeprecationWarning:
nx.nx_pydot.graphviz_layout depends on the pydot package, which hasknown issues
and is not actively maintained. Consider usingnx.nx_agraph.graphviz_layout
instead.
4
See https://github.com/networkx/networkx/issues/5723
pos = nx.nx_pydot.graphviz_layout(G)
#Part B - R2
[1]: import networkx as nx
import random
5
# 2.2 Removing any edges that could create a bidirectional connection between␣
↪two nodes.
for u in G.nodes():
adj_list = G.adj[u]
for nbr, datdict in adj_list.copy().items():
print(u,nbr)
if (u > nbr) and G.has_edge(nbr, u):
G.remove_edge(u, nbr)
print (G.edges())
0 1
0 11
0 14
0 16
0 18
1 5
1 7
1 10
2 4
2 10
2 11
2 15
2 17
3 2
3 5
3 17
3 21
3 26
4 5
5 6
5 11
6 2
6 10
6 14
6 26
7 12
7 13
8 27
9 3
9 6
10 12
10 16
10 21
10 22
6
10 26
11 1
11 3
11 8
11 17
12 8
12 21
12 29
13 5
13 18
13 21
13 29
14 15
14 24
15 0
15 1
15 7
15 16
16 3
16 11
16 19
17 13
18 4
18 15
18 17
18 19
18 26
19 3
19 12
19 22
19 29
19 20
20 25
21 0
21 2
21 16
22 3
22 9
22 12
22 16
23 14
23 25
24 2
24 3
24 13
25 26
26 8
26 18
7
27 10
27 12
27 18
27 23
27 28
28 2
28 12
29 4
29 8
29 10
29 24
[(0, 1), (0, 11), (0, 14), (0, 16), (0, 18), (1, 5), (1, 7), (1, 10), (2, 4),
(2, 10), (2, 11), (2, 15), (2, 17), (3, 2), (3, 5), (3, 17), (3, 21), (3, 26),
(4, 5), (5, 6), (5, 11), (6, 2), (6, 10), (6, 14), (6, 26), (7, 12), (7, 13),
(8, 27), (9, 3), (9, 6), (10, 12), (10, 16), (10, 21), (10, 22), (10, 26), (11,
1), (11, 3), (11, 8), (11, 17), (12, 8), (12, 21), (12, 29), (13, 5), (13, 18),
(13, 21), (13, 29), (14, 15), (14, 24), (15, 0), (15, 1), (15, 7), (15, 16),
(16, 3), (16, 11), (16, 19), (17, 13), (18, 4), (18, 15), (18, 17), (18, 19),
(18, 26), (19, 3), (19, 12), (19, 22), (19, 29), (19, 20), (20, 25), (21, 0),
(21, 2), (21, 16), (22, 3), (22, 9), (22, 12), (22, 16), (23, 14), (23, 25),
(24, 2), (24, 3), (24, 13), (25, 26), (26, 8), (27, 10), (27, 12), (27, 18),
(27, 23), (27, 28), (28, 2), (28, 12), (29, 4), (29, 8), (29, 10), (29, 24)]
[2]: True
[3]: # 2.4 Assigning a 'flow' and a 'capacity' for each node in the graph
for u, v in G.edges():
G[u][v]['flow'] = random.randint(0, 5)
G[u][v]['capacity'] = G[u][v]['flow'] + random.randint(1, 10)
8
def generate_successors(source, sink, G):
return valid_paths
# 5.1 Updating the flow along each edge and calculating the total flow of all␣
↪edges
while True:
successors = generate_successors(source, sink, G)
if not successors:
break
increase_value = compute_heuristic(selected_path, G)
for u, v in selected_path:
increase_flow(G, u, v, increase_value)
9
return sum_of_flow_into_sink(G, sink)
def initialize_graph(N):
G = nx.gnp_random_graph(N, 0.1, directed=True)
# 2.2 Remove any edges that could create a bidirectional connection between␣
↪two nodes.
# This simplification can help avoid complexities in network flow␣
↪algorithms.
for u in G.nodes():
adj_list = G.adj[u]
for nbr, datdict in adj_list.copy().items():
if (u > nbr) and G.has_edge(nbr, u):
G.remove_edge(u, nbr)
10
# 2.3 Assigning a 'flow' and a 'capacity' for each node in the graph
for u, v in G.edges():
G[u][v]['flow'] = random.randint(0, 5)
G[u][v]['capacity'] = G[u][v]['flow'] + random.randint(1, 10)
return G
[7]: print(total_flow_values)
[18, 15, 8, 28, 3, 10, 25, 2, 14, 18, 10, 6, 6, 7, 37, 8, 4, 18, 22, 13, 22, 10,
15, 18, 9, 8, 14, 8, 26, 15]
def initialize_graph(N):
G = nx.gnp_random_graph(N, 0.067, directed=True)
11
for u in G.nodes():
in_list = G.in_edges(u)
out_list = G.out_edges(u)
if len(in_list) == 0 and u > 0:
G.add_edge(u - 1, u)
if len(out_list) == 0 and u < N - 1:
G.add_edge(u, u + 1)
# 2.2 Remove any edges that could create a bidirectional connection between␣
↪two nodes.
# This simplification can help avoid complexities in network flow␣
↪algorithms.
for u in G.nodes():
adj_list = G.adj[u]
for nbr, datdict in adj_list.copy().items():
if (u > nbr) and G.has_edge(nbr, u):
G.remove_edge(u, nbr)
# 2.3 Assigning a 'flow' and a 'capacity' for each node in the graph
for u, v in G.edges():
G[u][v]['flow'] = random.randint(0, 5)
G[u][v]['capacity'] = G[u][v]['flow'] + random.randint(1, 10)
return G
#Part B - R3.c
Yes, there’s a noticeable difference. T his d ifference is no t su rprising, an d it ca n be at tributed to
the following reasons: 1. Graph Connectivity: A graph with a higher average connectivity
12
(like R3.a) typically has more edges than one with a lower average connectivity (like R3.b). More
edges mean there are potentially more paths from the source to the sink, leading to a greater
chance of finding paths with higher capacities and flows. Imagine we have two cities; each one
represents a “graph”. One city where almost every street (edge) is connected to numerous others
vs. the second city with fewer connected roads. The many different paths in this highly accessible
network of the first city makes it a lot easier to travel from a source location to a given destination
(sink). 2. Redundant Paths: as mentioned earlier, with a higher average connectivity, there’s a
greater chance of having multiple paths between any two nodes. This redundancy in paths provides
flexibility and multiple options for the hill-climbing algorithm. It allows for better distribution,
optimization, and utilization of the available capacities in the graph, leading to an enhanced flow
from the source to the sink. For example, in the context of the hill-climbing algorithm, multiple
paths between the source and the sink provide alternative routes for routing the flow. If one path
becomes saturated (i.e., reaches its capacity), the algorithm can look for another path that still
has unused capacity. This increases the chances of finding paths where additional flow can be
pushed through. 3. Bottleneck Reduction: In network flow problems, the maximum flow is
often determined by the “bottleneck”, typically an edge (or a set of edges) that has the smallest
capacity along a path from the source to the sink. With more edges and paths available (due
to higher connectivity), the impact of these bottlenecks can be minimized, as flow can be routed
through alternate paths.
References
1. Mahmood, A., Javaid, N., Razzaq, S., Ilahi, M., Khan, Z.A., Qasim, U. (2021). Optimal Route
Selection with Load Balancing in IoT-Based Vehicular Networks Using water Flow-Based Technique.
IEEE Access, 9, 5568-5583.
2. Nandi, S., Nandi, S.K., Yadav, A., Singh, S.P., Kumar, A., Rodrigues, J.J. (2022). Telco-Cloud-
Empowered 6G: A Graph-Theory-Based Connectivity Model Using SDN. IEEE Network, 36(2),
184-192.
13