Learning Objectives

  • To be able to develop and read an activity flow diagram
  • To be able to create, run, and examine the results of a KSL model of a simple DEDS

Simple Event Scheduling

class SchedulingEventExamples (parent: ModelElement, name: String? = null) :
    ModelElement(parent, name) {
    private val myEventActionOne: EventActionOne = EventActionOne()
    private val myEventActionTwo: EventActionTwo = EventActionTwo()

    override fun initialize() {
        // schedule a type 1 event at time 10.0
        schedule(myEventActionOne, 10.0)
        // schedule an event that uses myEventAction for time 20.0
        schedule(myEventActionTwo, 20.0)
    }

    private inner class EventActionOne : EventAction<Nothing>() {
        override fun action(event: KSLEvent<Nothing>) {
            println("EventActionOne at time : $time")
        }
    }

    private inner class EventActionTwo : EventAction<Nothing>() {
        override fun action(event: KSLEvent<Nothing>) {
            println("EventActionTwo at time : $time")
            // schedule a type 1 event for time t + 15
            schedule(myEventActionOne, 15.0)
            // reschedule the EventAction event for t + 20
            schedule(myEventActionTwo, 20.0)
        }
    }
}

Remarks for the Simple Event Scheduling Model

  • Notice that in this example the initialize() method does two things:
  1. schedules the first event action one event at time 10.0 via the call: scheduleEvent(myEventActionOne, 10.0)
  2. schedules the first action two event at time 20.0 via the call: scheduleEvent(myEventActionTwo, 20.0)
  • In the implemented EventActionTwo class, a simple message is printed and event action one is scheduled. In addition, the schedule() method is used to reschedule the action() method.

Running the Simple Event Scheduling Model

  • The model is created and used to create SchedulingEventExamples. The model is set to run for 100.0 time units and then simulated.
fun main() {
    val m = Model("Scheduling Example")
    SchedulingEventExamples(m.model)
    m.lengthOfReplication = 100.0
    m.simulate()
}
EventActionOne at time : 10.0
EventActionTwo at time : 20.0
EventActionOne at time : 35.0
EventActionTwo at time : 40.0
EventActionOne at time : 55.0
EventActionTwo at time : 60.0
EventActionOne at time : 75.0
EventActionTwo at time : 80.0
EventActionOne at time : 95.0
EventActionTwo at time : 100.0

Overview of Simulation Run Context

  1. Setup the simulation experiment

  2. For each replication of the simulation:

    1. Initialize the replication

    2. Initialize the executive and calendar

    3. Initialize the model and all model elements

    4. While there are events on the event calendar or the simulation is not stopped

      1. Determine the next event to execute

      2. Update the current simulation time to the time of the next event

      3. Call the action() method of the instance of EventActionIfc that was attached to the next event.

      4. Execute the actions associated with the next event

    5. Execute end of replication model logic

  3. Execute end of simulation experiment logic

Simulating a Poisson Process

This code example illustrates how to generate a Poisson process by scheduling events that have an inter-event time that is exponentially distributed. The code also illustrates how to use a Counter to collect statistics.

class SimplePoissonProcess (parent: ModelElement, name: String? = null) :
    ModelElement(parent, name) {
    private val myTBE: RandomVariable = RandomVariable(parent = this, rSource = ExponentialRV(1.0, streamNum = 1))
    private val myCount: Counter = Counter(parent = this, name = "Counts events")
    private val myEventHandler: EventHandler = EventHandler()

    override fun initialize() {
        super.initialize()
        schedule(myEventHandler, myTBE.value)
    }

    private inner class EventHandler : EventAction<Nothing>() {
        override fun action(event: KSLEvent<Nothing>) {
            myCount.increment()
            schedule(myEventHandler, myTBE.value)
        }
    }
}

Poisson Process Model Results

fun main(){
    val s = Model("Simple PP")
    SimplePoissonProcess(s.model)
    s.lengthOfReplication = 20.0
    s.numberOfReplications = 50
    s.simulate()
    println()
    val r = s.simulationReporter
    r.printAcrossReplicationSummaryStatistics()
    println("Done!")
}
Number of Replications: 50
Length of Warm up period: 0.0
Length of Replications: 20.0
-------------------------------------------------------------------------------
Counters
-------------------------------------------------------------------------------
Name                                  Average       Std. Dev.    Count 
-------------------------------------------------------------------------------
Counts events                       20.060000        3.235076       50.000000 
-------------------------------------------------------------------------------

