SIM : a C++ library for Discrete Event Simulation

Abstract

In this report we give a full description of sim, a C++ library for discrete event simulation. The sim library supports both an event and process-oriented approach to developing simulations. Events as well as entities (which may be considered as objects in the simulation combining several related events and having an additional phase signifying episodes in its life-time) are provided as abstract classes that must be refined by the application programmer to define the actual events and entities participating in the simulation. The sim library is integrated with the hush library, thus offering powerful graphic and animation facilities. However, the sim library may also be used independently, on both Unix and MS-Dos platforms. This report presents an overview of the classes constituting the sim library (including the classes event, entity, simulation, generator, resource, queue, histogram, analysis and screen) as well as two standard examples illustrating the deployment of the classes in writing simulation programs. Also, an example is given of how to create a graphical animation of a particular simulation. The appendix contains, moreover, a more extensive example of a jobshop simulation illustrating how the analysis class may be used to obtain measurements of complex queing behavior.

Keywords: discrete event simulation, events, entities, modeling, simulation experiments, simulation animation, simulation analysis

Introduction

Discrete Event Simulation

[-
<--%--^-->-] A simulation is the execution of a model, represented by a computer program, that gives information about the system being investigated. The simulation approach of analyzing a model is opposed to the analytical approach, where the method of analyzing the system is purely theoretical. As this approach is more reliable, the simulation approach gives more flexibility and convenience. The library sim adopts the method of the Discrete Event Simulation. With this approach, the components of the model consist of events, which are activated at certain points in time and in this way affect the overall state of the system. The points in time that an event is activated are randomized, so no input from outside the system is required. Events exist autonomously and they are discrete so between the execution of two events nothing happens. The library sim provides an event-based, as well as a process-oriented approach of writing a simulation program. With the process-oriented approach, the components of the program consist of entities, which combine several related events into one process.

Object Oriented

Sim is written in the C++ programming language, which is presented in [Stroustrup 91]. The library provides classes, that can be used by writing a C++ simulation program. We provided the library with such a functionality, that the resulting language is similar to simula and simscript, which are described in [Birtwhistle 73] and [Kiviat 69] respectively. The library certainly offers additional advantages. If you are interested in these before reading further, check the Conclusions section. The library is a C++ extension of the C simulation library, presented in [Watkins 93], in this way allowing for an Object Oriented approach towards Discrete Event Simulation. The extension consists of the classes analysis and screen, the report methods, the reset methods, scheduling priorities, graphical features and additional functionality for the other classes (more probability distributions for the class generator for example).

Hush

The library can be compiled to both a pure ASCII version as well as a hush version (see [Eliëns 94] for a description of the hush library). Both ASCII and hush simulation programs run in the ASCII as well as the hush version of the library. If you run the library in hush mode, you can use the built-in graphical features as the printing of histogram and analysis objects. The implemented process can then be tested on correctness in no time by building a simple but powerful graphical screen. For a more complex interface (a graphical animation for example) the features explicit for the hush library can be used.

The library SIM

[-<--%--^-->-] The library consists of the following C++ classes:

Program structure

[-<--%--^-->-] A typical simulation program, employing the sim library, creates a simulation object in main or application::main, together with the required resources and queues, that represent the static objects in the system and the required histogram and analysis objects for gathering and analyzing results. The dynamic objects should be derived from the classes event or entity, and be given functionality by overriding the application operator of these classes. You should use application::main and create the simulation object with a pointer to the hush toolkit, if you want to run the library in hush mode. The application should then be created and invoked in the main function. Before running the simulation, it should be set up by scheduling some events or entities depending on the type of simulation.

The event scheduling strategy

When a simulation is set up it runs after invoking the simulation::run method. The simulation then runs until the simulation::quit method is invoked, until there are no events left or for a specified number of time units or events. When using the latter approach the events should receive a kill flag when created and be terminated when not used any more. As a remark the termination of a not used event is good programming practice and should in each case be done to prevent the system from overflow. At the time an event is due to be activated, it is extracted from the scheduler and the main simulation routine executes the code from the application operator of that event. The library adopts the algorithm that is outlined in the next figure as its event scheduling strategy.

