EMAworkbench icon indicating copy to clipboard operation
EMAworkbench copied to clipboard

Connector for Repast Symphony

Open stefaneidelloth opened this issue 5 years ago • 11 comments

On the ema homepage it is stated

Future plans include support for Netlogo and Repast http://simulation.tbm.tudelft.nl/ema-workbench/contents.html

While there is already some connector for Netlogo...

  • https://github.com/quaquel/EMAworkbench/tree/master/ema_workbench/connectors
  • https://github.com/quaquel/pyNetLogo
  • http://jasss.soc.surrey.ac.uk/21/2/4.html

... I could not find information about a connector for Repast.

=>Is somebody already working on that? (Tried and failed for some reason?)

Some Links about controlling Repast:

  • https://sourceforge.net/p/repast/mailman/message/30465311/
  • https://sourceforge.net/p/repast/mailman/message/19672480/
  • https://repast.github.io/docs/api/repast_simphony/index.html

stefaneidelloth avatar Mar 27 '19 15:03 stefaneidelloth

I had hoped to do it quickly, but other stuff keeps me busy. I know it probably is an hour of work, because the remote api of repast is quite straight forward. Through jpype, it should be quite simple.

Happy to help if you have time. All I need is some simple pieces of python code:

  • loading a repast model
  • setting variables
  • "pressing run"
  • getting variables

with this, a proof is easy to put together

quaquel avatar Mar 28 '19 12:03 quaquel

If you would get that done in an hour I would be very impressed. :) I currently try to get jpype running, which needs C++ build tools to be installed. I'll play around with jpype and repast and will let you know how far I get.

stefaneidelloth avatar Mar 29 '19 10:03 stefaneidelloth

if you have conda installing jpype is easy: https://jpype.readthedocs.io/en/latest/install.html

For reference, I built the first prototype of pyNetLogo on the train in 2 hours without much prior jpype knowledge and no netlogo knowledge. It helps that I learned programming in Java.

quaquel avatar Mar 29 '19 10:03 quaquel

Repast Symphony comes with a view examples. I used the JZombies_Demo from https://github.com/Repast/repast.simphony.models/tree/master/JZombies_Demo

The model consists mainly of the *.class files that are located in the bin folder after compiling the Demo:

Human.class JZombiesBuilder.class Zombie.class

and configuration files that are located in the folder JZombies_Demo.rs, e.g. parameters.xml

In order to run the model, the Repast Symphony jar files are required, which are located in the lib folder, e.g. repast.simphony.runtime.jar

From the Windows command line the Repast example model can be batch executed with

