# 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