Web Tutorial 1: Parameterize Your Simulation

Author: Gabriel Catalin Balan, with some additions by Sean Luke

An oft-heard request is how to write MASON simualtions which read from parameters specified on-disk. There are lots of ways to do this; depending on one's needs. For the most simple applications, Java's properties class (java.util.Properties) is often enough since it provides the framework for reading/writing/saving/loading String-based parameters and values.

In this tutorial we'll use a subclass of Properties called ParameterDatabase, partly because we know it well (Sean Luke wrote it for the ECJ evolutionary computation toolkit) and partly because it provides a number of advanced features over the simple Properties class. ParameterDatabase can load from a hierarchy of parameter files; load strings, ints, doubles, longs, file pointers, and even create arbitrary instances constructed dynamically by the class defined in the file. This is a lot more than Properties does. But we will only show a very simple situation.

In the simplest situation, a single set of global model parameters will be loaded from a file. This is the scenario we will cover in this tutorial, and the example we will use is loading parameters into HeatBugs. More sophisticated situations may require that each separate agent have its own separate set of parameters, or other conditions where one global set of parameters is not sufficient. For example, you may wish to load unique parameters for every HeatBug. We have a methodology we use for this (it's how ECJ works -- every class in ECJ is defined by the parameter file) but will not cover it in this tutorial. But you should be able to hack something together once you understand the basic approach here.

This tutorial teaches:

Files in the Tutorial

Before starting, you need to install the ParameterDatabase facility. Place the following four files in MASON's ec/util/ directory:

You can also download all four of these files as parameterdatabase.zip.

The files you'll create in this tutorial will all be stored in the directory sim/app/webtutorial2 in MASON. If you want to just read the files rather than follow along and build them, they're here:

If you like, you can download all four of these files as source.zip

The parameter file

In sim/app/webtutorial2 create a file called heatbugs.params. This file will be used to store parameter - value pairs in the "java properties" format. Add the following lines to the file:


##
## Random Number Generator Seeed
##
seed = time
#(alternatively use seed = 12345)

##
## Grid Size
##
width = 100
height = 100

## number of bugs
bugs = 100

##
## Heat model
##
min-ideal-heat = 17000
max-ideal-heat = 31000
min-output-heat = 6000
max-output-heat = 10000
evaporation-rate  = .993
diffusion-rate = 1
random-move-probability = .1

NOTE: The equal sign is optional but it is really bad style to not include it. The parameter name (to the left of the equal sign) cannot contain spaces -- whitespace on either end is trimmed. The value (to the right of the equal sign) can contain spaces, but again, the leading and trailing spaces are trimmed out.

Empty lines are ignored. Lines starting with # are considered comments. Note that this is not the same as the Java-style line comment, where everything following // is comment until the end of the line.

Parameterize Heatbugs

We'll begin by loading the parameters "the hard way", namely, loading each parameter separately and setting values based on them. Later in the tutorial we'll show a nifty approach to loading all properties at once using Java Beans.

In the directory sim/app add a new directory called webtutorial2 and in this directory, create a class called ParameterizedHeatBugs.java Add the following code to the file:


package sim.app.webtutorial2;
import ec.util.*;
import java.io.*;
import sim.app.heatbugs.*;

public /*strictfp*/ class ParameterizedHeatBugs extends HeatBugs
    {
    public static final String P_SEED =           "seed";
    public static final String P_WIDTH =          "width";
    public static final String P_HEIGHT =         "height";
    public static final String P_BUG_COUNT =      "bugs";
    public static final String P_MIN_IDEAL_HEAT = "min-ideal-heat";
    public static final String P_MAX_IDEAL_HEAT = "max-ideal-heat";
    public static final String P_MIN_OUT_HEAT =   "min-output-heat";
    public static final String P_MAX_OUT_HEAT =   "max-output-heat";
    public static final String P_EVAPORATION =    "evaporation-rate";
    public static final String P_DIFFUSION =      "diffusion-rate";
    public static final String P_RAND_PROB =      "random-move-probability";

Notice that these are equivalent to the parameter names we put in the params file.

Next, we'll create a function which loads the parameters from the parameter database. This function is quite long; mostly because we're loading each parameter manually to show how to do it. But as we'll soon see, there's an easier way to do it.

The parameter database has a variety of get... methods. We'll call one each to extract the parameters.


    ParameterDatabase paramDB;
    
    void loadParams()
        {
        if(paramDB==null)
            return;
        Parameter param;
        
        long seed = paramDB.getLong(param = new Parameter(P_SEED));
        random.setSeed(seed);

        gridWidth = paramDB.getInt(param = new Parameter(P_WIDTH),null, 1);
        if(gridWidth<1)
            throw new RuntimeException("Invalid value for "+param);
        gridHeight = paramDB.getInt(param = new Parameter(P_HEIGHT),null, 1);
        if(gridHeight<1)
            throw new RuntimeException("Invalid value for "+param);
            
        bugCount = paramDB.getInt(param = new Parameter(P_BUG_COUNT),null, 0);
        if(bugCount<0)
            throw new RuntimeException("Invalid value for "+param);
                
        randomMovementProbability = paramDB.getDouble(param = new Parameter(P_RAND_PROB), null, 0, 1);
        if(randomMovementProbability<0)
            throw new RuntimeException("Invalid value for "+param);
        
        evaporationRate = paramDB.getDouble(param = new Parameter(P_EVAPORATION), null, 0, 1);
        if(evaporationRate<0)
            throw new RuntimeException("Invalid value for "+param);
    
        diffusionRate = paramDB.getDouble(param = new Parameter(P_DIFFUSION), null, 0, 1);
        if(diffusionRate<0)
            throw new RuntimeException("Invalid value for "+param);

        minIdealTemp = paramDB.getDouble(param = new Parameter(P_MIN_IDEAL_HEAT), null, 0);
        if(minIdealTemp<0)
            throw new RuntimeException("Invalid value for "+param);        
        maxIdealTemp = paramDB.getDouble(param = new Parameter(P_MAX_IDEAL_HEAT), null, minIdealTemp);
        if(maxIdealTemp<minIdealTemp)
            throw new RuntimeException("Invalid value for "+param);
        minOutputHeat = paramDB.getDouble(param = new Parameter(P_MIN_OUT_HEAT), null, 0);
        if(minOutputHeat<0)
            throw new RuntimeException("Invalid value for "+param);        
        maxOutputHeat = paramDB.getDouble(param = new Parameter(P_MAX_OUT_HEAT), null, minOutputHeat);
        if(maxOutputHeat<minOutputHeat)
            throw new RuntimeException("Invalid value for "+param);        
        }

NOTE: Some explanations are in order here. First, the property-value retrieval mechanism was designed to consider some default parameter in case the main parameter was not found in the data base. This explains the null following param = new Parameter(...) in most of the above calls.

Second, most methods for retrieving numerical values in the parameter data base require lower bounds. Providing upper bounds is also possible. When the value is outside the provided bounds, these mehtods return a value 1 unit smaller than the smallest acceptable value. This is the explanation for the last argument(s) in getInt, getDouble, etc., and also for the tests leading to throwing a RuntimeExeption.

Create a public contructor and also overwrite the start method to call loadParams:


    public ParameterizedHeatBugs(ParameterDatabase paramDB)
        {
        super(1);//some bogus seed, we'll use the right one as soon as we load the parmaters from DB
        this.paramDB = paramDB;        
        loadParams();
        createGrids();
        }
    
    public void start()
        {
        loadParams();
        super.start();
        }
Now have the parameter data base loaded from a file in the main method:

    public static void main(String[] args)
        {
        ParameterizedHeatBugs heatbugs = null;
        ParameterDatabase parameters = null; 
        for(int x=0;x<args.length-1;x++)
            if (args[x].equals("-file"))
                {
                try
                    {
                    parameters=new ParameterDatabase(
                            new File(args[x+1]).getAbsoluteFile(),
                            args);
                    }catch(IOException ex){ex.printStackTrace();}
                break;
                }
        
        heatbugs = new ParameterizedHeatBugs(parameters); 
        heatbugs.start();
        System.out.println("Starting HeatBugs.  Running for 500 steps.");
        long time;
        while((time = heatbugs.schedule.time()) < 500)
            {
            if (!heatbugs.schedule.step(heatbugs)) break;
            if (time%100==0 && time!=0)            System.out.println("Time Step " + time);
            }
        heatbugs.finish();
        }

Save the file and compile the java file. Run java sim.app.webtutorial2.ParameterizedHeatBugs -file heatbugs.params.

Notice that if you omit the -file heatbugs.params, the default model values are used instead.

Other Basic Issues

Read Strings-Valued Parameters

So far we had only numerical values. As an application for reading strings, let us extend the random number generator (RNG) seeding mechanism: if the value of the seed property is time, the simulation's RNG will be seeded using the clock.

Add the following line to ParameterizedHeatBugs.java

    public static final String V_TIME =         "time";
Inside the loadParams method change:

FROM...CHANGE TO

long seed = paramDB.getLong(param = new Parameter(P_SEED));
random.setSeed(seed);

long seed;
String s = paramDB.getString(param = new Parameter(P_SEED), null);
if(s.equalsIgnoreCase(V_TIME))
    seed = System.currentTimeMillis();
else
    seed = paramDB.getLong(param);

random.setSeed(seed);

Specify Parameters on the Command-Line or in Multiple Files

Notice that when the parameter database is constructed, it is done so by passing in the command line arguments (args). The reason for this is because the parameter database can also load parameters from the command line. For example, you can change the width to 9 by adding to the command line: -p width=9

Command-line arguments supercede arguments loaded from files.

Parameter files can have multiple parent files. Parent files are specified with the parameter names parent.0 , parent.1 , ... etc. For example, a file might say parent.0=../parent.params indicating that its parent file is called parent.params and is located in the parent directory relative to the main parameter file. Parent files themselves can have parent files, etc., creating a tree of files. If the same parameter name appears in multiple files, here's how conflicts are resolved. Children take precedence over parents or ancestors. Earlier-numbered ancesters take precedence over later ancesters (for example, files traced through parent.0 take precedence over files traced through, say, parent.4 of a given child file).

For example, consider the parameter files shown at left. Each of them has defined some parent files. Parameters added to the ParameterDatabase programmatically take precedence; then command-line paramters; then parameters in the root parameter file (in this example, "prog.params"), then its left parent ("foo.params"), then "bar.params", then "baz.params", then "quux.params", then "quuux.params", and finally "quuuux.params".

Print Parameter Values as They are Loaded

You can simply print out parameters in real time as they are being loaded. This is done by adding a parameter print-params=true and the Parameter database will print out parameters as they are requested. You can also dump parameters in different configurations at the end of the simulation. For example, to dump those parameters which were used at least onece, insert the following piece of code at the end of the main method:


        if(parameters != null)
            {
            PrintWriter pw = new PrintWriter(System.out, true); 
            parameters.listGotten(pw);
            }

NOTE: If you want to print into a file, here's the way java documentation says you should make the PrintWriter:
    PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));

