"""
Module to execute the simulation for a given instance. 




"""

""" import packages """
import logging
from importlib import import_module
import numpy.random as rdm
import copy
import numpy as np

""" import project configurations """
import configurations.settings_simulation as config 

""" import project libraries """
import modules.data.datamgm as dtm 
from modules.simulation.entities import Tram, Stop, Passengers, CargoRequest, write_entities_log, init_entities_log

# Global logger
logger = dtm.initialise_logger(__name__)

""" 
GLOBAL VARIABLES 
----------------
- These variables must be resetted after every simulation run

"""

#: now Simulation Clock
now = -1

#: last_now Last event
last_now = 0

#:event_queue Event queue
event_queue = []

#:trams List of running trams 
trams = []

#:stops List of stops
stops = []

#:cargo List of cargo
cargo = []

#:updates List of updates
updates = set()

#:numEvents Number of total events
numEvents = 0 

def reset_variables():
    """
    Function to reset all global variables

    """
    
    global now, last_now, numEvents, trams, stops, event_queue, cargo, updates
    
    now = -1
    last_now = 0
    numEvents = 0
    if trams: 
        trams[0].reset()
    trams.clear()
    for stop in stops:
        stop.reset()
    stops.clear()
    event_queue.clear()
    Passengers.reset()
    if cargo: 
        cargo[0].reset()
    cargo.clear()
    updates.clear()

"""
SIMULATION LOGGING
------------------
- Simluation log (Text File): Includes all information about the events in the simulation 
- Entities Log (csv file): Includes the relevant data information of single entities

"""

# "Simulation Log": What does in a single simulation run happen? (Descriptive)
sim_log = logging.getLogger("simulation")

# "Entities Log": How do the variables change during one simulation run? 
ent_log = logging.getLogger("entities")


""" 
SIMULATION METHODS 
------------------

"""		

def run(instance, passengerData, seed=False, index_child_seed=False): 
    """
    Run the simulation
    
    :param instance: Path to the instance file
    :param passengerData: Path to the passenger data file
    :param seed: Seed to replicate the simulation
    :param index_child_see: Index of the child of the global seedsequence 
    
    """
    
    # Used global variables  
    global inst, now, last_now, event_queue, numEvents
    
    """ Initialise random generator """
    # Check seed for random generator
    if seed: 
        # seed sequence
        entropy = seed.entropy 
    else:
        seed = rdm.SeedSequence()
        entropy = seed.entropy
    
    # Import instance (from .py-file)
    
    inst = dtm.import_instance(instance)

    # Initialize the simulation
    passenger = initialize(seed, passengerData) 

    # Run the simulation 
    running = True

    while running: 
        # sort the upcoming events according to the time they occur
        event_queue = sorted(event_queue,key = lambda i: i['time'])
        
        if event_queue:
        
            if event_queue[0]['time'] != now: 
                
                if now >= 0:
                    status(now)
                    
                for entity in updates: 
                    if entity == "passenger": 
                        entity = passenger
                    entity.last_event = now
                    write_entities_log(entity,now)
                    
                updates.clear()
                
                last_now = now
                now = event_queue[0]['time']
                
                sim_log.info("\n-----------------------------------------------------------------------------------")
                sim_log.info(f"Events at {now}:")
            
            sim_log.info("***")				
            next_event()
            numEvents+= 1
            event_queue.pop(0)
            
        # No more events
        else:
            last_time_period(inst.numPeriods-1,passenger)
            running = False
    
    # Save values for replicability
    sim_log.info(f"\nentropy:\n{entropy}\n")
    sim_log.info(f"index_child_seed:\n{entropy}\n")

    # Reset after simulation run
    reset_variables()
	