slide: Scheduling

Notice, that the algorithm extracts all events with the same activation time. The main simulation routine activates them in priority order with the highest scheduling priority first. Before executing the events, the simulation clock is updated to the activation time of the current events. Furthermore, the conditional list is traversed in priority order with highest scheduling priority first. Events, that can run now, are executed. After the running of the simulation some statistics of the starting and end time, of the number of created, activated and terminated events and of the actual time taken are given.

Primitives and event states

When an event is activated it can be manipulated in various ways. The event can be appended to a queue, it can be put back in the scheduler and so on. On its way it changes state. We identify the following states an event can be in: The diverse primitives that manipulate the event and cause the change of state are then: As you may have noticed the invoking of a primitive has not always effect. The event should be in the correct state first. The invoking of the hold primitive for example assumes a passive event. This state can be obtained by first passivating the event. Consider the following transition diagram that shows the different states an event can be in, in relationship with the different functions, that manipulate them. The central state of an event is the passive state. Most events should first be passivated before they continue. Notice that an event also changes state if it is pending and is due to execute. This event (the first event in the scheduler) is extracted from the scheduler and becomes active. An event that is on the conditional list stays conditional while executing until it is explicitly passivated.

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 count^{th} 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 count^{th} 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.

Queuing events

[-<--%--^-->-] 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 :

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 count^{th} 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.

Gathering and analyzing results

[-<--%--^-->-] 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, x_{i} is the value of the i^{th} observation and p_{i} is the number of occurrences of the i^{th} 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 \frac{1}{3} and a mean service time of \frac{1}{4}.

-------------------------------------------------------------------
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 \mu, the estimated standard deviation as s and the sum of frequencies as n, then, for n large enough, the $100(1-\alpha)% confidence interval for the true value of the mean is given by
μ±z_α/2[ s/√n]
where
z_{\beta} denotes the \beta^{th} percentile of the standard normal distribution. The library sim calculates this interval for \alpha = 0.01, \alpha = 0.05 and \alpha = 0.10. If n is not large enough, the central limit theory doesn't apply here. In this case sim takes the percentile from the student distribution. The library adopts a value of 30 for determining whether the student or the normal distribution should be used. This is also the value that is used in [Watkins 93]. You should be careful how to interpret this $100(1-\alpha)% confidence level. The correct interpretation, according to [Kalvelagen 90] is as follows. If you construct a large number of $100(1-\alpha)% confidence intervals each based on a sample of n observations with n large enough, the proportion of intervals which cover the true value of the mean is approximately equal to $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} and y_{i} are the values of the i^{th} observation in the two samples, \overline{x} and \overline{y} are the means of sample x and sample y and p_{i} is the number of occurrences of the i^{th} observation in both samples. The p_{i} values are adjusted for the two samples. If, for example, the correlation of the sizes of two queues over time is determined, we take equal intervals for the p_{i} values. The correlation coefficient is then :
\rho(X,Y) = \frac{Cov(X,Y)}{\sqrt{\sigma^{2}(X)\sigma^{2}(Y)}}
which divides the covariance with the two variances. The square of the correlation coefficient tells us what proportion of the variation of the y values can be attributed to a linear relationship with the x values. If the variables are independent (or uncorrelated) the covariance and correlation coefficient are equal to 0. The methods of the library assume that the samples of the two histograms are written to a file. If we have a simulation that exhibits queue swapping we could determine the correlation of the sizes of the queues as follows :

    q1 -> reportsize(h1);             // add reports
    q2 -> reportsize(h2);

    h1 -> file("sizes1");             // raw samples
    h2 -> file("sizes2");

    sim -> run(10000.0);
  
    a = new analysis(h1);
    a -> covariance(h2);              // covariance h1 and h2
    a -> correlation(h2);             // correlation h1 and h2
    cout << (*a);

fig 5.8. Covariance and correlation.

Fitting curves and distributions

