Skip to content
This repository has been archived by the owner on May 11, 2020. It is now read-only.

UserGuide

Roland Ewald edited this page May 10, 2020 · 4 revisions

SESSL User Guide

The Basics

SESSL comes as a set of jar files, which you need to include into the classpath of your project. Most of the time, you will just need two of them: the first is the sessl-X.Y.jar file, which contains the core of SESSL. As a second file, you need to include a so-called binding that lets you use SESSL with a specific simulation system. The following examples rely on the SESSL binding for JAMES II, which is provided by the sessl-james-X.Y.jar file.

To conveniently use SESSL constructs, it makes sense to at first import both the major SESSL language elements (package sessl) and the binding (a package called sessl.simsystem, e.g. sessl.james). (This is not considered best practice; however, it may make learning SESSL easier, as this imports all relevant SESSL entities.)

Hence, you could start out as follows:

import sessl._
import sessl.james._
//...

SESSL experiments are run by invoking the predefined execute function. This function takes one (or more) Experiment objects and triggers their execution. If only a single experiment shall be executed, Scala permits to call execute with curly braces. Otherwise, parentheses have to be used:

import sessl._
import sessl.james._

//Call single experiment:

execute{
  new Experiment {
    //.. your configuration goes here
  }
}

//Call multiple experiments:

val exp1 = new Experiment {
  //.. your configuration
}
val exp2 = new Experiment {
  //.. your configuration
}

execute(exp1,exp2)

A Simple Experiment

Experiments are specified within a new Experiment block. (Technically speaking, you instantiate an anonymous sub-class of the Experiment class, which is provided by the binding you use, and your specification statements are its constructor.) A simple experiment would look like this:

import sessl._
import sessl.james._

execute{
  new Experiment {
    model = "./model.sr"
    stopTime = 100
    replications = 2
  }
}

This experiment simulates the model stored in file model.sr (in the working directory), and stops after a simulation time of 100 time units (e.g., seconds or hours, depending on the time scale of the simulation). After the first run has finished, the simulation is repeated once more, i.e. 2 replications are conducted.

Replication and Stop Conditions

Of course, more complex conditions are sometimes needed to determine when to stop a simulation or when to stop replicating. To do so, one may specify the stopCondition and replicationCondition, respectively. Depending on the possible stop conditions supported by your simulation system, stopping conditions could be specified as follows:

import sessl._
import sessl.james._

execute{
  new Experiment {
    
    //...
    
    //Stop after 100 time units simulation time:
    stopCondition = AfterSimTime(100)
    
    //Stop after either 10000 simulation steps or 3s wall-clock time have passed
    stopCondition = AfterSimSteps(10000) or AfterWallClockTime(seconds = 3)
    
    //Stop only after (at least) 10000 steps have been simulated and (at least) 3s wall-clock time passed
    stopCondition = AfterSimSteps(10000) and AfterWallClockTime(seconds = 3)
    
    //... and so on:
    stopCondition = (AfterSimSteps(10000) and AfterSimTime(100)) or AfterWallClockTime(seconds = 3)
  }
}

The logical expressions for stop conditions can be arbitrarily complex, but may only be set once in an experiment (i.e., the above example is not valid, as stopCondition is assigned four times). Note that and and or are functions, i.e. operator precendence would not be as expected in the last example without putting the expression with the and function into parentheses. Therefore, it is advisable to use parentheses for all more complex conditions that are joined with and and or. The configuration of replicationCondition works similarly.

Specifying Model Parameters

In case you want to set a model parameter (e.g. x) to a fixed value for the whole experiment (e.g. 42), you can do so as follows:

new Experiment {
  //...
  set("x" <~ 42)
}

The function scan is used to specify parameter scans. It can be called multiple times, each time specifying a parameter name and a sequence of values over which this model parameter shall be iterated. For example, the following code specifies that three different model setups shall be simulated in the experiment, one with y set to 2, one with y set to 3, and one with y set to 4:

//...
new Experiment {
  //...
  scan("y" <~ (2,3,4))
}

Of course, multiple parameters can be given:

scan("x" <~ range(1,1,10), "y" <~ (2,4,7))

The function range(from,stepsize,to) generates a range of values. For example, range(1,1,10) generates the values 1, 2, ..., 10. It is very similar to the built-in Range object in Scala, which can be used as well (beware of the differing parameter order, i.e. range(1,1,10) is equivalent to Range(1,10,1)).

The example above also declares values for the variable y. Hence, it defines a full-factorial experiment on both variables, i.e. each possible combination of x and y values are simulated:

  • x = 1, y = 2
  • x = 1, y = 4
  • x = 1, y = 7
  • x = 2, y = 2
  • ...
  • x = 10, y = 7