# Initialisation 	
def initialize(seed, passengerData):
    """
    This function initialises the simulation run, i.e., creates the needed variables and adds the first events to the event log. 
    
    :param seed: Seed for replicability
    :type seed: int
    :param passengerData: Path to passenger data file
    :type passengerData: string or path
    :return: Global passenger object to track number of passengers
    :rtype: Passengers object

    """
    
    global event_queue

    sim_log.info("Initialisation...\n--------------------------------------")
    
    # Create child seedsequence per entity
    seeds = seed.spawn(10)
         
    # Entities Log
    init_entities_log()

    # initialize stops 
    for s in range(inst.numStops): 
        #sim_log.info("Creating Stop {}.".format(s))
        distance_to = {"Stop": inst.stops_distance[s],"Customer": [0]}
        distance_from = {"Stop": [inst.stops_distance[j][s] for j in range(inst.numStops)], "Customer": [0]}
        if s == 0:
            stops.append(Stop(distance_to,distance_from,True))
        else:
            stops.append(Stop(distance_to,distance_from))
    
    pas = dtm.import_instance(passengerData)
    
    """ Initialize passengers """
    
    passenger_seeds = seeds[0].spawn(6)
    
    if config.random_passenger_arrival:
        arriving = pas.arriving_intensity
        config.random_passenger_arrival = passenger_seeds[0]
    else: 
        arriving = pas.passenger_arriving
        # instantiate passenger arrivals
        nonzero = np.nonzero(arriving)
        for i in range(len(nonzero[0])): 
            p = nonzero[0][i]
            s = nonzero[1][i]
            create_event(p, 6, [s])
    
    if config.random_passenger_boarding: 
        config.random_passenger_boarding = passenger_seeds[1]
    
    if config.random_passenger_alighting:
        config.random_passenger_boarding = passenger_seeds[2]   

    if config.random_passenger_changing:
        config.random_passenger_changing = passenger_seeds[3]   
        
    if config.random_boarding_time:
        config.random_boarding_time = passenger_seeds[4]
    
    if config.random_alighting_time:
        config.random_alighting_time = passenger_seeds[5]
        
    """ Global passenger variables """ 
    passenger = Passengers(
        # passenger arrival
        random_arrival = config.random_passenger_arrival,
        arriving_passengers = arriving,
        arriving_passengers_cum = pas.passenger_arriving_acc,
        # passenger boarding
        random_boarding = config.random_passenger_boarding,
        boarding_rate = [1 for tram in range(inst.numTrams)],
        # passenger alighting
        random_alighting = config.random_passenger_alighting,
        alighting_rate = pas.passenger_allighting_rate,
        # passenger changing
        random_changing = config.random_passenger_changing,
        changing_rate = [0 for tram in range(inst.numStops)],
        # time
        random_boarding_time = config.random_boarding_time,
        random_alighting_time = config.random_alighting_time,
        service_time = inst.passenger_service_time_board,
        service_time_alight = inst.passenger_service_time_alight,
        )
           
           
    # Initialize the starting times of each tram
    tram_seeds = seeds[1].spawn(inst.numTrams)
    for t in range(inst.numTrams):
        sim_log.info(f"Tram {t} will start at {inst.tram_time_arrival[t][0]}.")
        Tram.numTotal += 1
        create_event(inst.tram_time_arrival[t][0],1,[t,tram_seeds[t]])
    
    # Initialize the cargo release
    cargo_seeds = seeds[2].spawn(inst.numCargo)  
    for c in range(inst.numCargo): 
        sim_log.info(f"Cargo request {c} will start at {inst.cargo_release[c]}.")
        create_event(inst.cargo_release[c],5,[c,cargo_seeds[c]])
    
    # sort the event queue according to the time
    event_queue = sorted(event_queue,key = lambda i: i['time'])
    
    sim_log.info("\n-----------------------------------------------------------------------------------\n")
    
    return passenger
    
def last_time_period(time,passenger): 
    """
    Write the log for the last period of the simulation
    
    :param time: last period
    :type time: float
    :param passenger: passenger object
    :type passenger: Passengers object

    """

    status(time)
    
    for t in trams: 
        write_entities_log(t,time)
    
    for s in stops: 
        write_entities_log(s,time)
    
    write_entities_log(passenger,time)
    
    for c in cargo: 
        c.estimate_delay(time)
        write_entities_log(c,time)

def status(time):
    """
    Add the status of all entities to the simulation log
    
    :param time: Time of update
    :type time: float

    """

    global updates
    
    sim_log.info("\n*~* Status *~*")

    for t in trams: 
        t.info()
        if len(t.sequences) < t.stopped: 
            t.sequences.append( {"time": time, "cargo": t.cargosize, "passengers": t.passengers, "delay": t.delay} )
        
    for t in stops: 
        t.info()
        if len(t.sequences) < t.stopped: 
            t.sequences.append( {"time": time, "cargo": t.cargosize, "passengers": t.passengers} )
    
    CargoRequest.info()
        
    Passengers.info()
    


""" 
METHODS FOR HANDLING EVENTS 
---------------------------
"""	

def create_event(t,event_id,par):
    """
    Creating a new event given an event id and a list of parameters (if the event is within the time horizon)
    
    :param t: time
    :type t: float
    :param event_id: event id
    :type event_id: int
    :param par: event parameters
    :type par: list

    """
    if np.ceil(t) < inst.numPeriods:
        event_queue.append({"time": t, "id":event_id,"par":par})


def next_event(): 
    """
    Execute the next event in the event queue

    """
	
    # Choose the next event
    event = event_queue[0] 
    
    # Extract event id and parameters
    event_id = event["id"]
    par = event["par"]

    # Event-id: 1
    # Description: Starting a new tram 
    if event_id == 1: 
        starting_tram(par[0],seed=par[1])
            
    # Event-id: 2
    # Description: Tram reaches stop (but does not enter yet) 
    if event_id == 2: 
        tram_reaches_stop(par[0])
        
    # Event-id: 3
    # Description: Tram enters stop
    if event_id == 3: 
        tram_entering_stop(par[0])
        
    # Event-id: 4
    # Description: Tram leaves stop (and next tram can enter this stop)
    if event_id == 4: 
        tram_leaves_stop(par[0])
        
    # Event-id: 5
    # Description: Cargo is released
    if event_id == 5: 
        starting_cargo(par[0], seed=par[1])
    
    # Event-id 6: 
    # Description: Update passengers 
    if event_id == 6:
        passenger_update(par[0])

	
""" 
EVENT METHODS 
-----------------------------------

"""
 