Drive Through Pharmacy

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.

Activity Diagrams

  • An entity is a conceptual thing of importance that flows through a system potentially using the resources of the system while experiencing activities.
  • An activity is an operation that takes time to complete. Activities are defined by the occurrence of two events which represent the activity’s beginning time and ending time and mark the entrance and exit of the state associated with the activity.
  • A resource is something that is used by the entities and that may constrain the flow of the entities within the system.
  • An activity diagram is a pictorial representation of the process (steps of activities) for an entity and its interaction with resources while within the system.

Elements of an Activity Diagram

The notation of an activity diagram is very simple, and can be augmented as needed to explain additional concepts:

  • Queues: shown as a circle with queue labeled inside

  • Activities: shown as a rectangle with appropriate label inside

  • Resources: shown as small circles with resource labeled inside

  • Lines/arcs: indicating flow (precedence ordering) for engagement of entities in activities or for obtaining resources. Dotted lines are used to indicate the seizing and releasing of resources.

  • zigzag lines: indicate the creation or destruction of entities

Activity Diagram for Pharmacy System

Summary of Conceptual Model

  • System: The system has a pharmacist that acts as a resource, customers that act as entities, and a queue to hold the waiting customers. The state of the system includes the number of customers in the system, in the queue, and in service.

  • Events: Arrivals of customers to the system, which occur within an inter-event time that is exponentially distributed with a mean of 6 minutes.

  • Activities: The service time of the customers are exponentially distributed with a mean of 3 minutes.

  • Conditional delays: A conditional delay occurs when an entity has to wait for a condition to occur in order to proceed. In this system, the customer may have to wait in a queue until the pharmacist becomes available.

  • Resources: The pharmacist is the resource for the system.

Variable Definitions:

  • Let \(t\) represent the current simulation clock time.

  • Let \(c\) represent the number of available pharmacists

  • Let \(N(t)\) represent the number of customers in the system at any time \(t\).

  • Let \(Q(t)\) represent the number of customers waiting in line for the at any time \(t\).

  • Let \(B(t)\) represent the number of pharmacists that are busy at any time \(t\).

  • Let \(TBA_i\) represent the time between arrivals, which we will assume is exponentially distributed with a mean of 6 minutes.

  • Let \(ST_i\) represent the service time of the \(i^{th}\) customer, which we will assume is exponentially distributed with a mean of 3 minutes.

  • Let \(E_a\) represent the arrival event.

  • Let \(E_s\) represent the end of service event.

  • Let \(K\) represent the number of customers processed

Pseudo-Code: Arrival Actions for Event \(E_a\)

N(t) = N(t) + 1
if (B(t) < c)
  B(t) = B(t) + 1
  schedule E_s at time t + ST_i
else
  Q(t) = Q(t) + 1
endif
schedule E_a at time t + TBA_i

Pseudo-Code: Departure Actions for Event \(E_s\)

B(t) = B(t) - 1
if (Q(t) > 0)
  Q(t) = Q(t) - 1
  B(t) = B(t) + 1
  schedule E_s at time t + ST_i
endif
N(t) = N(t) - 1
K = K + 1

KSL Kotlin Code Overview

  • The drive through pharmacy system is modeled via a class DriveThroughPharmacy that sub-classes from ModelElement to provide the ability to schedule events.

  • The RandomVariable instances myServiceRV and myArrivalRV are used to represent the \(ST_i\) and \(TBA_i\) random variables.

  • \(B(t)\), \(N(t)\), and \(Q(t)\) are modeled with the objects myNumBusy, myNS, and myQ, respectively, all instances of the TWResponse class.

  • The tabulation of the number of processed customers, \(K\), is modeled with a KSL counter, using the Counter class.

  • The arrival event is represented by the inner class ArrivalEventAction extending the abstract class EventAction to model the arrival event action.

  • The end of service event is represented by the inner class EndServiceEventAction extending the abstract class EventAction to model the end of service event action.

KSL DriveThroughPharamcy Class