Suppose you want to investigate if there is some form of linear or quadratic relationship between the samples you made. The library provides members to fit a linear or quadratic curve on the data. You can fit a curve with your own parameters, otherwise the curve that minimizes the sum of the squared deviations between the recorded and predicted data is taken. Furthermore, the belonging
\chi^{2} statistic and probability are given. The probability is calculated with the method chisquaretest. This method calculates the \chi^{2} statistic, taken from [Kalvelagen 90], that is defined as :
\chi^{2} = \sum_{j=1}^{r}\frac{(N_j-np_j)(N_j-np_j)}{np_j}
where r is the number of columns (and r-1 the degree of freedom of the test),
N_j is the observed value for column j (the column frequency) and np_j is the expected value for that column (the calculated value). The \chi^{2} test can only be applied if the size of the sample is large enough. Therefore we pool the columns with an expected value of less then 5 (and the corresponding observed columns) with columns that have a frequency that is high enough. The degree of freedom decreases by 1 for each pooled column then. The belonging \chi^{2} probability is the chance that the \chi^{2} statistic is greater then the calculated \chi^{2} statistic, given the degree of freedom. In the case of fitting a curve, a \chi^{2} probability that is equal to 0 suggests no reasonable fit. The following code fits a linear curve.

   analysis* a;
   a = new analysis(h);   // h is the histogram to be analyzed
   a -> linear();         // fit linear curve
   cout << (*a);          // print results
   

fig 5.9. Fitting a curve
The library also provides methods to fit a probability distribution on the histogram to be analyzed. The method calculates the relevant parameters of the distribution from the mean and variance of the histogram data or, if the parameter(s) to the distribution are specified, it just uses these parameters. The method then constructs the distribution and fits this distribution on the histogram data. After performing the
\chi^{2} test, the belonging \chi^{2} statistic and probability are given. The printing of the analysis object is on standard output and if the library runs in hush mode also on a window. The curve is also shown on the window of the histogram, if this window exists. The following window was obtained by filling a histogram (with a column width of 0.5) with 10000 samples from the normal distribution with a mean of 5 and a standard deviation of 1.3. The analysis::normal method received these parameters, when invoked. If not, the method had calculated the a and b values from the histogram data.

slide: Fitting the normal distribution

The behavior of the simulation

The behavior of the simulation should be expected to fluctuate because of the stochastic nature of the model. This behavior can be broadly classified, according to [Watkins 93], into one of the three following categories :
  • steady-state behavior - in which the average fluctuation tends to remain essentially constant.
  • transient behavior - in which the average fluctuations do not settle down to a single value.
  • regenerative behavior - which is cyclic.

Transient and steady-state behavior

Consider the M/M/1 queue. In the beginning customers are appended to an empty queue. The average of the queue sizes and waiting times increases, so the system shows transient behavior. After some time the situation is normalized and the average fluctuation tends to remain constant. This period of time is in most cases the period, that is of interest to the user. The library provides a member behavior, that takes a SERIES histogram h and an integer n. The method fills the histogram h with the moving averages of the histogram to be analyzed, i.e. the samples recorded averaged over the last n values. The time series histogram is printed to standard output and (if in hush mode) on a window when the simulation stops. The histogram h should be of type SERIES, so it shows the fluctuation of the moving average as a function of time. Indications for the behavior of the simulation are :
  • steady-state behavior - the moving average remains more or less constant.
  • transient behavior - the moving average increases or decreases for a relevant period.
The following code generates a SERIES histogram from the histogram of the queue size of the M/M/1 queue.

   analysis* a;
   histogram* h3;         // create time series histogram

   h3 = new histogram(0,100,100,SERIES,GRAPH,"moving average queue size");
   a = new analysis(h1);  // h1 is the histogram, to be analyzed
   a -> behavior(h3,20);  // generate a report on behavior
   sim -> run(10000.0);   // call behavior before running the simulation
   