def starting_tram(index,seed):
    """
    Event no. 1: Starting a tram
    
    :param index: Index of the tram
    :type index: int
    :param seed: Seed for replicability
    :type seed: int

    """
    
    global now, updates
    
    tram_id = len(trams)
    
    if config.random_travel_time: 
        config.random_travel_time = seed
        
    # debugging
    #logger.debug(f"tram_travel_deviation: {config.tram_travel_deviation}")
    
    # if passengers and cargo share vehicles
    if inst.scheme == "SV":
        trams.append(Tram(
            tour = inst.tram_tour[index],
            capacity_passenger = inst.tram_capacity-inst.tram_capacity_min_cargo,
            capacity_cargo = inst.tram_capacity-inst.tram_capacity_min_passenger,
            capacity_total = inst.tram_capacity,
            schedule_arrival = inst.tram_time_arrival[index],
            schedule_departure = inst.tram_time_departure[index],
            speed = inst.tram_speed,	
            # Simulation deterministic by default
            random_travel_time = config.random_travel_time,
            travel_deviation = config.tram_travel_deviation, 
            max_service = inst.tram_max_service             
            )
            )
    # if passengers and cargo have dedicated vehicles
    elif inst.scheme == "SI":
        if index in inst.cargo_tram_assignment:
            # cargo tram
            trams.append(Tram(
                tour = inst.tram_tour[index],
                capacity_passenger = 0,
                capacity_cargo = inst.tram_capacity_cargo,
                capacity_total = inst.tram_capacity,
                schedule_arrival = inst.tram_time_arrival[index],
                schedule_departure = inst.tram_time_departure[index],
                speed = inst.tram_speed,	
                # Simulation deterministic by default
                random_travel_time = config.random_travel_time,
                travel_deviation = config.tram_travel_deviation, 
                max_service = inst.tram_max_service         
                )
                )
        else:
            # passenger tram
            trams.append(Tram(
                tour = inst.tram_tour[index],
                capacity_passenger = inst.tram_capacity,
                capacity_cargo = 0,
                capacity_total = inst.tram_capacity,
                schedule_arrival = inst.tram_time_arrival[index],
                schedule_departure = inst.tram_time_departure[index],
                speed = inst.tram_speed,	
                # Simulation deterministic by default
                random_travel_time = config.random_travel_time,
                travel_deviation = config.tram_travel_deviation, 
                max_service = inst.tram_max_service              
                )
                )
    tram = trams[-1]
            
    if tram.is_operating:
        tram_reaches_stop(tram_id)
    else: 
        updates.add(tram)

def tram_reaches_stop(tram_id):
    """
    Event no. 2: Tram reaches stop. It either queues up or enters the stop.
    
    :param tram_id: tram id
    :type tram_id: int

    """
    
    global now

    tram = trams[tram_id]

    tram.reach_next_location(now)
    stop = stops[tram.tour[tram.position]]

    if stop.check_queue(tram):  
        tram_entering_stop(tram_id)
    else: 
        updates.add(tram)


def tram_entering_stop(tram_id):
    """
    Event no. 3: Tram enters the platform of the stop.
     
    :param tram_id: tram id
    :type tram_id: int

    """
    global now, updates
    
    tram = trams[tram_id]
    stop=stops[tram.tour[tram.position]]
    
    tram.enter_next_stop(stop,now)   
    
    boarding_time = 0
    alighting_time = 0
    
    # Update passengers
    if tram.passenger_transport:
        boarding_time, alighting_time = passenger_update(stop.index,True,True)
    
    # Compute leaving time with passengers only
    leaving_time = tram.compute_leaving_time(now,boarding_time,alighting_time)
    
    new_leaving_time = False
    if tram.cargo_transport:
        # unloading
        tram_cargoload = copy.copy(tram.cargoload)
        for c in tram_cargoload: 
            request = cargo[c]
            if request.end_stop == stop.index: 
                unloading_time = request.unload(tram,stop,now)
                new_leaving_time = tram.compute_leaving_time(now,unloading_time=unloading_time)                                       
                updates.add(request)
                
        tram_cargoload.clear()
        
        # loading 
        stop_cargoload = copy.copy(stop.cargoload)
        for c in stop_cargoload: 
            request = cargo[c]
            if request.assigned_vehicle == tram.index: 
                loading_time = request.load(tram,stop) 
                new_leaving_time = tram.compute_leaving_time(now,loading_time=loading_time)
                    
                updates.add(request)
        stop_cargoload.clear()
    
    updates.add(tram)
    
    create_event(tram.leaving_time, 4, [tram_id])
    
    return updates

def tram_leaves_stop(tram_id):
    """
     Event no. 4: Tram leaves the stop.  
     
    :param tram_id: tram id
    :type tram_id: int

    """
    global now
	
    tram = trams[tram_id]
    stop = stops[tram.tour[tram.position]]
    
    if tram.leaving_time == now: 

        travel_time = tram.leave_location(stop,now)
        
        updates.add(tram)
        updates.add(stop)

        if tram.is_operating:
            create_event(now + travel_time, 2, [tram_id])
        
        next_tram = stop.next_tram_in_queue(tram)
        if next_tram >= 0: 
            create_event(now + inst.min_time_next_tram , 3, [next_tram])
            

