2. Getting Started

This section takes you through a the entire process of conducting a co-simulation with Maestro2.

This guide is based on the command line interface (CLI) of Maestro2 and consists of two parts:
The first part Part 1: First MaBL Specification concerns writing a MaBL Specification by hand and executing it.
The second part Part 2: Specification Generation and Expansion concerns using the specification generation feature of Maestro for generating the specification.

Note that Maestro2 also features a web API presented in Web Interface (API).

2.1. DEVELOPMENT BRANCH

If you are using the development branch, then the documentation might not be up to date. However, you can consult the tests webapi_test.py, maestro_cli_test.py and cli_legacy_test.py. These tests are run by our CI system, so if the build passes, then these are valid.

2.2. Setup

The first step is to install the dependencies along with the tools and resources that are to be used

  • Download and install Java 11
  • Download maestro-2.x.x-jar-with-dependencies.jar from the newest Maestro2 2.x.x release on the Maestro release page https://github.com/INTO-CPS-Association/maestro/releases. To easier follow the tutorials below it is recommended to rename the jar file to maestro2.jar, such that the terminal commands can be copy-pasted.
  • Optional: Python 3 for plotting capabilities

2.3. Example

The example throughout this getting started guide concerns a water tank. The tank (Continuous Time component) has a constant inflow of water and a valve that is controlled by a controller (Discrete-Event component). When the valve is open, the water level within the tank decreases, and whe the valve is closed, the water level increases. The controller determines when to open and close the valve based on a maximum and a minimum water level.

../_images/wt-example-graphic.png

Water Tank Example

This is modelled as two FMUs with external connections, internal connections and parameters as presented in the figure below.

../_images/wt-example.png

Water Tank FMUs and ports

2.4. Part 1: First MaBL Specification

This part presents a MaBL specification written by hand with descriptive comments.

To follow this part download the getting started part1 resources and unzip into a directory. To easier follow this guide the downloaded jar file can be placed in the same directory as the resources has been unzipped into.

The corresponding MaBL code, including descriptive comments, to execute this co-simulation is presented below:

simulation
import FMI2;
import DataWriter;
import Logger;
import Math;
{
    real START_TIME = 0.0;
    real END_TIME = 10.0;
    real STEP_SIZE = 0.1;

    // #############################
    // DataWriter for results
    // #############################
    DataWriter dataWriter = load("DataWriter");
    string data_headers[2] = {"{crtl}.crtlInstance.valve","{wt}.wtInstance.level"};
    DataWriterConfig dataWriterConfiguration = dataWriter.writeHeader(data_headers);


    // #############################
    // Load the FMUs and instantiate instances
    // #############################
    FMI2 tankController = load("FMI2", "{8c4e810f-3df3-4a00-8276-176fa3c9f000}", "watertankcontroller-c.fmu");
    FMI2 singleWatertank = load("FMI2", "{cfc65592-9ece-4563-9705-1581b6e7071c}",  "singlewatertank-20sim.fmu");
    FMI2Component crtlInstance = tankController.instantiate("crtlInstance", false, false);
    FMI2Component wtInstance = singleWatertank.instantiate("wtInstance", false, false);

    wtInstance.setupExperiment(false, 0.0, START_TIME, true, END_TIME);
    crtlInstance.setupExperiment(false, 0.0, START_TIME, true, END_TIME);

    // #############################
    // Set maximum level and minimum level parameters
    // #############################
    real crtlParameters[2] = { 2.0 , 1.0 };
    uint crtlParameterValRefs[2] = { 0 , 1 };
    crtlInstance.setReal(crtlParameterValRefs, 2, crtlParameters);

    wtInstance.enterInitializationMode();
    crtlInstance.enterInitializationMode();


    uint crtlValveStateValRef[1] = { 4 };
    bool crtlValveStateOutput[1];

    uint crtlLevelInputValRef[1] = { 3 };
    real crtlLevelInputValue[1];

    uint wtValveStateValRef[1] = { 16 };
    real wtValveStateInputs[1];

    uint wtLevelValRef[1] = { 17 };
    real wtLevelOutputValue[1];

    // #############################
    // Perform the Initialization:
    // 1. Extract valve state from controller
    // 2. Convert the valve state from boolean to real
    // 3. Set the valve state on the tank
    // 4. Extract the level from the tank
    // 5. Set the level on the controller
    // #############################
    crtlInstance.getBoolean(crtlValveStateValRef, 1, crtlValveStateOutput);
    real wtValveStateInput;
    if( crtlValveStateOutput[0] )
    {
        wtValveStateInput = 1.0;
    }
    else
    {
        wtValveStateInput = 0.0;
    }


    wtValveStateInputs[0] = wtValveStateInput;
    wtInstance.setReal(wtValveStateValRef, 1, wtValveStateInputs);

    wtInstance.getReal(wtLevelValRef, 1, wtLevelOutputValue);
    crtlInstance.setReal(crtlLevelInputValRef, 1, wtLevelOutputValue);

    wtInstance.exitInitializationMode();
    crtlInstance.exitInitializationMode();

    // #############################
    // Get the valve state from the controller
    // Get the level from the tank 
    // Write the valve state and the water level to the results
    // #############################
    crtlInstance.getBoolean(crtlValveStateValRef, 1, crtlValveStateOutput);
    wtInstance.getReal(wtLevelValRef, 1, wtLevelOutputValue);
    dataWriter.writeDataPoint(dataWriterConfiguration, START_TIME, crtlValveStateOutput[0], wtLevelOutputValue[0]);


    real time = START_TIME;
    // #############################
    // Simulation Loop until END_TIME
    // #############################
    while(time + STEP_SIZE <= END_TIME)
    {
        // #############################
        // Convert valve state from boolean to real
        // #############################
        if( crtlValveStateOutput[0] )
        {
            wtValveStateInput = 1.0;
        }
        else
        {
            wtValveStateInput = 0.0;
        }
        // #############################
        // Set the valve state on the tank
        // Set the level on the controller
        // #############################
        wtValveStateInputs[0] = wtValveStateInput;
        wtInstance.setReal(wtValveStateValRef, 1, wtValveStateInputs);
        crtlInstance.setReal(crtlLevelInputValRef, 1, wtLevelOutputValue);

        // #############################
        // Step the tank
        // Step the controller
        // #############################
        wtInstance.doStep(time, STEP_SIZE, true);
        crtlInstance.doStep(time, STEP_SIZE, true);

        // #############################
        // Get the valve state from the controller
        // Get the level from the tank
        // #############################
        crtlInstance.getBoolean(crtlValveStateValRef, 1, crtlValveStateOutput);
        wtInstance.getReal(wtLevelValRef, 1, wtLevelOutputValue);

        // #############################
        // Increase the time with STEP_SIZE
        // Write the valve state and the water level to the results
        // #############################
        time = time + STEP_SIZE;
        dataWriter.writeDataPoint(dataWriterConfiguration, time, crtlValveStateOutput[0], wtLevelOutputValue[0]);

    }
    
    // #############################
    // Clean up
    // #############################
    dataWriter.close();
    wtInstance.terminate();
    crtlInstance.terminate();
    tankController.freeInstance(crtlInstance);
    singleWatertank.freeInstance(wtInstance);

    unload(tankController);
    unload(singleWatertank);
    unload(dataWriter);
}

To execute this (one can use the file corresponding wt-example.mabl from the unzipped example resouces) run the following command from the terminal:

java -jar maestro2.jar --interpret wt-example.mabl

Where --interpret is to interpret a specification

The result is available in outputs.csv. This can be plotted with python part1pythoncsvplotter.py, and the result should look like the figure below.

../_images/wt-example-result.png

Result from co-simulation

2.5. Part 2: Specification Generation and Expansion

This part also concerns the water tank but uses the capabilities of specification generation and expansion to create the specification. Thus, the user does not have to write MaBL by hand. Specification generation and Expansion are not treated in detail in this guide, but more information is available in Specification Generator and Expansion.

To follow this part download the getting started part2 resources and unzip into a directory. To easier follow this guide the downloaded jar file can be placed in the same directory as the resources has been unzipped into.

The specification generator is based on JSON configuration files with the same structure as the ones used in Maestro1. For this reason, it is also possible to use the INTO-CPS Application to create the specification files. These files are available in the folder of the given Multi-Model. This is also mentioned below where the actual terminal command is issued.

2.5.1. Configuration file

The content of the configuration file is briefly described after the example below. A more detailed description is available at Legacy Configuration / INTO-CPS Multi-model and coe config File Format. The configuration for this example is:

{
  "fmus": {
    "{crtl}": "watertankcontroller-c.fmu",
    "{wt}": "singlewatertank-20sim.fmu"
  },
  "connections": {
    "{crtl}.crtlInstance.valve": [
      "{wt}.wtInstance.valvecontrol"
    ],
    "{wt}.wtInstance.level": [
      "{crtl}.crtlInstance.level"
    ]
  },
  "parameters": {
    "{crtl}.crtlInstance.maxlevel": 2,
    "{crtl}.crtlInstance.minlevel": 1
  },
  "algorithm": {
    "type": "fixed-step",
    "size": 0.1
  },
  "endTime": 10
}
  • fmus contains mappings between a key, enclosed in {} and the corresponding FMU.
  • connections contains mappings from a single FMU output port to one or more FMU input ports. The format of a port is {fmuKey}.instance.scalarVariableName
  • parameters contains mappings from a single FMU parameter port and a value.
  • algorithm describes that the fixed-step algorithm should be used with the size 0.001. Currently only fixed-step is supported.
  • end_time is the end-time of the simulation

