/* Epidemic Simulator * Author: Douglas Jones * Status: MP7 solution; it works, but see BUG notices * Version: 10/30/2020 */ import java.lang.Math; import java.lang.NumberFormatException; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.PriorityQueue; import java.util.Random; import java.util.regex.Pattern; import java.util.Scanner; import java.io.File; import java.io.FileNotFoundException; // Utility classes /** * Error handling */ class Error{ private static int errorCount = 0; private static final int errorLimit = 10; /** Report a warning to System.err * @param message the text of the warning */ public static void warn( String message ) { System.err.println( message ); errorCount = errorCount + 1; if (errorCount > errorLimit) System.exit( 1 ); } /** Report a fatal error to System.err * @param message the text reporting the error * Note that this code exits the program with an error indication */ public static void fatal( String message ) { warn( message ); System.exit( 1 ); } /** Quit if there were any errors */ public static void quitIfAny() { if (errorCount > 0) System.exit( 1 ); } } /** * Wrapper or Adapter for Scanners that integrates error handling * @see java.util.Scanner * @see Error */ class MyScanner { Scanner self; // the scanner this object wraps /** * Parameter carrier class for deferred string construction * used only for error message parameters to getXXX() methods */ public static interface ErrorMessage { String myString(); } // patterns for popular scannables, compiled just once static Pattern delimPat = Pattern.compile( "([ \t\r\n]|(//[\\S \t]*\n))*" ); // allow empty delimiters, and allow Java style comments static Pattern intPat = Pattern.compile( "-?[0-9]*" ); // integers static Pattern realPat = Pattern.compile( "-?\\d*\\.?\\d*(E(\\+|-)?\\d*)?" ); /** Construct a MyScanner to read from a file * @param f the file to read from * @throws FileNotFoundException if the file could not be read */ public MyScanner( File f ) throws FileNotFoundException { self = new Scanner( f ); } // methods we wish could inherit from Scanner but can't beause it's final // BUG -- to properly handle end of line delimiters, these need redefinition public boolean hasNext( String s ) { return self.hasNext( s ); } public boolean hasNextDouble() { return self.hasNextFloat(); } public boolean hasNextFloat() { return self.hasNextFloat(); } public boolean hasNextInt() { return self.hasNextInt(); } public String next( String s ) { return self.next( s ); } public float nextDouble() { return self.nextFloat(); } public float nextFloat() { return self.nextFloat(); } public int nextInt() { return self.nextInt(); } public String nextLine() { return self.nextLine(); } // redefined methods from class Scanner /** Is there a next token? * but first skip optional extended delimiters * @return true if there is a token, otherwise false */ public boolean hasNext() { self.skip( delimPat ); // skip the delimiter, if any return self.hasNext(); } /** Get the next token, * but first skip optional extended delimiters * @return the token as a string */ public String next() { self.skip( delimPat ); // skip the delimiter, if any return self.next(); } // 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 token as a String or the default */ public String getNext( String def, ErrorMessage msg ) { if (self.hasNext()) return self.next(); Error.warn( msg.myString() ); return def; } /** Get the next match to pattern, if one is available * @param pat the pattern string we are trying to match * @param def the default value if no match available * @param msg the error message to print if no match available * @return the token as a String or the default */ public String getNext( String pat, String def, ErrorMessage msg ) { self.skip( delimPat ); // skip the delimiter, if any self.skip( "(" + pat + ")?" ); // skip the pattern if present String next = self.match().group(); if (!next.isEmpty()) { // non-empty means next thing matched pat return next; } else { Error.warn( msg.myString() ); return def; } } /** Get the next double, if one is available * @param def the default value if no float is available * @param msg the error message to print if no double is available * @return the token as a double or the default */ public double getNextDouble( double def, ErrorMessage msg ) { self.skip( delimPat ); // skip the delimiter, if any self.skip( realPat ); // skip the float, if any String next = self.match().group(); try { return Double.parseDouble( next ); } catch ( NumberFormatException e ) { 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 token as a float or the default */ public float getNextFloat( float def, ErrorMessage msg ) { self.skip( delimPat ); // skip the delimiter, if any self.skip( realPat ); // skip the float, if any String next = self.match().group(); try { return Float.parseFloat( next ); } catch ( NumberFormatException e ) { 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 token as an int or the default */ public int getNextInt( int def, ErrorMessage msg ) { self.skip( delimPat ); // skip the delimiter, if any self.skip( intPat ); // skip the float, if any String next = self.match().group(); try { return Integer.parseInt( next ); } catch ( NumberFormatException e ) { Error.warn( msg.myString() ); return def; } } } /** Singleton wrapper for Java's Random class */ 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 */ static final MyRandom stream = new MyRandom(); /** an alternate way to expose users to the stream * @return handle on the stream */ public static MyRandom stream() { return stream; } /** get the next exponentially distributed pseudo-random number * @param mean value of the distribution * @return the next number drawn from this distribution */ public double nextExponential( double mean ) { return -Math.log( this.nextDouble() ) * mean; } /** get the next log-normally distributed pseudo-random number * @param median value of the distribution * @param scatter of the distribution * @return the next number drawn from this distribution */ public double nextLogNormal( double median, double scatter ) { double sigma = Math.log( (scatter + median) / median ); return Math.exp( sigma * this.nextGaussian() ) * median; } } /** Framework for discrete event simulation. */ abstract class Simulator { private Simulator(){} // prevent anyone from instantiating this class // BUG -- this may not be the right place to specify time units public static final double day = 1.0; public static final double hour = day / 24.0; public static final double minute = day / (24.0 * 60.0); public static final double second = day / (24.0 * 60.0 * 60.0); public static final double week = day * 7; /** Interface to allow lambda parameters to schedule() * as such, no external code ever uses Action */ public interface Action { // actions contain the specific code of each event void trigger( double time ); } private static class Event { public double time; // the time of this event public Action act; // 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 act happen at time. * Users typically pass the action as a lambda expression: *
     *  Simulator.schedule( t, ( double time )-> method( ... time ... ) )
     *  
