# shell archive created by dwjones on Wed 12 May 2021 09:31:59 PM CDT # To install this software on a UNIX compatable system (Linux or MacOS): # 1) create a directory (e.g. with the shell command mkdir project) # 2) change to that directory (e.g. with the command cd project), # 3) direct the remainder of this text to sh (e.g. sh < ../thisfile). # 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. # On other systems, extract files from this file using a text editor. # Each file is bracketed between two lines of xxxx. # The first line of xxxx contains a cat command giving the file name. cat > README <<\xxxxxxxxxx EPIDEMIC SIMULATOR ================== Author: Douglas W. Jones Version: May 9, 2021 The code in this directory includes a moderately polished version of the epidemic simulator originally developed for the spring CS:2820 offering at the University of Iowa. This epidemic simulator takes an input file containing a description of the places in a community, the roles fulfilled by the population of that community and the nature of the disease. The output is a CSV file showing the progress of the disease through the community. This simulator was developed as an academic exercise for a couse that serves, for most students, as their first introduction to programming in the large. Students were not expected to know anything about simulation or about epidemiology coming into this course. Nonetheless, this simulator seems to be worth further exploration. The motivation behind this effort was, in part, the wish that many of the world leaders who did a rather poor job with the COVID 19 epidemic could have had a simulator something like this in order to inform them a bit more about how epidemics work and to allow them to play with the impacts of various mitigation efforts. Files ----- This directory contains the following source files for the epidemic simulator: * README this file * Epidemic.java the main program * Schedule.java How do people decide to move from place to place * Person.java How does each person behave, also population statistics * Place.java How does each place work * PlaceKind.java What kinds of places are there * Role.java What kinds of roles to people fit into * InfectionRule.java How do stages of the infection progress * Week.java Days of the week and sets of days * Check.java Utility to do sanity checks on values * Time.java Definitions of time units, format of time input * Probability.java Format of probability input * MyRandom.java Extensions to Java.util.random * Simulator.java A discrete-event simulation framework * MyScanner.java Wrapper around java.util.scanner * Error.java error reporting framework The following additional files are included * Makefile instructions for building and running the simulator (this also serves to documentat the program structure) * testa test input, workers spread disease between families * testb test input, everyone works sometimes, spreading it * testc test input, two compartment, everyone has brief contact * testd test input, two compartment, fewer extended contacts * PlotScript instructions for Gnuplot to graph the output Instructions ------------ To build the epidemic simulator, use this shell command make To make a web site of documentation from the Javadoc comments in the code make html To clean up files that were created by make make clean To test or demonstrate the simulator, use one of these shell commands make demo # same as: 'make demo f=testa' make demo f=testa # same as: 'make; java Epidemic testa' java Epidemic testa java Epidemic testb java Epidemic testc java Epidemic testd make plot # same as: 'make plot f=testa' make plot f=testa # same as the following two line sequence java Epidemic testa > testa.png gnuplot -p -c PlotScript testa.png Tests A and B should produce very similar results as a wave of infection sweeps through the community until everyone has either recovered or died of the simulated disease. (Extremely rarely, the disease will not escape from one household.) Tests C and D are bi-stable; they involve places named earth and mars, where people from those planets travel to the moon and make brief contact. Sometimes, the epidemic fails to spread between planets, sometimes, it jumps the gap between planets and sweeps through both. Frequently, there is a delay before the disease jumps to the second planet, so the overall statistics show two distinct waves of infection. Tests A and C involve people following fixed schedules, while tests B and D involve schedules with random elements, where the random elements have been adjusted so that test B produces output similar to A and test D is similar to C. Command Line Options -------------------- Normally, the epidemic simulator is run with just the input file name on the command line, as: java Epidemic inputfile The output (to standard output) is the CSV file of the simulation results. There are two command line options. java Epidemic -h -d inputfile The -h option suppresses the headline, the first line of the CSV file giving captions for the data on the following line. Some CSV utilities do not deal gracefully with captioned data, while most do. The -d option causes a dump of the entire population listing all the people that have been created to popuate each role, and for each person, listing all of the specific places they are associated with, and the schedule that person follows for that place. This dump is output before any simulation output and is largely there for debugging purposes. License ------- All code in this distribution is released to the world under GPLv3, the GNU General Public License version 3, documented at -- http://www.gnu.org/licenses/gpl-3.0.html This means that anyone is free to copy any or all of this code and modify it to do just about anything they want, so long as they share the results of their work under a compatible license. Warranty -------- You get what you pay for. This is free software, it seems to work and the author thinks it is both fun and instructive to play with. At this point, the only thing that can be guaranteed is that this software contains bugs. If you find it useful, the author is happy for you, but the author does not accept responsibility for the consequences of relying on this code to do anything of consequence. Some bugs are noted in the code with comments containing the upper-case text BUG. If you find others, the author would not mind hearing about them and might have the time to fix them in the public-facing copy of the code. The author may be contacted by e-mail at: -- douglas-w-jones@uiowa.edu Anyone wishing to make gifts or endowments to express gratitutde for the free distribution of this code may make them in care of the University of Iowa Foundation, Iowa City, Iowa 52242, naming the beneficiary as the University of Iowa Department of Computer Science. xxxxxxxxxx cat > Epidemic.java <<\xxxxxxxxxx // Epidemic.java import java.io.File; import java.io.FileNotFoundException; /** The main class of an epidemic simulator. *
This class cannot be instantiated. * It is a container for the main method and its (private) support code. *
Input items in the model description * begin with one of the following keywords: *
InfectionRule
* InfectionRule
* InfectionRule
* InfectionRule
* Role
* PlaceKind
* Most of the input parsing is done by the constructor for the * indicated class. Each item ends with * a semicolon, so multiple items may be listed on a line and items may * be broken over multiple lines. A model may include any number of * role and place specifications. The others may be given just once each. * @author Douglas W. Jones * @version May 5, 2021 added -d and -h command-line options * @see MyScanner * @see InfectionRule * @see Role * @see PlaceKind * @see Person * @see Simulator */ public class Epidemic { private Epidemic() {} // prevent construction of instances! /** Read and process the details of the model from an input stream. * @param in the stream to read from */ private static void buildModel( MyScanner in ) { int pop = 0; // the population of the model, 0 = uninitialized int infected = 0; // number initially infected, 0 = uninitialized double endOfTime = -1.0; // negative = uninitialized // rules describing the progress of the infection InfectionRule latent = null; InfectionRule asymptomatic = null; InfectionRule symptomatic = null; InfectionRule bedridden = null; while ( in.hasNext() ) { // scan the input file // each item begins with a keyword String keyword = in.getNextName( ()-> "keyword expected" ); if ("population".equals( keyword )) { if (pop != 0) { Error.warn( "population specified more than once" ); } pop = Check.posIntSemicolon( in, ()-> "population" ); } else if ("infected".equals( keyword )) { if (infected != 0) { Error.warn( "infected specified more than once" ); } infected = Check.posIntSemicolon( in, ()-> "infected" ); } else if ("latent".equals( keyword )) { if (latent != null) { Error.warn( "latency time specified more than once" ); } latent = new InfectionRule( in, ()-> "latent" ); } else if ("asymptomatic".equals( keyword )) { if (asymptomatic != null) { Error.warn( "asymptomatic time specified more than once" ); } asymptomatic = new InfectionRule( in, ()-> "asymptomatic" ); } else if ("symptomatic".equals( keyword )) { if (symptomatic != null) { Error.warn( "symptomatic time specified more than once" ); } symptomatic = new InfectionRule( in, ()-> "symptomatic" ); } else if ("bedridden".equals( keyword )) { if (bedridden != null) { Error.warn( "bedridden time specified more than once" ); } bedridden = new InfectionRule( in, ()-> "bedridden" ); } else if ("end".equals( keyword )) { if (endOfTime >= 0.0) { Error.warn( "end " + ": duplicate end time" ); } final double et = Time.get( in, Time.day, ()-> "time:" ); endOfTime = et; // et needed for ()-> restriction on final vars in.getNextLiteral( Check.semicolonPat, ()-> "end " + Time.toString( et ) + ": missing ;" ); } else if ("role".equals( keyword )) { new Role( in ); } else if ("place".equals( keyword )) { new PlaceKind( in ); } else if (MyScanner.defaultName == keyword) { // there wasn't one // == is allowed here 'cause we're detecting the default value // we need to advance the scanner here or we'd stick in a loop if (in.hasNext()) in.next(); } else { // none of the above Error.warn( "not a keyword: " + keyword ); } } // check that all required fields are filled in if (pop == 0) Error.warn( "population not given" ); if (latent == null) Error.warn( "latency times not given" ); if (asymptomatic == null) Error.warn( "asymptomatic times not given" ); if (symptomatic == null) Error.warn( "symptomatic times not given" ); if (bedridden == null) Error.warn( "bedridden times not given" ); if (endOfTime < 0.0) Error.warn( "end time not given" ); Error.exitIfWarnings( "Aborted due to errors in input" ); Person.setDiseaseParameters( latent, asymptomatic, symptomatic, bedridden ); // schedule the end of time // note, users usually give integer count of days // making the end simultaneous with the final daily report. // Adding just a bit prevents simultaneous events which // lead to nondeterministic printing of the final report. Simulator.schedule( endOfTime + Time.second, ()-> System.exit( 0 ) ); // Role is responsible for figuring out how many people per role Role.populateRoles( pop, infected ); } /** The main method. *
Most of this code is entirely about command line argument processing. * It builds the model and then starts the simulation. *
One command line argument is mandatory, name of the file * holding the model description. Other command line options are: *
-d
pre-simulation dump of all people and places.
* -h
suppress inclusion of the headline in CSV file.
* Roles create links from people to the categories of places they visit
* and create schedules for those visits.
* @author Douglas W. Jones
* @version May 12, 2021 add compliance attribute for quarantine
* @see Person
* @see Schedule
* @see MyRandom
* @see MyScanner
*/
public class Role {
// linkage from role to associated place involves a schedule
private class PlaceSchedule {
public PlaceKind placeKind;
public Schedule schedule;
public PlaceSchedule( PlaceKind p, Schedule s ) {
placeKind = p;
schedule = s;
}
}
// instance variables
/** The name of this role */
public final String name;
/** Will this role's people comply with quarantine directives?
* 0.0 = not at all, 1.0 = always
*/
public final double compliance;
// the list of all of the kinds of places associated with this role
private final LinkedList The stream must contain the following items, in order:
* Each place, actually the name of a This is common code used to produce context for error messages.
* @return the description
*/
private String describe() {
return "role " + name + " " + fraction;
}
/** Find a role, by name.
* Used to prevent duplicate definition of roles.
* As it turns out, there is no case where roles need to be looked up
* by name except to prevent duplication.
* @param n the name of the role
* @return the role with that name, or null if none has been defined
*/
private static Role findRole( String n ) {
for (Role r: allRoles) {
if (r.name.equals( n )) return r;
}
return null; // role not found
}
/** Create the total population, divided up by roles.
* Only after all roles have been defined should this be called in
* order to actually populate the roles.
* In addition to creating people to fill the roles, this code
* tells the associated places about the people that live there or
* will visit. This is done in two steps, first sending people to
* categories of places, and then populating the specific places
* using The stream must contain the following fields, in order:
* Infectivity is measure in infections per hour per infected
* person in that place; for details, see class This is used to create context for error messages.
* @return the description
*/
private String describe() {
return "place " + name + " " + median + " " + scatter
+ " " + transmissivity;
}
/** Find or make a place of a particular kind.
* This should be called when a person is to be linked to a place
* of some particular kind, potentially occupying a space in that place.
* @return a place that is an instance of the indicated kind
*/
private Place findPlace() {
if (unfilledCapacity <= 0 ) { // need to make a new place
// make new place using a log-normal distribution for the size
unfilledCapacity
= (int)Math.round( rand.nextLogNormal( median, sigma) );
unfilledPlace = new Place( this );
}
unfilledCapacity = unfilledCapacity - 1;
return unfilledPlace;
}
/** Add a person to the population of this kind of place.
* The entire population of the place kind must be created before
* any person can be associated with a particular place of that kind.
* @param p the new person
* @param s the associated schedule
*/
public void populate( Person p, Schedule s ) {
people.add( new PersonSchedule( p, s ) );
}
/** Distribute the people from all PlaceKinds to their individual places.
* This should be called after all people are created and associated
* with various Used to prevent duplicate definitions and to look up place names
* when roles are created that are associated with the place.
* @param n the name of the category
* @return the PlaceKind with that name, or null if none has been defined
*/
public static PlaceKind findPlaceKind( String n ) {
for (PlaceKind pk: allPlaceKinds) {
if (pk.name.equals( n )) return pk;
}
return null; // category not found
}
}
xxxxxxxxxx
cat > Person.java <<\xxxxxxxxxx
// Person.java
import java.util.LinkedList;
/** People are the central actors in the simulation.
* @author Douglas W. Jones
* @version May 12, 2021 support for places doing screening for admission
* @see Role
* @see Place
* @see MyRandom
*/
public class Person {
private static enum DiseaseStates {
uninfected,
latent,
asymptomatic, //
symptomatic, // These 3 states are defined as contageous
bedridden, //
recovered,
dead; // this must be the last state
// note that the order of the above enumeration defines the order
// of the fields of the CSV file output by the simulator.
int pop = 0; // each disease state has a population
}
// timing characteristics of disease state
private static InfectionRule latent;
private static InfectionRule asymptomatic;
private static InfectionRule symptomatic;
private static InfectionRule bedridden;
/** Set the disease parameters for the disease states.
* This must be called once before simulation starts.
* @param l the infection rule for disease latency
* @param a the infection rule for the asymptomatic phase
* @param s the infection rule for the symptomatic phase
* @param b the infection rule for the bedridden phase
*/
public static void setDiseaseParameters(
InfectionRule l, InfectionRule a, InfectionRule s, InfectionRule b
) {
latent = l;
asymptomatic = a;
symptomatic = s;
bedridden = b;
}
// linkage from person to place involves a schedule
private class PlaceSchedule {
public Place place;
public Schedule schedule;
public PlaceSchedule( Place p, Schedule s ) {
place = p;
schedule = s;
}
}
// instance variables created from model description
private final Role role; // role of this person
private Place home; // this person's home place, set by emplace
private final LinkedList This constructor deliberately defers putting people in any places.
* For each constructed person Each person must be emplaced before simulation begins.
* Emplacing a pereson commits that person to visiting the place
* according to the given schedule, and it also places the person
* in a home place identified by a null schedule.
* @param p the place
* @param s the associated schedule
*/
public void emplace( Place p, Schedule s ) {
if (s != null) {
places.add( new PlaceSchedule( p, s ) );
s.apply( this, p ); // commit to following schedule s for place p
} else {
assert home == null: "Role guarantees only one home place";
home = p;
location = home;
location.arrive( this ); // tell location about new occupant
}
}
// state query
/** Is this person contageous?
* A person is defined as being contageous if they are in any disease
* state between A person is defined as being symptomatic if they are in any disease
* state between This may be used to reschedule infection for someone who was
* previously scheduled to be infected at a different time.
* The actual delay until infection is randomized based on the mean
* delay provided.
* @param meanDelay the mean delay until infection
*/
public void scheduleInfect( double meanDelay ) {
if (diseaseState == DiseaseStates.uninfected) { // irrelevant if not
if (Double.isInfinite( meanDelay )) { // I will not be infected
if (infectMe != null) {
Simulator.cancel( infectMe );
infectMe = null;
}
} else { // If current conditions continue, I will be infected
double delay = rand.nextExponential( meanDelay );
if (infectMe == null) {
infectMe = Simulator.schedule( delay, ()-> infect() );
} else {
Simulator.reschedule( infectMe, delay );
}
}
}
}
/** Infect this person.
* This is a schedulable event service routine.
* This may be called on a person in any infection state but it only
* moves the person to During simulation, this should only be called from
* This is a schedulable event service routine.
* This may only be called on a person in with a This is a schedulable event service routine.
* This may only be called on a person in with an
* This is a schedulable event service routine
* This may only be called on a person in with a
* This is a schedulable event service routine.
* This may be called on a person in any disease state
* and leaves the person This is a schedulable event service routine.
* This may only be called only on a person who is already
* The person will hesitate to leave home to an extent determined by
* their role.
*/
public void quarantine() {
hesitance = role.compliance;
}
/** Tell this person to go home at this time.
* This is a schedulable event service routine.
* Note that if a person is already home, this does nothing.
*/
public void goHome() {
if (location != home) travelTo( home );
}
/** Tell this person to go somewhere.
* This is a schedulable event service routine.
* Note that this enforces the rule that The report is in CSV format sent to This is a schedulable event service routine.
* Each report is a CSV line sent to Every place is an instance of some Places are characterized by a population, and characteristics
* inherited from their This is a schedulable event service routine.
* This checks the contageon state of the person to keep tabs on
* how many contageous people are currently in this place.
* @param p the person involved
*/
public void arrive( Person p ) {
if (p.isContageous()) {
if ((kind.testAntigenRate != 0.0)
&& (rand.nextFloat() < kind.testAntigenRate)) { // try screening
Simulator.schedule( 10 * Time.minute, ()->p.goHome() );
// BUG constant screening time? Should be settable and random
// BUG person not told to quarantine
} else if ((kind.testSymptomRate != 0.0)
&& p.isSymptomatic()
&& (rand.nextFloat() < kind.testSymptomRate)) { // more screening
Simulator.schedule( 3 * Time.minute, ()->p.goHome() );
// BUG constant screening time? Should be settable and random
// BUG person not told to quarantine
}
contageous( +1 );
}
occupants.add( p );
}
/** Make a person depart from a place.
* This is a schedulable event service routine.
* This checks the contageon state of the person to keep tabs on
* how many contageous people are currently in this place.
* @param p the person involved
*/
public void depart( Person p ) {
occupants.remove( p );
if (p.isContageous()) contageous( -1 );
}
/** Signal that a person in this place has changed their contageon state.
* This is a schedulable event service routine but
* It is more likely to be called directly from other
* event service routines. It must be called from outside whenever
* a person in some place changes disease state without departing.
* @param c, +1 means one more is contageous, -1 means one less.
*/
public void contageous( int c ) {
contageous = contageous + c;
// when the number of contageous people in a place changes,
for (Person p: occupants) {
p.scheduleInfect( 1 / (contageous * kind.transmissivity) );
}
}
}
xxxxxxxxxx
cat > Schedule.java <<\xxxxxxxxxx
// Schedule.java
import java.util.regex.Pattern;
/** Tuple of visit times and probability used to send people to places.
* @author Douglas W. Jones
* @version May 9, 2021 added support for irregular schedules like 9-5pm@mwf
* @see Person
* @see Place
* @see MyScanner
* @see Time
* @see Week
* @see Error
* @see Simulator
* @see MyRandom
*/
public class Schedule {
// instance variables
private final double startTime; // times are in seconds anno midnight
private final double duration; // duration of visit
private final int whatDays; // which days of the week
private final double likelihood;// probability this visit will take place
// Warning: whatDays can be the empty set after parsing the schedule
// but an error message will have been output. Taking any action when
// whatDays is empty can be dangerous. Make sure simulation does not
// start when this is the case.
// source of randomness
private static final MyRandom rand = MyRandom.stream;
// patterns used for parsing schedules
// note that parens are tricky because they mean something to Pattern
private static final Pattern endParenPat = Pattern.compile( "\\)|" );
private static final Pattern dashPat = Pattern.compile( "-|" );
private static final Pattern atPat = Pattern.compile( "@|" );
/** Construct a new Schedule.
* The Sysntax of a schedule is That is, a person following this schedule will go to some place
* at the start time and stay there until the end time,
* but they will only go there on the indicated days
* and only with the indicated probability on the days they go.
* The begin paren must just have been scanned from the input stream
* before the constructor call.
* Times are read using This starts the logical process of making a particular person
* follow this schedule for visiting some particular place.
* @param person the person to commit
* @param place the place that person will visit using this schedule
*/
public void apply( Person person, Place place ) {
assert whatDays != 0: "unexpected empty schedule";
Simulator.schedule(
(Week.daysUntil( 0, whatDays ) * Time.day) + startTime,
()-> go( person, place )
);
}
/** Keep a person on schedule.
* This is a schedulable event service routine.
* This continues the logical process of moving a person according
* to this schedule.
* @param person
* @param place
*/
private void go( Person person, Place place ) {
assert whatDays != 0: "unexpected empty schedule";
int today = Week.dayOfWeek( Simulator.time() );
// first, ensure that we keep following this schedule
Simulator.schedule(
(Week.daysUntil( today + 1, whatDays ) + 1) * Time.day,
()-> go( person, place )
);
if (rand.nextFloat() < likelihood) {
// second, make the person go there if they take the trip
person.travelTo( place );
// third, make sure we get home if we took the trip
Simulator.schedule(
duration, ()-> person.goHome()
);
}
}
/** Convert a Schedule back to textual form.
* Useful largely during debugging when it is useful to
* reconstruct the simulator input to see if it was read correctly.
* @return the schedule as a string
* @see Schedule for syntax details
*/
public String toString() {
StringBuilder returnVal = new StringBuilder( "(" );
returnVal.append( Time.toString( startTime ) );
returnVal.append( '-' );
returnVal.append( Time.toString( startTime + duration ) );
if (whatDays != Week.everyDay) {
returnVal.append( '@' );
returnVal.append( Week.toString( whatDays ) );
}
if (likelihood < 1.0) {
returnVal.append( ' ' );
returnVal.append( likelihood );
}
returnVal.append( ')' );
return returnVal.toString();
}
}
xxxxxxxxxx
cat > InfectionRule.java <<\xxxxxxxxxx
// InfectionRule.java
/** Statistical Description of the disease progress.
* @author Douglas W. Jones
* @version May 2, 2021 improved javadoc comments
* @see MyRandom
* @see MyScanner
*/
public class InfectionRule {
private final double median; // median of the distribution
private final double sigma; // sigma of the distribution
private final double recovery; // recovery probability
private static final MyRandom rand = MyRandom.stream();
/** scan a new InfectionRule from an input stream.
* An infection rule has the following format:
* Recovery probabilities are a fixed property of the rule.
* Uses Disease duration is determined by a log-normal distribution
* specified with the rule.
* Uses There are two basic abstract data types we are concerned with here,
*
* This class provides tools for finding the day of the week and for asking
* if the day is in some set of days, and it provides tools for converting
* sets of days to and from textual format.
* @author Douglas W. Jones
* @version May 9, 2021 new to this version
* @see Time
*/
public class Week {
private Week(){} // nobody ever constructs a Week object
// WARNING: This class should work just fine, but code is rather lame!
/* patterns for matching basic day names */
private static final Pattern sundayPat
= Pattern.compile( "Sunday|sunday|Sun|sun|" );
private static final Pattern mondayPat
= Pattern.compile( "Monday|monday|Mon|mon|" );
private static final Pattern tuesdayPat
= Pattern.compile( "Tuesday|tuesday|Tue|tue|" );
private static final Pattern wednesdayPat
= Pattern.compile( "Wednesday|wednesday|Wed|wed|" );
private static final Pattern thursdayPat
= Pattern.compile( "Thursday|thursday|Thu|thu|" );
private static final Pattern fridayPat
= Pattern.compile( "Friday|friday|Fri|fri|" );
private static final Pattern saturdayPat
= Pattern.compile( "Saturday|saturday|Sat|sat|" );
/* patterns for matching predefined sets of days */
private static final Pattern weekdaysPat = Pattern.compile( "weekdays|" );
private static final Pattern weekendsPat = Pattern.compile( "weekends|" );
private static final Pattern dailyPat = Pattern.compile( "daily|" );
private static final Pattern mwfPat = Pattern.compile( "MWF|mwf|" );
private static final Pattern tuthPat = Pattern.compile( "TuTh|tuth|" );
/* patterns for matching punctuation in sets of days */
private static final Pattern dashPat = Pattern.compile( "-|" );
private static final Pattern commaPat = Pattern.compile( ",|" );
/* Note that sets of days are represented as bit vectors so that
* day i is a member of the set w when (w & (1<given as:
* All times are stored as time can be given as:
* probability can be given as either:
* This is a place to put stuff that is to application specific to put
* in lower level code but needs repository outside of the core parts of the
* epidemic simulator.
* Some of the code here might take up more space than the
* code it helps clarify, but there is a small net gain in readability.
* @author Douglas W. Jones
* @version May 2, 2021 improved Javadoc comments, moved some Patterns here
* @see Error
*/
public class Check {
private Check(){} // nobody should ever construct a check object
/** Parameter for Report errors in scanning such as a missing integer,
* non-positive value and dmissing semicolonr
* @param in the scanner to use
* @param msg the error message prefix to output if error
* @return the value scanned or 1 if the value was defective
*/
public static int posIntSemicolon( MyScanner in, MyScanner.Message msg ) {
final int num = in.getNextInt( ()-> msg + ": missing integer" );
in.getNextLiteral(
semicolonPat,
()-> msg.myString() + num + ": missing ;"
);
if (num == MyScanner.defaultInt) return 1;
if (num <= 0) {
Error.warn( msg.myString() + num + ": not positive" );
return 1;
}
return num;
}
}
xxxxxxxxxx
cat > MyRandom.java <<\xxxxxxxxxx
// MyRandom.java
import java.util.Random;
/** Wrapper extending class Random, turning it into a singleton class.
* Ideally, no user should ever create an instance of calls to Users will generally never mention Action or trigger because,
* when a value implementing this interface is needed, it will usually
* take the form of a lambda expression like this:
* Because class Typically, users schedule events using a lambda expression for
* the action to be take after the fiven delay, for example:
* This schedules an event to occur
* at In most cases, the caller will ignore the return value because
* this is only needed for events that will be cancelled or rescheduled.
* @param d, the delay until the event
* @param a, what to do for that event
* @returns a handle on the scheduled event
*/
public static Event schedule( double d, Action a ) {
RealEvent e = new RealEvent( time + d, a );
eventSet.add( e );
return e; // the RealEvent is returned as an Event, minus all detail
}
/** Cancel a previously scheduled event.
* Note that nothing happens if the event being cancelled has
* already been simulated or has not been scheduled.
* @param e the event to cancel
*/
public static void cancel( Event e ) {
RealEvent re = (RealEvent)e; // This is not free, but it's cheap
// only pay this price if we cancel
eventSet.remove( re );
}
/** Re-schedule a previously scheduled event.
* Note that nothing happens if the event being rescheduled has
* already been simulated or has not been scheduled.
* @param e the event to reschedule
* @param d the delay until that event
*/
public static void reschedule( Event e, double d ) {
RealEvent re = (RealEvent)e; // This is not free, but it's cheap
// only pay this price if we reschedule
if (eventSet.remove( re )) {
re.time = time + d;
eventSet.add( re );
}
}
/** Run the simulation.
* Before running the simulation, All error and warning messages go to System.err
* (aka stderr, the standard error stream).
* @author Douglas W. Jones
* @version May 2, 2021 better Javadoc usage and added maxWarnings
*/
public class Error {
private Error() {} // prevent construction of instances!
private static int warningCount = 0;
/** Maximum number of warnings allowed
* This is checked by This never returns, the program terminates reporting failure.
* @param msg error message to be output
*/
public static void fatal( String msg ) {
System.err.println( "Epidemic: " + msg );
System.exit( 1 ); // abnormal termination
}
/** Give a non-fatal warning.
* This keeps a running count of warnings used by
* If there were warnings, the program terminates reporting failure.
* @param msg the message to use
*/
public static void exitIfWarnings( String msg ) {
if (warningCount > 0) fatal( msg );
}
}
xxxxxxxxxx
cat > MyScanner.java <<\xxxxxxxxxx
// MyScanner.java
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.regex.Pattern;
/** Support for scanning input files with error reporting
* This is a wrapper class around class Scanner.
* Ideally, this should extend class To pass a specific message, create a subclass of The reason for passing messages this way is because most input is
* correct most of the time, so there is no reason to construct a
* complete error message unless there is an actual error in the input.
* Therefore, instead of passing a Names begin with a letter followed by letters or digits.
* If there is no name, the scanner skips until if finds one.
* This differs from Integers consist of at least one decimal digit in sequence.
* If there is no integer, the scanner skips until if finds one.
* This number need not
* end with a delimiter but may abut trailing punctuation.
* @param errorMessage λ expression giving error message
* @return the integer that was found or Numbers may be simple integers (no point) or an optional
* integer part, a point and an optional fractional part, but a point
* standing alone is not a number. Exponential notation is not currently
* supported. If there is no number, the scanner skips until it finds one.
* This number need not
* end with a delimiter but may abut trailing punctuation.
* @param errorMessage λ expression giving error message
* @return the number that was found or If the next input to the scanner is a literal, scan over it.
* Otherwise, report that the literal was not present.
* This differs from For technical reasons, the pattern used to specify the literal must
* match the empty string as well. So, to match This uses compliance
followed by a probabilty
* PlaceKind
* should either be
*
* @param in the input stream
* @see PlaceKind
* @see Schedule
*/
public Role( MyScanner in ) {
PlaceKind homePlaceKind = null; // the home place for this role
name = in.getNextName( ()-> "role with no name" );
fraction = in.getNextDouble(
()-> "role " + name + ": not followed by population"
);
// is population followed by compliance?
if (in.tryNextLiteral( complyPat )) {
compliance = Probability.get( in,
()-> "role " + name
+ " " + fraction
+ "compliance"
);
} else {
compliance = 1.0;
}
// get the list of places associated with this role
boolean hasNext = in.hasNext(); // needed below for missing semicolon
while (hasNext && !in.tryNextLiteral( Check.semicolonPat )) {
String placeName = in.getNextName(
()->"role " + name + " " + fraction + ": place name expected"
);
PlaceKind pk = PlaceKind.findPlaceKind( placeName );
Schedule s = null;
// is placeName followed a schedule?
if (in.tryNextLiteral( Check.beginParenPat )) {
s = new Schedule( in, ()-> this.describe() + " " + placeName );
}
// was it a real place name?
if (pk == null) {
Error.warn(
this.describe() + " " + placeName + ": undefined place?"
);
}
// see if this role is already associated with PlaceKind pk
boolean duplicated = false;
boolean overlap = false;
if (pk != null) {
if (pk == homePlaceKind) duplicated = true;
for (PlaceSchedule ps: placeKinds) {
if (ps.placeKind == pk) duplicated = true;
if ((ps.schedule != null) && (ps.schedule.overlap(s))) {
overlap = true;
}
}
}
if (duplicated) {
Error.warn(
this.describe() + " " + placeName + ": place name reused?"
);
} else if (overlap) {
Error.warn(
this.describe() + " " + placeName + ": schedule overlap?"
);
} else { // only record non-duplicate entries
placeKinds.add( new PlaceSchedule( pk, s ) ); // schedule all
if (s == null) {
if (homePlaceKind != null) Error.warn(
this.describe() + " " + placeName + ": a second home?"
);
homePlaceKind = pk;
}
}
hasNext = in.hasNext();
}
if (!hasNext) Error.warn( this.describe() + ": missing semicolon?" );
// complain if the name is not unique
if (findRole( name ) != null) {
Error.warn( this.describe() + ": role name reused?" );
}
// force the fraction or population to be positive
if (fraction <= 0.0) Error.warn(
this.describe() + ": fraction of population must be positive!"
);
sum = sum + fraction;
// complain if no places for this role
if (homePlaceKind == null) {
Error.warn( this.describe() + ": no home specified?" );
}
if (placeKinds.isEmpty()) {
Error.warn( this.describe() + ": has no places?" );
}
allRoles.add( this ); // include this role in the list of all roles
}
/** Produce a reasonably full textual description of this role.
* Schedule
* PlaceKind.distributePeople()
.
* The division is needed in order to randomize which specific place
* in each category gets visited by each person.
* @param population the total population to be created
* @param infected the total number of initially infected people
* @see Person
* @see PlaceKind
*/
public static void populateRoles( int population, int infected ) {
int pop = population; // working copy used only in infection decisions
int inf = infected; // working copy used only in infection decisions
final MyRandom rand = MyRandom.stream;
if (allRoles.isEmpty()) Error.fatal( "no roles specified" );
for (Role r: allRoles) {
// how many people are in this role
r.number = (int)Math.round( (r.fraction / sum) * population );
// make that many people and infect the right number at random
for (int i = 0; i < r.number; i++) {
Person p = new Person( r );
// the ratio inf/pop is probability this person is infected
if (rand.nextFloat() < ((float)inf / (float)pop)) {
p.infect();
inf = inf - 1;
}
pop = pop - 1;
// each person is associated all their role's place kinds
// note that this does not create places yet
for (PlaceSchedule ps: r.placeKinds) {
ps.placeKind.populate( p, ps.schedule );
}
}
}
// finish putting people in their places
// this actually creates the places and puts people in them
PlaceKind.distributePeople();
}
}
xxxxxxxxxx
cat > PlaceKind.java <<\xxxxxxxxxx
// PlaceKind.java
import java.util.Collections;
import java.util.LinkedList;
import java.util.regex.Pattern;
/** Categories of places.
* @author Douglas W. Jones
* @version May 12, 2021 places now have screening policies
* @see Place
* @see MyRandom
* @see MyScanner
*/
public class PlaceKind {
// linkage from person to associated place involves a schedule
private class PersonSchedule {
public Person person;
public Schedule schedule;
public PersonSchedule( Person p, Schedule s ) {
person = p;
schedule = s;
}
}
// instance variables from the input
/** name of this category of place */
public final String name;
/** likelihood of disease transmission here */
public final double transmissivity;
/** effectiveness of antigen screening tests here */
public final double testAntigenRate;
/** effectiveness of symptom screening tests here */
public final double testSymptomRate;
private final double median; // median population for each place
private final double scatter; // scatter of pop. distr., see sigma
// instance variables developed during model elaboration
private final double sigma; // sigma of log normal pop. distribution
private Place unfilledPlace = null; // a place of this kind being filled
private int unfilledCapacity = 0; // capacity of unfilledPlace
// a list of all the people associated with this kind of place
private final LinkedListPlace
.
* @param in the input stream
*/
public PlaceKind( MyScanner in ) {
name = in.getNextName( ()->"place with no name" );
median = in.getNextDouble(
()-> "place " + name + ": not followed by median"
);
scatter = in.getNextDouble(
()-> "place " + name + " " + median + ": not followed by scatter"
);
transmissivity = (1/Time.hour) * in.getNextDouble(
()-> "place " + name + " " + median + " " + scatter
+ ": not followed by transmissivity"
); // BUG: conversion factors this is given in per hour!!!
double tsr = 0.0; // default values of what will be final variables
double tar = 0.0;
if (!in.tryNextLiteral( Check.semicolonPat )) { // optional testing
in.tryNextLiteral( screenPat ); // optional keyword "screen"
if (in.tryNextLiteral( antigensPat )) {
tsr = Probability.get( in,
()-> this.describe() + "screen antigens"
);
}
if (in.tryNextLiteral( symptomsPat )) {
tar = Probability.get( in,
()-> this.describe() + "screen symptoms"
);
}
if ((tsr == 0.0) && (tar == 0.0)) Error.warn(
this.describe() + " screen: must give place's screening rule"
);
in.getNextLiteral( Check.semicolonPat,
()->this.describe() + ": missing semicolon"
);
}
testSymptomRate = tsr;
testAntigenRate = tar;
// complain if the name is not unique
if (findPlaceKind( name ) != null) {
Error.warn( this.describe() + ": duplicate name" );
}
// force the median to be positive
if (median < 0.0) {
Error.warn( this.describe() + ": non-positive median?" );
}
// force the scatter to be positive or zero
if (scatter < 0.0) {
Error.warn( this.describe() + ": negative scatter?" );
}
// force the transmissivity to be positive or zero
if (transmissivity < 0.0) {
Error.warn( this.describe() + ": negative transmissivity?" );
}
sigma = Math.log( (scatter + median) / median );
allPlaceKinds.add( this ); // include this in the list of all
}
/** Produce a reasonable textual description of this place.
* PlaceKind
objects by calls to
* populate
. At this point, each PlaceKind
* knows all the people that will be associated with places of that kind.
* The job of distributePeople
is to assure that
* there is no correlation between the order people were created in and
* the specific places to which they are associated. To this end, no
* specific places are created until all the people associated with the
* PlaceKind
are known, and then places of that kind are
* created, each with an associated capacity, until they just hold the
* people associated with that kind.
*/
public static void distributePeople() {
// for each kind of place
for (PlaceKind pk: allPlaceKinds) {
// shuffle its people to break correlations from people to places
Collections.shuffle( pk.people, MyRandom.stream );
// for each person, associate that person with a specific place
for (PersonSchedule ps: pk.people) {
ps.person.emplace( pk.findPlace(), ps.schedule );
}
}
}
/** Find a category of place, by name.
* p
, a call must be made to
* p.emplace(place,schedule)
before simulation begins.
* The separation between constructing people and emplacing them allows
* for shuffling the set of people in order to randomize the places into
* which they fall.
* @param r the role of this person
*/
public Person( Role r ) {
role = r;
hesitance = 0.0; // initially, people don't hesitate to travel
allPeople.add( this ); // include this person in the list of all
diseaseState.pop ++; // keep the population statistics up to date
};
// methods used during model construction, at time 0.0
/** Associate this person with a particular place and schedule.
* asymptomatic
and bedridden
,
* inclusive.
* @return true if they are
*/
public boolean isContageous() {
return
(diseaseState.compareTo( DiseaseStates.asymptomatic ) >= 0)
&& (diseaseState.compareTo( DiseaseStates.bedridden ) <= 0);
}
/** Is this person showing symptoms?
* symptomatic
and bedridden
,
* inclusive.
* @return true if they are
*/
public boolean isSymptomatic() {
return
(diseaseState.compareTo( DiseaseStates.symptomatic ) >= 0)
&& (diseaseState.compareTo( DiseaseStates.bedridden ) <= 0);
}
// simulation of behavior
/** Schedule the time at which a person will be infected.
* latent
if they are currently.
* scheduleInfect
but it is also called during initialization
* in order to create the initially infected sub-population.
* uninfected
.
*/
public void infect() {
infectMe = null; // discard expired event
if (diseaseState == DiseaseStates.uninfected) {
final double duration = latent.duration();
// update population statistics
diseaseState.pop--;
diseaseState = DiseaseStates.latent;
diseaseState.pop++;
if (latent.recover()) {
Simulator.schedule( duration, ()->recover() );
} else {
Simulator.schedule( duration, ()-> beContageous()
);
}
}
}
/** This person becomes contageous and asymptomatic.
* latent
* infection and makes the person asymptomatic
.
*/
private void beContageous() {
assert diseaseState == DiseaseStates.latent : "not latent";
final double duration = asymptomatic.duration();
// update population statistics
diseaseState.pop--;
diseaseState = DiseaseStates.asymptomatic;
diseaseState.pop++;
// tell place that I'm sick
if (location != null) location.contageous( +1 );
if (asymptomatic.recover()) {
Simulator.schedule( duration, ()-> recover() );
} else {
Simulator.schedule( duration, ()-> feelSick() );
}
}
/** This person is contageous and starts feeling sick.
* asymptomatic
infection and makes them
* symptomatic
.
* makes the person symptomatic.
*/
private void feelSick() {
assert diseaseState == DiseaseStates.asymptomatic: "not asymptomatic";
final double duration = symptomatic.duration();
// update population statistics
diseaseState.pop--;
diseaseState = DiseaseStates.symptomatic;
diseaseState.pop++;
if (symptomatic.recover()) {
Simulator.schedule( duration, ()-> recover() );
} else {
Simulator.schedule( duration, ()-> goToBed() );
}
}
/** This person is contageous and feels so bad they go to bed.
* symptomatic
infection and makes the person
* bedridden
.
*/
private void goToBed() {
assert diseaseState == DiseaseStates.symptomatic: "not symptomatic";
final double duration = bedridden.duration();
// update population statistics
diseaseState.pop--;
diseaseState = DiseaseStates.bedridden;
diseaseState.pop++;
// cease leaving home
hesitance = 1.0;
if (symptomatic.recover()) {
Simulator.schedule( duration, ()-> recover() );
} else {
Simulator.schedule( duration, ()-> die() );
}
}
/** This person gets better.
* recovered
* and immune from further infection.
*/
private void recover() {
// update population statistics
diseaseState.pop--;
diseaseState = DiseaseStates.recovered;
diseaseState.pop++;
// return to be willing to travel
hesitance = 0.0;
if (location != null) location.contageous( -1 );
}
/** This person dies
* bedridden
, and it makes that person dead
.
*/
private void die() {
assert diseaseState == DiseaseStates.bedridden: "not bedridden";
// leave place before state change so place contageon count updates
if (location != null) {
location.depart( this );
}
// update population statistics
diseaseState.pop--;
diseaseState = DiseaseStates.dead;
diseaseState.pop++;
// no new event is scheduled.
}
/** Tell this person to quarantine until they're better
* bedridden
* people never leave home.
* @param place where the person goes
*/
public void travelTo( Place place ) {
if (place != location) { // no work to do if already there
if ( (place == home) // always willing to go home
|| (hesitance == 0.0)
|| ((hesitance != 1.0) && (rand.nextFloat() > hesitance)) ) {
location.depart( this );
location = place;
location.arrive( this );
}
}
}
// reporting tools
/** Start the logical process of reporting results.
* system.out
* (aka stdout
). If a headline is requested, the first
* line gives the names of each column. All following lines are
* numeric, giving the time and the number of people in each disease
* state. The order of the disease states is set by a private
* in class Person
and disclosed in the headline.
* printed here.
* @param headline is a headline to be included
*/
public static void startReporting( boolean headline ) {
if (headline) {
System.out.print( "time" );
for (DiseaseStates s: DiseaseStates.values()) {
System.out.print( "," );
System.out.print( s.name() );
}
System.out.println();
}
// schedule the first report
Simulator.schedule( 0.0, ()-> Person.report() );
}
/** Report population statistics at the given time.
* system.out
* (aka stdout
) giving the time and the
* population statistics for each disease state.
*/
private static void report() {
System.out.print( Double.toString( Simulator.time()/Time.day ) );
for (DiseaseStates s: DiseaseStates.values()) {
System.out.print( "," );
System.out.print( Integer.toString( s.pop ) );
}
System.out.println();
// schedule the next report
Simulator.schedule( 24*Time.hour, ()-> Person.report() );
}
/** Print out the entire population.
* This is needed only in the early stages of debugging
* and obviously useless for large populations.
*/
public static void printAll() {
for (Person p: allPeople) {
// line 1: person id and role
System.out.print( p.toString() );
System.out.print( " " );
System.out.println( p.role.name );
// line 2 the home
System.out.print( " " ); // indent following lines
System.out.print( p.home.kind.name );
System.out.print( " " );
System.out.print( p.home.toString() );
System.out.println();
// lines 3 and up: each place and its schedule
for (PlaceSchedule ps: p.places ) {
System.out.print( " " ); // indent following lines
System.out.print( ps.place.kind.name );
System.out.print( " " );
System.out.print( ps.place.toString() );
assert ps.schedule != null: "guaranteed by PlaceKind";
System.out.print( ps.schedule.toString() );
System.out.println();
}
}
}
}
xxxxxxxxxx
cat > Place.java <<\xxxxxxxxxx
// Place.java
import java.util.LinkedList;
/** Places that people are associate with and may occupy.
* PlaceKind
.
* @author Douglas W. Jones
* @version May 12, 2021 try to add screening tests to places
* @see PlaceKind for most of the attributes of places
*/
public class Place {
// instance variables fixed at creation
/** What kind of place is this? */
public final PlaceKind kind;
// Instance variables that vary with circumstances
// how many infectious people are here
private int contageous = 0;
// who is currently in this place?
private final LinkedListPlaceKind
.
* The population changes as people come and go, and for those who are
* there, the transmissivity and the infected fraction of the population
* determine how likely they are to be infected. Greater transmissivity
* implies a greater likelihood of infections; transmissivity is measured
* in infections per time unit.
* @param k the kind of place
*/
public Place( PlaceKind k ) {
kind = k;
}
/** Make a person arrive at a place.
* (time-time[@day][prob])
* Time.get()
,
* Days are read using Week.get()
and
* probabilities are read using Probability.get()
.
* @param in the input stream
* @param context the context for error messages
*/
public Schedule( MyScanner in, MyScanner.Message context ) {
// get start time of schedule
startTime = Time.get( in, Time.hour, ()-> context.myString() + "(: " );
in.getNextLiteral( dashPat,
()-> context.myString() + "(" + Time.toString( startTime )
+ ": - expected"
);
// get end time of schedule
final double endTime = Time.get( in, Time.hour,
()-> context.myString() + "(" + Time.toString( startTime ) + "-: "
);
duration = endTime - startTime;
if (in.tryNextLiteral( atPat )) {
whatDays = Week.get( in,
()-> context.myString() + "(" + Time.toString( startTime )
+ '-' + Time.toString( endTime )
);
} else {
whatDays = Week.everyDay;
}
if (!in.tryNextLiteral( endParenPat )) {
likelihood = Probability.get( in,
()-> context.myString() + "(" + Time.toString( startTime )
+ '-' + Time.toString( endTime )
);
in.getNextLiteral( endParenPat,
()-> context.myString() + "(" + Time.toString( startTime )
+ "-" + Time.toString( endTime )
+ " " + likelihood + ": ) expected"
);
} else {
likelihood = 1.0;
}
// the object is completely constructed, it's now safe to use toString()
// check sanity constraints on schedule
if (startTime >= Time.day) Error.warn(
context.myString() + this.toString()
+ ": start time is tomorrow"
);
if (duration >= Time.day) Error.warn(
context.myString() + this.toString()
+ ": one day limit exceeded"
);
}
/** Compare two schedules to see if they overlap.
* This is used to detect schedule conflicts within a Role
.
* @param s the schedule to compare with
* @return true if they overlap, false otherwise
* @see Role
*/
public boolean overlap( Schedule s ) {
if (s == null) return false; // avoids exception when input syntax error
if (!Week.setsIntersect( this.whatDays, s.whatDays )) return false;
double thisEnd = this.startTime + this.duration;
if (this.startTime <= s.startTime) {
if (s.startTime <= (this.startTime + this.duration)) return true;
}
double sEnd = s.startTime + s.duration;
if (s.startTime <= this.startTime) {
if (this.startTime <= (s.startTime + s.duration)) return true;
}
// BUG: Fails to check schedules like (10pm-6am tomorrow) vs (1am-2am)
return false;
}
/** Commit some person to some schedule for visits to some place.
*
* The median and duration are scanned using Time.getTime()
.
* @param in the input stream
* @param context the context for error messages
* @see Time
*/
public InfectionRule( MyScanner in, MyScanner.Message context ) {
final double scatter;
median = Time.get( in, Time.day, ()-> context.myString() + ": median" );
scatter = Time.get( in, Time.day,
()-> context.myString() + " " + Time.toString( median )
+ ": scatter"
);
if (!in.tryNextLiteral( Check.semicolonPat )) {
recovery = Probability.get( in,
()-> context.myString() + " " + Time.toString( median )
+ " " + Time.toString( scatter )
+ ": recovery"
);
if (!in.tryNextLiteral( Check.semicolonPat )) Error.warn(
context.myString() + " " + Time.toString( median )
+ " " + recovery + ": semicolon expected"
);
} else {
recovery = 0.0;
}
// sanity checks on the values
if (median <= 0.0) Error.warn(
// note that Time.get() only guarantees non-negative
context.myString() + " " + Time.toString( median )
+ " " + Time.toString( scatter )
+ " " + recovery + ": non-positive median?"
);
// no need to check scatter, Time.get guarantees non-negative
// no need to check recovery, Probability.get guarantees validity
// we do this up front so scatter is never seen again.
// this is safe because (scatter+median) is guaranteed non-negative
// and median == 0.0, +x/median gives Double.PositiveInfinity
sigma = Math.log( (scatter + median) / median );
}
/** Toss the dice to see if someone recovers under the terms of this rule.
* MyRandom
as a source of randomness.
* @return true if recovers, false if not
* @see MyRandom
*/
public boolean recover() {
return rand.nextFloat() <= recovery;
}
/** Toss the dice to see how long this disease state lasts under this rule.
* MyRandom
as a source of randomness.
* @return the time until the next change of disease state
* @see MyRandom
*/
public double duration() {
return rand.nextLogNormal( median, sigma );
}
}
xxxxxxxxxx
cat > Week.java <<\xxxxxxxxxx
// Week.java
import java.util.regex.Pattern;
/** All about dividing time into weeks.
*
* Note that Monday
— a single day
* Mon
— abbreviated (always 3 letters)
* -
day — consecutive day range
* ,
range } — set of days
* weekdays
— Monday-Friday
* weekends
— Saturday-Sunday
* daily
— Sunday-Saturday
* MWF
— Monday,Wednesday,Friday
* TuTh
— Tuesday,Thursday
* Monday-Thursday
means
* Monday, Tuesday, Wednesday, Thursday
while
* Note that Thursday-Monday
means
* Thursday, Friday, Saturday, Sunday, Monday
.
* @param sc the scanner to use
* @param context the context string for error messages
* @return the set of days encoded as an integer
*/
public static int get( MyScanner sc, MyScanner.Message context ) {
// WARNING: The following code is a bit lame but works.
if (sc.tryNextLiteral( weekdaysPat )) return weekdays;
if (sc.tryNextLiteral( weekendsPat )) return weekends;
if (sc.tryNextLiteral( dailyPat )) return everyDay;
if (sc.tryNextLiteral( mwfPat )) return mwf;
if (sc.tryNextLiteral( tuthPat )) return tuth;
return getDayList( sc, context );
}
/* utility used by get to scan dayrange,dayrange,dayrange
* same parameters and return as get()
*/
private static int getDayList( MyScanner sc, MyScanner.Message context ) {
int accumulator = getDayRange( sc, context );
while (sc.tryNextLiteral( commaPat )) {
int newRange = getDayRange( sc, context );
int overlap = newRange & accumulator;
if (overlap != 0) Error.warn(
context.myString() + toString( overlap )
+ ": day included twice?"
);
accumulator = accumulator | newRange;
}
return accumulator;
}
/* utility used by get to scan day-day
* same parameters and return as get()
*/
private static int getDayRange( MyScanner sc, MyScanner.Message context ) {
int first = getOneDay( sc, context );
if (sc.tryNextLiteral( dashPat )) {
int second = getOneDay( sc, context );
if (first < second) {
// WARNING: Fun trick with set represented as int bit vector!
first = ((second - 1) - (first - 1)) + second;
} else if (first > second) {
// WARNING: Fun trick with set represented as int bit vector!
first = (everyDay - (first - 1)) + (second - 1) + second;
} else {
Error.warn(
context.myString()
+ toString( first ) + "-" + toString( second )
+ ": day included twice?"
);
}
}
return first;
}
/* utility used by get to scan one day name
* same parameters and return as get()
*/
private static int getOneDay( MyScanner sc, MyScanner.Message context ) {
// WARNING: The following code is a bit lame but works.
if (sc.tryNextLiteral( sundayPat )) return sunday;
if (sc.tryNextLiteral( mondayPat )) return monday;
if (sc.tryNextLiteral( tuesdayPat )) return tuesday;
if (sc.tryNextLiteral( wednesdayPat )) return wednesday;
if (sc.tryNextLiteral( thursdayPat )) return thursday;
if (sc.tryNextLiteral( fridayPat )) return friday;
if (sc.tryNextLiteral( saturdayPat )) return saturday;
Error.warn(
context.myString() + sc.getNextName( context ) + "not a day name"
);
return 0; // empty set prevents some error cascades, beware of others!
}
/** Given a time return the day of the week
* @param time the time to convert
* @return the day of the week, from 0 (Sunday) to 6 (Saturday)
*/
public static int dayOfWeek( double time ) {
return ((int)Math.floor( time / Time.day )) % 7;
}
/** How long is it until a a day is in this set of days?
* @param a day of the week from 0 (Sunday) to 8 (next Sunday)
* @param a non-empty set of days
* @return a number of days, from 0 to 7
*/
public static int daysUntil( int day, int set ) {
assert set != 0: "unexpected empty set of days";
int dayCount = 0;
do {
if (day > 6) day = day - 7;
if (((1 << day) & set) != 0 ) break;
day = day + 1;
dayCount = dayCount + 1;
} while (true); // loop exit is by break
return dayCount;
}
/** Do two sets of days overlap
* @param dayset1 a set of days
* @param dayset2 another set of days
* @return true if the intersection of the sets is not empty
*/
public static boolean setsIntersect( int dayset1, int dayset2 ) {
return (dayset1 & dayset2) != 0;
}
/** Convert a set of days to textual format
* @param days the set of days
* @return the time in textual format
*/
public static String toString( int dayset ) {
// WARNING: The following code is a bit lame but works.
if (dayset == weekdays) return "weekdays";
if (dayset == weekends) return "weekends";
if (dayset == everyDay) return "daily";
if (dayset == mwf) return "MWF";
if (dayset == tuth) return "TuTh";
// WARNING: The following code is almost inexcusably stupid but works!
StringBuilder returnVal = new StringBuilder();
while (dayset != 0) {
int remainingdays = dayset & (dayset - 1);
int thisday = dayset - remainingdays;
if (thisday == sunday) {
returnVal.append( "Sun" );
} else if (thisday == monday) {
returnVal.append( "Mon" );
} else if (thisday == tuesday) {
returnVal.append( "Tue" );
} else if (thisday == wednesday) {
returnVal.append( "Wed" );
} else if (thisday == thursday) {
returnVal.append( "Thu" );
} else if (thisday == friday) {
returnVal.append( "Fri" );
} else if (thisday == saturday) {
returnVal.append( "Sat" );
} else {
assert false:"this should never happen";
}
if (remainingdays != 0) returnVal.append( ',' );
dayset = remainingdays;
}
return returnVal.toString();
}
}
xxxxxxxxxx
cat > Time.java <<\xxxxxxxxxx
// Time.java
import java.util.regex.Pattern;
/** All about simulated time.
* double
values. Code outside of
* this package should use the constants second
,
* minute
, hour
and day
,
* and should not make assumptions about the underlying time units.
* @author Douglas W. Jones
* @version May 2, 2021 supports notations like 5:15PM, 6 days, noon.
*/
public class Time {
private Time(){} // nobody ever constructs a Time object
/** one day of simulated time.
* Multiply a time in days by day
to convert to internal
* form, and divide internal times by day
go create a
* human-readable time in days.
*/
public static final double day = 1.0F;
/** one hour of simulated time.
* See day
for the general use of this constant.
*/
public static final double hour = day / (24.0);
/** one minute of simulated time.
* See day
for the general use of this constant.
*/
public static final double minute = day / (24.0 * 60);
/** one second of simulated time.
* See day
for the general use of this constant.
*/
public static final double second = day / (24.0 * 60 * 60);
// literals used to scan time units
private static Pattern colonPat = Pattern.compile( ":|" );
private static Pattern dayPat = Pattern.compile( "day(s?)|" );
private static Pattern hrPat = Pattern.compile( "hr(s?)|" );
private static Pattern minPat = Pattern.compile( "min(s?)|" );
private static Pattern secPat = Pattern.compile( "sec(s?)|" );
private static Pattern amPat = Pattern.compile( "am|AM|" );
private static Pattern pmPat = Pattern.compile( "pm|PM|" );
private static Pattern noonPat = Pattern.compile( "noon|" );
private static Pattern midnightPat = Pattern.compile( "midnight|" );
private static Pattern todayPat = Pattern.compile( "today|" );
private static Pattern tomorrowPat = Pattern.compile( "tomorrow|" );
/** Scan a time from the input.
*
* @param sc the scanner to use
* @param defaultUnit the default time unit if units not explicitly given
* @param context the context string for error messages
*/
public static double get( MyScanner sc, double defaultUnit,
MyScanner.Message context ) {
double time;
if (sc.tryNextLiteral( noonPat )) {
time = 12 * hour;
} else if (sc.tryNextLiteral( midnightPat )) {
time = day;
} else {
time = sc.getNextDouble(
()-> context.myString() + " time expected"
);
if (sc.tryNextLiteral( colonPat )) {
double m = sc.getNextDouble(
()-> context.myString() + " minutes expected"
);
if (m < 0.0) Error.warn(
context.myString() + " negative minutes?"
); else if (m > 60.0) Error.warn(
context.myString() + " only 60 minutes/hour"
);
time = (time * hour) + (m * minute);
if (sc.tryNextLiteral( colonPat )) {
double s = sc.getNextDouble(
()-> context.myString() + " seconds expected"
);
if (m < 0.0) Error.warn(
context.myString() + " negative seconds?"
); else if (s > 60.0) Error.warn(
context.myString() + " only 60 seconds/minute"
);
time = time + (s * second);
}
} else if (sc.tryNextLiteral( dayPat )) {
time = time * day;
} else if (sc.tryNextLiteral( hrPat )) {
time = time * hour;
} else if (sc.tryNextLiteral( minPat )) {
time = time * minute;
} else if (sc.tryNextLiteral( secPat )) {
time = time * second;
} else { // no units give
time = time * defaultUnit;
}
if (sc.tryNextLiteral( amPat )) {
// no action required
} else if (sc.tryNextLiteral( pmPat )) {
time = time + (12 * hour);
}
if (time < 0.0) Error.warn(
context.myString() + " time is negative?" + time
);
}
if (sc.tryNextLiteral( todayPat )) {
// no action
} else if (sc.tryNextLiteral( tomorrowPat )) {
time = time + day;
}
return time;
}
/** Convert a time to textual format
* Times under one day are returned in 1.05
– using default time units
* 10:01
– hours and minutes
* 10:01:01
— hours, minutes, seconds
* 1.05 day
— time in days
* 1.05 hr
— time in hours
* 1.05 min
— time in minutes
* 1.05 sec
— time in seconds
* am
— time before noon
* (AM
too)
* pm
— time after noon
* (PM
too)
* noon
— eliminate ambiguity about 12am
* midnight
— – eliminate more ambiguity
* today
— the default
* tomorrow
— 24 hours in the future
* 00:00
— midnight at the start of the day
* 12:00
— noon
* 24:00
— midnight at the end of the day
* HH:MM:SS
format
* in 24-hour form, rounded to the nearest
* second. Times over one day are in days with a decimal fractional part,
* so 1.5
is the same as noon tomorrow
.
* @param time the time to convert
* @return the time in textual format
*/
public static String toString( double time ) {
if (time >= day) {
return Double.toString( time / day );
} else {
int seconds = (int)Math.round(time / second);
int minutes = seconds / 60;
int hours = minutes / 60;
seconds = seconds % 60;
minutes = minutes % 60;
if (seconds == 0) {
return String.format( "%1d:%02d", hours, minutes );
}
return String.format( "%1d:%02d:%02d", hours, minutes, seconds );
}
}
}
xxxxxxxxxx
cat > Probability.java <<\xxxxxxxxxx
// Probability.java
import java.util.regex.Pattern;
/** Support for reading probabilities, either just a float or a percentage.
* @author Douglas W. Jones
* @version May 2, 2021 better Javadoc comments
*/
public class Probability{
private Probability(){} // nobody ever constructs a Probability
// literals used to scan probability
private final static Pattern percentPat = Pattern.compile( "%|" );
/** Scan a probability from the input file.
*
* @param sc the scanner to use
* @param context the context for error messages
*/
public static double get( MyScanner sc, MyScanner.Message context ) {
double prob = sc.getNextDouble(
()-> context.myString() + " probability expected"
);
if (sc.tryNextLiteral( percentPat )) {
prob = prob / 100.0;
}
if (prob < 0.0) {
Error.warn( context.myString() + " negative probability? " + prob );
}
if (prob > 1.0) {
Error.warn( context.myString() + " probability over one? " + prob );
}
return prob;
}
}
xxxxxxxxxx
cat > Check.java <<\xxxxxxxxxx
// Check.java
import java.util.regex.Pattern;
/** A collection of application specific input scanning bits and pieces.
* 0.95
– as a fraction
* 95%
– as a percentage
* MyScanner.tryNextLiteral
for begin paren */
public static final Pattern beginParenPat = Pattern.compile( "\\(|" );
/** Parameter for MyScanner.tryNextLiteral
for semicolon */
public static final Pattern semicolonPat = Pattern.compile( ";|" );
/** Scan for a positive integer followed by a semicolon.
* Random
* because of the risk of autocorrelation between different streams. To
* prevent that, this wrapper creates and exports exactly one instance of
* itself that should be the only source of randomness in the program.
* @author Douglas W. Jones
* @version May 2, 2021 improved Javadoc comments
* @see Random
*/
public class MyRandom extends Random {
/** the only random number stream.
*/
public static final MyRandom stream = new MyRandom();
// nobody can construct a MyRandom except the above line of code
private MyRandom() {
super();
}
/** Alternative access to the only random number stream.
* MyRandom.stream() are equivalent to
* use of
MyRandom.stream.
* @return the only stream
*/
public static MyRandom stream() {
return stream;
}
// add distributions that weren't built in
/** Exponential distribution.
* @param mean the mean value of the distribution
* @return a positive finite exponentially distributed random value
*/
public double nextExponential( double mean ) {
// 1.0 - nextDouble() guarantees that the result will not be infinite
return mean * -Math.log( 1.0 - this.nextDouble() );
}
/** Log-normal distribution.
* @param median the median value of the distribution
* @param sigma the sigma of the underlying normal distribution
* @return a log-normally distributed random value
*/
public double nextLogNormal( double median, double sigma ) {
return Math.exp( sigma * this.nextGaussian() ) * median;
}
}
xxxxxxxxxx
cat > Simulator.java <<\xxxxxxxxxx
// Simulator.java
import java.util.PriorityQueue;
/** Framework for discrete event simulation.
* @author Douglas W. Jones
* @version May 2, 2021 Change simulation framework, better Javadoc.
*/
class Simulator {
private Simulator() {} // prevent construction of instances! Don't call!
/** The current simulated time
*/
private static double time = 0.0;
/** Get the time
* @return the current simulated time
*/
public static double time() { return time; };
/** Functional interface for scheduling actions to be done later.
*
* (double t)-> someMethod( t, otherParameters )
*
*/
public static interface Action {
void trigger();
}
/** Event is the parent of real events scheduled in the simulator.
* RealEvent
is private to class
* simulator
, users cannot access fields or methods of
* class event
. This protects users from dangerous
* errors involving access to fields that should not be touched
*/
public static class Event {}
/** RealEvents scheduled in the simulation framework
*/
private static class RealEvent extends Event {
public double time; // when will this event occur
public final Action act; // what to do then
public RealEvent( double t, Action a ) {
time = t;
act = a;
}
}
// the pending event set, holding all scheduled but not triggered events
private static final PriorityQueue
* Simulator.schedule( later, ()-> whatToDo( stuff ) );
*
* Simulator.time()+later
; At that simulated time,
* whatToDo(stuff)
will be called.
* schedule()
the initial events
* all of the simulation occurs as side effects of scheduled events
*/
public static void run() {
while (!eventSet.isEmpty()) {
RealEvent e = eventSet.remove();
time = e.time;
e.act.trigger();
}
}
}
xxxxxxxxxx
cat > Error.java <<\xxxxxxxxxx
// Error.java
/** Error reporting framework
*
warn
.
* Users can change the default limit of 15.
* @see warn
*/
public static int maxWarnings = 15;
/** Report a fatal error.
* exitIfWarnings
. In addition, if the count
* exceeds maxWarnings
, the program terminates reportinf
* failure.
* @see exitIfWarnings
* @see maxWarnings
* @param msg the warning message
*/
public static void warn( String msg ) {
System.err.println( "Warning: " + msg );
warningCount = warningCount + 1;
if (warningCount > maxWarnings) System.exit( 1 ); // too many errors
}
/** Terminate program if there were any warnings.
* java.util.Scanner
but
* that class is final, precluding extension.
* @author Douglas W. Jones
* @version May 2, 2021 better Javadoc comments
* @see Error
* @see "java.util.Scanner"
*/
public class MyScanner {
private Scanner sc; // the scanner we are wrapping
/** The default returned by getNextName()
. */
public static final String defaultName = "???";
/** The default returned by getNextInt()
. */
public static final int defaultInt = Integer.MIN_VALUE;
/** The default returned by getNextDouble()
. */
public static final double defaultDouble = Double.MIN_VALUE;
/** Construct a scanner to scan a text file.
* @param f the file to scan from
* @throws FileNotFoundException if f
cannot be opened to read
*/
public MyScanner( File f ) throws FileNotFoundException {
sc = new Scanner( f );
}
// the following methods are ones we wish we could inherit, so we fake it!
/** A method from class java.util.Scanner
.
* @return true if there is more text to scan
*/
public boolean hasNext() { return sc.hasNext(); }
/** A method from class java.util.Scanner
.
* @param s the string to look for (a regular expression)
* @return true if the next token is a particular literal string
*/
public boolean hasNext( String s ) { return sc.hasNext( s ); }
/** A method from class java.util.Scanner
.
* @return the next token
*/
public String next() { return sc.next(); }
// patterns that matter in what follows
// delimiters are spaces, tabs, newlines and carriage returns
private static final Pattern delimPat
= Pattern.compile( "([ \t\n\r]|#[^\n]*\n)*" );
// note that all of the following patterns allow an empty string to match
// this is used in error detection below
// if it's not a name, it begins with a non-letter
private static final Pattern NotNamePat
= Pattern.compile( "([^A-Za-z]*)|" );
// names consist of a letter followed optionally by letters or digits
private static final Pattern namePat
= Pattern.compile( "([A-Za-z][0-9A-Za-z]*)|" );
// if it's not an int, it begins with a non-digit, non-negative-sign
private static final Pattern NotIntPat
= Pattern.compile( "([^-0-9]*)|" );
// ints consist of an optional sign followed by at least one digit
private static final Pattern intPat = Pattern.compile(
"((-[0-9]|)[0-9]*)"
);
// floats consist of an optional sign followed by
// at least one digit, with an optional point before between or after them
private static final Pattern floatPat = Pattern.compile(
"-?(([0-9]+\\.[0-9]*)|(\\.[0-9]+)|([0-9]*))"
);
/** Tool to defer computation of messages output by methods of MyScanner.
* Message
* to do it. In general, users will not need to use this interface by
* name; instead, they will pass parameters using λ expressions such
* as:
*
* ()-> "Some message" + composed + "as needed"
*
* String
, we pass a
* λ expression that allows the string to be computed in the
* rare case that it is actually needed.
*/
public interface Message {
/** evaluate the expression that computes the text
* @return the text of the error message
*/
String myString();
}
// new methods added to class Scanner
/** Scan the next name or complain if it is missing.
* next()
in that the name need not
* end with a delimiter but may abut trailing punctuation.
* @param errorMessage λ expression giving error message
* @return the name that was found or defaultName
if none
*/
public String getNextName( Message errorMessage ) {
// first skip the delimiter, accumulate anything that's not a name
String notName = sc.skip( delimPat ).skip( NotNamePat ).match().group();
// second accumulate the name
String name = sc.skip( namePat ).match().group();
if (!notName.isEmpty()) { // there's something else a name belonged
Error.warn(
errorMessage.myString() + ": name expected, skipping " + notName
);
}
if (name.isEmpty()) { // missing name
Error.warn( errorMessage.myString() );
return defaultName;
} else { // there was a name
return name;
}
}
/** Get the next integer from the scanner or complain if there is none.
* defaultInt
if none
*/
public int getNextInt( Message errorMessage ) {
// first skip the delimiter, accumulate anything that's not an int
String notInt = sc.skip( delimPat ).skip( NotIntPat ).match().group();
// second accumulate the int, if any
String text = sc.skip( delimPat ).skip( intPat ).match().group();
if (!notInt.isEmpty()) { // there's something else where an int belonged
Error.warn(
errorMessage.myString() + ": int expected, skipping " + notInt
);
}
if (text.isEmpty()) { // missing name
Error.warn( errorMessage.myString() );
return defaultInt;
} else { // the name was present and it matches intPat
return Integer.parseInt( text );
}
}
/** Scan the next floating point number or complain if there is none.
*
defaultDouble
if none.
*/
public double getNextDouble( Message errorMessage ) {
// skip the delimiter, if any, then the float, if any; get the latter
String text = sc.skip( delimPat ).skip( floatPat ).match().group();
if (text.isEmpty()) { // missing name
Error.warn( errorMessage.myString() );
return defaultDouble;
} else { // the name was present and it matches intPat
return Float.parseFloat( text );
}
}
/** Try to scan the next literal.
* hasNext(String)
in two ways:
* the literal need not end with a delimiter but may abut what follows,
* and if the literal is present, it is skipped.
* pat, use
* the string
"pat|"
. Be careful to escape characters with
* special meanings in regular expressions. For example, to match
* begin paren, use the string "\\(|"
.
*
* MyScanner.tryNextLiteral( MyScanner.dash )
*
* @param literal the literal to get, a regular expression
* @return true if the literal was present and skipped, false otherwise
* @see java.util.regex.Pattern
*/
public boolean tryNextLiteral( Pattern literal ) {
sc.skip( delimPat ); // allow delimiter before literal!
String s = sc.skip( literal ).match().group();
return !s.isEmpty();
}
/** Scan the next literal or complain if missing.
* tryNextLiteral
to identify the literal.
* @param literal the literal to get, a regular expression
* @param errorMessage λ expression giving error message
* @see tryNextLiteral
*/
public void getNextLiteral( Pattern literal, Message errorMessage ) {
if ( !tryNextLiteral( literal ) ) {
Error.warn( errorMessage.myString() );
}
}
}
xxxxxxxxxx
cat > Makefile <<\xxxxxxxxxx
# Makefile for the epidemic simulator
# Author: Douglas W. Jones
# Version: May 12, 2021 -- places now have screening policies
# Support for:
# make -- make the default target
# make Epidemic.class -- the default target
# Plus the following utilities
# make demo f=file -- demonstrate the simulator with input from file
# make demo -- same as make demo f=testa
# make plot f=file -- demonstrate and plot results with input from
# make plot -- same as make plot f=testa
# make clean -- delete all files created by make
# make html -- make a Javadoc web site from the simulator code
# make shar -- make shell archive from this directory
########
# named categories of files used below
# These names are grouped in categories related to the hierarchic
# structure of the program so this makefile also serves as a road map
# to the structure of the simulator
# Top layer used by Epidemic.java
# Sublayer: The model files
ModSrc = Role.java PlaceKind.java Person.java Place.java Schedule.java
ModCls = Role.class PlaceKind.class Person.class Place.class Schedule.class
# Sublayer: Upper Support for the model
ModSupUpSrc = InfectionRule.java Week.java
ModSupUpCls = InfectionRule.class Week.class
# Sublayer: Lower Support for the model
ModSupDnSrc = Time.java Probability.java Check.java
ModSupDnCls = Time.class Probability.class Check.class
# Lower layer general purpose utilities usee by the above
# Sublayer: simulation utilities
SimUtilSrc = MyRandom.java Simulator.java
SimUtilCls = MyRandom.class Simulator.class
# Sublayer: Input utilities
InpUtilSrc = Error.java MyScanner.java
InpUtilCls = Error.class MyScanner.class
# named supercategories of files already categorized above
# Groupings are useful to show
# all model related files
ModAllSrc = $(ModSrc) $(ModSupUpSrc) $(ModSupDnSrc)
ModAllCls = $(ModCls) $(ModSupUpCls) $(ModSupDnCls)
# all utility files
UtilSrc = $(SimUtilSrc) $(InpUtilSrc)
UtilCls = $(SimUtilCls) $(InpUtilCls)
# all top level file groups
AllSrc = Epidemic.java $(ModAllSrc) $(UtilSrc)
AllCls = Epidemic.class $(ModAllCls) $(UtilCls)
AllTest = testa testb testc testd
########
# Make Targets for java code
# Dependencies for each target are organizaed
# by the layering documented above, upper layers first.
# The source file dependency is always listed last.
# Java works pretty well with such makefiles, surprisingly.
####
# default make for the epidemic simulator
Epidemic.class: Person.class PlaceKind.class Role.class
Epidemic.class: InfectionRule.class
Epidemic.class: Check.class Time.class
Epidemic.class: Simulator.class
Epidemic.class: $(InpUtilCls)
Epidemic.class: Epidemic.java
javac Epidemic.java
####
# core classes of the model used for the epidemic simulator
# Note: there is a dependency knot tying all these together,
# Make complains about the loop, but it is included to document
# the dependencies. Aside from complaints, it causes no harm.
Role.class: Person.class PlaceKind.class Role.class
Role.class: Schedule.class
Role.class: Probability.class Check.class
Role.class: MyRandom.class
Role.class: $(InpUtilCls)
Role.class: Role.java
javac Role.java
PlaceKind.class: Person.class Place.class Schedule.class
PlaceKind.class: $(ModSupDnCls)
PlaceKind.class: MyRandom.class
PlaceKind.class: $(InpUtilCls)
PlaceKind.class: PlaceKind.java
javac PlaceKind.java
Person.class: Place.class Role.class Schedule.class
Person.class: InfectionRule.class
Person.class: Time.class
Person.class: $(SimUtilCls)
Person.class: Person.java
javac Person.java
Place.class: Person.class PlaceKind.class
Place.class: Time.class
Place.class: Simulator.class
Place.class: Place.java
javac Place.java
Schedule.class: Person.class Place.class
Schedule.class: Week.class
Schedule.class: Probability.class Time.class
Schedule.class: $(SimUtilCls)
Schedule.class: $(InpUtilCls)
Schedule.class: Schedule.java
javac Schedule.java
####
# Upper model support sublayer
InfectionRule.class: $(ModSupDnCls)
InfectionRule.class: MyRandom.class
InfectionRule.class: $(InpUtilSrc)
InfectionRule.class: InfectionRule.java
javac InfectionRule.java
Week.class: Time.class
Week.class: $(InpUtilSrc)
Week.class: Week.java
javac Week.java
####
# Lower model support sublayer
Check.class: $(InpUtilSrc)
Check.class: Check.java
javac Check.java
Probability.class: $(InpUtilSrc)
Probability.class: Probability.java
javac Probability.java
Time.class: $(InpUtilSrc)
Time.class: Time.java
javac Time.java
####
# generic simulation support classes
MyRandom.class: MyRandom.java
javac MyRandom.java
Simulator.class: Simulator.java
javac Simulator.java
####
# generic input management support classes
MyScanner.class: Error.class
MyScanner.class: MyScanner.java
javac MyScanner.java
Error.class: Error.java
javac Error.java
########
# Make Targets for using make to run demos
# Both of these accept command line arguments with the following default
f = testa
demo: Epidemic.class
java Epidemic $(f)
plot: PlotScript Epidemic.class
java Epidemic $(f) > $(f).csv
gnuplot -p -c PlotScript $(f).csv
########
# Make Targets for housekeeping
clean:
rm -f *.class
rm -f *.html
rm -f *.css
rm -f *.js
rm -f *.csv
rm -f package-list
rm -f element-list
rm -rf jquery
rm -rf resources
rm -rf *search-index*.zip
html: $(AllSrc)
javadoc $(AllSrc)
shar: README $(AllSrc) Makefile $(AllTest) PlotScript
shar README $(AllSrc) Makefile $(AllTest) PlotScript > shar
xxxxxxxxxx
cat > testa <<\xxxxxxxxxx
# testa -- little demo of simulator, workers spread disease between homes
population 100; latent 2.0 0 0;
infected 1; asymptomatic 2 0 0;
end 30 days; symptomatic 2 0 90%;
place home 10 0 0.01; bedridden 2 0 90%;
role homebody 60 home;
place work 10 0 0.01 screen antigens 1% symptoms 2%;
role worker 40 compliance 10%
home work (9:00am-5:00pm@weekdays 99%);
xxxxxxxxxx
cat > testb <<\xxxxxxxxxx
# testb -- little demo of simulator, like testa, but everyone works sometimes
# also this demo shows use of default time units and probability
population 100; latent 2.0 0;
infected 1; asymptomatic 2 0;
place home 10 0 0.01; symptomatic 2 0 0.9;
place work 10 0 0.01; bedridden 2 0 0.9;
role everybody 100 home work (9-17@Mon-Fri 0.4);
end 30;
xxxxxxxxxx
cat > testc <<\xxxxxxxxxx
# two planets where everyone goes and visits the moon with brief overlap.
population 100; latent 2.0 0;
infected 1; asymptomatic 3 0;
place earth 100 0 0.001; symptomatic 4 1 0.9;
place moon 100 0 .0001; bedridden 5 2 0.9;
place mars 100 0 0.001;
role human 50 earth moon (10-11.06);
role martian 50 mars moon (11-12);
end 40;
xxxxxxxxxx
cat > testd <<\xxxxxxxxxx
# two planets where everyone takes occasional trips to the moon.
population 100; latent 2.0 0;
infected 1; asymptomatic 3 0;
place earth 100 0 0.001; symptomatic 4 1 90%;
place moon 100 0 .0001; bedridden 5 2 90%;
place mars 100 0 0.001;
role human 50 earth moon (11 am - noon 10%);
role martian 50 mars moon (11 am - noon 10%);
end 40 days;
xxxxxxxxxx
cat > PlotScript <<\xxxxxxxxxx
# PlotScript -- instruct gnuplot on how to plot the data in a csv file
# author Douglas W. Jones
# version May 4, 2021
# use: from the shell,
# gnuplot -p -c PlotScript data.csv
# how to read a CSV file that has data captions in row 1
set datafile separator ',' # Commas separate values
set key autotitle columnhead # First line is column titles
# grab the filename from the command line
file = '"'."@ARG1".'"'
# colors used in plotting
set style line 101 lw 3 lt rgb "#DFDF00" #dark yellow
set style line 102 lw 3 lt rgb "#AFFF00" #green yellow
set style line 103 lw 3 lt rgb "#FF7F00" #orange
set style line 104 lw 3 lt rgb "#FF007F" #purple
set style line 105 lw 3 lt rgb "#DF0000" #dark red
set style line 106 lw 3 lt rgb "#00FF00" #green
set style line 107 lw 3 lt rgb "#000000" #black
# plot the data
plot @file using "time":"uninfected" with lines ls 101, \
@file using "time":"latent" with lines ls 102, \
@file using "time":"asymptomatic" with lines ls 103, \
@file using "time":"symptomatic" with lines ls 104, \
@file using "time":"bedridden" with lines ls 105, \
@file using "time":"recovered" with lines ls 106, \
@file using "time":"dead" with lines ls 107
xxxxxxxxxx