D:/EclipseJava/app/jdk/bin/java.exe" -cp ".;./lib/*;./bin/." repast.simphony.batch.BatchMain -params ./batch/batch_params.xml -interactive ./JZombies_Demo.rs

Some equivalent jpype code is:

import jpype
import os

repastPath = '.'

libPath = repastPath + '/lib'
libJarPaths = str.join(';', [libPath +  '/' + name for name in os.listdir(libPath)])
classPath = repastPath + '/.;' + libJarPaths + ';' + repastPath + '/bin/.'

batch_params_xml_file_path = repastPath + '/batch/batch_params.xml'

repast_config_path = repastPath + '/JZombies_Demo.rs'

jvmPath = 'D:/EclipseJava/App/jdk/bin/server/jvm.dll'  #jpype.getDefaultJVMPath()

jpype.startJVM(jvmPath, '-ea', '-Djava.class.path=' + classPath )

link = jpype.JClass('repast.simphony.batch.BatchMain')

link.main(['-params', batch_params_xml_file_path, '-interactive', repast_config_path])

jpype.shutdownJVM()

Still need to find out how to set variable values using the API instead of loading them from .xml files and also how to get output using the API: https://github.com/Repast/repast.simphony/issues/18

stefaneidelloth avatar Mar 29 '19 14:03 stefaneidelloth

you are using the batch functionality here. In many ways, the workbench is a replacement for this with more flexibility, so I would probably not look at doing at that way. Do you have access to the source code that is used by the batch run system?

quaquel avatar Mar 29 '19 18:03 quaquel

Fully agree. Just wanted to document what I got so far. (In case that single/subsequent execution through the repast API should be too slow, above strategy might be a fallback.) For now the InstanceRunner class seems to be the way to go and I'll have a closer look: https://github.com/Repast/repast.simphony/issues/18

stefaneidelloth avatar Apr 01 '19 06:04 stefaneidelloth

@quaquel I managed to create the first draft of some Java Code to remotely control a Repast model. I needed to adapt the repast code / create some new Java classes. Could we put that additional Java source code in the repository of the ema workbench? That would allow the jpype part in python to be more simple.

If you find some time (?), please feel free to improve my first draft, so that the connector can be used not only for TimeSeriesOutput but also for ArrayOutcome and Constraint ect.

https://github.com/Repast/repast.simphony/issues/18

stefaneidelloth avatar Apr 03 '19 15:04 stefaneidelloth

Here is a draft for the Repast connector that works for me. Please feel free to adapt it to your needs and include it in the official release if you want.

RepastRunner.java
package repast.simphony.batch;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import repast.simphony.scenario.ScenarioLoadException;

public class RepastRunner {

	public static void main(String[] args) throws IOException {

		var projectPath = ".";

		var instanceId = "myInstanceId";

		var inputs = new HashMap<String, Object>();
		inputs.put("zombie_count", 10);
		inputs.put("human_count", 20);

		var outcomes = Arrays.asList( //
				"Agent Counts.run1", //
				"Agent Counts.tick", //
				"Agent Counts.Human Count", //
				"Agent Counts.Zombie Count" //
		);

		var result = run(projectPath, instanceId, inputs, outcomes);

		System.out.println(result);

	}

	public static Map<String, List<Double>> run(String repastHomePath, String instanceId, Map<String, Object> inputs,
			List<String> outcomes) throws IOException {

		var parameterFilePath = repastHomePath + "/batch/batch_params.xml";

		var scenarioFolderPath = determineScenarioFolderPath(repastHomePath);

		System.out.println("Scenario folder path: '" + scenarioFolderPath + "'");

		var parameterString = "1\t" //
				+ inputs.entrySet() //
						.stream() //
						.map(entry -> entry.getKey() + "\t" + entry.getValue()) //
						.collect(Collectors.joining(","));

		// System.out.println("Parameter string: '" + parameterString + "'");

		var instanceRunner = new EmaInstanceRunner();

		try {
			instanceRunner.configure(new String[] { //
					"-pxml=" + parameterFilePath, //
					"-scenario=" + scenarioFolderPath, //
					"-id=" + instanceId, //
					parameterString });
		} catch (ScenarioLoadException exception) {
			var message = "Could not configure InstanceRunner with scenario folder '" + scenarioFolderPath + "' and"
					+ "parameter string '" + parameterString + "'.";
			throw new IllegalStateException(message, exception);
		}

		Map<String, List<Double>> result = null;
		try {
			result = instanceRunner.run(outcomes);
		} catch (ScenarioLoadException exception) {
			var message = "Could not run InstanceRunner with scenario folder '" + scenarioFolderPath + "' and"
					+ "parameter string '" + parameterString + "'.";
			throw new IllegalStateException(message, exception);
		}

		return result;
	}

	public static void killWorkspace() {

	}

	private static String determineScenarioFolderPath(String repastHomePath) {

		var directory = new File(repastHomePath);
		var scenarioFolderPathList = directory.list(new FilenameFilter() {
			@Override
			public boolean accept(File current, String name) {

				var currentElement = new File(current, name);
				if (!currentElement.isDirectory()) {
					return false;
				}
				;

				return name.endsWith(".rs");
			}
		});

		return repastHomePath + "/"+ scenarioFolderPathList[0];
	}

}

EmaInstanceRunner.java
package repast.simphony.batch;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

import repast.simphony.batch.BatchConstants;
import repast.simphony.batch.RunningStatus;
import repast.simphony.batch.parameter.ParameterLineParser;
import repast.simphony.data2.DataConstants;
import repast.simphony.data2.DataSetRegistry;
import repast.simphony.data2.DataSink;
import repast.simphony.engine.environment.ControllerAction;
import repast.simphony.engine.environment.RunState;
import repast.simphony.parameter.Parameters;
import repast.simphony.scenario.ScenarioLoadException;
import simphony.util.messages.MessageCenter;
import simphony.util.messages.MessageEvent;
import simphony.util.messages.MessageEventListener;

/**
 * Runs a single instance of a simphony model in a batch run. This expects to be
 * passed the following arguments:
 * <ol>
 * <li>-pxml <parameter xml file>
 * <li>-scenario <scenario directory>
 * <li>-id <instance id>
 * <li>optional -pinput <input file> if the parameter input is in a file.
 * </ul>
 * if no -pinput then last arg is expected to be a string in unrolled parameter
 * format. A parameter line hasthe format R\tP1\tV1,P2\tV2,P3\tV3,... R is the
 * run number followed by a tab. P* and V* is a parameter name and value pair
 * which are separated from each other by a tab and from other PV pairs by a
 * comma delimeter.
 * <p>
 * 
 * The InstanceRunner will feed each line from the file / command line to the
 * model and run the model for that parameter combination. When all the lines
 * have been processed the batch run is finished. If there are warnings or
 * errors produced during the run then those will be written to a WARN or
 * FAILURE file in the working directory. If there is an error, no more lines
 * will be read and the InstanceRunner will stop.
 * 
 * original author of InstanceRunner: Nick Collier
 * modified by Stefan Eidelloth
 */
