18. Yet more building a simulation

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

 

Where Are We?

In the last lecture, we fleshed out the mechanisms to inject vehicles into the road network simulator.

private void produce( double time ) {
    waiting = waiting + 1; // add this new car to the line!
    if (waiting <= 1) { // this is the only waiting car
        Simulator.schedule(
            time + travelTime,
            (double t)-> this.leave( t )
        );
    }

    Simulator.schedule(
        time + rand.nextExponential( period ),
        (double t)-> this.produce( t )
    );
}

We made source intersections a subclass of no-stop intersections, since the logic of what a vehicle does when it is freshly produced at a vehicle source should be similar to the logic of what a vehicle does when it arrives at an uncontrolled intersection. The method leave() scheduled above goes in the uncontrolled intersection, along with the variable waiting, which counts the number of vehicles in the intersection or waiting to get through the intersection.

protected void leave( double time ) {
    waiting = waiting - 1;
    if (waiting > 0) { // let next car in
        Simulator.schedule(
            time + travelTime,
            (double t)-> this.leave( t )
        );
    }

    this.pickOutgoing().enter( time );
}

When a vehicle leaves an intersection, it checks for waiting vehicles and lets one enter the intersection if one was waiting. In any case, after it leaves, it picks one of the roads away from that intersection and immediately enters that road.

Note: we could have written the final line as:

    Simulator.schedule( time, (double t)-> this.pickOutgoing().enter( t ) );

In some languages designed for discrete event simulation, event-service routines are the only abstraction tool, so event scheduling is used instead of normal calls and programmers who learned simulation in such environments or who learned simulation from those who learned in such environmennts frequently schedule many events at the current time when a normal call would suffice.

In any case the call to pickOutgoing() referenced above is a tool that we put in class Intersection to pick an outgoing road. This tool is put in the base class because how a person navigates usually does not depend on the type of intersection.

We have not made cars intelligent here, so picking an intersection is done without any attention to the car's intended destination or travel plans. As a result, all it does is draw a random number to pick from among the outgoing roads leading away from the intersection:

protected Road pickOutgoing() {
    return outgoing.get( rand.nextInt( outgoing.size() ) );
}

Roads

We added a leave method to class NoStop but it seems that all instances of class Intersection should have leave methods, and for that matter, all intersections should also have enter methods. Similarly, all roads should have enter and leave methods.

Leaving a road implies entering an intersection, and leaving an intersection implies entering a road. As a rule, entry to a road or intersection will schedule leaving that place, and the leave method of the place will directly call the enter method of the next place. This is the model that was demonstrated by NoStop.leave above.

These methods are easy for class Road:

/** A vehicle arrives at the road entrance
 *  @param time of vehicle arrival
 *  This should be called from i.leave(), for some intersection i
 */
public void enter( float time ) {
    Simulator.schedule(
        time + this.travelTime,
        (float t)->this.leave( t )
    );
}

/** A vehicle leaves this road
 *  @param time of vehicle departure
 *  This should be scheduled by this.enter()
 */
private void leave( float time ) {
    this.destination.enter( time );
}

We could have omitted Road.leave() by writing Road.enter() as follows:

public void enter( float time ) {
    Simulator.schedule(
        time + this.travelTime,
        (float t)->this.destination.enter( t )
    );
}

The disadvantage of the above is that it doesn't allow even the simplest of adjustments to the model. Consider, for example, the possibility of travel times on roads that depend on the vehicle density. If enter() increments the population of the road, and leave() decrements the population, then population/travelTime is the number of vehicles per unit time and we can compute the actual travel time for any trip down the road as, perhaps travelTime*(1+(population/travelTime)).

When there are only a few cars on the road, the travelTime is the real time it takes to travel down the road, but as the number of cars increases, the travel time gets slower and slower. With the right scale factors, this might be quite realistic.

Intersections

At the very outer level, in the abstract class Intersection, we need to add a commitment to a public Intersection.enter() method:

public abstract void enter( double t );

This commits every subclass of intersection to have such a method. Later, when we start dealing with stop lights, we'll find that this is an inadequate specification, but for now, we can add a dummy enter method to class StopLight that does nothing, and we can add a useful method to class NoStop:

/** a vehicle enters a no-stop intersection
 *  @param t, the time of entry
 *  this is typically called from the r.leave() for some road r
 */
public void enter( double t ) {
    this.waiting = this.waiting + 1;
    if (waiting <= 1) { // this is the only waiting car
        Simulator.schedule(
            time + travelTime,
            (double t)-> this.leave( t )
        );
    }
}

This looks very familiar. So familiar that we can rewrite the code we already wrote in class Source to simply call the enter method instead of duplicating the code:

private void produce( double time ) {
    super.enter( time );

    Simulator.schedule(
        time + rand.nextExponential( period ),
        (double t)-> this.produce( t )
    );
}

Of course, we must add Sink intersections to match our Source intersections, but a trivial Sink class will suffice. We need to

Testing

Once we have source intersections, roads and sinks, we have enough to test. Because we also have uncontrolled intersections as a side effect of the way we built source intersections, we can actually test the behavior of the simulation on complicated road networks, but initially, it makes sense to do some very simple tests.

Of course, none of the real intersections work, but we can get going with just source intersections and sink intersections, testing the code with something like this:

intersection A 1.0 source 10.0
intersection B 1.0 sink
road A B 5.0

We can even get ambitious, adding multiple sources or sinks:

intersection A 1.0 source 10.0
intersection B 1.0 sink
intersection C 1.0 sink
road A B 5.0
road A C 10.0

Once this works, the next obvious step is to add some intermediate non-stop intersections

intersection A 1.0 source 10.0
intersection B 1.0 source 10.0
intersection C 1.0
intersection D 1.0 sink
intersection E 1.0 sink
road A C 5.0
road B C 5.0
road C D 5.0
road C E 5.0

An interesting question is, what happens if we add some cycles to the road network, allowing cars to go around the block?

intersection A 1.0 source 10.0
intersection B 1.0 source 10.0
intersection C 1.0
intersection D 1.0 sink
intersection E 1.0 sink
road A B 5.0
road A C 5.0
road B A 5.0
road B C 5.0
road C A 5.0
road C B 5.0
road C D 5.0
road C E 5.0

At this point, it is clear that a simple trace of activity with outputs from each intersection each time a car passes through isn't very interesting except as a proof that our mechanism is doing something.