class DriveThroughPharmacy(
    parent: ModelElement, numServers: Int = 1,
    name: String? = null
) : ModelElement(parent, name) {
    init{
        require(numServers >= 1) {"The number of pharmacists must be >= 1"}
    }

    var numPharmacists = numServers
        set(value) {
            require(value >= 1){"The number of pharmacists must be >= 1"}
            field = value
        }

KSL DriveThroughPharamcy Class

private val myServiceRV: RandomVariable = RandomVariable(
        parent = this,
        rSource = ExponentialRV(mean = 0.5, streamNum = 2), name = "ServiceRV")
val serviceRV: RandomVariableCIfc
        get() = myServiceRV

private val myArrivalRV: RandomVariable = RandomVariable(
        parent = this,
        rSource = ExponentialRV(1.0, 1), name = "ArrivalRV")
val arrivalRV: RandomVariableCIfc
        get() = myArrivalRV

private val myNumInQ: TWResponse = TWResponse(this, "PharmacyQ")
val numInQ: TWResponseCIfc
    get() = myNumInQ

private val myNumBusy: TWResponse = TWResponse(this, "NumBusy")
private val myNS: TWResponse = TWResponse(this, "# in System")
private val myNumCustomers: Counter = Counter(this, name = "Num Served")
private val myArrivalEventAction: ArrivalEventAction = ArrivalEventAction()
private val myEndServiceEventAction: EndServiceEventAction = EndServiceEventAction()

KSL DriveThroughPharamcy Arrival Event

    private inner class ArrivalEventAction : EventAction<Nothing>() {
        override fun action(event: KSLEvent<Nothing>) {
            myNS.increment() // new customer arrived
            if (myNumBusy.value < numPharmacists) { // server available
                myNumBusy.increment() // make server busy
                // schedule end of service
                schedule(myEndServiceEventAction, myServiceRV)
            } else {
                myNumInQ.increment() // customer must wait
            }
            // always schedule the next arrival
            schedule(myArrivalEventAction, myArrivalRV)
        }
    }

KSL DriveThroughPharamcy End of Service Event

    private inner class EndServiceEventAction : EventAction<Nothing>() {
        override fun action(event: KSLEvent<Nothing>) {
            myNumBusy.decrement() // customer is leaving server is freed
            if (myNumInQ.value > 0) { // queue is not empty
                myNumInQ.decrement() //remove the next customer
                myNumBusy.increment() // make server busy
                // schedule end of service
                schedule(myEndServiceEventAction, myServiceRV)
            }
            myNS.decrement() // customer left system
            myNumCustomers.increment()
        }
    }

KSL DriveThroughPharamcy Initializing the Model

  • Notice the use of the initialize() method to schedule the first arrival event.

  • The initialize() method is called just prior to the start of the replication for the simulation.

  • The modeler can think of the initialize() method as being called at time \(t^{-}=0\).

    override fun initialize() {
        super.initialize()
        // start the arrivals
        schedule(myArrivalEventAction, myArrivalRV)
    }

KSL DriveThroughPharamcy Running the Model

fun main() {
    val model = Model("Drive Through Pharmacy", autoCSVReports = true)
    model.numberOfReplications = 30
    model.lengthOfReplication = 20000.0
    model.lengthOfReplicationWarmUp = 5000.0
    // add DriveThroughPharmacy to the main model
    val dtp = DriveThroughPharmacy(model, 1)
    dtp.arrivalRV.initialRandomSource = ExponentialRV(6.0, 1)
    dtp.serviceRV.initialRandomSource = ExponentialRV(3.0, 2)
    model.simulate()
    model.print()
}

KSL DriveThroughPharamcy Example Results

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

Name                         Count        Average      Half-Width 
----------------------------------------------------------------------
PharmacyQ                    30            0.5025          0.0222 
NumBusy                      30            0.5035          0.0060 
# in System                  30            1.0060          0.0271 
aggregate # in system        30            1.0060          0.0271 
Num Served                   30         2513.2667         17.6883 
--------------------------------------------------------------------- 

Validating the Results Against Theory

This particular pharmacy model happens to be an example of an M/M/1 queuing model. According to queuing theory, the expected number of customer in queue, \(L_q\), for the M/M/1 model with arrival rate \(\lambda\), service rate \(\mu\), and \(\rho = \lambda/\mu\) is: \[ L_q = \dfrac{\rho^2}{1 - \rho} \] In addition, the expected waiting time in queue is given by \(W_q = L_q/\lambda\). In the pharmacy model, \(\lambda\) = 1/6, i.e. 1 customer every 6 minutes on average, and \(\mu\) = 1/3, i.e. 1 customer every 3 minutes on average. The quantity, \(\rho\), is called the utilization of the server. Using these values in the formulas for \(L_q\) and \(W_q\) results in:

\[ \begin{aligned} \rho & = 0.5 \\ L_q & = \dfrac{0.5 \times 0.5}{1 - 0.5} = 0.5 \\ W_q & = \dfrac{0.5}{1/6} = 3 \: \text{minutes} \end{aligned} \]

The Queue and QObject Classes

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.

The Queue and QObject Classes

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.

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.

EventGenerator Class: Key Parameters

  • time until the first event - This parameter is specified with an object that implements the RandomIfc used to represent a positive real value that represents the time after time 0.0 for the first event to occur.

  • time between events - This parameter is specified with an object that implements the RandomIfc used to represent a positive real value that represents the time between events.

  • 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. The default is positive infinity.

  • maximum number of events - A value of type long that supplies the maximum number of events to generate. 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.

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

Event Generation Process

The following figure illustrates an arrival process where the time of the first arrival is given by T1, the time of the second arrival is given by T1 + T2, and the time of the third arrival is given by T1 + T2 + T3. In the figure, the number of arriving entities at each arriving event is given by N1, N2, and N3 respectively

Using the Queue and QObject in the Pharmacy Model

  • If we want to collect and report statistics on the time spent in the queue, then we should use and instance of the Queue class. To declare an instance of the Queue class to represent the pharmacy queue, we use the following code.
    private val myWaitingQ: Queue<QObject> = Queue(this, "PharmacyQ")
    val waitingQ: QueueCIfc<QObject>
        get() = myWaitingQ

Using the Queue and QObject in the Arrvial Event

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)
      }
  }
}

