next up previous
Next: Continuous Models Up: A Discrete EVent system Previous: Network Models

Subsections

Variable Structure Models

The composition of a variable structure model changes through time. New components are added as machinery is installed in a factory, organisms reproduce, or shells are fired from a cannon. Existing components are removed as machines break, organisms dies, or shells in flight find their targets. Components are rearranged as parts move through a manufacturing process, organisms migrate, or a command and control network loses communication lines. But structure change can not occur willy nilly if we want our simulation to produce well defined, repeatable outcomes. For this reason, Adevs provides a simple but effective mechanism for coordinating structure changes with model state transitions5.1.

Building and Simulating Variable Structure Models

Every Adevs model, Network and Atomic, has an abstract method called model_transition. This method is inherited from the Devs class that is at the top of the Adevs class hierarchy. The signature of the model_transition method is
bool model_transition()
and its default implementation simply returns false.

At the end of every simulation cycle the simulator invokes the model_transition method of every Atomic model that changed in that cycle. When the model_transition method is invoked the Atomic model can do almost anything it likes except alter the component set of a Network model. If the model_transition method returns true, then the simulator will also call the model's parent. The parent is, of course, a Network model; its model_transition method may add, remove, and rearrange components. But it must not delete components! The simulator will automatically delete removed components when the structure change calculations are finished. As before, if the Network's model_transition method returns true then the simulator will invoke the model_transition method of the Network's parent.

After invoking every eligible model's model_transition method, the simulator performs a somewhat complicated cleanup process. This process requires that simulator construct two sets. The first set contains all of the components that belonged to all of the Network models whose model_transition method was invoked and all of the components belonging to components that are in this set. The second set is defined in the same way, but it is computed using component sets as they exist after the model_transition methods have been invoked. The simulator deletes every model that has actually been removed; these are the models in the first set but not in the second. The simulator initializes every model that is genuinely new by computing its next event time (i.e., its creation time plus its time advance) and putting it into the event schedule; these are the models in the second second set but not in the first. The simulator leaves all other models alone. This confusing procedure is illustrated in Fig. [*].

Figure: The black models' model_transition methods returned true. The set of components considered before and after the structure change are shown in the before (left) and after (right) trees. The set of deleted components is $ \{c,D,d,e,f\}-\{e,g,d\} = \{c,D,f\}$ . The set of new components is $ \{e,g,d\} - \{c,D,d,e,f\} = \{g\}$ .
\begin{figure}\centering
\epsfig{file=var_struct_models_figs/var_struct_model_sets.eps}\end{figure}

The model_transition method can break the strict hierarchy and modularity that is usual observed when building Atomic and Network models. Any Network model can modify the component set of any other model regardless of proximity or hierarchy. The potential for anarchy is great; the design of a variable structure model should be carefully considered. There are two approaches that are simple and, in many cases, entirely adequate.

The first approach is to allow only Network models to effect structure changes and to restrict those changes to the Network's immediate sub-components. With this approach, an Atomic model initiates a structure change by posting a structure change request for its parent Network. The Atomic model's model_transition method then returns true causing its parent's model_transition method to be invoked. The parent Network model then retrieves and acts on the posted structure change request. The Network repeats this process if it wants to effect structure changes involving models other than its immediate children.

The second approach allows arbitrary structure changes by forcing the model at the very top of the hierarchy to invoke its model_transition method. This causes the simulator to consider every model in the aftermath of a structure change. As in the first approach, an Atomic model that wants to effect a structure change uses its model_transition method to post a change request for its parent. This is percolated up the model hierarchy by the Network models whose model_transition methods always return true.

The first approach trades flexibility for execution time; the second approach trades execution time for flexibility. With the first approach, structure changes that involve a small number of components require a small amount of work by the simulator. With the second approach, every structure change requires the simulator to include every part of the model in its set calculations regardless of the structure change's actual extent, but the scope of a structure change is unlimited.

A Variable Structure Example

The Custom Widget Company is expanding its operations. Plans are being drawn for a new factory that will make custom gizmos (and the company name will be changed to The Custom Widget and Gizmo Company). The factory machines are expensive to operate. To keep costs down, the factory will operate just enough machinery to fill outstanding gizmo orders in sufficient time. The factory must have enough machinery to meet peak demand, but much of the machinery will be idle much of the time. The factory engineers want to answer two questions: how many machines are needed and how much will it costs to operate the them.

