-
Notifications
You must be signed in to change notification settings - Fork 0
UserGuide
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)
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.
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.
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.
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).
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).
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.
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.
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
})
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.*
).