The purpose of this lab is to simulate the operation of a portion of an Industrial Internet of
Things (IIoT), that is modeled as a queuing system, and investigate its performance under
variable configurations, to understand how different scenario configurations and network
parameter settings may affect the system behavior and performance.
Consider the Industry 4.0 scenario depicted in Fig. 1. Each machinery component of the
production line is equipped with a set of sensors and actuators, that build up an IIoT system.
In particular, a number of sensor devices collect real time monitoring data at the edge of the
network, to collect several types of information about the status and the operation of the
various production line components. These data can be used, either directly or after some
processing, to take decisions and perform specific actions on the production line by means of
actuators, in order to manage the system operation, modulate the production system speed,
prevent or tackle service interruptions, etc.
More in details, data collected by the sensors are sent to a local Micro Data Center (arrow 1),
that provides computational capacity close to edge of IIoT network by means of edge nodes,
which are managed by an edge controller. edge nodes pre-process data (arrow 2), so that
most requests can be fulfilled locally, whereas only computationally intensive requests are
forwarded to a Cloud data center (arrow 3), thus saving bandwidth and energy. Finally, based
on the data processing output, operative commands are generated and sent to the actuators
(dashed arrows). In case all the edge nodes are busy, incoming data can be buffered, if a
buffer is present. However, if the buffer is full or it is not envisioned at all, data packets are
directly forwarded to the Cloud Data Center to perform the entire processing tasks (arrow
4).
1In the considered queuing system, the customers represent the IIoT data packets that arrive
at that Micro Data Center. The service represents the local processing of the data by edge
nodes before generating the actuation message packets or before forwarding data to the
Cloud for further computational processing. Finally, the waiting line represents the buffer
where packets are stored before performing the computational tasks. Packets that are sent to
the Cloud are further processed by Cloud server(s) before generating the actuation message
packets that are sent to the actuators. In this case the service is represented by the data
processing carried out by the Cloud server(s). Even in the Cloud a buffer may be envisioned.
Two types of data can be generated by the sensor nodes and sent to the Micro Data Center,
Figure 1: IoT system in an Industry 4.0 scenario.
each corresponding to a different class of task that can be performed by the actuators:
Class A - High priority tasks: these tasks are delay sensitive, hence implying high
priority operations that must be performed on the production line within a strict time
deadline. The packets generated by the sensors that are related to high priority tasks (type
A packets) typically require a simple processing, that can be locally performed by the edge
nodes.
Class B - Low priority tasks: these tasks require more complex computational operations, however the completion of these tasks is not constrained by strict time deadlines, hence
resulting in delay tolerant tasks that can be performed in a longer time period. Due to the
need for more complex computational operations, this type of packets (type B packets), after
a local pre-processing at the Micro Data Center level, must be forwarded to the Cloud Data
Center to complete the processing.
23
Denote f the fraction of packets of type B, i.e. the low priority data packets arriving
at the Micro Data Center from the sensors that must be further forwarded to the Cloud
Data Center after local pre-processing by the edge node(s), due to more complex required
computational tasks. In this case, assume a reasonably increased average service time on
the Cloud server(s). Note that, although the required tasks are more complex, the Cloud
Servers are likely to be better performing than the edge nodes.
Furthermore, when data cannot be locally pre-processed due to busy edge nodes and full
buffer, any incoming data (either of type A or B) are forwarded to the Cloud without any
local pre-processing. Since these forwarded data are fully processed in the Cloud, they experience an average service time that reflect the fact that both the simple pre-processing tasks
(for type A and type B data) and the more complex computational tasks (only for type B
data) are performed in the Cloud Data center itself.
You can include the propagation delay in your analysis, assuming a reasonable additional
delay due to the data transmission from the Micro Data Center to the Cloud Data Center
and for the transmission of the commands from Cloud Data Center to the actuators. The
other propagation delays can be neglected.
Tasks
1. For the first task, assume a single server with finite buffer for both the Micro Data
Center and the Cloud Data Center, with f=0.5. Focusing on the Cloud Data Center
sub-system, the packet drop probabil-ity (version B) of those data packets that are
forwarded to the Cloud for any reason.
(a) Observe the system behavior during the warm-up transient period and identify
the transition to the steady state.
(b) Try to apply a method to remove the warm-up transient in your simulations.
2. Consider now the overall system operation, including both the Micro Data Center and
the Cloud Data Center.
(a) How does the size of the buffer in the micro Data Center impact on the overall
system performance?
(b) Does the size of the buffer in the Cloud Data Center show a similar impact on
the system performance?
(c) How do the characteristics of input data (i.e. different values of f) affect
the overall average queuing delay (version B)?
3. Now define a desired value for the maximum average queuing time of type A packets,
denoted T
q.
(a) Assuming again a single-server Micro Data Center and a single-server Cloud Data
Center, replace the edge node with a progressively faster server. Which is the4
minimum value of the average service rate that is required to reduce the queuing
time of type A packets below the threshold Tq?
(b) Try to increase the number of edge nodes, assuming the same fixed average service
time for each edge node: which is the minimum number of servers required to
reduce the queuing time below the threshold Tq?
4. Consider a multi-server system, assuming various servers both in the Micro and in the
Cloud Data Centers. Assign an operational cost to each edge node and to the Cloud
servers, with the operational cost for a Cloud server being different with respect to the
one for an edge node. Simulate the system operation over a fixed period of time.
(a) Vary the value of f (version B) over time,
depending on the different time periods within the considered observation window.
Investigate how these configuration settings affect the overall system performance.
(b) Now assume at least 4 Cloud servers. Furthermore, consider at least three different
types of Cloud servers, each featuring a different service speed and a specific
operational cost (that may depend, for example, on the service speed). Test
various combinations of server types and investigate the trade off between the
overall cost, the queuing delays and the packet drop probability.
(c) Set a value of f < 0.5 (version B) and define a
desired threshold on the maximum operational cost.
• Identify the best combination of server types allowing to reduce the cost below
the desired threshold.
• Does this combination allow to respect the constraint on the maximum queuing delay, i.e. Tq, set in Task 3 for type A packets?
(d) Now, assume to install half the number of Cloud servers, keeping the same value
of f as in Task 4.(c).
• 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.

