package bsearch.nlogolink;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Queue;
import org.nlogo.agent.Observer;
import org.nlogo.api.AgentException;
import org.nlogo.api.CompilerException;
import org.nlogo.api.LogoException;
import org.nlogo.api.SimpleJobOwner;
import org.nlogo.nvm.Procedure;
import org.nlogo.util.MersenneTwisterFast;
import org.nlogo.headless.HeadlessWorkspace;
public strictfp class ModelRunner {
private HeadlessWorkspace workspace;
private Procedure setupCommands;
private Procedure stepCommands;
private Procedure stopConditionReporter;
private Procedure measureIfReporter = null;
private final boolean recordEveryTick;
private final int maxModelSteps;
// Note: ModelRunner can collect multiple measures, for eventual support for extending to multi-objective optimization.
private LinkedHashMap<String,Procedure> resultReporters = new LinkedHashMap<String,Procedure>();
private boolean runIsDone;
// Until setup() is run for the first time (and the random seed can be set appropriately),
// we'll leave the JobOwners as null.
private SimpleJobOwner mainJobOwner = null; // uses NetLogo world's mainRNG (and thus affects the state of the world)
private SimpleJobOwner extraJobOwner = null; // uses a private RNG, and thus may not affect world state (unless the NetLogo code being run has side effects)
private ModelRunner(String modelFileName, boolean recordEveryTick, int maxModelSteps)
throws LogoException, IOException, CompilerException
{
workspace = Utils.createWorkspace();
workspace.open(modelFileName);
this.recordEveryTick = recordEveryTick;
this.maxModelSteps = maxModelSteps;
}
public void setSetupCommands(String commands) throws CompilerException
{
setupCommands = workspace.compileCommands(commands);
}
public void setStepCommands(String commands) throws CompilerException
{
stepCommands = workspace.compileCommands(commands);
}
public void setStopConditionReporter(String reporter) throws CompilerException
{
if (reporter.trim().length() > 0)
{
stopConditionReporter = workspace.compileReporter(reporter);
}
}
public void addResultReporter(String reporter) throws CompilerException
{
resultReporters.put(reporter, workspace.compileReporter(reporter));
}
public void setMeasureIfReporter(String reporter) throws CompilerException
{
if (reporter.trim().length() > 0)
{
measureIfReporter = workspace.compileReporter(reporter);
}
}
public boolean checkStopCondition() throws NetLogoLinkException
{
if (extraJobOwner == null)
{
throw new IllegalStateException("ModelRunner.setup() must be called before running commands/reporters.");
}
if (stopConditionReporter != null)
{
Object obj = workspace.runCompiledReporter( extraJobOwner, stopConditionReporter);
LogoException ex = workspace.lastLogoException();
if (ex != null)
{
workspace.lastLogoException_$eq(null);
throw new NetLogoLinkException(ex.toString());
}
if (!(obj instanceof Boolean))
{
throw new NetLogoLinkException("The stop condition reporter must report a TRUE/FALSE value. Error occurred because it reported: " + obj);
}
return (Boolean) obj;
}
return false;
}
public void setup(int seed, LinkedHashMap<String,Object> parameterSettings ) throws LogoException, AgentException, NetLogoLinkException
{
workspace.clearAll();
for (String s: parameterSettings.keySet())
{
workspace.world.setObserverVariableByName(s, parameterSettings.get(s));
}
try {
workspace.world.mainRNG.setSeed( seed );
// For evaluating reporters (like measureIfReporter) we want to use a separate RNG
// (which is seeded by a random number that depends on the random seed for this model run,
// so that the results are deterministic/repeatable, but will generate an independent
// stream of random numbers that does not affect the main NetLogo world's RNG.)
// This approach should allow the user to recreate a run by setting RANDOM-SEED XXX
// and running SETUP followed by GO, without worrying about all of the additional conditions
// and reporters affecting the state of the RNG and changing the outcome of the run...
mainJobOwner = new SimpleJobOwner("BehaviorSearch ModelRunner Main", workspace.mainRNG(), Observer.class);
MersenneTwisterFast extraReporterRNG = new MersenneTwisterFast(workspace.mainRNG().clone().nextInt());
extraJobOwner = new SimpleJobOwner("BehaviorSearch ModelRunner Extra", extraReporterRNG, Observer.class);
} catch (Exception ex) {ex.printStackTrace(); }
if (setupCommands != null)
{
workspace.runCompiledCommands(mainJobOwner,setupCommands);
LogoException ex = workspace.lastLogoException();
if (ex != null)
{
workspace.lastLogoException_$eq(null);
throw new NetLogoLinkException(ex.toString());
}
}
runIsDone = checkStopCondition();
}
/** returns true if the run is finished
* @throws NetLogoLinkException **/
public boolean go() throws NetLogoLinkException
{
if (mainJobOwner == null)
{
throw new IllegalStateException("ModelRunner.setup() must be called before running commands/reporters.");
}
if (stepCommands != null)
{
workspace.runCompiledCommands(mainJobOwner, stepCommands );
LogoException ex = workspace.lastLogoException();
if (ex != null)
{
workspace.lastLogoException_$eq(null);
throw new NetLogoLinkException(ex.toString());
}
}
runIsDone = checkStopCondition();
return runIsDone;
}
public LinkedHashMap<String,Object> measureResults() throws NetLogoLinkException
{
LinkedHashMap<String,Object> results = new LinkedHashMap<String,Object>();
for (String key: resultReporters.keySet())
{
results.put(key, measureResultReporter(resultReporters.get(key)));
}
return results;
}
private Double measureResultReporter(Procedure reporter) throws NetLogoLinkException
{
if (extraJobOwner == null)
{
throw new IllegalStateException("ModelRunner.setup() must be called before running commands/reporters.");
}
Object obj = workspace.runCompiledReporter( extraJobOwner, reporter);
LogoException ex = workspace.lastLogoException();
if (ex != null)
{
workspace.lastLogoException_$eq(null);
throw new NetLogoLinkException(ex.toString());
}
if (! (obj instanceof Double))
{
throw new NetLogoLinkException("Result reporters must return numeric results! Invalid reported value was: " + obj );
}
return (Double) obj;
}
private boolean evaluateMeasureIfReporter() throws NetLogoLinkException
{
// If they left the field blank, we'll assume we should measure every time.
if (measureIfReporter == null)
{
return true;
}
Object obj = workspace.runCompiledReporter(extraJobOwner, measureIfReporter).equals(Boolean.TRUE);
LogoException ex = workspace.lastLogoException();
if (ex != null)
{
workspace.lastLogoException_$eq(null);
throw new NetLogoLinkException(ex.toString());
}
if (!(obj instanceof Boolean))
{
throw new NetLogoLinkException("The 'measure if' condition must report a TRUE/FALSE value. Error occurred because it reported: " + obj);
}
return (Boolean) obj;
}
private void conditionallyRecordResults(ModelRunResult results) throws NetLogoLinkException
{
if (extraJobOwner == null)
{
throw new IllegalStateException("ModelRunner.setup() must be called before running commands/reporters.");
}
if (evaluateMeasureIfReporter())
{
for (String key: resultReporters.keySet())
{
results.addResult(key, measureResultReporter(resultReporters.get(key)));
}
}
}
public ModelRunResult doFullRun(RunSetup runSetup) throws ModelRunnerException
{
try {
ModelRunResult results = new ModelRunResult(runSetup.seed);
setup(runSetup.seed, runSetup.parameterSettings);
int steps;
for (steps = 0; steps < maxModelSteps && !runIsDone; steps++)
{
if (recordEveryTick )
{
conditionallyRecordResults(results);
}
go();
}
conditionallyRecordResults(results);
if (results.isEmpty())
{
throw new NetLogoLinkException("No values were measured/collected during this model run! (Model was run for " + steps + " steps.)");
}
return results;
}
catch (Exception ex) {
throw new ModelRunnerException(runSetup, ex);
}
}
public boolean isRunDone()
{
return runIsDone;
}
public void dispose() throws InterruptedException {
workspace.dispose();
}
/** Just used for unit testing - preferable to use the ModelRunner.Factory paradigm */
public static ModelRunner createModelRunnerForTesting (String s, boolean recordEveryTick, int maxModelSteps) throws LogoException, IOException, CompilerException
{
return new ModelRunner(s, recordEveryTick, maxModelSteps);
}
/** Note: This uses the "extra" random number generator, so it
* won't affect the state of the main RNG for the run.
* Still, be careful because some reporters could have side-effects in the model
* (for instance, if the reporter creates some agents
* (even if it kills them before its done, the "who" numbers of agents
* created after that will be different...)
* @param reporter the NetLogo reporter expression to run.
* @return the result of running the reporter
* @throws CompilerException
* @throws NetLogoLinkException
*/
public Object report(String reporter) throws CompilerException, NetLogoLinkException
{
Procedure procReporter = workspace.compileReporter( reporter );
Object obj = workspace.runCompiledReporter( extraJobOwner, procReporter );
LogoException ex = workspace.lastLogoException();
if (ex != null)
{
workspace.lastLogoException_$eq(null);
throw new NetLogoLinkException(ex.toString());
}
return obj;
}
/**
* Note: this method uses the main random-number generator, and thus
* will affect the state of the RNG for future calls.
* @param cmd - the NetLogo command to be run on the current headless workspace.
* @throws CompilerException
* @throws LogoException
*/
public void command(String cmd) throws CompilerException, LogoException
{
workspace.command( cmd );
}
public static class ModelRunnerException extends NetLogoLinkException
{
private static final long serialVersionUID = 1L;
private final RunSetup runSetup;
public ModelRunnerException(RunSetup runSetup, Exception ex) {
super("", ex);
this.runSetup = runSetup;
}
public RunSetup getRunSetup()
{
return runSetup;
}
@Override
public String getMessage()
{
return getCause().toString() + "\n\nModel run configuration was: " + runSetup;
}
}
public static class RunSetup {
final int seed;
final private LinkedHashMap<String,Object> parameterSettings;
public RunSetup(int seed,
LinkedHashMap<String, Object> parameterSettings) {
super();
this.seed = seed;
this.parameterSettings = parameterSettings;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("{RANDOM-SEED: " + seed + ",\n SETTINGS: {");
for (String key : parameterSettings.keySet())
{
sb.append(key + "=" + org.nlogo.api.Dump.logoObject(parameterSettings.get(key), true, false) + ", ");
}
sb.append("}}");
return sb.toString();
}
}
public static class Factory {
private List<ModelRunner> allModelRunners = Collections.synchronizedList(new java.util.LinkedList<ModelRunner>());
private Queue<ModelRunner> unusedModelRunners = new java.util.concurrent.ConcurrentLinkedQueue<ModelRunner>();
private final String modelFileName, setupCommands, stepCommands, stopCondition, metricReporter, measureIfReporter;
private final boolean recordEveryTick;
private final int maxModelSteps;
public Factory(String modelFileName, boolean recordEveryTick, int maxModelSteps,
String setupCommands, String stepCommands, String stopCondition, String metricReporter,
String measureIfReporter)
{
this.modelFileName = modelFileName;
this.setupCommands = setupCommands;
this.stepCommands = stepCommands;
this.stopCondition = stopCondition;
this.metricReporter = metricReporter;
this.measureIfReporter = measureIfReporter;
this.recordEveryTick = recordEveryTick;
this.maxModelSteps = maxModelSteps;
}
/** Either creates a new one, or recycles a ModelRunner that has been released again into the pool */
public ModelRunner acquireModelRunner() throws NetLogoLinkException
{
ModelRunner runner = unusedModelRunners.poll();
if (runner != null)
{
return runner;
}
else
{
return newModelRunner();
}
}
public void releaseModelRunner(ModelRunner runner)
{
unusedModelRunners.add(runner);
}
private ModelRunner newModelRunner() throws NetLogoLinkException
{
ModelRunner runner;
try {
runner = new ModelRunner(modelFileName, recordEveryTick, maxModelSteps);
} catch (LogoException e) {
e.printStackTrace();
throw new NetLogoLinkException("Error opening NetLogo model. NetLogo sent back this error message: \"" + e.getMessage() + "\"");
} catch (IOException e) {
e.printStackTrace();
throw new NetLogoLinkException("I/O error when loading NetLogo model ( " + modelFileName + " ). Error message: \"" + e.getMessage() + "\"");
} catch (CompilerException e) {
e.printStackTrace();
throw new NetLogoLinkException("Error compiling NetLogo model ( " + modelFileName + " ). NetLogo's error message: \"" + e.getMessage() + "\"");
}
try {
runner.setSetupCommands(setupCommands);
} catch (CompilerException e) {
e.printStackTrace();
throw new NetLogoLinkException("Error compiling the model's setup commands : " +setupCommands.toUpperCase() + " \n NetLogo's error message: \"" + e.getMessage() + "\"");
}
try {
runner.setStepCommands(stepCommands);
} catch (CompilerException e) {
e.printStackTrace();
throw new NetLogoLinkException("Error compiling the model's step commands : " +stepCommands.toUpperCase() + " \n NetLogo's error message: \"" + e.getMessage() + "\"");
}
try {
runner.setStopConditionReporter(stopCondition );
} catch (CompilerException e) {
e.printStackTrace();
throw new NetLogoLinkException("Error compiling the model's stop condition: " +stopCondition.toUpperCase() + " \n NetLogo's error message: \"" + e.getMessage() + "\"");
}
try {
runner.setMeasureIfReporter(measureIfReporter);
} catch (CompilerException e) {
e.printStackTrace();
throw new NetLogoLinkException("Error compiling the model's measure-if condition: " +measureIfReporter.toUpperCase() + " \n NetLogo's error message: \"" + e.getMessage() + "\"");
}
try {
runner.addResultReporter(metricReporter);
} catch (CompilerException e) {
e.printStackTrace();
throw new NetLogoLinkException("Error compiling the model's fitness metric: " +metricReporter.toUpperCase() + " \n NetLogo's error message: \"" + e.getMessage() + "\"");
}
allModelRunners.add(runner);
return runner;
}
/** This method should only be called after you are done using this Factory, and all the ModelRunners created by it.
* It disposes the ModelRunners (and their corresponding NetLogo workspaces), and attempting to use them after this
* will result in errors.
* */
public void disposeAllRunners() throws InterruptedException {
synchronized(allModelRunners)
{
for (ModelRunner runner : allModelRunners) {
runner.dispose();
}
allModelRunners.clear();
}
}
}
}