Modeling and simulation with adevs

Adevs is a simulator for models described using the Discrete Event System Specification (DEVS) modeling framework. As the name suggests, the key feature of models described in DEVS (and implemented in adevs) is that their dynamic behavior is defined in terms of events. An event can be any kind of change that is significant within the contex of the model being developed.

Discrete event system modeling can be most easily introduced with an example. Suppose we want to model the checkout line at a convenience store. There is a single clerk working at the counter, and the clerk serves customers in a first come-first serve fashion. Each customer has a different number of items, and so they require more or less time for the clerk to ring up their bill. For this study, we are interested in determining the average and maximum amount of time that customers spend waiting in line.


Figure 1. Customers waiting in line at BusyMart.




To model this system, we will need an object to represent each customer in the line. We can accomplish this by creating a new class that is derived from the adevs object class. Our customer objects will need at least one attribute, which is the time needed to ring up the customer's bill. Since we want to be able to determine how long a customer has been waiting in line, we will also include attributes that record the time at which the customer entered the queue and the time that the customer left the queue. The customer's waiting time can then be computed as the difference in these times. Here is the customer class, from which we will create customer objects as needed. The class is coded in a single header file, customer.h.

#include "adevs.h"

class customer: public object
{
        public:
                /// Create a customer.
                customer():
                object(),
                twait(0.0),
                tenter(0.0),
                tleave(0.0)
                {
                }
                /// Copy constructor
                customer(const customer& src):
                object(src),
                twait(src.twait),
                tenter(src.tenter),
                tleave(src.tleave)
                {
                }
                /// Clone, creats a copy of the object
                object* clone() const
                {
                        return new customer(*this);
                }
                /// Time needed for the clerk to process the customer
                double twait;
                /// Time that the customer entered and left the queue
                double tenter, tleave;
};

The next aspect of the system that we want to capture is the clerk. The model of the clerk is our first example of an atomic model with dynamic behavior. Fortunately, the clerk's behavior is very simple. The clerk has a line of people waiting at her counter. When a customer arrives at the clerk's counter, that person is placed at the end of the line. When the clerk is not busy and somebody is waiting in line, the clerk will ringing up that customer's bill and then send the customer on his way. The clerk then looks for another customer waiting in line. If there is a customer, the clerk proceeds as before. Otherwise, the clerk sits idly at her counter waiting for more customers.

The DEVS model of the clerk is described in a particular way. First, we need to define the ports by which customers arrive and depart. These ports are used connect models together, as will be demonstrated shortly. Lets suppose that customers arrive in line via an “arrive” port and leave via a “depart” port. The second thing that we need to determine are the set of state variables needed to describe the clerk. In this case, we need to know which customers are in line. This can be captured in a list of customers, and we can use a list from the C++ Standard Template Library for that purpose. To complete the description of the clerk, we will need four methods that describe the behavior of the clerk over time. First, lets construct the header file for the clerk. Then we can proceed to fill in the details.

#include "adevs.h"
#include "customer.h"
#include <list>

class clerk: public atomic 
{
        public:
                /// Constructor.
                clerk():
                atomic(),
                line()
                {
                }
                /// State initialization function.
                void init();
                /// Internal transition function.
                void delta_int();
                /// External transition function.
                void delta_ext(stime_t e, const adevs_bag<PortValue>& x);
                /// Confluent transition function.
                void delta_conf(const adevs_bag<PortValue>& x);
                /// Output function.  
                void output_func(adevs_bag<PortValue>& y);
                /// Output value garbage collection.
                void gc_output(adevs_bag<PortValue>& g);
                /// Destructor.
                ~clerk();
                /// Model input port.
                static const port_t arrive;
                /// Model output port.
                static const port_t depart;

        private:        
                /// List of waiting customers.
                std::list<customer*> line;              
                /// Delete all waiting customers and clear the list.
                void empty_line();
}; 

This header file provides a template for most any atomic model that we want to create. The class is derived from the adevs atomic class, and it defines the virtual state initialization, state transition, output, and garbage collection methods required by the atomic base class. The class also includes a set of static, constant port variables that correspond to the clerk's input (arrival) and output (departure) ports. The constructor for the clerk class invokes the constructor of the atomic base class. The clerk state variables are defined as private class attributes. A private helper method has also been defined for clearing the list of customers.

