Chapter 1: Discrete Event Simulation

SeQUeNCe is a quantum network simulator based on discrete event simulation (DES). DES models systems as a sequence of discrete events. In this chapter, we will:

  • Introduce the basic concepts if DES (to avoid going through Wikipedia)

  • Use the API of the SeQUeNCe kernel to simulate a simple system

DES Background

The events in DES define a change of state of the system. The kernel of DES schedules and executes events in the simulated system, ordered by their time of occurrence. The executed events may generate several new events.

Note: executed events cannot generate events before the current (simulation) time.

We use the below figure to show the procedure of DES. The robot in the figure denotes the kernel of DES. The belt in figure denotes the event list of DES. The could put several boxes on the belt manually as the initial state. The robot will repeatedly processes the nearest box (event). While the robot is processing some boxes (e.g box 5), a new box (e.g box 12) may be created and placed at the correct position. The robot stops work when the belt is empty or a simulation end condition is met.

des

Example: Open and Close Store

Step 1: Create Store Class

Let’s use DES to simulate the business hours of a store. First, we create a class for our store:

from sequence.kernel.timeline import Timeline

class Store(object):
    def __init__(self, tl: Timeline):
        self.opening = False
        self.timeline = tl

    def open(self) -> None:
        self.opening = True

    def close(self) -> None:
        self.opening = False

The Store class has two attributes: open and timeline. open denotes the if the store is in its business hours. The timeline is an instance of a DES kernel. We will use the timeline to bind entities to the DES kernel.

The Store class also has two methods: open and close. These two functions change the state of the store.

Step 2: Schedule Events on Store

Let’s define a store that opens at 7:00.

from sequence.kernel.event import Event
from sequence.kernel.process import Process

tl = Timeline() # create timeline
tl.show_progress = False # turn of progress bar, we will address this in later tutorials.
store = Store(tl) # create store

# open store at 7:00
open_proc = Process(store, 'open', []) # Process(object, function name: string, arguments of function: List[])
open_event = Event(7, open_proc) # Event(occurring time: int, process: Process)
tl.schedule(open_event) # Timeline.schedule(Event)

In the above code, we first create instances of Timeline and Store. Then, we create a process with the object being Store, the method of Store and a list of arguments for the method. The Event class uses open_proc process to create an event occurring at 7:00. The object Timeline then schedules the created event in its event list.

Question: after the above code executes, what is the state of store?

print(store.opening) # False

Step 3: Run Simulation

You can observe the state of the store is still closed as the simulator has not executed the open function yet. We just placed the created event into the event list. To run the simulation, we need use Timeline.run() to process the scheduled events.

tl.run()
print(tl.now(), store.opening) # 7 True

Now, the state of store is updated to the open state. Meanwhile, the function Timeline.now() presents the current simulation time (7). When the simulator executes an event, the simulator will update its time to the timestamp of the event.

Quiz: can you change the store to close at 19:00?

An example of closing the store is shown below.

close_proc = Process(store, 'close', [])
close_event = Event(19, close_proc)
tl.schedule(close_event)
tl.run()
print(tl.time, store.opening) # 19 False

Question: will the state of store change if we schedule close_event first, then open_event?

tl.time = 0
tl.schedule(open_event)
tl.schedule(close_event)
tl.run()
print(tl.time, store.opening)
tl.time = 0
tl.schedule(close_event)
tl.schedule(open_event)
tl.run()
print(tl.time, store.opening)

You can compare the results of the above programs. Both generate the same result (19 False), as the order of executing events does not rely on the order of calling the scheduling method.

Example: Automatically Open and Close Store

Step 1: Upgrade Store Class

The previous example allows us to open and close the store manually. In this example, we upgrade Store to open and close automatically with 12 business hours. The code of the upgraded Store is shown below.

from sequence.kernel.timeline import Timeline
from sequence.kernel.event import Event
from sequence.kernel.process import Process

class Store(object):
    def __init__(self, tl: Timeline):
        self.opening = False
        self.timeline = tl

    def open(self) -> None:
        self.opening = True
        process = Process(self, 'close', [])
        event = Event(self.timeline.now() + 12, process)
        self.timeline.schedule(event)

    def close(self) -> None:
        self.opening = False
        process = Process(self, 'open', [])
        event = Event(self.timeline.now() + 12, process)
        self.timeline.schedule(event)

The updated Store.open()(Store.close()) function schedules a Store.close() (Store.open()) after 12 hours. Now, the Store can repeatedly open and close every 12 hours.

Step 2: Schedule Initial Event

We can then define a store with an initial state - that the store opens at 7.