2.5.2. Specification Generation

The command below generates the specification based on the configuration file above:

java -jar maestro2.jar --spec-generate1 wt-example-config.json --dump-simple "./"

Where --spec-generate1 instructs Maestro2 to use specification generator for legacy configuration files (i.e. configuration files for maestro1) and --dump-simple describes to dump the final specification at the current directory.

Note

The INTO-CPS Application creates two configuration files in JSON format: a multimodel configuration file and a simulate configuration file. These files can both be added to the terminal command, i.e. --spec-generate1 multimodel.json simulate.json, and will be merged by Maestro. Thus, one does not have to merge these files manually.

2.5.3. Final specification and Execution

The final specification is in the file spec.mabl and is not presented here but can be downloaded: spec.mabl.

Furthermore, a spec.runtime.json file has been produced with information for the runtime DataWriter CSV plugin with information on where to store the csv result:

{
  "DataWriter" : [ {
    "filename" : "outputs.csv",
    "type" : "CSV"
  } ]
}

2.5.4. Execution

To execute the specification execute the following command:

java -jar maestro2.jar --interpret spec.mabl

Where --interpret instructs Maestro2 to interpret the subsequent specifications, in this case spec.mabl.

The interpreter will automatically look for *.runtime.json files within the same directory as the jar file.

Similar to above in Part 1: First MaBL Specification, the results can be viewed and plotted with the accompanying python script: python part2pythoncsvplotter.py

2.5.5. Extra: Expansion

This section gives a small insight into the expansion feature of Maestro. It is possible to use expansion plugins within in a MaBL specification. When Maestro encounters a MaBL expand statement during processing, it will replace the statement with MaBL code generated by the related expansion plugin. This happens automatically in the part2 example, but it can be made obvious.

Run the command below:

java -jar maestro2.jar --spec-generate1 wt-example-config.json --dump-intermediate "./part2mabl-intermediate"
where
--dump-intermediate describes to dump the specification after every expansion iteration at “./part2mabl-intermediate”

The directory part2mabl-intermediate now contains files with different numbers in increasing order due to the flag --dump-intermediate "./part2mabl-intermediate". The initial specification, spec00000.mabl, refers to the specification generated by the specification generator without any expansions carried out. spec00001.mabl is the specification after 1 expansion has been carried out and so forth. The larget number referes to the specification after all expansions has been carried out.

