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. For this purpose, a Customer class is created Customer objects will need two attributes. One of these 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 two 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 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"

/**
A Busy-Mart customer.
*/
struct Customer
{
	/// Time needed for the clerk to process the customer
	double twait;
	/// Time that the customer entered and left the queue
	double tenter, tleave;
};

/// Create an abbreviation for the Clerk's input/output type.
typedef adevs::PortValue<Customer*> IO_Type;

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 the type of object that the clerk can consume and produce. For this model, we use PortValue objects. The port value class is part of the Digraph model class, which will be introduced later.For the moment, it is enough to know that the PortValue class describes a port/value pair. Ports are used by the Digraph class to connect models together. Lets suppose that customers arrive in line via an “arrive” port and leave via a “depart” port. The associated value object is a Customer.

The second thing that we need to determine is the set of state variables that 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>

/**
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;
};

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 transition, output, time advance, 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 template argument of the base class is used to define the Clerk's input/output type. The Clerk state variables are defined as private class attributes.

The port variables arrive and depart are assigned class unique integers. Typically, the ports for a model are simply numbered in a way that corresponds with the order in which they are listed.

 
// Assign locally unique identifiers to the ports
const int Clerk::arrive = 0;
const int Clerk::depart = 1;

The Clerk constructor places the Clerk into her initial state. For our experiment, this state is an empty line and the Clerk's clock is initialized to zero.

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
{
}

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.

The clerk's local clock is updated by adding the elapsed time to the current value of the clock. Next, the spent working on the current customer's order is updated by adding the elapsed time to the time spent so far. After doing this, the input events are processed. 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 the Customer which arrived. The arrival time of each arriving customer is recorded and then the customer is placed at the back of the line.

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

The time at which the clerk will finish processing a customer is described by the model's time advance function. The time advance function describes the amount of time that should elapsed before the clerk next does anything, barring an input arriving in the interim. In this case, the clerk's time advance function is very simple. If there are not Customers in line, then the Clerk will not do anything in the abscence of input, and so the time advance function returns infinity (here, represented by DBL_MAX). Otherwise, the clerk will wait until the first customer has been rung up (i.e., for the Customer's twait minus t_spent).

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

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 (i.e., the time of last event + ta()).

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. The output function and internal transition function for the clerk model are shown below.

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::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;
}

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 Bag<IO_Type>& xb)
{
	delta_int();
	delta_ext(0.0,xb);
}
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.

Clerk: Computed the external transition function at t = 1
Clerk: There are 1 customers waiting.
Clerk: The next customer will leave at t = 2.
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.
Clerk: Computed the external transition function at t = 3
Clerk: There are 2 customers waiting.
Clerk: The next customer will leave at t = 6.
Clerk: Computed the external transition function at t = 5
Clerk: There are 3 customers waiting.
Clerk: The next customer will leave at t = 6.
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.
Clerk: Computed the external transition function at t = 7
Clerk: There are 3 customers waiting.
Clerk: The next customer will leave at t = 10.
Clerk: Computed the external transition function at t = 8
Clerk: There are 4 customers waiting.
Clerk: The next customer will leave at t = 10.
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.
Clerk: Computed the external transition function at t = 11
Clerk: There are 5 customers waiting.
Clerk: The next customer will leave at t = 12.
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.
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.
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.
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.
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.

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 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 Customers that will arrive at the store. Here is the header file for the generator atomic model.

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

/**
This class produces Customers according to a schedule that
is provided via an input file.
*/
class Generator: public adevs::Atomic<IO_Type> 
{
	public:
		/// Constructor.
		Generator(const char* data_file);
		/// 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.
		~Generator();
		/// Model output port.
		static const int arrive;

	private:	
		/// List of arriving customers.
		std::list<Customer*> arrivals;
}; 

The dynamic behavior of this model is very simple. The constructor simply opens the file containing the arrival schedule and uses it to create a list of customers. The interarrival times of the customers are stored in the tenter fields.

// Assign a locally unique number to the arrival port
const int Generator::arrive = 0;

Generator::Generator(const char* sched_file):
Atomic<IO_Type>()
{
	// Open the file containing the schedule
	fstream input_strm(sched_file);
	// Store the arrivals in a list
	double next_arrival_time = 0.0;
	double last_arrival_time = 0.0;
	while (true)
	{
		Customer* customer = new Customer;
		input_strm >> next_arrival_time >> customer->twait;
		// Check for end of file
		if (input_strm.eof())
		{
			delete customer;
			break;
		}
		// The entry time holds the inter arrival times, not the
		// absolute entry time.
		customer->tenter = next_arrival_time-last_arrival_time;
		// Put the customer at the back of the line
		arrivals.push_back(customer);
		last_arrival_time = next_arrival_time;
	}
}

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(double e, const Bag<IO_Type>& xb)
{
	/// The generator is input free, and so it ignores external events.
}