Instead of generating all possible combinations of variable values, the following call to scan uses and to restrict the y and z value combinations to single pairs, determined by the order of the values:

scan("x" <~ Range(1,1,10), "y" <~ (2,4,7) and "z" <~ ("one", "two", "three"))

It therefore generates the following value combinations:

  • x = 1, y = 2, z = "one"
  • x = 1, y = 4, z = "two"
  • x = 1, y = 7, z = "three"
  • x = 2, y = 2, z = "one"
  • ...
  • x = 10, y = 7, z = "three"

Note that multiple variables can be combined via and, and that scan can also be called multiple times. Additional calls to scan are interpreted as full-factorial combinations; for example

scan("x" <~ Range(1,1,10), "y" <~ (2,4,7) and "z" <~ ("one", "two", "three"))
scan("a" <~ Range(10,10,100))

generates these combinations (10 * 3 * 10 = 300 overall):

  • x = 1, y = 2, z = "one", a = 10
  • x = 1, y = 2, z = "one", a = 20
  • ...
  • x = 1, y = 2, z = "one", a = 100
  • x = 1, y = 4, z = "two", a = 10
  • ...
  • x = 1, y = 7, z = "three", a = 100
  • x = 2, y = 2, z = "one", a = 10
  • ...
  • x = 10, y = 7, z = "three", a = 100

Strings, integers, and floating point numbers are supported as variable types.

Event Handling

Since both the number of value combinations (also called variable assignments in SESSL) and the number of replications can be quite large, SESSL allows you to react whenever a simulation run is finished, or the replications to be simulated for a certain variable assignment are finished, or the whole experiment is finished.

Event handling code can be added as follows:

new Experiment {
  afterRun {
    result => println("Run with ID " + result.id + " is finished.")
  }
  afterReplications {
    result => println("Replications with ID " + result.id + " are finished.")
  }
  afterExperiment {
    result => println("Experiment finished.")
  }
}

The above event handlers take arbitrary functions to react on each of the events, and also lend access to the current results (the results before the => refers to the single argument of these function literals). Arbitrarily many event handlers can be specified. Please note that the IDs of individual simulation runs are specific to the given simulation system, which allows to link the results back to other output the simulation system may produce.

Also note that the result objects are nested, e.g. it is possible to access run and replication results via the result object passed into an afterExperiment event handling function (see section on reporting).

Observing Simulation Output

Simulation output is not automatically collected, as this may severely slow down the execution and is not always necessary. To observe any output, you need to use Scala traits (which are mixins). This mechanism is used in SESSL whenever a certain simulation experiment aspect should be configured, i.e. a certain facet of the experiment execution that is not always relevant. The SESSL traits are provided by the binding to the simulation system, so that a system (or its SESSL binding) does not need to offer support for all aspects of simulation systems. In JAMES II, for example, Observation currently only works for Species-Reaction models.

Configuring a simulation experiment to observe certain variables can be done as follows:

new Experiment with Observation {
  model = "mymodel.sr"
  stopTime = 1.0
  replications = 10
  scan("parameter" <~ (10, 15))
  observe("output1", "output2")
  observeAt(range(.0, .1, .9))
}

This experiment simulates the model in mymodel.sr twenty times (2 variable assignments (for parameter) * 10 replications = 20), until a simulation time of 1.0 is reached. The last two lines configure the observation, but this only compiles when Observation is mixed in (via with Observation in the first line).

The function observe defines that values from the model variables output1 and output2 shall be observed. The function observeAt defines at which points in simulation time the model variables shall be observed. Here, you can either use the range function to observe data at equidistant time points, or you call it with a list of explicit time points, like

observeAt(.0, .2, .5, .9)

which would observe only four instead of ten variable values. In the original experiment, ten values are observed for each of the two variables and for each of the twenty runs, i.e. overall 10 * 2 * 20 = 400 data points are observed.

To access the results easier, SESSL allows you to bind the output to new names used throughout the experiment. For example,

observe("x" ~ "output1")

observes the model variable output1 and allows to access it as x in the event handlers (but not as output1 anymore), as shown below:

new Experiment with Observation {
  //...
  observe("x" ~ "output1")
  //...
  withRunResult {
    result => {
      println("Last x-value:" + result("x"))
      println("Trajectory of x-values" + result~"x")
    }
  }
  withReplicationsResult {
    result => println("Last x-values of all replications:" + result("x"))
  }
  withExperimentResult {
    result => {
      println("Overall variance of last x-values:" + result.variance("x"))
      println("Variance of last x-values for all replications where" +
              "model parameter 'parameter' had been set to 10:" + 
              result.having("parameter" <~ 10).variance("x"))
    }
  }
}

