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)
}
}
}initialize() method does two things:scheduleEvent(myEventActionOne, 10.0)scheduleEvent(myEventActionTwo, 20.0)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.SchedulingEventExamples. The model is set to run for 100.0 time units and then simulated.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
Setup the simulation experiment
For each replication of the simulation:
Initialize the replication
Initialize the executive and calendar
Initialize the model and all model elements
While there are events on the event calendar or the simulation is not stopped
Determine the next event to execute
Update the current simulation time to the time of the next event
Call the action() method of the instance of EventActionIfc that was attached to the next event.
Execute the actions associated with the next event
Execute end of replication model logic
Execute end of simulation experiment logic
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)
}
}
}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
-------------------------------------------------------------------------------
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 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
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.
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
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
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
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.
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
}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() 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)
}
} 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()
}
}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\).
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()
}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
---------------------------------------------------------------------
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 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 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.
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.
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.
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
Queue class. To declare an instance of the Queue class to represent the pharmacy queue, we use the following code.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)
}
}
}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.
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()
}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.
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
---------------------------------------------------------------
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.
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.