Ok, now considering this code:
import simpy
import random
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from prettytable import PrettyTable

# Parameters
num_edge_nodes = 2
num_cloud_nodes = 4
edge_buffer_size = 10
cloud_buffer_size = 15

# service times
cloud_server_time = 10
A_edge_service_time = 4
A_cloud_service_time = 2
B_edge_sevice_time = 2
B_cloud_service_partialprocess = 4
B_cloud_service_fullprocess = 5
propagation_delay = 1

cloud_server_times = [8, 10, 12] # Add a list of service times for each cloud server type
edge_server_times = [A_edge_service_time, B_edge_sevice_time] # Add edge service times for easy indexing


arrival_rate = 0.8

edge_costs = [1, 2, 3]
cloud_costs = [2, 4, 6]
# Modify cloud_costs and edge_costs to support more configurations
cloud_costs = [cloud_cost * server_time for cloud_cost, server_time in zip(cloud_costs, cloud_server_times)]
edge_costs = [edge_cost * server_time for edge_cost, server_time in zip(edge_costs, edge_server_times)]


# Measurements
class Measure:
def init(self, N_arr_a, N_arr_b, drop, total_queuing_delay, edge_cost, cloud_cost):
self.N_arr_A = N_arr_a
self.N_arr_B = N_arr_b
self.drop = drop
self.total_queuing_delay = total_queuing_delay
self.edge_cost = edge_cost
self.cloud_cost = cloud_cost

measurements = Measure(0, 0, 0, 0, 0, 0)

def packet_arrivals(env, micro_data_center, cloud_data_center, data, edge_buffer, cloud_buffer, f_value):
packet_type_options = [‘A’, ‘B’]
packet_id = 1

while True:
packet_type = random.choices(packet_type_options, weights=(1 - f_value, f_value))[0]

if packet_type == ‘A’:
data.N_arr_A += 1
else:
data.N_arr_B += 1

arrival_time = env.now # Adding current time stamp for calculating delay
if len(micro_data_center.items) < edge_buffer:
if packet_type == ‘A’:
micro_data_center.put((packet_id, packet_type, A_edge_service_time, arrival_time))
elif packet_type == ‘B’:
micro_data_center.put((packet_id, packet_type, B_edge_sevice_time, arrival_time))
else:
if len(cloud_data_center.items) < cloud_buffer:
if packet_type == ‘A’:
cloud_data_center.put((packet_id, packet_type, A_cloud_service_time + propagation_delay))
elif packet_type == ‘B’:
cloud_data_center.put((packet_id, packet_type, B_cloud_service_fullprocess + propagation_delay))
else:
data.drop += 1

yield env.timeout(random.expovariate(arrival_rate))
packet_id += 1

def edge_node(env, micro_data_center, cloud_data_center, node_id, data, edge_cost, service_time):
while True:
packet_id, packet_type, packet_processing_time, arrival_time = yield micro_data_center.get()