The first thing that we need to do is define the helper method and instantiate the state port variables. This is done in the model's C++ source file. The corresponding C++ source code is

const port_t clerk::arrive = 0;
const port_t clerk::depart = 1;

void clerk::empty_line()
{
        while (!line.empty())
        {
                delete line.front();
                line.pop_front();
        }
}

The port variables arrive and depart are assigned class unique integers. Typically, the ports for a model a simply numbered in a way that corresponds with the order in which they are listed. The helper method simply deletes and then removes each customer waiting in the list.

The init() method of our clerk model will be the first method called prior to beginning a simulation execution. This method should place the clerk into her initial state and schedule the clerk's firt event. For our experiment, this state is an empty line and so the init() method just calls the empty_line() method. Since the list is initially empty, the clerk has no events to schedule. This is indicated by calling the passivate() method, which sets the clerk's time of next event to infinity.

void clerk::init()
{
        empty_line();
        passivate();
}

Since the clerk has an empty line at first, the only interesting thing that can happen is to have a customer arrive. Customer arrivals are events that appear on the clerk's “arrive” input port. The arrival of a customer will cause the clerk's external transition method to be activated. The arguments to the method are the time that has elapsed since the clerk last changed state and a bag of PortValue events. Each of the PortValue events has two fields. The first is the port field. It contains the number of the port that the event arrived on. The second is a pointer to an object that is the customer which arrived. Since the input events are const qualified objects, we will need to make a copy of each customer before placing them at the back of the line. At this time, we also set the entry time for each customer.

After adding all arriving customer's to the back of the line, we check to see if we are already busy with a customer. Since the clerk is idle unless she is busy with a customer, it is sufficient to check the clerk's time of next event. If that time is not infinity, then the clerk is busy. If the clerk is busy, then we simple continue with the clerk's next event time unchanged. Otherwise, the clerk can begin ringing up a new customer. This is done by using the hold method. By calling hold with the customer checkout time, the clerk is able to set its next event time to the current time plus the checkout time. The clerk then records the time at which the customer will depart the line. Notice that this time is not the time of next event, which is when the customer departs the store. Rather, the time spent in line by the customer is the time at which the customer departs the store minus the time required to ring up the customer. Here is the implementation of the clerk's external transition function.

void clerk::delta_ext(double e, const adevs_bag<PortValue>& x)
{
        /// Add the new customers to the back of the line.
        for (int i = 0; i < x.getSize(); i++)
        {
                /// Cast the object* values to a customer* type.
                const customer* new_customer = dynamic_cast<const customer*>(x.get(i).value);
                /// Copy the customer object and place it at the back of the line.
                line.push_back(new customer(*new_customer));
                /// Record the time at which the customer entered the line.
                line.back()->tenter = timeCurrent();
        }
        /// If we are not already processing a customer,
        if (timeNext() == ADEVS_INFINITY)
        {
                /// Then schedule our next event when the customer should depart.
                hold(line.front()->twait);
                /// Record the time at which the customer will leave the line.
                line.front()->tleave = timeNext() - line.front()->twait;
        }
        /// Otherwise, continue with our next event time unchanged.
        else
        {
           hold(sigma());
        }
}

Eventually, the clerk will be done ringing up the customer. At this time, the clerk sends the customer on his way and looks for a new customer in the line. If there is another customer waiting in line, the clerk will begin ringing that customer up in the same fashion as before. This will all occur when the simulation clock has finally reach the clerk's time of next event. At this time, two things happen. First, the clerk's output method is called. When this happens, the clerk model will place the departing customer on its “depart” output port. Next, the clerk's internal transition method is activated. The clerk's internal transition method changes the state of the clerk by removing the departed customer from the line and scheduling another customer depature event (if the line is not empty) or setting the time of next event to infinity if there are no customers waiting. The output function and internal transition function for the clerk model are shown below.

void clerk::output_func(adevs_bag<PortValue>& y)
{
        /// Place the customer at the front of the line onto the depart port.
        /// A corresponding PortValue object is added to the bag y by the output()
        /// method.
        output(depart,line.front(),y);
}