public class EmaInstanceRunner {

	private static MessageCenter msg = MessageCenter.getMessageCenter(EmaInstanceRunner.class);
	
	private static final String PXML = "pxml";
	private static final String ID = "id";
	private static final String SCENARIO = "scenario";
	private static final String PINPUT = "pinput";

	private String input;
	private boolean isFileInput = false;

	private ParameterLineParser lineParser;
	private EmaOneRunBatchRunner runner;
	private String id;
	private RunningStatus status = RunningStatus.OK;

	private Options options;
	
	private Map<String, MemoryDataSink> memoryDataSinks = new HashMap<>();

	public EmaInstanceRunner() throws IOException {
		Properties props = new Properties();
		File in = new File("../MessageCenter.log4j.properties");
		props.load(new FileInputStream(in));
		PropertyConfigurator.configure(props);

		MessageCenter.addMessageListener(new MessageEventListener() {
			public void messageReceived(MessageEvent evt) {
				Level level = evt.getLevel();
				if (level == Level.ERROR || level == Level.WARN || level == Level.FATAL) {
					if (level == Level.WARN && status == RunningStatus.OK)
						status = RunningStatus.WARN;
					else if (level != Level.WARN)
						status = RunningStatus.FAILURE;
					writeMessage(evt);

				}
			}
		});

		initOptions();
	}

	
	public Map<String, List<Double>> run(List<String> outcomeKeys) throws ScenarioLoadException {
		
		this.createMemoryDataSinks(outcomeKeys);

		runner.batchInit();

		String line = null;
		try (BufferedReader reader = new BufferedReader(
				isFileInput ? new FileReader(input) : new StringReader(input))) {

			while ((line = reader.readLine()) != null) {
				if (line.trim().length() > 0) {
					
					Parameters params;
					try {
						params = lineParser.parse(line);
					} catch (NumberFormatException exception) {
						var message = "Could not parse Parameter line '" + line + "'. Please check if the parameters have the right format, e.g. Integer instead of Float.";
						throw new IllegalStateException(message, exception);
					}
					
					int runNum = (Integer) params.getValue(BatchConstants.BATCH_RUN_PARAM_NAME);
					runner.run(runNum, params);
					if (status == RunningStatus.FAILURE)
						break;
				}
			}
		} catch (IOException ex) {
			throw new ScenarioLoadException("Error while reading parameter input", ex);
		}
		
		var resultMap = collectDataFromMemoryDataSinks(outcomeKeys);
		
		runner.batchCleanup();
		
		return resultMap;	
	}

