consider the following lab excercise:
"
Summary
The purpose of this lab is to simulate the operation of the output link of a router that is
modeled as a queuing system and investigate its performance under variable configurations,
to understand how different parameter settings may affect the system behaviour and how
modifying some parameters of the network system may impact on performance metrics.
In the considered queuing system, representing the output link of a router, the customers
represent the packets that arrive to that output link, whereas the service represents the
transmission on the channel. Finally, the waiting line represents the buffer where packets
are stored before transmission.
Tasks
In order to investigate the performance of the router output link, you should focus on the
following aspects:
a. Derive the metrics describing the system performance in the baseline scenario, considering both the cases with and without packet losses.
b. Modify the baseline scenario varying the size of the buffer and considering a multiserver system, assuming that two transmitters are present in the system. Test the
system behaviour under different buffer sizes (infinite waiting line, medium to short
finite waiting line, no buffer at all) and compare its performance with the single server
scenario.
You can start from the network simulator code available in the course material, that represents the operation of an M/M/1 queuing system with infinite waiting line, and event scheduling approach. The simulator code is: 

"

#!/usr/bin/python3

import random
from queue import Queue, PriorityQueue

# ******************************************************************************
# Constants
# ******************************************************************************

SERVICE = 10.0 # SERVICE is the average service time; service rate = 1/SERVICE
ARRIVAL = 5.0 # ARRIVAL is the average inter-arrival time; arrival rate = 1/ARRIVAL
LOAD=SERVICE/ARRIVAL # This relationship holds for M/M/1

TYPE1 = 1 

SIM_TIME = 500000

arrivals=0
users=0
BusyServer=False # True: server is currently busy; False: server is currently idle

MM1=[]


# ******************************************************************************
# To take the measurements
# ******************************************************************************
class Measure:
    def __init__(self,Narr,Ndep,NAveraegUser,OldTimeEvent,AverageDelay):
        self.arr = Narr
        self.dep = Ndep
        self.ut = NAveraegUser
        self.oldT = OldTimeEvent
        self.delay = AverageDelay
        
# ******************************************************************************
# 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(time, FES, queue):
    global users
    
    #print("Arrival no. ",data.arr+1," at time ",time," with ",users," users" )
    
    # cumulate statistics
    data.arr += 1
    data.ut += users*(time-data.oldT)
    data.oldT = time

    # sample the time until the next event
    inter_arrival = random.expovariate(lambd=1.0/ARRIVAL)
    
    # schedule the next arrival
    FES.put((time + inter_arrival, "arrival"))

    users += 1
    
    # create a record for the client
    client = Client(TYPE1,time)

    # insert the record in the queue
    queue.append(client)

    # if the server is idle start the service
    if users==1:
        
        # sample the service time
        service_time = random.expovariate(1.0/SERVICE)
        #service_time = 1 + random.uniform(0, SEVICE_TIME)

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

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

# departures *******************************************************************
def departure(time, FES, queue):
    global users

    #print("Departure no. ",data.dep+1," at time ",time," with ",users," users" )
        
    # cumulate statistics
    data.dep += 1
    data.ut += users*(time-data.oldT)
    data.oldT = time
    
    # get the first element from the queue
    client = queue.pop(0)
    
    # do whatever we need to do when clients go away
    
    data.delay += (time-client.arrival_time)
    users -= 1
    
    # see whether there are more clients to in the line
    if users >0:
        # sample the service time
        service_time = random.expovariate(1.0/SERVICE)

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

        
# ******************************************************************************
# the "main" of the simulation
# ******************************************************************************

random.seed(42)

data = Measure(0,0,0,0,0)

# the simulation time 
time = 0

# the list of events in the form: (time, type)
FES = PriorityQueue()


# schedule the first arrival at t=0
FES.put((0, "arrival"))

# simulate until the simulated time reaches a constant
while time < SIM_TIME:
    (time, event_type) = FES.get()

    if event_type == "arrival":
        arrival(time, FES, MM1)

    elif event_type == "departure":
        departure(time, FES, MM1)

# print output data
print("MEASUREMENTS \n\nNo. of users in the queue:",users,"\nNo. of arrivals =",
      data.arr,"- No. of departures =",data.dep)

print("Load: ",SERVICE/ARRIVAL)
print("\nArrival rate: ",data.arr/time," - Departure rate: ",data.dep/time)