void Generator::delta_conf(const Bag<IO_Type>& xb)
{
	/// 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(Bag<IO_Type>& yb)
{
	// First customer in the list is produced as output
	IO_Type output(arrive,arrivals.front());
	yb.insert(output);;
}

After the generator has produced this output event, its internal transition function removes the newly arrived customer from the arrival list.

void Generator::delta_int()
{
	// Remove the first customer.  Because it was used as the
	// output object, it will be deleted during the gc_output()
	// method call at the end of the simulation cycle.
	arrivals.pop_front();
}

The scheduling of internal events is accomplished with the time advance function, which simply returns the time until the next customer arrival (remember that the tarrival field contains the interarrival times, not the absolute arrival times).

double Generator::ta()
{
	// If there are not more customers, next event time is infinity
	if (arrivals.empty()) return DBL_MAX;
	// Otherwise, wait until the next arrival
	return arrivals.front()->tenter;
}

To conduct the simulation experiment, the generator output is coupled to the customer input. By doing this, Customer's apprearing on the generator's “arrive” output port cause 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 Digraph model that has the generator and clerk as component models. Shown below is the code snippet needed to create the coupled model.

// Create a digraph model whose components use PortValue
// objects as input and output objects.
adevs::Digraph store;
// Create and add the component models
Clerk* clrk = new Clerk();
Generator* genr = new Generator(argv[1]);
store.add(clrk);
store.add(genr);
// Couple the components
store.couple(genr,genr->arrive,clrk,clrk->arrive);

The store, which consists of the clerk and the customer generator, is a Digraph 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.

// Create a simulator and run until its done
adevs::Simulator<IO_Type> sim(&store);
while (sim.nextEventTime() < DBL_MAX)
{
	sim.execNextEvent();
}

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 "Clerk.h"
#include "Generator.h"
#include <iostream>
using namespace std;

int main(int argc, char** argv)
{
	if (argc != 2)
	{
		cout << "Need an input file!" << endl;
		return 1;
	}
	// Create a digraph model whose components use PortValue<Customer*>
	// objects as input and output objects.
	adevs::Digraph<Customer*> store;
	// Create and add the component models
	Clerk* clrk = new Clerk();
	Generator* genr = new Generator(argv[1]);
	store.add(clrk);
	store.add(genr);
	// Couple the components
	store.couple(genr,genr->arrive,clrk,clrk->arrive);
	// Create a simulator and run until its done
	adevs::Simulator<IO_Type> sim(&store);
	while (sim.nextEventTime() < DBL_MAX)
	{
		sim.execNextEvent();
	}
	// Done, component models are deleted when the Digraph is
	// deleted.
	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 every simulation cycle. The argument to the garbage collection method is the bag of 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(Bag<IO_Type>& g)
{
	// Delete the customer that was produced as output
	Bag<IO_Type>::iterator i;
	for (i = g.begin(); i != g.end(); i++)
	{
		delete (*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 

/**
The Observer records performance statistics for a Clerk model
based on its observable output.
*/
class Observer: public adevs::Atomic<IO_Type>
{
	public:
		/// Input port for receiving customers that leave the store.
		static const int departed;

		/// Constructor. Results are written to the specified file.
		Observer(const char* results_file);
		/// 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.
		~Observer();

	private:	
		/// File for storing information about departing customers.
		std::ofstream output_strm;
}; 

Below is the observer source file.

#include "Observer.h"
using namespace std;
using namespace adevs;

// Assign a locally unique number to the input port
const int Observer::departed = 0;

Observer::Observer(const char* output_file):
Atomic<IO_Type>(),
output_strm(output_file)
{
	// Write a header describing the data fields
	output_strm << "# Col 1: Time customer enters the line" << endl;
	output_strm << "# Col 2: Time required for customer checkout" << endl;
	output_strm << "# Col 3: Time customer leaves the store" << endl;
	output_strm << "# Col 4: Time spent waiting in line" << endl;
}

double Observer::ta()
{
	// The Observer has no autonomous behavior, so its next event
	// time is always infinity.
	return DBL_MAX;
}

void Observer::delta_int()
{
	// The Observer has no autonomous behavior, so do nothing
}

void Observer::delta_ext(double e, const Bag<IO_Type>& xb)
{
	// Record the times at which the customer left the line and the
	// time spent in it.
	Bag<IO_Type>::const_iterator i;
	for (i = xb.begin(); i != xb.end(); i++)
	{
		const Customer* c = (*i).value;
		// Compute the time spent waiting in line 
		double waiting_time = (c->tleave-c->tenter)-c->twait;
		// Dump stats to a file
		output_strm << c->tenter << " " << c->twait << " " << c->tleave << " " << waiting_time << endl;
	}
}

void Observer::delta_conf(const Bag<IO_Type>& xb)
{
	// The Observer has no autonomous behavior, so do nothing
}

void Observer::output_func(Bag<IO_Type>& yb)
{
	// The Observer produces no output, so do nothing
}

void Observer::gc_output(Bag<IO_Type>& g)
{
	// The Observer produces no output, so do nothing
}

Observer::~Observer()
{
	// Close the statistics file
	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 until the there are no scheduled state changes.

#include "Clerk.h"
#include "Generator.h"
#include "Observer.h"
#include <iostream>
using namespace std;

int main(int argc, char** argv)
{
	if (argc != 3)
	{
		cout << "Need input and output files!" << endl;
		return 1;
	}
	// Create a digraph model whose components use PortValue<Customer*>
	// objects as input and output objects.
	adevs::Digraph<Customer*> store;
	// Create and add the component models
	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);
	// Couple the components
	store.couple(genr,genr->arrive,clrk,clrk->arrive);
	store.couple(clrk,clrk->depart,obsrv,obsrv->departed);
	// Create a simulator and run until its done
	adevs::Simulator<IO_Type> sim(&store);
	while (sim.nextEventTime() < DBL_MAX)
	{
		sim.execNextEvent();
	}
	// Done, component models are deleted when the Digraph is
	// deleted.
	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 ta() method always returns infinity.

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.