5.5 Sequential Sampling for Finite Horizon Simulations
The methods discussed in Section 3.3.2 of Chapter 3 for determining the sample size are based on pre-determining a fixed sample size and then making the replications. If the half-width equation is considered as an iterative function of \(n\):
\[h(n) = t_{\alpha/2, n - 1} \dfrac{s(n)}{\sqrt{n}} \leq E\]
Then, it becomes apparent that additional replications of the simulation can be executed until the desired half-with bound is met. This is called sequential sampling, and in this case the sample size of the experiment is not known in advance. The brute force method for implementing this approach would be to run and rerun the simulation each time increasing the number of replications until the criterion is met.
To implement this within the KSL, we need a way to stop or end a
simulation when a criteria or condition is met. Because of the
hierarchical nature of the model elements within a model and because
there are common actions that occur when running a model the Observer
pattern can be used here.
Figure 5.8 presents part of the
ksl.observers
package which defines a base class called
ModelElementObserver
that can be attached to instances of ModelElement
and then will be notified if various actions take place. There are a
number of actions associated with a ModelElement
that occur during a
simulation that can be listened for by a ModelElementObserver
:
beforeExperiment()
- This occurs prior to the first replication and before any events are executed.beforeReplication()
- This occurs prior to each replication and before any events are executed. The event calendar is cleared after this action.initialize()
- This occurs at the start of every replication (after beforeReplication() and after the event calendar is cleared) but before any events are executed. As we have seen, it is safe to schedule events in this method.warmUp()
- This occurs during a replication if a warm up period has been specified for the model. The statistical accumulators are cleared during this action if applicable.replicationEnded()
- This occurs at the end of every replication prior to the clearing of any statistical accumulators.afterReplication()
- This occurs at the end of every replication after the statistical accumulators have been cleared for the replication.afterExperiment()
- This occurs after all replications have been executed and prior to the end of the simulation.
ModelElementObservers
are notified right after the ModelElement
experiences the above mentioned actions. Thus, users of the
ModelElementObserver
need to understand that the state of the model
element is available after the simulation actions of the model element
have occurred.
The AcrossReplicationHalfWidthChecker
class listens to the
afterReplication()
method of a Response
instance and checks the current
value of the half-width. This is illustrated in the following code listing.
override fun afterReplication(modelElement: ModelElement) {
if (modelElement.model.currentReplicationNumber <= 2){
return
}
val statistic = myResponse.myAcrossReplicationStatistic
val hw = statistic.halfWidth(confidenceLevel)
if (hw <= desiredHalfWidth){
modelElement.model.endSimulation("Half-width = ($desiredHalfWidth) condition met for response ${myResponse.name}")
}
}
Notice that a reference to the
observed Response
is used to get access to the across
replication statistics. If there are more than 2 replications, then the
half-width is checked against a user supplied desired half-width. If the
half-width criterion is met, then the simulation is told to end using
the Model
class’s endSimulation()
method. The endSimulation()
method causes the simulation to not execute any future replications and to halt further execution.
All that is needed is to get a reference to the response
variable that needs to be checked so that the observer can be attached.
This can be done easily if a reference to the response is exposed through the class’s interface. In this case the property, totalProcessingTime,
an instance of ResponseCIfc,
is used. Alternatively if the string name representation of the response is available, it can be used to extract the response from the model via the model’s functionality for getting model elements.
fun response(name: String): Response?
fun timeWeightedResponse(name: String): TWResponse?
fun counter(name: String): Counter?
Example 5.2 (Sequential Sampling) The following code listing illustrates how to set up half-width checking for the pallet processing model.
fun main() {
val model = Model("Pallet Processing Ex 2")
model.numberOfReplications = 10000
model.experimentName = "Two Workers"
// add the model element to the main model
val palletWorkCenter = PalletWorkCenter(model)
val hwc = AcrossReplicationHalfWidthChecker(palletWorkCenter.totalProcessingTime)
hwc.desiredHalfWidth = 5.0
// simulate the model
model.simulate()
// demonstrate that reports can have specified confidence level
val sr = model.simulationReporter
sr.printHalfWidthSummaryReport()
}
Notice that the number of replications is set to an arbitrarily high value (10000) and the desired half-width is specified as 5.0. In the original example, the half-width for the total processing time was 16.3269 based on 10 replications. As we can see from the following output, the half-width criteria has been met with 149 replications.
Half-Width Statistical Summary Report - Confidence Level (95.000)%
Name Count Average Half-Width
-----------------------------------------------------------------------------------------
NumBusyWorkers 149 1.9197 0.0101
PalletQ:NumInQ 149 7.3231 0.4997
PalletQ:TimeInQ 149 44.2002 2.9565
Num Pallets at WC 149 9.2428 0.5047
System Time 149 55.8544 2.9579
Total Processing Time 149 495.6675 4.9841
P{total time > 480 minutes} 149 0.7114 0.0736
Num Processed 149 81.5503 0.6339
-----------------------------------------------------------------------------------------