All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jasima.core.experiment.Experiment Maven / Gradle / Ivy

/*******************************************************************************
 * This file is part of jasima, v1.3, the Java simulator for manufacturing and 
 * logistics.
 *  
 * Copyright (c) 2015 		jasima solutions UG
 * Copyright (c) 2010-2015 Torsten Hildebrandt and jasima contributors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 *******************************************************************************/
package jasima.core.experiment;

import jasima.core.expExecution.ExperimentExecutor;
import jasima.core.expExecution.ExperimentFuture;
import jasima.core.experiment.Experiment.ExperimentEvent;
import jasima.core.random.RandomFactory;
import jasima.core.run.ConsoleRunner;
import jasima.core.util.ConsolePrinter;
import jasima.core.util.TypeUtil;
import jasima.core.util.Util;
import jasima.core.util.observer.Notifier;
import jasima.core.util.observer.NotifierAdapter;
import jasima.core.util.observer.NotifierListener;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

/**
 * 

* An Experiment is something that produces results depending on various * parameters. The usual lifecycle is to create an experiment, set parameters to * their proper values, execute the experiment by calling it's * {@code runExperiment()} method. After execution a set of results are * available using the {@code getResults()} method. *

*

* Experiments are not supposed to be executed more than once, e.g., to run * multiple replications of an experiment (see * {@link MultipleReplicationExperiment}) you have to create multiple instances. * Therefore experiments should be cloneable. *

*

* This class is intended as the base class for Experiments doing something * useful. Besides a name this class only has a single parameter "initialSeed" * (see {@link #getInitialSeed()}/{@link #setInitialSeed(long)}). This parameter * is supposed to be used as the starting value for all (pseudo) random number * generation activities at experiment runtime. This means two experiments * having the same {@code initialSeed} and all other experiment parameters being * the same should behave deterministically and produce exactly the same * results. *

*

* The only results produced by this class are "runTime" (type Double; measuring * the real time required to execute an experiment) and "expAborted" (type * Integer, a value >0 indicates some problems causing early termination). *

*

* Experiments can have listeners registered (derived from * {@link ExperimentListenerBase}), which are informed of various events such as * an experiment's start and completion and can be used by subclasses to provide * additional events. *

* * @author Torsten Hildebrandt * @version * "$Id: Experiment.java 73 2013-01-08 17:16:19Z [email protected]$" */ public abstract class Experiment implements Cloneable, Serializable, Notifier { private static final long serialVersionUID = -5981694222402234985L; public static final String RUNTIME = "runTime"; public static final String EXP_ABORTED = "expAborted"; public static final String EXCEPTION = "exception"; public static final String EXCEPTION_MESSAGE = "exceptionMessage"; /** * Simple base class for events used by the notification mechanism. */ public static class ExperimentEvent { } public static final ExperimentEvent EXPERIMENT_STARTING = new ExperimentEvent(); public static final ExperimentEvent EXPERIMENT_INITIALIZED = new ExperimentEvent(); public static final ExperimentEvent EXPERIMENT_BEFORE_RUN = new ExperimentEvent(); public static final ExperimentEvent EXPERIMENT_AFTER_RUN = new ExperimentEvent(); public static final ExperimentEvent EXPERIMENT_DONE = new ExperimentEvent(); public static final ExperimentEvent EXPERIMENT_COLLECT_RESULTS = new ExperimentEvent(); public static final ExperimentEvent EXPERIMENT_FINISHING = new ExperimentEvent(); public static final ExperimentEvent EXPERIMENT_FINISHED = new ExperimentEvent(); /** * Enum for the category of a message produced by an experiment. */ public static enum ExpMsgCategory { OFF, ERROR, WARN, INFO, DEBUG, TRACE, ALL; } /** * Class to store print messages of an experiment. */ public static class ExpPrintEvent extends ExperimentEvent { public final Experiment exp; public final ExpMsgCategory category; private String message; private String messageFormatString; private Object[] params; public ExpPrintEvent(Experiment exp, ExpMsgCategory category, String message) { super(); if (message == null) throw new NullPointerException(); this.exp = exp; this.category = category; this.message = message; } public ExpPrintEvent(Experiment exp, ExpMsgCategory category, String messageFormatString, Object... params) { super(); this.exp = exp; this.category = category; this.messageFormatString = messageFormatString; this.params = params; this.message = null; } /** * Returns this message formatted as a {@code String} using the default * {@link Locale}. * * @return The formatted message using the default {@code Locale}. * @see Util#DEF_LOCALE */ public String getMessage() { return getMessage(Util.DEF_LOCALE); } /** * Returns this message formatted using the given {@link Locale}. * * @param locale * The {@link Locale} to use when formatting the message. * @return The formatted message. */ public String getMessage(Locale locale) { // lazy creation of message only when needed if (message == null) { message = String.format(locale, messageFormatString, params); messageFormatString = null; params = null; } return message; } @Override public String toString() { return getMessage(); } } // fields to store parameters private int nestingLevel = 0; private String name = null; private long initialSeed = 0xd23284FEA3L; // just an arbitrary default seed // fields used during run private long runTimeReal; protected int aborted; protected Map resultMap; /** * used during event notification and only temporarily contains a reference * to {@link #resultMap}. */ public Map results; public static class UniqueNamesCheckingHashMap extends LinkedHashMap { private static final long serialVersionUID = -6783419937586790463L; @Override public Object put(String key, Object value) { if (containsKey(key)) throw new RuntimeException("Map already contains value '" + key + "'."); return super.put(key.intern(), value); } } public Experiment() { super(); } /** * This method is called to perform any initializations required before the * experiment is run. */ protected void init() { aborted = 0; } /** * This method is called immediately before {@link #performRun()}, but after * {@link #init()}. */ protected void beforeRun() { } /** * Contains the code to actually do something useful. This is the only * abstract method that sub-classes are required to implement. */ protected abstract void performRun(); /** * This method can be overridden to perform any required clean-up. It is * executed immediately after {@link #performRun()}, but before * {@link #produceResults()} and {@link #finish()}. */ protected void done() { } /** * Populates the result map {@link #resultMap} with values produced during * {@link #results} experiment execution. The implementation in Experiment * adds the two results {@value #RUNTIME} and {@value EXP_ABORTED}. */ protected void produceResults() { resultMap.put(RUNTIME, runTimeReal()); resultMap.put(EXP_ABORTED, aborted); } /** * This method gives experiments and listeners a chance to view/modify * results. It is called after {@link #produceResults()}. */ protected void finish() { } /** * Runs the experiment. This is the main method to call to execute an * experiment. Sub-classes normally don't have to overwrite this method but * create customized behavior by overriding on of the methods like * {@link #init()}, {@link #beforeRun()}, {@link #performRun()} (this one is * required), {@link #done()}, {@link #produceResults()} or * {@link #finish()}. * * @return The results of experiment execution. */ public Map runExperiment() { try { runTimeReal = System.currentTimeMillis(); if (numListener() > 0) fire(EXPERIMENT_STARTING); init(); if (numListener() > 0) fire(EXPERIMENT_INITIALIZED); beforeRun(); if (numListener() > 0) fire(EXPERIMENT_BEFORE_RUN); performRun(); if (numListener() > 0) fire(EXPERIMENT_AFTER_RUN); done(); if (numListener() > 0) fire(EXPERIMENT_DONE); } finally { runTimeReal = System.currentTimeMillis() - runTimeReal; } // build result map resultMap = new UniqueNamesCheckingHashMap(); produceResults(); if (numListener() > 0) { results = resultMap; fire(EXPERIMENT_COLLECT_RESULTS); results = null; } // give experiments and listener a chance to view/modify results finish(); if (numListener() > 0) { results = resultMap; fire(EXPERIMENT_FINISHING); results = null; } // we are done, don't change results any more resultMap = Collections.unmodifiableMap(resultMap); if (numListener() > 0) fire(EXPERIMENT_FINISHED); // return results return getResults(); } /** * Returns the result map produced when executing this experiment. * * @return This experiment's results. */ public final Map getResults() { return resultMap; } /** * Returns the run time (in seconds) of an Experiment. The returned value is * only valid after calling {@link #runExperiment()} and measures the time * between calling {@link #init()} and the completion of {@link #done()}. * * @return The real time (wall time in seconds) it took to run the * experiment. */ protected double runTimeReal() { return (runTimeReal / 1000.0d); } /** * This is a convenience method to run a sub experiment without having to * worry about {@code ExperimentExecutor} and {@code nestingLevel}. * * @param sub * The sub-experiment to run. * @return An {@link ExperimentFuture} to access results. */ protected ExperimentFuture executeSubExperiment(Experiment sub) { sub.nestingLevel(nestingLevel() + 1); return ExperimentExecutor.getExecutor().runExperiment(sub, this); } /** * Retrieves a map containing the name and current value for each of this * class's properties. * * @return A map of all Java Bean properties and their values. */ public Map getPropsWithValues() { Map props = new LinkedHashMap(); PropertyDescriptor[] pds = TypeUtil.findWritableProperties(this); for (PropertyDescriptor pd : pds) { try { props.put(pd.getName(), pd.getReadMethod().invoke(this)); } catch (Exception e) { throw new RuntimeException(pd.getName(), e); } } return props; } /** * Triggers a print event of category "info". * * @param message * The message to print. * @see #print(ExpMsgCategory, String) */ public void print(String message) { print(ExpMsgCategory.INFO, message); } /** * Triggers a print event of the given category. If an appropriate listener * is installed, this should produce an output of {@code message}. * * @param category * Category of the message. * @param message * The message to print. * @see ConsolePrinter */ public void print(ExpMsgCategory category, String message) { if (numListener() > 0) { fire(new ExpPrintEvent(this, category, message)); } } /** * Triggers a print event of the given category. If an appropriate listener * is installed, this should produce a message created by the given format * string and parameters. * * @param category * Category of the message. * @param messageFormat * Format string for the message to produce. * @param params * Parameters to use in the format string. */ public void print(ExpMsgCategory category, String messageFormat, Object... params) { if (numListener() > 0) { fire(new ExpPrintEvent(this, category, messageFormat, params)); } } /** * Same as {@link #print(ExpMsgCategory, String, Object...)}, just * defaulting to the category {@code INFO}. * * @param messageFormat * The format String to use. * @param params * Parameters to use when formatting the message. */ public void print(String messageFormat, Object... params) { print(ExpMsgCategory.INFO, messageFormat, params); } /** * Prints the results of this experiment to {@link System#out}. */ public final void printResults() { ConsolePrinter.printResults(this, getResults()); } public String toString() { return getName() == null ? super.toString() : getName(); } @Override public Experiment clone() throws CloneNotSupportedException { Experiment c = (Experiment) super.clone(); if (adapter != null) { c.adapter = adapter.clone(); c.adapter.setNotifier(c); } return c; } /** * This is the same as {@code clone()}, just without throwing the checked * exception {@code CloneNotSupportedException}. If such an exception * occurs, it is wrapped in a {@code RuntimeException}. * * @return A clone of this experiment. */ public Experiment silentClone() { try { return clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } /** * Sets the nesting level. This method is only for internal purposes. * * @param nestingLevel * The nesting level for this experiment. */ public void nestingLevel(int nestingLevel) { this.nestingLevel = nestingLevel; } /** * The level in the call hierarchy this experiment is executed in. * Experiments that spawn new sub-experiments (like * {@link MultipleReplicationExperiment}) are required to increase their * children's nestingLevel by 1. If * {@link #executeSubExperiment(Experiment)} is used, then this is set * automatically to the correct value. * * @return This experiment's nesting level. */ public int nestingLevel() { return nestingLevel; } /** * Set some descriptive name for this experiment. * * @param name * The name of the experiment. */ public void setName(String name) { this.name = name; } public String getName() { return name; } public long getInitialSeed() { return initialSeed; } /** * Sets the initial seed for this experiment. If an experiment makes use of * random influences, they should all and solely depend on this value. * * @see RandomFactory * * @param initialSeed * The initial seed to use. */ public void setInitialSeed(long initialSeed) { this.initialSeed = initialSeed; } // // // event notification // // private NotifierAdapter adapter = null; @Override public void addNotifierListener( NotifierListener listener) { if (adapter == null) adapter = new NotifierAdapter(this); adapter.addNotifierListener(listener); } @Override public NotifierListener getNotifierListener( int index) { return adapter.getNotifierListener(index); } @Override public void removeNotifierListener( NotifierListener listener) { adapter.removeNotifierListener(listener); } protected void fire(ExperimentEvent event) { if (adapter != null) adapter.fire(event); } @Override public int numListener() { return adapter == null ? 0 : adapter.numListener(); } // ******************* static methods ************************ public static void main(String[] args) throws Exception { // create instance of the Experiment sub-class that was specified as // Java's main class Class klazz = TypeUtil.getMainClass(); Experiment e = (Experiment) klazz.newInstance(); // parse command line arguments and run new ConsoleRunner(e).parseArgs(args).run(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy