Atomic Models

Atomic models are the basic modeling units in the DEVS modeling and simulation framework. The behavior of an atomic model is described by its state transition functions (internal, external, and confluent), its output function, and its time advance function. Within adevs, these aspects of an atomic model are implemented by subclassing the atomic class and implementing the the pure virtual functions that correspond with the internal, external, confluent, and output functions. The time advance function is implemented indirectly via the hold() method call, or it can be specified directly by overriding the virtual ta() method.

We can explore the basic elements of an atomic model by looking more closely at how the atomic base class is realized within the adevs simulation engine. In order to have a familiar working example, consider the clerk model from the introduction. The complete implementation of the clerk model is presented below. Here is the header file.

#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();
}; 

And here is the source file.

#include "clerk.h"
using namespace std;

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();
        }
}

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

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());
        }
}
        
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;
        }
}

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

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::gc_output(adevs_bag<PortValue>& g)
{
        for (int i = 0; i < g.getSize(); i++)
        {
                delete g[i].value;
        }
}

clerk::~clerk()
{
        empty_line();
}

The atomic model base class includes three attributes not directly visible to the clerk subclass. These are the time of next event, time of last event, and the current simulation time. These attributes can be accessed using the methods timeNext(), timeCurrent(), and timeLast(). When the transition functions or output function of the clerk model are called, the current time and time of last event are fixed. The current time is the time at which the state change or output is happening. This is apparent from the implementation of the clerk model, in which the timeCurrent() method is used to obtain the times at which customers arrive and depart the queue.

The time of last event is the time of the previous model state change. That is, it gives the simulation time at which the delta_ext(), delta_int(), or delta_conf() method was activated in the previous simulation cycle. To illustrate this, consider a simulation of the store with the same sequence of customer arrivals that were used in the previous example. This sequence of customer arrivals is shown 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.

Simulation of the clerk model is accomplished via the code snippet shown below. This code fragment works as follows. The variable x is a bag of PortValue objects that are the inputs being injected into the atomic model at the current simulation time. The variable y is a bag of PortValue objects that will hold the model's output values. The variable t is the simulation time. The code snippet is executed once per simulation cycle.

The code snippet first informs the atomic model of the current simuation time. Next, it checks the model's time of next event and looks to see if any input is available. If there is no input and the current time is the model's time of next event, then the model's output function is activated, followed by the internal transition function. If there is input available and the model time of next event is greater than the current time, then the model's external transition function is activated with the available inputs. If there is input available and the model time of next event is equal to the current time, then the model's output function is activated, followed by confluent transition function. The model's time of last event is set following the state transition.

...
model->setTimeCurrent(t)
if (model->timeNext() == t && x.getSize() == 0)
{
   model->output_func(y);
   model->delta_int();
   model->setTimeLast(t);
   model->setTimeNext(t+model->ta());
}
else if (model->timeNext() == t && x.getSize() != 0)
{
   model->output_func(y);
   model->delta_conf(x);
   model->setTimeLast(t);
   model->setTimeNext(t+model->ta());
}
else if (model->timeNext() > t && x.getSize() != 0)
{
   model->delta_ext(model->timeCurrent()-model->timeLast(),x);
   model->setTimeLast(t);
   model->setTimeNext(t+model->ta());
}
...

By adding print statements to the very end of each of the state transition functions and output functions, we can obtain an execution trace of the clerk model. The execution trace resulting from the above customer arrival sequence is shown below. Notice that the time of last event listed in the execution trace is always equal to the time of the previous state change.

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

Unlike the time of last event and current simulation time, the next event time can be modified during a state transition (i.e., in the delta_int(), delta_ext(), and delta_conf() methods) via the hold() method. The effect of calling the hold() method is to set the next event time to the current time plus the argument to the hold method. For example, if the current simulation time is 5, then the effect of calling hold(5) is to set the time of next event to 5 + 5 = 10. The next event time can be obtained by calling the timeNext() method. If you try to call the hold() method with a negative value, then an exception will be generated. This prevents any attempt to have time flow backwards. A value of zero is acceptable, and indicates that the model should produce output and undergoe an internal event immediately (i.e., in the next simulation cycle), and to do this without advancing the simulation clock.

