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.
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.
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 theinitialTimeUntilFirstEvent
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 theinitialTimeBetweenEvents
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 theinitialEndingTime
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 beLong.MAX_VALUE
when the time until next always returns a value of 0.0. This is specified by theinitialMaximumNumberOfEvents
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:
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.