fig 5.11. Analyzing the behavior of the simulation
Notice that the total width of the histogram should cover the entire simulation time. Furthermore, the method behavior should be invoked, before the simulation is run. Suppose we were interested in the queue sizes and waiting times of the M/M/1 queue, when its behavior was steady. Furthermore, from inspecting the SERIES histogram of the moving average, we obtained a initial transient period of 500 time units. The following addition to main would do the job :

   sim -> run(500.0);          // run for initial transient period
   q -> reportsize(h1);        // generate a report on queue sizes
   q -> reportwaiting(h2);     // generate a report on waiting times
   sim -> run(10000.0);        // run for steady period
   

fig 5.12. Running for an initial transient period
The reports are generated after the initial run, so they only cover the results of the steady period.

Regenerative behavior

If the simulation shows regenerative behavior the simulation returns to a particular state where the past history of states has no influence on the future of the system. Consider the M/M/1 queue. If no customers are waiting and none being served, the system is in a regenerative state. At this time the past history has no effect on its future behavior. Suppose we want to run the simulation of the M/M/1 queue for n cycles. First we quit the simulation when the regenerative state is entered. The following addition to the departure event will do the job :

   case DEPARTURE :
     r -> release();
     sim -> terminate(this);

     // check for a cycle
     if (q -> empty() && r -> full())       // system empty ?
       sim -> quit();                       // quit the simulation
     return OK;

fig 5.13. Detecting the regenerative state
In this way when a customer departs we look if s/he was the last being served and if the waiting line is empty or not. The simulation then quits and can easily be started again in the main loop. The addition to the main function is :

   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 run();                   // run until quit (for a cycle)
     h3 -> sample(h1 -> mean());     // sample means of each cycle
     h4 -> sample(h2 -> mean());
     h1 -> reset();                  // reset reports
     h2 -> reset();
   }
   cout << (*h3);
   cout << (*h4);

fig 5.14. Running for n cycles
Furthermore we can discard the statistics of the initial transient period of each cycle, as described in the preceding section. A cycle should cover a relevant period if we model a simulation like that.

The design of experiments

In this section we look briefly at the design of experiments. Suppose we are not only interested in the results of a single simulation run, but we'd also like to compare the results of runs under varying conditions. You can think of a post office with a fixed arrival and service pattern but a varying number of service points. The set of input variables and their values is called the treatment of the experiment then. The single execution of an experiment is called a trial and the configuration of the experiment is called the experimental unit. An experiment consists of several trials with several treatments. The results can then be gathered and analyzed as described in the preceding sections. Consider the M/M/1 queue. If we run several trials and take as treatment the same conditions except for a different resource amount, we can investigate the relationship with the mean queue size and mean waiting time. After that we can make a trade-off between the decrease in these values and the costs of providing more of the resource and find some optimum. The addition to the main function :

   double amount = 1;
   r = new resource(amount);
   h3 = new histogram(1,1,5,FREQUENCY,BARSGRAPH,"AMOUNT - QUEUE SIZE");
   h4 = new histogram(1,1,5,FREQUENCY,BARSGRAPH,"AMOUNT - WAITING TIMES");
 
   while (amount <= 5)
   {
     sim -> run(10000.0);
     h3 -> sample(amount,h1 -> mean());
     h4 -> sample(amount,h2 -> mean());
     h1 -> reset();
     h2 -> reset();
     amount += 1;
     delete r;
     r = new resource(amount);
     q -> reset();
     g1 -> reset();
     g2 -> reset();
     sim -> reset();
     c = new customer(ARRIVAL);
     sim -> schedule(c,0.0);
     s = new server();
     sim -> hold(s);
   }
   cout << (*h3);
   cout << (*h4);

fig 5.15. An experiment
Notice that the total experimental unit is reset, except for the resource. The resource is destroyed and created with a new initial amount on each trial.

The analysis class