def starting_cargo(index,seed):
    """
    Event no. 5: New cargo request arrives  
        
    :param index: cargo index
    :type index: int
    :param seed: seed for randomisation
    :type seed: int

    """
    
    global now, updates, trams
    
    # Generate new cargo request 
    cargo.append(CargoRequest(
        release = inst.cargo_release[index],
        deadline = inst.cargo_station_deadline[index],
        end_stop = inst.cargo_station_destination[index], 
        assigned_vehicle = inst.cargo_tram_assignment[index], 
        stop = stops[0], 
        service_time = inst.cargo_service_time_load,
        service_time_unload = inst.cargo_service_time_unload,
        size = inst.cargo_size,
        random_service_time = seed,
        )
        )
    
    request = cargo[-1]
        
    # Check if tram is currently at platform
    stop = stops[request.start_stop]
    
    # Update the log of stop and request
    updates.add(stop)
    updates.add(request)
    
    # If the assigned vehicle is currently at the depot
    if stop.current_tram == request.assigned_vehicle:
        
        # load tram
        tram = trams[request.assigned_vehicle]
        
        # update the current loading and leaving time of the tram
        loading_time = request.load(tram, stop)
        leaving_time = tram.compute_leaving_time(now,loading_time = loading_time)
        
        # update the log of the tram
        updates.add(tram)
        
        # Did the leaving time change? 
        if leaving_time:
            # -> Create a new event for leaving the stop
            create_event(leaving_time, 4, [tram.index]) 

def passenger_update(stop_id,recent_tram_arrival = False, consider_tram=False):
    """
    Event no. 6: New passengers arrive and/or alight and board a vehicle
    
    :param stop_id: Index of the stop
    :type stop_id: int
    :param recent_tram_arrival: New arrival of tram (True) or update while tram is waiting (False)?, defaults to False
    :type recent_tram_arrival: bool, optional
    :param consider_tram: Consider boarding and alighting process (True) or only arrival (False), defaults to False
    :type consider_tram: bool, optional
    :return: boarding and alighting time
    :rtype: tuple

    """

    global now, updates
    
    stop = stops[stop_id]
    
    if consider_tram:
        tram_id = stop.current_tram
    else: 
        tram_id = -1
    
    # Update arriving passengers 
    Passengers.arrival(now,stop)
    boarding_time = 0
    alighting_time = 0    
    
    # if currently a tram waits at the platform
    if tram_id >= 0:
    
        tram = trams[tram_id]
        
        if recent_tram_arrival or tram.leaving_time != now:
        
            if recent_tram_arrival:          
                # compute number and time for alighting passengers
                alighting_passengers, alighting_time = Passengers.alighting(stop,tram,now)
                
            # compute number and time for boarding passengers
            boarding_passengers, boarding_time = Passengers.boarding(stop,tram,now)
                
            if recent_tram_arrival: 
                # compute number and time for changing passengers
                changing_passengers = Passengers.changing(stop,alighting_passengers,now)
            
            # Update leaving time
            if not recent_tram_arrival:
                leaving_time = tram.compute_leaving_time(now,boarding_time,alighting_time, 0, 0)
                updates.add(tram)
                #write_entities_log(tram,now)
                
                # Did the leaving time change?
                if leaving_time:    
                    create_event(leaving_time, 4, [tram_id])
            
            #next_arrival = Passengers.compute_next_arrival_time(now,stop,tram)
            
            #if next_arrival:
                # create new event (for passengers that may arrive before the current tram leaves)
                #create_event(next_arrival, 6, [stop_id])
    
    updates.add(stop)
    updates.add("passenger")
    
    return boarding_time, alighting_time

   
"""
Module to execute the simulation for a given instance. 




"""

""" import packages """
import logging
from importlib import import_module
import numpy.random as rdm
import copy
import numpy as np

""" import project configurations """
import configurations.settings_simulation as config 

""" import project libraries """
import modules.data.datamgm as dtm 
from modules.simulation.entities import Tram, Stop, Passengers, CargoRequest, write_entities_log, init_entities_log

# Global logger
logger = dtm.initialise_logger(__name__)

""" 
GLOBAL VARIABLES 
----------------
- These variables must be resetted after every simulation run

"""

#: now Simulation Clock
now = -1

#: last_now Last event
last_now = 0

#:event_queue Event queue
event_queue = []

#:trams List of running trams 
trams = []

#:stops List of stops
stops = []

#:cargo List of cargo
cargo = []

#:updates List of updates
updates = set()

#:numEvents Number of total events
numEvents = 0 

def reset_variables():
    """
    Function to reset all global variables

    """
    
    global now, last_now, numEvents, trams, stops, event_queue, cargo, updates
    
    now = -1
    last_now = 0
    numEvents = 0
    if trams: 
        trams[0].reset()
    trams.clear()
    for stop in stops:
        stop.reset()
    stops.clear()
    event_queue.clear()
    Passengers.reset()
    if cargo: 
        cargo[0].reset()
    cargo.clear()
    updates.clear()

"""
SIMULATION LOGGING
------------------
- Simluation log (Text File): Includes all information about the events in the simulation 
- Entities Log (csv file): Includes the relevant data information of single entities

"""

# "Simulation Log": What does in a single simulation run happen? (Descriptive)
sim_log = logging.getLogger("simulation")

# "Entities Log": How do the variables change during one simulation run? 
ent_log = logging.getLogger("entities")


""" 
SIMULATION METHODS 
------------------

"""		