There are three other time related methods that are associated with an atomic model. These are the elapsed() method, the sigma() method, and the ta() method. Each of these returns a time that is derived from the next, current, and last event times. The value retuned by the elapsed() method is equal to the current time minus the last event time (i.e., timeCurrent() - timeLast()). The value returned by the sigma() method is the time of next event minus the current time (i.e., timeNext() - timeCurrent()). The value returned by the ta() method is the last value passed to hold, or whatever is dictated by the current state if the ta() method is specialized by the model. Notice that the elapsed time is fixed during any state transition while the sigma() and ta() values can be modified by the hold() method.

The argument e that is passed to the delta_ext() method is equal to elapsed() when the external transition method is activated by the simulation engine and not from within some other state transition method (e.g., the confluent state transition method). Also note that if the hold() method has not been called during an internal transition, then ta() and elapsed() will be equal and sigma() will return a value of zero. However, during an external transition it will always be true that ta() <= elapsed() and sigma() >= 0. More generally, the sigma() method can be thought of as giving the time remaining before the internal transition and output methods will be activated. The elapsed() method gives the time that has passed since the model has changed state. The ta() method describes the time advance function for the model.

In order to illustrate these concepts, lets define a new clerk model that will interrupt the checkout of one customer in order to more quickly serve customers with very small orders. This new clerk operates as follows. If a customer is being served and another customer arrives whose order can be processed very quickly, then the clerk stops serving the current customer and begins serving the new customer. The clerk will only do this every so often, however. To be precise, lets say that a small order is one that requires no more than a single unit of time to process. Moreover, the clerk will not interrupt the processing of an order more often than every 10 units of time. However, after this time the clerk will again be looking for customers will small orders. If one arrives, then it is handled as before. If one is already in line, then the clerk will take the first such customer process his order.

The new clerk model has two state variables. The first state variable records the amount of time that must elapsed before the clerk is willing to preempt the processing of one customer in order to service a customer with a small order. The second is the list of customers waiting to be served. Here is the header file for the new clerk model, which we will call clerk2.

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

class clerk2: public atomic 
{
        public:
                /// Constructor.
                clerk2():
                atomic(),
                line(),
                preempt(0.0)
                {
                }
                /// 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.
                ~clerk2();
                /// 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;
                //// Time before we can preempt another customer
                double preempt;
                /// Threshold correspond to a 'small' order processing time
                static const double SMALL_ORDER;
                /// Minimum time between preemptions.
                static const double PREEMPT_TIME;
                /// Delete all waiting customers and clear the list.
                void empty_line();
}; 

The output function for clerk2 is identical to the output function for the original clerk. The state initialization method for clerk2 differs from that of the original clerk only by a single line which sets the preempt field to zero. Here is the state initialization function for the clerk2 model.

void clerk2::init()
{
   preempt = 0.0;
   empty_line();
   passivate();
}

The external transition function for the clerk2 model is significantly different than its predecessor. When a new customer arrives, the first thing that needs to be done is to reduce the checkout time of the customer that is currently being processed. This reduction needs to reflect the amount of time that has already been spent on the customer's order, which is the time elapsed since the last state transition. Next, we reduce the preemption wait time by the same amount. For each arriving customer, we record the time at which they enter the line. If the customer has a small checkout time and the preemption wait time has expired, then that customer goes to the front of the line. Notice that this preempts the current customer, who now has the second place in line, and causes the preempt wait time to be reset. Otherwise, the new customer simply goes to the back of the line. Finally, we record the time at which the customer who is now at the front of the line got there. The next customer departure event is then scheduled for the time required to ring up that customer.