spec00000.mabl is the specification generated by the specification generator which contains several annotations (marked with @) and two expand statements (begins with: expand): initialize and fixedStep. The reason for these annotations are that the file is self-contained. The relevant information to generate the final specification is added to the specifications via these annotations. See the example below and the description afterwards:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
simulation 
import FixedStep;
import Initializer;
import DebugLogging;
import TypeConverter;
import DataWriter;
import FMI2;
import Math;
import ArrayUtil;
import Logger;
import BooleanLogic;
@Framework( "FMI2");
@FrameworkConfig( "FMI2", "{\"fmus\":{\"{crtl}\":\"watertankcontroller-c.fmu\",\"{wt}\":\"singlewatertank-20sim.fmu\"},\"connections\":{\"{crtl}.crtlInstance.valve\":[\"{wt}.wtInstance.valvecontrol\"],\"{wt}.wtInstance.level\":[\"{crtl}.crtlInstance.level\"]},\"parameters\":{\"{crtl}.crtlInstance.maxlevel\":2,\"{crtl}.crtlInstance.minlevel\":1},\"startTime\":0.0,\"endTime\":10.0,\"loggingOn\":false,\"visible\":false,\"algorithm\":{\"type\":\"fixed-step\",\"size\":0.1}}");
{
 	bool global_execution_continue = true;
	int FMI_STATUS_OK = 0;
	int FMI_STATUS_WARNING = 1;
	int FMI_STATUS_DISCARD = 2;
	int FMI_STATUS_ERROR = 3;
	int FMI_STATUS_FATAL = 4;
	int FMI_STATUS_PENDING = 5;
	int status = 0;
	Math math = load("Math");
	Logger logger = load("Logger");
	DataWriter dataWriter = load("DataWriter");
	BooleanLogic booleanLogic = load("BooleanLogic");
	FMI2 crtl = load("FMI2", "{8c4e810f-3df3-4a00-8276-176fa3c9f000}", "/C:/Users/frdrk/AppData/Local/Temp/tmplaq3m7sj/watertankcontroller-c.fmu");
	if( (crtl == null) )
	{
		global_execution_continue = false;
	}
	FMI2 wt = load("FMI2", "{cfc65592-9ece-4563-9705-1581b6e7071c}", "/C:/Users/frdrk/AppData/Local/Temp/tmplaq3m7sj/singlewatertank-20sim.fmu");
	if( (wt == null) )
	{
		global_execution_continue = false;
	}
	@map wtInstance -> "wtInstance";
	FMI2Component wtInstance = null;
	if( (global_execution_continue) )
	{
			wtInstance = wt.instantiate("wtInstance", false, false);
			if( (wtInstance == null) )
			{
				global_execution_continue = false;
			}
	}
	@map crtlInstance -> "crtlInstance";
	FMI2Component crtlInstance = null;
	if( (global_execution_continue) )
	{
			crtlInstance = crtl.instantiate("crtlInstance", false, false);
			if( (crtlInstance == null) )
			{
				global_execution_continue = false;
			}
	}
	FMI2Component components[2] = { wtInstance , crtlInstance };
	real START_TIME = 0.0;
	real STEP_SIZE = 0.1;
	real END_TIME = 10.0;
	@Config("{\"parameters\":{\"{crtl}.crtlInstance.maxlevel\":2,\"{crtl}.crtlInstance.minlevel\":1}}");
	Initializer.expand initialize(components, START_TIME, END_TIME);
	FixedStep.expand fixedStep(components, STEP_SIZE, START_TIME, END_TIME);
	wt.freeInstance(wtInstance);
	crtl.freeInstance(crtlInstance);
	unload(crtl);
	unload(wt);
	unload(math);
	unload(logger);
	unload(dataWriter);
	unload(booleanLogic);
}
  • Line 12-13 describes the framework and the JSON necessary to generate the configuration of the framework context, which is needed by the expansion plugins.
  • Line 61 describes a configuration used specifically for the initialize expansion plugin on the subsequent line
  • Line 62 specifies that the initialize expansion plugin should be expanded.
  • Line 63 specifies that the fixedStep expansion plugin should be expanded.

