Adevs User's Manual

1 Introduction

The adevs library supports the construction of discrete event simulations in C++. It is based on the Parallel DEVS and DSDEVS (Dynamic Structure DEVS) formalisms. This simulation library was built, primarily, for self-education. Beyond that more general goal, adevs tries to meet the following criteria.

Ease of use. Constructing a simulator should be fairly straightforward. To that end, adevs checks for bad coupling, illegal scheduling, and ensures that all of the necessary output, transition, and initialization functions are implemented by the simulation builder (i.e. functions that are meant to be implemented by the library user are pure virtual functions).

Runtime efficiency. Careful implementation of the abstract simulators using appropriately selected data structures should be able to yield 'reasonable' performance. Gross inefficiencies are avoided wherever possible.

Extensible design. The core simulator should be extensible. To date, adevs has been used in two distributed simulation environments, HLA and an MPI based environment called DSE. Over its lifetime, adevs has been expanded to support dynamic structure models, parallel simulation on shared memory multiprocessors, and hybrid (i.e. mixed continuous and discrete) models.

A detailed treatment of DEVS can be found in Theory of Modelling and Simulation, Second Edition by B.P. Zeigler, H. Praehofer, and T.G. Kim. On-line information about DEVS can be found at http://www.acims.arizona.edu

1.1 The adevs namespace

All of the adevs classes and functions are in the adevs namespace. This was done to prevent name clashes with the STL. If you are not using a conflicting namespace in your application, putting the line using namespace adevs; at the top of your source file will be sufficient. Otherwise, the full name of the adevs classes is adevs::<class name>.

2 Simulating Parallel DEVS models

Models described in the Parallel DEVS formalism are either atomic models or coupled models. Atomic models are the fundamental building blocks of more complex coupled models. The behavior of an atomic model is defined by four functions; internal, external, and confluent transition functions and the output function. Coupled models are constructed from other coupled and atomic models.

The DEVS simulation algorithm is shown in algorithm 1. The compute next state function for an atomic model is shown in algorithm 2. A coupled model's next state is determined by computing the next state of all of its components.

Algorithm 1. DEVS simulation algorithm

initialize every model 
   while (more to do)
      tN := min (next event time of models)
      for each model whose time of next event is tN
         compute output for model
      endfor
      map outputs to inputs
      for each model with an input or whose time of next event is tN
         compute model's next state
      endfor
endwhile

Algorithm 2. Atomic model compute next state function

compute next state (input bag x, time t)
   if (time of next event = t and x is empty)
      compute internal transition function
   else if (time of next event < t and x is not empty)
      compute external transition function
   else if (time of next event = t and x is not empty)
      compute confluent transition function
end compute next state

2.1 Atomic models

Atomic models are described by a set of input and output ports, an output function, and external, internal, and confluent transition functions. The input/output ports of the atomic model are used to receive/send events. The output function changes the values of the output ports.

The internal transition function handles autonomous state changes. The atomic model has a time advance function that indicates how long the model will remain in a given state before the next internal transition occurs. When the time advance expires, the internal transition function is used to determine the next state of the model.

The external transition function defines how the system responds to input events. When the model receives an input event at some time prior to the next scheduled internal transition, the external transition function is used to determine the model's next state and next internal event time.

The confluent transition function determines the model's next state when an internal and external transitions coincide.

Atomic model implementations are constructed by subclassing the atomic class. The derived class supplies a constructor, destructor, transition functions, output function, and a reset (initialization) function.

2.1.1 Constructor

The constructor for the atomic model should have the following form.


<model name>::<model name> ():atomic () {

/*

Your setup code here.

*/

}

Alternately, the constructor can be passed a string (char* type) that can be used to identify the model in a printable fashion.

Input and output ports can be added to the atomic model during construction. This is done with the addInputPort(port_t) and addOutputPort(port_t) methods. Definitions of input and output ports is generally done at the beginning of the model's source file. For example, the header might include a class definition like the following:


class myModel: public atomic {

public:

static const port_t in;
static const port_t out;

...

}