Hey GPT! I have the following code for performing the lab exercise I provided above. Look at the code and tell me how I should modify this code for performing different tasks of simulation.(How and what should be modified for performing each part of the simulation)

#!/usr/bin/python3

import random
from queue import Queue, PriorityQueue
import matplotlib.pyplot as plt


# ******************************************************************************
# Constants
# ******************************************************************************
class simulator:
    def __init__(self, service, arrival, sim_time, max_buffer_size = float('inf')):
        self.service = service  # SERVICE is the average service time; service rate = 1/SERVICE
        self.arrival = arrival  # ARRIVAL is the average inter-arrival time; arrival rate = 1/ARRIVAL
        self.load= self.service / self.arrival  # This relationship holds for M/M/1
        self.max_buffer_size = max_buffer_size
        self.type1 = 1
        self.sim_time = sim_time
        self.arrivals=0
        self.users=0
        self.data = None
        self.BusyServer=False # True: server is currently busy; False: server is currently idle
        self.MM1= None
        self.FES = None
        self.result = None
        random.seed(42)

    # ******************************************************************************
    # To take the measurements
    # ******************************************************************************
    class Measure:
        def __init__(self,Narr,Ndep,NAveraegUser,OldTimeEvent,AverageDelay,Drop):
            self.arr = Narr
            self.dep = Ndep
            self.ut = NAveraegUser
            self.oldT = OldTimeEvent
            self.delay = AverageDelay
            self.drop = Drop

    # ******************************************************************************
    # Client
    # ******************************************************************************
    class Client:
        def __init__(self,type,arrival_time):
            self.type = type
            self.arrival_time = arrival_time

    # ******************************************************************************
    # Server
    # ******************************************************************************
    class Server(object):

        # constructor
        def __init__(self):

            # whether the server is idle or not
            self.idle = True


    # ******************************************************************************

    # arrivals *********************************************************************
    def Arrival(self,time, queue):

        #print("Arrival no. ",data.arr+1," at time ",time," with ",users," users" )

        # cumulate statistics
        self.data.arr += 1
        self.data.ut += self.users*(time-self.data.oldT)
        self.data.oldT = time

        # sample the time until the next event
        inter_arrival = random.expovariate(lambd=1.0/self.arrival)

        # schedule the next arrival
        self.FES.put((time + inter_arrival, "arrival"))

        self.users += 1

        # create a record for the client
        client = self.Client(self.type1,time)

        # check the buffer size before adding a new packet into the MM1 queue
        if len(queue) < self.max_buffer_size:
            queue.append(client)
        else:
            # increase the dropped packets count
            self.data.drop += 1
            self.users -= 1

        # ensures that the server only starts serving a new client when there is exactly one user in the system and
        # the server is idle. It helps avoid scheduling duplicate departure events or attempting to serve multiple
        # clients simultaneously.
        if self.users==1:

            # sample the service time
            service_time = random.expovariate(1.0/self.service)
            #service_time = 1 + random.uniform(0, SEVICE_TIME)

            # schedule when the client will finish the server
            self.FES.put((time + service_time, "departure"))

    # ******************************************************************************

    # departures *******************************************************************
    def Departure(self,time, queue):
        #print("Departure no. ",data.dep+1," at time ",time," with ",users," users" )

        # cumulate statistics
        self.data.dep += 1
        self.data.ut += self.users*(time-self.data.oldT)
        self.data.oldT = time

        # get the first element from the queue
        client = queue.pop(0)

        # do whatever we need to do when clients go away

        self.data.delay += (time-client.arrival_time)
        self.users -= 1

        # see whether there are more clients to in the line
        if self.users >0:
            # sample the service time
            service_time = random.expovariate(1.0/self.service)

            # schedule when the client will finish the server
            self.FES.put((time + service_time, "departure"))


    # ******************************************************************************
    # the "main" of the simulation
    # ******************************************************************************
    def run(self):
        self.users = 0
        self.data = self.Measure(0,0,0,0,0,0)
        self.MM1 = []
        self.time = 0
        # the list of events in the form: (time, type)
        self.FES = None
        self.FES = PriorityQueue()
        # schedule the first arrival at t=0
        self.FES.put((0, "arrival"))
        # simulate until the simulated time reaches a constant
        while self.time < self.sim_time:
            (self.time, self.event_type) = self.FES.get()

            if self.event_type == "arrival":
                self.Arrival(self.time, self.MM1)

            elif self.event_type == "departure":
                self.Departure(self.time, self.MM1)

        # print output data
        print("MEASUREMENTS")
        print("{:<30}{}".format("No. of users in the queue:", self.users))
        print("{:<30}{}".format("No. of arrivals :", self.data.arr))
        print("{:<30}{}".format("No. of departures :", self.data.dep))
        print("{:<30}{}".format("Dropped packets:", self.data.drop))
        print("{:<30}{}".format("Loss probability:", self.data.drop / self.data.arr))  # Add this line
        print("{:<30}{}".format("Buffer size:", self.max_buffer_size if self.max_buffer_size != float("inf") else "infinite"))
        print("{:<30}{}".format("Load:", self.load ))
        print("{:<30}{}".format("Arrival rate:", self.arrival))
        print("{:<30}{}".format("Service rate:", self.service))
        print("{:<30}{}".format("Average number of users:", self.data.ut / self.time))
        print("{:<30}{}".format("Average delay:", self.data.delay / self.data.dep))
        print("{:<30}{}".format("Actual queue size:", len(self.MM1)))


        if len(self.MM1) > 0:
            print("{:<30}{}".format("Arrival time of the last element in the queue:", self.MM1[len(self.MM1) - 1].arrival_time))

        # Create a dictionary of data related to the simulation
        self.result = {
            "No. of users in the queue": self.users,
            "No. of arrivals": self.data.arr,
            "No. of departures": self.data.dep,
            "Dropped packets": self.data.drop,
            "Loss probability": self.data.drop / self.data.arr,
            "Buffer size": self.max_buffer_size if self.max_buffer_size != float("inf") else "infinite",
            "Load": self.load,
            "Arrival rate": self.arrival,
            "Service rate": self.service,
            "Average number of users": self.data.ut / self.time,
            "Average delay": self.data.delay / self.data.dep,
            "Actual queue size": len(self.MM1)
        }

        return self.data, self.result

def plot_graph(data1, data2, label1, label2, title):
    plt.plot(data1, label=label1)
    plt.plot(data2, label=label2)
    plt.xlabel('X-axis')
    plt.ylabel('Y-axis')
    plt.title(title)
    plt.legend()
    plt.show()

if __name__ == "__main__":
    # simulator inputs : service, arrival , sim_time, buffer_size
    packet_simulator = simulator(10,20,500000)
    print(packet_simulator.run()[1])