void clerk::delta_int()
{
        /// Remove the departing customer from the front of the line.
        /// The departing customer will be deleted later by our garbage
        /// collection method.
        line.pop_front();
        /// Check to see if any customers are waiting.
        if (line.empty())
        {
                /// If not, then set our time of next event to infinity.
                passivate();
        }
        else
        {
                /// Otherwise, start ringing up the next customer in line.
                /// Schedule our next event when the customer should depart.
                hold(line.front()->twait);
                /// Record the time at which the customer will leave the line.
                line.front()->tleave = timeNext() - line.front()->twait;
        }
}

At this point we have almost completely defined the behavior of the clerk model. Only one thing remains to be done. Suppose that a customer arrives at the clerk's line at the same time that the clerk has finished up with a customer. In this case we have a conflict since the internal transition function and external transition function must both be activated to handle both of these events. In DEVS, such conflicts are resolved by the confluent transition function. For the clerk model, the confluent transition function can be computed using the internal transition function first (to remove the newly departed customer from the list) followed by the external transition function (to add new customers to the end of the list and begin ringing up the first customer). Below is the implementation of the clerk's confluent transition function.

void clerk::delta_conf(const adevs_bag<PortValue>& x)
{
        delta_int();
        delta_ext(0.0,x);
}

To see how this model behaves, suppose we had customers arrive according to the schedule shown in the table below.

Customer arrival time

Customer checkout time

1

1

2

4

3

4

5

2

7

10

8

20

10

2

11

1

Table 1. Customer arrival and checkout times.

In this example, the first customer appears on the clerk's “arrive” port at time 1, the next customer appears on the “arrive” port at time 2, and so on. By adding print statements at the very end of the internal, external, and output functions for the clerk model, we can watch the evolution of the clerk's line. Here is the output trace produced by the above sequence of inputs.

Computed the external transition function at t = 1
There are 1 customers waiting.
The next customer will leave at t = 2.
Computed the output function at t = 2
A customer just departed!
Computed the internal transition function at t = 2
There are 0 customers waiting.
The next customer will leave at t = 1.79769e+308.
Computed the external transition function at t = 2
There are 1 customers waiting.
The next customer will leave at t = 6.
Computed the external transition function at t = 3
There are 2 customers waiting.
The next customer will leave at t = 6.
Computed the external transition function at t = 5
There are 3 customers waiting.
The next customer will leave at t = 6.
Computed the output function at t = 6
A customer just departed!
Computed the internal transition function at t = 6
There are 2 customers waiting.
The next customer will leave at t = 10.
Computed the external transition function at t = 7
There are 3 customers waiting.
The next customer will leave at t = 10.
Computed the external transition function at t = 8
There are 4 customers waiting.
The next customer will leave at t = 10.
Computed the output function at t = 10
A customer just departed!
Computed the internal transition function at t = 10
There are 3 customers waiting.
The next customer will leave at t = 12.
Computed the external transition function at t = 10
There are 4 customers waiting.
The next customer will leave at t = 12.
Computed the external transition function at t = 11
There are 5 customers waiting.
The next customer will leave at t = 12.
Computed the output function at t = 12
A customer just departed!
Computed the internal transition function at t = 12
There are 4 customers waiting.
The next customer will leave at t = 22.
Computed the output function at t = 22
A customer just departed!
Computed the internal transition function at t = 22
There are 3 customers waiting.
The next customer will leave at t = 42.
Computed the output function at t = 42
A customer just departed!
Computed the internal transition function at t = 42
There are 2 customers waiting.
The next customer will leave at t = 44.
Computed the output function at t = 44
A customer just departed!
Computed the internal transition function at t = 44
There are 1 customers waiting.
The next customer will leave at t = 45.
Computed the output function at t = 45
A customer just departed!
Computed the internal transition function at t = 45
There are 0 customers waiting.
The next customer will leave at t = 1.79769e+308.

The basic simulation algorithm is illustrated by this example. Notice that the external transition function is always activated when an input (in this case, a customer) arrives on an input port. This is because the external transition function describes the response of the model to input events. The internal transition function is always activated when the simulation clock has reached the model's time of next event. The internal transition function describes the autonomous behavior of the system (i.e., how the system responds to events that it has scheduled for itself). The internal transition function is always immediately preceded by the output function. Consequently, a model can only produce outputs by scheduling an event for itself. The value of the output is computed using the current state of the model.