tl = Timeline()
tl.show_progress = False
store = Store(tl)
process = Process(store, 'open', [])
event = Event(7, process)
tl.schedule(event)

However, if you use tl.run() to run the simulation, the simulation will get stuck in an infinite loop.

Step 3: Define the Condition of Stopping Simulation and Run Simulation

There are two methods to terminate simulation:

  • set the stop time of simulation when creating the Timeline

  • stop the simulation in a function of Store

For the first method, we can construct the object of Timeline with the code below:

tl = Timeline(60) # simulate system for 60 hours

For the second method, we can call Timeline.stop() in the Store.open() and Store.close() methods:

...
    def open(self) -> None:
        if self.timeline.now() >= 60:
            self.timeline.stop()
        self.opening = True
        process = Process(self, 'close', [])
        event = Event(self.timeline.now() + 12, process)
        self.timeline.schedule(event)
...
    def close(self) -> None:
        if self.timeline.now() >= 60:
            self.timeline.stop()
        self.opening = False
        process = Process(self, 'open', [])
        event = Event(self.timeline.now() + 12, process)
        self.timeline.schedule(event)
...

with the above methods, we can observe the state of store after a specfic time.

for t in [15, 32, 52]:
    tl = Timeline(t)
    store = Store(tl)
    print(tl.now())
    
    process = Process(store, 'open', [])
    event = Event(7, process)
    tl.schedule(event)
    
    tl.run()
    print(store.opening)
# True, True, False

Logging

To help track the numerous events that could be occurring during our simulation, and errors of varying levels of severity, we can use SeQUeNCe’s logging, based off of Python’s built-in logging system. This system has 5 built-in log levels, which in increasing level of significance are DEBUG, INFO, WARNING, ERROR, and CRITICAL. DEBUG is detailed information about the working of the program, INFO is specific important steps that are occuring, WARNING indicates an unexpected but non-detrimental issue is occuring, ERROR indicates some bug is causing a method to malfunction, and CRITICIAL indicates that some bug is halting the ability of the program to run at all. One should note that the default is for the ‘log level’ to be set to WARNING such that only logs of level WARNING or above are noticed. Now, we want to include logging in the Store class and must decide which occurances must be at which levels. As there are really only two things to log here, we only include INFO and WARNING levels:

  • INFO: As the logging level meant to notate important steps, INFO will record each time the self.opening value changes.

  • WARNING: As the logging level with fine but unexpected occurances, WARNING will record each time the open() or close() methods are called when the store is already respectively open or closed.

Now, we update the Store class:

from sequence.kernel.timeline import Timeline
from sequence.kernel.event import Event
from sequence.kernel.process import Process
import sequence.utils.log as log


class Store:
    def __init__(self, tl: Timeline):
        self.opening = False
        self.timeline = tl

    def open(self) -> None:
        if self.timeline.now() >= 60:
            self.timeline.stop()
        
        log.logger.info('Store being opened.')
        if self.opening == True:
            log.logger.warning('Store was already open.')

        self.opening = True
        process = Process(self, 'close', [])
        event = Event(self.timeline.now() + 12, process)
        self.timeline.schedule(event)

    def close(self) -> None:
        if self.timeline.now() >= 60:
            self.timeline.stop()

        log.logger.info('Store being closed.')
        if self.opening == False:
            log.logger.warning('Store was already closed.')

        self.opening = False
        process = Process(self, 'open', [])
        event = Event(self.timeline.now() + 12, process)
        self.timeline.schedule(event)

Assume the class Store is in a file named des_example3.py. To let the logging system work, we need to use the following code.

if __name__ == '__main__':
    tl = Timeline()
    tl.show_progress = False
    store = Store(tl)
    process = Process(store, 'open', [])
    event = Event(7, process)
    tl.schedule(event)
    
    log_filename = 'store.log'
    log.set_logger(__name__, tl, log_filename)
    log.set_logger_level('INFO')
    log.track_module('des_example3')
    tl.run()

The key parameters are:

  • log_filename: the name of the log file, which is store.log in this example

  • level: levels of severity, which is INFO in this example

  • module: the modules that we want to log, which is des_example3 in this example, because we assumed class Store is written in the file named des_example3.py.

After running the code, and open the log file named store.log, we will see the following:

7                    INFO    des_example3           Store being opened.
19                   INFO    des_example3           Store being closed.
31                   INFO    des_example3           Store being opened.
43                   INFO    des_example3           Store being closed.
55                   INFO    des_example3           Store being opened.
67                   INFO    des_example3           Store being closed.