	private void createMemoryDataSinks(List<String> outcomeKeys) {
		var controller = runner.getController();

		var controllerRegistry = controller.getControllerRegistry();
		var contextId = controllerRegistry.getMasterContextId();
		var actionTree = controllerRegistry.getActionTree(contextId);
		
		var self = this;

		actionTree.addNode(actionTree.getRoot(), new ControllerAction() {

			@Override
			public void batchInitialize(RunState runState, Object contextId) {
				var dataSetRegistry = (DataSetRegistry) runState.getFromRegistry(DataConstants.REGISTRY_KEY);
				var dataSetManager = dataSetRegistry.getDataSetManager(contextId);				
				
				for(var outcomeKey: outcomeKeys) {			
					var parts = outcomeKey.split("\\.");
					var dataSetKey = parts[0];
					
					if(!self.memoryDataSinks.containsKey(dataSetKey)){
						var dataSetBuilder = dataSetManager.getDataSetBuilder(dataSetKey);
						if(dataSetBuilder!=null) {
							 var dataSink = new MemoryDataSink();	
				             self.memoryDataSinks.put(dataSetKey, dataSink);
							 dataSetBuilder.addDataSink(dataSink);
						} 
					}						               
				}					
			}

			@Override
			public void runInitialize(RunState runState, Object contextId, Parameters runParams) {
			}

			@Override
			public void runCleanup(RunState runState, Object contextId) {
			}

			@Override
			public void batchCleanup(RunState runState, Object contextId) {
			}

		});
	}
	
	private  Map<String, List<Double>> collectDataFromMemoryDataSinks(List<String> outcomeKeys){
		var resultMap = new HashMap<String, List<Double>>();
		for(var outcomeKey: outcomeKeys) {			
			var parts = outcomeKey.split("\\.");
			var dataSetId = parts[0];
			var dataSourceId = parts[1];			
			var dataSink = this.memoryDataSinks.get(dataSetId);
			if(dataSink!=null) {
				//var sourceIds = dataSink.getSourceIds();
				var sourceValues = dataSink.getSourceValues(dataSourceId);
				if(sourceValues!=null) {
					resultMap.put(outcomeKey, sourceValues);
				} else {
					var message = "Could not find DataSource '" + outcomeKey + "'. Please create it in the Repast model or correct the outcome identifier.";					
					msg.warn(message);
					resultMap.put(outcomeKey, new ArrayList<>());
				}				
			} else {
				var message = "Could not find DataSet '" + dataSetId + "'. Please create it in the Repast model or correct the outcome identifier.";
				msg.warn(message);
				resultMap.put(outcomeKey, new ArrayList<>());
			}			
		}
		return resultMap;
	}
	
	@SuppressWarnings("static-access")
	private void initOptions() {
		options = new Options();
		Option help = new Option("help", "print this message");
		options.addOption(help);
		Option pxml = OptionBuilder.withArgName("file").hasArg().withDescription("use given parameter xml file")
				.create(PXML);
		options.addOption(pxml);

		Option scenario = OptionBuilder.withArgName("directory").hasArg().withDescription("use given scenario")
				.create(SCENARIO);
		options.addOption(scenario);

		Option id = OptionBuilder.withArgName("value").hasArg().withDescription("use given value as instance id")
				.create(ID);
		options.addOption(id);

		Option pinput = OptionBuilder.withArgName("file").hasArg()
				.withDescription("use given file as run parameter input").create(PINPUT);
		options.addOption(pinput);
	}

	private void writeMessage(MessageEvent evt) {
		String fname = status.toString();
		File file = new File(fname + "_" + id);
		boolean append = file.exists();
		PrintWriter writer = null;
		try {
			writer = new PrintWriter(new FileWriter(file, append));
			writer.append(evt.getMessage().toString());
			writer.append("\n");
			if (evt.getThrowable() != null) {
				evt.getThrowable().printStackTrace(writer);
			}
		} catch (IOException ex) {
			if (evt.getThrowable() != null) {
				evt.getThrowable().printStackTrace();
			}
			ex.printStackTrace();
		} finally {
			if (writer != null)
				writer.close();
		}
	}

