- Chapter 23 Statistics Gathering in Event-Driven Simulations
Statistics Gathering in Event-Driven Simulations
A framework for specifying event-driven simulations was presented in the previous chapter. This framework did not include methods for gathering statistics about a simulation as it is running. Statistics might consist of the amount of time each simulation object spends in the simulation (and, therefore, how well the model supports carrying out the kinds of tasks specified by the objects); information about the length of the queues such as the maximum, minimum and average lengths and amount of time spent in each queue; and information about the utilization of the simulation's resources.
There are a number of ways to add statistics gathering to class Simulation or class SimulationObject. In this chapter, we provide four examples of statistics gathering: duration statistics, throughput histograms, event tallying, and event monitoring. Each of these examples involves creating a subclass of Simulation or SimulationObject in order to provide a dictionary in which data can be stored or a file into which data can be written, and, in that subclass, modifying the appropriate methods in order to store the appropriate data.
For the first example of statistics gathering, we create a general subclass of Simulation that simply stores the time an object enters and exits the simulation. The times are kept in a special record whose fields are the time at which the object entered the simulation (entranceTime) and the length of time the object spent in the simulation (duration). Records are described as follows.
|instance variable names||entranceTime|
accessing entrance: currentTime entranceTime ← currentTime exit: currentTime duration ← entranceTime - currentTime entrance ↑entranceTime exit ↑entranceTime + duration duration ↑duration printing printOn: aStream entranceTime printOn: aStream. aStream tab. duration printOn: aStream
An example subclass of Simulation which uses SimulationObjectRecords for gathering statistics is StatisticsWithSimulation. It is defined as follows.
|instance variable names||statistics|
initialization initialize super initialize. statistics ← Dictionary new simulation scheduling enter: anObject statistics at: anObject put: (SimulationObjectRecord new entrance: currentTime) exit: anObject (statistics at: anObject) exit: currentTime statistics printStatisticsOn: aStream | stat | aStream cr. aStream nextPutAll: 'Object'. aStream tab. aStream nextPutAll: 'Entrance Time'. aStream tab. aStream nextPutAll: 'Duration'. aStream cr. "Sort with respect to the time the object entered the simulation. Because the keys as well as the values are needed, it is necessary to first obtain the set of associations and then sort them." stat ← SortedCollection sortBlock: [ :i :j | i value entrance < = j value entrance]. statistics associationsDo: [ :each | stat add: each]. stat do: [ :anAssociation | aStream cr. anAssociation key printOn: aStream. aStream tab. anAssociation value printOn: aStream "The value is a SimulationObjectRecord which prints the entrance time and duration"]
Suppose we created NothingAtAll as a subclass of StatisticsWithSimulation. In this example, NothingAtAll schedules two simulation objects: one that does nothing (a DoNothing), arriving according to a uniform distribution from 3 to 5 units of time starting at 0; and one that looks around for 5 units of simulated time (a Visitor), arriving according to a uniform distribution from 5 to 10 units of time starting at 1. Whenever one of these objects enters the simulation, an entry is put in the statistics dictionary. The key is the object itself; the equals (=) test is the default (= =), so each object is a unique key. The value is an instance of SimulationObjectRecord with entrance data initialized. When the object exits, the record associated with it is retrieved and a message is sent to it in order to set the exit data.
no new methods
simulation control tasks self holdFor: 5
initialization defineArrivalSchedule self scheduleArrivalOf: DoNothing accordingTo: (Uniform from: 3 to: 5). self scheduleArrivalOf: Visitor accordingTo: (Uniform from: 5 to: 10) startingAt: 1
Whenever we halt the simulation, we can send the message printStatisticsOn: aFile (where aFile is a kind of FileStream). The result might look like:
A common statistic gathered in simulations is the throughput of objects, that is, how many objects pass through the simulation in some amount of time; this is proportional to how much time each object spends in the simulation. Gathering such statistics involves keeping track of the number of objects that spend time within predetermined intervals of time. Reporting the results involves displaying the number of objects, the minimum time, maximum time, and the number of objects whose times fall within each specified interval. Such statistics are especially useful in simulations involving resources in order to determine whether or not there are sufficient resources to handle requests-if objects have to wait a long time to get a resource, then they must spend more time in the simulation.
In order to support the gathering of throughput statistics, we provide class Histogram. Histograms maintain a tally of values within prespecified ranges. For example, we might tally the number of times various values fall between 1 and 5, 5 and 10, 10 and 20, 20 and 25, 25 and 30, 30 and 35, 35 and 40, and 40 and 45. That is, we divide the interval 5 to 45 into bins of size 5 and keep a running count of the number of times a value is stored into each bin.
Class Histogram is created by specifying the lower bound, the upper bound, and the bin size. To obtain a Histogram for the above example, evaluate
Histogram from: 5 to: 45 by: 5
Besides data on the bins, a Histogram keeps track of minimum, maximum, and total values entered. An entry might not fit within the bounds of the interval; an additional variable is used to store all such entries (extraEntries). The bins are stored as elements of an array; the array size equals the number of bins (that is, upper bound lower bound // bin size). The message store: aValue is used to put values in the Histogram. The index of the array element to be incremented is 1 + (aValue - lower bound // bins size). Most of the methods shown support printing the collected information.
|instance variable names||tallyArray|
class initialization from: lowerNum to: upperNum by: step ↑self new newLower: lowerNum upper: upperNum by: step
accessing contains: aValue ↑lowerBound < = aValue and: [aValue < upperBound] store: aValue | index | minValue isNil ifTrue: [minValue ← maxValue ← aValue] ifFalse: [minValue ← minValue min: aValue. maxValue ← maxValue max: aValue]. totalValues ← totalValues + aValue. (self contains: aValue) ifTrue: [index ← (aValue - lowerBound // step) + 1. tallyArray at: index put: (tallyArray at: index) + 1] ifFalse: [extraEntries ← extraEntries + 1] printing printStatisticsOn: aStream | totalObjs pos | self firstHeader: aStream. aStream cr; tab. totalObjs ← extraEntries. "count the number of entries the throughput records know" tallyArray do: [ :each | totalObjs ← totalObjs + each]. totalObjs printOn: aStream. aStream tab. minValue printOn: aStream. aStream tab. maxValue printOn: aStream. aStream tab. (totalValues / totalObjs) asFloat printOn: aStream. aStream cr. self secondHeader: aStream. aStream cr. pos ← lowerBound. tallyArray do: [ :entry | pos printOn: aStream. aStream nextPut: $-. (pos ← pos + step) printOn: aStream. aStream tab. entry printOn: aStream. aStream tab. (entry / totalObjs) asFloat printOn: aStream. aStream tab. aStream nextPut: $| "print the X's" entry rounded timesRepeat: [aStream nextPut: $X]. aStream cr] firstHeader: aStream aStream cr; tab. aStream nextPutAll: 'Number of'. aStream tab. aStream nextPutAll: 'Minimum'. aStream tab. aStream nextPutAll: 'Maximum'. aStream tab. aStream nextPutAll: 'Average'. aStream cr; tab. aStream nextPutAll: 'Objects'. aStream tab. aStream nextPutAll: 'Value'. aStream tab. aStream nextPutAll: 'Value'. aStream tab. aStream nextPutAll: 'Value' secondHeader: aStream aStream cr; tab. aStream nextPutAll: 'Number of'. aStream cr. aStream nextPutAll: 'Entry'. aStream tab. aStream nextPutAll: 'Objects'. aStream tab. aStream nextPutAll: 'Frequency'. private newLower: lowerNum upper: upperNum by: stepAmount tallyArray ← Array new: (upperNum - lowerNum // stepAmount). tallyArray atAllPut: 0. lowerBound ← lowerNum. upperBound ← upperNum. step ← stepAmount. minValue ← maxValue ← nil. totalValues ← 0. extraEntries ← 0
A simulation of visitors to a museum serves as an example of the use of a Histogram. Museum is like NothingAtAll in that Visitors arrive and look around. The Visitors take a varying amount of time to look around, depending on their interest in the museum artifacts. We assume that this time is normally distributed with a mean of 20 and a standard deviation of 5. Visitors come to the museum throughout the day, one every 5 to 10 units of simulated time.
|instance variable names||statistics|
initialization initialize super initialize. statistics ← Histogram from: 5 to: 45 by: 5 defineArrivalSchedule self scheduleArrivalOf: Visitor accordingTo: (Uniform from: 5 to: 10) scheduling exit: aSimulationObject super exit: aSimulationObject. statistics store: currentTime - aSimulationObject entryTime printStatisticsOn: aStream statistics printStatisticsOn: aStream
In order for class Museum to update the statistics, Visitors must keep track of when they entered the museum and be able to respond to an inquiry as to their time of entry.
|instance variable names||entryTime|
initialization initialize super initialize. entryTime ← ActiveSimulation time accessing entryTime ↑entryTime simulation control tasks self holdFor: (Normal mean: 20 deviation: 5) next
To create and run the simulation, we evaluate
aSimulation ← Museum new startUp. [aSimulation time < 50] whileTrue: [aSimulation proceed]
When the Museum was created, a Histogram for statistics was created. Each time a Visitor left the Museum, data was stored in the Histogram. The data consists of the duration of the visit.
After running the simulation until time 50, we ask the Museum for a report.
aSimulation printStatisticsOn: (Disk file: 'museum.report')
The method associated with printStatisticsOn: sends the same message, printStatisticsOn:, to the Histogram, which prints the following information on the file museum.report.
|Number of Objects||Minimum Value||Maximum Value||Average Value|
|Entry||Number of Objects||Frequency|
As another example of tallying the events in a simulation, we present a commonly-used example of a simulation of Traffic. In this simulation, we tally the number of cars that enter an intersection, distinguishing those that drive straight through the intersection from those that turn left and those that turn right. By observation, we note that twice as many cars go straight as turn right or left, but twice as many turn left as right. A new car arrives at the intersection according to a uniform distribution every 0.5 to 2 units of time (self scheduleArrivalOf: Car accordingTo: (Uniform from: 0.5 to: 2)). We will run the simulation until simulated time exceeds 100.
|instance variable names||statistics|
initialization initialize super initialize. statistics ← Dictionary new: 3. statistics at: #straight put: 0. statistics at: #right put: 0. statistics at: #left put: 0 defineArrivalSchedule self scheduleArrivalOf: Car accordingTo: (Uniform from: 0.5 to: 2). self schedule: [self finishUp] at: 100 statistics update: key statistics at: key put: (statistics at: key) + 1 printStatisticsOn: aStream aStream cr. aStream nextPutAll: 'Car Direction Tally' statistics associationsDo: [ :assoc | aStream cr. assoc key printOn: aStream. aStream tab. assoc value printOn: aStream]
Note that in the method associated with defineArrivalSchedule, the action self finishUp is scheduled to occur at simulated time 100. This event will terminate the simulation as desired.
simulation control tasks "Sample, without replacement, the direction through the intersection that the car will travel." | sample | sample ← SampleSpace data: #(left left right straight straight straight straight straight straight) ActiveSimulation update: sample next
SampleSpace was a class we introduced in Chapter 21. Cars are scheduled to enter the simulation with the sole task of picking a direction to tell the simulation. After running the simulation, we ask the Traffic simulation to report the tallies by sending it the printStatisticsOn: message. A possible outcome of evaluating
aSimulation ← Traffic new startUp. [aSimulation proceed] whileTrue. aSimulation printStatisticsOn: (Disk file: 'traffic.data')
is the following information written on the file traffic.data.
Another possible technique for gathering data from a simulation is to note the occurrence of each (major) event, including the entering and exiting of simulation objects. This is accomplished by creating a subclass of SimulationObject that we call EventMonitor. In this example, a class variable refers to a file onto which notations about their events can be stored. Each message that represents an event to be monitored must be overridden in the subclass to include the instructions for storing information on the file. The method in the superclass is still executed by distributing the message to the pseudo-variable super.
Basically, the notations consist of the time and identification of the receiver (a kind of SimulationObject), and an annotation such as "enters" or "requests" or "releases."
|class variable names||DataFile|
class initialization file: aFile DataFile ← aFile
scheduling startUp self timeStamp. DataFile nextPutAll: 'enters'. super startUp finishUp super finishUp. self timeStamp. DataFile nextPutAll: 'exits' task language holdFor: aTimeDelay self timeStamp. DataFile nextPutAll: 'holds for'. aTimeDelay printOn: DataFile. super holdFor: aTimeDelay acquire: amount ofResource: resourceName | aStaticResource | "Store fact that resource is being requested." self timeStamp. DataFile nextPutAll: 'requests'. amount printOn: DataFile. DataFile nextPutAll: 'of', resourceName. "Now try to get the resource." aStaticResource ← super acquire: amount ofResource: resourceName. "Returns here when resource is obtained; store the fact." self timeStamp. DataFile nextPutAll: 'obtained'. amount printOn: DataFile. DataFile nextPutAll: 'of ', resourceName. ↑aStaticResource acquire: amount ofResource: resourceName withPriority: priorityNumber | aStaticResource | "Store fact that resource is being requested" self timeStamp. DataFile nextPutAll: 'requests'. amount printOn: DataFile. DataFile nextPutAll: 'at priority'. priorityNumber printOn: DataFile. DataFile nextPutAll: 'of', resourceName. "Now try to get the resource." aStaticResource ← super acquire amount ofResource: resourceName withPriority: priorityNumber. "Returns here when resource is obtained; store the fact." self timeStamp. DataFile nextPutAll: "obtained'. amount printOn: DataFile. DataFile nextPutAll: 'of', resourceName. ↑aStaticResource produce: amount ofResource: resourceName self timeStamp. DataFile nextPutAll: 'produces' amount printOn: DataFile. DataFile nextPutAll: 'of', resourceName. super produce amount ofResource resourceName release: aStaticResource self timeStamp. DataFile nextPutAll: 'releases' aStaticResource amount printOn: DataFile. DataFile nextPutAll: 'of', aStaticResource name. super release: aStaticResource acquireResource: resourceName | anEvent | "Store fact that resource is being requested" self timeStamp. DataFile nextPutAll: 'wants to serve for'. DataFile nextPutAll: resourceName. "Now try to get the resource." anEvent ← super acquireResource resourceName. "Returns here when resource is obtained store the fact." self timeStamp. DataFile nextPutAll: 'can serve" anEvent condition printOn: DataFile. ↑anEvent produceResource: resourceName self timeStamp. DataFile nextPutAll: 'wants to get service as' DataFile nextPutAll: resourceName. super produce amount ofResource resourceName resume: anEvent self timeStamp. DataFile nextPutAll: 'resumes'. anEvent condition printOn: DataFile. super resume: anEvent private timeStamp DataFile cr. ActiveSimulation time printOn: DataFile. DataFile tab. self printOn: DataFile.
We can monitor the events of the NothingAtAll simulation consisting of arrivals of Visitors and default simulations (DoNothings). Except for creating Visitor and DoNothing as subclassses of EventMonitor rather than of SimulationObject, the class definitions are the same.
no new methods
simulation control tasks self holdFor: (Uniform from: 4.0 to: 10.0) next
NothingAtAll is redefined so that the default simulation is an EventMonitor.
initialization defineArrivalSchedule self scheduleArrivalOf: DoNothing accordingTo: (Uniform from: 1 to: 5). self scheduleArrivalOf: Visitor accordingTo: (Uniform from: 4 to: 8) startingAt: 3
Visitor file: (Disk file: 'NothingAtAll.events'). "This informs DoNothing too" aSimulation ← NothingAtAll new startUp. [aSimulation time < 25] whileTrue: [aSimulation proceed]
the file 'NothingAtAll.events' contains the following information
|0.0||a DoNothing enters|
|0.0||a DoNothing exits|
|3.0||a Visitor enters|
|3.0||a Visitor holds for 7.5885|
|4.32703||a DoNothing enters|
|4.32703||a DoNothing exits|
|7.74896||a Visitor enters|
|7.74896||a Visitor holds for 4.14163|
|8.20233||a DoNothing enters|
|8.20233||a DoNothing exits|
|10.5885||a Visitor exits|
|11.8906||a Visitor exits|
|12.5153||a DoNothing enters|
|12.5153||a DoNothing exits|
|14.2642||a Visitor enters|
|14.2642||a Visitor holds for 4.51334|
|16.6951||a DoNothing enters|
|16.6951||a DoNothing exits|
|18.7776||a Visitor exits|
|19.8544||a Visitor enters|
|19.8544||a Visitor holds for 5.10907|
|20.5342||a DoNothing enters|
|20.5342||a DoNothing exits|
|23.464||a DoNothing enters|
|23.464||a DoNothing exits|
|24.9635||a Visitor exits|
Distinctively labeling each arriving SimulationObject might improve the ability to follow the sequence of events. The goal is to have a trace for an execution of the NothingAtAll simulation look like the following.
|0.0||DoNothing 1 enters|
|0.0||DoNothing 1 exits|
|3.0||Visitor 1 enters|
|3.0||Visitor 1 holds for 7.5885|
|4.32703||DoNothing 2 enters|
|4.32703||DoNothing 2 exits|
|7.74896||Visitor 2 enters|
|7.74896||Visitor 2 holds for 4.14163|
|8.20233||DoNothing 3 enters|
|8.20233||DoNothing 3 exits|
|10.5885||Visitor 1 exits|
|11.8906||Visitor 2 exits|
|12.5153||DoNothing 4 enters|
|12.5153||DoNothing 4 exits|
|14.2642||Visitor 3 enters|
|14.2642||Visitor 3 holds for 4.51334|
|16.6951||DoNothing 5 enters|
|16.6951||DoNothing 5 exits|
|18.7776||Visitor 3 exits|
|19.8544||Visitor 4 enters|
|19.8544||Visitor 4 holds for 5.10907|
|20.5342||DoNothing 6 enters|
|20.5342||DoNothing 6 exits|
|23.464||DoNothing 7 enters|
|23.464||DoNothing 7 exits|
|24.9635||Visitor 4 exits|
Each subclass of EventMonitor must create its own sequence of labels. EventMonitor sets up a label framework in which the subclass can implement a way of distinguishing its instances. EventMonitor itself provides a model that the subclasses can duplicate; in this way, a default simulation using instances of EventMonitor can be used to produce the trace shown above. In addition to the scheduling, task language and private, messages shown in the earlier implementation of EventMonitor, the class description has the following messages.
|instance variable names||label|
|class variable names||DataFile|
class initialization file: aFile DataFile ← aFile. Counter ← 0
initialization initialize super initialize. self setLabel accessing setLabel Counter ← Counter + 1. label ← Counter printString label ↑label printing printOn: aStream self class name printOn: aStream. aStream space. aStream nextPutAll: self label
Visitor, as a subclass of EventMonitor, has to have an independent class variable to act as the counter of its instances. The class description of Visitor is now
|class variable names||MyCounter|
class initialization file: aFile super file: aFile. MyCounter ← 0
accessing setLabel MyCounter ← MyCounter + 1. label ← MyCounter printString simulation control tasks self holdFor: (Uniform from: 4.0 to: 10.0) next
MyCounter is set to 0 when the instance is tom initialize; the method is found in class EventMonitor. Printing retrieves the label that Visitor redefines with respect to MyCounter. We let DoNothing use the class variable, Counter, of its superclass. Then the desired trace will be produced using these definitions.