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 methods that correspond with the internal, external, confluent, output functions, and time advance functions.
The state of an atomic model is represented by attributes of the class that implements the model. The internal transition function describes how the system state evolves in the absense of input. The time advance function is used to schedule internal changes of state, and the output function gives the model output when these internal events occur. The external function describes how the system state changes in response to input. The confluent function resolves the case where an internal and external event coincide. The types of objects that can be accepted as input and output are specified with a template argument to the Atomic base class.
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> /** The Clerk class is derived from the adevs Atomic class. The Clerk's input/output type is specified using the template parameter of the base class. */ class Clerk: public adevs::Atomic<IO_Type> { public: /// Constructor. Clerk(); /// Internal transition function. void delta_int(); /// External transition function. void delta_ext(double e, const adevs::Bag<IO_Type>& xb); /// Confluent transition function. void delta_conf(const adevs::Bag<IO_Type>& xb); /// Output function. void output_func(adevs::Bag<IO_Type>& yb); /// Time advance function. double ta(); /// Output value garbage collection. void gc_output(adevs::Bag<IO_Type>& g); /// Destructor. ~Clerk(); /// Model input port. static const int arrive; /// Model output port. static const int depart; private: /// The clerk's clock double t; /// List of waiting customers. std::list<Customer*> line; /// Time spent so far on the customer at the front of the line double t_spent; };
And here is the source file.
#include "Clerk.h" #include <iostream> using namespace std; using namespace adevs; // Assign locally unique identifiers to the ports const int Clerk::arrive = 0; const int Clerk::depart = 1; Clerk::Clerk(): Atomic<IO_Type>(), // Initialize the parent Atomic model t(0.0), // Set the clock to zero t_spent(0.0) // No time spent on a customer so far { } void Clerk::delta_ext(double e, const Bag<IO_Type>& xb) { // Print a notice of the external transition cout << "Clerk: Computed the external transition function at t = " << t+e << endl; // Update the clock t += e; // Update the time spent on the customer at the front of the line if (!line.empty()) { t_spent += e; } // Add the new customers to the back of the line. Bag<IO_Type>::const_iterator i = xb.begin(); for (; i != xb.end(); i++) { // Copy the incoming Customer and place it at the back of the line. line.push_back(new Customer(*((*i).value))); // Record the time at which the customer entered the line. line.back()->tenter = t; } // Summarize the model state cout << "Clerk: There are " << line.size() << " customers waiting." << endl; cout << "Clerk: The next customer will leave at t = " << t+ta() << "." << endl; } void Clerk::delta_int() { // Print a notice of the internal transition cout << "Clerk: Computed the internal transition function at t = " << t+ta() << endl; // Update the clock t += ta(); // Reset the spent time t_spent = 0.0; // Remove the departing customer from the front of the line. line.pop_front(); // Summarize the model state cout << "Clerk: There are " << line.size() << " customers waiting." << endl; cout << "Clerk: The next customer will leave at t = " << t+ta() << "." << endl; } void Clerk::delta_conf(const Bag<IO_Type>& xb) { delta_int(); delta_ext(0.0,xb); } void Clerk::output_func(Bag<IO_Type>& yb) { // Get the departing customer Customer* leaving = line.front(); // Set the departure time leaving->tleave = t + ta(); // Eject the customer IO_Type y(depart,leaving); yb.insert(y); // Print a notice of the departure cout << "Clerk: Computed the output function at t = " << t+ta() << endl; cout << "Clerk: A customer just departed!" << endl; } double Clerk::ta() { // If the list is empty, then next event is at inf if (line.empty()) return DBL_MAX; // Otherwise, return the time remaining to process the current customer return line.front()->twait-t_spent; } void Clerk::gc_output(Bag<IO_Type>& g) { // Delete the outgoing customer objects Bag<IO_Type>::iterator i; for (i = g.begin(); i != g.end(); i++) { delete (*i).value; } } Clerk::~Clerk() { // Delete anything remaining in the customer queue list<Customer*>::iterator i; for (i = line.begin(); i != line.end(); i++) { delete *i; } }
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.
Table 1 describes an input sequence that will be applied to the Clerk model. The model denote the Atomic model that is being simulated, and let x be the sequence of time stamped inputs that are going to be applied to the model. The variable y is a bag that stores that contains the model output at each simulution cycle. In a multi-component simulation, this would be used to generate inputs for othe models. The behavior of the model is simulated using the algorithm shown below.
t := 0 while (model->ta() != DBL_MAX or x is not empty) tL := t t := minimum of model->ta()+t and time of the first input in x clear y if (t = model->ta()+tL and t < the time of the first input in x) model->output_func(y) model->delta_int() else if (t = model->ta()+tL and t = the time of the first input in x) input := the first input in x remove the first input from x model->output_func(y) model->delta_conf(input) else if (t < model->ta()) input := the first input in x remove the first input from x model->delta_ext(input time - tL, input) endif endwhile
The main simulation loop is repeated until there are not more internal or external events left to process. Within each execution of the loop, the algorithm first remembers the last event time. The next event time is take as the smallest internal event time (i.e., tL+ta()) and next input time. If the next event time is earlier than the next input time, then the model output is computed and the next state of the model is computed using the internal transition time. If the next event time is equal to both the model's internal transition time and next input time, then the output is computed and the next state is computed using the confluent transition function. Otherwise, the next input occurs before the internal transition time, and so the next state is computed using the external transition function. The arguments to the external transition function are the time elapsed since the model last changed state and the input event.
The execution trace resulting from the above customer arrival sequence is shown below. It has been broken up to show where each simulation cycle begins and ends, and the type of event occurring in each cycle.
-External event---------------------------------------------- Clerk: Computed the external transition function at t = 1 Clerk: There are 1 customers waiting. Clerk: The next customer will leave at t = 2. -Confluent event---------------------------------------------- Clerk: Computed the output function at t = 2 Clerk: A customer just departed! Clerk: Computed the internal transition function at t = 2 Clerk: There are 0 customers waiting. Clerk: The next customer will leave at t = 1.79769e+308. Clerk: Computed the external transition function at t = 2 Clerk: There are 1 customers waiting. Clerk: The next customer will leave at t = 6. -External event---------------------------------------------- Clerk: Computed the external transition function at t = 3 Clerk: There are 2 customers waiting. Clerk: The next customer will leave at t = 6. -External event---------------------------------------------- Clerk: Computed the external transition function at t = 5 Clerk: There are 3 customers waiting. Clerk: The next customer will leave at t = 6. -Internal event---------------------------------------------- Clerk: Computed the output function at t = 6 Clerk: A customer just departed! Clerk: Computed the internal transition function at t = 6 Clerk: There are 2 customers waiting. Clerk: The next customer will leave at t = 10. -External event---------------------------------------------- Clerk: Computed the external transition function at t = 7 Clerk: There are 3 customers waiting. Clerk: The next customer will leave at t = 10. -External event---------------------------------------------- Clerk: Computed the external transition function at t = 8 Clerk: There are 4 customers waiting. Clerk: The next customer will leave at t = 10. -Confluent event---------------------------------------------- Clerk: Computed the output function at t = 10 Clerk: A customer just departed! Clerk: Computed the internal transition function at t = 10 Clerk: There are 3 customers waiting. Clerk: The next customer will leave at t = 12. Clerk: Computed the external transition function at t = 10 Clerk: There are 4 customers waiting. Clerk: The next customer will leave at t = 12. -External event---------------------------------------------- Clerk: Computed the external transition function at t = 11 Clerk: There are 5 customers waiting. Clerk: The next customer will leave at t = 12. -Internal event---------------------------------------------- Clerk: Computed the output function at t = 12 Clerk: A customer just departed! Clerk: Computed the internal transition function at t = 12 Clerk: There are 4 customers waiting. Clerk: The next customer will leave at t = 22. -Internal event---------------------------------------------- Clerk: Computed the output function at t = 22 Clerk: A customer just departed! Clerk: Computed the internal transition function at t = 22 Clerk: There are 3 customers waiting. Clerk: The next customer will leave at t = 42. -Internal Event---------------------------------------------- Clerk: Computed the output function at t = 42 Clerk: A customer just departed! Clerk: Computed the internal transition function at t = 42 Clerk: There are 2 customers waiting. Clerk: The next customer will leave at t = 44. -Internal event---------------------------------------------- Clerk: Computed the output function at t = 44 Clerk: A customer just departed! Clerk: Computed the internal transition function at t = 44 Clerk: There are 1 customers waiting. Clerk: The next customer will leave at t = 45. -Internal event---------------------------------------------- Clerk: Computed the output function at t = 45 Clerk: A customer just departed! Clerk: Computed the internal transition function at t = 45 Clerk: There are 0 customers waiting. Clerk: The next customer will leave at t = 1.79769e+308.
To provide another, more complex, example of an atomic model, 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 and 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 adevs::Atomic<IO_Type> { public: /// Constructor. Clerk2(); /// Internal transition function. void delta_int(); /// External transition function. void delta_ext(double e, const adevs::Bag<IO_Type>& xb); /// Confluent transition function. void delta_conf(const adevs::Bag<IO_Type>& xb); /// Time advance function. double ta(); /// Output function. void output_func(adevs::Bag<IO_Type>& yb); /// Output value garbage collection. void gc_output(adevs::Bag<IO_Type>& g); /// Destructor. ~Clerk2(); /// Model input port. static const int arrive; /// Model output port. static const int depart; private: /// Structure for storing information about customers in the line struct customer_info_t { // The customer Customer* customer; // Time remaining to process the customer order double t_left; }; /// List of waiting customers. std::list<customer_info_t> line; //// Time before we can preempt another customer double preempt; /// The clerk's clock double t; /// Threshold correspond to a 'small' order processing time static const double SMALL_ORDER; /// Minimum time between preemptions. static const double PREEMPT_TIME; };
The Clerk2 constructor simply sets the clerks' clock and preempt timer to zero.
Clerk2::Clerk2(): Atomic<IO_Type>(), preempt(0.0), t(0.0) { }The output function for Clerk2 sets the leaving time for the departing customer and then ejects the customer via its depart port.
void Clerk2::output_func(Bag<IO_Type>& yb) { /// Set the exit time of the departing customer line.front().customer->tleave = t+ta(); /// Place the customer at the front of the line onto the depart port. IO_Type y(depart,line.front().customer); yb.insert(y); // Report the departure cout << "Clerk: A customer departed at t = " << t+ta() << endl; }
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.
void Clerk2::delta_ext(double e, const Bag<IO_Type>& xb) { /// Update the clock t += e; /// Update the time spent working on the current order if (!line.empty()) { line.front().t_left -= e; } /// Reduce the preempt time preempt -= e; /// Place new customers into the line Bag<IO_Type>::const_iterator iter = xb.begin(); for (; iter != xb.end(); iter++) { cout << "Clerk: A new customer arrived at t = " << t << endl; /// Create a copy of the incoming customer and set the entry time customer_info_t c; c.customer = new Customer(*((*iter).value)); c.t_left = c.customer->twait; /// Record the time at which the customer enters the line c.customer->tenter = t; /// If the customer has a small order if (preempt <= 0.0 && c.t_left <= SMALL_ORDER) { cout << "Clerk: 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(c); } /// otherwise just put the customer at the end of the line else { cout << "Clerk: The new customer is at the back of the line" << endl; line.push_back(c); } } }
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 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. Here is the internal transition function for the Clerk2 model.
void Clerk2::delta_int() { // Update the clerk's clock t += ta(); // 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 << "Clerk: The line is empty at t = " << t << endl; return; } // If the preemption time has passed, then look for a small // order that can be promoted to the front of the line. list::iterator i; for (i = line.begin(); i != line.end() && preempt <= 0.0; i++) { if ((*i).t_left <= SMALL_ORDER) { cout << "Clerk: A queued customer has a small order at time " << t << endl; customer_info_t small_order = *i; line.erase(i); line.push_front(small_order); preempt = PREEMPT_TIME; break; } } }
The time advance function returns the time remaining to process the customer that is at the front of the line, or infinity (DBL_MAX) if there are no customers to be processed.
double Clerk2::ta() { // If the line is empty, then there is nothing to do if (line.empty()) return DBL_MAX; // Otherwise, wait until the first customer will leave else return line.front().t_left; }
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.
Clerk: A new customer arrived at t = 1 Clerk: The new customer has preempted the current one! Clerk: A customer departed at t = 2 Clerk: The line is empty at t = 2 Clerk: A new customer arrived at t = 2 Clerk: The new customer is at the back of the line Clerk: A new customer arrived at t = 3 Clerk: The new customer is at the back of the line Clerk: A new customer arrived at t = 5 Clerk: The new customer is at the back of the line Clerk: A customer departed at t = 6 Clerk: A new customer arrived at t = 7 Clerk: The new customer is at the back of the line Clerk: A new customer arrived at t = 8 Clerk: The new customer is at the back of the line Clerk: A customer departed at t = 10 Clerk: A new customer arrived at t = 10 Clerk: The new customer is at the back of the line Clerk: A new customer arrived at t = 11 Clerk: The new customer has preempted the current one! Clerk: A customer departed at t = 12 Clerk: A customer departed at t = 13 Clerk: A customer departed at t = 23 Clerk: A customer departed at t = 43 Clerk: A customer departed at t = 45 Clerk: The line is empty at t = 45
The evolution of the Clerk2 line is depicted below. The formation of the line is the same as that of 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 the Clerk2 line in response to the customer
arrivals shown in Table 1.