*/ static void schedule( double time, Action act ) { Event e = new Event(); e.time = time; e.act = act; eventSet.add( e ); } /** run the simulation. * Call run() after scheduling some initial events to run the simulation. */ static void run() { while (!eventSet.isEmpty()) { Event e = eventSet.remove(); e.act.trigger( e.time ); } } } // Model classes /** * People occupy places * @see Place * @see Employee */ class Person { // private stuff needed for instances enum States { // BUG -- does this go here? Is it public or private? uninfected, latent, bedridden, recovered, dead // the order of the above is significant: >= uninfected is infected } // fixed attributes of each instance private final HomePlace home; // all people have homes public final String name; // all people have names // instance variables protected Place place; // when not in transit, where the person is public States infectionState; // all people have infection states // the collection of all instances private static final LinkedList allPeople = new LinkedList (); // need a source of random numbers private static final MyRandom rand = MyRandom.stream(); /** The only constructor * @param h the home of the newly constructed person */ public Person( HomePlace h ) { name = super.toString(); home = h; place = h; // all people start out at home infectionState = States.uninfected; h.addResident( this ); allPeople.add( this ); // this is the only place items are added! } /** Primarily for debugging * @return textual name and home of this person */ public String toString() { return name ;// DEBUG + " " + home.name + " " + infectionState; } /** Shuffle the population * This allows correlations between attributes of people to be broken */ public static void shuffle() { Collections.shuffle( allPeople, rand ); } /** Allow outsiders to iterate over all people * @return an iterator over people */ public static Iterator iterator() { return allPeople.iterator(); } // simulation methods /** Infect a person * called when circumstances call for a person to become infected */ public void infect() { // BUG -- what if this person is already infected? infectionState = States.latent; // BUG -- when simulation is added, will this launch a disease process? } /** Make a person arrive at a new place * @param p new place * @param t the current time * scheduled */ private void arriveAt( double time, Place p ) { p.arrive( this, time ); this.place = p; } /** Move a person to a new place * @param place where the person travels * @param time at which the move will be completed takes place * BUG -- if time was the time the trip started: * travelTo could do the call to this.place.depart() * and it could compute the travel time */ public void travelTo( Place p, double time ) { this.place = null; Simulator.schedule( time, (double t) -> arriveAt( t, p ) ); } /** Simulate the trip home from wherever * @param time of departure */ public void goHome( double time ) { double travelTime = rand.nextLogNormal( 20 * Simulator.minute, // mean travel time 3 * Simulator.minute // scatter in travel time ); this.place.depart( this, time ); this.travelTo( this.home, time + travelTime ); } } /** * Employees are People who work * @see Person * @see WorkPlace */ class Employee extends Person { // instance variables private WorkPlace job; // employees have WorkPlaces // can't be final because set post constructor // need a source of random numbers private static final MyRandom rand = MyRandom.stream(); /** The only constructor * @param h the HomePlace of the newly constructed Employee * Note that employees are created without well-defined workplaces */ public Employee( HomePlace h ) { super( h ); // construct the base person job = null; // go to work every day at 25 minutes before 8 AM Simulator.schedule( (8 * Simulator.hour) - (25 * Simulator.minute), (double t)-> goToWork( t ) ); } /** Set workplace of employee * @param w the workPlace of the newly constructed Employee * No employee's workplace may be set more than once */ public void setWorkplace( WorkPlace w ) { assert job == null; job = w; w.addEmployee( this ); } /** Primarily for debugging * @return textual name home and employer of this person */ public String toString() { return super.toString() ;// DEBUG + " " + job.name; } // simulation methods /** Simulate the daily trip to work * @param time of departure */ private void goToWork( double time ) { double travelTime = rand.nextLogNormal( 20 * Simulator.minute, // mean travel time 3 * Simulator.minute // scatter in travel time ); this.place.depart( this, time ); this.travelTo( this.job, time + travelTime ); // go to work every day at the same time Simulator.schedule( time + Simulator.day, (double t)-> goToWork( t ) ); } } /** * Places are occupied by people * @see HomePlace * @see WorkPlace */ abstract class Place { // instance variables public final String name; protected final LinkedList occupants = new LinkedList<> (); // contructor (effectively protected Place() { name = super.toString(); allPlaces.add( this ); } // tools for moving people in and out of places /** a person arrives at this place * @param p, the person who arrives */ public void arrive( Person p, double time ) { occupants.add( p ); System.out.println( (Object)p.toString() + " arrives " + (Object)this + " at " + time ); // DEBUG } /** a person leaves from this place * @param p, the person who leaves */ public void depart( Person p, double time ) { boolean wasPresent = occupants.remove( p ); assert wasPresent: "p=" + p + " this=" + this; assert !occupants.contains( p ): "p=" + p + " this=" + this; System.out.println( (Object)p.toString() + " departs " + (Object)this + " at " + time ); // DEBUG } // the collection of all instances private static final LinkedList allPlaces = new LinkedList (); /** Allow outsiders to iterate over all places * @return an iterator over places */ public static Iterator iterator() { return allPlaces.iterator(); } } /** * HomePlaces are occupied by any type of person * @see Place * @see Person */ class HomePlace extends Place { private final LinkedList residents = new LinkedList (); /** The only constructor for Place * Places are constructed with no occupants */ public HomePlace() { super(); // initialize the underlying place } /** Add a resident to a place * Should only be called from the person constructor * @param r a Person, the new resident */ public void addResident( Person r ) { residents.add( r ); occupants.add( r ); // no need to check to see if the person already lives there? } /** Primarily for debugging * @return textual name and residents of the home */ public String toString() { String res = name; // DEBUG for (Person p: residents) { res = res + " " + p.name; } return res; } } /** * WorkPlaces are occupied by employees * @see Place * @see Employee */ class WorkPlace extends Place { private final LinkedList employees = new LinkedList (); /** The only constructor for WorkPlace * WorkPlaces are constructed with no residents */ public WorkPlace() { super(); // initialize the underlying place // make the workplace open at 8 AM Simulator.schedule( 8 * Simulator.hour, (double t)-> this.open( t ) ); } /** Add an employee to a WorkPlace * Should only be called from the person constructor * @param r an Employee, the new worker */ public void addEmployee( Employee r ) { employees.add( r ); // no need to check to see if the person already works there? } /** Primarily for debugging * @return textual name and employees of the workplace */ public String toString() { String res = name; // DEBUG for (Employee p: employees) { res = res + " " + p.name; } return res; } // simulation methods /** open the workplace for business * @param t the time of day * Note that this workplace will close itself 8 hours later, and * opening plus closing should create a 24-hour cycle. * @see close */ private void open( double time ) { // System.out.println( this.toString() + " opened at time " + time ); // BUG -- we should probably do something useful too // close this workplace 8 hours later Simulator.schedule( time + 8 * Simulator.hour, (double t)-> this.close( t ) ); } /** close the workplace for the day * @param t the time of day * note that this workplace will reopen 16 hours later, and * opening plus closing should create a 24-hour cycle. * @see open */ private void close( double time ) { //System.out.println( this.toString() + " closed at time " + time ); // open this workplace 16 hours later, with no attention to weekends Simulator.schedule( time + 16 * Simulator.hour, /* opens 8 hours later */ (double t)-> this.open( t ) ); // send everyone home for (Person p: occupants) { // schedule it for now in order to avoid modifying list inside loop // not doing this gives risk of ConcurrentModificationException Simulator.schedule( time, (double t)-> p.goHome( t ) ); } } } /** * Main class builds model and will someday simulate it * @see Person * @see Place */ public class Epidemic { // the following are set by readCommunity and used by buildCommunity // default values are used to check for failure to initialize static int pop = -1; /* the target population */ static double houseMed = -1; /* median household size */ static double houseSc = -1; /* household size scatter */ static double workMed = -1; /* median workplace size */ static double workSc = -1; /* workplace size scatter */ static int infected = -1; /* the target number of infected people */ static double employed = -1; /* the likelihood that someone is employed */ /** Read and check the simulation parameters * @param sc the scanner to read the community description from * Called only from the main method. */ private static void readCommunity( MyScanner sc ) { while (sc.hasNext()) { // until the input file is finished String command = sc.next(); if ("pop".equals( command )) { if (pop > 0) Error.warn( "population already set" ); pop = sc.getNextInt( 1, ()-> "pop with no argument" ); sc.getNext( ";", "", ()-> "pop " +pop+ ": missed semicolon" ); if (pop < 1) { /* sanity check on value given */ Error.warn( "pop " +pop+ ": non-positive population?" ); pop = 0; } } else if ("house".equals( command )) { if (houseMed > 0) Error.warn( "household size already set" ); if (houseSc >= 0) Error.warn( "household scatter already set" ); houseMed = sc.getNextDouble( 1, ()-> "house with no argument" ); sc.getNext( ",", "", ()-> "house "+houseMed+": missed comma" ); houseSc = sc.getNextDouble( 0, ()-> "house "+houseMed+", missing argument " ); sc.getNext( ";", "", ()-> "house "+houseMed+", "+houseSc+": missed semicolon" ); if (houseMed < 1) { /* sanity check on value given */ Error.warn( "house "+houseMed+", "+houseSc+": median nonpositive?" ); houseMed = 0; } if (houseSc < 0) { /* sanity check on value given */ Error.warn( "house "+houseMed+", "+houseSc+": scatter negative?" ); houseSc = 0; } } else if ("workplace".equals( command )) { if (workMed > 0) Error.warn( "workplace size already set" ); if (workSc >= 0) Error.warn( "workplace scatter already set" ); workMed = sc.getNextDouble( 1, ()-> "workplace with no argument" ); sc.getNext( ",", "", ()-> "workplace "+workMed+": missed comma" ); workSc = sc.getNextDouble( 0, ()-> "workplace "+workMed+", missed argument " ); sc.getNext( ";", "", ()-> "workplace "+workMed+", "+workSc+": missed semicolon" ); if (workMed < 1) { /* sanity check on value given */ Error.warn( "workplace "+workMed+", "+workSc+": median nonpositive?" ); workMed = 0; } if (workSc < 0) { /* sanity check on value given */ Error.warn( "workplace "+workMed+", "+workSc+": scatter negative?" ); workSc = 0; } } else if ("infected".equals( command )) { if (infected > 0) Error.warn( "infected already set" ); infected = sc.getNextInt( 1, ()-> "infected with no argument" ); sc.getNext( ";", "", ()-> "infected " +infected+ ": missed semicolon" ); if (infected < 0) { /* sanity check on value given */ Error.warn( "infected "+infected+": negative value?" ); infected = 0; } if (infected > pop) { /* sanity check on value given */ Error.warn( "infected "+infected+": greater than population?" ); infected = pop; } } else if ("employed".equals( command )) { if (employed >= 0) Error.warn( "employed rate already set" ); employed = sc.getNextDouble( 1, ()-> "employed with no argument" ); sc.getNext( ";", "", ()-> "employed "+employed+": missed semicolon" ); if (employed < 0) { /* sanity check on value given */ Error.warn( "employed "+employed+": negative value?" ); employed = 0; } if (employed > 1) { /* sanity check on value given */ Error.warn( "employed "+employed+": greater than 1.0?" ); employed = 1.0; } } else if ("end".equals( command )) { Double endTime = sc.getNextDouble( 1, ()-> "end: floating point end time expected" ); if (endTime <= 0) { Error.warn( "end "+endTime+": non positive end of time?" ); } sc.getNext( ";", "", ()-> "end "+endTime+": missed semicolon" ); Simulator.schedule( endTime, (double t)->System.exit( 0 ) ); // BUG -- A better end mechanism would output a results report } else { Error.warn( "unknown command: "+command ); } } // BUG -- if there were errors, it might be best to quit now // check for complete initialization if (pop < 0) Error.warn( "population not initialized" ); if (houseMed < 0) Error.warn( "median household size not set" ); if (houseSc < 0) Error.warn( "household scatter not set" ); if (workMed < 0) Error.warn( "median workplace size not set" ); if (workSc < 0) Error.warn( "workplace scatter not set" ); if (infected < 0) Error.warn( "infected number not given" ); if (employed < 0) Error.warn( "employment rate not given" ); } /** Build a community that the simulation parameters describe * Called only from the main method. */ private static void buildCommunity() { // must always have a home available as we create people int currentHomeCapacity = 0; int currentWorkCapacity = 0; HomePlace currentHome = null; WorkPlace currentWork = null; // need a source of random numbers final MyRandom rand = MyRandom.stream(); // create the population for (int i = 0; i < pop; i++) { Person p = null; if (currentHomeCapacity < 1) { // must create a new home currentHome = new HomePlace(); currentHomeCapacity = (int)Math.ceil( rand.nextLogNormal( houseMed, houseSc ) ); } currentHomeCapacity = currentHomeCapacity - 1; // create the right kind of person if (rand.nextDouble() <= employed) { // this is as an employee p = new Employee( currentHome ); } else { // this is an unemployed generic person p = new Person( currentHome ); } // decide who to infect // note: pop - i = number of people not yet considered to infect // and infected = number we need to infect, always <= (pop - i) if (rand.nextInt( pop - i ) < infected) { p.infect(); infected = infected - 1; } } Person.shuffle(); // shuffle the population to break correlations // go through the population again for (Iterator i = Person.iterator(); i.hasNext(); ){ Person p = i.next(); // for each person if (p instanceof Employee) { Employee e = (Employee)p; if (currentWorkCapacity < 1) { // must create new workplace currentWork = new WorkPlace(); currentWorkCapacity = (int)Math.ceil( rand.nextLogNormal( workMed, workSc ) ); } currentWorkCapacity = currentWorkCapacity - 1; e.setWorkplace( currentWork ); } } } /** Output the community * Called only from the main method. * This code exists only for debugging. */ private static void writeCommunity() { System.out.println( "People" ); // Note: Not required in assignment for (Iterator i = Person.iterator(); i.hasNext(); ){ System.out.println( i.next().toString() ); } System.out.println( "Places" ); // Note: Not required in assignment for (Iterator i = Place.iterator(); i.hasNext(); ){ System.out.println( i.next().toString() ); } } /** The main method * This handles the command line arguments. * If the args are OK, it calls other methods to build and test a model. */ public static void main( String[] args ) { if (args.length < 1) { Error.fatal( "Missing file name argument\n" ); } else try { readCommunity( new MyScanner( new File( args[0] ) ) ); Error.quitIfAny(); buildCommunity(); // build what was read above // writeCommunity(); // DEBUG -- this is just for debugging Simulator.run(); } catch ( FileNotFoundException e) { Error.fatal( "Can't open file: " + args[0] + "\n" ); } } }