def run(instance, passengerData, seed=False, index_child_seed=False): 
    """
    Run the simulation
    
    :param instance: Path to the instance file
    :param passengerData: Path to the passenger data file
    :param seed: Seed to replicate the simulation
    :param index_child_see: Index of the child of the global seedsequence 
    
    """
    
    # Used global variables  
    global inst, now, last_now, event_queue, numEvents
    
    """ Initialise random generator """
    # Check seed for random generator
    if seed: 
        # seed sequence
        entropy = seed.entropy 
    else:
        seed = rdm.SeedSequence()
        entropy = seed.entropy
    
    # Import instance (from .py-file)
    
    inst = dtm.import_instance(instance)

    # Initialize the simulation
    passenger = initialize(seed, passengerData) 

    # Run the simulation 
    running = True

    while running: 
        # sort the upcoming events according to the time they occur
        event_queue = sorted(event_queue,key = lambda i: i['time'])
        
        if event_queue:
        
            if event_queue[0]['time'] != now: 
                
                if now >= 0:
                    status(now)
                    
                for entity in updates: 
                    if entity == "passenger": 
                        entity = passenger
                    entity.last_event = now
                    write_entities_log(entity,now)
                    
                updates.clear()
                
                last_now = now
                now = event_queue[0]['time']
                
                sim_log.info("\n-----------------------------------------------------------------------------------")
                sim_log.info(f"Events at {now}:")
            
            sim_log.info("***")				
            next_event()
            numEvents+= 1
            event_queue.pop(0)
            
        # No more events
        else:
            last_time_period(inst.numPeriods-1,passenger)
            running = False
    
    # Save values for replicability
    sim_log.info(f"\nentropy:\n{entropy}\n")
    sim_log.info(f"index_child_seed:\n{entropy}\n")

    # Reset after simulation run
    reset_variables()
	
# Initialisation 	
def initialize(seed, passengerData):
    """
    This function initialises the simulation run, i.e., creates the needed variables and adds the first events to the event log. 
    
    :param seed: Seed for replicability
    :type seed: int
    :param passengerData: Path to passenger data file
    :type passengerData: string or path
    :return: Global passenger object to track number of passengers
    :rtype: Passengers object

    """
    
    global event_queue

    sim_log.info("Initialisation...\n--------------------------------------")
    
    # Create child seedsequence per entity
    seeds = seed.spawn(10)
         
    # Entities Log
    init_entities_log()

    # initialize stops 
    for s in range(inst.numStops): 
        #sim_log.info("Creating Stop {}.".format(s))
        distance_to = {"Stop": inst.stops_distance[s],"Customer": [0]}
        distance_from = {"Stop": [inst.stops_distance[j][s] for j in range(inst.numStops)], "Customer": [0]}
        if s == 0:
            stops.append(Stop(distance_to,distance_from,True))
        else:
            stops.append(Stop(distance_to,distance_from))
    
    pas = dtm.import_instance(passengerData)
    
    """ Initialize passengers """
    
    passenger_seeds = seeds[0].spawn(6)
    
    if config.random_passenger_arrival:
        arriving = pas.arriving_intensity
        config.random_passenger_arrival = passenger_seeds[0]
    else: 
        arriving = pas.passenger_arriving
        # instantiate passenger arrivals
        nonzero = np.nonzero(arriving)
        for i in range(len(nonzero[0])): 
            p = nonzero[0][i]
            s = nonzero[1][i]
            create_event(p, 6, [s])
    
    if config.random_passenger_boarding: 
        config.random_passenger_boarding = passenger_seeds[1]
    
    if config.random_passenger_alighting:
        config.random_passenger_boarding = passenger_seeds[2]   

    if config.random_passenger_changing:
        config.random_passenger_changing = passenger_seeds[3]   
        
    if config.random_boarding_time:
        config.random_boarding_time = passenger_seeds[4]
    
    if config.random_alighting_time:
        config.random_alighting_time = passenger_seeds[5]
        
    """ Global passenger variables """ 
    passenger = Passengers(
        # passenger arrival
        random_arrival = config.random_passenger_arrival,
        arriving_passengers = arriving,
        arriving_passengers_cum = pas.passenger_arriving_acc,
        # passenger boarding
        random_boarding = config.random_passenger_boarding,
        boarding_rate = [1 for tram in range(inst.numTrams)],
        # passenger alighting
        random_alighting = config.random_passenger_alighting,
        alighting_rate = pas.passenger_allighting_rate,
        # passenger changing
        random_changing = config.random_passenger_changing,
        changing_rate = [0 for tram in range(inst.numStops)],
        # time
        random_boarding_time = config.random_boarding_time,
        random_alighting_time = config.random_alighting_time,
        service_time = inst.passenger_service_time_board,
        service_time_alight = inst.passenger_service_time_alight,
        )
           
           
    # Initialize the starting times of each tram
    tram_seeds = seeds[1].spawn(inst.numTrams)
    for t in range(inst.numTrams):
        sim_log.info(f"Tram {t} will start at {inst.tram_time_arrival[t][0]}.")
        Tram.numTotal += 1
        create_event(inst.tram_time_arrival[t][0],1,[t,tram_seeds[t]])
    
    # Initialize the cargo release
    cargo_seeds = seeds[2].spawn(inst.numCargo)  
    for c in range(inst.numCargo): 
        sim_log.info(f"Cargo request {c} will start at {inst.cargo_release[c]}.")
        create_event(inst.cargo_release[c],5,[c,cargo_seeds[c]])
    
    # sort the event queue according to the time
    event_queue = sorted(event_queue,key = lambda i: i['time'])
    
    sim_log.info("\n-----------------------------------------------------------------------------------\n")
    
    return passenger
    
