consider this lab report:
Title: Lab Report on Management and Content Delivery for Smart Networks:
Algorithms and Modeling

Introduction

The objective of this lab experiment is to simulate the operation of a part of an Industrial Internet of Things (IIoT) system modeled as a queuing system and to analyze its performance under different configurations and network parameter settings. The scenario considered is Industry 4.0, where machinery components in the production line are equipped with sensors and actuators, which together constitute an IIoT system. The study aims to enhance the understanding of how different scenario configurations and network parameter settings may affect system behavior, performance, and optimization.

Methodology

To investigate the system behavior, performance, and cost under various configurations of edge and Cloud data center servers, the following tasks were performed:

1. Simulation of a single-server system with finite buffer for both Micro Data Center and Cloud Data Center, with a packet drop probability analysis for those forwarded to the Cloud for any reason.
2. Investigation of the impact of the size of the buffer in the Micro and Cloud Data Centers on overall system performance and the effect of different input data characteristics on the overall average queuing delay.
3. Determination of the minimum service rate and number of servers required to reduce the queuing time of type A packets below a certain threshold (Tq).
4. Analysis of multi-server systems in terms of operational cost and performance trade-offs under different configurations and time-varying input data.

Results

Task 1:
• The system’s behavior showed a transient period before reaching steady-state.
• A warm-up transient removal method was applied in the simulations, which ensured more accurate performance metrics during subsequent analysis.

Task 2:
• The size of the buffer in the Micro Data Center greatly influenced the overall system performance, highlighting the importance of adequate buffering in IoT systems.
• The buffer size in the Cloud Data Center had a similar impact on the system performance, further emphasizing the crucial role of proper buffer dimensioning.
• The characteristics of input data (different values of f) affected the overall average queuing delay, suggesting that varying data type composition should be considered when designing and optimizing IoT systems.

Task 3:
• The minimum value of the average service rate required to reduce the queuing time of type A packets below the threshold Tq was found. This information can help in choosing the appropriate processing capabilities for such systems.
• The minimum number of servers needed to reduce the queuing time below the threshold Tq in a multi-server system was also identified, providing insights into scalability requirements and resource allocation.

Task 4:
• Different combinations of server types and their effects on overall cost, queuing delays, and packet drop probability were examined, finding trade-offs between these factors. This analysis aids in decision-making when selecting server types and balancing cost and performance objectives.
• The best combination of server types allowing for cost reduction below a desired threshold was identified while respecting Tq for type A packets, demonstrating the feasibility of optimizing costs without compromising crucial performance constraints.
• Comparisons were made between scenarios with N Cloud servers and N/2 Cloud servers in terms of queuing delay, cost, and packet drop probability, highlighting the different effects on packets of type A and type B. This comparison allows the understanding of the cost-benefit trade-offs when deciding on server allocations.

Conclusion

The simulation and analysis of the IoT system with different configurations, parameter settings, and input data provided valuable insights into system performance, cost, and optimization for Industry 4.0 scenarios. The findings showed the significance of buffer size, service rates, and server types on overall performance and optimization. Moreover, this study demonstrated the importance of considering varying data type composition and cost-benefit trade-offs in the design and optimization of IoT systems. The insights gained can be instrumental in deploying and managing effective IoT systems in real-world industrial environments.


I used the following code to perform the tasks. (Although I have modified it for each of the tasks , I used it as the base code for all the 4 tasks)
now I want you to add some description of the code to the report and rewrite the report for me.
|here is 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
half_num_cloud_nodes = 2
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.5

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


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 = 250000
packet_loss_rates = []
f_list = np.linspace(0, 1, 10)
desired_f = 0.4
desired_cost_threshold = 500000
# In the simulation loop, run the simulation by varying the server types
def run_simulation(num_cloud_nodes):
    combinations = []
    for f in f_list:
        if f <= desired_f:
            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
                    })
    return combinations

combinations_original = run_simulation(num_cloud_nodes)
combinations_half = run_simulation(half_num_cloud_nodes)

df_original = pd.DataFrame(combinations_original)
df_half = pd.DataFrame(combinations_half)

# Filtering proper configurations and comparing results
conf_original = df_original[df_original['f'] <= desired_f]
conf_half = df_half[df_half['f'] <= desired_f]

print("Original Configuration:")
print(conf_original)
print("Half Configuration:")
print(conf_half)

# Add this plotting function after running both scenarios
def plot_metrics(df, title_prefix):
    # 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 cost: {edge_cost}, Cloud cost: {cloud_cost}')
    plt.xlabel('f (proportion of traffic offloaded to the cloud)')
    plt.ylabel('Total Cost')
    plt.title(f'{title_prefix} 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 cost: {edge_cost}, Cloud cost: {cloud_cost}')
    plt.xlabel('f (proportion of traffic offloaded to the cloud)')
    plt.ylabel('Queuing Delay')
    plt.title(f'{title_prefix} 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 cost: {edge_cost}, Cloud cost: {cloud_cost}')
    plt.xlabel('f (proportion of traffic offloaded to the cloud)')
    plt.ylabel('Packet Loss Rate')
    plt.title(f'{title_prefix} Packet Loss Rate vs f')
    plt.legend()
    plt.show()

# Call the plotting function for both scenarios
plot_metrics(df_original, "Original Configuration:")
plot_metrics(df_half, "Half Configuration:")