To complete our simulation of the convenience store, we need two other atomic models. The first model should produce customers for the clerk to serve. The customer arrival rate could be modeled using a random variable or stochastic process with appropriate statistics, or it could be driven by a table of values such as the one used in the previous example. In either case, we hope that the customer arrival process accurately reflects what happens in a typical day at the convenience store. For this example, we will use a table to drive the customer arrival process. Data for this table could come directly from observing customers at the store, or it might be produced by a statistical model in another tool (e.g., a spreadsheet program).

We will use an atomic model called a generator to create customer arrival events. The input file format will be identical to that used in the previous example. The input file will contain a line for each customer that arrives. Each line has the customer arrival time first, followed by the customer service time. The generator is an input free atomic model since all of its events are scripted in the input file. The generator will need a single output port, which we will call “arrive”, through which is can export arriving customers. The model state is simply a C++ file input stream by which the input file is read, and a customer object that represents the next customer that will arrive at the store. The model includes one private helper method for reading customer arrival data and scheduling the corresponding customer arrival event. Here is the header file for the generator atomic model.

#include "adevs.h"
#include "customer.h"
#include <fstream>

class generator: public atomic 
{
        public:
                /// Constructor.
                generator(const char* data_file):
                atomic(),
                input_strm(data_file)
                {
                }
                /// State initialization function.
                void init();
                /// Internal transition function.
                void delta_int();
                /// External transition function.
                void delta_ext(stime_t e, const adevs_bag<PortValue>& x);
                /// Confluent transition function.
                void delta_conf(const adevs_bag<PortValue>& x);
                /// Output function.  
                void output_func(adevs_bag<PortValue>& y);
                /// Output value garbage collection.
                void gc_output(adevs_bag<PortValue>& g);
                /// Destructor.
                ~generator();
                /// Model output port.
                static const port_t arrive;

        private:        
                /// List of waiting customers.
                std::ifstream input_strm;
                /// Next customer.
                customer next;
                /// Schedule the next customer arrival event.
                void schedule_next_output();
}; 

The dynamic behavior of this model is very simple. The state initialization method simply resets the file pointer to the beginning of the file and then schedules the first output event. The implementation of the init() method and its supporting schedule_next_output() method are shown below. If the end of the customer arrival table is reached, the model will set its time of next event to infinity. Otherwise, we read the next record from the file and schedule an event at the appropriate time in the future.

void generator::init()
{
        input_strm.seekg(0,ios::beg);
        schedule_next_output();
}

void generator::schedule_next_output()
{
        double tnext = 0.0;
        input_strm >> tnext >> next.twait;
        if (input_strm.eof())
        {
                passivate();
        }
        else
        {
                hold(tnext - timeCurrent());
        }
}

Since the generator is input free, the external transition function is empty. Similarly, the confluent transition function merely calls the internal transition function.

void generator::delta_ext(stime_t e, const adevs_bag<PortValue>& x)
{
        /// The generator is input free, and so it ignores external events.
}

void generator::delta_conf(const adevs_bag<PortValue>& x)
{
        /// The generator is input free, and so it ignores input.
        delta_int();
}

The effect of an internal event (i.e., an event scheduled for the generator by itself) is to first place the arriving customer on the generator's “arrive” output port. This is done by the output function.

void generator::output_func(adevs_bag<PortValue>& y)
{
        output(arrive,next.clone(),y);
}

After the generator has produced this output event, its internal transition function schedules the next customer arrival using the schedule_next_output() method.

void generator::delta_int()
{
        schedule_next_output();
}

By adding print statements at the very end of the internal, external, and output functions for the clerk model, we can watch the production of customers by the generator. Here is the execution trace produced by the input data shown in Table 1.

Computed the output function at t = 1
A customer has arrived!
Computed the internal transition function at t = 1
The next customer will arrive at t = 2.
Computed the output function at t = 2
A customer has arrived!
Computed the internal transition function at t = 2
The next customer will arrive at t = 3.
Computed the output function at t = 3
A customer has arrived!
Computed the internal transition function at t = 3
The next customer will arrive at t = 5.
Computed the output function at t = 5
A customer has arrived!
Computed the internal transition function at t = 5
The next customer will arrive at t = 7.
Computed the output function at t = 7
A customer has arrived!
Computed the internal transition function at t = 7
The next customer will arrive at t = 8.
Computed the output function at t = 8
A customer has arrived!
Computed the internal transition function at t = 8
The next customer will arrive at t = 10.
Computed the output function at t = 10
A customer has arrived!
Computed the internal transition function at t = 10
The next customer will arrive at t = 11.
Computed the output function at t = 11
A customer has arrived!
Computed the internal transition function at t = 11
The next customer will arrive at t = 1.79769e+308.