The next example presents the intermediate expansion spec00002.mabl generated after the expansion of line 61 and 62 in spec00000.mabl containing @Config(...) and Initializer.expand initialize(...) respectively. Thus the @Config(...) and Initializer.expand initialize(...) have been replaced with the MaBL generated by the corresponding Initialize plugin as seen on line 61 to 158. As it can be seen on line 159, FixedStep.expand fixedStep(...) has not been expanded yet.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
simulation 
import FixedStep;
import Initializer;
import DebugLogging;
import TypeConverter;
import DataWriter;
import FMI2;
import Math;
import ArrayUtil;
import Logger;
import BooleanLogic;
@Framework( "FMI2");
@FrameworkConfig( "FMI2", "{\"fmus\":{\"{crtl}\":\"watertankcontroller-c.fmu\",\"{wt}\":\"singlewatertank-20sim.fmu\"},\"connections\":{\"{crtl}.crtlInstance.valve\":[\"{wt}.wtInstance.valvecontrol\"],\"{wt}.wtInstance.level\":[\"{crtl}.crtlInstance.level\"]},\"parameters\":{\"{crtl}.crtlInstance.maxlevel\":2,\"{crtl}.crtlInstance.minlevel\":1},\"startTime\":0.0,\"endTime\":10.0,\"loggingOn\":false,\"visible\":false,\"algorithm\":{\"type\":\"fixed-step\",\"size\":0.1}}");
{
 	bool global_execution_continue = true;
	int FMI_STATUS_OK = 0;
	int FMI_STATUS_WARNING = 1;
	int FMI_STATUS_DISCARD = 2;
	int FMI_STATUS_ERROR = 3;
	int FMI_STATUS_FATAL = 4;
	int FMI_STATUS_PENDING = 5;
	int status = 0;
	Math math = load("Math");
	Logger logger = load("Logger");
	DataWriter dataWriter = load("DataWriter");
	BooleanLogic booleanLogic = load("BooleanLogic");
	FMI2 crtl = load("FMI2", "{8c4e810f-3df3-4a00-8276-176fa3c9f000}", "/C:/Users/frdrk/AppData/Local/Temp/tmplaq3m7sj/watertankcontroller-c.fmu");
	if( (crtl == null) )
	{
		global_execution_continue = false;
	}
	FMI2 wt = load("FMI2", "{cfc65592-9ece-4563-9705-1581b6e7071c}", "/C:/Users/frdrk/AppData/Local/Temp/tmplaq3m7sj/singlewatertank-20sim.fmu");
	if( (wt == null) )
	{
		global_execution_continue = false;
	}
	@map wtInstance -> "wtInstance";
	FMI2Component wtInstance = null;
	if( (global_execution_continue) )
	{
			wtInstance = wt.instantiate("wtInstance", false, false);
			if( (wtInstance == null) )
			{
				global_execution_continue = false;
			}
	}
	@map crtlInstance -> "crtlInstance";
	FMI2Component crtlInstance = null;
	if( (global_execution_continue) )
	{
			crtlInstance = crtl.instantiate("crtlInstance", false, false);
			if( (crtlInstance == null) )
			{
				global_execution_continue = false;
			}
	}
	FMI2Component components[2] = { wtInstance , crtlInstance };
	real START_TIME = 0.0;
	real STEP_SIZE = 0.1;
	real END_TIME = 10.0;
	while( global_execution_continue )
	{
		int status = 0;
		status = wtInstance.setupExperiment(false, 0.0, START_TIME, true, END_TIME);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "Setup Experiment Failed:  %d", status);
				global_execution_continue = false;
				break;
		}
		status = crtlInstance.setupExperiment(false, 0.0, START_TIME, true, END_TIME);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "Setup Experiment Failed:  %d", status);
				global_execution_continue = false;
				break;
		}
		real realValueSize7[7] = { 9.0 , 1.0 , 1.0 , 9.81 , 1.0 , 0.0 , 0.0 };
		uint valRefsSize7[7] = { 0 , 1 , 2 , 3 , 4 , 5 , 6 };
		status = wtInstance.setReal(valRefsSize7, 7, realValueSize7);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "set failed %d", status);
				global_execution_continue = false;
				break;
		}
		real realValueSize2[2] = { 2.0 , 1.0 };
		uint valRefsSize2[2] = { 0 , 1 };
		status = crtlInstance.setReal(valRefsSize2, 2, realValueSize2);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "set failed %d", status);
				global_execution_continue = false;
				break;
		}
		status = wtInstance.enterInitializationMode();
		status = crtlInstance.enterInitializationMode();
		real realValueSize1[1] = { 0.0 };
		uint valRefsSize1[1] = { 16 };
		status = wtInstance.setReal(valRefsSize1, 1, realValueSize1);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "set failed %d", status);
				global_execution_continue = false;
				break;
		}
		realValueSize1[0] = 1.0;
		valRefsSize1[0] = 3;
		status = crtlInstance.setReal(valRefsSize1, 1, realValueSize1);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "set failed %d", status);
				global_execution_continue = false;
				break;
		}
		bool BooleanValueSize1[1];
		valRefsSize1[0] = 4;
		status = crtlInstance.getBoolean(valRefsSize1, 1, BooleanValueSize1);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "get failed %d", status);
				global_execution_continue = false;
				break;
		}
		bool crtlInstanceSvValRef4 = BooleanValueSize1[0];
		real wtInstanceSvValRef16Real;
		TypeConverter.expand convertBoolean2Real(crtlInstanceSvValRef4, wtInstanceSvValRef16Real);
		realValueSize1[0] = wtInstanceSvValRef16Real;
		valRefsSize1[0] = 16;
		status = wtInstance.setReal(valRefsSize1, 1, realValueSize1);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "set failed %d", status);
				global_execution_continue = false;
				break;
		}
		valRefsSize1[0] = 17;
		status = wtInstance.getReal(valRefsSize1, 1, realValueSize1);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "get failed %d", status);
				global_execution_continue = false;
				break;
		}
		real wtInstanceSvValRef17 = realValueSize1[0];
		realValueSize1[0] = wtInstanceSvValRef17;
		valRefsSize1[0] = 3;
		status = crtlInstance.setReal(valRefsSize1, 1, realValueSize1);
		if( status == 3 || status == 4 )
		{
				logger.log(4, "set failed %d", status);
				global_execution_continue = false;
				break;
		}
		status = wtInstance.exitInitializationMode();
		status = crtlInstance.exitInitializationMode();
		break;
	}
	FixedStep.expand fixedStep(components, STEP_SIZE, START_TIME, END_TIME);
	wt.freeInstance(wtInstance);
	crtl.freeInstance(crtlInstance);
	unload(crtl);
	unload(wt);
	unload(math);
	unload(logger);
	unload(dataWriter);
	unload(booleanLogic);
}