/** * Copyright (c) 2012-2016 Marsha Chechik, Alessio Di Sandro, Michalis Famelis, * Rick Salay. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Alessio Di Sandro - Implementation. */ package edu.toronto.cs.se.modelepedia.operator.experiment; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.emf.common.util.BasicEList; import org.eclipse.emf.common.util.EList; import org.eclipse.jdt.annotation.NonNull; import edu.toronto.cs.se.mmint.MMINT; import edu.toronto.cs.se.mmint.MMINTException; import edu.toronto.cs.se.mmint.MIDTypeHierarchy; import edu.toronto.cs.se.mmint.MIDTypeRegistry; import edu.toronto.cs.se.mmint.mid.GenericElement; import edu.toronto.cs.se.mmint.mid.MID; import edu.toronto.cs.se.mmint.mid.Model; import edu.toronto.cs.se.mmint.mid.operator.Operator; import edu.toronto.cs.se.mmint.mid.operator.OperatorGeneric; import edu.toronto.cs.se.mmint.mid.operator.OperatorInput; import edu.toronto.cs.se.mmint.mid.operator.RandomOperator; import edu.toronto.cs.se.mmint.mid.operator.impl.OperatorImpl; import edu.toronto.cs.se.mmint.mid.utils.FileUtils; import edu.toronto.cs.se.mmint.mid.utils.MIDOperatorIOUtils; import edu.toronto.cs.se.modelepedia.operator.experiment.ExperimentSamples.DistributionType; //TODO MMINT[OPERATOR] Create a separate feature for these generic megamodel operators public class ExperimentDriver extends OperatorImpl { protected class ExperimentWatchdog implements Runnable { private ExperimentDriver driver; private Model initialModel; private int experimentIndex; private boolean[] outputConfidences; public ExperimentWatchdog(ExperimentDriver driver, Model initialModel, int experimentIndex) { this.driver = driver; this.initialModel = initialModel; this.experimentIndex = experimentIndex; outputConfidences = new boolean[outputs.length]; for (int out = 0; out < outputs.length; out++) { outputConfidences[out] = (outputDoConfidences[out]) ? false : true; } } @Override public void run() { try { // create experiment folder IFolder folder = ResourcesPlugin.getWorkspace().getRoot().getFolder(new Path(FileUtils.replaceLastSegmentInUri(initialModel.getUri(), EXPERIMENT_SUBDIR + experimentIndex))); if (!folder.exists(null)) { folder.create(true, true, null); } List<Operator> outerOperatorWorkflow = new ArrayList<>(); EList<Model> outerInputModels = new BasicEList<>(); outerInputModels.add(initialModel); for (int op = 0; op < experimentOperators.length; op++) { try { outerInputModels = executeOperator(experimentIndex, -1, op, experimentOperators[op], outerInputModels, outerOperatorWorkflow, outputConfidences); } catch (Exception e) { MMINTException.print(IStatus.WARNING, "Experiment " + experimentIndex + " out of " + (numExperiments-1) + " failed", e); MIDOperatorIOUtils.writePropertiesFile( writeProperties(null, experimentIndex), driver, initialModel, EXPERIMENT_SUBDIR + experimentIndex, MIDOperatorIOUtils.OUTPUT_PROPERTIES_SUFFIX ); return; } } ExperimentSamples[] experiment = new ExperimentSamples[outputs.length]; for (int out = 0; out < outputs.length; out++) { experiment[out] = new ExperimentSamples(minSamples, maxSamples - skipWarmupSamples, distribution, outputMins[out], outputMaxs[out], requestedConfidence, outputDoConfidences[out]); } // inner cycle: experiment setup is fixed, vary randomness and statistics int j; for (j = 0; j < maxSamples; j++) { // create sample folder folder = ResourcesPlugin.getWorkspace().getRoot().getFolder(new Path(FileUtils.replaceLastSegmentInUri(initialModel.getUri(), EXPERIMENT_SUBDIR + experimentIndex + MMINT.URI_SEPARATOR + SAMPLE_SUBDIR + j))); if (!folder.exists(null)) { folder.create(true, true, null); } List<Operator> innerOperatorWorkflow = new ArrayList<>(outerOperatorWorkflow); EList<Model> innerInputModels = outerInputModels; boolean timedOut = false; // run time-bounded chain of operators ExecutorService executor = Executors.newSingleThreadExecutor(); try { executor.submit( new SampleWatchdog(experimentIndex, j, innerInputModels, innerOperatorWorkflow, outputConfidences) ).get(maxProcessingTime, TimeUnit.SECONDS); executor.shutdown(); } catch (Exception e) { executor.shutdownNow(); timedOut = true; MMINTException.print(IStatus.WARNING, "Experiment " + experimentIndex + " out of " + (numExperiments-1) + ", sample " + j + " ran over time limit", e); } // skip warmup phase if (j < skipWarmupSamples) { continue; } // get results for (int out = 0; out < outputs.length; out++) { try { double sample = (timedOut) ? outputDefaults[out] : getOutput(initialModel, out, experimentIndex, j); if (sample == Double.MAX_VALUE) { MMINTException.print(IStatus.WARNING, "Experiment " + experimentIndex + " out of " + (numExperiments-1) + ", sample " + j + ", output " + outputs[out] + " skipped", null); continue; } outputConfidences[out] = experiment[out].addSample(sample); } catch (Exception e) { MMINTException.print(IStatus.WARNING, "Experiment " + experimentIndex + " out of " + (numExperiments-1) + ", sample " + j + ", output " + outputs[out] + " not available", e); } } // evaluate confidence intervals boolean allConfident = true; for (int out = 0; out < outputs.length; out++) { allConfident = outputConfidences[out] && allConfident; } if (allConfident) { break; } } // save output MIDOperatorIOUtils.writePropertiesFile( writeProperties(experiment, experimentIndex), driver, initialModel, EXPERIMENT_SUBDIR + experimentIndex, MIDOperatorIOUtils.OUTPUT_PROPERTIES_SUFFIX ); writeGnuplotFile(driver, initialModel, experiment, experimentIndex, varX); } catch (Exception e) { MMINTException.print(IStatus.WARNING, "Experiment " + experimentIndex + " out of " + (numExperiments-1) + " failed", e); } } } protected class SampleWatchdog implements Runnable { private int experimentIndex; private int statisticsIndex; private EList<Model> inputModels; private List<Operator> operatorWorkflow; private boolean[] outputConfidences; public SampleWatchdog(int experimentIndex, int statisticsIndex, EList<Model> inputModels, List<Operator> operatorWorkflow, boolean[] outputConfidences) { this.experimentIndex = experimentIndex; this.statisticsIndex = statisticsIndex; this.inputModels = inputModels; this.operatorWorkflow = operatorWorkflow; this.outputConfidences = outputConfidences; } @Override public void run() { System.err.println("Running experiment " + experimentIndex + " out of " + (numExperiments-1) + ", sample " + statisticsIndex); for (int op = 0; op < statisticsOperators.length; op++) { try { inputModels = executeOperator(experimentIndex, statisticsIndex, op, statisticsOperators[op], inputModels, operatorWorkflow, outputConfidences); } catch (Exception e) { MMINTException.print(IStatus.WARNING, "Experiment " + experimentIndex + " out of " + (numExperiments-1) + ", sample " + statisticsIndex + " failed", e); return; } } } } // input-output private final static @NonNull String IN_MODEL = "initial"; /** The variables to variate the experiment setup. */ private static final String PROPERTY_IN_VARIABLES = "variables"; /** The variable values property suffix. */ private static final String PROPERTY_IN_VARIABLEVALUES_SUFFIX = ".values"; private static final String PROPERTY_IN_VARIABLEX_SUFFIX = ".varX"; /** The initial seed for the pseudorandom generator. */ private static final String PROPERTY_IN_SEED = "seed"; /** Number of samples to discard at the beginning of each experiment (warmup phase). */ private static final String PROPERTY_IN_SKIPWARMUPSAMPLES = "skipWarmupSamples"; /** Min number of iterations (i.e. samples to generate). */ private static final String PROPERTY_IN_MINSAMPLES = "minSamples"; /** Max number of iterations (i.e. samples to generate). */ private static final String PROPERTY_IN_MAXSAMPLES = "maxSamples"; /** The distribution type to be used when evaluating the confidence. */ private static final String PROPERTY_IN_DISTRIBUTIONTYPE = "distributionType"; /** The requested range of confidence interval (% with respect to average value), after which the experiment can be stopped. */ private static final String PROPERTY_IN_REQUESTEDCONFIDENCE = "requestedConfidence"; private static final String PROPERTY_IN_NUMTHREADS = "numThreads"; private static final int PROPERTY_IN_NUMTHREADS_DEFAULT = 1; /** The operators to be launched in the outer experiment setup cycle. */ private static final String PROPERTY_IN_EXPERIMENTOPERATORS = "experimentOperators"; private static final String[] PROPERTY_IN_EXPERIMENTOPERATORS_DEFAULT = new String[] {}; /** The operators to be launched in the inner statistics cycle. */ private static final String PROPERTY_IN_STATISTICSOPERATORS = "statisticsOperators"; /** The variable operators property suffix. */ private static final String PROPERTY_IN_ALLOPERATORS_SUFFIX = ".operator"; /** The outputs of the experiment. */ private static final String PROPERTY_IN_OUTPUTS = "outputs"; private static final String[] PROPERTY_IN_OUTPUTS_DEFAULT = new String[0]; /** The output default result property suffix. */ private static final String PROPERTY_IN_OUTPUTDEFAULT_SUFFIX = ".defaultResult"; /** Min value a sample can take. */ private static final String PROPERTY_IN_OUTPUTMINSAMPLEVALUE_SUFFIX = ".minSampleValue"; /** Max value a sample can take, -1 for unlimited. */ private static final String PROPERTY_IN_OUTPUTMAXSAMPLEVALUE_SUFFIX = ".maxSampleValue"; private static final String PROPERTY_IN_OUTPUTDOCONFIDENCE_SUFFIX = ".doConfidence"; /** Max processing time to generate the outputs. */ private static final String PROPERTY_IN_MAXPROCESSINGTIME = "maxProcessingTime"; public static final String PROPERTY_OUT_RESULTLOW_SUFFIX = ".resultLow"; public static final String PROPERTY_OUT_RESULTAVG_SUFFIX = ".resultAvg"; public static final String PROPERTY_OUT_RESULTUP_SUFFIX = ".resultUp"; private static final String PROPERTY_OUT_NUMSAMPLES_SUFFIX = ".numSamples"; public static final String PROPERTY_OUT_VARIABLEINSTANCE_SUFFIX = ".instance"; private static final String EXPERIMENT_SUBDIR = "experiment"; private static final String SAMPLE_SUBDIR = "sample"; private static final String GNUPLOT_SUFFIX = MIDOperatorIOUtils.OUTPUT_PROPERTIES_SUFFIX + ".dat"; // experiment setup parameters private String[] vars; private String[][] varValues; private int varX; private int numExperiments; private String[][] experimentSetups; // experiment randomness parameters private long seed; private Random[] state; private int skipWarmupSamples; private int minSamples; private int maxSamples; private DistributionType distribution; private double requestedConfidence; // experiment operators private String[] experimentOperators; private String[] statisticsOperators; private String[][] varOperators; // experiment outputs private String[] outputs; private String[] outputOperators; private double[] outputDefaults; private double[] outputMins; private double[] outputMaxs; private boolean[] outputDoConfidences; // experiment efficiency private int maxProcessingTime; private int numThreads; private @NonNull String[] getArrayStringProperties(@NonNull Properties properties, @NonNull String propertyName) throws MMINTException { String property = MIDOperatorIOUtils.getStringProperty(properties, propertyName); if (property.startsWith("[") && property.endsWith("]")) { return property.substring(1, property.length()-1).split("\\],\\["); } else { return MIDOperatorIOUtils.getStringProperties(properties, propertyName); } } @Override public void readInputProperties(Properties inputProperties) throws MMINTException { // outer cycle parameters: vary experiment setup vars = MIDOperatorIOUtils.getStringProperties(inputProperties, PROPERTY_IN_VARIABLES); varValues = new String[vars.length][]; numExperiments = 1; for (int i = 0; i < vars.length; i++) { varValues[i] = getArrayStringProperties(inputProperties, vars[i]+PROPERTY_IN_VARIABLEVALUES_SUFFIX); numExperiments *= varValues[i].length; if (MIDOperatorIOUtils.getOptionalBoolProperty(inputProperties, vars[i]+PROPERTY_IN_VARIABLEX_SUFFIX, false)) { varX = i; } } // inner cycle parameters: experiment setup is fixed, vary randomness and statistics seed = MIDOperatorIOUtils.getIntProperty(inputProperties, PROPERTY_IN_SEED); skipWarmupSamples = MIDOperatorIOUtils.getIntProperty(inputProperties, PROPERTY_IN_SKIPWARMUPSAMPLES); minSamples = MIDOperatorIOUtils.getIntProperty(inputProperties, PROPERTY_IN_MINSAMPLES); maxSamples = MIDOperatorIOUtils.getIntProperty(inputProperties, PROPERTY_IN_MAXSAMPLES); distribution = DistributionType.valueOf(MIDOperatorIOUtils.getStringProperty(inputProperties, PROPERTY_IN_DISTRIBUTIONTYPE)); requestedConfidence = MIDOperatorIOUtils.getDoubleProperty(inputProperties, PROPERTY_IN_REQUESTEDCONFIDENCE); // operators experimentOperators = MIDOperatorIOUtils.getOptionalStringProperties(inputProperties, PROPERTY_IN_EXPERIMENTOPERATORS, PROPERTY_IN_EXPERIMENTOPERATORS_DEFAULT); statisticsOperators = MIDOperatorIOUtils.getStringProperties(inputProperties, PROPERTY_IN_STATISTICSOPERATORS); varOperators = new String[vars.length][]; for (int i = 0; i < vars.length; i++) { varOperators[i] = MIDOperatorIOUtils.getStringProperties(inputProperties, vars[i]+PROPERTY_IN_ALLOPERATORS_SUFFIX); } // outputs outputs = MIDOperatorIOUtils.getOptionalStringProperties(inputProperties, PROPERTY_IN_OUTPUTS, PROPERTY_IN_OUTPUTS_DEFAULT); outputOperators = new String[outputs.length]; outputDefaults = new double[outputs.length]; outputMins = new double[outputs.length]; outputMaxs = new double[outputs.length]; outputDoConfidences = new boolean[outputs.length]; for (int i = 0; i < outputs.length; i++) { outputOperators[i] = MIDOperatorIOUtils.getStringProperty(inputProperties, outputs[i]+PROPERTY_IN_ALLOPERATORS_SUFFIX); outputDefaults[i] = MIDOperatorIOUtils.getDoubleProperty(inputProperties, outputs[i]+PROPERTY_IN_OUTPUTDEFAULT_SUFFIX); outputMins[i] = MIDOperatorIOUtils.getDoubleProperty(inputProperties, outputs[i]+PROPERTY_IN_OUTPUTMINSAMPLEVALUE_SUFFIX); outputMaxs[i] = MIDOperatorIOUtils.getDoubleProperty(inputProperties, outputs[i]+PROPERTY_IN_OUTPUTMAXSAMPLEVALUE_SUFFIX); outputDoConfidences[i] = MIDOperatorIOUtils.getBoolProperty(inputProperties, outputs[i]+PROPERTY_IN_OUTPUTDOCONFIDENCE_SUFFIX); } // efficiency maxProcessingTime = MIDOperatorIOUtils.getIntProperty(inputProperties, PROPERTY_IN_MAXPROCESSINGTIME); numThreads = MIDOperatorIOUtils.getOptionalIntProperty(inputProperties, PROPERTY_IN_NUMTHREADS, PROPERTY_IN_NUMTHREADS_DEFAULT); } private Properties writeProperties(ExperimentSamples[] experiment, int experimentIndex) { Properties properties = new Properties(); if (experiment != null) { // only with outputs for (int out = 0; out < outputs.length; out++) { properties.setProperty(outputs[out]+PROPERTY_OUT_RESULTAVG_SUFFIX, String.valueOf(experiment[out].getAverage())); if (outputDoConfidences[out]) { properties.setProperty(outputs[out]+PROPERTY_OUT_RESULTUP_SUFFIX, String.valueOf(experiment[out].getUpperConfidence())); properties.setProperty(outputs[out]+PROPERTY_OUT_RESULTLOW_SUFFIX, String.valueOf(experiment[out].getLowerConfidence())); } properties.setProperty(outputs[out]+PROPERTY_OUT_NUMSAMPLES_SUFFIX, String.valueOf(experiment[out].getNumSamples())); } } for (int i = 0; i < vars.length; i++) { properties.setProperty(vars[i]+PROPERTY_OUT_VARIABLEINSTANCE_SUFFIX, experimentSetups[experimentIndex][i]); } return properties; } private void writeGnuplotFile(Operator driver, Model initialModel, ExperimentSamples[] experiment, int experimentIndex, int varX) { if (experiment == null) { // no outputs return; } // append outputs StringBuilder gnuplotBuilder = new StringBuilder(experimentSetups[experimentIndex][varX]); for (int out = 0; out < outputs.length; out++) { gnuplotBuilder.append(' '); gnuplotBuilder.append(experiment[out].getAverage()); if (outputDoConfidences[out]) { gnuplotBuilder.append(' '); gnuplotBuilder.append(experiment[out].getUpperConfidence()); gnuplotBuilder.append(' '); gnuplotBuilder.append(experiment[out].getLowerConfidence()); } } // write output try { MIDOperatorIOUtils.writeTextFile(driver, initialModel, EXPERIMENT_SUBDIR + experimentIndex, GNUPLOT_SUFFIX, gnuplotBuilder); } catch (IOException e) { MMINTException.print(IStatus.WARNING, "Experiment " + experimentIndex + " out of " + (numExperiments-1) + ", gnuplot output failed", e); } } private void cartesianProduct(String[][] experimentSetups) { for (int i = 0; i < numExperiments; i++) { int k = 1; for (int j = 0; j < varValues.length; j++) { String[] value = varValues[j]; String choice = value[(i/k) % value.length]; experimentSetups[i][j] = choice; k *= value.length; } } } private EList<Model> executeOperator(int experimentIndex, int statisticsIndex, int operatorIndex, String operatorUri, EList<Model> inputModels, List<Operator> operatorWorkflow, boolean[] outputConfidences) throws Exception { // empty operator list if (operatorUri.equals("")) { return inputModels; } // get operator Operator operatorType = MIDTypeRegistry.getType(operatorUri); if (operatorType == null) { throw new MMINTException("Operator uri " + operatorUri + " is not registered"); } if (operatorType.getInputs().size() == 0) { // fix operator with no model input at the beginning of the experiment inputModels = new BasicEList<>(); } // write operator input properties Properties inputProperties = new Properties(); for (int i = 0; i < varOperators.length; i++) { for (int j = 0; j < varOperators[i].length; j++) { if (varOperators[i][j].equals(operatorUri)) { inputProperties.setProperty(vars[i], experimentSetups[experimentIndex][i]); break; } } } for (int out = 0; out < outputs.length; out++) { if (outputDoConfidences[out] && operatorUri.equals(outputOperators[out])) { if (!outputConfidences[out]) { inputProperties.setProperty(outputs[out]+MIDOperatorIOUtils.PROPERTY_IN_OUTPUTENABLED_SUFFIX, "true"); } else { inputProperties.setProperty(outputs[out]+MIDOperatorIOUtils.PROPERTY_IN_OUTPUTENABLED_SUFFIX, "false"); } } } if (operatorIndex == 0) { String nextSubdir; if (statisticsIndex < 0) { nextSubdir = EXPERIMENT_SUBDIR + experimentIndex; } else { nextSubdir = (experimentOperators.length == 0) ? EXPERIMENT_SUBDIR + experimentIndex + MMINT.URI_SEPARATOR + SAMPLE_SUBDIR + statisticsIndex: SAMPLE_SUBDIR + statisticsIndex; } inputProperties.setProperty(MIDOperatorIOUtils.PROPERTY_IN_SUBDIR, nextSubdir); } inputProperties.setProperty(MIDOperatorIOUtils.PROPERTY_IN_UPDATEMID, "false"); // execute, get state and add to workflow EList<OperatorInput> inputs = operatorType.checkAllowedInputs(inputModels); EList<OperatorGeneric> generics = operatorType.selectAllowedGenerics(inputs); Map<String, MID> outputMIDsByName = new HashMap<>(); if (operatorType instanceof RandomOperator) { // random state passing ((RandomOperator) operatorType).setState(state[experimentIndex]); } if (!operatorWorkflow.isEmpty()) { // operator workflow passing operatorType.setPreviousOperator(operatorWorkflow.get(operatorWorkflow.size()-1)); } Operator operator = operatorType.startInstance(inputs, inputProperties, generics, outputMIDsByName, null); if (operatorType instanceof RandomOperator) { // random state passing state[experimentIndex] = ((RandomOperator) operator).getState(); ((RandomOperator) operatorType).setState(null); } if (!operatorWorkflow.isEmpty()) { // operator workflow passing operatorType.setPreviousOperator(null); } operatorWorkflow.add(operator); EList<Model> outputModels = operator.getOutputModels(); return outputModels; } private double getOutput(Model initialModel, int outputIndex, int experimentIndex, int statisticsIndex) throws Exception { // get output operator Operator operatorType = MIDTypeRegistry.getType(outputOperators[outputIndex]); if (operatorType == null) { throw new MMINTException("Operator uri " + outputOperators[outputIndex] + " is not registered"); } String experimentSubdir = EXPERIMENT_SUBDIR + experimentIndex + MMINT.URI_SEPARATOR + SAMPLE_SUBDIR + statisticsIndex; Properties resultProperties = MIDOperatorIOUtils.getPropertiesFile( operatorType, initialModel, experimentSubdir, MIDOperatorIOUtils.OUTPUT_PROPERTIES_SUFFIX ); return MIDOperatorIOUtils.getDoubleProperty(resultProperties, outputs[outputIndex]); } @Override public Map<String, Model> run( Map<String, Model> inputsByName, Map<String, GenericElement> genericsByName, Map<String, MID> outputMIDsByName) throws Exception { // prepare experiment setup Model initialModel = inputsByName.get(IN_MODEL); state = new Random[numExperiments]; for (int i = 0; i < numExperiments; i++) { state[i] = new Random(seed + i); } experimentSetups = new String[numExperiments][vars.length]; cartesianProduct(experimentSetups); MIDTypeHierarchy.clearCachedRuntimeTypes(); ExecutorService executor = Executors.newFixedThreadPool(numThreads); // outer cycle: vary experiment setup for (int i = 0; i < numExperiments; i++) { // run time-bounded chain of experiments try { executor.submit(new ExperimentWatchdog(this, initialModel, i)); } catch (Exception e) { MMINTException.print(IStatus.WARNING, "Experiment " + i + " out of " + (numExperiments-1) + " failed", e); } } executor.shutdown(); executor.awaitTermination(24, TimeUnit.HOURS); MIDTypeHierarchy.clearCachedRuntimeTypes(); return new HashMap<>(); } }