Note that the names of these event handlers are different from those discussed in a prior section (afterRun(...) and so on); this is because experiment aspects that yield some data (like Observation) may define their own event handlers. This allows them to pass specific result objects into the event handlers, which provide helper functions and thus simplify the access to the recorded data (as in the sample code above).

Result Storage

If the data shall not only be observed but also stored, you can use the DataSink trait to configure this:

execute {
  new Experiment with Observation with DataSink {
    //...
    dataSink = MySQLDataSink(schema = "myresults")
  }
}

The trait DataSink allows to configure a data sink via dataSink = ..., but which kind of data sink is available depends on your simulation system. In the above example, the JAMES II data sink for MySQL is configured to write the data to a schema myresults; user name, password, and server URL are not given and hence set to default values.

If result storage is not yet supported by the SESSL binding you are using, and you need relatively few data points (<< 1 GB), you may consider using result reporting instead.

Reporting

Result reporting is currently only supported by the SESSL binding for JAMES II, but fortunately this can be mixed into any other SESSL experiment.

The following code sample shows the usage of the Report trait:

new Experiment with Observation with Report {
  //...
  observe("x" ~ "S0", "y" ~ "S1")
  observeAt(range(0.0, 0.05, 0.5))

  //Generate report: 
  reportName = "My SESSL Sample Report"
  reportDescription = "This was generated by the James II 'Report' trait in SESSL."

  withRunResult {
    results => {
      reportSection("From run " + results.id) {
        linePlot(results ~ ("x"), results ~ ("y"))(title = "Trajectories of x and y.")
      }
    }
  }

  withExperimentResult {
    results => {
      reportSection("My Sample Section") {
        reportSection("A Subsection") {
          scatterPlot(results ~ ("x"), 
            results ~ ("y"))(yLabel = "sessl-label for y-axis", caption = "This is a sessl figure.")
        }
        reportSection("Another Subsection") {
          histogram(results ~ ("x"))(title = "A histogram.")
          boxPlot(results ~ ("x"), results ~ ("y"))(title = "A boxplot (with named variables).")
          boxPlot(results("x"), results("y"))(title = "Another boxplot (without names).")
          reportStatisticalTest(results ~ ("x"), results ~ ("y"))()
          reportTable(results ~ ("x"), results ~ ("y"))(caption = "This is a table.")
        }
      }
    }
  }
}

Report generation in JAMES II relies on the R programming language, LaTeX, and Sweave: a directory with the name reportName will be created, and the report will be written to an Rtex file. This file contains the LaTeX document and the R code to create the desired plots. The raw data is written into csv files within a separate folder raw. As seen above, the function reportSection can be used to structure the report, and additional functions like histogram or boxPlot are used to generate plots.

To compile the report with Sweave, execute the following R command:

Sweave("path/to/report.Rtex")

This should produce pdf files for figures and a tex file for the report.

Simulation-based Optimization

There is a generic interface for optimization, located in package sessl.optimization. Currently, only Opt4J is supported. Minimization, maximization, and multi-objective experiments can be specified. Here is a small sample experiment that maximizes the mean of species S0 at simulation time 0.8 by adapting two parameters, p and n:

import sessl._
import sessl.optimization._
 
//Bindings: we use JAMES II for simulation and opt4J for optimization
import sessl.james._
import sessl.opt4j._
 
maximize { (params, objective) =>
  execute{
    new Experiment with Observation with ParallelExecution {
      model = "java://examples.sr.LinearChainSystem"
      set("propensity" <~ params("p")) //Set model parameters as defined by optimizer
      set("numOfInitialParticles" <~ params("n"))
      stopTime = 1.0
      replications = 10
      observe("S0", "S1")
      observeAt(0.8)
      withReplicationsResult(results => {
        objective <~ results.mean("S0") //Store value of objective function
      })
    }
  }
} using (new Opt4JSetup {
  param("p", 1, 1, 15) // Optimization parameter bounds
  param("n", 10000, 100, 15000)
  optimizer = EvolutionaryAlgorithm(generations = 30, alpha = 10) //Configure optimizer
})

Parallel Execution, Algorithm Configuration, Runtime Performance Analysis ...

This documentation is not complete and will (hopefully) grow over time. In any case, more examples can be found in the sample experiments repository and the test code of the bindings (see packages tests.sessl.*).