print("\nAverage number of users: ",data.ut/time)

print("Average delay: ",data.delay/data.dep)
print("Actual queue size: ",len(MM1))

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

"


You can make your modifications and integration to the code, in order to investigate the
system performance via simulation. Different metrics can be useful to understand the system
operation and evaluate its performance, like:
1In the context of queueing theory and simulating the output link of a router
with different buffer sizes, the difference between an "event scheduling approach
" and"process interaction" lies in the way they handle the scheduling of events
and the processing of those events.
Event Scheduling Approach:
An event scheduling approach is a simulation technique in which events are
scheduled to occur at specific times. These events could include the arrival of
packets at the router, the completion of packet transmissions, or the expiration of
a timer. The event scheduling approach maintains a queue of pending events,
sorted by the time at which they are scheduled to occur. As events are processed,
they may trigger additional events to be scheduled. In the case of a router, for
example, the arrival of a packet might trigger the scheduling of a transmission
event for that packet.
Process Interaction:
In contrast, process interaction is a simulation technique in which a set of
processes are simulated concurrently. Each process represents a component of
the system being modeled, such as the router, the buffer, or the packets. The
processes interact with one another by exchanging messages or signals,
indicating when an event has occurred. In the case of a router, for example, the
arrival of a packet might trigger a signal to be sent to the buffer process,
indicating that a packet is ready to be stored.
In summary, the event scheduling approach relies on a queue of pending events,
while the process interaction approach relies on the exchange of messages or
signals between concurrent processes. Both approaches can be used to simulate
the behavior of a queue, but the choice of approach will depend on the specific
requirements of the simulation and the nature of the system being modeled.2
• number of transmitted packets
• number of dropped packets
• average number of packets in the system
• average queuing delay, i.e. the average time spent in the system
• distribution of queuing delay
• average waiting delay per packet, assuming that the packet waiting delay is
the time elapsed from the moment at which a packet enters the system to the
instant at
which the service begins.
It is useful to compute both of the following types of waiting delay:
– the average waiting delay experienced by any packet (averaging over all packets);
– the average waiting delay considering only those packets that actually experience some delay, since they enter the system while the server(s) is (are)
busy and are hence buffered.
• average buffer occupancy, i.e. the average number of packets in the buffer.
• loss probability, i.e. the fraction of packets that cannot be transmitted since the
server(s) is (are) currently busy and no buffer is present in the system or, in case of
finite buffer, it is full.
• busy time, i.e. the cumulative time that during the simulation each server spends in
a busy state serving packets requests. In a single-server system, you can observe how
it varies with the buffer size. In a multi-server system, based on the busy time analysis
per each server, you can examine the distribution of the load among servers.
You can think of other metrics that may be helpful in your analysis to highlight specific
aspects or critical issues in the system operation. When running your simulations, pay
attention on setting appropriate values of the simulation duration.
In your analysis of the network operation and performance, you should perform the following
tasks:
1. investigate the system performance under different arrival rates, keeping a fixed
value for the average service rate:
(a) how is the system affected? Highlight your findings by showing the variation of
relevant performance metrics;
(b) set a desired confidence level and, for one of the metrics reported in the graphs
produced in task 1.a, show the confidence intervals of the performed measurements;
(c) are the results of your analysis consistent with theoretical expected values?2. consider a multi-server system, assuming two transmitters and a finite buffer:
(a) compare its performance with the single server case;
(b) test different values of the buffer size: how do they affect the system performance
with respect to an infinite buffer?
3. considering a multi-server scenario, investigate the load distribution among servers.
Test different algorithms to assign each new request to an available server (random
assignment; round robin; assignment to the fastest servers, assuming for example that
each server features a different service rate...) and compare the system performance in
the various cases. How is load distribution affected?
4. try to vary the distribution of the service time, i.e. considering the case of M/G/1,
and observe how the system performance changes, assuming one or more different
distribution types for the service time instead of exponential distribution;
Discuss in your report the main findings emerged from the various investigated scenarios.
Support your claims and observations by plotting graphs that report the most significant
performance metrics from different cases and help to highlight the relevant findings under
variable configuration settings. Remember to always specify the unit of measure for the
parameters and metrics represented in the graphs. For each graph, check that the main
configuration settings that have been adopted to obtain the corresponding simulation results
are reported."

Gimme a comprehensive and structured report for this lab