# 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 Thu 10 Dec 2020 10:27:08 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 Epidemic simulator Author: Douglas Jones Version: 12/8/2020 -- the 11/16/2020 version with MP12 changes to Person.java This distribution contains: -- README -- this file -- Makefile -- a makefile to build and test the simulator -- *.java -- the source files -- testepi -- input to the simulator See the makefile for a list of source files and their dependencies Shell command to build the simulator: make Shell command to build Javadoc web site: make html Shell command to test the simulator: make test This test runs the simulator for one simulated month, using a community of 1000 people, 10 of which are initially infected with a COVID-like disease. By the end of the month, on the order of 10 people will die and from 500 to 900 people will remain uninfected. The rate of disease spread through the community depends on a randomly constructed social network of family and job relationships, with a 50% employment rate where both homes and workplaces have random variations in their transmissivity. No two runs are likely to produce the same results. The community is describe in the file testepi, but currently, the disease characteristics are described by constants hard-coded into the simulator. There are bug notices in the code, in the form of comments with a // BUG header noting several places where it is clear that the code can be improved. The input community description file format needs a manual, but moving the disease characteristics into this file is an even higher priority. The logic of workplaces in the model could be replicated to support schools with students and stores with customers. The effect of mitigation strategies such as closing workplaces with more than some number of employees when the number of beridden people exceeds some threshold could be modeled by making people stay home from such places when these criteria are met. xxxxxxxxxx cat > Makefile <<\xxxxxxxxxx # Makefile for the Epidemic model # Author: Douglas Jones # Version: 2020-12-1 -- solution to MP11 # Usage # make -- builds Epidemic.class and everything else needed # make html -- builds the javadoc files # make clean -- cleans up all automatically generated files # make test -- runs java Epidemic testepi to demonstrate it ########## # all source files supportJava = Error.java MyScanner.java MyRandom.java Simulator.java peopleJava = Person.java Employee.java placesJava = Place.java HomePlace.java WorkPlace.java modelJava = $(peopleJava) $(placesJava) mainJava = Epidemic.java allJava = $(supportJava) $(modelJava) $(mainJava) ########## # all classes from the above supportJava = Error.class MyScanner.class MyRandom.class Simulator.class peopleClasses = Person.class Employee.class placesClasses = Place.java HomePlace.java WorkPlace.java modelClasses = $(peopleClasses) $(placesClasses) ########## # primary make target Epidemic.class: Epidemic.java $(modelClasses) $(supportClasses) javac Epidemic.java ########## # secondary make targets: Simulation model Employee.class: Employee.java Person.class WorkPlace.class $(supportClasses) javac Employee.java Person.class: Person.java Place.class HomePlace.class $(supportClasses) javac Person.java HomePlace.class: HomePlace.java Place.class Person.class $(supportClasses) javac HomePlace.java WorkPlace.class: WorkPlace.java Place.class Employee.class $(supportClasses) javac WorkPlace.java Place.class: Place.java $(supportClasses) Person.class javac Place.java ########## # secondary make targets: Support Simulator.class: Simulator.java javac Simulator.java MyRandom.class: MyRandom.java javac MyRandom.java MyScanner.class: MyRandom.java Error.class javac MyScanner.java Error.class: Error.java javac Error.java ########## # utility make targets: html: $(allJava) javadoc $(allJava) test: Epidemic.class java Epidemic testepi clean: rm -f *.class *.html package-list script.js stylesheet.css xxxxxxxxxx cat > Employee.java <<\xxxxxxxxxx /** * Employees are People who work * @author Douglas Jones * @version 11/16/2020 * Status: Works with new simulation framework * @see Person * @see WorkPlace */ public 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 class MyEvent extends Simulator.Event { MyEvent() { super( (8 * Simulator.hour) - (25 * Simulator.minute) ); } public void trigger() { goToWork( time ); } } Simulator.schedule( new MyEvent() ); } /** 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 t, the time of departure */ private void goToWork( double t ) { if (infectionState == States.dead) return; // finish killing the dead! // people only leave home if feeling OK if (infectionState != States.bedridden) { double travelTime = rand.nextLogNormal( 20 * Simulator.minute, // mean travel time 3 * Simulator.minute // scatter in travel time ); this.place.depart( this, t ); this.travelTo( this.job, t + travelTime ); } // go to work every day at the same time class MyEvent extends Simulator.Event { MyEvent() { super( t + Simulator.day ); } public void trigger() { goToWork( time ); } } Simulator.schedule( new MyEvent() ); } } xxxxxxxxxx cat > Epidemic.java <<\xxxxxxxxxx import java.lang.Math; import java.util.Iterator; import java.io.File; import java.io.FileNotFoundException; /** * Main class for the Epidemic simulator, builds model and simulates it * @author Douglas Jones * @version 11/16/2020 * Status: Works with new simulation framework * @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" ); class MyEvent extends Simulator.Event { MyEvent() { super( endTime ); } public void trigger() { System.exit( 0 ); // BUG -- A better end would output a results report } } Simulator.schedule( new MyEvent() ); } 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( 0 ); // infected from the beginning of time 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 ); } } class MyEvent extends Simulator.Event { MyEvent() { super( 0.0 ); } public void trigger() { Person.report( time ); } } Simulator.schedule( new MyEvent() ); } /** 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. * @param args, the array of 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" ); } } } xxxxxxxxxx cat > Error.java <<\xxxxxxxxxx /** * Error handling * @author Douglas Jones * @version 11/2/2020 * Status: Stable through many previous versions */ public 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 ); } } xxxxxxxxxx cat > HomePlace.java <<\xxxxxxxxxx import java.util.LinkedList; /** * HomePlaces are occupied by any type of person * @author Douglas Jones * @version 11/2/2020 * Status: Broken off from MP8 solution; it works, but see BUG notices * @see Place * @see Person */ class HomePlace extends Place { private final LinkedList residents = new LinkedList (); // transmissivity median and scatter for homes // BUG -- These should come from model description file, not be hard coded private static final double transMed = 0.03 * Simulator.hour; private static final double transScat = 0.02 * Simulator.hour; // need a source of random numbers private static final MyRandom rand = MyRandom.stream(); /** The only constructor for Place * Places are constructed with no occupants */ public HomePlace() { super(); // initialize the underlying place super.transmissivity = rand.nextLogNormal( transMed, transScat ); } /** 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; } } xxxxxxxxxx cat > LogNormal.java <<\xxxxxxxxxx /* LogNormal.java * Demonstration of how to generate a log-normal probability distribution. * This reads the median and scatter as command line arguments, and then * generates a histogram of the distribution over 100 trials on the interval * from 0 to 19, with bins 0 and 19 holding results outside that range. * * author Douglas W. Jones * version Sept. 25, 2020 */ import java.util.Random; import java.lang.Math; import java.lang.NumberFormatException; class LogNormal { public static void main( String arg[] ) { Random rand = new Random(); // a source of random numbers double median = 0.0; // the median of the log normal distr double scatter = 0.0; // the scatter of the distribution // the histogram accumulator final int bound = 20; int [] histogram = new int[bound]; // get median and scatter from the command line if (arg.length != 2) { System.err.println( "2 arguments required -- median and scatter" ); System.exit( 1 ); } else try { median = Double.parseDouble( arg[0] ); scatter = Double.parseDouble( arg[1] ); } catch (NumberFormatException e) { System.err.println( "non numeric argument " + arg[0] + " " + arg[1] ); System.exit( 1 ); } // check median and scatter for legitimacy if (median <= 0.0) { System.err.println( "median " + arg[0] + " must be positive" ); System.exit( 1 ); } if (scatter < 0.0) { System.err.println( "scatter " + arg[1] + " must not be negative" ); System.exit( 1 ); } // output heading for the histogram System.out.println( "median " + median + ", scatter " + scatter ); // sigma is the standard deviation of the underlying normal distribution double sigma = Math.log( (scatter + median) / median ); // do the experiment to generate the histogram for (int i = 0; i < 100; i++) { // draw a random number from a log normal distribution double lognormal = Math.exp( sigma * rand.nextGaussian() ) * median; // find what bin of the histogram it goes in and increment that bin int bin = (int)Math.ceil( lognormal ); if (bin <= 0) bin = 0; if (bin >= bound) bin = bound - 1; histogram[bin]++; } // print the histogram for (int i = 0; i < bound; i++) { System.out.printf( " %2d", i ); for (int j = 0; j < histogram[i]; j++ ) System.out.print( 'X' ); System.out.print( '\n' ); } } } xxxxxxxxxx cat > MyRandom.java <<\xxxxxxxxxx import java.lang.Math; import java.util.Random; /** * Singleton wrapper for Java's Random class * @author Douglas Jones * @version 11/2/2020 * Status: Relatively stable code */ 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 */ 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; } } xxxxxxxxxx cat > MyScanner.java <<\xxxxxxxxxx import java.lang.NumberFormatException; import java.util.regex.Pattern; 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 11/2/2020 * Status: Relatively stable code * @see java.util.Scanner * @see Error */ public 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; } } } xxxxxxxxxx cat > Person.java <<\xxxxxxxxxx /* * Person.java includes supporting classes, People occupy places * author Douglas Jones * version 11/8/2020 -- To be turned in as MP12 * Status: All inner classes moved to outer level * * Note that the former inner classes are all moved to the front of the file * The main code for class Person follows after these little support classes */ import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; /** Event indicating that person becomes infectious at the indicated time * Used only by Person.infect */ class ToBeInfectious extends Simulator.Event { final Person who; /** construct a state change event * @param who identifies the Person involved in the state change * @param when sets the time of the state change * Called only from Person.infect */ ToBeInfectious( Person who, double when ) { super( when ); this.who = who; } public void trigger() { who.beInfectious( time ); } } /** Event indicating that a person becomes recovered at the indicated time * Used only by Person.beInfectious and Person.beBeridden */ class ToBeRecovered extends Simulator.Event { final Person who; /** construct a state change event * @param who identifies the Person involved in the state change * @param when sets the time of the state change * Called only from Person.beInfectious and Person.beBedridden */ ToBeRecovered( Person who, double when ) { super( when ); this.who = who; } public void trigger() { who.beRecovered( time ); } } /** Event indicating that a person becomes bedridden at the indicated time * Used only by Person.beInfectious */ class ToBeBedridden extends Simulator.Event { final Person who; /** construct a state change event * @param who identifies the Person involved in the state change * @param when sets the time of the state change * Called only from Person.beInfectious */ ToBeBedridden( Person who, double when ) { super( when ); this.who = who; } public void trigger() { who.beBedridden( time ); } } /** Event indicating that a person becomes dead at the indicated time * Used only by Person.beBedridden */ class ToBeDead extends Simulator.Event { final Person who; /** construct a state change event * @param who identifies the Person involved in the state change * @param when sets the time of the state change * Called only from Person.beBedridden */ ToBeDead( Person who, double when ) { super( when ); this.who = who; } public void trigger() { who.beDead( time ); } } /** Event requesting an epidemic report at the indicated time * Used only by Person.report */ class ToReport extends Simulator.Event { /** construct a report request event * @param when sets the time of the report * Called only from Person.report */ ToReport( double when ) { super( when ); } public void trigger() { Person.report( time ); } } /** Schedule a Person to arrive at a Place at the indicated time * Used only by Person.travelTo */ class ToArriveAt extends Simulator.Event { final Person who; final Place where; /** construct an arrival event * @param who identifies the Person who will arrive * @param where identifies the place they will arrive at * @param when sets the arrival time * Called only from Person.travelTo */ ToArriveAt( Person who, Place where, double when ) { super( when ); this.who = who; this.where = where; } public void trigger() { who.arriveAt( time, where ); } } /** * People occupy places * @author Douglas Jones * @version 12/8/2020 * Status: Inner classes removed * @see Place * @see Employee */ class Person { // private stuff needed for instances static protected enum States { uninfected, latent, infectious, bedridden, recovered, dead // the order of the above is significant: >= uninfected is infected } // static attributes describing progression of infection // BUG -- These should come from model description file, not be hard coded private static final double latentMedT = 2 * Simulator.day; private static final double latentScatT = 1 * Simulator.day; private static final double bedriddenProb = 0.7; private static final double infectRecMedT = 1 * Simulator.week; private static final double infectRecScatT = 6 * Simulator.day; private static final double infectBedMedT = 3 * Simulator.day; private static final double infectBedScatT = 5 * Simulator.day; private static final double deathProb = 0.2; private static final double bedRecMedT = 2 * Simulator.week; private static final double bedRecScatT = 1 * Simulator.week; private static final double bedDeadMedT = 1.5 * Simulator.week; private static final double bedDeadScatT = 1 * Simulator.week; // static counts of infection progress private static int numUninfected = 0; private static int numLatent = 0; private static int numInfectious = 0; private static int numBedridden = 0; private static int numRecovered = 0; private static int numDead = 0; // 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; numUninfected = numUninfected + 1; h.addResident( this ); allPeople.add( this ); // this is the only place items are added! } /** Predicate to test person for infectiousness * @return true if the person can transmit infection */ public boolean isInfectious() { return (infectionState == States.infectious) || (infectionState == States.bedridden); } /** 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 relating to infection process /** Infect a person * @param t, the time at which the person is infected * called when circumstances call for a person to become infected */ public void infect( double t ) { if (infectionState == States.uninfected) { // infecting an already infected person has no effect double delay = rand.nextLogNormal( latentMedT, latentScatT ); numUninfected = numUninfected - 1; infectionState = States.latent; numLatent = numLatent + 1; Simulator.schedule( new ToBeInfectious( this, delay + t ) ); } } /** An infected but latent person becomes infectous * scheduled by infect() to make a latent person infectious *
Used only by ToBeInfectious */ void beInfectious( double t ) { numLatent = numLatent - 1; infectionState = States.infectious; numInfectious = numInfectious + 1; if (place != null) place.oneMoreInfectious( t ); if (rand.nextFloat() > bedriddenProb) { // person stays asymptomatic double delay = rand.nextLogNormal( infectRecMedT, infectRecScatT ); Simulator.schedule( new ToBeRecovered( this, delay + t ) ); } else { // person becomes bedridden double delay = rand.nextLogNormal( infectBedMedT, infectBedScatT ); Simulator.schedule( new ToBeBedridden( this, delay + t ) ); } } /** An infectious person becomes bedridden * scheduled by beInfectious() to make an infectious person bedridden *
Used only by ToBeBedridden */ void beBedridden( double t ) { numInfectious = numInfectious - 1; infectionState = States.bedridden; numBedridden = numBedridden + 1; if (rand.nextFloat() > deathProb) { // person recovers double delay = rand.nextLogNormal( bedRecMedT, bedRecScatT ); Simulator.schedule( new ToBeRecovered( this, delay + t ) ); } else { // person dies double delay = rand.nextLogNormal( bedDeadMedT, bedDeadScatT ); Simulator.schedule( new ToBeDead( this, delay + t ) ); } // if in a place (not in transit) that is not home, go home now! if ((place != null) && (place != home)) goHome( t ); } /** A infectious or bedridden person recovers * scheduled by beInfectious() or beBedridden to make a person recover *
Used only by ToBeRecovered */ void beRecovered( double time ) { if (infectionState == States.infectious) { numInfectious = numInfectious - 1; } else { numBedridden = numBedridden - 1; } infectionState = States.recovered; numRecovered = numRecovered + 1; if (place != null) place.oneLessInfectious( time ); } /** A bedridden person dies * scheduled by beInfectious() to make a bedridden person die *
Used only by ToBeDead */ void beDead( double time ) { numBedridden = numBedridden - 1; infectionState = States.dead; // needed to prevent resurrection numDead = numDead + 1; // if the person died in a place, make them leave it! if (place != null) place.depart( this, time ); // BUG: leaves them in the directory of residents and perhaps employees } // simulation methods relating to daily reporting /** Make the daily midnight report * @param t, the current time *
used only by ToReport and the main program */ public static void report( double t ) { System.out.println( "at " + t + ", un = " + numUninfected + ", lat = " + numLatent + ", inf = " + numInfectious + ", bed = " + numBedridden + ", rec = " + numRecovered + ", dead = " + numDead ); // make this happen cyclically Simulator.schedule( new ToReport( t + Simulator.day ) ); } // simulation methods relating to personal movement /** Make a person arrive at a new place * @param p new place * @param t the current time *
used only by ToArriveAt */ void arriveAt( double time, Place p ) { if ((infectionState == States.bedridden) && (p != home)) { // go straight home if you arrive at work while sick goHome( time ); } else if (infectionState == States.dead) { // died on the way to work // allow this person to be forgotten } else { // only really arrive if not sick p.arrive( this, time ); this.place = p; } } /** Move a person to a new place * @param p, the place where the person travels * @param t, time at which the move will be completed * 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 t ) { this.place = null; Simulator.schedule( new ToArriveAt( this, p, t ) ); } /** 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 ); // the possibility of arriving at work after falling ill requires this if (this.place != null) this.place.depart( this, time ); this.travelTo( this.home, time + travelTime ); } } xxxxxxxxxx cat > Place.java <<\xxxxxxxxxx import java.lang.Math; import java.util.Iterator; import java.util.LinkedList; /** * Places are occupied by people * @author Douglas Jones * @version 11/2/2020 * Status: Broken off from MP8 solution; it works, but see BUG notices * @see HomePlace * @see WorkPlace */ public abstract class Place { // invariant attributes of each place public final String name; protected double transmissivity; // how infectious is this place // initialized by subclass! // dynamic attributes of each place protected final LinkedList occupants = new LinkedList<> (); private int infectiousCount = 0; // number of infected occupants; double lastCheck = 0.0; // time of last check on infectiousness // contructor (effectively protected Place() { name = super.toString(); allPlaces.add( this ); } // manage the infectiousness of this place // need a source of random numbers private static final MyRandom rand = MyRandom.stream(); /** see who to infect at this time * @param time, the time of the change * called just before any any change to the population or infection count */ private void whoToInfect( double time ) { // note that transmissivities are per hour, so convert time to hours double interval = (time - lastCheck) / Simulator.hour; double pInfection = transmissivity * infectiousCount * interval; if (interval <= 0) return; // short circuit the process for efficiency // probability cannot exceed one! if (pInfection > 1.0) pInfection = 1.0; // BUG -- should it be: pInfection = 1.0 - Math.exp( -pInfection ); // give everyone a fair chance to catch the infection for (Person p: occupants) { if (rand.nextDouble() < pInfection) { p.infect( time ); } } lastCheck = time; } /** another person here has become infectious * @param time, the time of the change * they either arrived while infectous * or transitioned to infectous while here */ public void oneMoreInfectious( double time ) { whoToInfect( time ); infectiousCount = infectiousCount + 1; } /** one less person here is infectious * @param time, the time of the change * they either departed here while infectous * or transitioned to recovered or dead while here */ public void oneLessInfectious( double time ) { whoToInfect( time ); infectiousCount = infectiousCount - 1; } // tools for moving people in and out of places /** a person arrives at this place * @param p, the person who arrives * @param time, the time of arrival */ public void arrive( Person p, double time ) { occupants.add( p ); if (p.isInfectious()) { oneMoreInfectious( time ); } else { whoToInfect( time ); } // DEBUG System.out.println( // (Object)p.toString() + " arrives " + (Object)this + " at " + time // ); // DEBUG } /** a person leaves from this place * @param p, the person who leaves * @param time, the time of departure */ public void depart( Person p, double time ) { if (p.isInfectious()) { oneLessInfectious( time ); } else { whoToInfect( time ); } boolean wasPresent = occupants.remove( p ); assert wasPresent: "p=" + p + " this=" + this; assert !occupants.contains( p ): "p=" + p + " this=" + this; // DEBUG 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(); } } xxxxxxxxxx cat > Simulator.java <<\xxxxxxxxxx import java.util.PriorityQueue; /** * Framework for discrete event simulation. * @author Douglas Jones * @version 11/13/2020 -- new simulation framework with time units * Status: New code! Seems to work */ public 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; /** 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 > WorkPlace.java <<\xxxxxxxxxx import java.util.LinkedList; /** * WorkPlaces are occupied by employees * @author Douglas Jones * @version 11/16/2020 * Status: Works with new simulation framework * @see Place * @see Employee */ public class WorkPlace extends Place { private final LinkedList employees = new LinkedList (); // transmissivity median and scatter for workplaces // BUG -- These should come from model description file, not be hard coded private static final double transMed = 0.02 * Simulator.hour; private static final double transScat = 0.25 * Simulator.hour; // need a source of random numbers private static final MyRandom rand = MyRandom.stream(); /** The only constructor for WorkPlace * WorkPlaces are constructed with no residents */ public WorkPlace() { super(); // initialize the underlying place super.transmissivity = rand.nextLogNormal( transMed, transScat ); // make the workplace open at 8 AM class MyEvent extends Simulator.Event { MyEvent() { super( 8 * Simulator.hour ); } public void trigger() { open( time ); } } Simulator.schedule( new MyEvent() ); } /** 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 t ) { // System.out.println( this.toString() + " opened at time " + time ); // BUG -- we should probably do something useful too // close this workplace 8 hours later class MyEvent extends Simulator.Event { MyEvent() { super( t + 8 * Simulator.hour ); } public void trigger() { close( time ); } } Simulator.schedule( new MyEvent() ); } /** 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 t ) { //System.out.println( this.toString() + " closed at time " + time ); // open this workplace 16 hours later, with no attention to weekends class MyOpenEvent extends Simulator.Event { MyOpenEvent() { super( t + 16 * Simulator.hour ); } public void trigger() { open( time ); } } Simulator.schedule( new MyOpenEvent() ); // send everyone home now for (Person p: occupants) { // schedule it for now in order to avoid modifying list inside loop // not doing this gives risk of ConcurrentModificationException class MyHomeEvent extends Simulator.Event { MyHomeEvent() { super( t ); } public void trigger() { p.goHome( time ); } } Simulator.schedule( new MyHomeEvent() ); } } } xxxxxxxxxx cat > testepi <<\xxxxxxxxxx pop 1000; house 3.3,3; // household size median 3.3 scatter 3 workplace 10,9; // workplace size median 10 scatter 9 infected 10; // begin with 1% infected employed 0.5; // 50% of population works end 30.0001; // run for 30 days xxxxxxxxxx