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.
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 staticDigraph model is part of the adevs simulation library. The staticDigraph 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 staticDigraph 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(adevs_bag<PortValue>& y) { output(arrive,next.clone(),y); }
The first argument to the output method is the port on which the output value should appear. The second argument to the output method is the output value itself. The third argument is the bag passed to the output_func() method. The output() method creates a PortValue object with the supplied port and value. The effect of connecting the generator's 'arrive' port to the clerk's 'arrive' port will be to cause the external transition function of the clerk model to be activated. The input bag passed to the clerk's delta_ext() method will contain a PortValue object whose port is equal to the clerk's 'arrive' port and whose value is the object passed to the generator output method.
The essential features of the staticDigraph 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 <list> // Number of lines to consider. #define NUM_LINES 2 class decision: public atomic { public: /// Constructor. decision(); /// 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. ~decision(); /// Input port that receives new customers static const port_t decide; /// Input ports that receive customers leaving the two lines static const port_t departures[NUM_LINES]; /// Output ports that produce customers for the two lines static const port_t 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<port_t,object*> > deciding; /// Delete all waiting customers and clear the list. void clear_deciders(); /// Returns the arrive port associated with the shortest line port_t find_shortest_line(); }; #include "decision.h" using namespace std; // Assign identifiers to ports. Assumes NUM_LINES = 2. // The numbers are selected to allow indexing into the // line length and port number arrays. const port_t decision::departures[NUM_LINES] = { 0, 1 }; const port_t decision::arrive[NUM_LINES] = { 0, 1 }; // Inport port for arriving customer that need to make a decision const port_t decision::decide = NUM_LINES; decision::decision(): atomic() { } void decision::init() { // Clear the list of deciders that have joined their lines clear_deciders(); // Set the initial line lengths to zero for (int i = 0; i < NUM_LINES; i++) { line_length[i] = 0; } // Wait for a customer that needs to decide passivate(); } void decision::delta_int() { // Move out all of the deciders deciding.clear(); // Wait for a new customer passivate(); } void decision::delta_ext(stime_t e, const adevs_bag<PortValue>& x) { // Assign new arrivals to a line and update the line length port_iterator new_customers(x,decide); for (; !new_customers.end_of_input(); new_customers++) { pair<int,object*> p(find_shortest_line(),(*new_customers)->clone()); 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++) { port_iterator old_customers(x,i); for (; !old_customers.end_of_input(); old_customers++) { line_length[i]--; } } // If there are customers getting into line, then produce output // immediately. if (!deciding.empty()) { hold(0.0); } // Otherwise, wait for another customer else { passivate(); } } void decision::delta_conf(const adevs_bag<PortValue>& x) { delta_int(); delta_ext(0.0,x); } void decision::output_func(adevs_bag<PortValue>& y) { // Send all customers to their lines list<pair<port_t,object*> >::iterator i = deciding.begin(); for (; i != deciding.end(); i++) { output((*i).first,(*i).second,y); } } void decision::gc_output(adevs_bag<PortValue>& g) { for (int i = 0; i < g.getSize(); i++) { delete g[i].value; } } decision::~decision() { clear_deciders(); } void decision::clear_deciders() { list<pair<port_t,object*> >::iterator i = deciding.begin(); for (; i != deciding.end(); i++) { delete (*i).second; } deciding.clear(); } port_t decision::find_shortest_line() { port_t shortest = 0; for (port_t 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., 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 derived a new class from the staticDigraph class. The constructor of the new class creates and adds the component models and establishes there 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 multi_clerk: public staticDigraph { public: // Model input port static const port_t arrive; // Model output port static const port_t depart; // Constructor. multi_clerk(); // Destructor. ~multi_clerk(); };
And here is the source file.
#include "multi_clerk.h" using namespace std; // Assign identifiers to I/O ports const port_t multi_clerk::arrive = 0; const port_t multi_clerk::depart = 1; multi_clerk::multi_clerk(): staticDigraph() { // 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); } } multi_clerk::~multi_clerk() { }
Notice that the multi_clerk 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.