queuing_delay = env.now - arrival_time
data.total_queuing_delay += queuing_delay
data.edge_cost += edge_cost * packet_processing_time
print(f"Edge Node {node_id} processed packet {packet_id} of type {packet_type} at time {env.now}“)

yield env.timeout(service_time) # Use parametrized service time for delay
if packet_type == ‘B’:
yield cloud_data_center.put((packet_id, packet_type, B_cloud_service_partialprocess + propagation_delay))

def cloud_node(env, cloud_data_center, node_id, data, cloud_cost, service_time):
while True:
packet_id, packet_type, packet_processing_time = yield cloud_data_center.get()

yield env.timeout(service_time) # Use parametrized service time for delay
data.cloud_cost += cloud_cost * packet_processing_time
print(f"Cloud Node {node_id} processed {packet_type} packet {packet_id} (including propagation delay) at time {env.now}”)

# Simulation setup
simtime = 200000
packet_loss_rates = []
f_list = np.linspace(0, 1, 5)

# In the simulation loop, run the simulation by varying the server types

combinations = []
for f in f_list:
for i in range(len(edge_costs)):
for j in range(len(cloud_costs)):
# Create a new environment for each combination
env = simpy.Environment()

# Initialize queue and measurements
micro_data_center = simpy.Store(env)
cloud_data_center = simpy.Store(env)
measurements = Measure(0, 0, 0, 0, 0, 0)

# Configure the packet arrival function
env.process(packet_arrivals(
env, micro_data_center, cloud_data_center, measurements, edge_buffer_size, cloud_buffer_size, f))

# Configure edge nodes with specific server types
for node_id in range(num_edge_nodes):
env.process(edge_node(env, micro_data_center, cloud_data_center, node_id + 1,
measurements, edge_costs[i], edge_server_times[node_id % len(edge_server_times)]))

# Configure cloud nodes with specific server types
for node_id in range(num_cloud_nodes):
env.process(cloud_node(env, cloud_data_center, node_id + 1,
measurements, cloud_costs[j], cloud_server_times[node_id % len(cloud_server_times)]))

# Run the simulation and gather results
env.run(until=simtime)
packet_loss_rate = measurements.drop / (measurements.N_arr_A + measurements.N_arr_B)

# Append the current combination’s cost and packet loss rate to the results
combinations.append({
“edge_cost”: edge_costs[i],
“cloud_cost”: cloud_costs[j],
“f”: f,
“total_cost”: measurements.edge_cost + measurements.cloud_cost,
“queuing_delay”: measurements.total_queuing_delay,
“packet_loss_rate”: packet_loss_rate
})

df = pd.DataFrame(combinations)
print(combinations) # Print the list of combinations and their corresponding costs, queuing delays, and packet drop rates

#### TABLE #####
# Create the table with headers
table = PrettyTable()
table.field_names = [“f Value”, “Edge Cost”, “Cloud Cost”, “Total Cost”, “Queuing Delay”, “Packet Drop Rate”]

# Iterate through the combinations and add a row in the table for each one
for comb in combinations:
table.add_row([comb[“f”],
comb[“edge_cost”],
comb[“cloud_cost”],
comb[“total_cost”],
comb[“queuing_delay”],
comb[“packet_loss_rate”]])

# Format the table
table.align = ‘r’

# Sort the table by Total Cost
table.sortby = “Total Cost”

# Pretty print the table
print(table)

#### PLOTTINGS #####

# Load the data into a pandas DataFrame
df = pd.DataFrame(combinations)

# Plot Total Cost vs ‘f’ for different edge_cost and cloud_cost combinations
for edge_cost, edge_group in df.groupby(‘edge_cost’):
for cloud_cost, group in edge_group.groupby(‘cloud_cost’):
plt.plot(group[‘f’], group[‘total_cost’], label=f’Edge: {edge_cost}, Cloud: {cloud_cost}‘)
plt.xlabel(‘f (proportion of traffic offloaded to the cloud)’)
plt.ylabel(‘Total Cost’)
plt.title(‘Total Cost vs f’)
plt.legend()
plt.show()

# Plot Queuing Delay vs ‘f’ for different edge_cost and cloud_cost combinations
for edge_cost, edge_group in df.groupby(‘edge_cost’):
for cloud_cost, group in edge_group.groupby(‘cloud_cost’):
plt.plot(group[‘f’], group[‘queuing_delay’], label=f’Edge: {edge_cost}, Cloud: {cloud_cost}’)
plt.xlabel(‘f (proportion of traffic offloaded to the cloud)’)
plt.ylabel(‘Queuing Delay’)
plt.title(‘Queuing Delay vs f’)
plt.legend()
plt.show()

# Plot Packet Loss Rate vs ‘f’ for different edge_cost and cloud_cost combinations
for edge_cost, edge_group in df.groupby(‘edge_cost’):
for cloud_cost, group in edge_group.groupby(‘cloud_cost’):
plt.plot(group[‘f’], group[‘packet_loss_rate’], label=f’Edge: {edge_cost}, Cloud: {cloud_cost}')
plt.xlabel(‘f (proportion of traffic offloaded to the cloud)’)
plt.ylabel(‘Packet Loss Rate’)
plt.title(‘Packet Loss Rate vs f’)
plt.legend()
plt.show()


Gimme the full modified code in order to perform the following task:
Now, assume to install half the number of Cloud servers(initially it was 4 and now it is 2) , keeping the same value
of f<0.4 :
• In this case, can you identify the proper configuration of server types allowing
to reduce the cost below the same desired threshold?
• Compare the obtained queueing delay and cost under these two scenarios (i.e.,
N Cloud servers versus N/2 Cloud servers), also highlighting how packets of
type A and packets of type B are differently affected in terms of delay and
packet drop probability