4.5 Enhancing the Drive Through Pharmacy Model

In this section, we re-implement the drive through pharmacy model to illustrate a few more KSL constructs.

Example 4.5 (Enhance Drive Through Pharmacy) Consider again the drive through pharmacy situation. Assume that customers arrive at a drive through pharmacy window according to a Poisson distribution with a mean of 10 per hour. The time that it takes the pharmacist to serve the customer is random and data has indicated that the time is well modeled with an exponential distribution with a mean of 3 minutes. Customers who arrive to the pharmacy are served in the order of arrival and enough space is available within the parking area of the adjacent grocery store to accommodate any waiting customers. In this situation, we desire to estimate the time spent waiting in the queue. In addition, we would like to collect a histogram on the total time spent in the system.

Specifically, we will introduce the Queue and QObject classes and the EventGenerator class. The purpose is to cover the basics of these classes for future modeling. The Queue and QObject classes facilitate the holding of entities within waiting lines or queues, while the EventGenerator class codifies the basics of generating events according to a repetitive pattern. We start with the Queue and QObject classes.

The Queue class, is used to model waiting lines. The Queue class is a sub-class of ModelElement that is able to hold instances of the class QObject and will automatically collect statistics on the number in the queue and the time spent in the queue.

Overview of Classes Related to Queue and QObject

Figure 4.11: Overview of Classes Related to Queue and QObject

Figure 4.11 illustrates the classes involved when using the Queue and QObject classes. The first thing to note is that QObject is an inner class of ModelElement. This permits subclasses of ModelElement to create instances of QObject that have access to all the architecture of the model but are not model elements. That is, instances of QObject are transitory and are not added to the model element hierarchy that has been previously described. Users create QObject instances, use them to model elements of interest in the system that may experience waiting and then allow the Kotlin garbage collector to deallocate the memory associated with the instances. In Figure 4.11, we also see the class Queue<T: ModelElement.QObject is a sub-class of ModelElement that is parameterized by sub-types of QObject. Thus, users can develop sub-classes fo QObject and still use Queue to hold and collect statistics on those object instances. As noted in the figure, Queue also implements iterable. The last item to notice from Figure 4.11 is that queues can be governed by four basic queue disciplines: FIFO, LIFO, random, and ranked. In the case of a ranked queue, the queue is ordered by the priority property of QObject. The KSL permits the changing of the queue discipline during the simulation. The default queue discipline is FIFO.

Properties and Methods of Queue and QObject

Figure 4.12: Properties and Methods of Queue and QObject

Figure 4.12 presents the properties and methods of the Queue and QObject classes. Here we can see that the Queue class has some standard methods for inserting and removing QObject instances. For the purposes of this chapter, the most noteworthy of these are:

fun enqueue(qObject: T, priority: Int = qObject.priority, obj: Any? = qObject.attachedObject)

fun peekNext(): T? 

fun removeNext(): T? 

The enqueue method places QObject instances into the queue using the supplied priority. It also allows the user to attach an instance of Any to the QObject instance. The peekNext method provides a reference to the next QObject to be removed according the specified queue discipline and the removeNext method will remove the next QObject instance. During the enqueue and removal processes statistics are tabulated on the number of items in the queue and how much time the items spent in the queue. These responses are available via the timeInQ and numInQ properties. We will see how to use these classes within the revised pharmacy model. Before proceeding with reviewing the implementation, let us examine the EventGenerator class.

The EventGenerator class allows for the periodic generation of events similar to that achieved by “CREATE” modules in other simulation languages. This class works in conjunction with the GeneratorActionIfc interface, which is used to listen and react to the events that are generated by this class. Users of the class can supply an instance of an GeneratorActionIfc to provide the actions that take place when the event occurs. Alternatively, if no GeneratorActionIfc is supplied, by default the generator(event: KSLEvent) method of this class will be called when the event occurs. Thus, sub-classes can simply override this method to provide behavior for when the event occurs. If no instance of an GeneratorActionIfc instance is supplied and the generate() method is not overridden, then the events will still occur; however, no meaningful actions will take place. The key input parameters to the EventGenerator include:

time until the first event

This parameter is specified with an object that implements the RandomIfc. It should be used to represent a positive real value that represents the time after time 0.0 for the first event to occur. If this parameter is not supplied, then the first event occurs at time 0.0. This parameter is specified by the initialTimeUntilFirstEvent property.

time between events

This parameter is specified with an object that implements the RandomIfc. It should be used to represent a positive real value that represents the time between events. If this parameter is not supplied, then the time between events is positive infinity. This parameter is specified by the initialTimeBetweenEvents property.

time until last event