The interface of the class analysis is :

   typedef enum {PERCENT90,PERCENT95,PERCENT99} level;

   interface analysis 
   {
     analysis(histogram* h); // taking histogram to analyze
     virtual ~analysis();    // destructor
   
     void reset();  // reset output structures

     double confidence(level cl); // confidence interval of mean
     double confidence(level cl,histogram* h); // difference

     double covariance(histogram* h2);       // covariance h and h2
     double correlation(histogram* h2);      // correlation h and h2
   
     double chisquaretest(double* expected); // chi-square test
   
     double linear();                    // linear fit, return chi-square
     double linear(double a,double b);   
     double quadratic();                 // quadratic fit, return chi-square
     double quadratic(double a,double b,double c);
   
     double uniform();            // uniform fit, return chi-square
     double uniform(double a,double b);     
               .
               .
               .
     double laplace();            // laplace fit, return chi-square
     double laplace(double a); 
   
     int behavior(histogram* h,int n);   // report behavior

     int MM1size(double lb,double mu);   // M/M/1 queue values
     int MM1waitingtime(double lb,double mu);
     int MMcsize(double lb,double mu);   // M/M/c queue values
     int MMcwaitingtime(double lb,double mu);
     int MD1size(double lb,double mu);   // D/D/1 queue values
     int MD1waitingtime(double lb,double mu);
   
     void postscript(char* fn);   // generate postscript
   
     friend ostream& operator<<(ostream& os,analysis& a);
   };
   

fig 5.16. The interface of the class analysis
The complete listing of the interface is given in appendix C. The constructor takes the histogram, to be analyzed and creates output structures, which can be set by the different methods and be printed to standard output and on the belonging windows (if in hush mode), using the operator
<< function. The method reset can be used to reset the structures as if nothing was analyzed yet. This method removes every existing window from the screen. The method confidence returns the confidence interval for the mean of the histogram, to be analyzed. If it takes a second histogram as parameter, it returns the confidence interval for the difference of means. Each time the function confidence is invoked, it creates an output structure, that is printed when the analysis object is. The methods covariance and correlation determine and return the covariance and correlation coefficient of the two histograms. The function chisquaretest takes an array of doubles and returns the \chi^{2} probability, according to the values of the histogram. The probability that the statistic is greater then the calculated statistic is returned. The output structure contains also the \chi^{2} statistic and degree of freedom. The functions linear and quadratic respectively fit a linear and quadratic curve on the histogram, the output contains the (estimated) parameters and the \chi^{2} probability. The distribution methods fit the belonging distributions on the histograms, the output contains the (estimated) parameter(s), the \chi^{2} statistic and the \chi^{2} probability. They all return the belonging \chi^{2} probability. The method behavior is used to analyze the behavior of the simulation, as described in section 5.3.4. The queue methods compare the mean of the histogram with the mean of the queue size or waiting time and also give the confidence interval for the histogram mean. If the theoretical mean lies within the confidence interval of the histogram mean the method returns OK, otherwise FALSE. The function postscript writes the windows of the analysis object (if created) in postscript format to the specified file. The overloaded operator<< function makes it possible to write an analysis object to standard output and (if in hush mode) on the belonging windows (one for each method). Successive prints take the same window.

Creating a graphical interface

[-<--%--^-->-] The library also offers a class to develop in no time a simple but powerful graphical interface for the simulation program (for example to check if the implemented process is right). If you want to provide the simulation with a more complex graphical interface (for a graphical animation for example), you can use the hush library as described in [Eliëns 94]. The screen class of the sim library offers built-in tools as the simulation clock, a start-stop button, means for adjusting the speed of the simulation, queries for input variables and methods for creating and moving figures. The functionality of the screen class is explained at the hand of a graphical interface for the philosophers program. When using the screen the library should run in hush mode, otherwise only the query method has any effect.

A graphical interface for the Dining Philosophers

