Atomic Models

An atomic model is described by a state space, a collection of state transition functions, an output function, and a time advance function. The internal transition function describes the autonomous behavior of the model. The external transition function describes the input response of the model. Model inputs are bags of (input port, value) pairs. The confluent transition function determines the behavior of the model when an internal event and external event coincide. The output function maps the system state into a bag of (output port,value) pairs that can serve as inputs to other models. The time advance function indicates the time at which the next internal event will occur.

The basic algorithm for simulating an atomic model is described below. The atomic model generally has a parent network model of which it is but a single component (although this need not necessarily be the case).

tL : = 0
while (more to do)
     send local time of next event = tL + time advance to parent model
     get global time of next event from parent model
     if (time of next event = tL + time advance)
          compute output function and send output to parent network model
     else
          send NULL to parent network model
     endif 
     get input from parent model
     if (no input and time of next event = tL + time advance)
          compute internal transition function
          tL := time of next event
     else if (input available and time of next event = tL + time advance)
          compute confluent transition function using input
          tL := time of next event
     else if (input available and time of next event < tL + time advance)
          compute external transition function using input and elapsed time = time of next event - tL
          tL := time of next event
     endif
endwhile

Examining the above listing, it can be seen that outputs always coincide with internal events. The internal transition function is computed whenever the simulation clock is equal to the atomic model's time of next event (i.e. tL + time advance). The confluent transition function is computed whenever the simulation clock is equal to the atomic model's time of next event and input is available to be processed. The external transition function is computed when the simulation clock is less than the atomic model's time of next event and input is available. The external transition function uses the time elapsed since the last state change and the available input to compute the next model state.

Atomic models are implemented as classes derived from the atomic class. The internal transition function is implemented via the delta_int() method. The confluent transition function is implemented as the delta_conf(const adevs_bag<PortValue>& x) method where x is the (adevs_)bag of PortValue pairs that are being inputted into the model. The external transition function is implemented via the delta_ext(stime_t e, const adevs_bag<PortValue>& x) method where e is the elapsed time and x is the model input. The output function is implemented as the method output_func(adevs_bag<PortValue>& y) where y is an empty bag to be filled with model outputs. Finally, the hold(stime_t dt) method can be called from within the transition functions to set the time of next event, where dt is the time that should elapse before another internal event occurs.

The simulation library requires a handful of other methods to be provided by the user. The init() method should place the model into its initial state. This method is called once prior to the start of each simulation run. The gc_output(adevs_bag<PortValue>& g) method is a garbage collection function. The bag g is filled with outputs that where generated by the model and are no longer in use by the simulation engine. The garbage collection method should dispose of the output values appropriately.

To illustrate the implementation of an atomic model, a simple logical not gate with a fixed delay is implemented below. The not gate takes a single input at any one time, sets its state to the negation of that input value, and then produces an output some time later. Assuming that adevs has been configured to use bool types as input/output values, the implementation is as follows:

class not_gate: public atomic
{
   public:
     static const port_t in;
     static const port_t out;
     not_gate():atomic(),state(false),delay(0.0001){}
     void init() { state = false; passivate(); }
     void delta_int() { passivate(); }
     void delta_ext(stime_t e, const adevs_bag<PortValue>& x) { state = !x[0].value; hold(delay); }
     void delta_conf(const adevs_bag<PortValue>& x) { state = !x[0].value; hold(delay); }
     void output_func(adevs_bag<PortValue>& y) { output(out,state,y); }
     void gc_output(adevs_bag<PortValue>& g){}
     ~not_gate(){}
   private:
      bool state;
      stime_t delay;
};

const port_t not_gate::in(0);
const port_t not_gate::out(0);

This simple model will hold forever if we send inputs to it faster than its delay time (why?)! An alternative implementation could simply report an error if another input is received before the first has finished propagating through the system. For example,

class not_gate: public atomic
{
   public:
     static const port_t in;
     static const port_t out;
     not_gate():atomic(),state(false),delay(0.0001){}
     void init() { state = false; passivate(); }
     void delta_int() { passivate(); }
     void delta_ext(stime_t e, const adevs_bag<PortValue>& x) 
     { 
          if (ta() < INFINITY)
               cerr << "Not gate received input too fast!" << endl;
          else
          {
               state = !x[0].value; 
               hold(delay);
          } 
     }
     void delta_conf(const adevs_bag<PortValue>& x) { state = !x[0].value; hold(delay); }
     void output_func(adevs_bag<PortValue>& y) { output(out,state,y); }
     void gc_output(adevs_bag<PortValue>& g){}
     ~not_gate(){}
   private:
      bool state;
      stime_t delay;
};

const port_t not_gate::in(0);
const port_t not_gate::out(0);

This version of the not gate checks to see if the model is passive (i.e. ta() = INFINITY) before changing the state of the model in response to an input.

The simple single input/single output not-gate does not take advantage of the port part of the PortValue input pairs. To see how ports can be used, consider a model of a one bit adder. It has two inputs (call them a and b) and produces three outputs (call them carry_in, carry_out, and c). We'll consider a fixed propagation delay as before. Here is an implementation of the model

class adder_2bit: public atomic
{
   public:
     static const port_t a;
     static const port_t b;
     static const port_t carry_in;
     static const port_t carry_out;
     static const port_t c;
     adder_2bit():atomic(),one(false),two(false),carry(false),delay(0.0001){}
     void init() { carry = one = two = false; passivate(); }
     void delta_int() { passivate(); }
     void delta_ext(stime_t e, const adevs_bag<PortValue>& x) 
     { 
        for (int i = 0; i < x.getSize(); i++)
        {
           if (x[i].port == a) one = x[i].value;
           else if (x[i].port == carry_in) carry = x[i].value;
           else two = x[i].value;
        }
        hold(delay); 
     }
     void delta_conf(const adevs_bag<PortValue>& x) { delta_ext(ta(),x); }
     void output_func(adevs_bag<PortValue>& y) 
     {
        bool result = xor(xor(one,two),three);
        output(c,result,y);
        output(carry, (one&&two)||(two&&carry)||(one&&carry), y);
     }
     void gc_output(adevs_bag<PortValue>& g){}
     ~adder_2bit(){}
   private:
      bool one, two, carry;
      stime_t delay;
      static bool xor(bool x,bool y)
      {
         return (x||y)&&!(x&&y);
      }
};

const port_t adder_2bit::a(0);
const port_t adder_2bit::b(1);
const port_t adder_2bit::carry_in(2);
const port_t adder_2bit::carry_out(3);
const port_t adder_2bit::c(4);

In the next section, an n-bit adder will be constructed from 2-bit adders using a DEVS network model.