21. An Alternative Simulation Framework

Part of CS:2820 Object Oriented Software Development Notes, Fall 2015
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

 

An Alternative Simulation Framework

The simulation framework we have used, supporting lambda binding as the primary way we schedule new events, is only one alternative. Consider this alternative idea:

Class Simulator exports an abstract class, Event to the world. Each particular type of event scheduled within the simulation is a concrete subclass of Simulator.Event. At the top level, each event is characterized by just two attributes: The time at which it occurs and the action that is to be triggered at that time. So, we can begin the declaration of a new version of class Simulator as follows:

class Simulator {

        public static abstract class Event {
                public float time;       // the time of this event
                abstract void trigger(); // the action to take
        }

To schedule an event, just provide a specific implementation of Simulator.Event with appropriate fields and a trigger() method, then initialize an object of that class with the desired time and schedule it. Within class Simulator the basic mechanism for scheduling an event now looks like this:

        private static PriorityQueue  eventSet
        = new PriorityQueue  (
                (Event e1, Event e2) -> Float.compare( e1.time, e2.time )
        );

	Static void schedule( Event e ) {
                eventSet.add( e );
        }

Note that we have made no changes to the eventSet data structure. It is still private to class Simulator, and it is still sorted on the time field.

The Simulator.schedule() method is significantly simpler than the old version. It is up to the caller to create a new event and pass it to schedule(), and instead of having to schedule() do the work of binding an action to the event time, we let the caller pre-populate the event's fields with appropriate content.

The final part of our new class Simulator actually runs the simulation. It no longer has to pass the event time to the trigger() method because trigger() is now a method of a subclass of Event and it has direct access to its own time.

        static void run() {
                while (!eventSet.isEmpty()) {
                        Event e = eventSet.remove();
                        e.trigger();
                }
        }
}

Now, let's explore rewriting our road-network simulator to use this new simulation framework. In summary, what we will have to do in every place where there was a call to simulator.schedule() is replace that call with new code to create and schedule an appropriate address.

Roads

Class Road is comparatively simple -- entry to a road causes an event to be scheduled to leave that road at a later time, without any complex queueing such as we had at intersections. We will work on this first. Using lambda expressions, our original code contained the following code within Road.enter():

public void enter( Vehicle v, float t ) {
                /** Simulate v entering this road at time t.
                 *  This is always called at the same time as intersection exit.
                 */

                System.out.println( "Entering " + this.toString()
                        + " at time = " + t
                );

                // track state of road
                population = population + 1;

                Simulator.schedule(
                        t + travelTime,
                        (float time)->this.exit( v, time )
                );
        }

We need to replace the call to Simulator.schedule() above. We could do this with something like the following:

		ExitEvent e = new ExitEvent();
		e.time = t + travelTime;
		e.vehicle = v;
		e.road = this;
		Simulator.schedule( e );

This gets cumbersome, but we can move the initialization of the event into the initializer for class ExitEvent. Let's declare this class as a local class within class Road:

	Class ExitEvent extends Simulator.Event {
		private Vehicle v;
                private Road r;

                ExitEvent( Vehicle v, Road r, float t ) {
                        time = t;
                        this.v = v;
                        this.r = r;
                        Simulator.schedule( this );
                }

                void trigger() {
                        r.exit( v, time );
                }
        }

First note that we had the initializer ExitEvent() do the scheduling. As a result, the result of the initializer will usually be discarded by the caller.

Note that the trigger() method above makes the identical call to exit that was passed in the original lambda expression. Therefore, triggering this event will result in the exact same computation as was done in the original.

With this code done, we can replace the call to Simulator.schedule() that used lambda expressions with this line of code to schedule vehicle v to leave this road at time t+travelTime:

		new ExitEvent( v, this, t + travelTime );

An optimization

There is only one call to Road.exit() in the modified code above, and that is from within ExitEvent(). Because of this, we can eliminate Road.exit() from our program and simply move its code into ExitEvent.trigger(). This gives us the following code (basing the computations on the version class Road distributed to the class in Lecture 17:

                void trigger() {
                        System.out.println( "Exiting " + this.toString()
                                + " at time = " + time
                        );

                        r.population = r.population - 1;

                        r.destination.enter( v, time );
                }

Intersections

The abstract class Intersection in our road-network model requires that every subclass of road contain an exit() method. Aside from the internal details of this method, all of the intersection exit events are called through the identical interface. Therefore, in our new model, we can declare the new class Road.ExitEvent local to road. It must not be a private inner class, however, so that instances can be created from outside:

        class ExitEvent extends Simulator.Event {
                /** Generic exit from any intersection, but it
                 *  calls Subclass.exit() when triggered.
                 */
                Vehicle v;
                Intersection i;

                ExitEvent( Vehicle v, Intersection i, float t ) {
                        time = t;
                        this.v = v;
                        this.i = i;
                        Simulator.schedule( this );
                }

                void trigger() {
                        // We could put the code for r.exit here
                        i.exit( v, time );
                }
        }

The simplest illustration of a call to this method is in Source intersections. In our rudimentary model, each source intersection launches a vehicle into the road network simultaneously at time zero and never launches another. This job is done in the initializer; in the original version with lambda expressions, the code was as follows:

        public Source( Scanner sc ) {
                super( sc );
                Simulator.schedule( // the first car leaves here immediately
                        0,
                        (float time)->this.exit(
                                new Vehicle(),
                                time
                        )
                );
        }

The new initializer looks like this. The code looks simpler than the old version, but only because we have already declared the class Interseciton.ExitEvent. We haven't really eliminated any complexity, we just moved it around. class:

        public Source( Scanner sc ) {
                super( sc );
                new ExitEvent( new Vehicle(), this, 0 );
        }

There is a big difference between the old lambda-based simulation framework and the new framework that crops up in this code. In the lambda-based framework, the new vehicle is not created until the event is triggered. Only at that time is the body of the lambda expression evaluated, causing a call to new Vehicle().

In contrast, in the new code, the new vehicle is created first and then this vehicle is passed to the initializer for the new event record. This means that the vehicle exists before the event is triggered. In the context of our simulation, this has no impact at all, but if we were tracking the number of vehicles in existance at any time, they could be give different outputs.

Concluding Remark

It is important to understand that, while these two simulation frameworks lead to very different program structures, both the system being simulated (in this example, a road network) and the basic discrete-event simulation algorithm have not changed.

Changing from one framework to another is frequently messy. If you start with an inadequate framework, you can back yourself into a corner and find that some parts of your system are extremely difficult. Once you have a substantial body of code written, changing to a new framework can be quite difficult.

The Code

Example code incorporating this new framework is available here. This is a modified version of the source code distributed with Lecture 17.