sim.engine.SimState Maven / Gradle / Ivy
Show all versions of mason Show documentation
/*
Copyright 2006 by Sean Luke and George Mason University
Licensed under the Academic Free License version 3.0
See the file "LICENSE" for more information
*/
package sim.engine;
import ec.util.*;
import java.util.*;
import java.io.*;
import java.util.zip.*;
import java.text.*;
/** SimState represents the simulation proper. Your simulations generally will contain one top-level object which subclasses from SimState.
A SimState contains the random number generator and the simulator's schedule. You should not change the schedule to another Schedule object.
When a simulation is begun, SimState's start() method is called. Then the schedule is stepped some N times. Last, the SimState's finish() method is called, and the simulation is over.
SimStates are serializable; if you wish to be able to checkpoint your simulation and read from checkpoints, you should endeavor to make all objects in the simulation serializable as well. Prior to serializing to a checkpoint, preCheckpoint() is called. Then after serialization, postCheckpoint() is called. When a SimState is loaded from a checkpoint, awakeFromCheckpoint() is called to give you a chance to make any adjustments. SimState also implements several methods which call these methods and then serialize the SimState to files and to streams.
SimState also maintains a private registry of AsynchronousSteppable objects, and handles pausing and resuming
them during the checkpointing process, and killing them during finish() in case they had not completed yet.
If you override any of the methods foo() in SimState, should remember to always call super.foo() for any such method foo().
*/
public class SimState implements java.io.Serializable
{
private static final long serialVersionUID = 1;
/** The SimState's random number generator */
public MersenneTwisterFast random;
/** SimState's schedule */
public Schedule schedule;
// All registered AsynchronousSteppables
HashSet asynchronous = new HashSet();
// Lock for accessing the HashSet
Object asynchronousLock = new boolean[1]; // an array is a unique, serializable object
// Are we cleaning house and replacing the HashSet?
boolean cleaningAsynchronous = false;
SimState(long seed, MersenneTwisterFast random, Schedule schedule)
{
this.random = random;
this.schedule = schedule;
this.seed = (int) seed; // force to 32 bits since that's what MTF will be using anyway
}
/** Creates a SimState with a new random number generator initialized to the given seed,
plus a new, empty schedule. */
public SimState(long seed)
{
this(seed, new MersenneTwisterFast(seed), new Schedule());
}
/** Creates a SimState with the given random number generator and schedule, and
sets the seed to a bogus value (0). This should only be used by SimState
subclasses which need to use an existing random number generator and schedule.
*/
protected SimState(MersenneTwisterFast random, Schedule schedule)
{
this(0, random, schedule); // 0 is a bogus value. In fact, MT can't have 0 as its seed value.
}
/** Creates a SimState with the schedule, creating a new random number generator.
This should only be used by SimState subclasses which need
to use an existing schedule.
*/
protected SimState(long seed, Schedule schedule)
{
this(seed, new MersenneTwisterFast(seed), schedule);
}
/** Creates a SimState with a new schedule, the provided random number generator,
and a bogus seed (0). This should only be used by SimState subclasses which need
to use an existing random number generator.
*/
protected SimState(MersenneTwisterFast random)
{
this(0, random, new Schedule()); // 0 is a bogus value. In fact, MT can't have 0 as its seed value.
}
public void setSeed(long seed)
{
seed = (int) seed; // force to 32 bits since that's what MTF will be using anyway
random = new MersenneTwisterFast(seed);
this.seed = seed;
}
/** Primes the generator. Mersenne Twister seeds its first 624 numbers using a basic
linear congruential generator; thereafter it uses the MersenneTwister algorithm to
build new seeds. Those first 624 numbers are generally just fine, but to be extra
safe, you can prime the generator by calling nextInt() on it some (N>1) * 624 times.
This method does exactly that, presently with N=2 ( + 1 ). */
public static MersenneTwisterFast primeGenerator(MersenneTwisterFast generator)
{
// 624 = MersenneTwisterFast.N which is private duh
for(int i = 0; i < 624 * 2 + 1; i++)
generator.nextInt();
return generator;
}
/** Called immediately prior to starting the simulation, or in-between
simulation runs. This gives you a chance to set up initially,
or reset from the last simulation run. The default version simply
replaces the Schedule with a completely new one. */
public void start()
{
// prime the generator so it's got better statistial properties
random = primeGenerator(random);
// just in case
cleanupAsynchronous();
// reset schedule
schedule.reset();
}
/** Called either at the proper or a premature end to the simulation.
If the user quits the program, this function may not be called. It is
possible for this method to be called multiple times. If you need to
check for this possibility, the easiest way is to set a flag in start()
and clear it in the first finish(). */
public void finish()
{
kill(); // cleans up asynchronous and resets the schedule, a good ending
}
/** A Steppable on the schedule can call this method to cancel the simulation.
All existing AsynchronousSteppables are stopped, and then the schedule is
reset. AsynchronousSteppables, ParallelSequences,
and non-main threads should not call this method directly -- it will deadlock.
Instead, they may kill the simulation by scheduling a Steppable
for the next timestep which calls state.kill(). */
public void kill()
{
cleanupAsynchronous();
schedule.clear();
schedule.seal();
}
/** Registers an AsynchronousSteppable to get its pause() method called prior to checkpointing,
its resume() method to be called after checkpointing or recovery, and its stop()
method to be called at finish() time. The purpose of the addToCleanup() method is to provide
the simulation with a way of stopping existing threads which the user has created in the background.
An AsynchronousSteppable cannot be added multiple times
to the same registry -- if it's there it's there. Returns false if the AsynchronousSteppable could
not be added, either because the simulation is stopped or in the process of finish()ing.
*/
public boolean addToAsynchronousRegistry(AsynchronousSteppable stop)
{
if (stop==null) return false;
synchronized(asynchronousLock)
{
if (cleaningAsynchronous) return false;
asynchronous.add(stop);
return true;
}
}
/**
Unregisters an AsynchronousSteppable from the asynchronous registry.
*/
public void removeFromAsynchronousRegistry(AsynchronousSteppable stop)
{
if (stop==null) return;
synchronized(asynchronousLock)
{
if (!cleaningAsynchronous)
asynchronous.remove(stop);
}
}
/** Returns all the AsynchronousSteppable items presently in the registry. The returned array is not used internally -- you are free to modify it. */
public AsynchronousSteppable[] asynchronousRegistry()
{
synchronized(asynchronousLock)
{
AsynchronousSteppable[] b = new AsynchronousSteppable[asynchronous.size()];
int x = 0;
Iterator i = asynchronous.iterator();
while(i.hasNext())
b[x++] = (AsynchronousSteppable)(i.next());
return b;
}
}
/*
Calls all the registered Asynchronnous. During this period, any methods which attempt to
register things for the schedule will simply be ignored.
*/
// perhaps use a LinkedHashSet instead of a HashSet?
void cleanupAsynchronous()
{
AsynchronousSteppable[] b = null;
synchronized(asynchronousLock)
{
b = asynchronousRegistry();
cleaningAsynchronous = true;
}
final int len = b.length;
for(int x=0;x(). */
public static void doLoop(final Class c, String[] args)
{
doLoop(new MakesSimState()
{
public SimState newInstance(long seed, String[] args)
{
try
{
return (SimState)(c.getConstructor(new Class[] { Long.TYPE }).newInstance(new Object[] { Long.valueOf(seed) } ));
}
catch (Exception e)
{
throw new RuntimeException("Exception occurred while trying to construct the simulation " + c + "\n" + e);
}
}
public Class simulationClass() { return c; }
}, args);
}
/** A convenient top-level loop for the simulation command-line. Takes a MakesSimState which is
responsible for providing a SimState to run the simulation on, plus the application's argument
list in args. This loop is capable of:
- Repeating a job multiple times
*/
public static void doLoop(final MakesSimState generator, final String[] args)
{
// print help?
if (keyExists("-help", args))
{
System.err.println(
"Format: java " + generator.simulationClass().getName() + " \\\n" +
" [-help] [-repeat R] [-parallel P] [-seed S] \\\n" +
" [-until U] [-for F] [-time T] [-docheckpoint D] \\\n" +
" [-checkpoint C] [-quiet] \n\n" +
"-help Shows this message and exits.\n\n" +
"-repeat R Long value > 0: Runs R jobs. Unless overridden by a\n" +
" checkpoint recovery (see -checkpoint), the random seed for\n" +
" each job is the provided -seed plus the job# (starting at 0).\n" +
" Default: runs once only: job number is 0.\n\n" +
"-parallel P Long value > 0: Runs P separate batches of jobs in parallel,\n" +
" each one containing R jobs (as specified by -repeat). Each\n" +
" batch has its own independent set of checkpoint files. Job\n" +
" numbers are 0, P, P*2, ... for the first batch, then 1, P+1,\n" +
" P*2+1, ... for the second batch, then 2, P+2, P*2+2, ... for\n" +
" the third batch, and so on. -parallel may not be used in\n" +
" combination with -checkpoint.\n" +
" Default: one batch only (no parallelism).\n\n" +
"-seed S Long value not 0: the random number generator seed, unless \n" +
" overridden by a checkpoint recovery (see -checkpoint).\n" +
" Default: the system time in milliseconds.\n\n" +
"-until U Double value >= 0: the simulation must stop when the\n" +
" simulation time U has been reached or exceeded.\n" +
" If -for is also included, the simulation terminates when\n" +
" either of them is completed.\n" +
" Default: don't stop.\n\n" +
"-for N Long value >= 0: the simulation must stop when N\n" +
" simulation steps have transpired. If -until is also\n" +
" included, the simulation terminates when either of them is\n" +
" completed.\n" +
" Default: don't stop.\n\n" +
"-time T Long value >= 0: print a timestamp every T simulation steps.\n" +
" If 0, nothing is printed.\n" +
" Default: auto-chooses number of steps based on how many\n" +
" appear to fit in one second of wall clock time. Rounds to\n" +
" one of 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, etc.\n\n" +
"-docheckpoint D Long value > 0: checkpoint every D simulation steps.\n" +
" Default: never.\n" +
" Checkpoint files named\n"+
" .." +
generator.simulationClass().getName().substring(generator.simulationClass().getName().lastIndexOf(".") + 1) +
".checkpoint\n\n" +
"-checkpoint C String: loads the simulation from file C, recovering the job\n" +
" number and the seed. If the checkpointed simulation was begun\n" +
" on the command line but was passed through the GUI for a while\n" +
" (even multiply restarted in the GUI) and then recheckpointed,\n" +
" then the seed and job numbers will be the same as when they\n" +
" were last on the command line. If the checkpointed simulation\n" +
" was begun on the GUI, then the seed will not be recovered and\n"+
" job will be set to 0. Further jobs and seeds are incremented\n" +
" from the recovered job and seed.\n" +
" Default: starts a new simulation rather than loading one, at\n" +
" job 0 and with the seed given in -seed.\n\n" +
"-quiet Does not print messages except for errors and warnings.\n" +
" This option implies -time 0.\n" +
" Default: prints all messages.\n"
);
System.exit(0);
}
final boolean quiet = keyExists("-quiet", args);
java.text.NumberFormat n = java.text.NumberFormat.getInstance();
n.setMinimumFractionDigits(0);
if (!quiet) System.err.println("MASON Version " + n.format(version()) + ". For further options, try adding ' -help' at end.");
// figure the checkpoint modulo
double _until = Double.POSITIVE_INFINITY;
String until_s = argumentForKey("-until", args);
if (until_s != null)
try
{
_until = Double.parseDouble(until_s);
if (_until < 0.0) throw new Exception();
}
catch (Exception e)
{
throw new RuntimeException("Invalid 'until' value: " + until_s + ", must be a positive real value");
}
final double until = _until; // stupid Java
long _seed = System.currentTimeMillis();
String seed_s = argumentForKey("-seed", args);
if (seed_s != null)
try
{
_seed = Long.parseLong(seed_s);
if (_seed == 0) throw new Exception();
}
catch (Exception e)
{
throw new RuntimeException("Invalid 'seed' value: " + seed_s + ", must be a non-zero integer, or nonexistent to seed by clock time");
}
final long seed_init = _seed; // grrrr
long __for = -1;
String for_s = argumentForKey("-for", args);
if (for_s != null)
try
{
__for = Long.parseLong(for_s);
if (__for < 0) throw new Exception();
}
catch (Exception e)
{
throw new RuntimeException("Invalid 'for' value: " + for_s + ", must be an integer >= 0");
}
final long _for = __for; // argh
long _time = -1;
String time_s = argumentForKey("-time", args);
if (time_s != null)
try
{
_time = Long.parseLong(time_s);
if (_time < 0) throw new Exception();
}
catch (Exception e)
{
throw new RuntimeException("Invalid 'time' value: " + time_s + ", must be a positive integer");
}
final long time_init = _time; //blah
long _cmod = 0;
String cmod_s = argumentForKey("-docheckpoint", args);
if (cmod_s != null)
try
{
_cmod = Long.parseLong(cmod_s);
if (_cmod <= 0) throw new Exception();
}
catch (Exception e)
{
throw new RuntimeException("Invalid checkpoint modulo: " + cmod_s + ", must be a positive integer");
}
final long cmod = _cmod;
long _repeat = 1;
String repeat_s = argumentForKey("-repeat", args);
if (repeat_s != null)
try
{
_repeat = Long.parseLong(repeat_s);
if (_repeat <= 0) throw new Exception();
}
catch (Exception e)
{
throw new RuntimeException("Invalid repeat value: " + repeat_s + ", must be a positive integer");
}
final long repeat = _repeat;
int _parallel = 1;
String parallel_s = argumentForKey("-parallel", args);
if (parallel_s != null)
try
{
_parallel = Integer.parseInt(parallel_s);
if (_parallel <= 0) throw new Exception();
}
catch (Exception e)
{
throw new RuntimeException("Invalid parallel value: " + parallel_s + ", must be a positive integer");
}
final int parallel = _parallel;
// check for parallelism with checkpoints
final String checkpointFile = argumentForKey("-checkpoint", args);
if (parallel > 1 && checkpointFile != null)
{
System.err.println("Cannot load from checkpoint and run in parallel at the same time. Sorry.");
System.exit(1);
}
// okay, now we actually get down to brass tacks
// Note that this is kind of a mess -- we create a new thread even if we have
// a single thread to run. Furthermore, note that we load from checkpoint the
// initial job within a thread. This will likely change the job number, which
// could conflict with other job numbers in other threads, so this is only permitted
// if there is a SINGLE thread. We already checked for that situation above.
Thread[] threads = new Thread[parallel];
for(int _thread = 0; _thread < parallel; _thread++)
{
final int thread = _thread; // stupid Java
threads[thread] = new Thread(new Runnable()
{
public void run()
{
long time = time_init - 1;
long job = thread * repeat;
long seed = seed_init + job; // initially anyway
for(long rep = 0 ; rep < repeat; rep++)
{
SimState state = null;
// start from checkpoint? Note this will only happen if there is only ONE thread, so it's okay to change the job number here
if (rep == 0 && checkpointFile!=null) // only job 0 loads from checkpoint
{
if (!quiet) printlnSynchronized("Loading from checkpoint " + checkpointFile);
state = SimState.readFromCheckpoint(new File(checkpointFile));
if (state == null) // there was an error -- it got printed out to the screen, so just quit
System.exit(1);
else if (state.getClass() != generator.simulationClass()) // uh oh, wrong simulation stored in the file!
{
printlnSynchronized("Checkpoint contains some other simulation: " + state + ", should have been of class " + generator.simulationClass());
System.exit(1);
}
state.nameThread();
job = state.job();
if (state.seed() != 0) // likely good seed from the command line earlier
{
seed = state.seed();
if (!quiet) printlnSynchronized("Recovered job: " + state.job() + " Seed: " + state.seed());
}
else if (!quiet) printlnSynchronized("Renamed job: " + state.job() + " (unknown seed)");
}
// ...or should we start fresh?
if (state==null) // no checkpoint file requested
{
state = generator.newInstance(seed,args);
state.nameThread();
state.job = job;
state.seed = seed;
if (!quiet) printlnSynchronized("Job: " + state.job() + " Seed: " + state.seed());
state.start();
}
NumberFormat rateFormat = NumberFormat.getInstance();
rateFormat.setMaximumFractionDigits(5);
rateFormat.setMinimumIntegerDigits(1);
// do the loop
boolean retval = false;
long steps = 0;
long clock;
long oldClock = System.currentTimeMillis();
Schedule schedule = state.schedule;
long firstSteps = schedule.getSteps();
while((_for == -1 || steps < _for) && schedule.getTime() <= until)
{
if (!schedule.step(state))
{
retval=true;
break;
}
steps = schedule.getSteps();
if (time < 0) // don't know how long to make the time yet
{
if (System.currentTimeMillis() - oldClock > 1000L) // time to set the time
{
time = figureTime(steps - firstSteps);
}
}
if (time > 0 && steps % time == 0)
{
clock = System.currentTimeMillis();
if (!quiet) printlnSynchronized("Job " + job + ": " + "Steps: " + steps + " Time: " + state.schedule.getTimestamp("At Start", "Done") + " Rate: " + rateFormat.format((1000.0 *(steps - firstSteps)) / (clock - oldClock)));
firstSteps = steps;
oldClock = clock;
}
if (cmod > 0 && steps % cmod == 0)
{
String s = "" + steps + "." + state.job() + "." + state.getClass().getName().substring(state.getClass().getName().lastIndexOf(".") + 1) + ".checkpoint";
if (!quiet) printlnSynchronized("Job " + job + ": " + "Checkpointing to file: " + s);
state.writeToCheckpoint(new File(s));
}
}
state.finish();
if (retval)
{
if (!quiet) printlnSynchronized("Job " + job + ": " + "Exhausted " + state.job );
}
else
{
if (!quiet) printlnSynchronized("Job " + job + ": " + "Quit " + state.job);
}
job++;
seed++;
}
}
});
threads[thread].start();
}
for(int thread = 0; thread < parallel; thread++)
{
try { threads[thread].join(); } catch (InterruptedException ex) { } // do nothing
}
System.exit(0);
}
static Object printLock = new Object[0];
public static void printlnSynchronized(String val)
{
synchronized(printLock) { System.err.println(val); }
}
/** Names the current thread an appropriate name given the SimState */
public void nameThread()
{
// name my thread for the profiler
Thread.currentThread().setName("MASON Model: " + this.getClass());
}
/** Returns MASON's Version */
public static double version()
{
return 18.0;
}
// compute how much time per step
// it's possible this could go into an infinite loop if time is gigantic
// but that's not likely. Otherwise takes O(lg(time)) time, which is
// reasonable for a long
static long figureTime(long time)
{
long n = 1;
while(true)
{
if (n >= time) return n;
if ((n*10)/4 >= time) return (n*10)/4;
if ((n*10)/2 >= time) return (n*10)/2;
n = n*10;
}
}
}