6.2 The Process View
Before introducing the many of the technical details of processing modeling within the KSL, we will start with a simple example. The example will again be the drive through pharmacy model but in this section it will be implemented using the process modeling constructs available within the KSL.
Recall that we have a small pharmacy that has a single line for waiting customers and only one pharmacist. 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.
The drive through pharmacy system can be conceptualized as a single server waiting line system, where the server is the pharmacist. An idealized representation of this system is shown in Figure 6.2. In Section 4.4.4 of Chapter 4, we presented an activity diagram for the pharmacy.
The activity diagram is a basic description of the process experienced by the customer. In Chapter 4, we used the activity diagram to identify the arrival and end of service events. In this chapter, we will almost directly translate the activity diagram to KSL code.
Here is the portion of the KSL code to model the process for the customers of the drive through pharmacy:
private inner class Customer : Entity() {
val pharmacyProcess: KSLProcess = process() {
wip.increment()
timeStamp = time
val a = seize(worker)
delay(serviceTime)
release(a)
timeInSystem.value = time - timeStamp
wip.decrement()
numCustomers.increment()
}
}
The code defines a class called Customer
which is a subclass of Entity.
The Entity
class is a base class that encapsulates the ability to experience processes. Now, notice the definition and initialization of the variable pharmacyProcess
within the Customer
class. This is a very special function builder that allows for the definition of a coroutine that defines the process for the entity. Coroutines are software constructs that permit the suspension and resumption of programming statements. Kotlin supports the use of coroutines for asynchronous programming. In this situation, the KSL leverages Kotlin’s coroutine library to implement the process view.
Example 6.1 (Process View Model for Drive Through Pharmacy System) This example illustrates how to represent the previously presented drive through pharmacy model of Example 4.4 in Section 4.4.4 using KSL process view constructs.
class DriveThroughPharmacy(
parent: ModelElement,
numPharmacists: Int = 1,
ad: RandomIfc = ExponentialRV(1.0, 1),
sd: RandomIfc = ExponentialRV(0.5, 2),
name: String? = null
) : ProcessModel(parent, name) {
init {
require(numPharmacists > 0) { "The number of pharmacists must be >= 1" }
}
private val pharmacists: ResourceWithQ = ResourceWithQ(this, "Pharmacists", numPharmacists)
private var serviceTime: RandomVariable = RandomVariable(this, sd)
val serviceRV: RandomSourceCIfc
get() = serviceTime
private var timeBetweenArrivals: RandomVariable = RandomVariable(parent, ad)
val arrivalRV: RandomSourceCIfc
get() = timeBetweenArrivals
private val wip: TWResponse = TWResponse(this, "${this.name}:NumInSystem")
val numInSystem: TWResponseCIfc
get() = wip
private val timeInSystem: Response = Response(this, "${this.name}:TimeInSystem")
val systemTime: ResponseCIfc
get() = timeInSystem
private val numCustomers: Counter = Counter(this, "${this.name}:NumServed")
val numCustomersServed: CounterCIfc
get() = numCustomers
private val mySTGT4: IndicatorResponse = IndicatorResponse({ x -> x >= 4.0 }, timeInSystem, "SysTime > 4.0 minutes")
val probSystemTimeGT4Minutes: ResponseCIfc
get() = mySTGT4
override fun initialize() {
schedule(this::arrival, timeBetweenArrivals)
}
private fun arrival(event: KSLEvent<Nothing>) {
val c = Customer()
activate(c.pharmacyProcess)
schedule(this::arrival, timeBetweenArrivals)
}
private inner class Customer : Entity() {
val pharmacyProcess: KSLProcess = process() {
wip.increment()
timeStamp = time
val a = seize(pharmacists)
delay(serviceTime)
release(a)
timeInSystem.value = time - timeStamp
wip.decrement()
numCustomers.increment()
}
}
}
Most of this code should look very familiar. It includes the definition of the random variables to model the time between arrivals and service time. It also defines the responses to collect statistics on the performance of the system. In addition, it uses the functional notation for referencing functional interfaces to schedule the arrival events. Within the arrival event, the customer is created and it is told to activate()
its process.
The process called pharmacyProcess
has a nice linear flow and avoids the event call back approach of the event-view. The first line wip.increment()
simply increments the number of customers in the system. The next line involving timeStamp
assigns the arrival time of the customer. Then, the customer attempts to seize the pharmacist. If the pharmacist is available, it is allocated to the customer. The variable “a” holds the allocation information. If a pharmacist is not available, the customer is held in a queue related to the invocation of the seize()
function. The seize()
function is a suspending function. This is the magic of coroutines. The execution of the coroutine literally stops at within the seize()
function if the pharmacist is not available. When the pharmacist becomes available, the customer (and suspended code) resumes moving through the process. The delay()
function is another suspending function. The delay function essentially schedules an event to represent the service time and when the event occurs the coroutine is resumed. The release()
deallocates the resource from the customer. The final three lines simply collect statistical quantities.
It is critical to understand that 1) there are many entities created via the arrival
method, 2) they all experience the same process, and 3) they may all be at different points of their processes at different times. Since many customers are active at the same time (in a pseudo-parallelism) they compete for the pharmacist. This causes queueing. This process view depends on shared state. The primary shared state is via the resource. This is a new construct and was defined with the following line.
A ResourceWithQ
is a construct that can be used in ProcessModel
instances. These resources track the number of entities that are allocated and can hold them in a common queue while they wait. The ability to describe processes in this manner is what makes the process view very popular and often serves as the basis for commercial software. The KSL provides this view in open-source code.
If we run the code, we get the following 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
-----------------------------------------------------------------------------------------
If you look back at the previous results, you will see that these results are exactly the same! Thus, we can model the pharmacy with either the event view or the process view with confidence.
Before discussing additional functionality enabled within the KSLProcessBuilder,
we present some functionality that facilitates the creation and activation of entities. Notice that within the pharmacy model that after an customer is created to start its process we must schedule its activation.
private fun arrival(event: KSLEvent<Nothing>) {
val c = Customer()
activate(c.pharmacyProcess)
schedule(this::arrival, timeBetweenArrivals)
}
This is so common the KSL provides a class called EntityGenerator
that automates this process. The EntityGenerator
class subclassed from EventGenerator
and allows for a creation pattern to be specified. Figure 6.4 illustrates this relationship.
The EntityGenerator
class is defined as an inner class of ProcessModel.
Reviewing its code is useful.
protected inner class EntityGenerator<T : Entity>(
private val entityCreator: () -> T,
timeUntilTheFirstEntity: RandomIfc = ConstantRV.ZERO,
timeBtwEvents: RandomIfc = ConstantRV.POSITIVE_INFINITY,
maxNumberOfEvents: Long = Long.MAX_VALUE,
timeOfTheLastEvent: Double = Double.POSITIVE_INFINITY,
var activationPriority: Int = KSLEvent.DEFAULT_PRIORITY + 1,
name: String? = null
) : EventGenerator(
this@ProcessModel, null, timeUntilTheFirstEntity,
timeBtwEvents, maxNumberOfEvents, timeOfTheLastEvent, name
) {
override fun generate() {
val entity = entityCreator()
require(entity.defaultProcess != null) {"There was no initial process specified for the entity"}
activate(entity.defaultProcess!!, priority = activationPriority)
}
}
As shown in the code, the key additional functionality is that the EntityGenerator
takes in a function that knows how to create a subclass of type Entity.
The simplest way to provide such a function is to provide a reference to the constructor function of the subclass. This is illustrated in the following simplified re-do of the pharmacy model.
Example 6.2 (Demonstrating an Entity Generator) This example illustrates how to construct an instance of the EntityGenerator
class and use it to create entity instances according to a time between event pattern.
class EntityGeneratorExample(
parent: ModelElement,
name: String? = null
) : ProcessModel(parent, name) {
private val worker: ResourceWithQ = ResourceWithQ(this, "worker")
private val tba = ExponentialRV(6.0, 1)
private val st = RandomVariable(this, ExponentialRV(3.0, 2))
private val wip = TWResponse(this, "${this.name}:WIP")
private val tip = Response(this, "${this.name}:TimeInSystem")
private val generator = EntityGenerator(::Customer, tba, tba)
private val counter = Counter(this, "${this.name}:NumServed" )
private inner class Customer: Entity() {
val mm1: KSLProcess = process{
wip.increment()
timeStamp = time
val a = seize(worker)
delay(st)
release(a)
tip.value = time - timeStamp
wip.decrement()
counter.increment()
}
}
}
Notice the following line which takes in a reference to the Customer
class constructor using the functional syntax ::Customer.
There is no arrival method necessary because that logic is within the entity generator’s generate()
method:
override fun generate() {
val entity = entityCreator()
require(entity.defaultProcess != null) {"There was no initial process specified for the entity"}
activate(entity.defaultProcess!!, priority = activationPriority)
}
The entity is created using the passed in constructor function and the entity’s default process is started. By default, the process()
function of the KSLProcessBuilder
class automatically adds each newly defined process to the entity’s map of named processes. By default, the first defined process will be the default process. Thus, by using an instance of an EntityGenerator
associated with a particular subclass of Entity
, we can automatically create the instances of the subclass and activate their processes. Of course, the creation of the subclass (e.g. Customer
) might be much more complex; however, the EntityGenerator
takes in a function that creates the instance of the subclass. Thus, it can be any function, not just the constructor function. Therefore, instances can be configured in complex ways before they are activated by supplying an appropriate function.
IMPORTANT!
Note that an EntityGenerator
relies on the entity having at least one process that has been added to its processes via the process()
method. An entity generator will create the entity and start the process that is defined first. By default, the code-listing order of the process()
function definitions in the class, defines the order in which the processes are added to the entity’s processes.
In the next section, we will take a closer look at how the KSL makes the process view possible. This will help you to better use the process modeling capabilities found in the KSL.