slide: The event state-transition diagram
Entities
An entity (that combines several related events) has the same states
and primitives as an event. The difference is in the use of an additional phase,
as is explained in section 3.4.
The event-based approach
With the event-based approach of writing a simulation program we
first identify the events in the model.
The behavior of an event is implemented by deriving it from the
class event and overriding the application operator of this class.
Consider the next problem
The Dining Philosophers
The problem is as follows.
Five philosophers sit around a table with five chopsticks in between. They think and if they're hungry
and if two chopsticks are available, they eat. If a philosopher gets
hungry and s/he can't acquire a chopstick, the philosopher waits until
s/he can. The philosopher doesn't
think, if s/he is waiting or eating. We're interested in the fraction
of the time, that a philosopher actually thinks.
To model this problem, three events can be identified, eat, think and await.
The corresponding classes are derived from the class event. Furthermore we
need a chopstick for every philosopher. These are represented as
a resource. The thinking times are gathered in an instance
of the class histogram and the generator takes care of the
variations in the time needed to think and eat.
We develop this program in the following steps.
First, the library is included as sim.h. The declarations of the global variables
and constants follow after that. The time unit in this simulation is an
hour, so a philosopher has a mean eating time of two hours and a mean
thinking time of five hours. The duration of the simulation is a year.
Finally, we define the various events.
#include
enum {NUMBER=5,EAT_TIME=2,THINK_TIME=5};
simulation* sim;
resource* chopstick[NUMBER];
histogram* thinking;
generator* g;
const double duration = 52*7*24.0; // a year
class eat : public event
{
private :
int id; // identity of the philosopher
public :
eat(int i); // constructor, taking identity
virtual int operator()(); // application operator
};
class think : public event
{
private :
int id; // identity of the philosopher
public :
think(int i); // constructor, taking identity
virtual int operator()(); // application operator
};
class await : public event
{
private :
int id; // identity of the philosopher
public :
await(int i); // constructor, taking identity
virtual int operator()(); // application operator
};
fig 3.5. The definitions of the philosopher program
Next, we implement the various events.
An event is given its functionality by deriving it from
the class event and overriding its application operator.
The logic of the eat event is that the philosopher eats for a
random time, exponentially distributed with the eating time as mean.
So, we first determine the actual eating time and schedule a think event
to be activated after this eating time. The eat event can be
terminated.
eat::eat(int i) : event()
{ id = i; } // set identity
int eat::operator()()
{
double t;
think* th;
t = g -> exponential(EAT_TIME); // determine eating time
th = new think(id); // create a thinking event
sim -> schedule(th,t); // schedule thinking
sim -> terminate(this); // terminate this eat event
return OK;
}
fig 3.6. The eat event
If a philosopher starts to think, the philosopher first releases
both chopsticks. The thinking time is determined and
a sample is made of this thinking time. An await event is
scheduled and the think event is terminated.
think::think(int i) : event()
{ id = i; } // set identity
int think::operator()()
{
double t;
await* aw;
chopstick[id] -> release(); // release left chopstick
chopstick[(id+1)%NUMBER] -> release(); // release right
t = g -> exponential(THINK_TIME); // determine thinking time
thinking -> sample(id,t/duration*100); // add a sample (%)
aw = new await(id); // create await event
sim -> schedule(aw,t); // schedule waiting
sim -> terminate(this); // terminate thinking
return OK;
}
fig 3.7. The think event
The await event acquires the left and right chopstick and
schedules an eat event immediately, if both chopsticks are
available. The await event is passivated as it could be on the
conditional list. If no chopsticks are available, the await
event stays on the conditional list or, if it was not conditional
as is the case the first time it is activated, it is added
to the conditional list.
await::await(int i) : event()
{ id = i; } // set identity
int await::operator()()
{
eat* e;
if ( (chopstick[id] -> available()) && // available ?
chopstick[(id+1)%NUMBER] -> available()) )
{
chopstick[id] -> acquire(); // acquire left
chopstick[(id+1)%NUMBER] -> acquire(); // acquire right
e = new eat(id);
sim -> passivate(this); // extract from conditional list
sim -> schedule(e,0); // schedule eat event immediately
sim -> terminate(this); // terminate await event
}
else if (!conditional()) // not on conditional list
{
sim -> passivate(this); // make passive
sim -> hold(this); // add to conditional list
}
return OK;
}
fig 3.8. The await event
If the library runs in ASCII mode all that needs to be done is to define
the main function.
This function takes care of setting up and running the simulation.
main()
{
int i;
await* aw;
sim = new simulation(); // create simulation object
g = new generator(1); // create generator
thinking = new histogram(1,1,NUMBER,FREQUENCY,
GRAPH,"THINKING TIME (%)"); // frequency graph
for (i=0;i schedule(aw,0); // philosopher to be waiting
}
sim -> run(duration); // run for duration time units
cout << (*thinking) << endl; // print resulting histogram
delete thinking;
delete sim;
}
fig 3.9. The main function
The main function first creates the simulation object. Furthermore, a generator,
a frequency histogram and five resources that represent the chopsticks are created.
The simulation starts with all philosophers waiting and runs for a
year (52*7*24 hours). After running the simulation, the resulting histogram is printed to standard output.
If the library sim runs in hush mode the application::main routine takes care of setting up and running the simulation.
It overrides the abstract session::main function from the hush
library that is called before starting the hush-event loop.
Consider the following definition and implementation of the application class,
that is derived from session.
class application : public session
{
public :
application(int argc,char** argv);
virtual void main(kit* tk,int argc,char** argv);
};
application::application(int argc,char** argv) : session(argc,argv,"philosophers")
{}
void application::main(kit* tk,int argc,char** argv)
{
int i;
await* aw;
sim = new simulation(tk); // create simulation object with the toolkit
g = new generator(1); // create generator
thinking = new histogram(1,1,NUMBER,FREQUENCY,
GRAPH,"THINKING TIME (%)"); // frequency graph
for (i=0;i schedule(aw,0); // philosopher waiting
}
sim -> run(duration); // run for duration time units
cout << (*thinking) << endl; // print resulting histogram
delete thinking;
delete sim;
}
fig 3.10. The application
The only difference between the main of figure 3.9. and the
application::main of figure 3.10. is the parameter of the constructor.
If this method receives the pointer to the hush toolkit the
library runs in hush mode. If these method does not receive anything
(or the default NULL pointer) the library runs in ASCII mode, as is the case in the first implementation of
of the main function.
Additionally, for the running in hush mode,
the creation and invocation of the session object in the main
function is required.
main(int argc,char** argv)
{
session* s;
s = new application(argc,argv); // create the session
s -> run(); // start the session
exit(0);
}
fig 3.11. The main function
Session is used to start up the hush toolkit. Its
use is illustrated in the philosopher program. You should derive an
application from the session, give it a name and override its session::main function.
In main all you do is create and start the session.
For a more detailed description of the session class, which is necessary
if you are not satisfied with the graphical features of the sim library, see [Eliëns 94].
The session class is also defined in the ASCII version of the library.
Its functionality is zero, but hush programs can be linked to the ASCII library
in this way.
The event class
The interface of the event class :
interface event
{
protected :
event(int qp=0,int sp=0,int kf = FALSE); // queuing and scheduling
// priority and kill flag as arguments
public :
virtual ~event(); // destructor.
virtual int operator()() = 0; // empty
virtual int verify(); // returns OK
int report(histogram* h,double interval = 0); // create report
void stamp(); // add time stamp
double timespent(); // time since the stamp
int queuingpriority(); // return queuing priority
int schedulingpriority(); // return scheduling priority
void queuingpriority(int p); // adjust queuing priority
void schedulingpriority(int p); // adjust scheduling priority
int active(); // is active ?
int pending(); // is pending ?
int conditional(); // is conditional ?
int closed(); // is closed ?
int passive(); // is passive ?
int queued(); // is queued ?
};
fig 3.12. The interface of the class event
The constructor gets the priorities (default 0) and the kill flag of the event.
The kill flag should be set when the simulation is ran for count
terminated events. The application operator is invoked
when the event is activated. This function is purely virtual. It must be
implemented
in a subtype, that is derived from event. The method verify is also
virtual, it returns OK and should be overwritten (and return FALSE) if this event
should be terminated when the simulation::cancel or the
queue::cancel methods are invoked.
The method report fills
a FREQUENCY histogram with samples of the lifetime of every instance of the event.
The histogram is printed to standard output and on a window (if the
library runs in hush mode) each interval time units and at the end
of the simulation. If interval is equal to 0, as in the default case, the
histogram is only printed, at the end of the simulation.
Successive prints are on the same window.
When it is used, the method should be invoked for every instance of the reported type, when
first activated and the instance of the reported type should be explicitly terminated, when
not used anymore. The function stamp adds a time stamp of the current
simulation time to the event.
The function timespent returns
the time since the event received a time stamp with
the stamp method. These two methods can be used to create samples of,
for example, service times of an event.
The priority functions return or adjust the scheduling or
queuing priority of the event. The scheduling priority is used to
order the activation of events with a same activation time.
The event with the highest priority is activated first. The queuing
priority is used to model priority queues. The event with the
highest queuing priority goes to the front of the queue.
The other functions return a boolean, according to the state of
the event.
As a remark, the notion of a simulation event and the notion of a hush-event
(for example the clicking on a button) are integrated, if the library
runs in hush mode. You can use the event class both as a
sim event and as a hush event. The hush event is
described in [Eliëns 94].
The process-oriented approach
With the process-oriented approach the components of the model consist
of entities, which represent the existence of some object in the
system such as a philosopher. An entity receives a phase
that determines the behavior of the entity.
We first identify the processes (or the types) in the model. The
events are represented as methods of a process. The application operator
calls these events at the hand of the phase the process is in.
The process philosopher
Consider the following definition and implementation of the process philosopher
enum {EATING,THINKING,WAITING} // phases of a philosopher
class philosopher : public entity
{
private :
int id;
public :
philosopher(int ph,int i); // constructor, taking phase and id
virtual int operator()(); // application operator
int eat(); // eat event
int think(); // think event
int await(); // await event
};
philosopher::philosopher(int ph,int i) : entity(ph)
{
id = i; // set phase and identity
}
int philosopher::operator()()
{
switch (phase()) // what phase is the philosopher in ?
{
case EATING :
return eat(); // the philosopher eats
case THINKING :
return think(); // the philosopher thinks
case WAITING :
return await(); // the philosopher waits
}
return FALSE;
}
int philosopher::eat()
{
double t;
t = g -> exponential(EAT_TIME); // determine thinking time
phase(THINKING); // set phase to thinking
sim -> wait(t); // schedule this philosopher thinking
return OK;
}
int philosopher::think()
{
double t;
chopstick[id] -> release(); // release left chopstick
chopstick[(id+1)%NUMBER] -> release(); // release right
t = g -> exponential(THINK_TIME); // determine thinking time
thinking -> sample(id,t/duration*100); // sample (%)
phase(WAITING); // set phase on waiting
sim -> wait(t); // schedule this philosopher waiting
return OK;
}
int philosopher::await()
{
if ( (chopstick[id] -> available()) && // available ?
chopstick[(id+1)%NUMBER] -> available()) )
{
chopstick[id] -> acquire(); // acquire left chopstick
chopstick[(id+1)%NUMBER] -> acquire(); // acquire right
sim -> passivate(this); // make passive
phase(EATING); // set phase on eating
sim -> activate(this); // activate as eating
}
else if (!conditional())
{
sim -> passivate(this); // make passive
sim -> hold(this); // add to conditional
}
return OK;
}
fig 3.13. The implementation of the philosopher process
Dependent on the phase the philosopher is in, the appropriate action on the
simulation environment is taken. These actions closely resemble the events,
described in the event-based approach of this problem.
The main difference is in the use of phase. If, for example, a
philosopher finishes eating, his/her phase is set to THINKING and s/he is
scheduled after t time units, whereas in the event-based approach a
think event is scheduled and the eat event is explicitly terminated.
So, in the process-oriented solution a philosopher exists for the entire
simulation.
In the main or session::main function the simulation is set up by
scheduling the five philosophers, initially waiting, instead of
scheduling five await events.
The entity class
The interface of the entity class. It is derived from
event and adds the phase of the process to event.
interface entity : public event
{
protected :
entity(int ph=0,int qp=0,int sp=0,int kf = FALSE); // constructor
public :
virtual ~entity(); // destructor
int phase(); // return phase
void phase(int p); // adjust phase
};
fig 3.14. The interface of the entity class
The constructor takes the phase, queuing priority and scheduling
priority and the kill flag of the entity (default 0),
The function phase adjust or returns this phase. Furthermore, the class entity
inherits every function of the class event, so these functions
can also be invoked for an entity.
The simulation class
The primitives and methods for running and ending the simulation are
provided by the simulation class.
If the simulation object receives a pointer to the Tcl/Tk toolkit
the sim library runs in hush mode, otherwise it runs in ASCII
mode.
The class simulation has the following interface :
interface simulation
{
simulation(kit* tk = NULL,double speed = 0.0);
virtual ~simulation(); // destructor
void run(); // run until no events left
void run(double t); // run for t time units
void run(int count); // run for count terminated events
void quit(); // quit simulation
void terminate(event* e); // terminate e
void scan(); // scan conditional always ?
void suppress(); // suppress reports
int schedule(event* e,double t); // schedule e at t
int wait(double t); // schedule current
int passivate(event* e); // passivate e
int activate(event* e); // activate e
int hold(event* e); // add e to conditional
int withdraw(event* e); // put e on closed
int reinstate(event* e); // put e on conditional
int cancel(); // cancel events
void reset(); // reset the simulation
double clock(); // return clock
void postscript(char* fn); // generate postscript
friend ostream& operator<<(ostream& os,simulation& s);
};
fig 3.15. The interface of the class simulation
If the constructor receives the pointer to the hush toolkit the
library runs in hush mode. It should be invoked before the
other objects participating in the program are created.
The method run is used to run the simulation for t time units,
for count terminated events or until no scheduled events are left.
In the Dining Philosopher Problem, we used the method taking a double,
as the simulation should run for 52*7*24 time units.
A simulation can also be run for count events, the simulation then stops,
if count events are terminated, that had set their kill-flag set to OK when created.
If run is done, it prints statistics of how the simulation stopped,
of the number of events that were created, activated and terminated and
of the actual time (in seconds) that the simulation ran.
These results are printed to standard output and, if hush is
enabled on a window (the same window for each run).
If the constructor takes the speed argument the library runs in real
time mode. A run then ends if the simulation time divided by speed
is greater then the actual time (in seconds) that elapsed.
The simulation can be stopped by calling quit.
With the function terminate, an event is terminated
and removed from the simulation. It calls the virtual destructor of the
class event, that can be overwritten by the derived event.
If the kill-flag of the terminated event is set to OK, the simulation stops
if this is the event that is terminated in this way.
The count should have been set in the run method.
If the function scan is used, the list with the
conditional events is traversed, even between events with the same activation time.
Invoking suppress has as effect that generated reports are not
printed after a run.
If schedule is used, the event e is
put on the pending list and is executed after t time units. In the case
of the dining philosophers for example, if an eat event is the current, a
think event is scheduled after the philosopher finishes eating. If wait
is used, the event that currently executes is put on the pending list and is executed after
t time units. The method passivate extracts an event from the pending or conditional
list and makes it passive. For example, an await event is passivated, if
both chopsticks become available. The function activate removes an event from the pending
and conditional list and schedules it to become active at the current
time. The method hold adds an event to the conditional list. For example, an await event is
made conditional, if no chopsticks are available. The method withdraw
prevents a conditional event from being scanned by changing its
state to closed. The method reinstate allows a conditional event to be
scanned again. The method cancel removes and deletes every event
from the scheduler and conditional list that receives FALSE from its
event::verify method. These methods can be used to model the
breakdown of a server for example, departure events should be canceled
then. The simulation can be reset as
nothing had happened to it by using the method reset.
Reset clears the scheduler and conditional list, resets the clock
to 0 and resets the reports and screen (if set) to their original state.
If you want to run the simulation without reports this time, you can
delete the simulation object and create a new one.
As with the terminate method, the cancel and reset
functions stop the simulation if count is specified and the
event has been terminated.
The function clock returns the current simulation time.
The function postscript writes the window of the simulation
object (if created) in postscript format to the specified file.
The operator function
writes the pending tree and conditional list to standard output.
This function can be used for the debugging of a simulation program.
The generator class
The class generator represents the random number generator.
The generator is used to get the randomized activation times of the
various events. It gets a seed and creates
a random number stream, according to the method described in appendix B.
Each time a member of the generator is invoked to get a random number, the next
number from the stream is calculated and is transformed, according to the
probability distribution belonging to that member. This number is returned and can be used in the
simulation. If, for example, a normal distribution with 10 as mean and 1 as
standard deviation is specified to transform the random number stream,
the generated numbers have 10 as mean and show little variation
because of the low standard deviation.
Refer to appendix A for a description of the various probability
distributions that the library uses.
The class generator has the following interface :
interface generator
{
public :
generator(unsigned int seed); // constructor
void antithetic(); // use 1-random()
void reset(); // reset random number stream
int probability(double p = 0.5); // OK with prob. p
double uniform(double a,double b);
double exponential(double a);
double hyperexponential(double p,double a,double b);
double laplace(double a);
double normal(double a,double b);
double chisquare(unsigned int n);
double student(unsigned int n);
double lognormal(double a,double b);
double erlang(unsigned int n,double a);
double gamma(double a,double b);
double beta(double a,double b);
double fdistribution(unsigned int n,unsigned int m);
double poisson(double a);
double geometric(double p);
double hypergeometric(unsigned int m,unsigned int n,double p);
double weibull(double a,double b);
double binomial(double p,unsigned int n);
double negativebinomial(double p,unsigned int n);
double triangular(double a);
protected :
double random(); // produces a random number between 0 and 1
};
fig 3.16. The interface of the generator class.
A generator, with given seed, produces the same random number stream every time, it
is created with that seed. One should fluctuate seeds to get reliable
random numbers.
If the method antithetic is invoked, the inverse of the current
stream is used, i.e. instead of the generated random number, say x, 1-x is used. This method can be used when the variance of the
samples made is too high (see section 5.2).
The function reset resets the random number stream, as if
no random number was generated from this stream.
The member probability returns OK with probability p
and FALSE with probability 1-p. The distribution functions all return a random
number from the stream, following the belonging probability distribution.
The method random produces a new variate (between 0 and 1) from
the random number stream. It is made protected so a derived generator
can use this stream and add new distributions to the existing ones.
[-<--%--^-->-]
The notion of a queue as a waiting line of a service facility is
familiar to most of us. Queues occur wherever an imbalance occurs between requests for
a limited resource and the ability of a service facility to provide
that resource. The size of the queue depends on the amount of the
resource available and the demand for it by customers. You can think
of queuing applications, that make a trade-off between the cost of
providing more of a resource and the cost of allowing larger queues
to form.
In the following sections we look at some typical queue types and
queue behavior. Furthermore we describe the interfaces of the
classes resource and queue at the hand of a standard
queuing example.
Queues and resources
To model the above we first need a resource and a queue. As customers
arrive and depart, after being served we need at least three events
(or an entity for both the customer and server). These types are in
fact general for every queuing application. The difference then comes
from the arrival and service pattern, from the service policy, from
the resource amount and from the queuing priority of every event.
These can be modeled as follows :
- arrival and service pattern - In the arrival event a
next arrival is scheduled after the appropriate time. The service event
schedules a departure event after the service time.
- service policy - When an arrival occurs it is served according
to the service policy. The most common ones are the LIFO (last in first
out) and FIFO (first in first out) queues. The first one can be modeled
by adding an arrival to the front of the queue with the queue::prepend method,
the last one is modeled by adding the arrival event to the back with
the queue::append method. In both cases we then take the front of
the queue with the queue::removefront method. Additional service
policies can easily be modeled as variations of the ones above.
- resource amount - The number of arrivals that can be served
depends on the resource amount. When a resource is created it receives
its initial amount. This amount is then increased and decreased every time
a certain amount is released or acquired, with the resource::release
and resource::acquire methods. A resource can be released in the
departure event (after being served) and can be acquired in the
service event if the asked amount is available, which can be checked
with the resource::available method.
- queuing priority - A priority queue can be modeled by giving
a queuing priority to an event, when created. When appended or prepended
an event that has a higher priority goes in front of an event with a
lower priority.
A M/M/1 Queue
A M/M/1 queue has an exponential interarrival and
service time and a single server.
We are interested in the statistics of the size of the queue and the waiting times of a
customer, with varying means for the interarrival times and service times.
Consider the following process-oriented simulation program.
We first include the library and define the phases for a customer.
The simulation object, the resource, the generators, the queue and the
means for the arrival and service times, as well as
the customer and server entities are declared.
#include
enum {ARRIVAL,DEPARTURE};
simulation* sim;
resource* r;
queue* q;
generator* g1, *g2; // for inter-arrival and service times
double meanarrival,meanservice; // mean arrival and service time
class customer : public entity
{
public :
customer(int ph);
virtual int operator()();
int arrival();
int departure();
};
class server : public entity
{
public :
server();
virtual int operator()();
};
fig 4.1. The definitions of the M/M/1 queue
A customer arrives or departs, so it receives a phase, when created.
The behavior of the process customer is implemented in its application
operator. When a customer arrives, first a new customer is scheduled
to arrive. We take the inter-arrival time to be exponential, with meanarrival as
the mean arrival time. Then the customer is appended to the queue. When
the customer is served the resource is acquired by the server, a departing
customer then releases the resource and is terminated. The default amount
of 1 is released and acquired each time.
customer::customer(int ph) : entity(ph)
{
}
int customer::operator()()
{
switch (phase())
{
case ARRIVAL :
return arrival();
case DEPARTURE :
return departure();
return FALSE;
}
int customer::arrival()
{
customer* c;
c = new customer(ARRIVAL); // schedule new customer
sim -> schedule(c,(g1 -> exponential(meanarrival)));
q -> append(this); // append to the queue
return OK;
}
int customer::departure()
{
r -> release(); // release the resource
sim -> terminate(this); // terminate this customer
return OK;
}
fig 4.2. The customer entity
The behavior of the process server does not depend on a phase.
Every time it is activated, it takes a
customer from the queue if possible, i.e. if the queue is not empty
and if the required amount of the resource (in this case the default
amount of 1) is available.
If it takes a customer, it acquires the resource, adjusts the phase
of the customer to DEPARTURE and schedules the customer to depart,
after it has been served.
server::server() : entity()
{
}
int server::operator()()
{
customer* c;
// can we serve a customer ?
if ( (!(q -> empty())) && (r -> available()) )
{
c = (customer *)q -> removefront(); // take first
r -> acquire(); // acquire the resource
c -> phase(DEPARTURE); // adjust phase and schedule departing
sim -> schedule(c,g2 -> exponential(meanservice));
}
return OK;
}
fig 4.3. The server entity
The function main creates the simulation object and two different
generators, that are used for calculating the inter arrival time of
the customers and the service time. Furthermore, it creates the queue and two histograms,
that are used for generating reports on the size of the queue and the waiting times of
a customer. A resource with an initial amount of 1 is created.
The simulation is set up by scheduling a customer to arrive immediately
and making the server conditional. The server is never passivated,
so it stays on the conditional list for the entire simulation
and is activated, whenever a customer is.
main(int argc,char** argv)
{
customer* c;
server* s;
histogram* h1,*h2;
// create objects and reports
if (argc == 3)
{
meanarrival = atof(argv[1]);
meanservice = atof(argv[2]);
}
else
exit(0);
sim = new simulation();
g1 = new generator(8120);
g2 = new generator(434);
h1 = new histogram(0,1,10,WEIGHTED,GRAPH,"QUEUE SIZE");
h2 = new histogram(0,1,10,FREQUENCY,GRAPH,"WAITING TIMES");
q = new queue();
q -> reportsize(h1); // generate a report on the size of the queue
q -> reportwaiting(h2); // generate a report on waiting times
r = new resource(1);
// set up and run the simulation
c = new customer(ARRIVAL);
sim -> schedule(c,0.0); // initially a customer arrives
s = new server();
sim -> hold(s); // the server is conditional
sim -> run(10000.0);
delete h1;
delete h2;
delete q;
delete sim;
}
fig 4.4. The main function
Queue behavior
In this section we look at some examples of how more complex scenarios
involving queues can be modeled.
Queue swapping
This behavior occurs when a member of the queue leaves in preference for an
alternative queue. We can model the swapping of an event e between
the queues q1 and q2 as follows :
if (condition)
{
q1 -> extract(e); // from every position
q2 -> append(e); // append to second queue
}
fig 4.5. Swapping
where condition could be something as the checking of the sizes of the
two queues.
Balking
If a queue is too long it usually deters further additions. This
phenomenon is called balking and its implementation is straightforward :
if (q -> size() < MAX)
q -> append(e); // append if possible
fig 4.6. Balking
The event is appended when there is room for it.
Reneging
Reneging is the premature departure of a customer from a queue before
it has had any service. This is typically the case when its waiting
time exceeded some upper limit. We could model this by checking the
entire queue with the queue::cancel method, but more efficient is the following addition to the arrival event :
e -> stamp(); // add a time stamp
q -> append(e); // append
and the checking of the waiting time in the service event.
if (q -> front() -> timespent() > MAX) // if waited too long
q -> terminatefront(); // destroy the front
fig 4.7. Reneging
The queue::front method only returns the front of the queue and does not
remove it. If the waiting time of the front exceeded the limit, the event is removed and terminated.
The queue::terminatefront then makes no sample of the waiting time,
even if the event had a report on waiting times.
Service pre-emption
Pre-emption occurs when a customer being served is interrupted by the
server in favour of a new customer that has a higher priority.
The interrupted customer can then wait to complete its service,
wait to be served again or simply leave the system.
The interrupting arrival then takes the place of the interrupted event.
Consider the following behavior of the server entity of the
M/M/1 queue. Events should then receive a queuing priority when created
and the event currently being served should be set to NULL when terminated.
int server::operator()()
{
if (!(q -> empty())) // if an event waiting
if (!current) // if nothing being served
return serve() // just serve
else if (q -> front -> queuingpriority() > current -> queuingpriority())
return preempt(); // front has a higher priority
}
int server::serve()
{
c = (customer* )q -> removefront();
r -> acquire();
c -> phase(DEPARTURE);
current = c; // record as current
sim -> schedule(c,g2 -> exponential(meanservice));
return OK;
}
int server::preempt()
{
sim -> terminate(current); // terminate the current
c = (customer* )q -> removefront();
r -> acquire();
c -> phase(DEPARTURE);
current = c; // record as current
sim -> schedule(c,g2 -> exponential(meanservice));
return OK;
}
fig 4.8. Pre-emption
If the front can be served we look if there is currently a customer
being served (the current variable is set to NULL in the departure
event). If so the front is served only if its
queuing priority is higher. The front is then recorded as the current.
A more complex scenario where we record the service time left and append
the interrupted entity once again is given in the case study of
appendix D.
Time dependencies
The arrival and service rates have been considered constant so far.
This is not always realistic. The arrival rate at a supermarket shows
a noticeable peak around evening for example. We can easily model
this by constructing an arrivaltime function as follows :
double arrivaltime()
{
if (sim -> clock() < TIME1) // use the appropriate mean
return (g -> exponential(MEAN1)); // at the hand of the current
else if (sim -> clock() < TIME2) // simulation time
return (g -> exponential(MEAN2));
else if (sim -> clock() < TIME3)
return (g -> exponential(MEAN3));
.
.
}
fig 4.9. Time dependencies
and we have different arrival rates at different times, if this function
is used when a new arrival is scheduled.
The resource class
A resource represents the static objects in the simulation, that events or entities
can use. The class resource has the following interface :
interface resource
{
resource(double a); // constructor
int report(histogram* h,double interval = 0); // generate report
void reset(); // reset amount left to initial
void release(double a = 1); // release amount left by a
void acquire(double a = 1); // acquire a from amount left
int available(double a = 1); // is an amount a available ?
int full(); // is full ?
int empty(); // is empty ?
double left(); // how much left ?
double used(); // how much used ?
double occupied(); // returns occupation (%).
friend ostream& operator<<(ostream& os,resource& r);
};
fig 4.10. The interface of the class resource
A resource maintains the initial and current amount. These amounts
are set to a, when created. When report is used, the given
histogram is filled with samples of the occupation of the resource.
Samples are taken each time release or acquire is invoked.
The histogram should be of type WEIGHTED and is printed to standard output
and on its window (if the library runs in hush mode)
each interval time units or if interval is equal to 0 (default), it is
printed only when the simulation stops.
Successive prints are on the same window.
The function reset can be used to reset the current amount to the initial.
With the method release an amount a of the resource is set free.
For example, a departing customer releases the resource for
the default amount 1.
With acquire an amount a is claimed. Before acquiring this amount,
available should be used to check if this is legal.
The server does accordingly, if it is trying to serve a customer.
The method full returns OK if the resource is full (i.e. as if
nothing has been acquired yet) and FALSE otherwise. The function
empty returns OK if the amount left is equal to zero. The
method left returns the amount left that can
be acquired. The method used returns the amount that already has been acquired.
The function occupied returns the occupation of the resource in percents.
The resource can be printed to standard output with the overloaded operator function.
The initial and current amount and the occupation are printed then.
This function can be used for the debugging of a simulation program.
The queue class
The class queue has the following interface :
interface queue
{
queue(); // constructor
virtual ~queue(); // destructor
void reset(); // reset queue
int reportsize(histogram* h,double interval = 0);
int reportwaiting(histogram* h,double interval = 0,int qp = 0);
int reportwaitingall(histogram* h,double interval = 0);
int prepend(event* e); // add to front
int append(event* e); // add to back
event* removefront(); // remove front
event* front(); // return front
event* removeback(); // remove back
event* back(); // return back
int extract(event* e); // extract e
int terminatefront(); // terminate front
int terminateback(); // terminate back
int cancel(); // cancel specified events
int size(); // return size
int empty(); // is empty ?
friend ostream& operator<<(ostream& os,queue& q);
};
fig 4.11. The interface of the class queue
The class queue maintains the queued events in priority order with highest
queuing priority at the front of the queue. Furthermore, it maintains the size of the queue.
The function reset resets the queue as if no events were queued.
The function reportsize fills the given histogram
with samples of the size of the queue. The samples are taken,
each time an event or entity is appended to, prepended to,
removed from or extracted from the queue. The histogram is
printed at the end of the simulation or, if interval is not equal to 0,
each interval time units. It should be of type
WEIGHTED. The method reportwaiting fills the histogram with samples
of the waiting times of the events that have as queuing priority qp.
Each time an event is appended or prepended,
it gets a time-stamp. When it is removed or extracted, a sample is made
of the time it has been in the queue. The histogram should be of type
FREQUENCY. The method reportwaitingall generates a report on the
waiting times of the events regardless of their queuing priority.
The method prepend adds an event
to the front of the queue, append adds it to the back of the queue.
If the events or entities received a queuing priority, they are added in
priority order, with higher priority first. The method removefront removes
the front of the queue and returns this event, removeback removes
the back and returns this event. The methods back and front only
return the back and the front of the queue. The extract function extracts an event
from the queue, if that event is in the queue.
The terminate functions terminate the front or back of the queue.
The cancel method removes and terminates every event from the
queue that returns FALSE from its event::verify method.
Even if the queue has a report no sample is made of the waiting time
of the terminated event or of the size of the queue if these functions
are invoked.
If the kill-flag of the terminated events is set to OK, the simulation stops if this is the
event that is terminated in this way. Count should
have been set in the simulation::run method then.
The cancel method can be used to model, for example, the canceling of events
that waited too long.
The function size returns the size of the queue
and empty returns OK, if the queue is empty, FALSE otherwise.
The operator function is overloaded for queue, so when used the
tree-structure and size of the queue are printed to standard
output. This function can be used for the debugging of a simulation program.
[-<--%--^-->-]
In this section we present the histogram and analysis class
from the sim library. These classes can be used to gather and analyze the
results of a simulation. Results can be gathered by taking explicit
samples with the method sample, as is the case in the example of the dining philosophers, or
by using the report members, as is the case in the example of
the M/M/1 queue.
Accuracy can be realized by having long runs. The increase in the number of samples made
obviously gives more accurate results. We can then reduce the variance of the results by
splitting a long run in short subruns or by having several independent
runs and take the means of the means of each run. Another method of reducing the
variance is by introducing antithetic variates.
The class analysis makes it possible to analyze a histogram.
It provides members for taking confidence intervals, for determining
the covariance and correlation of two variables, for
analyzing the behavior of the simulation and for fitting curves and probability
distributions.
Gathering results with histograms
The library sim uses the histogram as a means to gather data.
When a sample is made the value of the sample is added to the sum of values
and the frequency to the sum of frequencies to calculate the mean
and variance of the data.
The frequency of the sample is then recorded in the column the value
of the sample falls in.
The histogram and its statistics can be printed both in ASCII format and
on a window (if the library runs in hush mode).
Successive prints are on the same window then.
Histogram types, samples and reports
A histogram takes six parameters, when created, so it can be used in a
very flexible way. It takes the start value, the width of a column, the
number of columns, the type of the histogram, the type of the output and the title.
Three different types are specified for a histogram. The type FREQUENCY
is used for the distribution of the frequency data of a variable. The
type WEIGHTED also takes the length of
the simulated time, that the variable persisted, in account.
Finally, the type SERIES is used to create time series histograms, that
are used to show the fluctuation of a variable over time.
Data are recorded in a histogram, when using the method sample.
This method takes the value of the sample and its frequency, say x and y and adjusts the
internal histogram data. If the type of the histogram is equal to
FREQUENCY, the y value is added to the sum of frequencies of the
column, that x falls
in. If the type of the histogram is equal to WEIGHTED, the y value
is multiplied by the time since the last sample was taken.
In a time series histogram, the x-axis represents time, so the x value
is ignored and the y value is added to the columns between the last column
in which a sample was put and the column that represents the current
simulation time. In the case of the dining philosophers, for
example, a histogram of type FREQUENCY suffices. If we were interested
in weighted averages, as is the case in analyzing queue sizes, the
histogram should be of type WEIGHTED.
The report methods use the sample function. If you use these
methods the library adds the samples for you.
The method file writes every sample made in the histogram to the
specified file (the x value on the first new line and the y
value on the second one).
Mean and variance
The histogram class also provides methods for calculating three
important statistical measures that are used extensively, the mean,
variance and standard deviation. The mean gives a measure
of the average value of a set of samples. It is defined as :
[x] = [(∑i=1nxipi)/(∑i=1npi)]
where n is the number of observations made, is the
value of the observation and is the number of
occurrences of the observation (the y value as described
in the preceding section).
The variance gives a measure of the spread of a set of samples, it is
defined as :
[x] = [(∑i=1nxipi)/(∑i=1npi)]
The standard deviation is the square root of the
variance. It is useful because it transforms the variance to a dimension
which is comparable to the original set of samples.
Printing a histogram
The mean, variance and standard deviation are also given, if the histogram is printed.
Furthermore the sum of frequencies and the maximal and
minimal values are given. The set of samples can be printed as a
frequency graph, a cumulative frequency graph or a table. Furthermore
only the statistics or the bars of the histogram can be specified.
The histogram is printed to standard output as well as on a window
(if the library runs in hush mode).
Successive prints are on the same window then.
The following histograms were obtained by running the example of the M/M/1
queue, with a mean arrival time of and a mean service time of .
-------------------------------------------------------------------
QUEUE SIZE
mean = 2.296, variance = 11.034, std dev = 3.322
sum of frequencies = 10000
min val = 0.000, max val = 27.000
0.00 4367.71#######################################################
1.00 1375.03#############
2.00 1063.30#########
3.00 774.50####
4.00 594.07##
5.00 453.16
6.00 334.99
7.00 246.18
8.00 183.12##
9.00 141.73##
over 466.16
-------------------------------------------------------------------
WAITING TIMES
mean = 0.764, variance = 0.966, std dev = 0.983
sum of frequencies = 30069
min val = 0.000, max val = 8.352
0.00 21699.00#######################################################
1.00 5220.00########
2.00 1901.00
3.00 748.00##
4.00 364.00#
5.00 100.00
6.00 25.00
7.00 9.00
8.00 3.00
9.00 0.00
fig 5.1. The results as two ASCII graphs.
The last column represents the frequency of the samples, that fell
outside the scope of the histogram. The maximal queue size is 27, so
adjusting the width of a column
to 3 or the number of columns to 30 would prevent this from happening.
If the library runs in hush mode the method also generates a window as depicted in the following figure :
slide: The histogram of the queue size
The histogram class
The class histogram has the following interface :
typedef enum {WEIGHTED,FREQUENCY,SERIES} histogramtype;
typedef enum {GRAPH,CUMULATIVE,TABLE,STATISTICS,BARSGRAPH,
BARSCUMULATIVE,BARSTABLE} outputtype;
interface histogram
{
histogram(double st,double wd,int nc,histogramtype kd,
outputtype op,char* tl); // constructor
virtual ~histogram(); // destructor
void sample(double x,double y = 1); // adds a sample
FILE* file(char* fn); // write samples to fn
void reset(); // resets histogram
double mean(); // return mean
double variance(); // return variance
double standarddeviation(); // return standard deviation
void postscript(char* fn1,char* fn2); // generate postscript
friend ostream& operator<<(ostream& os,histogram& h);
};
fig 5.3. The interface of the class histogram.
Three different types and seven different output possibilities are specified for histogram.
The output types specify a frequency graph, a cumulative frequency graph
or a table. The last four types only print the statistics or the bars of
the histogram.
The constructor takes the start value, the width of a column, the
number of columns, type of histogram, output type and the title.
The function sample takes two doubles and adjusts the internal data accordingly, as described in section 5.1.
The method file writes every sample made in the histogram to the
specified file (the x value on the first new line and the y
value on the second new line). It returns the file pointer of the
created file.
The function reset resets the histogram (and its window),
as if no samples were recorded. The methods mean,
variance and standarddeviation return the belonging
values of the recorded samples.
The function postscript writes the windows of the histogram
object (if created) in postscript format to the specified files.
The operator function
is used to print the histogram to standard output and on a window
(if the library is in hush mode).
Realizing accuracy goals
An increase in the number of samples made obviously gives more accurate results.
{Reducing the variance} can then be obtained by splitting a long run in several subruns or
by repeating a single run. Methods to do these are the methods of batch
means and of repetition. If a run is repeated the use of antithetic
variates can also be of (some) help.
\subsubsection{Batch means and repetition}
The method of batch means splits a run into several subruns. It records
the means of each subrun. The following addition to the main function
of the M/M/1 queue uses this method to
obtain the means of the queue size and waiting times for the M/M/1 queue.
h3 = new histogram(0,1,10,WEIGHTED,STATISTICS,"MEANS QUEUE SIZE");
h4 = new histogram(0,1,10,WEIGHTED,STATISTICS,"MEANS WAITING TIMES");
for (i=0;i<100;i++)
{
sim -> run(10000.0);
h3 -> sample(h1 -> mean()); // sample the mean
h4 -> sample(h2 -> mean());
h1 -> reset(); // reset for the next subrun
h2 -> reset();
}
cout << (*h3);
cout << (*h4);
fig 5.4. Batch means
After sampling the mean of each subrun, the histograms are reset, so
the results only cover a single subrun. The means of means are in both
cases weighted, as runs differ some in their length.
The method of repeating a single run has the disadvantage that on each run
we have an initial transient phase. It can be implemented as follows :
h3 = new histogram(0,1,10,WEIGHTED,STATISTICS,"MEANS QUEUE SIZE");
h4 = new histogram(0,1,10,WEIGHTED,STATISTICS,"MEANS WAITING TIMES");
for (i=0;i<100;i++)
{
sim -> run(10000.0);
h3 -> sample(h1 -> mean()); // sample the mean
h4 -> sample(h2 -> mean());
h1 -> reset(); // reset objects
h2 -> reset();
r -> reset();
q -> reset();
sim -> reset();
c = new customer(ARRIVAL); // set up again
sim -> schedule(c,0.0);
s = new server();
sim -> hold(s);
}
cout << (*h3);
cout << (*h4);
fig 5.5. Repetition
In this case we also reset the resource, queue and simulation object so
we have several independent runs. The simulation is set up at the
beginning of each run by scheduling an arrival and making the server
conditional.
Antithetic variates
If a run produces estimates that are too high, the reverse of the
random number stream can be used (by first invoking the generator::antithetic
method) when repeating the run.
The second run then produces estimates that are too low. When combining the results
the overall error tends to cancel out and the variance reduces.
Notice that the generator should be reset at the beginning of the second
run. Furthermore this method will only have effect when the random number stream indeed produces numbers that
are too high (or too low).
Analyzing the results
The class analysis makes it possible to analyze a histogram.
It provides members for taking confidence intervals, for covariance and
correlation, for
analyzing the behavior of the simulation, for fitting curves and probability
distributions and for comparing the mean of the histogram with theoretical
queue values.
Confidence intervals
Suppose we are not only interested in the sample mean, but also in
some probability judgement of the accuracy of this mean.
The usual way to do this is to construct a confidence interval for the
value of the true mean. If we denote the estimated mean as , the
estimated standard deviation as s and the sum of frequencies as n,
then, for n large enough, the $100(1-\alpha)z_{\beta}\beta^{th}\alpha = 0.01\alpha = 0.05\alpha = 0.10% confidence level.
The correct interpretation, according to [Kalvelagen 90] is as follows. If you construct a large
number of $100(1-\alpha).
The class analysis also provides a method that returns the confidence
interval for the difference of means of the histogram to be analyzed,
say a, and the histogram given as parameter in the method, say
b. If we have common variances, the interval is equal to
$(\mu_a-\mu_b) \pm z_{\alpha/2}s\sqrt{\frac{1}{n_a}+\frac{1}{n_b}}
where s is the standard deviation of the samples.
In the more typical case in which the variances are not equal, the
percentile comes from the student distribution and the standard
deviation s is estimated as
s = \sqrt{\frac{(n_{a}-1)s_{a}^{2} + (n_{b}-1)s_{b}^{2}}{n_{a} + n_{b} - 2}}
In the example of the M/M/1 queue, we can take a 95% confidence
interval of the histogram of the queue size as follows :
analysis* a;
a = new analysis(h1); // h1 is the histogram to be analyzed
a -> confidence(PERCENT95); // take 95% conf. int. of the mean
cout << (*a); // print results
fig 5.6. Taking a confidence interval with sim
The resulting output of the analysis object is outlined in the next figure :
-------------------------------------------------------------------
CONFIDENCE : QUEUE SIZE
level : 95
estimated mean : 2.30
interval : +/-0.07
fig 5.7. The ASCII output of an analysis object
If the library runs in hush mode these results are also shown on a window (with successive prints on the same window).
Covariance and correlation
If the variables that are analyzed are dependent, i.e. their values affect
each other, we sometimes need information about their dependency.
A useful measure is the correlationcoefficient, that is determined from
the covariance of two variables.
The analysis class provides the methods covariance and correlation
to determine the belonging values. The covariance
is defined as
Cov(X,Y) = \frac{\sum_{i=1}^{n}(x_{i}-\overline{x})(y_{i}-\overline{y})p_{i}}{\sum_{i=1}^{n}p_{i}}
where n is the number of observations made, x_{i}y_{i}i^{th}\overline{x}\overline{y}p_{i}i^{th}p_{i}p_{i}\rho(X,Y) = \frac{Cov(X,Y)}{\sqrt{\sigma^{2}(X)\sigma^{2}(Y)}}\chi^{2}\chi^{2}\chi^{2} = \sum_{j=1}^{r}\frac{(N_j-np_j)(N_j-np_j)}{np_j}N_jnp_j\chi^{2}\chi^{2}\chi^{2}\chi^{2}\chi^{2}\chi^{2}\chi^{2}<<\chi^{2}\chi^{2}\chi^{2}\chi^{2}\chi^{2}\chi^{2}<<