A comparison of this execution trace with the execution trace produced by the clerk will reveal that, when the generator produces a customer on its “arrive” output port, there is a corresponding appearance of a customer on the clerk's “arrive” input port. This input event, in turn, causes the clerk's external transition function to be activated. The relationship between input and output events can be best understood by viewing the model as two distinct components, the generator and the clerk, that are connected via their input and output ports. This view of the model is depicted in figure 2.


Figure 2. The generator-clerk coupled model.




As figure 2 suggests, output events produced by the generator on its “arrive” port, via the output function, will appear as input events on the clerk's “arrive” port when its external transition function is evaluated. The component models and their interconnections constitute a coupled (or network) model. To create the coupled model depicted above, we need to create an instance of a staticDigraph model that has the generator and clerk as component models. Shown below is the code snippet needed to create the coupled model.

staticDigraph store;
clerk* clrk = new clerk();
generator* genr = new generator(input_data_file);
store.add(clrk);
store.add(genr);
store.couple(genr,genr->arrive,clrk,clrk->arrive);

The store, which consists of the clerk and the customer generator, is a staticDigraph model. First, the components models are created and added as components to the coupled model. Next, the components are interconnected by coupling the “arrive” output port of the generator model to the “arrive” input port of the clerk model. Having created a coupled model which represents the store, all that remains is to perform the simulation. Here is the code necessary to simulate our model of the store.

devssim sim(&store);
sim.run(100.0);

Putting this all together gives the main routine for the simulation program that will generate the execution traces that are shown in the above examples.

#include "customer.h"
#include "clerk.h"
#include "generator.h"
#include "adevs.h"
using namespace std;

int main(int argc, char** argv)
{
   staticDigraph store;
   clerk* clrk = new clerk();
   generator* genr = new generator(argv[1]);
   store.add(clrk);
   store.add(genr);
   store.couple(genr,genr->arrive,clrk,clrk->arrive);
   devssim sim(&store);
   sim.run();
   return 0;
}

We have completed our first adevs simulation program! However, a few of the details have been glossed over. The first question, and an essential one for a programming language without garbage collection, is what happens to the objects that we created as output events of the generator and clerk models? The answer is that each model has a garbage collection method that is called at the end of each simulation cycle. The argument to the garbage collection method is the bag of PortValue objects created as output in the current simulation cycle. In our store example, the atomic models simply delete the customer pointed to by each PortValue object in the garbage list. The implementation of the garbage collection method is shown below. While this listing is for the generator model, the clerk model's gc_output() method is identical.

void generator::gc_output(adevs_bag<PortValue>& g)
{
   for (int i = 0; i < g.getSize(); i++)
   {
      delete g[i].value;
   }
}

A second issue that has been overlooked is how to collect the statistics that were our original objective. One approach is to modify the clerk model so that it writes waiting times to a file as customer's are processed. While this could work, it has the unfortunate effect of cluttering up the clerk model with experiment specific code. A better approach is to have an observer model that is coupled to the “depart” output port of the clerk. The observer can record the desired statistics as it receives customers on its “depart” input port. The advantage of this approach is that we can modify the clerk model to perform the same experiment on different queueing strategies (e.g., we could add a social status to each customer and have the clerk process customers with a high social status first) without changing the experimental setup (i.e., customer generation and data collection). We can also change the experiment (i.e., customer generation and data collection) without changing the clerk model.

Below is a listing of the observer model. The model is driven solely by external events. The effect of an external event is simply to have the model record the time that the customer departed the queue (i.e., the current simulation time) and the amount of time that the customer had waited in line. Here is the observer header file.

#include "adevs.h"
#include "customer.h"
#include <fstream>

class observer: public atomic
{
   public:
      /// Input port for receiving customers leaving the store.
      static const port_t departed;

