7.1 Modeling with Processes and Resources
In this section, we explore some of the nuances of modeling systems via process and resources. Specifically, we will look more carefully at the KSL constructs for modeling resources. In general, a resource is something that is required or used by the entities within a process. In real systems, whenever there is requirement for an entity to wait, it is likely that a resource is needed. Within the KSL, a resource has number of identical units which represent its capacity. For the purposes of usage, the units of the resource are indistinguishable. That is, there can be no requirement to use one of the units over some other of the units. For the entity, it does not matter which unit is supplied for a request. It only matters that it gets the requested number of units from the resource. Resource units are allocated and deallocated to entities that request them. If a request for a specific number of units of the resource cannot be allocated immediately upon request, then the entity (generally) must wait for the units (or not proceed with the request). If there is a preference between types of resources, then a pool of resources should be used. Pools of resources will also be discussed in this section. We begin our study of resources by illustrating how resources can be used to model space.
7.1.1 Modeling Space with Resources
We start the modeling of space with resources by looking at a simple system involving a tandem queue. A tandem queue is a sequence of queues in order that must be visited to receive service from resources. Let’s setup the situation by first modeling the system without a space requirement between the queues. The following example presents the specifics of the situation.
Example 7.1 (Tandem Queueing System) Suppose a service facility consists of two stations in series (tandem), each with its own FIFO queue. Each station consists of a queue and a single server. A customer completing service at station 1 proceeds to station 2, while a customer completing service at station 2 leaves the facility. Assume that the inter-arrival times of customers to station 1 are IID exponential random variables with a mean of 1 minute. Service times of customers at station 1 are exponential random variables with a mean of 0.7 minute, and at station 2 are exponential random variables with mean 0.9 minute. Develop an model for this system. Run the simulation for exactly 20000 minutes and estimate for each station the expected average delay in queue for the customer, the expected time-average number of customers in queue, and the expected utilization. In addition, estimate the average number of customers in the system and the average time spent in the system.
To model this situation, we need to use two resources, one for each server at the two stations.
Figure 7.1 illustrates this situation. This is a perfect application of what we have previously studied about modeling processes. The following code sets up the KSL constructs that are needed within the process modeling.
class TandemQueue(parent: ModelElement, name: String? = null) : ProcessModel(parent, name) {
private val worker1: ResourceWithQ = ResourceWithQ(this, "worker1")
private val worker2: ResourceWithQ = ResourceWithQ(this, "worker2")
private val tba = ExponentialRV(2.0, 1)
private val st1 = RandomVariable(this, ExponentialRV(0.7, 2))
val service1RV: RandomSourceCIfc
get() = st1
private val st2 = RandomVariable(this, ExponentialRV(0.9, 3))
val service2RV: RandomSourceCIfc
get() = st2
private val myArrivalGenerator = EntityGenerator(::Customer, tba, tba)
val generator: EventGeneratorCIfc
get() = myArrivalGenerator
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
Since the resources will need a queue, we declare the two resources by using the ResourceWithQ
class. Then, we specify the random variables that will be used to represent the time between arrivals and the service time at the two stations. Finally, we define the response variables for collecting the number in the system and the time in the system. The process modeling is a straight-forward application of the (seize-delay-release) pattern that was presented in the previous chapter.
private inner class Customer : Entity(){
val tandemQProcess : KSLProcess = process {
wip.increment()
timeStamp = time
seize(worker1)
delay(st1)
release(worker1)
seize(worker2)
delay(st2)
release(worker2)
timeInSystem.value = time - timeStamp
wip.decrement()
}
Notice that this code does not save the allocations that are returned by the call to the seize
method. This is possible because of two reasons. First, the KSL overloads the release
method to allow for the specification of the resource to be released. This overloaded method will release all previously returned allocations that the entity holds of the named resource. Secondly, this situation is a perfect use case for this, because the entity only has seized the worker once before the specified release. Thus, we are simply releasing the last allocation held by the entity via the release()
function. It really is user preference whether to save the allocation and then release the allocation or to use the approach of specifying the name of the resource. Of course, this works because releasing all previous allocations is the same as releasing the last one in this situation. In fact, this situation is so common that the KSL provides an additional short cut as illustrated in the following code.
private inner class Customer : Entity(){
val tandemQProcess : KSLProcess = process {
wip.increment()
timeStamp = time
use(worker1, delayDuration = st1)
use(worker2, delayDuration = st2)
timeInSystem.value = time - timeStamp
wip.decrement()
}
}
The use
method combines the (seize-delay-release) pattern into one method call. The results are not very remarkable.
Name | Count | Average | Half-Width |
---|---|---|---|
worker1:InstantaneousUtil | 30 | 0.351 | 0.002 |
worker1:NumBusyUnits | 30 | 0.351 | 0.002 |
worker1:ScheduledUtil | 30 | 0.351 | 0.002 |
worker1:WIP | 30 | 0.54 | 0.007 |
worker1:Q:NumInQ | 30 | 0.189 | 0.005 |
worker1:Q:TimeInQ | 30 | 0.379 | 0.008 |
worker2:InstantaneousUtil | 30 | 0.45 | 0.003 |
worker2:NumBusyUnits | 30 | 0.45 | 0.003 |
worker2:ScheduledUtil | 30 | 0.45 | 0.003 |
worker2:WIP | 30 | 0.82 | 0.01 |
worker2:Q:NumInQ | 30 | 0.37 | 0.008 |
worker2:Q:TimeInQ | 30 | 0.741 | 0.014 |
TandemQModel:NumInSystem | 30 | 1.36 | 0.014 |
TandemQModel:TimeInSystem | 30 | 2.723 | 0.02 |
worker1:SeizeCount | 30 | 7492.333 | 32.526 |
worker2:SeizeCount | 30 | 7492.533 | 32.356 |
Now we are ready to study the situation of modeling finite space between the two stations.
Example 7.2 (Tandem Queueing System With Blocking) Imagine that at the first station there is a chair for the customer to sit in while receiving service from the first worker. Any customers that arrive while a customer is receiving service at the first station must wait for the server to be free (i.e. the chair to be available). We assume that there is (at least conceptually) an infinite amount of space for the waiting customers at the first station. Now, at the second station, there are two chairs. The customer arriving to the second station will sit in the first chair when receiving service from the server. The second chair is provided for one waiting customer and there is no space for any other customers to wait at the second station. Thus, a customer finishing service at the first station cannot move into (use) the second station if there is a customer waiting at the second station. If a customer at the first station cannot move into the waiting line (2nd chair) at the second station, then the customer is considered blocked. What does this customer do? Well, they are selfish and do not give up their current chair until they can get a chair at the second station. Thus, waiting at the second station may cause waiting to occur at the first station. This situation is called a tandem queue with blocking, as illustrated in Figure 7.2.
Let’s see how to model this situation using KSL constructs. The key is to model the chair that represents the waiting line at the second station with a resource. This is essentially modeling the waiting space with a resource. In the above scenario, there was one space for waiting.
class TandemQueueWithBlocking(parent: ModelElement, name: String? = null) : ProcessModel(parent, name) {
private val buffer: ResourceWithQ = ResourceWithQ(this, "buffer", capacity = 1)
private val worker1: ResourceWithQ = ResourceWithQ(this, "worker1")
private val worker2: ResourceWithQ = ResourceWithQ(this, "worker2")
In this code, we defined a resource called buffer
with capacity 1 to represent the chair designated for waiting. By changing the capacity of this resource, we can study the effect of the limited space at the second station on system performance.
Exploring the effect of the buffer size is left as an exercise for the reader. The process modeling needs to be adjusted to account for this space. In the following code, notice the overlapping nature of the seize and release statements.
private inner class Customer : Entity() {
val tandemQProcess: KSLProcess = process {
wip.increment()
timeStamp = time
val a1 = seize(worker1)
delay(st1)
val b = seize(buffer)
release(a1)
val a2 = seize(worker2)
release(b)
delay(st2)
release(a2)
timeInSystem.value = time - timeStamp
wip.decrement()
}
}
After receiving service at the first station, the customer attempts to seize the buffer (chair for waiting) at the second station. If there is space in the buffer, then the customer releases the first worker. Don’t give up your chair until you get the next chair! After moving into the chair (buffer), the customer attempts to seize the second worker. If the second worker is not available the customer waits; otherwise, the customer delays for service at the second station, releases the second worker, and then departs that system. Again the key is to have this overlapping seize and release statements.
Figure 7.3 provides the activity diagram for this situation. Notice how the arrows for the seize and release of the resources overlap.
For the given arrival rate and service parameters, the results indicate that the effect of blocking is not too significant.
Name | Count | Average | Half-Width |
---|---|---|---|
buffer:InstantaneousUtil | 30 | 0.196 | 0.003 |
buffer:NumBusyUnits | 30 | 0.196 | 0.003 |
buffer:ScheduledUtil | 30 | 0.196 | 0.003 |
buffer:WIP | 30 | 0.267 | 0.004 |
buffer:Q:NumInQ | 30 | 0.071 | 0.002 |
buffer:Q:TimeInQ | 30 | 0.143 | 0.003 |
worker1:InstantaneousUtil | 30 | 0.422 | 0.003 |
worker1:NumBusyUnits | 30 | 0.422 | 0.003 |
worker1:ScheduledUtil | 30 | 0.422 | 0.003 |
worker1:WIP | 30 | 0.761 | 0.012 |
worker1:Q:NumInQ | 30 | 0.339 | 0.009 |
worker1:Q:TimeInQ | 30 | 0.678 | 0.016 |
worker2:InstantaneousUtil | 30 | 0.45 | 0.003 |
worker2:NumBusyUnits | 30 | 0.45 | 0.003 |
worker2:ScheduledUtil | 30 | 0.45 | 0.003 |
worker2:WIP | 30 | 0.646 | 0.005 |
worker2:Q:NumInQ | 30 | 0.196 | 0.003 |
worker2:Q:TimeInQ | 30 | 0.392 | 0.004 |
TandemQModelWithBlocking:NumInSystem | 30 | 1.406 | 0.016 |
TandemQModelWithBlocking:TimeInSystem | 30 | 2.815 | 0.023 |
buffer:SeizeCount | 30 | 7492.467 | 32.384 |
worker1:SeizeCount | 30 | 7492.333 | 32.406 |
worker2:SeizeCount | 30 | 7492.5 | 32.367 |
However, the exercises ask the reader to explore what happens if the arrival rate is increased. A tandem queueing system is just a series of stations. The concept of having stations where work is performed is very useful. A later section illustrates how to generalize these ideas, but first we explore how to organize resources into sets or pools from which resources can be selected.
7.1.2 Resource Pools
A resource pool is a generalization of the concept of a resource that permits individual instances of the Resource
class to be combined together into a larger pool of units. Resource pools facilitate the sharing of instances of resources across processes. The important concepts involved in using resource pools are 1) how to select resources to satisfy a request, and 2) how to allocate a request for units across the pool of resources. For example, if a request for units of the pool was for 2 units, and the pool contained 3 individual resources all of capacity 1, which of the 3 resources should be selected to provide the requested units? Also, suppose, for example, the request was for 2 units, and there were 3 individual resources with capacity (1, 2, 3) units, respectively. Should the resource with capacity 2 be used? Should the resource with capacity 3 be used? Should the resource with capacity 1 be used in combination with one of the other resources? As you can see, there may be many possible ways to allocate units to requests when there is a pool of resources. The KSL provides a structure for users to supply selection and allocation rules when using pools of resources through some interfaces.
/**
* Provides for a method to select resources from a list such that
* the returned list may contain resources that can fill the amount needed
*/
fun interface ResourceSelectionRuleIfc {
/**
* @param amountNeeded the amount needed from resources
* @param list of resources to consider selecting from
* @return the selected list of resources. It may be empty
*/
fun selectResources(amountNeeded: Int, list: List<Resource>): List<Resource>
}
/**
* Function to determine how to allocate requirement for units across
* a list of resources that have sufficient available units to meet
* the amount needed.
*/
fun interface AllocationRuleIfc {
/** The method assumes that the provided list of resources has
* enough units available to satisfy the needs of the request.
*
* @param amountNeeded the amount needed from resources
* @param resourceList list of resources to be allocated from
* @return the amount to allocate from each resource as a map
*/
fun makeAllocations(amountNeeded: Int, resourceList: List<Resource>): Map<Resource, Int>
}
These two interfaces can be used in combination to form various selection and allocation possibilities for a variety of resource pool situations. The ResourcePool
and ResourcePoolWithQ
classes use default implementations of these functions. The KSL provides two implementations of the ResourceSelectionRuleIfc
interface.
FirstFullyAvailableResource
selects the first resource from a supplied list that can fully meet the request.ResourceSelectionRule
selects a list of resources that (in total) have enough available units to fully meet the request.
It is important to note that the ResourceSelectionRuleIfc
interface may return an empty list if the request cannot be met. This is used to determine if the entity must wait.
The KSL provides a default instance of the AllocationRuleIfc
interface called DefaultAllocationRule.
This rule takes in a list of resources that in total has enough units available and allocates from each listed resource (in the order listed) until the entire amount requested is filled. Thus, in both the selection rule and the allocation rule, the order of the resources within the pool are important. Again, if you want or need to have different rules, then you can implement these interfaces and supply your instances to the ResourcePool
and ResourcePoolWithQ
classes to use instead of the default implementations. Let’s take a look at an example situation involving resource pools.
Example 7.3 (Resource Pools) In this example, there are two pools. The first pool will have 3 resources (john, paul, and george) and the second pool will have 2 resources (ringo and george). One of the resources (george) is shared (in common) between the two pools. The following code creates the four resources, adds them to lists, and then supplies the lists to instances of the ResourcePoolWithQ
class.
class ResourcePoolExample(parent: ModelElement) : ProcessModel(parent, null) {
private val john = Resource(this, name = "John")
private val paul = Resource(this, name = "Paul")
private val george = Resource(this, name = "George")
private val ringo = Resource(this, name = "Ringo")
private val list1 = listOf(john, paul, george)
private val list2 = listOf(ringo, george)
private val pool1: ResourcePoolWithQ = ResourcePoolWithQ(this, list1, name = "pool1")
private val pool2: ResourcePoolWithQ = ResourcePoolWithQ(this, list2, name = "pool2")
private val tba = RandomVariable(this, ExponentialRV(1.0, 1), "Arrival RV")
private val st = RandomVariable(this, ExponentialRV(3.0, 2), "Service RV")
private val decideProcess = RandomVariable(this, BernoulliRV(0.7, 3))
private val wip1 = TWResponse(this, "${name}:WIP1")
private val tip1 = Response(this, "${name}:TimeInSystem1")
private val wip2 = TWResponse(this, "${name}:WIP2")
private val tip2 = Response(this, "${name}:TimeInSystem2")
private val generator = EventGenerator(this, this::arrivals, tba, tba)
The two pools are shared between two processes using straightforward (seize-delay-release) logic. Notice that the addToSequence
parameter of the process()
function is set to false. This is done because the Customer
class has two processes defined and we do not need the second process (usePool2
) to start immediately after the first process completes.
private inner class Customer: Entity() {
val usePool1: KSLProcess = process("Pool 1 Process") {
wip1.increment()
timeStamp = time
val a = seize(pool1, 1)
delay(st)
release(a)
tip1.value = time - timeStamp
wip1.decrement()
}
val usePool2: KSLProcess = process("Pool 2 Process") {
wip2.increment()
timeStamp = time
val a = seize(pool2, 1)
delay(st)
release(a)
tip2.value = time - timeStamp
wip2.decrement()
}
}
In this example, we randomly activate the two processes based on a distribution.
private fun arrivals(generator: EventGenerator){
val c = Customer()
if (decideProcess.value.toBoolean()){
activate(c.usePool1)
} else {
activate(c.usePool2)
}
}
Since there are four resources and two pools, the performance reports on all the individual resources usage as well as the overall performance of the pool.
Resource Pool Example Statistical Summary Report
Name | Count | Average | Half-Width |
---|---|---|---|
John:InstantaneousUtil | 30 | 0.663 | 0.003 |
John:NumBusyUnits | 30 | 0.663 | 0.003 |
John:ScheduledUtil | 30 | 0.663 | 0.003 |
Paul:InstantaneousUtil | 30 | 0.663 | 0.003 |
Paul:NumBusyUnits | 30 | 0.663 | 0.003 |
Paul:ScheduledUtil | 30 | 0.663 | 0.003 |
George:InstantaneousUtil | 30 | 0.834 | 0.003 |
George:NumBusyUnits | 30 | 0.834 | 0.003 |
George:ScheduledUtil | 30 | 0.834 | 0.003 |
Ringo:InstantaneousUtil | 30 | 0.833 | 0.003 |
Ringo:NumBusyUnits | 30 | 0.833 | 0.003 |
Ringo:ScheduledUtil | 30 | 0.833 | 0.003 |
pool1:NumBusy | 30 | 2.16 | 0.007 |
pool1:FractionBusy | 30 | 0.72 | 0.002 |
pool1:Q:NumInQ | 30 | 0.851 | 0.023 |
pool1:Q:TimeInQ | 30 | 1.708 | 0.044 |
pool2:NumBusy | 30 | 1.666 | 0.007 |
pool2:FractionBusy | 30 | 0.833 | 0.003 |
pool2:Q:NumInQ | 30 | 2.631 | 0.101 |
pool2:Q:TimeInQ | 30 | 5.253 | 0.193 |
ResourcePoolExample_3:WIP1 | 30 | 2.34 | 0.028 |
ResourcePoolExample_3:TimeInSystem1 | 30 | 4.698 | 0.05 |
ResourcePoolExample_3:WIP2 | 30 | 4.134 | 0.108 |
ResourcePoolExample_3:TimeInSystem2 | 30 | 8.255 | 0.201 |
John:SeizeCount | 30 | 3318.533 | 19.247 |
Paul:SeizeCount | 30 | 3335.833 | 24.126 |
George:SeizeCount | 30 | 4174.967 | 24.294 |
Ringo:SeizeCount | 30 | 4151.9 | 23.655 |
Resource pools can be helpful when modeling the sharing of resources between activities. In the next section, we discuss a more complex situation involving a flow shop.
7.1.3 Computer Test and Repair Shop Example
This section presents a common modeling situation in which entities follow a processing plan until they are completed. The KSL makes this type of modeling easy because it can leverage the full functionality of the Kotlin language.
Example 7.4 (Computer Test and Repair Shop) Consider a test and repair shop for computer parts (e.g. circuit boards, hard drives, etc.) The system consists of an initial diagnostic station through which all newly arriving parts must be processed. Currently, newly arriving parts arrive according to a Poisson arrival process with a mean rate of 3 per hour. The diagnostic station consists of 2 diagnostic machines that are fed the arriving parts from a single queue. Data indicates that the diagnostic time is quite variable and follows an exponential distribution with a mean of 30 minutes. Based on the results of the diagnostics, a testing plan is formulated for the parts. There are currently three testing stations 1. 2, 3 which consist of one machine each. The testing plan consists of an ordered sequence of testing stations that must be visited by the part prior to proceeding to a repair station. Because the diagnosis often involves similar problems, there are common sequences that occur for the parts. The company collected extensive data on the visit sequences for the parts and found that the sequences in Table 7.1 constituted the vast majority of test plans for the parts.
Test Plan | % of Parts | Sequence |
---|---|---|
1 | 25% | 2,3,2,1 |
2 | 12.5% | 3,1 |
3 | 37.5% | 1,3,1 |
4 | 25% | 2,3 |
For example, 25% of the newly arriving parts follow test plan 1, which consists of visiting test stations 2, 3, 2, and 1 prior to proceeding to the repair station.
The testing of the parts at each station takes time that may depend upon the sequence that the part follows. That is, while parts that follow test plan’s 1 and 3 both visit test station 1, data shows that the time spent processing at the station is not necessarily the same. Data on the testing times indicate that the distribution is well modeled with a lognormal distribution with mean, \(\ \mu\), and standard deviation, \(\sigma\) in minutes. Table 7.2 presents the mean and standard deviation for each of the testing time distributions for each station in the test plan.
Test Plan | Testing Time Parameters | Repair Time Parameters |
---|---|---|
1 | (20,4.1), (12,4.2), (18,4.3), (16,4.0) | (30,60,80) |
2 | (12,4), (15,4) | (45,55,70) |
3 | (18,4.2), (14,4.4), (12,4.3) | (30,40,60) |
4 | (24,4), (30,4) | (35,65,75) |
For example, the first pair of parameters, (20, 4.1), for test plan 1 indicates that the testing time at test station 2 has a lognormal distribution with mean,\(\mu = 20\), and standard deviation,\(\sigma = 4.1\) minutes.
The repair station has 3 workers that attempt to complete the repairs based on the tests. The repair time also depends on the test plan that the part has been following. Data indicates that the repair time can be characterized by a triangular distribution with the minimum, mode, and maximum as specified in the previous table. After the repairs, the parts leave the system. When the parts move between stations assume that there is always a worker available and that the transfer time takes between 2 to 4 minutes uniformly distributed. Figure 7.4 illustrates the arrangement of the stations and the flow of the parts following Plan 2 in the test and repair shop.
The company is considering accepting a new contract that will increase the overall arrival rate of jobs to the system by 10%. They are interested in understanding where the potential bottlenecks are in the system and in developing alternatives to mitigate those bottlenecks so that they can still handle the contract. The new contract stipulates that 80% of the time the testing and repairs should be completed within 480 minutes. The company runs 2 shifts each day for each 5 day work week. Any jobs not completed at the end of the second shift are carried over to first shift of the next working day. Assume that the contract is going to last for 1 year (52 weeks). Build a simulation model that can assist the company in assessing the risks associated with the new contract.
7.1.3.1 Implementing the Test and Repair Model
Before implementing the model, you should prepare by conceptualizing the process flow. Figure 7.5 illustrates the activity diagram for the test and repair system. Parts are created and flow first to the diagnostic station where they seize a diagnostic machine while the diagnostic activity occurs. Then, the test plan is assigned. The flow for the visitation of the parts to the test station is shown with a loop back to the transfer time between the stations. It should be clear that the activity diagram is representing any of the three test stations. After the final test station in the test plan has been visited, the part goes to the repair station, where 1 of 3 repair workers is seized for the repair activity. After the repair activity, the part leaves the system.
In many modeling contexts, entities will follow a specific path through the system. In a manufacturing job shop, this is often called the process plan. In a bus system, this is called a bus route. In the test and repair system, this is referred to as the test plan. To model a specify path through the system, we need an approach to specify a sequence of stations. A sequence consists of an ordered list of steps. Each step must indicate the resources and other information associated with the step. Thus, a sequence is built by simply providing the list of steps that must be visited. We will use Kotlin lists to hold this information. Let’s take a look at the implementation.
Because of the requirement that different parts follow different sequences and have different processing times based on what resource they are using and where they are in the sequence, there are many random variables that need to be defined for this model.
class TestAndRepairShop(parent: ModelElement, name: String? = null) : ProcessModel(parent, name) {
// define the random variables
private val tba = ExponentialRV(20.0)
private val t11 = RandomVariable(this, LognormalRV(20.0, 4.1))
private val t21 = RandomVariable(this, LognormalRV(12.0, 4.2))
private val t31 = RandomVariable(this, LognormalRV(18.0, 4.3))
private val t41 = RandomVariable(this, LognormalRV(16.0, 4.0))
private val t12 = RandomVariable(this, LognormalRV(12.0, 4.0))
private val t22 = RandomVariable(this, LognormalRV(15.0, 4.0))
private val t13 = RandomVariable(this, LognormalRV(18.0, 4.2))
private val t23 = RandomVariable(this, LognormalRV(14.0, 4.4))
private val t33 = RandomVariable(this, LognormalRV(12.0, 4.3))
private val t14 = RandomVariable(this, LognormalRV(24.0, 4.0))
private val t24 = RandomVariable(this, LognormalRV(30.0, 4.0))
private val r1 = RandomVariable(this, TriangularRV(30.0, 60.0, 80.0))
private val r2 = RandomVariable(this, TriangularRV(45.0, 55.0, 70.0))
private val r3 = RandomVariable(this, TriangularRV(30.0, 40.0, 60.0))
private val r4 = RandomVariable(this, TriangularRV(35.0, 65.0, 75.0))
private val diagnosticTime = RandomVariable(this, ExponentialRV(30.0))
private val moveTime = RandomVariable(this, UniformRV(2.0, 4.0))
The code uses a naming convention to keep track of which random variable is using on which sequence. For example, t21
is the random variable required for the second step of the first test plan and r1
is the repair time random variable for the first test plan. For each step of the test plan, we need to know the required resource and the processing time. Thus, we define each of the resources as follows.
// define the resources
private val myDiagnostics: ResourceWithQ = ResourceWithQ(this, "Diagnostics", capacity = 2)
private val myTest1: ResourceWithQ = ResourceWithQ(this, "Test1")
private val myTest2: ResourceWithQ = ResourceWithQ(this, "Test2")
private val myTest3: ResourceWithQ = ResourceWithQ(this, "Test3")
private val myRepair: ResourceWithQ = ResourceWithQ(this, "Repair", capacity = 3)
Then, we define a class to hold the information for each step and the lists to represent each of the test plans.
inner class TestPlanStep(val resource: ResourceWithQ, val processTime: RandomIfc)
// make all the plans
private val testPlan1 = listOf(
TestPlanStep(myTest2, t11), TestPlanStep(myTest3, t21),
TestPlanStep(myTest2, t31), TestPlanStep(myTest1, t41), TestPlanStep(myRepair, r1)
)
private val testPlan2 = listOf(
TestPlanStep(myTest3, t12),
TestPlanStep(myTest1, t22), TestPlanStep(myRepair, r2)
)
private val testPlan3 = listOf(
TestPlanStep(myTest1, t13), TestPlanStep(myTest3, t23),
TestPlanStep(myTest1, t33), TestPlanStep(myRepair, r3)
)
private val testPlan4 = listOf(
TestPlanStep(myTest2, t14),
TestPlanStep(myTest3, t24), TestPlanStep(myRepair, r4)
)
Now that the test plans are defined, we can develop a method for determining which plan is assigned to each part. The situation description provides a distribution associated with the test plans as provided in Table 7.2. We can use a random list (REmpiricalList
) to model this situation.
// set up the sequences and the random selection of the plan
private val sequences = listOf(testPlan1, testPlan2, testPlan3, testPlan4)
private val planCDf = doubleArrayOf(0.25, 0.375, 0.7, 1.0)
private val planList = REmpiricalList<List<TestPlanStep>>(this, sequences, planCDf)
The test plans, which are lists, are added to another list called sequences,
which will be used from which to randomly select the test plan according to the discrete empirical distribution as provided by the CDF across the test plans.
To capture the performance of the system, we can use Response
and TWResponse
instances.
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 myContractLimit: IndicatorResponse = IndicatorResponse({ x -> x <= 480.0 }, timeInSystem, "ProbWithinLimit")
val probWithinLimit: ResponseCIfc
get() = myContractLimit
Notice the use of an IndicatorResponse
to capture the probability of completing the job within the contract limit. Finally, we can specify the process description for this situation.
private inner class Part : Entity() {
val testAndRepairProcess: KSLProcess = process {
wip.increment()
timeStamp = time
//every part goes to diagnostics
use(myDiagnostics, delayDuration = diagnosticTime)
// determine the test plan
val plan: List<TestPlanStep> = planList.element
// get the iterator
val itr = plan.iterator()
// iterate through the plan
while (itr.hasNext()) {
val tp = itr.next()
use(tp.resource, delayDuration = tp.processTime)
if (tp.resource != myRepair) {
delay(moveTime)
}
}
timeInSystem.value = time - timeStamp
wip.decrement()
}
}
Notice how the KSL process constructs and the Kotlin language combine to implement a fairly complex situation within a short and compact process description. The critical item to note is the use of an instance of an iterator within the process. The variable plan
is actually randomly generated using the planList
discrete empirical list. Then, an iterator for this list is retrieved. The iterator is then used to march through the steps of the assigned test plan within a while
loop. Notice that we check if we are not at the repair machine. If we are not, we incur the delay to move to the next machine in the test plan. The process also includes the code to collect statistics on the part as it moves through the process. To setup to execute this code, we specify the run length and the number of replications.
fun main() {
val m = Model()
val tq = TestAndRepairShop(m, name = "TestAndRepair")
m.numberOfReplications = 10
m.lengthOfReplication = 52.0* 5.0*2.0*480.0
m.simulate()
m.print()
val r = m.simulationReporter
r.writeHalfWidthSummaryReportAsMarkDown(KSL.out, df = MarkDown.D3FORMAT)
}
The output is quite lengthy so the code captures it as a Markdown table.
Name | Count | Average | Half-Width |
---|---|---|---|
Diagnostics:InstantaneousUtil | 10 | 0.744 | 0.007 |
Diagnostics:NumBusyUnits | 10 | 1.489 | 0.013 |
Diagnostics:ScheduledUtil | 10 | 0.744 | 0.007 |
Diagnostics:WIP | 10 | 3.332 | 0.123 |
Diagnostics:Q:NumInQ | 10 | 1.844 | 0.11 |
Diagnostics:Q:TimeInQ | 10 | 36.964 | 2.043 |
Test1:InstantaneousUtil | 10 | 0.855 | 0.005 |
Test1:NumBusyUnits | 10 | 0.855 | 0.005 |
Test1:ScheduledUtil | 10 | 0.855 | 0.005 |
Test1:WIP | 10 | 4.1 | 0.283 |
Test1:Q:NumInQ | 10 | 3.245 | 0.279 |
Test1:Q:TimeInQ | 10 | 57.692 | 4.736 |
Test2:InstantaneousUtil | 10 | 0.773 | 0.006 |
Test2:NumBusyUnits | 10 | 0.773 | 0.006 |
Test2:ScheduledUtil | 10 | 0.773 | 0.006 |
Test2:WIP | 10 | 2.319 | 0.132 |
Test2:Q:NumInQ | 10 | 1.546 | 0.126 |
Test2:Q:TimeInQ | 10 | 41.279 | 3.062 |
Test3:InstantaneousUtil | 10 | 0.858 | 0.006 |
Test3:NumBusyUnits | 10 | 0.858 | 0.006 |
Test3:ScheduledUtil | 10 | 0.858 | 0.006 |
Test3:WIP | 10 | 3.417 | 0.273 |
Test3:Q:NumInQ | 10 | 2.559 | 0.269 |
Test3:Q:TimeInQ | 10 | 51.318 | 5.176 |
Repair:InstantaneousUtil | 10 | 0.864 | 0.006 |
Repair:NumBusyUnits | 10 | 2.591 | 0.017 |
Repair:ScheduledUtil | 10 | 0.864 | 0.006 |
Repair:WIP | 10 | 3.824 | 0.135 |
Repair:Q:NumInQ | 10 | 1.233 | 0.12 |
Repair:Q:TimeInQ | 10 | 24.733 | 2.273 |
TestAndRepair:NumInSystem | 10 | 17.572 | 0.803 |
TestAndRepair:TimeInSystem | 10 | 352.487 | 14.262 |
ProbWithinLimit | 10 | 0.817 | 0.028 |
Diagnostics:SeizeCount | 10 | 12443.4 | 75.921 |
Test1:SeizeCount | 10 | 14027 | 76.189 |
Test2:SeizeCount | 10 | 9337.3 | 77.976 |
Test3:SeizeCount | 10 | 12436.4 | 76.419 |
Repair:SeizeCount | 10 | 12430.7 | 75.528 |
It looks like test machine 3 and the repair station have the highest estimated utilization values. An increase in the amount of work to the test and repair shop may have issues at those stations. The reader is asked to explore the performance of this situation in the exercises.
In the next section, we examine an even more complex situation involving systems that have parameters or distributions that depend on time.