def last_time_period(time,passenger): 
    """
    Write the log for the last period of the simulation
    
    :param time: last period
    :type time: float
    :param passenger: passenger object
    :type passenger: Passengers object

    """

    status(time)
    
    for t in trams: 
        write_entities_log(t,time)
    
    for s in stops: 
        write_entities_log(s,time)
    
    write_entities_log(passenger,time)
    
    for c in cargo: 
        c.estimate_delay(time)
        write_entities_log(c,time)

def status(time):
    """
    Add the status of all entities to the simulation log
    
    :param time: Time of update
    :type time: float

    """

    global updates
    
    sim_log.info("\n*~* Status *~*")

    for t in trams: 
        t.info()
        if len(t.sequences) < t.stopped: 
            t.sequences.append( {"time": time, "cargo": t.cargosize, "passengers": t.passengers, "delay": t.delay} )
        
    for t in stops: 
        t.info()
        if len(t.sequences) < t.stopped: 
            t.sequences.append( {"time": time, "cargo": t.cargosize, "passengers": t.passengers} )
    
    CargoRequest.info()
        
    Passengers.info()
    


""" 
METHODS FOR HANDLING EVENTS 
---------------------------
"""	

def create_event(t,event_id,par):
    """
    Creating a new event given an event id and a list of parameters (if the event is within the time horizon)
    
    :param t: time
    :type t: float
    :param event_id: event id
    :type event_id: int
    :param par: event parameters
    :type par: list

    """
    if np.ceil(t) < inst.numPeriods:
        event_queue.append({"time": t, "id":event_id,"par":par})


def next_event(): 
    """
    Execute the next event in the event queue

    """
	
    # Choose the next event
    event = event_queue[0] 
    
    # Extract event id and parameters
    event_id = event["id"]
    par = event["par"]

    # Event-id: 1
    # Description: Starting a new tram 
    if event_id == 1: 
        starting_tram(par[0],seed=par[1])
            
    # Event-id: 2
    # Description: Tram reaches stop (but does not enter yet) 
    if event_id == 2: 
        tram_reaches_stop(par[0])
        
    # Event-id: 3
    # Description: Tram enters stop
    if event_id == 3: 
        tram_entering_stop(par[0])
        
    # Event-id: 4
    # Description: Tram leaves stop (and next tram can enter this stop)
    if event_id == 4: 
        tram_leaves_stop(par[0])
        
    # Event-id: 5
    # Description: Cargo is released
    if event_id == 5: 
        starting_cargo(par[0], seed=par[1])
    
    # Event-id 6: 
    # Description: Update passengers 
    if event_id == 6:
        passenger_update(par[0])

	
""" 
EVENT METHODS 
-----------------------------------

"""
 
def starting_tram(index,seed):
    """
    Event no. 1: Starting a tram
    
    :param index: Index of the tram
    :type index: int
    :param seed: Seed for replicability
    :type seed: int

    """
    
    global now, updates
    
    tram_id = len(trams)
    
    if config.random_travel_time: 
        config.random_travel_time = seed
        
    # debugging
    #logger.debug(f"tram_travel_deviation: {config.tram_travel_deviation}")
    
    # if passengers and cargo share vehicles
    if inst.scheme == "SV":
        trams.append(Tram(
            tour = inst.tram_tour[index],
            capacity_passenger = inst.tram_capacity-inst.tram_capacity_min_cargo,
            capacity_cargo = inst.tram_capacity-inst.tram_capacity_min_passenger,
            capacity_total = inst.tram_capacity,
            schedule_arrival = inst.tram_time_arrival[index],
            schedule_departure = inst.tram_time_departure[index],
            speed = inst.tram_speed,	
            # Simulation deterministic by default
            random_travel_time = config.random_travel_time,
            travel_deviation = config.tram_travel_deviation, 
            max_service = inst.tram_max_service             
            )
            )
    # if passengers and cargo have dedicated vehicles
    elif inst.scheme == "SI":
        if index in inst.cargo_tram_assignment:
            # cargo tram
            trams.append(Tram(
                tour = inst.tram_tour[index],
                capacity_passenger = 0,
                capacity_cargo = inst.tram_capacity_cargo,
                capacity_total = inst.tram_capacity,
                schedule_arrival = inst.tram_time_arrival[index],
                schedule_departure = inst.tram_time_departure[index],
                speed = inst.tram_speed,	
                # Simulation deterministic by default
                random_travel_time = config.random_travel_time,
                travel_deviation = config.tram_travel_deviation, 
                max_service = inst.tram_max_service         
                )
                )
        else:
            # passenger tram
            trams.append(Tram(
                tour = inst.tram_tour[index],
                capacity_passenger = inst.tram_capacity,
                capacity_cargo = 0,
                capacity_total = inst.tram_capacity,
                schedule_arrival = inst.tram_time_arrival[index],
                schedule_departure = inst.tram_time_departure[index],
                speed = inst.tram_speed,	
                # Simulation deterministic by default
                random_travel_time = config.random_travel_time,
                travel_deviation = config.tram_travel_deviation, 
                max_service = inst.tram_max_service              
                )
                )
    tram = trams[-1]
            
    if tram.is_operating:
        tram_reaches_stop(tram_id)
    else: 
        updates.add(tram)

def tram_reaches_stop(tram_id):
    """
    Event no. 2: Tram reaches stop. It either queues up or enters the stop.
    
    :param tram_id: tram id
    :type tram_id: int

    """
    
    global now

    tram = trams[tram_id]

    tram.reach_next_location(now)
    stop = stops[tram.tour[tram.position]]

    if stop.check_queue(tram):  
        tram_entering_stop(tram_id)
    else: 
        updates.add(tram)