See the ParameterDatabase documentation for other dumping options.

Alternative parametrization style

The loadParams can be considerably simplified if we connect the parameters (java.util.Properties) with the getters and setters we wrote for an inspectable model (sim.util.Properties). Instead of defining constants for the parameter names, simply use the names of the properties you have setters and getters for.

Copy ParameterizedHeatBugs.java into ParameterizedHeatBugs2.java and heatbugs.params into heatbugs2.params. In the ParameterizedHeatBugs2.java file change all references to ParameterizedHeatBugs to ParameterizedHeatBugs2. Make the following change to the loadParams method:

FROM...CHANGE TO

random.setSeed(seed);

gridWidth = paramDB.getInt(param = new Parameter(P_WIDTH),null,1);
...
if(maxOutputHeat<minOutputHeat)
    throw new RuntimeException("Invalid value for "+param);        

random.setSeed(seed);
        
Properties p = Properties.getProperties(this,false,true,false);
for(int i=0; i< p.numProperties(); i++)
    if (p.isReadWrite(i) && 
        !p.isComposite(i) && 
        paramDB.exists(new Parameter(p.getName(i))))
        {
        Parameter pa = new Parameter(p.getName(i));
        String value = paramDB.getString(pa,null);
        p.setValue(i,value);
        }
The content of heatbugs2.params should also be updated:

FROM...CHANGE TO

##
## Random Number Generator Seeed
##
seed = time
#(alternatively use seed = 12345)

##
## Grid Size
##
width = 100
height = 100

## number of bugs
bugs = 100

##
## Heat model
##
min-ideal-heat = 17000
max-ideal-heat = 31000
min-output-heat = 6000
max-output-heat = 10000
evaporation-rate  = .993
diffusion-rate = 1
random-move-probability = .1

##
## Random Number Generator Seeed
##
seed = time
#(alternatively use seed = 12345)

##
## Grid Size
##        
GridWidth = 100
GridHeight = 100

## number of bugs
BugCount = 100

##
## Heat model
##
MinimumIdealTemperature = 17000
MaximumIdealTemperature = 31000
MinimumOutputHeat = 6000
MaximumOutputHeat = 10000
EvaporationConstant  = .993
DiffusionConstant = 1
RandomMovementProbability = .1

To run this, use java sim.app.webtutorial2.ParameterizedHeatBugs2 -file heatbugs2.params.

NOTE:
  1. The parameter names are case sensitive.
  2. If you use this example verbatim, you need to have both the setter and a getter defined
  3. The range checks done in the first version of loadParams should be done in the setters.