	public void configure(String[] args) throws IOException, ScenarioLoadException {
		CommandLineParser parser = new GnuParser();
		try {
			CommandLine line = parser.parse(options, args);

			if (line.hasOption(PXML)) {
				String paramFile = line.getOptionValue(PXML);
				File params = new File(paramFile);
				lineParser = new ParameterLineParser(params.toURI());
			} else {
				throw new ScenarioLoadException("Command line is missing required -pxml option");
			}

			if (line.hasOption(SCENARIO)) {
				File scenario = new File(line.getOptionValue(SCENARIO));
				runner = new EmaOneRunBatchRunner(scenario);
			} else {
				throw new ScenarioLoadException("Command line is missing required -scenario option");
			}

			if (line.hasOption(ID)) {
				id = line.getOptionValue(ID);
			} else {
				throw new ScenarioLoadException("Command line is missing required -id option");
			}

			if (line.hasOption(PINPUT)) {
				input = line.getOptionValue(PINPUT);
				isFileInput = true;
			} else {
				String[] otherArgs = line.getArgs();
				input = otherArgs[otherArgs.length - 1];
			}

		} catch (ParseException ex) {
			throw new ScenarioLoadException("Error while parsing command line args", ex);
		}

	}

	
	class MemoryDataSink implements DataSink {		
		
		
		private Map<String, List<Double>> sourceIdToSourceValuesMap = new HashMap<>();			

		@Override
		public void append(String key, Object value) {
			//System.out.println("append");			
			if(!sourceIdToSourceValuesMap.containsKey(key)) {
				this.sourceIdToSourceValuesMap.put(key, new ArrayList<Double>());
			}
			var doubleValue = Double.parseDouble(value.toString());
			this.sourceIdToSourceValuesMap.get(key).add(doubleValue);		
		}
		
		@Override
		public void open(List<String> sourceIds) {
			// System.out.println("open");
		}

		@Override
		public void rowStarted() {
			// System.out.println("rowStarted");
		}

		@Override
		public void rowEnded() {
			// System.out.println("rowEnded");
		}

		@Override
		public void recordEnded() {
			// System.out.println("recordEnded");
		}

		@Override
		public void flush() {
			// System.out.println("flush");
		}

		@Override
		public void close() {
			// System.out.println("close");
		}	
		
		public List<Double> getSourceValues(String sourceId){
			return this.sourceIdToSourceValuesMap.get(sourceId);
		}
		
		public Set<String> getSourceIds(){
			return this.sourceIdToSourceValuesMap.keySet();
		}
	}

	
}

EmaOneRunBatchRunner.java
package repast.simphony.batch;

import java.io.File;

import repast.simphony.batch.BatchScenarioLoader;
import repast.simphony.batch.BatchScheduleRunner;
import repast.simphony.batch.OneRunBatchRunner;
import repast.simphony.engine.controller.DefaultController;
import repast.simphony.engine.environment.AbstractRunner;
import repast.simphony.engine.environment.ControllerRegistry;
import repast.simphony.engine.environment.DefaultRunEnvironmentBuilder;
import repast.simphony.engine.environment.RunEnvironmentBuilder;
import repast.simphony.engine.environment.RunListener;
import repast.simphony.parameter.ParameterConstants;
import repast.simphony.parameter.ParameterSetter;
import repast.simphony.parameter.Parameters;
import repast.simphony.parameter.ParametersCreator;
import repast.simphony.parameter.SweeperProducer;
import repast.simphony.scenario.ScenarioLoadException;
import repast.simphony.scenario.ScenarioLoader;
import simphony.util.messages.MessageCenter;


/**
 * original author of OneRunBatchRunner: Nick Collier
 * modified by Stefan Eidelloth
 */
public class EmaOneRunBatchRunner implements RunListener {

  private static MessageCenter msgCenter = MessageCenter.getMessageCenter(OneRunBatchRunner.class);

  private RunEnvironmentBuilder runEnvironmentBuilder;
  protected ORBController controller;
  protected boolean pause = false;
  protected Object monitor = new Object();
  protected SweeperProducer producer;

  public EmaOneRunBatchRunner(File scenarioDir) throws ScenarioLoadException {
    AbstractRunner scheduleRunner = new BatchScheduleRunner();    
   
    scheduleRunner.addRunListener(this);
    runEnvironmentBuilder = new DefaultRunEnvironmentBuilder(scheduleRunner, true);
   
    controller = new ORBController(runEnvironmentBuilder);
    controller.setScheduleRunner(scheduleRunner);
    init(scenarioDir);
  }