This parameter is specified with an object that implements the RandomIfc. It should be used to represent a positive real value that represents the time that the generator should stop generating. When the generator is created, this variable is used to set the ending time of the generator. Each time an event is to be scheduled the ending time is checked. If the time of the next event is past this time, then the generator is turned off and the event will not be scheduled. The default is positive infinity. This parameter is specified by the initialEndingTime property.

maximum number of events

A value of type long that supplies the maximum number of events to generate. Each time an event is to be scheduled, the maximum number of events is checked. If the maximum has been reached, then the generator is turned off. The default is Long.MAX_VALUE. This parameter cannot be Long.MAX_VALUE when the time until next always returns a value of 0.0. This is specified by the initialMaximumNumberOfEvents property.

the generator action

This parameter can be used to supply an instance of GeneratorActionIfc interface to supply logic to occur when the event occurs.

The most common use case for an EventGenerator is very similar to a compound Poisson process. The EventGenerator is setup to run with a time between events until the simulation completes; however, there are a number of other possibilities that are facilitated through various methods associated with the EventGenerator class. The first possibility is to sub-class the EventGenerator to make a custom generator for objects of a specific class. To facilitate this the user need only over ride the generate() method that is part of the EventGenerator class. For example, you could design classes to create customers, parts, trucks, demands, etc.

In addition to customization through sub-classes, there are a number of useful methods that are available for controlling the EventGenerator.

turnOffGenerator()

This method allows an EventGenerator to be turned off. The next scheduled generation event will not occur. This method will cancel a previously scheduled generation event if one exists. No future events will be scheduled after turning off the generator. Once the generator has been turned off, it cannot be restarted until the next replication.

turnOnGenerator(t: GetValueIfc)

If the generator was not started upon initialization at the beginning of a replication, then this method can be used to start the generator. The generator will be started \(t\) time units after the call. If this method is used when the generator is already started it does nothing. If this method is used after the generator is done it does nothing. If this method is used after the generator has been suspended it does nothing. In other words, if the generator is already on, this method does nothing.

suspend()

This method suspends the event generation pattern. The generator is still on, but the generation of events is suspended. The next scheduled generation event is canceled.

resume()

If the generator is suspended then this method causes the event generator to proceed with the event generation pattern by scheduling a new event according to the time between event distribution.

suspended

Checks if the generator is suspended.

done

Checks if the generator has been turned off. The generator can be turned off via the turnOffGenerator() method or it may turn off when it has reached its time until last event or if the maximum number of events is reached. As previously noted, once a generator has been turned off, it cannot be turned on again within the same replication.

In considering these methods, a generator can turn itself off (as an action) within or caused by the code within its generate()method or in the supplied GeneratorActionIfc interface. It might also suspend() itself in a similar manner. Of course, a class that has a reference to the generator may also turn it off or suspend it. To resume a suspended event generator, it is necessary to schedule an event whose action invokes the resume() method. Obviously, this can be within a sub-class of EventGenerator or within another class that has a reference to the event generator.

Now we are ready to review the revised implementation of the drive through pharmacy model which puts the Queue, QObject, and EventGenerator classes into action. Only portions of the code are illustrated here. For full details see the example files in the ksl.examples.book.chapter4 package. To declare an instance of the Queue class, we use the following code.

    private val mySTGT4: IndicatorResponse = IndicatorResponse({ x -> x >= 4.0 }, mySysTime, "SysTime > 4.0 minutes")

    private val mySysTime: Response = Response(this, "System Time")
    val systemTime: ResponseCIfc
        get() = mySysTime
        
    private val myWaitingQ: Queue<QObject> = Queue(this, "PharmacyQ")
    val waitingQ: QueueCIfc<QObject>
        get() = myWaitingQ

Notice how we also declare a public property that exposes part of the queue functionality, especially related to getting access to the statistical responses.

We can create and use an instance of EventGenerator with the following code. We see that the event generator uses an instance of the inner class Arrivals, which implements the GeneratorActionIfc. In addition, the time between arrivals random variable is supplied for both the time until the first event and the time between events. The initialize() method of the EventGenerator class ensures that the first event is scheduled at the start of the simulation. In addition, the EventGenerator class continues rescheduling the arrivals according to the time between arrival pattern. As previously noted, this process can be suspended, resumed, and turned off if needed. An event generator can also be specified not to automatically start at time 0.

    private val myArrivalGenerator: EventGenerator = EventGenerator(this, Arrivals(), myArrivalRV, myArrivalRV)
    private val endServiceEvent = this::endOfService

    private inner class Arrivals : GeneratorActionIfc {
        override fun generate(generator: EventGenerator) {
            myNS.increment() // new customer arrived
            val arrivingCustomer = QObject()
            myWaitingQ.enqueue(arrivingCustomer) // enqueue the newly arriving customer
            if (myNumBusy.value < numPharmacists) { // server available
                myNumBusy.increment() // make server busy
                val customer: QObject? = myWaitingQ.removeNext() //remove the next customer
                // schedule end of service, include the customer as the event's message
                schedule(endServiceEvent, myServiceRV, customer)
            }
        }
    }

In the code for arrivals, we also see the use of the Queue class (via the variable myWaitingQ) and the QObject class. On line 7 of the code, we see val arrivingCustomer = QObject() which creates an instance of QObject that represents the arriving customer. Then, using the enqueue method the customer is placed within the queue. This action is performed regardless of whether the customer has to wait to ensure that zero wait times are collected. Then, in lines 9-14, we see that the number busy is checked against the number of pharmacists. If there is an available pharmacists, the number busy is incremented, the next customer is removed from the queue, and the customer’s end of service action is scheduled. Notice how the schedule method is different from the previous implementation. In this implementation, the customer is attached to the KSLEvent instance and is held in the calendar with the event until the event is removed from the calendar and its execution commences. Let’s take a look at the revised end of service action.

    private fun endOfService(event: KSLEvent<QObject>) {
        myNumBusy.decrement() // customer is leaving server is freed
        if (!myWaitingQ.isEmpty) { // queue is not empty
            val customer: QObject? = myWaitingQ.removeNext() //remove the next customer
            myNumBusy.increment() // make server busy
            // schedule end of service
            schedule(endServiceEvent, myServiceRV, customer)
        }
        departSystem(event.message!!)
    }

    private fun departSystem(departingCustomer: QObject) {
        mySysTime.value = (time - departingCustomer.createTime)
        myNS.decrement() // customer left system
        myNumCustomers.increment()
    }

Here we see the same basic logic as in the previous example, except in this case we can use the queue. The queue is checked to see if it is empty, if not the next customer is removed and their service scheduled. We always handle the departing customer by grabbing it from the message attached to the event via the event.message property. This departing customer is sent to a private method that collects statistics on the customer. Notice two items. First, it is perfectly okay to call other methods from within the event routines. In fact, this is encouraged and can help organize your code.

Secondly, we are passing along the instance of QObject until it is no longer needed. In the departingSystem method, we get the current simulation time via the time property that is available to all model elements. This property represents the current simulation time. We then subtract off the time that the QObject was created by using the createTime property of the departing customer. This is assigned to the value property of an instance of Response called mySystemTime. This causes the system time of every departing customer to be collected and statistics reported. The process of collecting statistics on QObject instances is extremely common and only works if you understand how to pass the QObject instance along via the KSLEvent instances.

There is another little tidbit that is occurring in the reference coded snippet. Earlier in the arrivals code snippet, you might not have noticed the following line of code:

private val endServiceEvent = this::endOfService

For convenience, this line of code is capturing a functional reference to the endOfService method. The EventActionIfc interface is actually a functional interface, which allows functional references that contain the same signature to be used without having to implement the interface. This feature of Kotlin allows the functional reference to private fun endOfService(event: KSLEvent<QObject>) to serve as a parameter to the schedule() method. This alleviates the need to implement an inner class that extends the EventActionIfc interface. A similar strategy could have been done for the Arrivals implementation of GeneratorActionIfc for use in the event generator. The style that you employ can be based on your own personal preferences.

The results of running the simulation match the previously reported results.

Half-Width Statistical Summary Report - Confidence Level (95.000)% 

Name                                                Count         Average      Half-Width 
---------------------------------------------------------------------------------------------------- 
NumBusy                                                30          0.5035          0.0060 
# in System                                            30          1.0060          0.0271 
System Time                                            30          6.0001          0.1441 
PharmacyQ:NumInQ                                       30          0.5025          0.0222 
PharmacyQ:TimeInQ                                      30          2.9961          0.1235 
SysTime > 4.0 minutes                                  30          0.5136          0.0071 
Num Served                                             30       2513.2667         17.6883 
----------------------------------------------------------------------------------------------------

In the results, we see the system time and the queueing time reported. We also see a statistic called SysTime > 4.0 minutes. This was captured by using an IndicatorResponse, which is a subclass of Response that allows the user to specify a function that results in boolean expression and an instance of a Response to observe. The expression is collected as a 1.0 for true and 0.0 for false. In this example, we are observing the response called mySysTime.

    private val mySTGT4: IndicatorResponse = IndicatorResponse({ x -> x >= 4.0 }, mySysTime, "SysTime > 4.0 minutes")

This allow a probability to be estimated. In this case, we estimated the probability that a customer’s system time was more than 4.0 minutes.