We are going to use a variable structure model to answer these two questions. The model will have three components: a generator that creates factory orders, a model of a single machine, and a model of the factory which contains the machine models and activates and deactivates machines as required to satisfy demand. The complete factory model is illustrated in Fig. [*].

Figure: Block diagram of the variable structure factory model. The broken lines indicate structural elements that are subject to dynamic changes.
\begin{figure}\centering
\epsfig{file=var_struct_models_figs/factory_block_diagram.eps}\end{figure}

The generator creates new orders for the factory. Each order is identified with its own integer label, and the generator produces orders at the rate anticipated by the factory planners. The order arrival rate and, consequently, the time advance of the generator are not constants. Demand at the factory is expected to be fairly steady with a new order arriving every 1/2 to 2 days; demand is modeled with a random variable uniformly distributed in the range [0.5,2]. Here is the generator code:

#include "adevs.h"
// The Genr models factory demand. It creates new orders every 0.5 to 2 days.
class Genr: public adevs::Atomic<int>
{
    public:
        /**
         * The generator requires a seed for the random number that determines
         * the time between new orders.
         */
        Genr(unsigned long seed):adevs::Atomic<int>(),next(1),u(seed){ set_time_to_order(); }
        // Internal transition updates the order counter and determines the next arrival time
        void delta_int() { next++; set_time_to_order(); }
        // Output function produces the next order
        void output_func(adevs::Bag<int>& yb) { yb.insert(next); }
        // Time advance returns the time until the next order
        double ta() { return time_to_order; }
        // Model is input free, so these methods are empty
        void delta_ext(double,const adevs::Bag<int>&){}
        void delta_conf(const adevs::Bag<int>&){}
        // No explicit memory management is needed
        void gc_output(adevs::Bag<int>&){}
    private:
        // Next order ID
        int next;
        // Time until that order arrives
        double time_to_order;
        // Random variable for producing order arrival times
        adevs::rv u;
        // Method to set the order time
        void set_time_to_order() { time_to_order = u.uniform(0.5,2.0); }
};

The machine model is similar to the Clerk model that appeared in section [*]. Each machine requires 3 days make a gizmo and orders are processed first come first serve. The Machine's model_transition method is inherited from the Atomic class, which inherited it from the Devs class (the inheritance hierarchy is Devs $ \leftarrow$ Atomic Machine). I'll discuss the role of the model_transition method after introducing the Factory class; here is the Machine model code.

#include "adevs.h"
#include <cassert>
#include <deque>
/**
 * This class models a machine as a fifo queue and server with fixed service time.
 * The model_transition method is used, in conjunction with the Factory model_transition
 * method, to add and remove machines as needed to satisfy a 6 day turnaround time
 * for orders. 
 */
class Machine: public adevs::Atomic<int> 
{
    public:
        Machine():adevs::Atomic<int>(),tleft(DBL_MAX){}
        void delta_int()
        {
            q.pop_front(); // Remove the completed job
            if (q.empty()) tleft = DBL_MAX; // Is the Machine idle?
            else tleft = 3.0; // Or is it still working?
        }
        void delta_ext(double e, const adevs::Bag<int>& xb)
        {
            // Update the remaining time if the machine is working
            if (!q.empty()) tleft -= e;
            // Put new orders into the queue
            adevs::Bag<int>::const_iterator iter = xb.begin();
            for (; iter != xb.end(); iter++) 
            {
                // If the machine is idle then set the service time
                if (q.empty()) tleft = 3.0;
                // Put the order into the back of the queue
                q.push_back(*iter);
            }
        }
        void delta_conf(const adevs::Bag<int>& xb)
        {
            delta_int();
            delta_ext(0.0,xb);
        }
        void output_func(adevs::Bag<int>& yb)
        {
            // Expel the completed order
            yb.insert(q.front());
        }
        double ta()
        {
            return tleft;
        }
        // The model transition function returns true if another order can not
        // be accommodated or if the machine is idle.
        bool model_transition()
        {
            // Check that the queue size is legal
            assert(q.size() <= 2);
            // Return the idle or full status
            return (q.size() == 0 || q.size() == 2);
        }
        // Get the number of orders in the queue
        unsigned int getQueueSize() const { return q.size(); }
        // No garbage collection 
        void gc_output(adevs::Bag<int>&){}
    private:
        // Queue for orders that are waiting to be processed
        std::deque<int> q;
        // Time remaining on the order at the front of the queue
        double tleft; 
};

The number of Machine models in the Factory any time is determined by the current demand for gizmos. The real factory, of course, will have a set number of physical machines on the factory floor, but the planners do not yet know how many machines are needed. A variable structure model that creates and destroys machines as needed is a good way to accommodate this uncertainty (a design decision similar to using a linked list in place of a fixed size array).

The Custom Widget and Gizmo Company has built its reputation on a guaranteed service time, from order to delivery, of 15 days. This leaves only 6 days for the manufacturing process (the remaining time being consumed by order processing, delivery, etc.). A single machine can meet this schedule if it has at most one order waiting in its queue. But it costs a dollar a day to operate a machine and so the factory engineers want to minimize the number of machines working at any particular time. With this goal, the factory operating policy has two rules:

  1. assign incoming orders to the active machine that can provide the shortest turn around time and
  2. keep just enough active machines to have capacity for one additional order.
The Factory model implements this policy in the following way. When a Machine becomes idle or its queue is full (i.e., the machine is working on one order and has another order waiting in its queue) then its model_transition method returns true. This causes the Factory's model_transition method to be invoked. The Factory first looks for and removes machines that have no work and then examines each remaining machine to determine if the required one unit of additional capacity is available. If the required unit of additional capacity is not available then the Factory creates a new machine.

This is an example of the first approach to building a variable structure model. With this design, the set calculations that are done when the Factory's model_transition method is invoked are limited to instances where Machine models are likely to be created or destroyed. Our design, however, is complicated somewhat by the need for Machine and Factory objects to communicate (i.e., the Machines must watch their own status and inform the Factory when there is a potential capacity shortage). If we had used the second approached to build our variable structure model, then the Machines' model_transition methods could have merely returned true; no need for a status check. The Factory would have iterated through its list of Machines, adding and deleting Machines as needed. This is more computationally expensive; the simulator would look for changes in the Factory's component set at the end of every simulation cycle. But the software design is simpler, albeit only marginally so in this instance.

The Factory is a Network model, and we need to implement all of the Network's virtual methods: route, getComponents, and model_transition. The route method is responsible for assigning orders to the proper Machine. When an order arrives, it is sent to the machine with the shortest total service time. The getComponents method puts the current machine set into the output Set c. The model_transition method examines the status of each machine, deleting idle machines and adding a new machine if it is needed to maintain reserve capacity. The complete Factory implementation is shown below.

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

class Factory: public adevs::Network<int> {
   public:
      Factory();
      void getComponents(adevs::Set<adevs::Devs<int>*>& c);
      void route(const int& order, adevs::Devs<int>* src,
            adevs::Bag<adevs::Event<int> >& r);
      bool model_transition();
      ~Factory();
      // Get the number of machines
      int getMachineCount();
   private:
      // This is the machine set
      std::list<Machine*> machines;
      // Method for adding a machine to the factory
      void add_machine();
      // Compute time needed for a machine to finish a new job
      double compute_service_time(Machine* m);
};
#include "Factory.h"
using namespace adevs;
using namespace std;

Factory::Factory():
Network<int>() { // call the parent constructor
   add_machine(); // Add the first machine the the machine set
}

void Factory::getComponents(Set<Devs<int>*>& c) {
   // Copy the machine set to c
   list<Machine*>::iterator iter;
   for (iter = machines.begin(); iter != machines.end(); iter++)
      c.insert(*iter);
}

void Factory::route(const int& order, Devs<int>* src, Bag<Event<int> >& r) {
   // If this is a machine output, then it leaves the factory
   if (src != this) { 
      r.insert(Event<int>(this,order));
      return;
   }
   // Otherwise, use the machine that can most quickly fill the order
   Machine* pick = NULL;  // No machine
   double pick_time = DBL_MAX; // Infinite time for service
   list<Machine*>::iterator iter;
   for (iter = machines.begin(); iter != machines.end(); iter++) {
      // If the machine is available
      if ((*iter)->getQueueSize() <= 1) {
         double candidate_time = compute_service_time(*iter);
         // If the candidate service time is smaller than the pick service time
         if (candidate_time < pick_time) {
            pick_time = candidate_time;
            pick = *iter;
         }
      }
   }
   // Make sure we found a machine with a small enough service time
   assert(pick != NULL && pick_time <= 6.0);
   // Use this machine to process the order
   r.insert(Event<int>(pick,order));
}