  private void init(File scenarioDir) throws ScenarioLoadException {
    if (scenarioDir.exists()) {
      ScenarioLoader loader = createScenarioLoader(scenarioDir);
      ControllerRegistry registry = loader.load(runEnvironmentBuilder);
      controller.setControllerRegistry(registry);
    } else {
      msgCenter.error("Scenario not found", new IllegalArgumentException("Invalid scenario "
          + scenarioDir.getAbsolutePath()));
    }
  }

  //edit: adapted this function to avoid issues with non-existing resource file  
  private ScenarioLoader createScenarioLoader(File scenarioDir) {       	
    return new BatchScenarioLoader(scenarioDir);
  }

  public void batchInit() {
    controller.batchInitialize();
  }

  public void batchCleanup() {
    controller.batchCleanup();
  }

  public void run(int runNum, Parameters params) {
    pause = true;
    params = setupSweep(params);
    controller.runParameterSetters(params);
    controller.setRunNumber(runNum);
    controller.runInitialize(params);
    controller.execute();
    waitForRun();    
    controller.runCleanup();
  }

  protected boolean keepRunning() {
    for (ParameterSetter setter : controller.getControllerRegistry().getParameterSetters()) {
      if (!setter.atEnd()) {
        return true;
      }
    }
    return false;
  }

  private Parameters setupSweep(Parameters params) {

    if (!params.getSchema().contains(ParameterConstants.DEFAULT_RANDOM_SEED_USAGE_NAME)) {
      ParametersCreator creator = new ParametersCreator();
      creator.addParameters(params);
      creator.addParameter(ParameterConstants.DEFAULT_RANDOM_SEED_USAGE_NAME, Integer.class,
          (int) System.currentTimeMillis(), false);
      params = creator.createParameters();
    }

    return params;
  }

  protected void waitForRun() {
    // msgCenter.info("Waiting");
    synchronized (monitor) {
      while (pause) {
        try {
          monitor.wait();
        } catch (InterruptedException e) {
          e.printStackTrace();
          break;
        }
      }
    }
    // msgCenter.info("Done Waiting");
  }

  protected void notifyMonitor() {
    synchronized (monitor) {
      monitor.notify();
    }
  }


  /**
   * Invoked when the current run has been paused.
   */
  public void paused() {
  }

  /**
   * Invoked when the current run has been restarted after a pause.
   */
  public void restarted() {
  }

  /**
   * Invoked when the current run has been started.
   */
  public void started() {
  }

  /**
   * Invoked when the current run has been stopped. This will stop this thread
   * from waiting for the current run to finish.
   */
  public void stopped() {
    pause = false;
    notifyMonitor();
    // msgCenter.info("Stopped Called");
  }
  
  //edit: added to allow access to controller
  public DefaultController getController() {
	  return controller;
  }

  private static class ORBController extends DefaultController {

    private int runNumber;

    /**
     * @param runEnvironmentBuilder
     */
    public ORBController(RunEnvironmentBuilder runEnvironmentBuilder) {
      super(runEnvironmentBuilder);
    }

    public void setRunNumber(int runNumber) {
      this.runNumber = runNumber;
    }

    /*
     * (non-Javadoc)
     * 
     * @see repast.simphony.engine.controller.DefaultController#prepare()
     */
    @Override
    protected boolean prepare() {
      boolean retVal = super.prepare();
      this.getCurrentRunState().getRunInfo().setRunNumber(runNumber); 
      
      return retVal;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * repast.simphony.engine.controller.DefaultController#prepareForNextRun()
     */
    @Override
    protected void prepareForNextRun() {
      super.prepareForNextRun();
      this.getCurrentRunState().getRunInfo().setRunNumber(runNumber);
    }
  }
}

EMA Connector repast.py
'''
This module specifies a generic ModelStructureInterface for controlling
Repast Symphony models. 
'''
from __future__ import (absolute_import, print_function, division, unicode_literals)
from ema_workbench.em_framework.model import Replicator, SingleReplication
from ema_workbench.em_framework.model import WorkingDirectoryModel
from ema_workbench.util.ema_logging import method_logger
from ema_workbench.util import debug

import os
import sys
import jpype

__all__ = ['RepastModel']


def find_jars(path):
    """Find all jar files in directory and return as list

    Parameters
    ----------
    path : str
        Path in which to find jar files

    Returns
    -------
    str
        List of jar files

    """

