# You may have to edit this file to delete header lines produced by
# mailers or news systems from this file (all lines before these shell
# comments you are currently reading).
# Shell archive made by dwjones on Wed 11 Nov 2020 11:30:43 AM CST
# To install this software on a UNIX system:
# 1) create a directory (e.g. with the shell command mkdir stuff)
# 2) change to that directory (e.g. with the command cd stuff),
# 3) direct the remainder of this text to sh (e.g. sh < ../savedmail).
# This will make sh create files in the new directory; it will do
# nothing else (if you're paranoid, you should scan the following text
# to verify this before you follow these directions). Then read README
# in the new directory for additional instructions.
cat > README <<\xxxxxxxxxx
Road Network Simulator
Version: Nov 11, 2020 -- new simulation framework based on the Nov 3 version
Author: Douglas Jones
This distribution contains
-- README -- this file
-- RoadFiles -- the list of all Java source files
-- testfile -- a road network description to test the simulator
To build the simulator use the shell command: javac @RoadFiles
To generate HTML documentation, run the shell command: javadoc @RoadFiles
To test the simulator use the shell command: java RoadNetwork testfile
Other input files can be substituted for testfile.
BUG: document the input file format somewhere
xxxxxxxxxx
cat > RoadFiles <<\xxxxxxxxxx
Error.java
MyScanner.java
MyRandom.java
Simulator.java
Road.java
Intersection.java
NoStop.java
StopLight.java
Sink.java
Source.java
RoadNetwork.java
xxxxxxxxxx
cat > Error.java <<\xxxxxxxxxx
/**
* Error handling
* @author Douglas Jones
* @version 10/27/2020
* Status: Stable code
*/
abstract class Error{
private Error(){} // prevent instantiation of this class
private static int errorCount = 0;
private static final int errorLimit = 10;
/** Warn the user of a non-fatal error
* @param message to output
* This version outputs the message to System.err (standard error).
* Other versions could output via a pop-up window or something.
* If there are too many errors, this code will terminate the application.
* The error limit is currently hard coded to 10.
*/
public static void warn( String message ) {
System.err.println( message + "\n" );
errorCount = errorCount + 1;
if (errorCount > errorLimit) System.exit( 1 );
}
/** Warn the user of a fatal error
* @param message to output
* This version outputs the message exactly as warn() does.
* @see warn
* it terminates the application immediately, indicating a failure.
*/
public static void fatal( String message ) {
warn( message );
System.exit( 1 );
}
/** Terminate application if there have been any warnings
*/
public static void quitIfAny() {
if (errorCount > 0) System.exit( 1 );
}
}
xxxxxxxxxx
cat > MyScanner.java <<\xxxxxxxxxx
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
/**
* Wrapper or Adapter for scanners that integrates error handling
* @author Douglas Jones
* @version 10/27/2020
* Status: Stable code, but probably not as good as it could be
* @see java.util.Scanner
* @see Error
*/
public class MyScanner {
private Scanner self; // the scanner this object wraps
/**
* Parameter carrier interface for deferred string construction
* Wsed only for error message parameters to getXXX() methods.
* Most code that uses this will use it implicitly through
* a lambda expression.
*/
static interface ErrorMessage {
String myString();
}
/**
* Construct a new MyScanner to read from a file
* @param f, the file to read
* @exception FileNotFoundException thrown if the file cannot be opened
*/
public MyScanner( File f ) throws FileNotFoundException {
self = new Scanner( f );
}
// methods we wish could inherit from Scanner but can't beause it's final
public boolean hasNext() { return self.hasNext(); }
public boolean hasNext( String s ) { return self.hasNext( s ); }
public boolean hasNextFloat() { return self.hasNextFloat(); }
public boolean hasNextInt() { return self.hasNextInt(); }
public String next() { return self.next(); }
public float nextFloat() { return self.nextFloat(); }
public String nextLine() { return self.nextLine(); }
// new methods we add to this class
/** Get the next string, if one is available
* @param def the default value if no string is available
* @param msg the error message to print if no string is available
* @return the next token from the input, or def if none
*/
public String getNext( String def, ErrorMessage msg ) {
if (self.hasNext()) return self.next();
Error.warn( msg.myString() );
return def;
}
/** Get the next float, if one is available
* @param def the default value if no float is available
* @param msg the error message to print if no float is available
* @return the value of the next token from the input, or def if none
*/
public float getNextFloat( float def, ErrorMessage msg ) {
if (self.hasNextFloat()) return self.nextFloat();
Error.warn( msg.myString() );
return def;
}
/** Get the next int, if one is available
* @param def the default value if no int is available
* @param msg the error message to print if no int is available
* @return the value of the next token from the input, or def if none
*/
public int getNextInt( int def, ErrorMessage msg ) {
if (self.hasNextInt()) return self.nextInt();
Error.warn( msg.myString() );
return def;
}
}
xxxxxxxxxx
cat > MyRandom.java <<\xxxxxxxxxx
import java.util.Random;
/**
* Singleton wrapper for Java's Random class
* @author Douglas Jones
* @version 10/27/2020
* Status: Fairly stable, but other probability distributions could be added
* @see java.util.Random
*/
public class MyRandom extends Random {
private MyRandom() {
// uncomment exactly one of the following!
super(); // let Java pick a random seed
// super( 3004 ); // set seed so we can debug
}
/** the only stream visible to users
* users can use the stream field or
* the stream() method interchangably.
*/
public static final MyRandom stream = new MyRandom();
/** an alternate way to expose users to the stream
* @return handle on the stream
* users can use the stream field or
* the stream() method interchangably.
*/
public static MyRandom stream() {
return stream;
}
/** get the next exponentially distributed pseudo-random number
* @param mean, the mean value of the distribution
* @return the next number drawn from this distribution
*/
public double nextExponential( double mean ) {
return -Math.log( nextDouble() ) * mean;
}
}
xxxxxxxxxx
cat > Simulator.java <<\xxxxxxxxxx
import java.util.PriorityQueue;
/**
* Framework for discrete event simulation.
* @author Douglas Jones
* @version 11/10/2020 -- new simulation framework
* Status: New code! Unstable?
*/
public abstract class Simulator {
private Simulator(){} // prevent anyone from instantiating this class
/** Users create and schedule subclasses of events
*/
public static abstract class Event {
/** The time of the event, set by the constructor */
public final double time; // the time of this event
/** Construct a new event and set its time
* @param t, the event's time
*/
Event( double t ) {
time = t;
}
/** What to do when this event is triggered
* Within trigger, this.time is the time of this event,
* Each subclass of event must provide a trigger method.
*/
public abstract void trigger(); // what to do at that time
}
private static PriorityQueue eventSet
= new PriorityQueue (
(Event e1, Event e2)-> Double.compare( e1.time, e2.time )
);
/** Call schedule to make an event happen at its time.
* Users create events with trigger method and a time, then schedule it
*/
static void schedule( Event e ) {
eventSet.add( e );
}
/** run the simulation.
* Call run() after scheduling some initial events
* to run the simulation.
* This becomes the main loop of the program; typically, some scheduled
* event will terminate the program by calling System.exit().
*/
static void run() {
while (!eventSet.isEmpty()) {
eventSet.remove().trigger();
}
}
}
xxxxxxxxxx
cat > Road.java <<\xxxxxxxxxx
import java.util.LinkedList;
import java.util.Iterator;
/**
* Roads connect intersections
* @author Douglas Jones
* @version 11/11/2020 -- new simulation framework
* Status: Works, but see BUG notices
* @see Intersection
*/
public class Road {
public static class ConstructorFail extends Exception {};
// instance variables
private final double travelTime;
private final Intersection destination;
private final Intersection source;
private final int queueNumber;
// the collection of all instances
private static final LinkedList allRoads = new LinkedList ();
/** The only constructor
* @param sc MyScanner from which description comes
* @throws ConstructorFail if the Road cannot be constructed
* Input format scanned from sc: source-name destination-name travel-time
*
where source-name is the name of the source intersection and
*
destination-name is the name of the destination intersection and
*
travel-time is a floating point time in seconds
*/
public Road( MyScanner sc ) throws ConstructorFail {
// keyword Road was already scanned
final String src; // where does it come from
final String dst; // where does it go
src = sc.getNext( "???", () -> "road: from missing source" );
dst = sc.getNext(
"???", () -> "road " + src + ": to missing destination"
);
travelTime = sc.getNextFloat(
Float.NaN,
() -> "road " + src + " " + dst + ": missing travel time"
);
if (sc.hasNextInt()) { // get the queue number
queueNumber = sc.getNextInt(
-1,
() -> "road " + src + " " + dst + " " + travelTime
+ ": missing travel time"
);
} else {
queueNumber = -1;
}
if ((src == "???") || (dst == "???") || Double.isNaN( travelTime )) {
throw new ConstructorFail();
}
destination = Intersection.lookup( dst );
if (destination == null) {
Error.warn( "road " + src + " " + dst + " undefined: " + dst );
throw new ConstructorFail();
}
source = Intersection.lookup( src );
if (source == null) {
Error.warn( "road " + src + " " + dst + " undefined: " + src );
throw new ConstructorFail();
}
if (travelTime < 0.0) {
Error.warn( this.toString() + ": negative travel time?" );
throw new ConstructorFail();
}
allRoads.add( this ); // this is the only place items are added!
destination.addIncoming( this, queueNumber ); // connect the road
source.addOutgoing( this ); // connect the road
}
/** Primarily for debugging
* @return textual name and travel time of the road
*/
public String toString() {
return
"Road " + source.name + " " + destination.name + " " + travelTime;
// BUG throws exception when source/destination is null
}
/** Allow outsiders to iterate over all roads
* @return textual name and travel time of the road
*/
public static Iterator iterator() {
return allRoads.iterator();
}
// Simulation methods
/** Signal that a vehicle is entering this road
* @param t, the time of entry to this road
*/
public void enter( double t ) {
// BUG -- this.occupants = this.occupants + 1
// BUG -- congestion ?=? this.occupants/travelTime (vehicles/time)
class MyEvent extends Simulator.Event {
MyEvent() { super( t ); }
public void trigger() {
leave( time );
}
}
Simulator.schedule( new MyEvent() );
}
/** Event indicating that a vehicle is leaving this road
* @param time of departure from this road
* called only from enter!
*/
private void leave( double time ) {
// BUG -- this.occupants = this.occupants - 1
// tell the destination intersection that we're arriving
this.destination.enter( time, this.queueNumber );
}
}
xxxxxxxxxx
cat > Intersection.java <<\xxxxxxxxxx
import java.util.LinkedList;
import java.util.Iterator;
/**
* Intersections are connected by roads, only subclasses are ever instantiated
* @author Douglas Jones
* @version 11/11/2020 -- new simulation framework
* Status: Works
* @see Road
*/
public abstract class Intersection {
public static class ConstructorFail extends Exception {};
// instance variables
final String name;
final Double travelTime;
private final LinkedList outgoing = new LinkedList ();
protected final LinkedList incoming = new LinkedList ();
private static final MyRandom rand = MyRandom.stream();
// the collection of all instances
private static final LinkedList allIntersections =
new LinkedList ();
/** Constructor needed to initialize final fields
* @param name of the intersection
*/
Intersection( String name, double travelTime ) {
this.name = name;
this.travelTime = travelTime;
}
/** The the factory for intersections
* @param sc, scanner from which the Intersection description is scanned
* @return the Intersection constructed from that description
* @throws ConstructorFail when it cannot construct an Intersection
* Input format scanned from sc: name sub ...
*
where name is a string
*
where sub is a string naming the subclass
*
where ... signifes details the deals with, if any
*/
public static Intersection factory( MyScanner sc ) throws ConstructorFail {
// pick off name of intersection;
String name = sc.getNext( "???", () -> "Intersection name missing" );
double time = sc.getNextFloat(
Float.NaN, () -> "Intersection " + name + ": missing travel time"
);
if ((name == "???") || Double.isNaN( time )) {
throw new ConstructorFail();
}
if (lookup( name ) != null) {
Error.warn( "Intersection " + name + ": redefined" );
throw new ConstructorFail();
}
if (time < 0.0) {
Error.warn(
"Intersection " + name + " " + time + ": negative travel time?"
);
throw new ConstructorFail();
}
Intersection self; // this is the intersection the factory is working on
// find out what kind of intersection call right constructor
if (sc.hasNext( "stoplight" )) {
sc.next(); // skip keyword stoplight
self = new StopLight( sc, name, time );
} else if (sc.hasNext( "source" )) {
sc.next(); // skip keyword source
self = new Source( sc, name, time );
} else if (sc.hasNext( "sink" )) {
sc.next(); // skip keyword source
self = new Sink( sc, name, time );
} else { // plain uncontrolled intersection
self = new NoStop( sc, name, time );
}
allIntersections.add( self );
return self;
}
/** Primarily for debugging
* @return textual name and travel time of the road
*/
public String toString() {
return "Intersection " + name + " " + travelTime;
}
/** Allow outsiders to iterate over all roads
* @return textual name and travel time of the road
*/
public static Iterator iterator() {
return allIntersections.iterator();
}
/** Allow finding intersections by name
* @param n, the textual name of the intersection
* @return the Intersection with that name
*/
public static Intersection lookup( String n ) {
for (Intersection i: allIntersections) {
if (i.name.equals( n )) return i;
}
return null;
}
/** Allow a road to connect to this intersection
* @param r the road that goes here
* @param q the incoming road's queue number
*/
public abstract void addIncoming( Road r, int q );
/** Allow a road to connect form this intersection
* @param r the road that leaves here
*/
public void addOutgoing( Road r ) {
this.outgoing.add( r );
}
// Simulation methods
/** Allow vehicles in an intersection to pick outgoing road
* @return one of the roads at random
*/
protected Road pickOutgoing() {
return outgoing.get( rand.nextInt( outgoing.size() ) );
}
/** Signal that a vehicle on a road is entering this intersection
* @param time of vehicle entry
* @param queue vehicle enters
*/
public abstract void enter( double time, int queue );
}
xxxxxxxxxx
cat > NoStop.java <<\xxxxxxxxxx
/**
* Uncontrolled Intersections
* @author Douglas Jones
* @version 11/10/2020 -- new simulation framework
* Status: NoStop intersections might work, but new framework
* @see Intersection
*/
public class NoStop extends Intersection {
protected int waiting = 0;
/** Constructor
* @param sc scanner from which to scan any extra details
* @param name the name of the intersection
*/
NoStop( MyScanner sc, String name, double time ) {
super( name, time ); // needed because of final fields
}
/** Allow a road to connect to this intersection
* @param r the road that goes here
* @param q the incoming road's queue number
*/
public void addIncoming( Road r, int q ) {
this.incoming.add( r );
if (q > 0) {
Error.warn( r.toString() + ": positive queue number not allowed" );
}
}
/* Primarily for debugging
* @return textual name and attributes of the intersection
*/
// public String toString() {
// // the superclass version does this!
// }
// simulation methods
/** Signal that a vehicle on a road is entering this intersection
* @param t, the time of vehicle entry
* @param queue vehicle enters (ignored)
*/
public void enter( double t, int queue ) {
waiting = waiting + 1; // enqueue this car
if (waiting <= 1) { // am I the only one in the queue, send me on
class MyEvent extends Simulator.Event {
MyEvent() { super( t ); }
public void trigger() {
leave( time );
}
}
Simulator.schedule( new MyEvent() );
}
System.out.println( this.toString() +": entered at t ="+ t );
}
/** Event that a vehicle in this intersection is leaving for a road
* @param t, the time of vehicle departure
*/
protected void leave( double t ) {
waiting = waiting - 1; // dequeue one car (or not, if queue emptied)
if (waiting > 0) { // if I dequeued a car, send it through
class MyEvent extends Simulator.Event {
MyEvent() { super( t ); }
public void trigger() {
leave( time );
}
}
Simulator.schedule( new MyEvent() );
}
this.pickOutgoing().enter( t );
}
}
xxxxxxxxxx
cat > StopLight.java <<\xxxxxxxxxx
/**
* Stoplight intersection
* @author Douglas Jones
* @version 11/11/2020 -- new simulation framework
* Status: Works
* @see Intersection
*/
public class StopLight extends Intersection {
// instance variables set on construction
private final double period; // interval between light changes
private final int queueCount; // number of distinct incoming queues
// instance variables change during simulation
private boolean occupied = false;// is a car currently in this intersection
private final int waiting[]; // one queue per queueCount
private int greenDir; // queue number for which light is green
// we need a stream of random numbers
private static final MyRandom rand = MyRandom.stream();
/** Constructor
* @param sc scanner from which to scan any extra details
* @param name the name of the intersection
*/
StopLight( MyScanner sc, String name, double time ) {
super( name, time ); // needed because Interesection.name is final
period = sc.getNextFloat( 0.1F,
()-> super.toString() +" stoplight: expected stoplight period"
);
queueCount = sc.getNextInt( -1,
()-> super.toString() +" stoplight "+ period
+": expected queue count"
);
if (period <= 0) {
Error.warn( this.toString() + ": stoplight period not positive" );
} else {
class MyEvent extends Simulator.Event {
MyEvent() { super( period ); }
// BUG -- period above should scale by random from 0.0 and 1.0
public void trigger() {
lightChange( time );
}
}
Simulator.schedule( new MyEvent() );
}
if (queueCount > 0) {
waiting = new int[ queueCount ];
greenDir = rand.nextInt( queueCount );
} else {
Error.warn( this.toString() + ": queue count not positive" );
waiting = null;
greenDir = 0;
}
}
/** Allow a road to connect to this intersection
* @param r the road that goes here
* @param q the incoming road's queue number
*/
public void addIncoming( Road r, int q ) {
this.incoming.add( r );
if ((q < 0) || (q >= queueCount)) {
Error.warn( r.toString() + ": queue number out of bounds" );
}
}
/** Primarily for debugging
* @return textual name and attributes of the intersection
*/
public String toString() {
return super.toString() + " Stoplight " + period + " " + queueCount;
}
// simulation methods for StopLight
private void lightChange( double t ) {
{
class MyEvent extends Simulator.Event {
MyEvent() { super( t + period ); }
public void trigger() {
lightChange( time );
}
}
Simulator.schedule( new MyEvent() );
}
// update the light direction
greenDir = (greenDir + 1) % queueCount;
if ((waiting[greenDir] > 0) && !occupied) {
class MyEvent extends Simulator.Event {
MyEvent() { super( t + travelTime ); }
public void trigger() {
leave( time );
}
}
Simulator.schedule( new MyEvent() );
waiting[greenDir] = waiting[greenDir] - 1;
occupied = true;
}
System.out.println( this.toString() +": light change at t ="+ t );
}
/** Signal that a vehicle on a road is entering this intersection
* @param t, the time of vehicle entry
* @param queue vehicle enters at this stop light
*/
public void enter( double t, int queue ) {
// do I have to wait?
if ((waiting[queue] <= 0) && (greenDir == queue) && !occupied ) {
// do not wait
class MyEvent extends Simulator.Event {
MyEvent() { super( t + travelTime ); }
public void trigger() {
leave( time );
}
}
Simulator.schedule( new MyEvent() );
occupied = true;
} else {
// wait
waiting[queue] = waiting[queue] + 1;
}
}
/** Event simulation of a vehicle leaving this stop light
* @param t, the time of vehicle exit
*/
public void leave( double t ) {
assert occupied == true;
// does my departure unblock another vehicle
if (waiting[greenDir] > 0) {
class MyEvent extends Simulator.Event {
MyEvent() { super( t + travelTime ); }
public void trigger() {
leave( time );
}
}
Simulator.schedule( new MyEvent() );
waiting[greenDir] = waiting[greenDir] - 1;
// assert occupied still true
} else {
occupied = false;
}
this.pickOutgoing().enter( t );
}
}
xxxxxxxxxx
cat > Sink.java <<\xxxxxxxxxx
/**
* Sink intersections that consume traffic
* @author Douglas Jones
* @version 10/27/2020
* Status: works
* @see Intersection
*/
public class Sink extends Intersection {
/** Constructor
* @param sc scanner from which to scan any extra details
* @param name the name of the intersection
*/
Sink( MyScanner sc, String name, double time ) {
super( name, time );
// sinks don't seem to need anything else
}
/** Allow a road to connect to this intersection
* @param r the road that goes here
* @param q the incoming road's queue number (ignored)
*/
public void addIncoming( Road r, int q ) {
this.incoming.add( r );
if (q >= 0) {
Error.warn( r.toString() + ": queue number not allowed" );
}
}
/** Primarily for debugging
* @return textual name and attributes of the intersection
*/
public String toString() {
return super.toString() + " Sink";
}
/** Signal that a vehicle on a road is entering this intersection
* @param time of vehicle entry
* @param queue queue the vehicle enters (ignored)
*/
public void enter( double time, int queue ) {
// BUG -- missing code for entry to intersection!
System.out.println( this.toString() +": entered at t ="+ time );
}
}
xxxxxxxxxx
cat > Source.java <<\xxxxxxxxxx
/**
* Source intersections that produce traffic
* @author Douglas Jones
* @version 11/10/2020 -- new simulation framework
* Status: Works
* @see Intersection
*/
public class Source extends NoStop {
private final double period; // average interval between vehicle production
private static MyRandom rand = MyRandom.stream; // we need randomness
/** Constructor
* @param sc scanner from which to scan any extra details
* @param name the name of the intersection
*/
Source( MyScanner sc, String name, double time ) {
super( sc, name, time );
period = sc.getNextFloat( 0.1F,
()-> super.toString() +" source: expected source average interval"
);
if (period <= 0) {
Error.warn( this.toString() + ": source interval not positive" );
} else {
class MyEvent extends Simulator.Event {
MyEvent() { super( rand.nextExponential( period ) ); }
public void trigger() {
produce( time );
}
}
Simulator.schedule( new MyEvent() );
}
}
/** Primarily for debugging
* @return textual name and attributes of the intersection
*/
public String toString() {
return super.toString() + " Source " + period;
}
// simulation methods for Source
private void produce( double t ) {
this.enter( t, -1 );
class MyEvent extends Simulator.Event {
MyEvent() { super( t + rand.nextExponential( period ) ); }
public void trigger() {
produce( time );
}
}
Simulator.schedule( new MyEvent() );
}
}
xxxxxxxxxx
cat > RoadNetwork.java <<\xxxxxxxxxx
import java.util.Iterator;
import java.io.File;
import java.io.FileNotFoundException;
/**
* Main class builds model and will someday simulate it
* @author Douglas Jones
* @version 11/11/2020 -- new simulation framework
* Status: Works
* @see Road
* @see Intersection
*/
public abstract class RoadNetwork {
private static void readNetwork( MyScanner sc ) {
while (sc.hasNext()) {
// until the input file is finished
String command = sc.next();
if ("intersection".equals( command )) {
try {
Intersection.factory( sc );
} catch ( Intersection.ConstructorFail e ) {};
} else if ("road".equals( command )) {
try {
new Road( sc );
} catch ( Road.ConstructorFail e ) {};
} else if ("endtime".equals( command )) {
float endTime = sc.getNextFloat(
0, ()-> "endtime: time expected"
);
class MyEvent extends Simulator.Event {
MyEvent() { super( endTime ); }
public void trigger() {
System.exit( 0 );
}
}
Simulator.schedule( new MyEvent() );
} else if ("//".equals( command )) {
sc.nextLine();
} else {
Error.warn( "unknown command: " + command );
}
}
}
private static void writeNetwork() {
for (Iterator i = Intersection.iterator(); i.hasNext(); ){
System.out.println( i.next().toString() );
}
for (Iterator i = Road.iterator(); i.hasNext(); ){
System.out.println( i.next().toString() );
}
}
public static void main( String[] args ) {
if (args.length < 1) {
Error.fatal( "Missing file name argument" );
} else try {
readNetwork( new MyScanner( new File( args[0] ) ) );
} catch ( FileNotFoundException e ) {
Error.fatal( "Can't open file: " + args[0] );
}
if (args.length > 1) {
try {
float endTime = Float.parseFloat( args[1] );
class MyEvent extends Simulator.Event {
MyEvent() { super( endTime ); }
public void trigger() {
System.exit( 0 );
}
}
Simulator.schedule( new MyEvent() );
} catch ( NumberFormatException e ) {
Error.fatal(
"RoadNetwork "+ args[0] +" "+ args[1] +": Negative end?"
);
}
}
Error.quitIfAny();
writeNetwork(); // BUG -- this code is for debugging only
Simulator.run();
}
}
xxxxxxxxxx
cat > testfile <<\xxxxxxxxxx
intersection A 0.1 source 10.0
intersection B 1.0 stoplight 20.0 2
intersection C 1.0 sink
road A B 0.4 0
road A B 0.4 1
road B B 10.4 0
road B B 10.4 1
road B C 0.4
road B C 0.4
endtime 40.0
xxxxxxxxxx