Block Diagram Models

As we saw in the introductory sections, atomic models can be combined to produce more complex models. We can also reuse multi-component models as components in new multi-component models. In this way, modular and multi-level models can be constructed. This section introduces block diagram, or directed graph, multi-component models. The model of the convenience store, developed in the introduction, provided our first example of a block diagram model. The code used to construct the convenience store model is shown below.

// Create a digraph model whose components use PortValue
// 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);

The Digraph model is part of the adevs simulation library. All models that are part of a Digraph must use adevs::PortValue objects as their input and output types. The Digraph model is a template class with two template parameters. The first is the type of object that will be used as a Value in a PortValue object. The second parameter is the type of object that will be used as a Port in the PortValue object. The Port parameter is of type 'int' be default.

The Digraph model has two primary methods. The add() method is used to add component models to the block diagram model. The couple() method is used to add connections to the Digraph model. The first two arguments to the couple method are the source model and source port. The second two arguments are the destination model and the destination port. The block diagram model that corresponds to the above code snippet is shown below.


Figure 1. A two component block-diagram model.



The effect of coupling a source model to a destination model is to have outputs of the source model placed onto the source port appear as inputs to the destination model on the destination port. To illustrate this, consider the output function of the generator model shown in figure 1.

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

This will place an output value of type Customer* on the arrive output port of the Generator. A corresponding PortValue object will appear in the input bag of the clerk. The value of this PortValue object will point the the Customer* object provided by the Generator, and the port be equal to the clerk's arrive port.

The essential features of the Digraph model have already been illustrated in our model of the convenience store. One last feature remains to be demonstated, and that is hierarchical model construction. To motivate our example of hierarchical model construction, suppose that we wanted to model a convenience store that has two checkout clerks. When customers are ready to pay their bill, they look for the line with the smallest number of people and then enter that line. We can reuse the clerk, generator, and observer models to build this new model. The only missing atomic component is a model of the decision process. Shown below are the header and source code for the decision process model. The model has two output ports, one for each line. There are three input ports. One of these accepts new customers. The others are used to keep track of the number of customers in the each line. The state transition and output functions should be self explanatory.

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

// Number of lines to consider.
#define NUM_LINES 2

class Decision: public adevs::Atomic<IO_Type>
{
	public:
		/// Constructor.
		Decision();
		/// Internal transition function.
		void delta_int();
		/// External transition function.
		void delta_ext(double e, const adevs::Bag<IO_Type>& x);
		/// Confluent transition function.
		void delta_conf(const adevs::Bag<IO_Type>& x);
		/// Output function.  
		void output_func(adevs::Bag<IO_Type>& y);
		/// Time advance function.
		double ta();
		/// Output value garbage collection.
		void gc_output(adevs::Bag<IO_Type>& g);
		/// Destructor.
		~Decision();
		/// Input port that receives new customers
		static const int decide;
		/// Input ports that receive customers leaving the two lines
		static const int departures[NUM_LINES];
		/// Output ports that produce customers for the two lines
		static const int arrive[NUM_LINES];

	private:
		/// Lengths of the two lines
		int line_length[NUM_LINES];
		/// List of deciding customers and their decision.
		std::list<std::pair<int,Customer*> > deciding;
		/// Delete all waiting customers and clear the list.
		void clear_deciders();
		/// Returns the arrive port associated with the shortest line
		int find_shortest_line();
};


#include "Decision.h"
#include <iostream>
using namespace std;
using namespace adevs;

// Assign identifiers to ports.  Assumes NUM_LINES = 2.
// The numbers are selected to allow indexing into the
// line length and port number arrays.
const int Decision::departures[NUM_LINES] = { 0, 1 };
const int Decision::arrive[NUM_LINES] = { 0, 1 };
// Inport port for arriving customer that need to make a decision
const int Decision::decide = NUM_LINES;

Decision::Decision():
Atomic<IO_Type>()
{
	// Set the initial line lengths to zero
	for (int i = 0; i < NUM_LINES; i++)
	{
		line_length[i] = 0;
	}
}

void Decision::delta_int()
{
	// Move out all of the deciders
	deciding.clear();
}