def tram_entering_stop(tram_id):
    """
    Event no. 3: Tram enters the platform of the stop.
     
    :param tram_id: tram id
    :type tram_id: int

    """
    global now, updates
    
    tram = trams[tram_id]
    stop=stops[tram.tour[tram.position]]
    
    tram.enter_next_stop(stop,now)   
    
    boarding_time = 0
    alighting_time = 0
    
    # Update passengers
    if tram.passenger_transport:
        boarding_time, alighting_time = passenger_update(stop.index,True,True)
    
    # Compute leaving time with passengers only
    leaving_time = tram.compute_leaving_time(now,boarding_time,alighting_time)
    
    new_leaving_time = False
    if tram.cargo_transport:
        # unloading
        tram_cargoload = copy.copy(tram.cargoload)
        for c in tram_cargoload: 
            request = cargo[c]
            if request.end_stop == stop.index: 
                unloading_time = request.unload(tram,stop,now)
                new_leaving_time = tram.compute_leaving_time(now,unloading_time=unloading_time)                                       
                updates.add(request)
                
        tram_cargoload.clear()
        
        # loading 
        stop_cargoload = copy.copy(stop.cargoload)
        for c in stop_cargoload: 
            request = cargo[c]
            if request.assigned_vehicle == tram.index: 
                loading_time = request.load(tram,stop) 
                new_leaving_time = tram.compute_leaving_time(now,loading_time=loading_time)
                    
                updates.add(request)
        stop_cargoload.clear()
    
    updates.add(tram)
    
    create_event(tram.leaving_time, 4, [tram_id])
    
    return updates

def tram_leaves_stop(tram_id):
    """
     Event no. 4: Tram leaves the stop.  
     
    :param tram_id: tram id
    :type tram_id: int

    """
    global now
	
    tram = trams[tram_id]
    stop = stops[tram.tour[tram.position]]
    
    if tram.leaving_time == now: 

        travel_time = tram.leave_location(stop,now)
        
        updates.add(tram)
        updates.add(stop)

        if tram.is_operating:
            create_event(now + travel_time, 2, [tram_id])
        
        next_tram = stop.next_tram_in_queue(tram)
        if next_tram >= 0: 
            create_event(now + inst.min_time_next_tram , 3, [next_tram])
            

def starting_cargo(index,seed):
    """
    Event no. 5: New cargo request arrives  
        
    :param index: cargo index
    :type index: int
    :param seed: seed for randomisation
    :type seed: int

    """
    
    global now, updates, trams
    
    # Generate new cargo request 
    cargo.append(CargoRequest(
        release = inst.cargo_release[index],
        deadline = inst.cargo_station_deadline[index],
        end_stop = inst.cargo_station_destination[index], 
        assigned_vehicle = inst.cargo_tram_assignment[index], 
        stop = stops[0], 
        service_time = inst.cargo_service_time_load,
        service_time_unload = inst.cargo_service_time_unload,
        size = inst.cargo_size,
        random_service_time = seed,
        )
        )
    
    request = cargo[-1]
        
    # Check if tram is currently at platform
    stop = stops[request.start_stop]
    
    # Update the log of stop and request
    updates.add(stop)
    updates.add(request)
    
    # If the assigned vehicle is currently at the depot
    if stop.current_tram == request.assigned_vehicle:
        
        # load tram
        tram = trams[request.assigned_vehicle]
        
        # update the current loading and leaving time of the tram
        loading_time = request.load(tram, stop)
        leaving_time = tram.compute_leaving_time(now,loading_time = loading_time)
        
        # update the log of the tram
        updates.add(tram)
        
        # Did the leaving time change? 
        if leaving_time:
            # -> Create a new event for leaving the stop
            create_event(leaving_time, 4, [tram.index]) 

def passenger_update(stop_id,recent_tram_arrival = False, consider_tram=False):
    """
    Event no. 6: New passengers arrive and/or alight and board a vehicle
    
    :param stop_id: Index of the stop
    :type stop_id: int
    :param recent_tram_arrival: New arrival of tram (True) or update while tram is waiting (False)?, defaults to False
    :type recent_tram_arrival: bool, optional
    :param consider_tram: Consider boarding and alighting process (True) or only arrival (False), defaults to False
    :type consider_tram: bool, optional
    :return: boarding and alighting time
    :rtype: tuple

    """

    global now, updates
    
    stop = stops[stop_id]
    
    if consider_tram:
        tram_id = stop.current_tram
    else: 
        tram_id = -1
    
    # Update arriving passengers 
    Passengers.arrival(now,stop)
    boarding_time = 0
    alighting_time = 0    
    
    # if currently a tram waits at the platform
    if tram_id >= 0:
    
        tram = trams[tram_id]
        
        if recent_tram_arrival or tram.leaving_time != now:
        
            if recent_tram_arrival:          
                # compute number and time for alighting passengers
                alighting_passengers, alighting_time = Passengers.alighting(stop,tram,now)
                
            # compute number and time for boarding passengers
            boarding_passengers, boarding_time = Passengers.boarding(stop,tram,now)
                
            if recent_tram_arrival: 
                # compute number and time for changing passengers
                changing_passengers = Passengers.changing(stop,alighting_passengers,now)
            
            # Update leaving time
            if not recent_tram_arrival:
                leaving_time = tram.compute_leaving_time(now,boarding_time,alighting_time, 0, 0)
                updates.add(tram)
                #write_entities_log(tram,now)
                
                # Did the leaving time change?
                if leaving_time:    
                    create_event(leaving_time, 4, [tram_id])
            
            #next_arrival = Passengers.compute_next_arrival_time(now,stop,tram)
            
            #if next_arrival:
                # create new event (for passengers that may arrive before the current tram leaves)
                #create_event(next_arrival, 6, [stop_id])
    
    updates.add(stop)
    updates.add("passenger")
    
    return boarding_time, alighting_time

   