      /// Constructor.
      observer(const char* results_file):
      atomic(),
      output_strm(results_file)
      {
      }
      /// State initialization function.
      void init();
      /// Internal transition function.
      void delta_int();
      /// External transition function.
      void delta_ext(stime_t e, const adevs_bag<PortValue>& x);
      /// Confluent transition function.
      void delta_conf(const adevs_bag<PortValue>& x);
      /// Output function.
      void output_func(adevs_bag<PortValue>& y);
      /// Output value garbage collection.
      void gc_output(adevs_bag<PortValue>& g);
      /// Destructor.
      ~observer();
      /// Model output port.
      static const port_t depart;

   private:
      /// File for storing info on departing customers.
      std::ofstream output_strm;
};

Below is the observer source file.

#include "observer.h"
using namespace std;

const port_t observer::depart = 0;

void observer::init()
{
        output_strm.seekp(0,ios::beg);
        passivate();
}

void observer::delta_int()
{
        passivate();
}

void observer::delta_ext(stime_t e, const adevs_bag<PortValue>& x)
{
        for (int i = 0; i < x.getSize(); i++)
        {
                const customer* c = dynamic_cast<const customer*>(x.get(i).value);
                output_strm << timeCurrent() << " " << c->tleave - c->tenter << endl;
        }
}

void observer::delta_conf(const adevs_bag<PortValue>& x)
{
        delta_ext(0.0,x);
}

void observer::output_func(adevs_bag<PortValue>& y)
{
}

void observer::gc_output(adevs_bag<PortValue>& g)
{
}

observer::~observer()
{
        output_strm.close();
}

This model is coupled the the “depart” output port of the clerk model in the same manner as before. Here is the main function which constructs the coupled model and executes the simulation. The argument of 100.0 that is passed to the sim objects run method will cause the simulator to cease executing when the store model has a time of next event that is less than or equal to 100.0. That is to say, when the smallest time of next event for any component model is less than or equal to 100.0. If we did not pass this argument to the run method, then the simulator would continue to run until the time of next event was equal to infinity. Of course, given the input sequence shown in table 1, the time of next event will be become infinity when the last customer depart event is executed at time 45. In this case, specifying a simulator end time of 100.0 is unnecessary.

#include "customer.h"
#include "clerk.h"
#include "generator.h"
#include "observer.h"
#include "adevs.h"
using namespace std;
int main(int argc, char** argv)
{
 staticDigraph store;
 clerk* clrk = new clerk();
 generator* genr = new generator(argv[1]);
 observer* obsrv = new observer(argv[2]);
 store.add(clrk);
 store.add(genr);
 store.add(obsrv);
 store.couple(genr,genr->arrive,clrk,clrk->arrive);
 store.couple(clrk,clrk->depart,obsrv,obsrv->depart);
 devssim sim(&store);
 sim.run(100.0);
 return 0;
}

The resulting coupled model can be visualized just as in figure 2, but now we have three model components instead of just two. The three component generator -> clerk -> observer model is shown in figure 3.


Figure 3. The generator/clerk/observer model.




Given the customer arrival data in table 1, the corresponding customer depature and waiting times are shown in table 2. Given this output, we could use a spreadsheet or some other suitable software to find the maximum and average customer wait times.

Customer depart time

Customer wait time

2

0

6

0

10

3

12

5

22

5

42

14

44

32

45

33

Table 2. Customer departure times and wait times.

Again, notice that the customer depature times correspond exactly with the production of customer depature events by the clerk model. These customer depature events are delivered to the observer model via the clerk -> observer coupling shown in figure 3. Each entry in table 2 is the result of executing the external transition function of the observer model. Also notice that the internal and confluent transition functions of the observer model will never be executed. This is because the observer's init() method sets the model's time of next event to inifinity, and the external transition function does not change the model's next event time.

At this point, you have all the basic elements needed to implement DEVS models in adevs. Subsequent sections of this manual expose some of the more sophisticated features of the DEVS modeling formalism and their implementation in the adevs simulation engine. Of particular interest is the use of the external transition function's elapsed time parameter. This feature of the DEVS modeling formalism is essential to the construction of complex discrete event models. This is explored in the section on Atomic models, and the reader is strongly encouraged to pursue it.