    jars = []
    for root, _, files in os.walk(path):
        for file in files:  # @ReservedAssignment
            if file == 'NetLogo.jar':
                jars.insert(0, os.path.join(root, file))
            elif file.endswith(".jar"):
                jars.append(os.path.join(root, file))

    return jars

#Basic project exception
class RepastException(Exception):    
    pass


class RepastLink(object):
    """Create a link with Repast Simphony. Underneath, the NetLogo JVM
    is started through Jpype.

    If `jvm_home` is not provided, the link will try to identify the correct parameters automatically.

    Parameters
    ----------
    repast_home : str, optional
        Home directory of repast project. That home directory must 
        * contain a scenario sub folder whose name ends with ".rs", e.g. JZombies_Demo.rs
        * contains batch sub folder that contains a batch parameter file batch_params.xml
        * contain a lib folder with all required repast *.jar files
        * contain a bin folder with the compiled model files
    jvm_home : str, optional
        Java home directory for Jpype
    jvmargs : list of str, optional
              additional arguments that should be used when starting
              the jvm

    """

    def __init__(self, repast_home, jvm_path=None, jvmargs=[]):
        
        self.repast_home = repast_home 
       
        if not jvm_path:
            jvm_path = jpype.getDefaultJVMPath()
       
        self.jvm_path = jvm_path

        if sys.platform == 'win32':
            path_sep = ';'
        else:
            path_sep = ':'

        if not jpype.isJVMStarted():
            
            libPath = repast_home + '/lib'
             
            paths = find_jars(libPath)  
            
            paths.append(os.path.join(repast_home, '.'))
            paths.append(os.path.join(repast_home, 'bin'))
            joined_paths = path_sep.join(paths)
            classPathArgument = '-Djava.class.path={}'.format(joined_paths)

            jvm_args = [classPathArgument, ] + jvmargs

            try:
                jpype.startJVM(jvm_path, *jvm_args)
            except RuntimeError as e:
                raise e    
        
       
        self.link = jpype.JClass('repast.simphony.batch.RepastRunner')
        
    def run(self, name, experiment, outputVariables):
        
        jExperiment = jpype.java.util.HashMap();
        for key, value in experiment.items(): 
            jExperiment.put(key,value)
        
        jOutputs = jpype.java.util.ArrayList()
        for outputVariable in outputVariables:
            jOutputs.add(outputVariable)        
                
        jResults = self.link.run(self.repast_home, name, jExperiment, jOutputs) 
        
        '''  #This conversion does not seem to be required because ema seem to
             #iterate and convert the results by iteself      
        results = {}
        for entry in jResults.entrySet():
            key = entry.getKey()
            valueList = entry.getValue()
            result = [float(value) for value in valueList]
            results[key] = np.asarray(result)
        '''
            
        return jResults
           

    def kill_workspace(self):
        self.link.killWorkspace()
        
       

class BaseRepastModel(WorkingDirectoryModel):
    '''Base class for interfacing with repast models. This class
    extends :class:`em_framework.ModelStructureInterface`.
    '''
        

    def __init__(self, name, repast_home, jvm_path=None):
        """
        init of class

        Parameters
        ----------
        wd   : str
               working directory for the model. 
        name : str
               name of the modelInterface. The name should contain only
               alpha-numerical characters.
        repast_home : str, optional
               Path to the Repast project       
        jvm_home : str, optional
               Java home directory for Jpype      

        Raises
        ------
        EMAError if name contains non alpha-numerical characters

        Note
        ----
        Anything that is relative to `self.working_directory`should be 
        specified in `model_init` and not in `src`. Otherwise, the code 
        will not work when running it in parallel. The reason for this is that 
        the working directory is being updated by parallelEMA to the worker's 
        separate working directory prior to calling `model_init`.

        """
        super(BaseRepastModel, self).__init__(name, wd=repast_home)

        self.run_length = None       
        self.jvm_path = jvm_path       