Module to execute the simulation for a given instance. import packages import project configurations import project libraries # Global logger GLOBAL VARIABLES  ---------------- - These variables must be resetted after every simulation run #: now Simulation Clock #: last_now Last event #:event_queue Event queue #:trams List of running trams #:stops List of stops #:cargo List of cargo #:updates List of updates #:numEvents Number of total events Function to reset all global variables SIMULATION LOGGING ------------------ - Simluation log (Text File): Includes all information about the events in the simulation  - Entities Log (csv file): Includes the relevant data information of single entities # "Simulation Log": What does in a single simulation run happen? (Descriptive) # "Entities Log": How do the variables change during one simulation run? SIMULATION METHODS  ------------------ Run the simulation          :param instance: Path to the instance file     :param passengerData: Path to the passenger data file     :param seed: Seed to replicate the simulation     :param index_child_see: Index of the child of the global seedsequence # Used global variables Initialise random generator # Check seed for random generator # seed sequence # Import instance (from .py-file) # Initialize the simulation # Run the simulation # sort the upcoming events according to the time they occur # No more events # Save values for replicability # Reset after simulation run # Initialisation This function initialises the simulation run, i.e., creates the needed variables and adds the first events to the event log.           :param seed: Seed for replicability     :type seed: int     :param passengerData: Path to passenger data file     :type passengerData: string or path     :return: Global passenger object to track number of passengers     :rtype: Passengers object # Create child seedsequence per entity # Entities Log # initialize stops #sim_log.info("Creating Stop {}.".format(s)) Initialize passengers # instantiate passenger arrivals Global passenger variables # passenger arrival # passenger boarding # passenger alighting # passenger changing # time # Initialize the starting times of each tram # Initialize the cargo release # sort the event queue according to the time Write the log for the last period of the simulation          :param time: last period     :type time: float     :param passenger: passenger object     :type passenger: Passengers object Add the status of all entities to the simulation log          :param time: Time of update     :type time: float METHODS FOR HANDLING EVENTS  --------------------------- Creating a new event given an event id and a list of parameters (if the event is within the time horizon)          :param t: time     :type t: float     :param event_id: event id     :type event_id: int     :param par: event parameters     :type par: list Execute the next event in the event queue # Choose the next event # Extract event id and parameters # Event-id: 1 # Description: Starting a new tram # Event-id: 2 # Description: Tram reaches stop (but does not enter yet) # Event-id: 3 # Description: Tram enters stop # Event-id: 4 # Description: Tram leaves stop (and next tram can enter this stop) # Event-id: 5 # Description: Cargo is released # Event-id 6: # Description: Update passengers EVENT METHODS  ----------------------------------- Event no. 1: Starting a tram          :param index: Index of the tram     :type index: int     :param seed: Seed for replicability     :type seed: int # debugging #logger.debug(f"tram_travel_deviation: {config.tram_travel_deviation}") # if passengers and cargo share vehicles # Simulation deterministic by default # if passengers and cargo have dedicated vehicles # cargo tram # Simulation deterministic by default # passenger tram # Simulation deterministic by default Event no. 2: Tram reaches stop. It either queues up or enters the stop.          :param tram_id: tram id     :type tram_id: int Event no. 3: Tram enters the platform of the stop.           :param tram_id: tram id     :type tram_id: int # Update passengers # Compute leaving time with passengers only # unloading # loading Event no. 4: Tram leaves the stop.             :param tram_id: tram id     :type tram_id: int Event no. 5: New cargo request arrives                :param index: cargo index     :type index: int     :param seed: seed for randomisation     :type seed: int # Generate new cargo request # Check if tram is currently at platform # Update the log of stop and request # If the assigned vehicle is currently at the depot # load tram # update the current loading and leaving time of the tram # update the log of the tram # Did the leaving time change? # -> Create a new event for leaving the stop Event no. 6: New passengers arrive and/or alight and board a vehicle          :param stop_id: Index of the stop     :type stop_id: int     :param recent_tram_arrival: New arrival of tram (True) or update while tram is waiting (False)?, defaults to False     :type recent_tram_arrival: bool, optional     :param consider_tram: Consider boarding and alighting process (True) or only arrival (False), defaults to False     :type consider_tram: bool, optional     :return: boarding and alighting time     :rtype: tuple # Update arriving passengers # if currently a tram waits at the platform # compute number and time for alighting passengers # compute number and time for boarding passengers # compute number and time for changing passengers # Update leaving time #write_entities_log(tram,now) # Did the leaving time change? #next_arrival = Passengers.compute_next_arrival_time(now,stop,tram) #if next_arrival: # create new event (for passengers that may arrive before the current tram leaves) #create_event(next_arrival, 6, [stop_id])