The graphical interface of the philosophers program consists of a table, five dishes and five chopsticks. The current state of the philosopher is presented as a piece of text. The clock, speed adjuster and the start-stop button are built-in in the screen object. At last the duration is asked before running the simulation. The interface is created in the application::initscreen() method, that is called in application::main(), just before running the simulation. The code merely consists of adding circles for the dishes and the table, lines for the chopsticks, text for the state of the philosopher and a query for the duration of the simulation program. The figures are added, knowing that the screen defaults are a width of 800 units and a height of 500 units.

   void application::initscreen()
   {
     s = new screen(5);                         // create the screen

     s -> circle("table",400,250,200,"-fill black");   // table

     s -> circle("b1",275,295,25,"-fill white");       // dishes
     s -> circle("b1",275,295,20,"-fill white");
     s -> text("b1",275,295,"1");
     s -> circle("b2",400,395,25,"-fill white");
     s -> circle("b2",400,395,20,"-fill white");
     s -> text("b2",400,395,"2");
     s -> circle("b3",525,295,25,"-fill white");
     s -> circle("b3",525,295,20,"-fill white");
     s -> text("b3",525,295,"3");
     s -> circle("b4",475,145,25,"-fill white");
     s -> circle("b4",475,145,20,"-fill white");
     s -> text("b4",475,145,"4");
     s -> circle("b5",325,145,25,"-fill white");
     s -> circle("b5",325,145,20,"-fill white");
     s -> text("b5",325,145,"5");

     s -> line("c1",270,210,320,240,"-fill white");    // chopsticks
     s -> line("c2",300,380,340,340,"-fill white");
     s -> line("c3",460,340,500,380,"-fill white");
     s -> line("c4",530,210,480,240,"-fill white");
     s -> line("c5",400,115,400,175,"-fill white");

     s -> text("t1",150,330,"waiting");                // states
     s -> text("t2",400,475,"waiting");
     s -> text("t3",650,330,"waiting");
     s -> text("t4",570,50,"waiting");
     s -> text("t5",230,50,"waiting");

     duration = s -> query("duration");         // query for the duration
   }

fig 6.1. Creating and initializing the screen
The screen is made global as it is used in the events from the philosopher program. If a philosopher starts to think it releases the chopsticks. We wrote a function releasechopsticks that not only releases the chopsticks but also changes the screen. It moves the chopsticks away from the dish to their central positions and changes the state of the philosopher on the screen. This function then replaces the releasing of the chopsticks in the philosopher::think function. Consider the implementation.


   void philosopher::releasechopsticks()
   {
     chopstick[id] -> release();             // release left chopstick
     chopstick[(id+1)%NUMBER] -> release();  // release right chopstick
     switch(id)                              // which philosopher ?
     {
       case 0 :
         s -> move("c1",10,-40);             // move chopsticks to
         s -> move("c2",30,30);              // their central positions
         s -> destroy("t1");                 // change the state on the
         s -> text("t1",150,330,"thinking"); // screen to thinking
         break;
       case 1 :
         s -> move("c2",-50,-20);            // move chopsticks to
         s -> move("c3",50,-20);             // their central positions
         s -> destroy("t2");                 // change the state on the
         s -> text("t2",400,475,"thinking"); // screen to thinking
         break;
       case 2 :
         s -> move("c3",-30,30);             // move chopsticks to 
         s -> move("c4",-10,-40);            // their central positions
         s -> destroy("t3");                 // change the state on the
         s -> text("t3",650,330,"thinking"); // screen to thinking
         break;
       case 3 :
         s -> move("c4",20,50);              // move chopsticks to 
         s -> move("c5",-40,0);              // their central positions
         s -> destroy("t4");                 // change the state on the
         s -> text("t4",570,50,"thinking");  // screen to thinking
         break;
       case 4 :
         s -> move("c5",40,0);               // move chopsticks to 
         s -> move("c1",-20,50);             // their central positions
         s -> destroy("t5");                 // change the state on the
         s -> text("t5",230,50,"thinking");  // screen to thinking
         break;
     }
   }

fig 6.2. Releasing the chopsticks
The functions acquirechopsticks and waitonchopsticks are the same and are not included here. The first one acquires the chopsticks, moves them in the opposite direction and changes the state on the screen to eating. The second one doesn't acquire or move the chopsticks, it only changes the state on the screen to waiting. We obtained the following screen :

slide: The philosophers screen

The screen class

