Skip to main content

Scripted simulation

This page describes groovy script functionalities in Trenissimo's simulations. The goal of the scripts is to provide the ability to execute one or more simulations with different combinations of timetable, infrastructure models, rolling stock, and settings, or to perform operations on the timetables, for example, train filtering, copying, path shortening, trainset changes, assignment of new delay and performance distributions. Aside from the output files created by the single simulations, it is also possible to create ad hoc output files, compare the results between the simulation results, create X-Y charts for results and input variables comparison, make aggregated statistics, or compare various simulation scenarios.

To be able to execute a script, firstly you need to create a "Simulation Script" project, inside of which you will find a sim.script file. The only settings of a "Simulation Script" project are the references to the project "Macro" to be used, and the unit of measurement for possible outputs. All the other simulation characteristics can be specified in the script. In order to be consistent with the other types of simulation, the base settings (Initial date, initial time, duration, simulation area, dispatching, etc) are available in the Properties panel as well.

A single "Simulation Script" can contain many different scripts, you can create other ones by right-clicking on the node Script files, New, Simulation script. Normally, the default script executed by the "Run" action is the sim.script. By right-clicking another script, and then on "Set as main script" it is possible to change the default script.

Quick start

Create the project, open the default script, modify it, build, and run

How it works

Script structure

The script is in completely free-from, and the developers can use any class defined in the Trenissimo model, in Java Development Kit, and in Groovy Development Kit. The documentation on Groovy is available on the Apache Groovy site.

Grape

Grape is a dependency manager embedded in Groovy. Grape lets you quickly add dependencies to your projects, making scripting even easier. The simplest use is as simple as adding an annotation to your script:

@Grab('com.xlson.groovycsv:groovycsv:1.3')
import static com.xlson.groovycsv.CsvParser.parseCsv

You can find the full documentation of Grape on the Grape website.

Basic functions

Some functions implemented in Ant scripts can be directly called from Groovy.

RollingStock loadRollingStock(String rollingStockDir) // loads a RollingStock project

Timetable loadTimetable(String timetableDir, RollingStock) // loads a Timetable project, with its relative distributions, if present

Micro loadMicro(String microDir) // load a Micro model

Micro loadMicroFiltered(String microDir, Timetable) // load a Micro model, filtered on the area covered by the specified timetable

Amendments loadAmendments(String amendmentsFile) // amendmentsFile is the name of the file to load, without any extension

Incidents loadIncidents(String incidentsFile) // incidentsFile is the name of the file to load, without any extension

SimulationActualTimetable runSimulation(RollingStock, Timetable, Micro, [TimetableTransformations, SimulationTimetableTransformations, SimulationSettings, String simulationName, Collection additionalContext])

SimulationActualTimetable runDelayScenarioSimulation(String setKey, String runKey, RollingStock, Timetable, Micro, [TimetableTransformations, SimulationTimetableTransformations, SimulationSettings, String simulationName, Collection additionalContext])

SimulationActualTimetable runDelayPercentileSimulation(double percentile, RollingStock, Timetable, Micro, [TimetableTransformations, SimulationTimetableTransformations, SimulationSettings, String simulationName, Collection additionalContext])

SimulationResults runStochasticSimulation(int runs, RollingStock, Timetable, Micro, [TimetableTransformations, SimulationTimetableTransformations, SimulationSettings, DrivingPolicies, String simulationName, Collection additionalContext])

Other functions are implemented in Groovy. For a complete list, see Scripted simulations reference.

Execution phases

The same script is executed during the Build phase as well as during the Run phase of the simulation.

During the Build phase, the load* methods are verifying the inputs and preparing the working files; the run methods execute a minimal simulation, without output, useful in case this minimal simulation is useful to prepare the data for the next scenarios.

On the other hand, during the Run phase, the load* methods skip possible checks and only load the working files, and the run* methods execute the simulation with all the set of functionalities e configured output data.