void Decision::delta_ext(double e, const Bag<IO_Type>& x)
{
	// Assign new arrivals to a line and update the line length
	Bag<IO_Type>::const_iterator iter = x.begin();
	for (; iter != x.end(); iter++)
	{
		if ((*iter).port == decide)
		{
			int line_choice = find_shortest_line();
			Customer* customer = new Customer(*((*iter).value));
			pair<int,Customer*> p(line_choice,customer);
			deciding.push_back(p);
			line_length[p.first]++;
		}
	}
	// Decrement the length of lines that had customers leave
	for (int i = 0; i < NUM_LINES; i++)
	{
		iter = x.begin();
		for (; iter != x.end(); iter++)
		{
			if ((*iter).port < NUM_LINES)
			{
				line_length[(*iter).port]--;
			}
		}
	}
}

void Decision::delta_conf(const Bag<IO_Type>& x)
{
	delta_int();
	delta_ext(0.0,x);
}

double Decision::ta()
{
	// If there are customers getting into line, then produce output
	// immediately.
	if (!deciding.empty())
	{
		return 0.0;
	}
	// Otherwise, wait for another customer
	else
	{
		return DBL_MAX;
	}
}
		
void Decision::output_func(Bag<IO_Type>& y)
{
	// Send all customers to their lines
	list<pair<int,Customer*> >::iterator i = deciding.begin();
	for (; i != deciding.end(); i++)
	{
		IO_Type event((*i).first,(*i).second);
		y.insert(event);
	}
}

void Decision::gc_output(Bag<IO_Type>& g)
{
	Bag<IO_Type>::iterator iter = g.begin();
	for (; iter != g.end(); iter++)
	{
		delete (*iter).value;
	}
}

Decision::~Decision()
{
	clear_deciders();
}

void Decision::clear_deciders()
{
	list<pair<int,Customer*> >::iterator i = deciding.begin();
	for (; i != deciding.end(); i++)
	{
		delete (*i).second;
	}
	deciding.clear();
}

int Decision::find_shortest_line()
{
	int shortest = 0;
	for (int i = 0; i < NUM_LINES; i++)
	{
		if (line_length[shortest] > line_length[i])
		{
			shortest = i;
		}
	}
	return shortest;
}

A multi-clerk model that incorporates the customer decision procedure can be constructed using the block diagram model shown below. The external interface for this block diagram model is identical to the atomic clerk models (i.e., the clerk and clerk2 models), and so we can use the generator and observer models to conduct the same experiments as before. The block diagram model of the convenience store has three components. These are the model of the decision process and two clerk models. The external “arrive” input of the multi-clerk model is connected to the “decide” input of the decision model. The “depart” output ports of each of the clerk models is connected to the external “arrive” output port of the multi-clerk model. The decision process has two ports, each one producing customers for a distinct clerk. These output ports are coupled to the “arrive” port of the appropriate clerk model. The “depart” output ports are then coupled to the appropriate “departure” port of the decision model.


Figure 2. Component models and their interconnections in the multi-clerk convenience store model.




The multi-clerk model is implemented by deriving a new class from the Digraph class. The constructor of the new class creates and adds the component models and establishes their interconnections. The multi_clerk model can be used in place of both the clerk and clerk2 models since it exports the same interface (i.e., has the same input and output ports). Here is the header file for the new multi-clerk model.

#include "adevs.h"
#include "Clerk.h"
#include "Decision.h"

/**
A model of a store with multiple clerks and a "shortest line"
decision process for customers.
*/
class MultiClerk: public adevs::Digraph<Customer*>
{
	public:
		// Model input port
		static const int arrive;
		// Model output port
		static const int depart;
		// Constructor.
		MultiClerk();
		// Destructor.
		~MultiClerk();
};

And here is the source file.

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

// Assign identifiers to I/O ports
const int MultiClerk::arrive = 0;
const int MultiClerk::depart = 1;

MultiClerk::MultiClerk():
Digraph<Customer*>()
{
	// Create and add component models
	Decision* d = new Decision();
	add(d);
	Clerk* c[NUM_LINES];
	for (int i = 0; i < NUM_LINES; i++)
	{
		c[i] = new Clerk();
		add(c[i]);
	}
	// Create model connections
	couple(this,this->arrive,d,d->decide);
	for (int i = 0; i < NUM_LINES; i++)
	{
		couple(d,d->arrive[i],c[i],c[i]->arrive);
		couple(c[i],c[i]->depart,d,d->departures[i]);
		couple(c[i],c[i]->depart,this,this->depart);
	}
}

MultiClerk::~MultiClerk()
{
}

Notice that the MultiClerk destructor does not delete the component models. This is because the component models are adopted by the base class when they are added to the Digraph via the add() method. Consequently, the component models are deleted by the base class destructor, rather than the destructor of the derived class.