The screen class has the following interface :

   interface screen
   {
     screen(double fct,int x = 800,int y = 500,
                            char* options = "-background lightskyblue1");
     virtual ~screen();                  // destructor

     void reset();                       // reset the screen
   
     void line(char* fig,int x1,int y1,int x2,int y2,char* options = "");
     void line(char* fig,char* linespec,char* options = "");
     void oval(char* fig,int x1,int y1,int x2,int y2,char* options = "");
     void circle(char* fig,int x,int y,int r,char* options = "");
     void polygon(char* fig,char* linespec,char* options = "");
     void rectangle(char* fig,int x1,int y1,int x2,int y2,char* options = "");
     void square(char* fig,int x,int y,int r,char* options = "");
     void bitmap(char* fig,int x,int y,char* bitmap,char* options = "");
     void text(char* fig,int x,int y,char* txt,char* options = "");

     void move(char* fig,int x,int y);   // move figure

     void destroy(char* fig);            // destroy figure
   
     double query(char* txt);            // pop-up a query
 
     void postscript(char* fn);          // generate postscript
   };


fig 6.4. The interface of the class screen
The constructor creates the components of the screen. As you have seen the screen consists of a start-stop button, which starts and stops the simulation, an adjuster for the speed, the simulation clock, a window on which figures can be put and moved and a quit button, which ends the session. The constructor takes four arguments. The first one is the factor that we multiply the delay between the execution of two events with. If the current speed is 0, the delay is the inter-execution time in seconds (regardless of the time unit in the simulation) times this factor. As you increase the speed by 1 (up to 100), the delay decreases by 1 % (down to 0 %). The x and y arguments set the width (default 800) and height (default 500) of the screen. The options argument comes from the hush library. It can be used to set the background color, for example. If you click on the quit button or invoke the destructor the session is stopped and the screen and the other windows (for the histograms etc.) are removed. The reset function resets the clock to 0 and the start-stop button to the stop state but doesn't delete the figures from the screen. These should be explicitly removed by destroying the figures. The method line takes two coordinates and puts a line between them. The line is added to the specified figure. The coordinates can also be specified by an array of coordinates (that can easily be constructed with the sprintf function). The options parameter (empty by default) comes from the hush library. You can, for example, specify the fill color of a figure as in the philosopher program. The methods oval, polygon and rectangle create an oval, polygon and rectangle respectively. These items are added between the specified coordinates and belong to the specified figure. The methods square and circle add the figure on the specified coordinate with r as radius. The bitmap function takes a bitmap and adds this bitmap on the specified coordinate and to the specified figure. The method text adds a text item on the specified coordinate and adds it to the specified figure. A figure can be moved x to the right and y downwards with the move method. All items belonging to fig are moved in this direction. The method destroy deletes the specified figure from the screen. Furthermore you can use the function query. This function creates a window with the specified text, with room to enter a value and an OK button. If you click once on the entry you can enter the value. If you click on the OK button, the window disappears and the value is returned as a double and can be used in the simulation program (as is the duration of the philosophers simulation). The postscript method writes the screen in postscript format to the specified file. If the library doesn't run in hush mode only the query method is of any value. It prints the specified text and asks and returns an input value.

Conclusions

[-<--%--^-->-] The library sim extends C++, so that the resulting language exhibits the same functionality as widely used languages as simscript or simula. Sim provides a convenient, yet flexible way to write simulation programs in C++. It offers in addition the advantages of C++ :
  • efficiency - C++ compilers typically generate much more efficient code as is the case with simula and simscript.
  • readability - it is our experience that programs written with the sim library tend to be smaller, be more readable and describe the simulation model more accurate.
  • features - C++ provides more advanced object-oriented features than simula and simscript. For example, class instance variables to be either private or public, instead of only public. Furthermore the inheritance mechanism provides powerful means for code sharing and structuring programs as a type hierarchy. These features affect the structure and problems of debugging of a program.
  • generality - C++ is known and used in other areas as simulation.
  • the means to capture complexity - we wrote much larger simulations, then the ones included in this article. As C++ is well suited for developing complex software projects, sim is for writing complex simulations.
Furthermore, the library offers more advanced features than simula or simscript. For example the possibility to write both event-based and process-oriented simulations, the classes histogram, analysis and screen, the report members, scheduling priorities and the possibility to create complex graphical interfaces.

Acknowledgements

We would like to thank Arjan van der Valk, Erik van der Sluis and Aad van der Vaart for their comments on the library.