Aside from the Build and Run phases, there are other actions corresponding to specific phases: Bundle, used by the Bundle action in the context menu of Scripted simulation projects; Clean, called during the project Clean, but only when requested by the plugins; Index, called to obtain a list of simulation scenarios, later used for the execution of distributed multi-scenario simulations. The developer can ignore these phases because they are handled automatically by the default scripts.

It is useful to specify which parts of the script should be executed during the Build phase and which parts during the Run phase. To do so, the following methods are used:

phase

The phase method executes the block only if the execution phase is the one specified. The possible phases are: clean, build, index, bundle and run. This method is needed only in some particular cases, which go beyond the scope of this guide.

phase(String phase) { block } 

build

The build method has different behaviors depending on the phase it is called in. During the Build phase, the block is executed, and the result is serialized like a working file. During the Run phase, the block is not executed, but the working file saved during the **Build phase is red and returned to the caller.

In this example, during the Build phase, the block is executed, and something gets serialized on disk. During the Run phase, on the other hand, the block is skipped, and the value serialized previously is assigned to the data variable.

def data = build { ... return something } 

run

The run method executes the block only during the Run phase. It is a shortcut for phase('run') { block }

run { block } 

The calls to the run* methods are thus inserted in a run block to make sure that they are not executed during the Build phase of the simulation. In [matrix] simulations (#simulazioni-matrix) this is handled automatically.

Other methods

TimetableTransformations timetableTransformations { block } // builder for the TimetableTransformations construction

MacroTrainMatcher trainMatcher ( String expression ) // creates a matcher starting from an expression, see note.

MacroTrainPathEntryMatcher pathEntryMatcher ( String expression ) // creates a matcher starting from an expression, see note.

Object runSimulations(String baseName) { block } // builder for the execution of complex simulation

File file(String relativePath) // input file, relative to the basePath, or rather the project folder of the Scripted simulation

List<File> files(String glob) // input files that correspond to a glob, relative to the basePath, or rather the project folder of the Scripted simulation. Use double-asterisk to consider the sub-folders as well.

void files(String glob, Closure c) // as above, but instead of returning a list, calls the closure for each file that was found

File outputFile(String relativePath) // creates an output file, relative to the output folder of the simulation

List<File> outputFiles(String glob) // output files that correspond to a glob, relative to the output folder of the simulation. Use double-asterisk to consider the sub-folders as well.

void withOutputFiles(String glob, Closure c) as above, calls the closure for each file that was found

PrintWriter printWriter(String relativePath) // print writer for an output file, the path is relative to the output folder of the simulation

CSVPrinter csvPrinter(String relativePath) // CSVPrinter for an output file, the path is relative to the output folder of the simulation

CSVPrinter csvPrinter(String relativePath, String...headers) // CSCSVPrinter for an output file, the path is relative to the output folder of the simulation

ExcelPrinter excelPrinter(String relativePath) // Excel Printer for an output file, the path is relative to the output folder of the simulation

ExcelPrinter excelPrinter(String relativePath, String...headers) // Excel Printer for an output file, the path is relative to the output folder of the simulation

The builder usage for the MacroTrainMatcher and MacroTrainPathEntryMatcher creation is an advanced functionality necessary only in particular cases; in all the other cases it is advised to use the expressions described in the reference

available variables

Simple scripts and matrix scripts

Tutorials

Deterministic simulations

👷👷🏾

Stochastic simulations

👷👷🏾

Delay scenario simulations

👷👷🏾

Delay percentile simulations

👷👷🏾

Snippets

Some recurring use cases are implemented the inside the .snippet files, importable in the script's body using the void snippet(snippetName) method. The snippets can receive some optional parameters, passed through named arguments, for example, snippet('AggregateOutputFiles', glob: '**/*.itinerary.csv', dest: 'itineraries.csv'). Other snippets define a slot, or rather a closure called inside of a script but defined in the script's body, like in the following example:

snippet('lib/SystemCriticality',
// snippet configuration
...
) {
// questo è lo slot, definito nello script ma eseguito dallo snippet
withLog()
}

See the documentation of the snippet to find out which are the accepted arguments, and the slot usage, if defined.

Matrix simulations

In a matrix simulation, many combinations of variables are simulated, among which are the infrastructure, timetables, rolling stock, stochastic variables, etc. The runSimulations method defines the variables of the matrix simulation and the simulation type (deterministic, stochastic, scenario delay). At the end of the configuration, in the run phase, executes a simulation for each combination of the variables.

runSimulations {
withMicro('Micro')
withRollingStock('RollingStock')
withTimetables('Timetable1', 'Timetable2')
withTimetableTransformations(
'Real',
'20Del': timetableTransformations { changeDistribution(/Real(.+)/, /20$1/) }
)
stochastic(runs:250)
}

This script executes 4 stochastic simulations with 250 runs each, with the combination of the variables Timetable1+Real, Timetable1+20Del, Timetable2+Real, Timetable2+20Del. Timetable1 and Timetable2 are two Timetables present inside the project; Real is the base variant of the simulation, that is it doesn't apply any transformation. 20Del is a variant that applies the timetable transformation changeDistribution, which substitutes each distribution whose name starts with "Real", with the corresponding distribution whose name starts with "20".

Among the variables that are available in a matrix simulation, aside from the timetable, and the timetable transformations seen in the example, the following are also available:

  • Micro models: withMicros('Micro1', 'Micro2').
  • Rolling stock: withRollingStocks('RollingStock1', 'RollingStock2').
  • Simulation settings: withSettings('S1': settings1, 'S2': settings2).
  • Driving policies.
  • Timetable filters.
  • Parametric timetable transformations, in which a closure can construct a set of transformations taking in input all the combinations of one or more parameters.
  • Continuous parameters, which vary in a range by a chosen step.

Each matrix simulation builder instruction creates a stage, a tap, or a runner. The stage's task is to prepare all the input data (context) of the simulations. There are single stages, for example, withMicro, and multiple stages, for example withTimetables. Each multiple stage multiplies the number of executed simulations as if it were a new axis of a multidimensional matrix. The taps instead, allow the execution of operations before and after each simulation, before and after the execution of the script, or during the processing of the simulation results. They are used, among other things, to generate outputs, charts, register listeners for the simulation events, etc. Each matrix simulation has a runner, which task is to execute the selected type of simulation. By default, the simulation is deterministic, but there are also stochastic, delayScenario, dryRun runners available:

deterministic() // the default, executes a single deterministic simulation without delays
deterministic(suppressDetailedOutput: true) // same as above, but does not produce the detailed output. Useful when you manage the output yourself and do not need the detailed output.

stochastic(50) // stochastic simulation with 50 runs
stochastic(runs: 50) // like above
stochastic(runs: 50, suppressDetailedOutput: true) // like above, but does not produce the detailed output. Useful when you manage the output yourself and do not need the detailed output.
stochastic(setKey: '1', runs: 50) // like above, with specified setKey

delayScenario('005') // executes a deterministic simulation with the same delays as the run 005 of a stochastic simulation.
delayScenario(runKey: '005') // same as above
delayScenario(runKey: '005', suppressDetailedOutput: true) // same as above, but does not produce the detailed output. Useful when you manage the output yourself and do not need the detailed output.
delayScenario(setKey: '1', runKey: '005') // same as above, with specified setKey

delayPercentile(percentile: 75.0) // sample the 70th percentile from each delay distribution, and the 25th delay percentile from each performance distribution.

dryRun() // does not execute the simulation, only lists the simulations that would be executed

Timetable transformations

The reference method for applying timetable transformations is withTimetableTransformations. This method can be overloaded in various ways:

  • By applying the transformations to the timetable previously loaded, without creating a new variable: withTimetableTransformations(transformations). Transformations can be a builder, for example:
    withTimetableTransformations(
    timetableTransformations {
    changeDistribution(/Real(.+)/, /20$1/)
    }
    )
    For convenience, it is possible to directly pass the closure to the withTimetableTransformations method:
    withTimetableTransformations {
    changeDistribution(/Real(.+)/, /20$1/)
    }
  • By applying a series of timetable transformations, and by creating a new variable in the matrix simulation. In this case, the builder timetableTransformations builder must be used or a previously created transformation must be referenced.
 withTimetableTransformations(
'20Del': timetableTransformations { changeDistribution(/Real(.+)/, /20$1/) },
'40Del': timetableTransformations { changeDistribution(/Real(.+)/, /40$1/) }
)
  • By applying a series of timetable transformations, and by creating a new variable in the simulation matrix, but by executing the base variant as well, that is, the one that does not apply any transformations.
 withTimetableTransformations(
'Real',
'20Del': timetableTransformations { changeDistribution(/Real(.+)/, /20$1/) }
)
  • By generating the transformations at runtime, as combinations of variables. The builder is called for each possible combination of variables values, and for each variable, an event is passed to the builder. The following script generates 4 simulations:
    • Stop@TWYFORD Stop@TWYPWY
    • Stop@TWYFORD Pass@TWYPWY
    • Pass@TWYFORD Stop@TWYPWY
    • Pass@TWYFORD Pass@TWYPWY
    withTimetableTransformations(
    ['TWYFORD', 'TWYPWY'], // variable names
    [PathEntryType.stop, PathEntryType.pass], // values assigned to the variables
    '${TWYFORD}@TWYFORD ${TWYPWY}@TWYPWY' // template for tje simulation name
    ) { typeTWYFORD, typeTWYPWY -> // the closure receives each combination
    // for the variable values
    setEntryType(typeTWYFORD).whenEntry { station('TWYFORD') }
    setEntryType(typeTWYPWY).whenEntry { station('TWYPWY') }
    }
    If you have one variable only, you can specify its name without the need to create a list, or you can even omit it, param name will be automatically assigned.
  • If you want to specify a list of values for each variable, you can use a map:
    withTimetableTransformations(
    entryType: [PathEntryType.stop, PathEntryType.pass], // name of the first variable and its values
    initialDelay: [0, 30, 60, 90], // name of the second variable and its values
    '${entryType}-${initialDelay}' // template for the simulation name, which references the two variables
    ) { entryType, initialDelay -> // closure parameters
    // ...
    }

Timetable transformations with continuous con parameters 1.5.22+

The examples seen so far consider a list of discrete values for the variables, for example, the stop or pass entry type, or a limited set of values of initial delay. If on the other hand you want to run simulations while varying a numeric parameter wihtout the need to specify all its possible values, or the values in its range, you can use the withTimetableTransformations method using some ranges as parameter values. The range is expressed with the following syntax (from..to) with from and to both included in the interval. By default, the interval step is 1. The by method changes the step: 60..90.by(5), this interval contains 7 values: 60, 65, 70, 75, 80, 85 e 90.

runSimulations() {
withRollingStock('RollingStock')
withTimetable('Timetable_Dec19')
withMicro('Micro')
withTimetableTransformations(perf: 60..90, 'Braking ${perf}-${perf+10}') { perf ->
assignPerformancesDistribution(BRAKING) {
uniform(perf, perf+10)
}
}
stochastic(runs: 2)
}

In this example, perf variable was declared and it assumes integer values between 60 and 90 included. The template that generates the name of the simulation references the perf variable, and the builder is called for each value inside the range.

You can also use more variables, in this case, the builder receives each possible combination of values. The following example executes 7x7=49 simulations.

withTimetableTransformations(['braking', 'cruising'], (60..90).by(5), 'Braking ${braking}-${braking+10}, Cruising ${cruising}-${cruising+10}') { braking, cruising ->
assignPerformancesDistribution(BRAKING) {
uniform(braking, braking+10)
}
assignPerformancesDistribution(CRUISING) {
uniform(cruising, cruising+10)
}
}

If the various variables have different value ranges, you can specify them using a map.

withTimetableTransformations(braking: (50..80).by(10), cruising: (70..90).by(10), 'Braking ${braking}-${braking+10}, Cruising ${cruising}-${cruising+10}') { braking, cruising ->
assignPerformancesDistribution(BRAKING) {
uniform(braking, braking+10)
}
assignPerformancesDistribution(CRUISING) {
uniform(cruising, cruising+10)
}
}

Continuous parameters

The use of parameters is not limited to timetable transformations. you can generically use them for other operations as well, for example, to modify the visibility distance of the signals:

withParameter("sightDistance", (60..500).by(20), "SightDist${sightDistance}m") { sightDistance ->
micro.find(MovementAuthoritySignal).each {
it.sightDistance = sightDistance
}
}

Iteration interruption for continuous parameters

A use case could require running simulations while incrementing a parameter, till a certain condition is met. For example, find the initial intermediate time between two trains such that there are no more conflicts. To interrupt the iteration, from the listener's body o from a processor, call the breakParameter method specifying the name of the parameter for which you want to interrupt the iteration. The current simulation and all the processing of its results will be executed nonetheless, while all the other simulations with values higher than the parameter will be skipped.

def results = runSimulations {
...
eachTrain()
...
withTimetableTransformations(initialHeadway: (30..180).by(15)) { initialHeadway, params ->
shiftTrain(params.train2).withOffset(initialHeadway)
}
...
withProcessor { ctx, results ->
if (results.conflicts == 0) {
// break the iteration on initialHeadway and continue with the next train
ctx.breakParameter('initialHeadway')
}
}
}

If there are more continuous parameters, breakParameter acts on the specified parameter and on all the next parameters.

def results = runSimulations {
...
eachTrain()
...
withTimetableTransformations(initialHeadway: (30..180).by(15)) { initialHeadway, params ->
shiftTrain(params.train2).withOffset(initialHeadway)
}
...
withTimetableTransformations(dwellTimeDelay: (0..180).by(15)) { dwellTimeDelay, params ->
assignDelayDistribution { ... }
}
...
withProcessor { ctx, results ->
if (results.conflicts == 0) {
// interrupts the iteration on dwellTimeDelay and initialHeadway, and continues with the next train
ctx.breakParameter('initialHeadway')
}
}
}

In this case, as soon as a combination of parameters without conflicts is found, the iteration is interrupted on intialHeadway as well as on dwellTimeDelay.

Aside from breakParameter, there is also continueParameter, which interrupts the iteration on all the next parameters from the one specified, and continues from the next value of the specified parameter:

def results = runSimulations {
...
eachTrain()
...
withTimetableTransformations(dwellTimeDelay: (0..180).by(15)) { dwellTimeDelay, params ->
assignDelayDistribution { ... }
}
... // altri parametri
withTimetableTransformations(initialHeadway: (30..180).by(15)) { initialHeadway, params ->
shiftTrain(params.train2).withOffset(initialHeadway)
}
... // altri parametri
withProcessor { ctx, results ->
if (results.conflicts == 0) {
// stop the iteration on all the parameters following the dwellTimeDelay
// and continue with the next dwellTimeDelay value
ctx.continueParameter('dwellTimeDelay')
}
}
}

Parameters reading from file 1.5.22+

The parameters can be read from a CSV file (comma separated, double quoted strings that contain carriage returns or the separator) The first line of the file must contain the headers of the columns, that will also become the names of the parameters.

Let suppose we have a Headway.csv file with the following content:

train1,    train2,    fromStation, toStation
601_2.A1, 601_2.A1, OSL, SV
601_2.A1, 745_1.A1, OSL, SV
745_1.A1, 601_2.A1, OSL, SV
745_1.A1, 745_1.A1, OSL, SV
601_2.A1, 601_2.A1, SV, ASR
601_2.A1, 745_1.A1, SV, ASR
745_1.A1, 601_2.A1, SV, ASR
745_1.A1, 745_1.A1, SV, ASR
601_2.A1, 601_2.A1, ASR, DRM
601_2.A1, 745_1.A1, ASR, DRM
745_1.A1, 601_2.A1, ASR, DRM
745_1.A1, 745_1.A1, ASR, DRM

Each line of the file identifies a set of parameters. It is possible to use the withTimetableTransformations methid specifying the file name (or the relative path from the project folder). The closure is called once for each file line (excluding the header) with an argument for each column of the file.

withTimetableTransformations(inputFile) { train1, train2, fromStation, toStation, params ->
filterTrains(train1, train2)
shiftTrain(train2).withOffset(timetable[train1][params.fromStation].departureTime - timetable[train2][params.fromStation].departureTime)
partialPath(fromStation, toStation)
}

Also, withParameters method can read the parameters from a file:

withParameter('Signals.csv') { maximumTrainLength, minimumStopTime ->
micro.find(MainSignal).each {
if (it.exitSignal) {
it.maximumTrainLength = maximumTrainLength
it.minimumStopTime = minimumStopTime
}
}
}

Incidents 1.5.31+

The incidents can be defined programmatically, or loaded from one or more files. Each file contains a set of incidents and generates a new scenario. It is also possible to define a base scenario, where no incident is applied.

This example executes two scenarios, one called MissedStop and the other UnplannedStop, obtained by applying the incidents defined in the MissedStop.incidents and UnplannedStop.incidents files respectively.

runSimulations {
withMicro('Micro)
withRollingStock('RollingStock')
withTimetable('Timetable')
withIncidents('MissedStop', 'UnplannedStop')
...
}

This example executes first the base scenario, called Base, and the two scenarios, one called Incidents1 and the other Incidents2, obtained by applying the incidents defined in the MissedStop.incidents and UnplannedStop.incidents files respectively.

runSimulations {
withMicro('Micro)
withRollingStock('RollingStock')
withTimetable('Timetable')
withIncidents('Base', 'Incidents1': 'MissedStop , 'Incidents2': 'UnplannedStop')
...
}

To define a single incident programmatically, use the withIncident method. For a list of incident templates, please refer to Scripting ref.

runSimulations {
withMicro('Micro)
withRollingStock('RollingStock')
withTimetable('Timetable')
withIncident('genericEmergencyBraking') { ctx, configuration ->
configuration.markerName = 'marker'
configuration.triggerTrainExpression = 'train'
configuration.triggerTrainWaitTime = 60
configuration.triggerTrainBrakingPerformance = 125
configuration.triggerTrainTargetSpeed = 0
}
}

Timetable amendments 1.5.31+

The amendments are loaded from one or more files. Each file contains a set of amendments and generates a new scenario. Furthermore, it is possible to define a base scenario, in which no amendment is applied.

This example executes two scenarios, one called Amendments1 and the other one Amendments2, obtained by applying the amendments defined in the Amendments1.amendment e Amendments2.amendment files respectively.

runSimulations {
withMicro('Micro)
withRollingStock('RollingStock')
withTimetable('Timetable')
withAmendments('Amendments1', 'Amendments2')
...
}

This example executes first the base scenario, called Base, and then two scenarios, one called Amendments1 and the other Amendments2, obtained by applying the amendments defined in the ShortTurn1.amendment and ShortTurn2.amendment files respectively.

runSimulations {
withMicro('Micro)
withRollingStock('RollingStock')
withTimetable('Timetable')
withAmendments('Base', 'Amendments1': 'ShortTurn1 , 'Amendments2': 'ShortTurn2')
...
}

Create scenarios programmatically 1.4.24+

runSimulations {
withMicro(micro)
withRollingStock(rollingStock)
withTimetable(timetable)

importSnippet('lib/TimeSignalAtRed')

withScenarios {
runScenario("base")
triggerTrainsMarkers.each { train, markers ->
markers.each { marker ->
runScenario("${train}_${marker}") {
withIncident('genericEmergencyBraking') { ctx, configuration ->
configuration.markerName = marker
configuration.triggerTrainExpression = train
configuration.triggerTrainWaitTime = duration.seconds
configuration.triggerTrainBrakingPerformance = 125
configuration.triggerTrainTargetSpeed = 0
}
}
}
}
}
deterministic()
}

The method runScenario takes scenarioNameTemplate, scenarioDescriptionTemplate and a closure that defines the configuration of the scenario as arguments. If the closure is not specified, the base scenario will be used, without the addition of any configuration prior to calling withScenarios. scenarioDescriptionTemplate is optional. scenarioNameTemplate e scenarioDescriptionTemplate can be GStrings and use the declared variables, or some single quoted strings and define a template evaluated during the execution of the scenario. This is useful when the template is passed from a script into the configuration of a snippet, and the snippet's variables are not yet available.

The methods available in runScenario are similar to those present in runSimulations, with the exception that they don't have the overloads used for multiple scenarios definition. For example, the method withAmendments is available, but it allows you to load a single amendments file.

Phases in a matrix simulation

The phases of matrix simulations are handled automatically, except for some special cases, it is not necessary to insert the calls to the run* methods inside the run block. On the other hand, if necessary, you can execute a simulation during the Build phase as well, the call to runSimulations must be inserted inside a build block.

In the following example, the routesByTrain variable is calculated during the Build phase after the execution of the simulation has started.

def routesByTrain = build {
// route sequence visited by each train
def routesByTrain = [:].withDefault{[] as LinkedHashSet } // train number -> route list

// unrestricted
runSimulations() {
withMicro(micro)
withRollingStock(rollingStock)
withEachTrain(timetable)
withEventListener(SimRouteStateChangedEvent) { ctx, evt ->
if (evt.newState == SimRoute.State.OCCUPIED) {
routesByTrain[evt.train.number] << (evt.route as String)
}
}

deterministic()
}

return routesByTrain
}

This allows you to generate a set of simulation scenarios already at build time, and it is the starting point for advanced functionalities, such as distributed execution of multi-scenario simulations.

run {
runSimulations {
withRollingStock(rollingStock)
deterministic(suppressDetailedOutput: true)
slot(args.defaultSlot)

withScenarios {
// using the route sequences calculated during the build phase
routesByTrain.each { trainNumber, trainRoutes ->
trainRoutes.each { route ->
runScenario(trainNumber: trainNumber, route: route.name) {
// configuring the scenario
}
}
}
}
}
}

Prefix

To execute a subset of simulation scenarios, created with withScenarios or as matrix simulations, from the command line, use the params.prefix parameter specifying the prefix of the scenarios to be executed. For example, by specifying -Dparams.prefix=1 only the scenarios whose name is 1 or begins with 1_ will be executed. Note that the underscore is used to separate the name of the scenario to be executed from the prefix in multiple parts, and the comparison is done between them. So, scenario 11 is excluded from the 1 prefix, and 1_23 is excluded from the 1_2 prefix, but 1_2_34 is included in the 1_2 prefix.

Template name and scenario description

The scenarioNameTemplate and scenarioDescriptionTemplate arguments of runScenario method, in the case of single-quote string type, are evaluated during the execution of the scenario and can reference the template parameters. In addition to standard Java and Groovy methods, the following functions are available:

  • pad(int value, int digits): left padding of a number, with zeros. For example pad(27, 4) returns 0027. It can be useful to create simulation names that can be ordered alphanumerically: the template 'RHL_${pad(delay, 3)}_${pad(index, 2)} always returns a string in the RHL_000_00 format and thus it can be easily ordered by delay and by index.

  • padLeft(Object value, int numberOfCharacters, String padding = ' '): pads a string value to a minimum length specified by numberOfCharacters by adding the supplied padding string as many times as needed to the right. If the value is already the same size or bigger than the target numberOfCharacters, then the original value is returned.

  • padRight(Object value, int numberOfCharacters, String padding = ' '): pads a string value to a minimum length specified by numberOfCharacters by adding the supplied padding string as many times as needed to the left. If the value is already the same size or bigger than the target numberOfCharacters, then the original value is returned.

  • signed(Double value), signed(Integer value): force to prepend a plus (+) or a minus (-) sign to the specified number. 1.5.31+

Buckets 1.5.8+

To ease the subdivision of scenarios on more simulation processes, it is possible to generate a random bucket for each simulation and insert it at the beginning of the scenario name, using the scenarioNameTemplate argument and the bucket(int buckets) method; for example, the '${bucket(16)}_${param1}_${param2}' template generates scenario names with a hexadecimal character prefix, uniformly distributed in the interval 0-F. A number of buckets greater than 16 will generate a prefix of two or more hexadecimal characters.

If you want to group the scenario in a predictable manner, for example, based on a single matrix simulation parameter, you can use the bucket(int buckets, Object...params) method, where params is a list of available parameter names, that can be taken from a snippet, or from parametric timetable transformations, etc. The method tries to distribute uniformly the simulations across a chosen number of buckets, with the condition that the values or the combination of values of the parameters are uniformly distributed, or that the number of combinations of the values is much bigger than the number of requested buckets.

Parallel execution of scenarios 1.5.6+

The parallel() method enables the execution of simulation scenarios in parallel. It should not be used for the stochastic simulation, because it already uses the parallel execution of the simulations that compose it.

Optionally, it is possible to specify the number of threads to be used: parallel(threadCount: 7), or the number of threads to be used per processor / core: parallel(threadCountPerProcessor: 0.75).

Fork of the execution 1.5.22+

The fork() method executes all the next stages of the script in a separate thread. If there are multiple stages before the fork() method, they will be executed in parallel on multiple threads.

Optionally, it is possible to specify the number of threads to be used: fork(threadCount: 7) or the number of threads to be used per processor / core: fork(threadCountPerProcessor: 0.75. If omitted, by default one thread for every two processors / core is used (equivalent to threadCountPerProcessor: 0.5)

In the following example, withTimetableTransformations generates 4 sets of simulations. Each set is executed in a separate thread, while all the simulations of the set (with sightDistance from 60 to 500) are executed in sequence.

withTimetableTransformations(['TWYFORD', 'TWYPWY'], [PathEntryType.stop, PathEntryType.pass], '${TWYFORD}@TWYFORD ${TWYPWY}@TWYPWY') { typeTWYFORD, typeTWYPWY ->  
setEntryType(typeTWYFORD).whenEntry { station('TWYFORD') }
setEntryType(typeTWYPWY).whenEntry { station('TWYPWY') }
}

fork()

withParameter("sightDistance", (60..500).by(20), "SightDist${sightDistance}m") { sightDistance ->
micro.find(MovementAuthoritySignal).each {
it.sightDistance = sightDistance
}
}

Continuous and discrete parameters

Hierarchical contexts

Listener, initializers and finalizers

You can also register an event listener in a similar way as seen in Simulation listeners, check the next example:

runSimulations() {
withMicro('Micro')
withRollingStock('RollingStock')
withEachTrain('Timetable1', 'Timetable2')
withEventListener(SimRouteOccupiedEvent) { ctx, evt ->
ctx.counter ++
}.withInitializer { ctx ->
ctx.counter = 0
}.withFinalizer { ctx ->
println("Route occupation counter: $counter")
}

Unlike a Simulation listener, all the variables used by the listeners are located in a context, which guarantees the isolation between the simulation of different scenarios. The initializer is thus called each time a simulation starts, making the counter restart from zero. The finalizer is called at the end of each simulation, and writes the final value of the counter, for the simulation with Timetable1 and Timetable2 independently.

Output file aggregation

To ease the analysis of the results it is possible to obtain a single CSV file as output for all the scripted simulations instead of having a CSV file for each run, by using AggregateCsvOutputFiles snippet. The glob that selects the files to be aggregated must be specified, as well as the name of the aggregated file. The aggregated file has in the first column the name of the run, in the second column the name of the origin file, and then all the columns present in the origin file. The glob has as base folder the one for the output of the scripted simulation (for example .../output/2021-05-28-11-13/) so it is always necessary to put the ** wildcard before in order to consider the files inside the output directories of the single runs.

The following example shows how to aggregate all the files itinerary.csv for all the trains and all the runs in a single itineraries.csv.

snippet('AggregateOutputFiles', glob: '**/*.itinerary.csv', dest: 'itineraries.csv')

Examples

In depth guide