Using the Queue and QObject in the Arrvial Event

  • Notice that the event generator uses the time between arrival random variable and specifies an instance of the Arrival inner class as the object to represent the arrival actions.

  • When the arrival occurs the number of customers is incremented and an instance of QObject is created to represent the customer.

  • The newly arriving customer immediately enters the queue to ensure queue statistics are collected (even if they do not wait).

  • Then, the pharmacist is checked for availability. If the pharmacist is available, the customer is removed from the queue and their end of service event is scheduled.

  • 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. This occurs via the schedule() method.

Using the Queue and QObject in the End of Service Event

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()
}

Using the Queue and QObject in the End of Service Event

  • We 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.

  • The number of busy pharmacists is decremented.

  • Then the queue is checked to see if it is not empty. If it is not empty, the next customer is removed from the queue and started into service.

  • The departSystem() method collects the total system time, decrements the total number of customers in the system, and increments the count of the customers processed.

New Results for Pharmacy Model

  • The queue statistics are automatically reported.
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 
---------------------------------------------------------------

Summary of New Constructs

  • Model: Used to hold all model elements. Automatically created by the Simulation class. Used to create and control the execution of the model.

  • ModelElement: Used as an abstract base class for creating new model elements for a simulation.

  • RandomVariable: A sub-class of ModelElement used to model randomness within a simulation.

  • Response: A sub-class of ModelElement used to collect statistics on observation-based variables.

  • TWResponse: A sub-class of Response used to collect statistics on time-weighted variables in the model.

  • Counter: A sub-class of ModelElement used to count occurrences and collect statistics.

  • Queue: A sub-class of ModelElement that holds instances of the class QObject and will automatically collect statistics on the number in the queue and the time spent in the queue.

Summary of New Constructs

  • IndicatorResponse: A sub-class of Response used to collect on boolean expressions by observing another response.

  • AggregateTWResponse: A sub-class of TWResponse used to collect time weighted statistics by observing other TWResponse instances.

  • SimulationReporter: Used to gather and report statistics on a simulation model.

  • KSLEvent: Used to model different events scheduled in time during a simulation.

  • EventActionIfc: An interface used to define an action() method that represents event logic within the simulation.

  • EventGenerator: A subclass of ModelElement that facilitates the repeated generation of events.

  • GeneratorActionIfc: An interface used to implement the actions associated with event generators.

⌂ Index