Network models consist of three parts. They have input and output ports, just as atomic models do. A network model has a set of component models, each of which can be a network or atomic model. Lastly, the network model has an event routing function (also called a component interface map in the DEVS literature). The event routing function can describe three kinds of component interconnections. Internal connections move output events generated by component models to the input ports of other component models. External input connections move input events appearing at an input port of the network model to the input ports of component models. External output connections move output events generated by component models to the output ports of the network model.
The component set and event routing function are not implemented directly as part of the network model. Instead, each network model has a special atomic model component, called the network executive, that realizes these functions. This extra level of indirection has one important consequence. By making the component set and routing function part of a dynamic atomic model, it becomes possible to change the structure of a network model over the course of a simulation. In effect, the component set and event routing function are part of the network executive's state variable set, and they can be changes just like any other atomic model state variables.
The best way to describe the construction of network models is through an illustration. We will construct a variant of the classic phold discrete event simulation benchmark, but with a twist. Our phold model will be hierarchical, consisting of several interconnected phold networks. Moreover, our phold networks will change over time. This model can be found in the examples/phold directory.
To start, we will define a base class from which all of our model components, both network and atomic, will be partially derived. This base class will contain data structures that we will use later to implement the event routing function. The base class describes a common single input-single output interface that will be used by all of our models. It also contains an adjacency list that is used to describe the phold network. Each model's adjacency list will contain pointers to the components that are connected to the model's output. Here is the definition of the base phold_node class (both the header and source files are shown).
#include "adevs.h" #include <list> /** This is the base class for all models in the phold network. It is used as a 'mixin' or 'interface' class and should be inhereted in tandem with the necessary atomic or devsn base class. */ class phold_node { public: // Interface definition for all phold models static const port_t in; static const port_t out; // Default constructor phold_node(); // Destructor virtual ~phold_node(); /** The adjacency list contains a pointer to every model that is attached to the output of this phold node. */ std::list<devs*> adj; // Global performance statistics static unsigned long event_count; static unsigned long models_created; static unsigned long models_destroyed; }; #include "phold_node.h" using namespace std; const port_t phold_node::in = 0; const port_t phold_node::out = 1; unsigned long phold_node::event_count = 0; unsigned long phold_node::models_created = 0; unsigned long phold_node::models_destroyed = 0; phold_node::phold_node() { models_created++; } phold_node::~phold_node() { models_destroyed++; }
The basic component of the phold benchmark is a simple queue that holds incoming events for a short time and then ejects them. This can be implemented as an atomic model with a random hold time and a FIFO queue. In addition, our model phold queue model will have a flag indicating whether is should create an initial event at the simulation start. This model will use a char as the basic I/O type to avoid the cost of performing object allocation and deletion (and so measure, in some sense, just the event handling and dynamic structure overhead). Here are the header and source files for the phold queue model.
#include "adevs.h" #include "phold_node.h" #include <deque> /** A queue in the phold benchmark. */ class phold_queue: public atomic, public phold_node { public: /** If the constructor flag is true, then this phold_queue will begin the simulation with a token in its queue. */ phold_queue(bool originate_token = false); /// Adevs atomic model methods void init(); void delta_int(); void delta_ext(double e, const adevs_bag<PortValue>& x); void delta_conf(const adevs_bag<PortValue>& x); void output_func(adevs_bag<PortValue>& y); /// No garbage collection is needed since the tokens are primitive types void gc_output(adevs_bag<PortValue>& g){} /// Destructor ~phold_queue(); private: /// The outgoing token queue std::deque<char> q; /// Random variable for determining token hold times rv r; }; #include "phold_queue.h" using namespace std; phold_queue::phold_queue(bool originate_token): atomic(), phold_node() { if (originate_token) { q.push_back(0x00); } } void phold_queue::init() { if (!q.empty()) { hold(r.exp(1.0)); } else { passivate(); } } void phold_queue::delta_int() { event_count++; q.pop_front(); if (!q.empty()) { hold(r.exp(1.0)); } else { passivate(); } } void phold_queue::delta_ext(double e, const adevs_bag<PortValue>& x) { event_count++; if (q.empty()) { hold(r.exp(1.0)); } // Otherwise, continue with tN unchanged else { hold(sigma()); } for (int i = 0; i < x.getSize(); i++) { q.push_back(x.get(i).value); } } void phold_queue::delta_conf(const adevs_bag<PortValue>& x) { delta_int(); delta_ext(0.0,x); // Avoid double counting a confluent event event_count--; } void phold_queue::output_func(adevs_bag<PortValue>& y) { output(out,q.front(),y); } phold_queue::~phold_queue() { }
The network model for our phold benchmark contains two parts. One part is the network model interface, which is derived from the devsn class. This part consists of a single method getNetExec() that returns a pointer to the network executive for this network model. The second part is the network executive.
The network executive is a special kind of atomic model. Network executive models are derived from the netExec class, which in turn is derived from the atomic model class. The network executive inherits the atomic model interface (i.e., the state transition, output, and initialization functions). It has three methods in addition to these. These methods are
getComponents(adevs_set<devs*>& c),
route(const PortValue& pv, devs* model, adevs_bag<EventReceiver>& r), and
gc_models(adevs_set<devs*>& g).
The getComponents() method is called by the simulation engine when it needs to obtain the set of component models that belong to the network executive. The network executive that implements this method should fill the set c with a pointer to every one of it components (excepting the network executive itself and the parent of the network executive). The route() method is called by the simulation engine under two conditions. These are
an input arrives on an input port of the network executive's parent (i.e., an external input event), or
a component model or the network executive itself produces an output event (i.e., an internal event).
The route method is used to move events through the network model. The pv argument contains the port and value of the event that is being routed. The model argument is a pointer to the model that produced the event. If this is an external input event, then the model will point to the parent of the network executive (i.e., model == getParent()). If this is an internal event, then the model will be whichever component (possibly the network executive itself) that generated the event. The network executive should fill the bag r with EventReceiver objects that describe the set of models that will be recipients of the events. There can be any number of recipients. However, the recipients must be a component of the network, the network executive itself, or the parent model. A model can not receive its own output (this will cause an exception to be generated).
The gc_models() method is used to destroy models that were removed from the network during the network executive's state transition. Models can be removed by simply not reporting them during a call to getComponents(). The simulation engine will determine which components were removed from the simulator during a simulation cycle. It will call the gc_models() method of each network executive that removed any of its components (excepting those components that simply migrated from one network to another) at the end of the simulation cycle.
A UML diagram of this arrangement is shown below. Every network model is derived from the devsn class. Every network model has a network executive. The network executive is a special type of atomic model.
The implementation of the phold_network model is given below. The header file is shown first. The phold_network model contains a definition of its network executive. The phold network executive has a state that is defined by its component set, the interconnections of its components, and a random variable that describes how often the model changes it structure. Structure changes are implemented by changing the state of the network executive via its internal state transition function. If the structure change time is infinite, then this is really a static structure model because the structure of the network can only change if the network executive changes state. Here is the header file.
#include "adevs.h" #include "phold_queue.h" /** A phold network consisting of other phold_networks and phold_queues. */ class phold_network: public devsn, public phold_node { public: /** Creates a network with a specified number of atomic nodes, some of which can be token originators. Some of the atomic nodes will be contained in a random number of network subcomponents, rather than being contained directly. */ phold_network(int num_nodes, int num_originators, double net_change_interval = ADEVS_INFINITY): devsn(), phold_node(), nx(num_nodes,num_originators,net_change_interval,this) { } /// Returns the network executive associated with this model netExec* getNetExec() { return &nx; } /// The destructor destroys all of the component models. ~phold_network() { } private: /** The network executive for this network model. It is defined within the context of the network model because it will never exist independently of it. */ class net_exec: public netExec { public: /** Create the network using the same arguments as where passed to the parent model. The network executive needs to know who its parent is in order to implement external input and output connections. */ net_exec(int num_nodes,int num_originators, double network_change_interval, phold_network* parent); /// Atomic model methods void init(); void delta_int(); void delta_ext(double e, const adevs_bag<PortValue>& x); void delta_conf(const adevs_bag<PortValue>& x); void output_func(adevs_bag<PortValue>& y); /// No need for garbage collection because I/O type is primitive void gc_output(adevs_bag<PortValue>& g){} /** These are the network executive specific methods. */ /** Get the set of component models. This method fills the set c with a pointer to every component of the network except the network executive itself and the parent network model. */ void getComponents(adevs_set<devs*>& c); /** This is the event routing function. The method should fill the event receiver bag r with EventReceiver objects that describe the model and port that needs to receive the pv that was generated by the model pointed to by the model argument. */ void route(const PortValue& pv, devs* model, adevs_bag<EventReceiver>& r); /** This method is used to garbage collect models which were removed from the network during the last simulation cycle. Changes in the model set are computed using the set difference of the model set before and after the network executive changes state. This difference is passed the the gc_models() method after those models have been removed from the simulation kernel. At this point, the network executive can delete them. */ void gc_models(adevs_set<devs*>& removed); /// Destructor destroys all of the component models ~net_exec(); private: /// The set of network components adevs_set<devs*> nodes; /// Random variable for scheduling network restructuring rv r; double network_change_interval; /// Build or rebuild the network void build_connections(); }; // end of the net_exec class // Network executive model for the phold_network net_exec nx; };
The guts of the network are in the implementation of the network executive. The route() and getComponent() methods describe the structure of the network to the simulator. The state transition functions and output function of the network executive work just as they do for any other atomic model, except that the network executive's state includes a description of the network structure. The only other addition is a model garbage collection method, gc_models(), that can be used to cleanup any models that were removed from the network executive's component set. The gc_models() method is called at the end of any simulation structure in which the component set of the network executive changed. The garbage set will contain all of the models that were removed from the network, except any models that migrated from one network to another.
Each node in the phold_network has a list of adjacent nodes. There is a node to every node in its adjacency list. The adjacency list of any atomic components can contain other atomic or network components within the same network, or the devsn model that the atomic component belongs to. The adjacency list of any network components can contain atomic and network components within the same network, or any component within the network itself. Each of these possibilities is shown in the figure below.
The
network can change by adding and removing links, adding and removing
nodes at any level, or move nodes from one network to another. The
implementation shown below changes the network at random by the
addition and removal of links and nodes. Changes occur at randomly
selected times.
#include "phold_network.h" #include <vector> #include <iostream> using namespace std; phold_network::net_exec::net_exec(int num_nodes,int num_originators, double net_change_interval,phold_network* parent): netExec(parent), r(new crand()), network_change_interval(net_change_interval) { // Construct the initial set of components while (num_nodes > 0) { // Create a network model 15% of the time if (r.uniform(0.0,1.0) < 0.15) { // Lets not be too picky about accidentally adding an extra model or two int net_size = rand()%num_nodes+2; int net_orig = 0; if (num_originators > 0) { net_orig = rand()%num_originators; } // Knock these numbers off of what is left to do num_originators -= net_orig; num_nodes -= net_size; // Add the new network to the component set of this network nodes.add(new phold_network(net_size,net_orig,network_change_interval)); } // Otherwise just add a new queue else { num_nodes--; nodes.add(new phold_queue(num_originators-- > 0)); } } // Connect the network nodes build_connections(); } void phold_network::net_exec::init() { // If the model has a non-infinite structure change interval, // then schedule the first structure change. if (network_change_interval < ADEVS_INFINITY) { hold(r.exp(network_change_interval)); } // Otherwise, this will be a static structure model else { passivate(); } } void phold_network::net_exec::delta_int() { // Add and remove some models at random. for (int i = 0; i < 2; i++) { int choice = rand()%4; // Remove a node. This node will be passed to the gc_models() // method when it is safe to delete the model. if (choice == 0 && nodes.getSize() > 1) { nodes.remove(0); } // Add a new network model. This model will be initialized // by the simulation engine. else if (choice == 1) { nodes.add(new phold_network(rand()%5+1,rand()%2)); } // A a new atomic model else if (choice == 2) { nodes.add(new phold_queue(rand()%2 == 0)); } } // Rebuild the network topology build_connections(); // Schedule the next structure change hold(r.exp(network_change_interval)); } void phold_network::net_exec::delta_ext(double e, const adevs_bag<PortValue>& x) { // Our network executive won't respond to inputs passivate(); } void phold_network::net_exec::delta_conf(const adevs_bag<PortValue>& x) { // Our network executive does not respond to inputs delta_int(); } void phold_network::net_exec::output_func(adevs_bag<PortValue>& y) { // Our network executive produces no output } void phold_network::net_exec::getComponents(adevs_set<devs*>& c) { // Add all component models (except the network executive and the parent) // to the set c. c.append(nodes); } void phold_network::net_exec::route(const PortValue& pv, devs* model, adevs_bag<EventReceiver>& r) { // External inputs go to a randomly selected model if (model == getParent()) { EventReceiver rx(nodes.get(rand()%nodes.getSize()),phold_node::in); r.add(rx); } // Otherwise, this is an internal event or external output event else { // Cast the source as a phold_node so that we can access its adj list phold_node* src = dynamic_cast<phold_node*>(model); // Generate events at each adjacent model list<devs*>::iterator i = src->adj.begin(); for (; i != src->adj.end(); i++) { // If this is an external output if (*i == getParent()) { EventReceiver rx(*i,phold_node::out); r.add(rx); } // Otherwise it is an internal output to input connection else { EventReceiver rx(*i,phold_node::in); r.add(rx); } } } } void phold_network::net_exec::gc_models(adevs_set<devs*>& removed) { // Delete models that were removed during the last state change for (int i = 0; i < removed.getSize(); i++) { delete removed[i]; } } phold_network::net_exec::~net_exec() { // Delete all of the component models for (int i = 0; i < nodes.getSize(); i++) { delete nodes[i]; } } void phold_network::net_exec::build_connections() { // Build a vector of properly cast nodes vector<phold_node*> p_nodes(nodes.getSize()); // Remove any existing connections for (int i = 0; i < nodes.getSize(); i++) { p_nodes[i] = dynamic_cast<phold_node*>(nodes[i]); p_nodes[i]->adj.clear(); } // One model is connected to the external output p_nodes[rand()%nodes.getSize()]->adj.push_back(getParent()); // Setup the random internal network connections. for (int i = 0; i < nodes.getSize(); i++) { int in_degree = rand()%3+1; for (int k = 0; k < in_degree; k++) { int select = rand()%nodes.getSize(); // Do not connect a model to itself! if (p_nodes[select] != p_nodes[i]) { p_nodes[select]->adj.push_back(nodes[i]); } } int out_degree = rand()%3+1; for (int k = 0; k < out_degree; k++) { int select = rand()%nodes.getSize(); // Do not connect a model to itself! if (p_nodes[select] != p_nodes[i]) { p_nodes[i]->adj.push_back(nodes[select]); } } } // Done! }
This model exercises most of the simulator functionality. Since the costs of performing state changes are relatively small, it probably provides a reasonable indicator of the performance that can be achieved by the simulation engine. Of course, the benchmark is useless for predicting the time that is going to be required to simulate your model. But it does provide a reasonable gauge for measuring performance improvements in the simulation kernel code. That said, here is a table of some benchmark numbers that I managed on my laptop computer (with and AMD Athlon processor, 192 MB of RAM, running M$ Windows XP SP 2, and using the GCC 3.3.3 for Cygwin with the -O3 and -fomit-frame-pointer optimization flag). The structure change interval had a mean value of 100.0 with 10 initial tokens and a simulation stop time of 4000. The random numbers were based on the rand() C function with a seed of 1111201495.
Nodes |
Event rate (events/sec) |
---|---|
100 |
388337 |
500 |
289034 |
1000 |
283457 |
1500 |
248519 |
3000 |
238024 |
There is one more feature of a dynamic structure model that is worth noting. It is possible for models to migrate from one network to another. A migrating model retains its state and all other internal features. However, the model parent will change, as will the set of models that it is connected to. A model is moved by producing the model as the output of one network executive (causing it to leave the network) and then having the model appear as in input at another network executive (which adds it to its structure). A migrating model will not appear in the set of models passed to the gc_model() method of the network executive. The model will appear in the bag passed to the gc_output() method of the network executive that produced the model as output, and so you should take care not to delete it.
A very simple model will serve to illustrate the transfer of a model from one network executive to the next. The model is illustrated below. Initially, the single atomic component is part of network A. The network executive for network A has an initial time advance of 1. At this this time, the atomic component is moved from network A to network B.
The
code that implements this model is shown below. Notice that the
atomic component is scheduled to undergo periodic internal events.
Because the movement of the atomic model does not affect its internal
state, these internal events occur as scheduled.
Here is the code for the atomic component that will be migrated. This atomic model schedules internal events at a fixed rate. It was implemented as a single header file only because the code was very short and simple.
#include "adevs.h" #include <iostream> /** Simple atomic model that will be moved about the network. */ class simple_atomic: public atomic { public: /** Create an atomic model that will undergo periodic internal events, up to the specified limit. */ simple_atomic(double event_interval, int event_limit = 1): atomic(), event_interval(event_interval), event_limit(event_limit) { } /// Atomic model initialization method void init() { hold(event_interval); } /// Atomic internal transition function void delta_int() { event_limit--; std::cout << "Simple atomic event at " << timeCurrent() << std::endl; if (event_limit > 0) { hold(event_interval); } else { passivate(); } } /// Atomic external transition function void delta_ext(ADEVS_TIME_TYPE e, const adevs_bag<PortValue>& x) { // continue with next event time unchanged hold(sigma()); } /// Atomic confluent transition function void delta_conf(const adevs_bag<PortValue>& x) { delta_int(); } /// Atomic output function void output_func(adevs_bag<PortValue>& y) { output(0,new object(),y); } /// Output garbage collection function void gc_output(adevs_bag<PortValue>& g) { for (int i = 0; i < g.getSize(); i++) { delete g[i].value; } } private: double event_interval; int event_limit; };
The network model implementation is shown network. The network executive does not need to be visible outside of the mobile_example class, and so it was defined within the scope of the network model itself.
#include "adevs.h" #include "simple_atomic.h" #include <iostream> /** A network model to illustrate how a component model can be moved from one network to another. */ class mobile_example: public devsn { public: /** Create a network with (or without) an initial model, a model migration time, and parameters to the simple_atomic component that will be migrated. */ mobile_example(bool initial_model, double transfer_time = ADEVS_INFINITY, double simple_output_time = ADEVS_INFINITY, int simple_event_limit = 1): devsn(), nx(initial_model,transfer_time,simple_output_time,simple_event_limit,this) { } /** Get the network executive for this network model. */ netExec* getNetExec() { return &nx; } private: /** Definition of the network executive for the mobile_example network model. */ class mobile_net_exec: public netExec { public: /** Constructor flags indicate whether this model has a component initially, the parameters for the (possible) component, and a pointer to this model's parent. */ mobile_net_exec(bool initial_model, double transfer_time, double simple_output_time,int event_limit,devsn* parent): netExec(parent), event_limit(event_limit), initial_model(initial_model), transfer_time(transfer_time), simple_output_time(simple_output_time) { } /** Atomic model state initialization method. */ void init() { if (initial_model) model = new simple_atomic(simple_output_time,event_limit); else model = NULL; hold(transfer_time); } /** Internal transition function. */ void delta_int() { if (model != NULL) { std::cout << "Sent a model at time " << timeCurrent() << std::endl; } model = NULL; passivate(); } /** External transition function. */ void delta_ext(double e, const adevs_bag<PortValue>& x) { // When a model is received as input, replace the existing model // with the new model. model = dynamic_cast<devs*>(x.get(0).value); std::cout << "Received a model at time " << timeCurrent() << std::endl; hold(transfer_time); } /** Confluent transition function. */ void delta_conf(const adevs_bag<PortValue>& x) { delta_int(); delta_ext(0.0,x); } /** Output function generates the component model (if it exists) as output. */ void output_func(adevs_bag<PortValue>& y) { if (model != NULL) output(0,model,y); } /** Add the compnent to the set c, if the component exists. */ void getComponents(adevs_set<devs*>& c) { if (model != NULL) c.add(model); } /** Delete any models that have migrated. Note that the component model will not appear in this set if it is added to some other model. */ void gc_models(adevs_set<devs*>& g) { for (int i = 0; i < g.getSize(); i++) { delete g[i]; } } /** Output garbage collection. This model only produces other models as output, and so no garbage collection is performed. */ void gc_output(adevs_bag<PortValue>& g) { } /** The event routing function. */ void route(const PortValue& pv, devs* model, adevs_bag<EventReceiver>& r) { // External input if (model == getParent()) { EventReceiver rx(this,0); r.add(rx); } // Output produced by the network executive creates an // external output. else if (model == this) { EventReceiver rx(getParent(),0); r.add(rx); } } /// Destructor ~mobile_net_exec() { if (model != NULL) delete model; } private: devs* model; int event_limit; bool initial_model; double transfer_time, simple_output_time; }; /// The network executive for the mobile_network model. mobile_net_exec nx; };
The main program code and output from a simulation of this model is shown below. Again, notice that the state of the atomic component is not in any way affected by the movement of the model from one network to the next.
Here is the main program code. It creates two mobile_example networks and connects them using the prebuilt staticDigraph network model class.
#include "mobile_example.h" using namespace std; int main() { // Create two networks and move a model from one to the other // The first network has an atomic component at the start. The atomic // component schedule internal events for every 0.8 seconds, and it will // do this 3 times. The network executive will undergo an internal event // in 1 second and produce its component model as an output. mobile_example* t1 = new mobile_example(true,1.0,0.8,3); // The second network has no components initially. mobile_example* t2 = new mobile_example(false); // Connect the two models using the staticDigraph network model. staticDigraph dig; dig.add(t1); dig.add(t2); dig.couple(t1,0,t2,0); dig.couple(t2,0,t1,0); // Run the simulation devssim sim(&dig); sim.run(); }
And here is the output from the simulation run.
Simple atomic event at 0.8 Sent a model at time 1 Received a model at time 1 Simple atomic event at 1.6 Simple atomic event at 2.4