# 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: *

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: *

* @param args the command line arguments */ public static void main( String[] args ) { String filename = null; boolean dump = false; // command line option -d sets true boolean headline = true; // command line option -h sets false for (String arg: args) { if (arg.isEmpty()) { Error.fatal( "empty command line argument" ); } else if (arg.charAt(0) == '-') { // decode a command line option String option = arg.substring(1); // guaranteed non null! if (option.contentEquals( "d" )) { // -d option dump = true; } else if (option.contentEquals( "h" )) { // -h option headline = false; } else if ( option.contentEquals( "?" ) // -? option || option.contentEquals( "help" )) { // -help Error.fatal( "command line options: [-d][-h] filename" + System.lineSeparator() + "\t-d, optional, dump population before simulation" + System.lineSeparator() + "\t-h, optional, suppress heading line in CSV output" + System.lineSeparator() + "\tfilename, the name of the file holding the model" ); } else { Error.fatal( "unknown command line option: " + arg ); } } else { // try to treat it as a file name if (filename != null) { Error.fatal( "extra file name on command line: " + arg ); } else { filename = arg; } } } if (filename == null) Error.fatal( "no file name on command line" ); try { buildModel( new MyScanner( new File( filename ) ) ); if (dump) Person.printAll(); // optional dump of model Person.startReporting( headline ); // start the results report Simulator.run(); // and simulate } catch ( FileNotFoundException e ) { Error.fatal( "could not open file: " + args[0] ); } } } xxxxxxxxxx cat > Role.java <<\xxxxxxxxxx // Role.java import java.util.LinkedList; import java.util.regex.Pattern; /** People in the simulated community each have a role. *

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 placeKinds = new LinkedList<>(); private final double fraction; // fraction of the population in this role private int number; // number of people in role, would be final // static variables used for summary of all roles private static double sum = 0.0F; // sum of all the fractions private static final LinkedList allRoles = new LinkedList(); // patterns used to scan for keywords private static final Pattern complyPat = Pattern.compile( "compliance|" ); /** Scan the description of a new role from some input stream *

The stream must contain the following items, in order: *

Each place, actually the name of a 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. *

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 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 LinkedList people = new LinkedList<>(); // static variables used for categories of places private static final MyRandom rand = MyRandom.stream(); private static final LinkedList allPlaceKinds = new LinkedList<>(); // patterns needed in scanning place descriptions private static final Pattern screenPat = Pattern.compile( "screen|" ); private static final Pattern antigensPat = Pattern.compile( "antigens|" ); private static final Pattern symptomsPat = Pattern.compile( "symptoms|" ); /** Scan a new place category from an input stream. *

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 Place. * @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. *

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 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. *

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 places = new LinkedList<>(); // instance variables that change as simulation progressses private DiseaseStates diseaseState = DiseaseStates.uninfected; private Place location; // initialized by emplace private double hesitance; // 1.0 never leave home, 0.0 always goes private Simulator.Event infectMe = null; // pending infection event // static variables used for all people private static LinkedList allPeople = new LinkedList(); private static MyRandom rand = MyRandom.stream; /** Construct a new person to perform some role. *

This constructor deliberately defers putting people in any places. * For each constructed person 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. *

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 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? *

A person is defined as being symptomatic if they are in any disease * state between 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. *

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 latent if they are currently. *

During simulation, this should only be called from * 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. *

This is a schedulable event service routine. *

This may only be called on a person in with a 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. *

This is a schedulable event service routine. *

This may only be called on a person in with an * 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. *

This is a schedulable event service routine *

This may only be called on a person in with a * 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. *

This is a schedulable event service routine. *

This may be called on a person in any disease state * and leaves the person 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 *

This is a schedulable event service routine. *

This may only be called only on a person who is already * 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 *

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 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. *

The report is in CSV format sent to 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. *

This is a schedulable event service routine. *

Each report is a CSV line sent to 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. *

Every place is an instance of some 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 LinkedList occupants = new LinkedList<>(); // access to pseudo-random numbers? private static final MyRandom rand = MyRandom.stream(); /** Construct a new place. *

Places are characterized by a population, and characteristics * inherited from their PlaceKind. * 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. *

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 (time-time[@day][prob]) *

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 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. *

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: *

* 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. *

Recovery probabilities are a fixed property of the rule. * Uses 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. *

Disease duration is determined by a log-normal distribution * specified with the rule. * Uses 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. *

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: *

* Note that 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. *

All times are stored as 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. *

time can be given as: *

* @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 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. *

probability can be given as either: *

* @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. *

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 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. *

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 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. *

calls to 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. *

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: *

     *  (double t)-> someMethod( t, otherParameters )
     *  
*/ public static interface Action { void trigger(); } /** Event is the parent of real events scheduled in the simulator. *

Because class 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 eventSet = new PriorityQueue<>( ( RealEvent e1, RealEvent e2 )-> Double.compare( e1.time, e2.time ) ); /** Schedule an event to occur at a later time. *

Typically, users schedule events using a lambda expression for * the action to be take after the fiven delay, for example: *

     *    Simulator.schedule( later, ()-> whatToDo( stuff ) );
     *  
*

This schedules an event to occur * at Simulator.time()+later; At that simulated time, * whatToDo(stuff) will be called. *

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, 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 *

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 warn. * Users can change the default limit of 15. * @see warn */ public static int maxWarnings = 15; /** Report a fatal error. *

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 * 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. *

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 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. *

To pass a specific message, create a subclass of 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"
     *  
*

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 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. *

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 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. *

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 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. *

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 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. *

If the next input to the scanner is a literal, scan over it. * Otherwise, report that the literal was not present. * This differs from 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. *

For technical reasons, the pattern used to specify the literal must * match the empty string as well. So, to match 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. *

This uses 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