void clerk2::delta_ext(double e, const adevs_bag<PortValue>& x)
{
   /// Decrement the waiting time for the current customer
   if (!line.empty())
   {
      line.front()->twait -= e;
   }
   /// Reduce the preempt time
   preempt -= e;
   /// Place new customers into the line
   for (int i = 0; i < x.getSize(); i++)
   {
      cout << "A new customer arrived at t = " << timeCurrent() << endl;
      /// Cast the object* values to a customer* type.
      customer* new_customer =
         new customer(*(dynamic_cast<const customer*>(x.get(i).value)));
      /// Record the time at which the customer enters the line
      new_customer->tenter = timeCurrent();
      /// If the customer has a small order
      if (preempt <= 0.0 && new_customer->twait <= SMALL_ORDER)
      {
         cout << "The new customer has preempted the current one!" << endl;
         /// We won't preempt another customer for at least this long
         preempt = PREEMPT_TIME;
         /// Put the new customer at the front of the line
         line.push_front(new_customer);
      }
      /// otherwise just put the customer at the end of the line
      else
      {
         cout << "The new customer is at the back of the line" << endl;
         line.push_back(new_customer);
      }
   }
   /// Process the first customer in the line
   line.front()->tleave = timeCurrent();
   hold(line.front()->twait);
}

The internal transition function is similar, in many respects, to the external transition function. It begins by decrementing the preempt wait time by the amount of time that has elapsed since the last state transition. The customer that just departed the store via the clerk2 output function is then removed from the front of the queue. If the line is empty, then there is nothing else to do and so the clerk sits idly behind her counter. If the preemption wait time has expired, then the clerk scans the line for the first customer with a small order. If such a customer can be found, that customer is promoted to the front of the line. Finally, the clerk starts ringing up the first customer in her line. This customer is scheduled to depart when they are done being processed and the time at which they left the line is recorded. Here is the internal transition function for the clerk2 model.

void clerk2::delta_int()
{
   /// Update the preemption timer
   preempt -= ta();
   /// 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())
   {
      cout << "The line is empty at t = " << timeCurrent() << endl;
      /// If not, then set our time of next event to infinity.
      passivate();
      return;
   }
   /// If the preemption time has passed, then look for a small
   /// order that can be promoted to the front of the line.
   list<customer*>::iterator i;
   for (i = line.begin(); i != line.end() && preempt <= 0.0; i++)
   {
      if ((*i)->twait <= SMALL_ORDER)
      {
         cout << "A queued customer has a small order at time " <<
         timeCurrent() << endl;
         customer* small_order = *i;
         line.erase(i);
         line.push_front(small_order);
         preempt = PREEMPT_TIME;
         break;
      }
   }
   /// Otherwise, start ringing up the first customer in line.
   /// Schedule our next event when the customer should depart.
   hold(line.front()->twait);
   /// Record the time at which the customer left the line.
   line.front()->tleave = timeCurrent();
}

The last function to implement is the confluent transition function. The clerk2 model has the same confluent transition as the clerk model and so it is not listed again here.

The behavior of the clerk2 model is significantly more complex than that of the clerk model. In order to better understand this behavior, we can replace the clerk model in our original example with the new clerk2 model and perform the same experiment as before. Here is the execution output trace for the clerk2 model in response to the input sequence shown in table 1. This trace was generated using the print statements shown in the source code listings for the clerk2 model.

A new customer arrived at t = 1
The new customer has preempted the current one!
A customer departed at t = 2
The line is empty at t = 2
A new customer arrived at t = 2
The new customer is at the back of the line
A new customer arrived at t = 3
The new customer is at the back of the line
A new customer arrived at t = 5
The new customer is at the back of the line
A customer departed at t = 6
A new customer arrived at t = 7
The new customer is at the back of the line
A new customer arrived at t = 8
The new customer is at the back of the line
A customer departed at t = 10
A new customer arrived at t = 10
The new customer is at the back of the line
A new customer arrived at t = 11
The new customer has preempted the current one!
A customer departed at t = 12
A customer departed at t = 13
A customer departed at t = 23
A customer departed at t = 43
A customer departed at t = 45
The line is empty at t = 45

The evolution of the clerk2's line is depicted below. The formation of the line is the same as with the original clerk until time 11. At that time, a customer with a short checkout time is able to preempt another customer.


Figure 1. The evolution of clerk2's line in response to the customer arrivals shown in Table 1.