bool Factory::model_transition() {
   // Remove idle machines
   list<Machine*>::iterator iter = machines.begin();
   while (iter != machines.end()) {
      if ((*iter)->getQueueSize() == 0) iter = machines.erase(iter);
      else iter++;
   }
   // Add the new machine if we need it
   int spare_cap = 0;
   for (iter = machines.begin(); iter != machines.end(); iter++)
         spare_cap += 2 - (*iter)->getQueueSize();
   if (spare_cap == 0) add_machine();
   return false;
}

void Factory::add_machine() {
   machines.push_back(new Machine());
   machines.back()->setParent(this);
}

double Factory::compute_service_time(Machine* m) {
   // If the machine is already working
   if (m->ta() < DBL_MAX) return 3.0+(m->getQueueSize()-1)*3.0+m->ta();
   // Otherwise it is idle 
   else return 3.0;
}

int Factory::getMachineCount() {
   return machines.size();
}

Factory::~Factory() {
   // Delete all of the machines
   list<Machine*>::iterator iter;
   for (iter = machines.begin(); iter != machines.end(); iter++)
      delete *iter;
}
To illustrate how the model_transition method is used, let's manually simulate the processing of a few orders: the first order arrives at day zero, the second order at day one, and the third order at day three. At the start of day zero there is one idle Machine. When the first order arrives the Factory's route method is invoked and it sends the order to the idle Machine. The Machine's delta_ext method is invoked next and the Machine begins processing the order. Then the Machine's model_transition method is invoked, discovers that the Machine is working and has space in its queue, and returns false.

When the second order arrives on day one, the Factory's route method is called again. There is only one Machine and it has space in its queue so the order is routed to that Machine. The Machine's delta_ext method is invoked next, and the second order is queued. The Machine's model_transition method is now invoked; the queue is full and so the method returns true. This causes the the Factory's model_transition method to be invoked; it examines the Machine's status, sees that it overloaded, and creates a new Machine. At this time, the working Machine needs two more days to finish the first order and needs a total of five days to complete its second order.

There is a great deal of activity when the third order arrives on day three. First, the working Machine's output_func method is called and it spits out the completed order (the order begun on day zero). Then the Factory's route method is called twice. First it converts the Machine output into a Factory output, and then it routes the new order to the idle Machine (the order of these route calls could have been switched). Next the state transition methods for the two Machines are invoked. The working Machine's delta_int method is called and it starts work on its queued order. The idle Machine's delta_ext method is called and it begins processing the new order. Finally, the model_transition methods of both Machines are invoked; both Machine's have room in their queue and so both methods return false.

For the sake of illustration, suppose no orders arrive in the next three days (this is impossible when orders arrive every one half to two days, but bear with me). At day six, both machines will finish their orders. The Machines' output_func methods will be invoked, producing the finished orders which are sent to the Factory output via the Factory's route method. Next, the Machines' delta_int methods will be called and both Machines will become idle. Then the Machines' model_transition methods will be invoked and these will return true. This will cause the Factory's model_transition method to be called. It will examine the status of each Machine, see that they are idle, and delete both of them. Then the Factory will compute its available capacity, which is now zero, and create a new machine. Incidentally, this returns the Factory to its initial state of having one idle Machine.

The factory engineers have two questions: how many machines are needed and what is the factory's annual operating cost. These questions can be answered with a plot of the active machine count versus time. The required number of machines is the maximum value of the active machine count. Each machine costs a dollar per day to operate, and so the operating cost is just the one year time integral of the active machine count.

A plot of the active machine count versus time is shown in Fig. [*]. The maximum active machine count in this plot is 4 and the annual operating cost is $944 (this plot is from the first simulation run listed in Table [*]). The arrival rate is a random number, and so the annual operating cost and maximum machine count are themselves random numbers. Consequently, data from several simulation runs is needed to make an informed decision. Somewhat arbitrarily, I have listed ten simulation runs; each run uses a different random number generator seed and produces a different outcome (i.e., another sample of the maximum active machine count and annual operating cost). The maximum active machine count and annual operating cost generated by each run is shown in Table [*]. From this data, the factory engineers conclude that 4 machines are required and the average annual operating cost will be $961.

Figure: Active machine count over one year.
\begin{figure}\centering
\epsfig{file=var_struct_models_figs/machine_plot.eps}\end{figure}

Table: Outcomes of ten factory simulation runs.
Seed Maximum machine count Annual operating cost
1 4 $944.05
234 4 $968.58
15667 4 $980.96
999 3 $933.13
9090133 4 $961.65
6113 4 $977.33



next up previous
Next: Continuous Models Up: A Discrete EVent system Previous: Network Models
2009-03-16