The first lines in the source file (after the #include and using statements) would be


static const port_t myModel::in = 0;
static const port_t myModel::out = 1;

In the constructor, the calls


addInputPort(in);
addOutputPort(out);

would be used to add the input and output ports to the atomic model.

2.1.2 Destructor

The destructor only needs to be defined where model specific clean up is required. The template for the destrucutor is standard for C++.

2.1.3 Initialization/reset function

The reset function is used to both initialize and reset the model. It will be called prior to starting a simulation run. The template for the reset function is


<model name>::reset () {

/*

Your initialization code here.

*/

}

2.1.4 Transition functions

The internal, external, and confluent transition functions must be supplied by the model builder. The template for these methods is as follows.


<model name>::delta_int () {

/*

Your transition function here

*/

}

<model name>::delta_ext () {

/*

Your transition function here

*/

<model name>::delta_conf () {

/*

Your transition function here

*/

}

The int, ext, and conf suffixes refer to the internal, external, and confluent transition functions, respectively.

Input values can be retrieved from an input port with the inputOnPort(port_t) method. The method will return a bag of objects that are available on the specified input port. For more information on the bag, object, and other container classes, see the html documentation.

The model can access the simulation clock in a variety of ways. The hold(stime_t dt) method will schedule the model's next internal transition function for t + dt. Similarly, the passivate() method will cause the model to passivate (i.e. tN = INFINITY). By default, the model will not be rescheduled following a state change (that is, it will proceed with tN unchanged). This default behavior is identical to the continue() function provided by other simulation environments.

The current simulation time can be determined with the timeCurrent() method. Similarly, tL and tN can be retrieved by calling the timeLast() and timeNext() methods. The sigma() method returns the time remaining until the next internal transition will fire. The elapsed() method returns the time that has elapsed since the last state change. Finally, the ta() method returns the time advance for the current state. In all cases, these values are dependent on the last call made to hold().

2.1.5 Output function

The output function is called when the simulator expects to see output on the model's output ports (i.e. just prior to the next internal transition). The template for the output function is


<model name>::output_func () {

/*

Your output function here

*/

}

Within the output function, use the output(port_t, object*) method to write an output object to the specified output port. Any number of objects can be written to an output port. The output function should not change the state of the model.

It is important to realize that, in the most general case, the output function may be called any number of times prior to the activation of the internal transition function. Consequently, passing an object to the output function with the assumption that it will be deleted exactly once can lead to problems. To be safe, it is best to pass a copy of the output object to the output function and either retain the original object or delete the original object when the internal transition function is called. See section 8 for more information about garbage collection in adevs.

2.2 Coupled models

Coupled models consist of one or more atomic and coupled models and information on how the ports of these component models and the coupled model itself are connected. Generally, the prebuilt coupled model types should be used to construct coupled models. Presently, this includes the staticDigraph for models that can be represented as a digraph. The staticDiagraph is described in section 3. Coupled models should be derived from the coupled class. A coupled model is defined by implementing a constructor, destructor, and an event routing function.

2.2.1 Constructor

The constructor is used to add component models to the coupled model and establish the coupling between these models. The add(devs*) method is used to add a model to the coupled model, while input and output ports can be added to the coupled model with the addInputPort(port_t) and addOutputPort(port_t) methods as before. Methods for establishing coupling should be defined by the coupled model implementation. Derived classes should always call the base coupled class constructor.

2.2.2 Destructor

The coupled model destructor, by default, destroys all of the component models and their associated couplings.

2.2.3 Event routing

The computeNeighbors (const devs& src, const port_value& pv) method is called by the simulator when an output event is available from the src DEVS on the indicated port. The coupled model constructs an indexed object that contains an EventReceiver for each input DEVS and port that the output event should be delivered to. For example,


const indexed* myModel::computeNeighbors (const devs& src, const port_value& pv) {

// Clear receiver list
list.empty ();
// Add an EventReceiver to the list
list.add (new EventReceiver (a_model, a_model->input_port);
// Return the list
return &list;

}

The value of the event being routed can also be incorporated into the routing function by looking at the pv.value field. This field containers a pointer to the object that will be delivered to the event receivers. The source port can be obtained from the pv.port field. See the HTML API documentation for more details on the port_value class.

3 Static structure digraph models

The staticDigraph is a coupled model that can be used to construct model networks using a (model 1, port 1) -> (model 2, port 2) type structure. That is, it provides methods for constructing models that can be described by a digraph. A staticDiagraph models can be constructed as follows.


<model name>::<model name> (/* your arguments */):
staticDigraph () {

/*

add input and output ports to the coupled model

*/

/*

add component models

*/

/*

establish internal coupling

*/

/*

establish external coupling

*/

}

The following methods are used to add components and establish couplings.


void add (devs* model)

Adds a model (coupled or atomic) to the digraph model.


void couple (devs* src, port_t srcPort, devs* dst, port_t dstPort)

This method establishes coupling between two ports associated with two distinct component models (internal coupling) or a component and the coupled model itself (external coupling).

4 Simulating DSDEVS models

Adevs can simulate models specified in a variant of the DSDEVS (Dynamic Structure DEVS) formalism. The formalism as originally presented was an extension of the classic DEVS formalism. The version supported by adevs is an extension of the Parallel DEVS formalism. It is identical to the classic DSDEVS formalism in most respects.

A DSDEVS model can be a coupled or atomic model as before. DSDEVS atomic models are identical to the Parallel DEVS atomic models. A DSDEVS coupled model has a special component, the network executive, that maintains information about the structure of the coupled model. The network executive is, in fact, a DEVS atomic model whose state is the component set and coupling specification for a DSDEVS coupled model.

4.1 The network executive

To build a network executive, you must subclass the networkExec class. The derived class has reset, output, and transition functions just like the Parallel DEVS atomic model. In addition, it has a computeNeighbors method that is identical to the coupled model computeNeighbors method.

While the networkExec class behaves just like an atomic model, it has some special functionality that is worth noting. First, the networkExec has a component set that is automatically reset whenever the networkExec is reset. The networkExec can add and remove members from this set using the add (devs*) and remove (devs*) methods. Models added to the networkExec are owned by the simulator. Similarly, when a model is removed from the networkExec, it is automatically deleted by the simulation engine.

The component set of the networkExec can be accessed (read only) using the getComponents() method. This returns a const set* type that contains all of the networkExec's component models.

4.2 DSDEVS coupled models

Coupled models are created using the DScoupled class. The structure of a dynamic structure coupled model is defined by the coupled model's network executive (see 4.1). The DScoupled model constructor accepts a networkExec that will be adopted as the network executive for the coupled model. For example:


DScoupled* coupled_model = new DScoupled (new myNetworkExec ());

A DScoupled model does not, however, adopt the ports of the network executive. If the coupled model is to present an external interface, these input and output ports must be added to the model using the addInputPort() and addOutputPort() methods of the DScoupled model. Coupling between the DScoupled model's ports and the port's of its components is specified by the network executive.

5 Simulating continuous systems

The adevs library includes a quantized integrator and ODE solver for simulating differential equation system specifications. The quantized integrator is a DEVS approximation of a continuous integrator that operates on a discrete state space (as opposed to 'classical' integrators that operate on a discrete time base). The quantized integrator can be used to integrate continuous system models with DEVS models by embedding the quantized continous system inside of an atomic model, or to do standalone simulation of continuous systems. An example of the latter is shown below. In this example, a one dimensional ball falls from a certain altitude. The system can be specified by the equations

a = G / h2
dv/dt = a
dh/dt = v

To simulate this system using quantum size D and initial conditions v = v0 and h = h0 , we can use the following fragment of code

// Create a 2 variable ODE solver.  Let q(0) = h and q(1) = v.
qode q (2);
q.setParams(D);
// Initialize state variables and their derivatives
q.init(0,h0);
q.init(1,v0);
dq[0] = 0;
dq[1] = 0;
// Simulate until the ball hits the ground
while (q.out(0) > 0.0) {
   dq[0] = q.out(1);
   dq[1] = G / (q.out(0)*q.out(0));
   q.integ (q.ta(), dq);
   }

To reuse this model in a larger simulation, we could wrap the quantized ODE in a DEVS atomic model with velocity and height output ports and, for instance, a thrust input port that represents a motor acting on the falling object. The implementation might be as follows

// Constructor
falling_object::falling_object():atomic() {
   // Add the input and output ports
   addInputPort (thrustPort);
   addOutputPort (heightPort);
   addOutputPort (velocityPort);
   }
//Initialization function
void falling_object::reset() {
   // Set the initial state of the object
   q.init(0,h0);
   q.init(1,v0);
   thrust = 0.0;
   // output initial state at time 0
   hold (0.0);
   }
// Internal transition function
void falling_object::delta_int() {
   dq[0] = q.out(1);
   dq[1] = G / (q.out(0) * q.out(0)) + thrust;
   q.integ (ta(), dq);
   hold (q.ta());
   }
// External transition function
void falling_object::delta_ext() {
   // Get the new thrust value
   bag* x = inputOnPort (thrustPort);
   thrust = ((dbl*)x->getObj(0))->value;
   // Update derivatives and integrate over the elapsed time
   dq[0] = q.out(1,elapsed());
   dq[1] = G / (q.out(0,elapsed()) * q.out(0,elapsed())) + thrust;
   q.integ (elapsed(), dq);
   hold (q.ta());
   }
// Confluent transition function
void falling_object::delta_conf() {
   delta_ext ();
   }
// Output function
void falling_object::output_func() {
   output (positionOutput, new dbl (q.out(0));
   output (velocityOutput, new dbl (q.out(1));
   }

6 Running a simulation

The devssim class is the root simulator for coupled and atomic models. To simulate a model, a devssim object should be created with the model of interest provided as the argument to the constructor. The run() method is then used to start the simulation. The run method can be passed a stop time that will cause the simulator to halt when the time of last event is greater than or equal to the stop time.

By default, the simulator halts as soon as the model passivates. The halt() method can be overridden to provide specialized behavior as necessary. The halt() method is checked by the simulator at the end of each simulation cycle. If a value of true is return, the simulation halts. Otherwise, another cycle is executed.

The last method of interest provided by the devssim class is the reset() method. By default, this method calls the reset method of the top level model. It can be specialized as necessary. The reset method is always called by the devssim constructor.

An example of the main function for an adevs simulation is shown below.


int main () {

devssim sim (new myModel ());
sim.run ();

}

7 Parallel simulation

Adevs includes a conservative, parallel simulation algorithm for shared memory multiprocessors that support POSIX threads (pthreads). Parallel simulation is enabled with the devssim::setThreadLimit(int max_threads)method. The max_threads value sets an upper limit on the number of threads created by the simulation engine. The default value is 0, which results in a sequential simulation run.

Any sequential simulation can be parallelized 'automatically' by setting a positive thread limit, so long as it adheres strictly to the following set of rules during a simulation run (i.e. from the time the devssim run()method is called until it returns).

  1. The output function (output_func) does not change the state of the model.

  2. Models communicate exclusively via their input and outport ports. The only exception are shared variables that are constants (i.e. read-only) and for which access is properly synchronzied, or write-only access to a variable for which access is properly synchronized (e.g. a global counter that is incremented by every model, but never read by any model).

  3. Objects outside of the simulation do not access models directly, except via the devssim methods computeNextOutput() and step() or by read-only access to simulation models when the devssim halt() is activated at the end of a simulation cycle.

To exhibit good speedup, conservative simulation algorithms generally require that a model have large fanout, good lookahead, or both. Large fanout requires that an output event generated by one model results in input for several other models. For example, if a model A has a single output port that is connected to the input ports of models B, C, and D, model A would be said to have a large fanout.

Lookahead is a guarantee that the model will not generate output for some time into the future. Atomic models can specify a lookahead value by overriding the lookahead() function of the atomic class. By default, this method returns zero. If a model overrides it, it is claiming that, for the current state of the model, its next output will be null and, furthermore, no state reachable from the current state by internal events will produce output, up to lookahead units of time beyond the time advance. Formally, we can define lookahead as follows

Let s0 be the current model state. For all states s1, s2, ... , sn reachable from s0 by successive application of the internal transition function (i.e. for each si, deltaint(si-1) = si and deltaint(s0) = s1) such that the sum of their time advance functions is less than lookahead(s0) (i.e. lookahead(s0) < ta(s1) + ta(s2) + ... + ta(sn)), the output for si is the null event (i.e. output(si) = NULL, 0 <= i <= n).

Intuitively, lookahead for a state s is a guarantee that, in the absence of input, the model will produce no output up to (but not including) ta(s) + lookahead(s) units of time in the future.

The parallel simulator can accept negative lookaheads (treating them as being equal to zero). If the lookahead is set correctly, the model will behave exactly as in the sequential case. If the lookahead is set to an overly large value, the model may fail to produce outputs as expected (since the simulator will not call the output_func method when it is guaranteed, via the lookahead, to produce no output). The simulation engine DOES NOT CHECK FOR INVALID LOOKAHEAD, so be careful when using it to speedup your simulation.

7.1 Assiging threads to models

By default, adevs will attempt to assign a thread to every model. If threads are relatively scarce, this can result in less than optimal performance. The setThreaded(bool) method of the devs class can be used to assign threads to models explicitly. If the setThreaded(bool) method is called with a value of true, then the simulator will attempt to run the model in its own thread. If the method is called with a value of false, a thread will not be assigned to the model. As a general rule, threads should only be assigned to models that do enough computational work to justify the threading overhead.

8 Garbage collection

Discrete event simulators can generate a large number of events and, necessarily, objects to represent those events. The adevs library supports safe, automatic deletion of event objects at the end of each simulation cycle. The following rules are used to determine which events should be deleted.

  1. An object that is put on an output port with the output(port_t,object*) method will be deleted.

  2. An object that is put on an output port with the output(port_t, const object&) method will be cloned via its clone() method, and that cloned object will be deleted.

  3. An object that is put on an output port with the output(port_t, object*, false) method will not be deleted.

When passing an object to which the model is keeping a pointer (i.e. using the output(port_t, object*, false) method to place the object on an output port), it is important to remember that the order in which models compute their transition functions is non-deterministic. Consequently, you should avoid changing the state of the outputted object while computing the model's state change. In general, use of the non-gc version of the output() method is ill advised. It is provided for those cases where the creation and deletion of an object is too expensive within the context of the application, and the added code complexity can be justified.

9 Integrating adevs into other simulation environments

The adevs simulation engine can be integrated into other simulation environments via methods that query the state of the simulator and allow the advance of the simulation clock to be controlled by logic outside of adevs. The devssim class provides two methods for querying the simulator about the state of the model, and one method for advancing the simulation clock.

The computeNextOutput(indexed& results) method fills the results list with port_value objects that respresent the model output given its current state. This method causes the output_func method of the constituent models to be activated. If the output_func method does not change the state of any of the models, then the computeNextOutput method will not cause the simulator to change state. Objects that are extracted from the simulation using computeNextOutput() must implement the clone() method (see the API documentation).

The timeNext() method returns the model's time of next event. This value is also the timestamp associated with the output computed via computeNextOutput method. The timeNext method does not change the state of the simulator.

The step(stime_t dt) method advances the simulation clock by dt units of time. If the current simulation time plus dt is greater than the simulator's time of next event, the step method throws a ScheduleException. The step(stime_t dt, indexed& input) method is identical to step(stime_t dt), but applies the port_value objects in the input list to the simulator at time t + dt. The port_value objects in the input list are removed from the list and passed directly to the simulator. The input list will be empty when the method call returns.

The following example shows how these methods could be used to drive an adevs simulation.

devssim sim (new myModel());
stime_t t = 0;
stime_t tL = 0;
sim.reset();
while (!sim.halt()) {
   t = sim.timeNext();
   sim.step (t - sim.timeLast());
   tL = t;
   }

10 Examples

Several examples are included with this distribution. These can be found in adevs/src/examples directory. Among the examples are a DSDEVS implementation of Conway's Game of Life, a convulation algorithm implemented on a simulated systolic array, and a pong-like game in which the player is given a paddle and a ball and has to destroy a bunch of blocks.