    @method_logger
    def model_init(self, policy):
        '''
        Method called to initialize the model.

        Parameters
        ----------
        policy : dict
                 policy to be run  
        '''
        super(BaseRepastModel, self).model_init(policy)
        if not hasattr(self,'repast'):
            debug("trying to start Repast")
            self.repast = RepastLink(repast_home=self.working_directory, jvm_path=self.jvm_path)
            debug("repast started")       

    @method_logger
    def run_experiment(self, experiment):
        """
        Method for running an instantiated model structure. 

        Parameters
        ----------
        experiment : dict like


        Raises
        ------
        jpype.JavaException if there is any exception thrown by the repast model


        """
        return self.repast.run(self.name, experiment, self.output_variables) 

       
    def retrieve_output(self):
        """
        Method for retrieving output after a model run.

        Returns
        -------
        dict with the results of a model run. 

        """
        return self.output

    def cleanup(self):
        '''
        This method is called after finishing all the experiments, but 
        just prior to returning the results. This method gives a hook for
        doing any cleanup, such as closing applications. 

        In case of running in parallel, this method is called during 
        the cleanup of the pool, just prior to removing the temporary 
        directories. 

        '''
        if hasattr(self,'repast'):
            self.repast.kill_workspace() 
            jpype.shutdownJVM()         
    

class RepastModel(Replicator, BaseRepastModel):
    pass


class SingleReplicationRepastModel(SingleReplication, BaseRepastModel):
    pass

repastDemo.py
from ema_workbench import IntegerParameter

from ema_workbench import Constant
from ema_workbench import TimeSeriesOutcome
from ema_workbench import ema_logging
from ema_workbench import perform_experiments

from ema.connector.repast import SingleReplicationRepastModel 

from ema_workbench.analysis.plotting import envelopes 
from ema_workbench.analysis.plotting_util import KDE

from matplotlib import pyplot as plt

def main():
    
    #define input
    uncertainties = [IntegerParameter('human_count', 10, 20)]
    
    
    #levers = [IntegerParameter('zombie_count',5,10)]
    
    constants = [Constant('zombie_count',10)]
    
    
    #define output    
    outcomes = [
                TimeSeriesOutcome('Agent Counts.Human Count'),
                TimeSeriesOutcome('Agent Counts.Zombie Count')
               ]   
    
    
    #instantiate the repast model       
    repast_home = r'D:\EclipsePython\workspace\ExploratoryModelling\repast'
    jvm_path = r'D:\EclipsePython\App\jdk\bin\server\jvm.dll'
    vensimModel = SingleReplicationRepastModel("simpleModel", repast_home, jvm_path)    
    vensimModel.uncertainties = uncertainties    
    vensimModel.outcomes = outcomes       
    #vensimModel.levers = levers
    vensimModel.constants = constants
    
    #configure logging (e.g. progress messages)
    ema_logging.LOG_FORMAT = '[%(name)s/%(levelname)s/%(processName)s] %(message)s' 
    ema_logging.log_to_stderr(ema_logging.INFO)
         
          
   
    results = perform_experiments(vensimModel, scenarios=10)
    experiments, outcomes = results
    
       
    print(experiments.shape)
    print(list(outcomes.keys()))   
    
    
    # the plotting functions return the figure and a dict of axes
    envelopes(results, group_by='policy', density=KDE, fill=True)          
    plt.show()    
   

if __name__ == '__main__':   
        main()

stefaneidelloth avatar Apr 08 '19 13:04 stefaneidelloth

This looks pretty comprehensive and gives me a perfect basis for getting this done. I hope I can work on at the end of April, unless you need it sooner (in which case, I will try to get to it earlier).

quaquel avatar Apr 10 '19 07:04 quaquel

I also need the Repast connector... I hope it can be included in the official release soon. Thank you guys.

FrankWorldview avatar Jul 10 '19 13:07 FrankWorldview

hope to finally work on this after the summer break. It is quite high on my nice to have list. Problem is that I am not a repast user myself, nor do I have anyone in my direct vicinity that uses it. However, the proof of principle developed here gives me an excellent starting point to finally make this connector.

quaquel avatar Jul 18 '19 05:07 quaquel