The 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 = 10000
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
combinations = [{'edge_cost': ... }]  # Provided data
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()


prompts with the error:
Traceback (most recent call last):
  File "G:\My Drive\Ict 2023 - second semester\Management\Lab\Lab 2\Tasks\Task 4 - Multiserver , Operational cost\b. tradeoff between cost, lost and delay\task 4 -multi server - tradeoff.py", line 180, in <module>
    for cloud_cost, group in edge_group.groupby('cloud_cost'):
  File "C:\Users\98915\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pandas\core\frame.py", line 8389, in groupby
    return DataFrameGroupBy(
  File "C:\Users\98915\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pandas\core\groupby\groupby.py", line 959, in __init__
    grouper, exclusions, obj = get_grouper(
  File "C:\Users\98915\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pandas\core\groupby\grouper.py", line 888, in get_grouper
    raise KeyError(gpr)
KeyError: 'cloud_cost'