160

1. Introduction

esmini is a software tool to play OpenSCENARIO files. It’s provided both as a stand alone application and as a shared library for linking with custom applications. In addition some tools have been developed to support design and analysis of traffic scenarios.

The scope of this user guide is to provide examples on how to use the tools. It also includes a programming tutorial on how to use esmini in custom applications.

2. Getting started

Download latest release from here: https://github.com/esmini/esmini/releases/latest

First time make sure to pick the demo package for your platform (Windows, Linux or Mac). In addition to application binaries it also includes some content like example scenarios and basic 3D models. The binary (bin) packages includes only executables and libraries.

On Windows, make sure the zip file is not blocked: Right click, click Properties, at bottom right check Unblock. For more info, see Blocked by Windows Defender SmartScreen.

To install the package, just unzip it anywhere. A single subfolder named esmini is created. This is the root folder for esmini. No files are stored outside this folder structure and no system files or registry is modified in any way.

2.1. Run esmini

Try to run one of the examples:

  • go to folder esmini/run/esmini

  • double click on a .bat file, e.g. run_cut-in.bat or run it from a command line.

These scripts should work on all platforms (in spite extension ".bat").

You can also run the examples explicitly from a command line:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc

For common Mac issues, please see Mac issues and limitations.

2.2. Get complete esmini

The demo package contains only a subset of esmini tools and content (e.g. scenario examples, scripts and models).

To get the complete content, first download or clone the project from GitHub: https://github.com/esmini/esmini. E.g:
git clone https://github.com/esmini/esmini.git

Then either build yourself, see step 2 in next chapter Build esmini - quick guide, or:

  1. Download pre-built binary package for your system (e.g. esmini-bin_win_x64.zip) from latest release: https://github.com/esmini/esmini/releases. Copy the content into the esmini-demo folder.

  2. Download the complete 3D model package from here. Unpack into esmini/resources (it should end up in a models subfolder). These assets work on all platforms. The environment models (roads, landscape, buildings…​) have been created using VIRES Road Network Editor.

2.3. Build esmini - quick guide

Supported systems: Windows, Linux and Mac.

Make sure you have a C++ compiler and CMake installed.

On Windows Visual Studio is recommended (Community/free version is good enough for building esmini). Make sure to check the "Desktop development with C++" package for installation, no more is needed.

On Linux, e.g. Ubuntu, some additional system tools and libraries are needed. Run the following command:

sudo apt install build-essential gdb ninja-build git pkg-config libgl1-mesa-dev libpthread-stubs0-dev libjpeg-dev libxml2-dev libpng-dev libtiff5-dev libgdal-dev libpoppler-dev libdcmtk-dev libgstreamer1.0-dev libgtk2.0-dev libcairo2-dev libpoppler-glib-dev libxrandr-dev libxinerama-dev curl cmake

Now we’re ready to build esmini. From esmini root folder:

mkdir build
cd build
cmake ..
cmake --build . --config Release --target install

The build process automatically downloads 3rd party library binaries and the complete 3D model package.

After successful build, the binaries will be copied into esmini/bin folder. Try from command line:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc

That’s all. Details and variants, see Build guide.

3. Tools overview

3.1. Applications

esmini

Play OpenSCENARIO file. Many options, e.g. view on screen, save images to file or just save log data for post processing.
Example:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc

cut in
replayer

Replay recorded esmini scenarios. Main difference from esmini is the ability to freely move forward and backward in the scenario at various speeds.
Example:
./bin/replayer --window 60 60 800 400 --file sim.dat

See Use cases how to create recordings (.dat files) from esmini.

odrviewer

Visualize and verify OpenDRIVE road networks. Draw road features, like reference line and lanes, on top of a 3D model (provided or generated). Optionally populate with random traffic that will randomly find its way through the road network.
Example:
./bin/odrviewer --window 60 60 800 400 --odr ./resources/xodr/fabriksgatan.xodr

odrviewer

3.2. Scripts

esmini comes with a collection of scripts for various purposes, like preprocessing of scenario files and plotting or conversion of simulation output files. The scripts are located in the scripts folder. A few examples:

odrplot

Simple 2D plot of OpenDRIVE road lanes with indicated road IDs, in a two step process.
Prerequisites: Python + matplotlib
Example:
./bin/odrplot ./resources/xodr/fabriksgatan.xodr (will create a track.csv file)
python ./EnvironmentSimulator/Applications/odrplot/xodr.py track.csv

odrplot
plot_dat

Simple 2D plot of scenario data
Prerequisites: Python + matplotlib
Example:
./bin/esmini --headless --osc ./resources/xosc/acc-test.xosc --fixed_timestep 0.05 --record sim.dat (will create the .dat file)
./scripts/plot_dat.py sim.dat --param speed

plot dat
dat2csv

Convert esmini recording (.dat) file to standard .csv format
Example:
./scripts/dat2csv sim.dat
will create sim.csv.

osi2csv.py

Convert OSI trace file (from esmini) to .csv format
Example:
./scripts/osi2csv.py ./ground_truth.osi
will create ground_truth.csv

See Save OSI data how to create OSI groundtruth trace (.osi file) from esmini.

plot_csv

Simple 2D plot of scenario data, similar to plot_dat
Prerequisites: Python + matplotlib
Example:
./bin/esmini --headless --osc ./resources/xosc/acc-test.xosc --fixed_timestep 0.05 --osi_file sim.osi (will create an osi trace file)
./scripts/osi2csv.py sim.osi (convert to .csv)
./scripts/plot_csv.py sim.csv --param ax will plot the x component of acceleration vector.

dat2xosc.py

Convert any scenario to a fixed trajectory based scenario. See more info in script.

move_to_origin.py

Translate a scenario to origin. Useful for scenarios with large coordinates causing precision issues. See more info in script.

xodr_lines2curves.py

Smoothen polyline roads by converting them to parametric curves. See more info in script.

run_schema_comply.py

Validate xosc and xodr scenario files for compliance with the XML schema of the corresponding standard. See more info in script and section Run XML schema validation tests.

compile_osg_apps.sh

Compile OSG applications for viewing and converting OpenSceneGraph 3D models. See more info section Get osgconv / Build yourself.

run_repeat_compare.sh

Run a scenario multiple times and compare the results, expected to be identical. See more info in script.

osiviewer.py

Visualize OSI trace files. Only supporting a small, limited subset of OSI content, e.g. lanes, stationary and moving objects.
Example:
./scripts/osiviewer.py ground_truth.osi

osiviewer

3.3. Shared libraries

esminiLib

High level API for running, controlling and monitoring scenarios

esminiRMLib

High level API for parsing and query road networks (only road manager)

See headerfile esminiRMLib.hpp and code example rm-basic

4. Use cases

Here follows basic examples showing some, but not all, features in esmini and companion tools. It should give an idea of the possibilities and limitations. For a full list of features and functions, see Command reference.

To quickly see available launch options, simply run the corresponding application with no arguments, for example:

./bin/odrviewer

4.1. View a scenario

4.1.1. Basic features

Specify a window and scenario file. Example:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc

Visualize trails from moving entities:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --trail_mode 3

Show entities as bounding boxes:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --bounding_boxes

cut in trail and bb
Trail and bounding box in combination

4.1.2. Camera control

In addition to several OSG standard camera models esmini adds a set of special purpose camera modes that follows following scenario entities in various ways. Further, the user can add custom cameras via API or launch arguments.

Top level camera mode is selected on keys '1' to '8'. Mode 2-8 are OpenSceneGraph standard cameras. Mode 1 hands over camera control to esmini. In this mode, switch camera model by pressing 'k' which will toggle between all available esmini camera model.

The default camera model is esmini "orbit" which allows for rotating (left-mouse button), zooming (right-mouse button) and panning (scroll wheel / middle mouse button) while following the first entity.

To follow another vehicle press Tab (next vehicle) or Shift TAB (previous vehicle). Also Backspace is mapped to previous vehicle, since shift-TAB do not work on all platforms.

For a complete list of cameras and functions, see Command reference.

esmini camera mode can be selected by launch argument. Here are a few common use cases:

Follow exact behind vehicle:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --camera_mode fixed

Orthogonal (no perspective) top view follow vehicle:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --camera_mode top

Follow vehicle from inside "driver" point of view:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --camera_mode driver

Follow vehicle with some flex and allow rotate, pan and zoom:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --camera_mode flex-orbit

In addition to pre-defined camera modes, the user can add custom cameras from API or via launch argument. Custom cameras can be fixed, semi-fixed (fixed position but unconstrained orientation) or relative current vehicle. Here is a few examples:

Custom camera in front of vehicle, e.g. sensor mount position: ./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --custom_camera 3,0,0.6,0,0

Custom camera with fixed position but dynamic orientation, always pointing at current vehicle:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --custom_fixed_camera 170,10,5

Custom fixed camera:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --custom_fixed_camera 240,15,10,3.5,0.2

Custom fixed top view camera:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --custom_fixed_top_camera 180,-1.54,2301,4.71239

4.1.3. Some key shortcut commands

Space

Toggle pause/play simulation

Enter

Step simulation one frame forward

Tab

Move camera to next vehicle

Shift-Tab or Backspace

Move camera to previous vehicle

'j'

Toggle show entity trail points and lines (press mulitpe times to switch modes)

','

Toggle bounding box modes

Esc

Quit

See Command reference for a complete list of key shortcut commands.

4.1.4. Road network visualization

Visualize OpenDRIVE road features:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --road_features on

If a 3D model representation of the road network is missing (i.e. empty or missing SceneGraphFile element of the OpenSCENARIO RoadNetwork class), then esmini will generate a very simple model.

Example:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc

Add optional flat ground plane:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --ground_plane

The generated model can be saved:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --save_generated_model
Then look for generated_road.osgb in the current directory.

4.1.5. Background color

esmini default background color is skyish, light blue. Change by launch argument --clear-color <r,g,b>, where r, g, b are the red, green, blue components as floating numbers in the range (0:1). Some examples:

Black background:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --clear-color 0,0,0

White background:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --clear-color 1,1,1

Gray background:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --clear-color 0.3,0.3,0.3

4.1.6. Anti-alias

esmini make use of OpenSceneGraph sub-sampling method for anti-alias. Default setting is 4 (samples). This can be controlled by the launch argument --aa_mode <samples>. To disable anti-alias:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --aa_mode 0

4.2. Screenshots and video clips

esmini can save screenshots in uncompressed TGA format. These images can be converted into other image formats or even video clips in a post process.

esminiLib also supports grabbing screenshots via API, also from a pure off-screen rendering setup (requiring neither monitor nor graphics hardware). See image-capture code example.

Here follows a few examples using launch arguments and runtime key commands.

4.2.1. Save screenshots

Press 'c' at any time to store a single screenshot.

Press 'C' at any time to start storing screen shot for every frame onward. Press 'C' again to stop.

4.2.2. Create video clip of a scenario

First, run the scenario and automatically save screenshots for all frames:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --fixed_timestep 0.033 --capture_screen

Note We added the --fixed_timestep argument to make sure we get a smooth video with constant delta-time for a smooth and continuous video clip. If --fixed_timestep is skipped esmini will progress time between frames according to actual system time passed since last frame, which can vary over time for many reasons (e.g. due to esmini or other processes CPU load).

Then, create a video clip using ffmpeg:
ffmpeg -f image2 -framerate 30 -i screen_shot_%5d.tga -c:v libx264 -vf format=yuv420p -crf 20 out.mp4

The above ffmpeg command will create a MPEG4 video with reasonable compression (crf 20). To create a lossless, but still compressed, video e.g. for further post processing:

ffmpeg -f image2 -framerate 30 -i screen_shot_%5d.tga -c:v libx264 -qp 0 out.mp4

To change framerate of the output video, use the fps filter. E.g. from input 30 fps to output 15 fps:

ffmpeg -f image2 -framerate 30 -i screen_shot_%5d.tga -c:v libx264 -vf format=yuv420p,fps=15 -crf 20 out.mp4

Some details regarding H.264 encoding can be found here.

4.2.3. Off-screen rendering

Whenever esmini is launched with the --window <x-pos> <y-pos> <width> <height> argument a viewer will be created with specified window. The --headless flag controls whether any window will be "on screen" or only off-screen. Of course, --headless in combination with no window (no --window argument) will skip rendering altogether, which is a very performance-saving mode to use when possible.

Example:
./bin/esmini.exe --window 60 60 4000 2000 --headless --capture_screen --osc .\resources\xosc\cut-in.xosc
will render images into a virtual frame buffer of size 4k x 2k pixels and then store to file. Note: The size of the virtual frame buffer is not limited by size of any connected display.

To run esmini completely without rendering, just omit the --window argument:
./bin/esmini.exe --osc .\resources\xosc\cut-in.xosc --fixed_timestep 0.01 --record sim.dat
will run the specified scenario quickly and store a .dat file for later analysis or viewing (with replayer).

Note: If --fixed_timestep is omitted esmini will adapt timesteps to actual frametime, so a 30 seconds scenario will take 30 seconds - i.e. no performance gain.

4.2.4. Software rendering (no gfx hw)

By default any available 3D graphics hardware will be utilized. As fallback esmini (via OSG) will utilize Mesa3D which is a software implementation of the OpenGL graphics stack including parts normally hosted in the graphics hardware system. The Mesa3D approach is useful when running on cloud/cluster machines lacking graphics hardware, perhaps even lacking a window system (like slimmed and headless Ubuntu without X11 support).

Mesa3D on Linux
Mesa3D is normally installed with Linux distributions. Check OpenGL support with the following command:
glxinfo -B

If the command is not available, then Mesa3D utility package is probably missing. Install as:
sudo apt install mesa-utils

To run headless with a virtual frame buffer:

  • Activate xvfb:
    Xvfb :99 -screen 0 1920x1080x24+32 & export DISPLAY=:99

  • Then run esmini as usual

For troubleshooting, the following command might give information about the graphics system:
lspci -vnn | grep VGA -A 12

Mesa3D on Windows

  • Grab the Mesa3D binary from here.

  • Unpack and place opengl32.dll in the same folder as esmini executable.

  • Then run esmini as usual

4.3. Logging

esmini can produce log files in different formats and for different purposes, explained next.

4.3.1. The basic text log file

By default esmini is creating a log.txt in the folder from which esmini is launched. In case of esminiLib it will end up in the folder that the application linking esminiLib was launched from.

The log.txt includes the same information normally seen in the terminal window (stdout).

To disable output to terminal:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --disable_stdout

To disable creation of the logfile (log.txt):
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --disable_log

Developer info: Define the symbol DEBUG_TRACE (either as compile flag or by uncomment this code line) to log more details, like what code module and line number is the origin of the log entry.

4.3.2. Scenario recording (.dat)

In addition esmini can save a .dat file which captures the state of all entities. This file can later be used either to replay (see Replay scenario) the scenario or converted to .csv for further analysis, e.g. in Excel.

To create a recording with regular timesteps:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --fixed_timestep 0.05 --record sim.dat

To convert the .dat file into .csv, do either of:
./bin/dat2csv sim.dat
or
python ./scripts/dat2csv.py sim.dat

Only a subset of the .dat file information is extracted. To extract some more info, e.g. road coordinates, run: ./scripts/dat2csv --extended sim.dat

You can create .dat files from multiple scenarios in a single command, like this on Linux (bash):

for f in ./my_scenarios/*.xosc; do ./bin/esmini --headless --fixed_timestep 0.05 --osc $f --record `basename $f .xosc`.dat; done

It also works in Git bash on Windows.

4.3.3. CSV logger

To create a more complete csv logfile, compared to the content of the .dat file, activate the CSV_Logger:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --fixed_timestep 0.05 --csv_logger full_log.csv

full_log.csv will contain more detailed states for all scenario entities. To also include collision detection:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --fixed_timestep 0.05 --csv_logger full_log.csv --collision

All collisions (overlap) between entity bounding boxes will be registered in the collision_ids column of each entity. It will contain the IDs of any entities overlapping at given frame.

4.4. Replay scenario

Replay a scenario recording (.dat file):
./bin/replayer --window 60 60 800 400 --res_path ./resources --file sim.dat

The --res_path is a special path directive should normally point to esmini/resources folder, where it will look for the OpenDRIVE file, 3D models and model_ids table (model_ids.txt). It also works to add one or multiple --path <path> directives, like for esmini.

Once loaded the scenario recording will start playing in normal speed. Here are a few key commands:

Space

Toggle pause/play simulation

Tab

Move camera to next vehicle

Shift-Tab or Backspace

Move camera to previous vehicle

'n'

Toggle show active trajectories

','

Toggle bounding box modes

Esc

Quit

Arrow keys:

Left

Pause and move to previous frame

Right

Pause and move to next frame

Shift + Left

Pause and jump 10 frames back

Shift + Right

Pause and jump 10 frames forward

Ctrl + Left

Jump to beginning

Ctrl + Right

Jump to end

See Command reference for a complete list of key shortcut commands.

Some launch arguments:

--collision

Pause and move to previous frame

--quit_at_end

Quit application when reaching end of scenario

--repeat

Loop scenario

--dir <path>

Directory containing replays to overlay, pair with "file" argument, where "file" is .dat filename match substring

Examples:

Enable collision detection and pause playing at every occasion of collision:
./bin/replayer --window 60 60 800 400 --file sim.dat --res_path ./resources --collision

Video clip demonstrating some features in replayer

The .dat file in the clip above was created with the command:
./bin/esmini --headless --osc ./resources/xosc/ltap-od.xosc --disable_controllers --fixed_timestep 0.033 --record sim.dat

and then launched in replayer:
./bin/replayer --window 60 60 800 400 --file sim.dat --res_path ./resources --collision

Note: The --disable_controllers flag is just specified to disable the interactive controller that normally is activated in that specific scenario. For more information about controllers, see Controllers in esmini.

Combine esmini and replayer in a one-liner command:
./bin/esmini --headless --fixed_timestep 0.05 --record sim.dat --osc ./resources/xosc/cut-in.xosc;./bin/replayer --window 60 60 800 400 --res_path ./resources --file sim.dat

With bash (use Git Bash on Windows) preview all scenarios (.xosc files) in a folder, at double speed:
for f in my_scenarios/*.xosc; do ./bin/esmini --headless --fixed_timestep 0.05 --osc $f --record basename $f .xosc.dat;./bin/replayer --window 60 60 1200 600 --res_path ./resources --file basename $f .xosc.dat --time_scale 2 --quit_at_end; done

just replace my_scenarios with your folder path

4.4.1. Multiple scenarios in parallel

Next example, play multiple variants of a scenario in parallel:
./bin/replayer --window 60 60 800 400 --file sim --res_path ./resources --capture_screen --dir tmp

The --dir argument points to a folder containing sim1.dat, sim2.dat and sim3.dat.
The --capture_screen argument will save each frame as an image file on disk.

Video clip demonstrating parallel scenarios in replayer

4.4.2. Save merged .dat files

Merged data from multiple .dat files can be saved into one single .dat file, e.g. for plotting:
./bin/replayer --file sim_ --dir . --save_merged sim_merged.dat

replayer will look in current folder (".") for any .dat file starting with "sim_", i.e. "sim_*.dat". The files will be merged and saved as sim_merged.dat.

4.5. View road network

4.5.1. Visualize OpenDRIVE geometry

By default odrviewer will create a simple 3D model of the OpenDRIVE description and populate it with sparse traffic:
./bin/odrviewer --window 60 60 800 400 --odr ./resources/xodr/fabriksgatan.xodr

Add OpenDRIVE features, like reference line and lanes, on top (toggle on 'o'):
./bin/odrviewer --window 60 60 800 400 --odr ./resources/xodr/fabriksgatan.xodr --road_features

Remove all traffic, just visualize the road geometry:
./bin/odrviewer --window 60 60 800 400 --odr ./resources/xodr/fabriksgatan.xodr --density 0

4.5.2. Evaluate OpenDRIVE connectivity

Increase traffic to exercise all junctions and lanes of the road network:
./bin/odrviewer --window 60 60 800 400 --odr ./resources/xodr/fabriksgatan.xodr --density 7 --road_features

Use custom 3D model:
./bin/odrviewer --window 60 60 800 400 --odr ./resources/xodr/fabriksgatan.xodr --model ./resources/models/fabriksgatan.osgb --density 5

Slow down traffic:
./bin/odrviewer --window 60 60 800 400 --odr ./resources/xodr/fabriksgatan.xodr --model ./resources/models/fabriksgatan.osgb --density 3 --speed_factor 0.6

4.5.3. Inspect OpenDRIVE geometry and road IDs

esmini odrplot is a small application that creates a track.csv file that can be plotted with another small Python script xodr.py:
./bin/odrplot ./resources/xodr/fabriksgatan.xodr
./EnvironmentSimulator/Applications/odrplot/xodr.py track.csv

Combine in a one-liner:
./bin/odrplot ./resources/xodr/fabriksgatan.xodr;./EnvironmentSimulator/Applications/odrplot/xodr.py track.csv

To navigate in the plot, click the four-arrow icon (see image below) and then use mouse according to:

Left button

Pan (move left, right, up, down)

Right button

Zoom

odrplot help

4.6. Plot scenario data

esmini can plot data in two ways: Off-line (plot content of .dat files) and runtime (a set of pre-defined values).

4.6.1. Off-line plotting

esmini provides a Python based script, plot_dat.py, to plot information in .dat files.

First, create a .dat file. For that we don’t need a window. Run esmini headless:
./bin/esmini --headless --fixed_timestep 0.05 --osc ./resources/xosc/acc-test.xosc --record sim.dat

The scenario looks like this:

Next, plot speed over time:
./scripts/plot_dat.py sim.dat --param speed

plot dat

To see what parameters are available for plot:
./scripts/plot_dat.py sim.dat --list_params

will output a list similar to:
Plottable parameters:
id, model_id, obj_type, obj_category, ctrl_type, time, speed, wheel_angle, wheel_rot, centerOffsetX, centerOffsetY, centerOffsetZ, width, length, height, scaleMode, visibilityMask, x, y, z, h, p, r, roadId, laneId, offset, t, s

Most parameters does normally not make sense to plot, but it depends on the test case. Maybe it’s interesting to see when a car makes a lane change. Plot laneId over time:
./scripts/plot_dat.py sim.dat --param laneId

plot dat lane id

A plot can show multiple parameters:
./scripts/plot_dat.py sim.dat --param laneId --param speed

plot dat lane speed

To plot trajectories, change X-axis parameter from time to x and plot y parameter (over x):
./scripts/plot_dat.py sim.dat --x_axis x --param y

plot dat x y

Lock axis aspect ratio, i.e. make axis scale equal:
./scripts/plot_dat.py sim.dat --x_axis x --param y --equal_axis_aspect

plot dat x y eq axis

Plot merged .dat files (created as described in Save merged .dat files)

./scripts/plot_dat.py --param speed sim_merged.dat

plot merged dat files

Entity IDs will get an offset multiplier of 100, corresponding to the order of which the dat files were read by replayer. The mapping between ID range and dat file is printed to the console by replayer when the merged dat file is created, as the following example of four input files:

Scenarios corresponding to IDs (0:99): sim_fsm.dat
Scenarios corresponding to IDs (100:199): sim_ref.dat
Scenarios corresponding to IDs (200:299): sim_reg.dat
Scenarios corresponding to IDs (300:399): sim_rss.dat

4.6.2. Runtime plotting

From version 2.32.0 esmini can plot some pre-configured data values during simulation. Simply add launch flag --plot.

Features so far:

  • One or multiple entities can be selected by checkboxes on top

  • Selection of variables can be reduced by checkboxes to the right

implot
Screenshot of a cut-in scenario with line plotting feature enabled

By default the plotting is executing in a separate thread, having minimal impact on esmini performance. It is also possible to execute in synchronous mode, where plotting is part of esmini main thread and stepping is done in synch. The mode can be specified explicitly. Examples:

Simply enable plotting, default mode:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --fixed_timestep 0.05 --plot

Enforce synchronous mode:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --fixed_timestep 0.05 --plot synchronous

Asynchronous mode can be specified as well, although it’s default hence not needed:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --fixed_timestep 0.05 --plot asynchronous

Note: On macos OpenGL window must be part of main thread. Hence synchronous mode is enforced on macos.

4.7. Save OSI data

esmini can provide OSI groundtruth data in three ways:

  1. Send over UDP

  2. API to fetch via function call from a custom application

  3. Save to OSI trace file

Only trace file way is described here. To save OSI data, add argument --osi_file [filename]. The filename is optional. If omitted it will be named ground_truth.osi. Example:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --osi_file

will create ground_truth.osi in the current folder. The format is according to the OSI standard "Binary trace file", see OSI documentation 2.2.6 OSI trace files.

esmini provides a script, osi2csv.py, that converts an OSI trace file into .csv format. The script can also serve as example how to parse and extract data from an OSI trace file:

./scripts/osi2csv.py ground_truth.osi

will create ground_truth.csv.
(if file type association is not setup, try: python ./scripts/osi2csv.py ground_truth.osi)

However the .csv file created with osi2csv will not contain all OSI information. To get a readable text file including the complete content of a OSI trace file you can make use of the Python script format/osi2read.py in the OSI project.

Use as following example:

../open-simulation-interface/format/osi2read.py --data ./ground_truth.osi --output ground_truth --type GroundTruth

it should create ground_truth.txth which is readable in a text editor.

Note:

See OSI for Python on Windows for hints how to install dependencies (OSI and Protobuf).

The above script link points to OSI tag v3.5.0. In more recent OSI versions the script has moved to another folder: osi3trace/osi2read.py.

5. Scenario features

5.1. Speed profile

(introduced in esmini v2.23.0)

The OpenSCENARIO SpeedAction will reach a specified speed in a way described by additional attributes, e.g. shape and duration. To achieve multiple speed target over time you would need to add multiple SpeedAction events to the scenario. Moreover, there is no way to explicitly control the jerk (acceleration/deceleration rates).

However with the SpeedProfileAction (introduced in OpenSCENARIO v1.2) you can specify a series of speed targets over time, in one action. Optionally you can also specify dynamic constraints for jerk (gradually changing acceleration and deceleration), acceleration and speed.

If you run the example scenario speed-profile.xosc as follows:

./bin/esmini.exe --window 60 60 800 400 --osc ./resources/xosc/speed-profile.xosc --fixed_timestep 0.01 --record sim.dat
./scripts/plot_dat.py sim.dat --param speed

you should get the following plotted speed curves:

speed profile1

Note: You might need to add python or python2 in front of the python command. And matplotlib is needed, see odrplot in Tools overview.

Tip: For quick experiments, skip the visualization and bring the plot asap by replacing the two commands with:

./bin/esmini.exe --headless --osc ./resources/xosc/speed-profile.xosc --fixed_timestep 0.01 --record sim.dat;./scripts/plot_dat.py sim.dat --param speed

The scenario includes two cars with identical speed profiles with one exception: The white Car1 use activate dynamic constraints by setting FollowingMode="follow" while the red Car2 will apply constant acceleration (linear interpolation) between speed targets by setting FollowingMode="position".

The action for white car:

<SpeedProfileAction followingMode="follow">
    <DynamicConstraints
        maxAcceleration = "5.0"
        maxDeceleration = "10.0"
        maxAccelerationRate = "4.0"
        maxDecelerationRate = "3.0"
        maxSpeed = "50"
    />
    <SpeedProfileEntry time="0.0" speed="0.0"/>
    <SpeedProfileEntry time="4.0" speed="10.0"/>
    <SpeedProfileEntry time="4.0" speed="4.0"/>
    <SpeedProfileEntry time="2.0" speed="8.0"/>
</SpeedProfileAction>

The time values are relative each other and start of the action. In this case the action is triggered at simulation time = 2 seconds. Initial speed for both cars is 0. First entry has therefor no effect since it applies speed = 0 at time = 2 (2 + 0). After additional 4 seconds (sim time = 6s) the speed target is 10 m/s. At sim time 10s the speed target is 4 m/s and finally after 2 more seconds the final speed target value is 8.0 m/s.

The "follow" mode deserves some additional explanation. As shown in the figure, it start and ends with zero acceleration. Then is basically will try to match the acceleration "lines" but cutting the corners according to acceleration and deceleration rate constraints. This way the intermediate speed values will not always be reached. However, the final speed value will be reached.

If the target speed or accelerations can’t be reached with given constraints the action will revert to linear mode (FollowingMode="position") for the remainder of the profile. This "failure" is logged.

Another approach would be to try to perform a best effort, but that would require additional input to decide whether to prioritize reaching specified speed targets or respect time stamps…​

Note: The implementation of this feature is preliminary and experimental. Behavior and details might change.

Let’s manipulate the scenario in different ways to illustrate some special cases of the speed-profile feature.

5.1.1. Special case: Single entry

(Special case implementation introduced in esmini v2.23.1)

Compared to SpeedAction, the SpeedProfileAction offers more tools in terms of dynamic constraints. Hence it can be actually be useful also for single entry, i.e. reach a single target speed.

The implementation differs for the single entry case. Target speed will be reached if constraints allows for it. If not, the speed will still be reached, but later than specified.

There are three sub cases:

1. Speed can be reached within time

The speed profile will contain three phases: Jerk, constant acceleration, jerk.

Example:
Replace the four entries in speed-profile.xosc with the following ones:

    <SpeedProfileEntry time="0.0" speed="0.0"/>
    <SpeedProfileEntry time="4.0" speed="10.0"/>
speed profile5

Initial positive jerk will be applied until necessary acceleration is reached. Keep constant acceleration (linear segment in the speed profile) until negative jerk needs to be applied in order to reach target speed on time and at zero acceleration.

2. Speed can’t be reached in time due to acceleration constraints

This speed profile will also contain three phases: Jerk, constant acceleration, jerk.

Example:
Replace the four entries in speed-profile.xosc with the following ones:

    <SpeedProfileEntry time="0.0" speed="0.0"/>
    <SpeedProfileEntry time="3.0" speed="10.0"/>
speed profile6

Initial positive jerk will be applied until maximum acceleration is reached (or maximum deceleration). Keep constant acceleration (linear segment in the speed profile) until negative jerk needs to be applied in order to reach target speed at zero acceleration. Due to the acceleration limitation there will be a delay as well. The log file will include something like:

SpeedProfile: Constraining acceleration from 5.86 to 5.00
SpeedProfile: Extend 0.46 s

3. Speed can’t be reached in time due to jerk constraints

This speed profile will contain only two jerk phases.

Example:
Keep entries from last case, but change jerk settings as follows:

  maxAccelerationRate="3.0"
  maxDecelerationRate="2.0"
speed profile7

In this case the jerk settings are too weak to reach target speed in time. Not enough acceleration can be achieved in the given time window.

Positive jerk will be applied until negative jerk has to be applied in order to reach target speed at zero acceleration. Hence there is no room for a phase of constant acceleration. Due to the jerk limitation there will be a delay. The log file will include something like:

SpeedProfile: Can't reach target speed 10.00 on target time 3.00s with given jerk constraints, extend to 4.08s

5.1.2. What if current speed differ from the first entry?

Replace the four entries with the following ones:

    <SpeedProfileEntry time="0.0" speed="3.0"/>
    <SpeedProfileEntry time="4.0" speed="10.0"/>
speed profile2

What we see here is that for linear mode (FollowingMode="position") the speed of the first entry will apply immediately regardless of the current speed at the time of the action being triggered. For constrained mode (FollowingMode="follow") we see that the initial speed value (3.0) is overridden by the current speed (0.0). From there it will strive for the second entry, obeying the constraints.

The overall idea with the "follow" mode is to maintain continuity in the speed profile, up to jerk degree.

5.1.3. What if time is missing in entry?

Replace any entries with the following ones:

    <SpeedProfileEntry speed="10.0"/>
speed profile3

Specified max acceleration will be applied until target speed is reached. Note: In the non-linear case and with multiple entries, the function will fail if the specified acceleration can’t be reached with given jerk constraints (maxAcceleration and maxDeceleration). Try to lower the maxAcceleration/deceleration in this case.

You can also combine entries with and without time constraint, like in following example:

    <SpeedProfileEntry speed="10.0"/>
    <SpeedProfileEntry time="3.0" speed="15.0"/>
speed profile4

Car will accelerate until speed 10 m/s is reached, then spend 3 seconds to reach 15 m/s.

5.1.4. Initial acceleration taken into account

(from release v2.23.2)

What if the acceleration is not zero when the SpeedProfileAction is started, for example interrupting an ongoing SpeedAction in Follow mode?

To maintain a continuous acceleration profile the action will use current acceleration as initial value. The standard states that the acceleration is expected to be zero at start and end of the action. The esmini interpretation is that the CHANGE is zero at start while the ACTUAL value is zero at end (since the action can only control acceleration while being active, not before).

Example: Once again starting from speed-profile.xosc, instead of setting an instant initial speed make it ramp up from 0 to 5 m/s over a duration of 4 seconds by tweaking the initial speed actions, for both entities, as below:

    <SpeedActionDynamics dynamicsShape="linear" value="5.0" dynamicsDimension="time"/>
    <SpeedActionTarget>
        <AbsoluteTargetSpeed value="4.0"/>
    </SpeedActionTarget>
speed profile8

The initial speed action will apply constant acceleration until time = 2.0 seconds, when the SpeedProfileAction is triggered. While the linear profile will jump to 0 m/s (as specified in first entry) the follow mode profile will just apply necessary jerk to reach target acceleration with respect to following entries.

Initial acceleration is also respected for the special case of a single entry, for example:

    <SpeedProfileEntry time="3.0" speed="10.0"/>

the result becomes:

speed profile9

5.1.5. Sharp brake-to-stop profile

To skip jerk phase, e.g. as in emergency brake with an abrupt stop, just set AccelerationRate to a large number.

Example: Once again starting from speed-profile.xosc, change speed in the Init speed actions to 10.0 and replace the speed profile actions as below:

    <SpeedProfileAction followingMode="follow">
        <DynamicConstraints
            maxAcceleration = "5.0"
            maxDeceleration = "10.0"
            maxAccelerationRate = "1E10"
            maxDecelerationRate = "3.0"
            maxSpeed = "50"
        />
        <SpeedProfileEntry time="0.0" speed="10.0"/>
        <SpeedProfileEntry time="4.0" speed="0.0"/>
    </SpeedProfileAction>
speed profile10

Note: A drawback is that the setting will affect any acceleration phase in the complete profile. In other words, a limitation is that entries can’t have individual settings. In the example above it’s not problem since the first jerk phase is a deceleration while the second is an acceleration. If different rate values of same type are needed, it can be achieved by defining multiple SpeedProfile actions in a sequence, with individual performance settings.

5.1.6. More info

To get more understanding of the implementation, see a few slides here.

5.2. Road signs

(framework updated in esmini v2.25.0)

5.2.1. The system

Road signs are specified in the OpenDRIVE road network description file. A road sign is identified by up to four parameters:

  1. country code

  2. type

  3. subtype

  4. value

Country code is a two letter word according to the ISO 3166-1 alpha-2 system. Examples: Sweden = se, Germany = de.

Type and subtype defines the semantic meaning of the sign. The value is used when applicable to further specify information, e.g. speed or weight. Data type and meaning of the parameters may differ between countries. However a mapping to each country’s system is often feasible.

Examples: In Sweden a speed sign belongs to the category prohibition signs named "c". Within that category speed signs is a sub group with index 31. Within that group each sign has a value of speed/10, e.g. 5=50km/h, 10=100km/h. So the parameters for a Swedish speed limit 90 km/h sign becomes: se, c, 31, 9.

In Germany there are no similar categories. Instead all signs have a unique group or type number, e.g. speed signs make up group 274. Then the value defines the speed. No need for subtype. So the parameters for a German speed limit 90 km/h sign becomes: de, 274, -, 90.

5.2.2. esmini sign catalogs

Instead of hard-coding road sign support esmini will lookup the OSI code from a separate sign definition file, which is unique for each country. Name convention for the file is: country code + "_traffic_signals.txt". Example: Swedish catalog is named "se_traffic_signals.txt".

The file is a simple text format each line defining a sign ID and corresponding OSI type.

Swedish example: c.31-7=TYPE_SPEED_LIMIT_BEGIN
German example: 274=TYPE_SPEED_LIMIT_BEGIN

Type is mandatory. For esmini to distinguish between subtype and value different separators are used: "." before subtype and "-" before value. For some signs the value is optional, e.g. for speed signs it simply specify the speed limit. But for some signs it is used as a subtype to identify a variant of a sign, as in the German 101 type category.

Example files can be found in esmini/resources/traffic_signals.

Currently (v2.25.0) the esmini sign support is very limited. However, it should be fairly straight forward to add signs into existing catalogs and even catalogs (for more countries).

5.2.3. Sign 3D models

esmini road signs are created as follows:

  • A basic 3D geometry is modeled in Blender, e.g. round face for speed signs

  • Assign a material with any texture image mapped on the sign face

  • The 3D model is exported in .dae format (example here)

  • Find textures of the needed sign(s) and convert them into some common format, e.g. .png. Recommended size is 256, but larger is OK. If possible make it size 2^n x 2^m to avoid performance penalty on low spec/old systems.

  • Rename image to the same name as the texture image in the dae file (sign_image.png in the example dae)

  • Convert the .dae file into .osgb using osgconv. The texture will be embedded in the.osgb file by default.

speed sign blender
Speed sign in Blender

A script is available as a starting point for automating the two last steps in the process above.

5.2.4. Example

The OpenDRIVE file straight_500m_signs.xodr includes a bunch of speed signs, some Swedish and some German ones. It shows how the signs are specified in terms of the country code, type, subtype and value attributes.

It also shows how to specify the 3D model using the name attribute: If filename composed by country, type, subtype and value is not found, the name attribute + ".osgb" will be used for a second and final attempt.

speed sign1
Swedish speed sign
speed sign2
German speed sign

5.3. Expressions

From OpenSCENARIO v1.1 expressions including mathematical operations are supported in assignment of value to OpenSCENARIO parameters and XML element attributes. Examples:

<ParameterDeclaration name="PI" parameterType="double" value="3.14159"/>
<ParameterDeclaration name="KPH2MPS" parameterType="double" value="1/3.6"/>

...

<Position>
    <LanePosition roadId="1" laneId="-1" s="40" offset="0">
        <Orientation type="absolute" h="${sin(0.25*$PI)}"/>
    </LanePosition>
</Position>

...

<SpeedActionTarget>
    <AbsoluteTargetSpeed value="${110 * $KPH2MPS}"/>
</SpeedActionTarget>

h and (speed value) evaluates to 0.707106 and 30.55558 respectively

Supported expression operators and functions

Operators

+

-

*

multiply

/

%

IEEE 754 remainder, see C++ std::remainder

**

power

<

<=

>

>=

==

equal

!=

not equal

&&

logical AND

||

logical OR

See expr.h for complete and updated list.

Functions

round

see C++ rint (Round to nearest, halfway cases to even. Aligned with IEEE 754 default)

floor

see C++ floor

ceil

see C++ ceil

sqrt

square root

pow

see C++ pow

Following functions are supported by esmini, but not specified in OpenSCENARIO <= 1.2, hence use with care since they may not work in other tools

sin

cos

tan

asin

acos

atan

sign

abs

max

min

See simple_expr.c for complete and updated list.

Strings

From v2.35.0 string concatenation is supported. Whenever a string is detected that is netiher an opeator nor a function, the complete expression is treated as a string and '+' (plus) operator will merge surrounding evaluated strings which also can be evaluated parameters.

This can be useful when running permutations of scenarios, where names of actors or storyboard elements can be made unique by including parameters values.

Examples:

${Event_overtake_ + $ActorName}Event_overtake_Target4

${Event_change_speed_to_ + $Speed}Event_change_speed_to_110

5.4. Parameter distributions

Parameter values and distributions can be specified in a separate Parameter value distribution file. This is a convenient way of running multiple variations, or parameter value permutations, of a single scenario.

Here’s an example: cut-in_parameter_set.xosc.

To run all permutations with esmini:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc

How to abort:

"ESC" (in window) will terminate current run
"Ctrl-C" (in terminal) will terminate all runs

The log files are now named according to: log_<permutation>_of_<total#permutations>.txt

For example:

log_1_of_6.txt relates to the 1:th run of 6, permutation index 0
log_4_of_6.txt relates to the 4:th run of 6, permutation index 3

Of course this will apply also to custom log filenames. Example:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc --logfile_path my_scenario.txt

will result in my_scenario_1_of_6.txt…​

Also any recording (.dat), osi and csv files will be named in similar way. Example:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc --record sim.dat --osi_file gt.osi --csv_logger data_log.csv

will result in complete sets of files:
log_1_of_6.txt …​
sim_1_of_6.dat …​
gt_1_of_6.osi …​
data_log_1_of_6.csv …​

To run a specific permutation only, specify index like this:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc --param_permutation 2

will run 3:rd permutation (of 6 available in this case)

Save the resulting OpenSCENARIO file with populated parameter values:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc --save_xosc

will give cut-in_1_of_6.xosc…​

To view all results in parallel, make use of replayer like this:

./bin/esmini --headless --fixed_timestep 0.05 --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc --record sim.dat ; ./bin/replayer --window 60 60 800 400 --res_path ./resources/ --file sim_ --dir .

param dist0
View all permutations in parallel

5.4.1. Parallel execution

Making use of Python threading pool framework we can utilize any multiple CPU kernels and run scenario variants in parallel. This is handled by the script scripts/run_distribution.py.

Usage: run_distribution.py <esmini args>

Make sure to add at least the following esmini arguments:
--osc <scenario file>
--param_dist <parameter distribution file>
--fixed_timestep <timestep>
--headless

Example:

python ./scripts/run_distribution.py --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc --fixed_timestep 0.05 --headless

Of course, the --headless (and --fixed_timestep) is not required. It is possible to run with viewer as well:

python ./scripts/run_distribution.py --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc --window 60 60 800 400

but the windows will open on top of each other at the same location on the screen, so you need to manually move them apart to see the parallel runs. Anyway, this use case is probably not relevant so the recommendation is to run headless with fixed time step.

Putting it all together, execute the scenario permutations in parallel and then view the result in replayer:

python ./scripts/run_distribution.py --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc --fixed_timestep 0.05 --headless --record sim.dat ; ./bin/replayer --window 60 60 800 400 --res_path ./resources/ --file sim_ --dir .

5.4.2. Finding out number of permutations

To find out the number of permutations of a specific scenario and parameter distribution, use the --return_nr_permutations launch argument. Example:

./bin/esmini --osc ./resources/xosc/cut-in.xosc --param_dist ./resources/xosc/cut-in_parameter_set.xosc --return_nr_permutations

Output: Nr permutations: 6

For scripters: esmini will also return the number of permutations as exit code, for simple use in scripts. After executing the above the exit code can be inspected/verified from shell as well:

PowerShell:
echo $LastExitCode
6

bash:
echo $?
6

5.5. Trajectories

Using OpenSCENARIO FollowTrajectoryAction you can define various types of trajectories for entities to follow.

esmini supports all three (as of OpenSCENARIO 1.2) shapes:

  • polyline

  • clothoid

  • nurbs

Here follows a few design choices for the polyline type that affect the behavior.

When FollowMode is follow esmini will:

  1. Calculate heading based on direction of the line segments. However corners are interpolated in order to look better and avoid discontinuities.

  2. Calculate speed based on current speed (when action is triggered) and then with constant acceleration per segment reach destination vertex on time (or earlier if distance is too short vs time). This approach results in a continuous speed profile.

When FollowMode is position esmini will:

  1. For world coordinates, apply whatever heading is specified in the vertices (note that 0.0 is default). Lane and road coordinates will align to road direction as default.

  2. For each segment, calculate constant speed needed to reach next vertex on time. This approach results in a discontinuous speed profile.

5.6. Friction

From v2.37.0 esmini supports road friction defined in OpenDRIVE lane material.

Each lane within a lane section can have one or multiple segments of individual friction values, specified by s-value and friction coefficient.

Default friction coefficient is 1.0 as of v2.37.0, but might change in later versions. Lower friction (<1.0, slippery) will be visualized as bluish color. The lower friction, the more blue color. High friction values (>1.0, "grippy") is indicated by redish color.

The friction is calculated per wheel. The values are reported in OSI ground-truth.

friction
Road with various friction segments, coefficient evaluated per wheel

5.7. SUMO integration

SUMO is a great open source traffic simulation tool. It has been integrated with esmini, making it possible to run SUMO simulations in esmini, and even mix or co-simulate SUMO vehicles with OpenSCENARIO vehicles.

NOTE: Even though SUMO has been linked with esmini for quite long, it’s on a experimental level and has not been used a lot. Hence, expect shortcomings and issues. The currently integrated SUMO version, as of esmini v2.37.1, is 1.6.0 while the current SUMO version is 1.19.0. There is plans to update SUMO version in esmini in a soon future.

This chapter explains how to create a SUMO simulation and then how to use it in esmini. For just using SUMO simulations there is no need to install SUMO, it is already embedded with esmini (as long as it has not been explicitly excluded, see Slim esmini - customize configration). Only to create SUMO content it has to be installed.

5.7.1. Install SUMO

  1. Get SUMO from here: https://eclipse.dev/sumo/

  2. Install normally, check any option to add sumo to environment path variable

5.7.2. Try example from esmini

  1. Launch sumo-gui application

  2. File → Open Simulation

  3. Navigate to esmini/resources/sumo_inputs and select multi_intersections.sumocfg

  4. Set Delay (ms) to 100, otherwise simulation will run super-quick

  5. Run (Play button or Simulation → Run)

5.7.3. Running the same scenario in esmini

  1. In a terminal, from esmini root folder, run:

  2. ./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/sumo-test.xosc

5.7.4. Create a SUMO simulation

First, there is a great complete guide at https://sumo.dlr.de/docs/

The main flow of our example is:

  1. Convert an OpenDRIVE file into a SUMO road network

  2. Identify some trips (start and end points) within the road network

  3. From the trips, resolve complete routes wthich are assigned to vehicles and distribute over time

  4. Create a configuration referring to the road network and route definition

Step-by-step:

  1. Create a new folder, e.g. "my_sumo", for this example

  2. Copy esmini/EnvironmentSimulator/Unittest/xodr/simple_3way_motorway.xodr into the new folder

  3. Open a terminal in the new folder

  4. Convert the xodr into a SUMO road network:
    netconvert --opendrive fabriksgatan.xodr -o my_sumo.net.xml

  5. Create some traffic using randomTrips.py (and duarouter):
    randomTrips.py -n my_sumo.net.xml -e 60 -o my_sumo.trips.xml --route-file my_sumo.rou.xml

    If this fails, it may be because of lacking .py file association. Try:

    python3 randomTrips.py ...
    python randomTrips.py ...
    python3 tools/randomTrips.py ...
    python tools/randomTrips.py ...
  1. Create the configuration file

    Save the following lines into a new file named "my_sumo.sumocfg":

    <?xml version="1.0" encoding="iso-8859-1"?>
    <configuration>
        <input>
            <net-file value="my_sumo.net.xml"/>
            <route-files value="my_sumo.rou.xml"/>
        </input>
        <time>
            <begin value="0"/>
            <end value="60"/>
            <step-length value="0.01"/>
        </time>
    </configuration>
  2. Open Sumo, load the created config file "my_sumo.sumocfg" and run as in first example, Try example from esmini.

5.7.5. Connect a SUMO simulation to a scenario in esmini

Currently, as of esmini v2.37.1, SUMO is integrated in terms of a controller. To add a SUMO simulation, define an entity as usual, assign the SumoController and set its "filepath" property to the sumocfg file.

Now when we have a complete SUMO configuration, do the following steps in order to use it in esmini:

  1. Copy the three files: my_sumo.net.xml, my_sumo.rou.xml and my_sumo.sumocfg (roadnetwork, routes, config) to esmini/resources/sumo_inputs

  2. Create a file named "my_sumo.xosc", in esmini/resources/xosc with the following content:

    <?xml version="1.0" encoding="UTF-8"?>
    <OpenSCENARIO>
        <FileHeader author="esmini team" date="2024-03-06T10:00:00" description="sumo integration example" revMajor="1" revMinor="2"/>
        <ParameterDeclarations/>
        <CatalogLocations>
            <VehicleCatalog>
                <Directory path="../xosc/Catalogs/Vehicles"/>
            </VehicleCatalog>
            <ControllerCatalog>
                <Directory path="../xosc/Catalogs/Controllers"/>
            </ControllerCatalog>
        </CatalogLocations>
        <RoadNetwork>
            <LogicFile filepath="../xodr/fabriksgatan.xodr"/>
        </RoadNetwork>
        <Entities>
            <ScenarioObject name="SumoVehicles">
                <CatalogReference catalogName="VehicleCatalog" entryName="car_red"/>
                <ObjectController>
                    <Controller name="SumoController">
                        <Properties>
                            <File filepath="../sumo_inputs/my_sumo.sumocfg"/>
                        </Properties>
                    </Controller>
                </ObjectController>
            </ScenarioObject>
        </Entities>
        <Storyboard>
            <Init>
                <Actions/>
            </Init>
            <StopTrigger/>
        </Storyboard>
    </OpenSCENARIO>
  1. Run from esmini root folder:

    ./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/my_sumo.xosc

6. Positioning

esmini supports all OpenSCENARIO position types. To support various use cases esmini add some mechanisms and interpretations:

  • Align by omit: Lacking position component values (x, y, z, heading, pitch, roll) in the scenario file is interpreted as "please align this component to the road surface". E.g. missing z value will project the position on the road surface, missing pitch value will ensure object is aligned with road pitch (e.g. up- and downhill).

  • PositionMode: Any position/object can be set and configured with respect to alignment via API calls. For example: x, y and heading can be set as absolute values while pitch and roll can be set as relative to the road surface.

See more details below in Position modes

  • Trajectory interpolation: Based omitted position components and followingMode setting esmini will apply different mechanisms to follow and interpolate trajectory vertices/control points.

6.1. Position modes

The purpose of position modes is to allow flexible and detailed control whether to interpret Z (elevation), heading, pitch and roll as absolute values or relative to the road. X and Y will always be interpreted as absolute values.

For some use cases orientation should be aligned to road surface, e.g. a vehicle simply driving along a hilly road. In some cases pitch and roll should be zero, e.g. signs and buildings along the road. External vehicle models can choose whether to report relative or absolute values. It can also be combination, e.g. explicit/absolute heading while z, roll and pitch are realative (to the road) - or any other combination.

The mode can be specified explicitly when calling a set position function. Example:

pos.SetInertiaPosMode(x, y, z, h, p, r, PosMode::Z_REL | PosMode::H_ABS | PosMode::P_REL | PosMode::R_REL);

In above example x, y, and h will be interpretated as absolute values while z, p, and r as relative to the road.

6.1.1. Persistent mode setting

In most cases the same mode setting will be used throughout the simulation for each object. For this reason, as conveience, the mode can be set once per object. Example:

pos.SetMode(Position::PosModeType::SET, PosMode::Z_REL | PosMode::H_ABS | PosMode::P_REL | PosMode::R_REL);

Then, when calling set functions, mode can be set to zero. Example:

pos.SetInertiaPosMode(x, y, z, h, p, r, 0);

or use the simple variants skipping the mode argument. Example:

pos.SetInertiaPos(x, y, z, h, p, r);

These cases will utilize the current setting.

6.1.2. Two types of settings

Note the type=SET argument of the SetMode() example above. It specifies as to which type of operations the settings applies to. There are two types of modes: SET and UPDATE. SET applies to API calls setting Cartesian coordinates. UPDATE applies to position updates by default controller and API calls setting Lane and Road coordinates.

This way a position object can, for example, maintain relative heading follow the curvature of the road when updated by the default controller, while heading can be set explicitly in terms of absolute value by API calls.

pos.SetMode(Position::PosModeType::UPDATE, PosMode::Z_REL | PosMode::H_REL | PosMode::P_REL | PosMode::R_REL);

pos.SetMode(Position::PosModeType::SET, PosMode::Z_REL | PosMode::H_ABS | PosMode::P_REL | PosMode::R_REL);

6.1.3. Default values

There will be a default setting for both SET and UPDATE which will be applied until changed by SetMode() function:

SET

PosMode::Z_REL | PosMode::H_ABS | PosMode::P_REL | PosMode::R_REL

UPDATE

PosMode::Z_REL | PosMode::H_REL | PosMode::P_REL | PosMode::R_REL

Further examples

Specify only mode for Z, reuse whatever setting for the remaining components:

pos.SetInertiaPosMode(x, y, z, h, p, r, PosMode::Z_ABS);

Set/reset default value for pitch, reuse whatever setting for the other components:

pos.SetInertiaPosMode(x, y, z, h, p, r, PosMode::Z_MASK & PosMode::Z_DEF);

6.2. Mode specification

Mode is coded in four chunks of three bits. One chunk for each of the position components Z, Heading, Pitch and Roll. Chunks are padded and aligned to segments of four bytes.

Z Comment Hex Int

SKIP

XXXX XXXX XXXX XXX0

first bit is zero

0x0000

0

Z_DEF

XXXX XXXX XXXX XX01

first bit is one, second is zero

0x0001

1

Z_ABS

XXXX XXXX XXXX X011

first and second bit is one, third zero

0x0003

3

Z_REL

XXXX XXXX XXXX X111

first, second and third bits are all one

0x0007

7

H Comment Hex Int

SKIP

XXXX XXXX XXX0 XXXX

first bit is zero

0x0000

0

H_DEF

XXXX XXXX XX01 XXXX

first bit is one, second is zero

0x0010

16

H_ABS

XXXX XXXX X011 XXXX

first and second bit is one, third zero

0x0030

48

H_REL

XXXX XXXX X111 XXXX

first, second and third bits are all one

0x0070

112

P Comment Hex Int

SKIP

XXXX XXX0 XXXX XXXX

first bit is zero

0x0000

0

P_DEF

XXXX XX01 XXXX XXXX

first bit is one, second is zero

0x0100

256

P_ABS

XXXX X011 XXXX XXXX

first and second bit is one, third zero

0x0300

768

P_REL

XXXX X111 XXXX XXXX

first, second and third bits are all one

0x0700

1,792

R Comment Hex Int

SKIP

XXX0 XXXX XXXX XXXX

first bit is zero

0x0000

0

R_DEF

XX01 XXXX XXXX XXXX

first bit is one, second is zero

0x1000

4,096

R_ABS

X011 XXXX XXXX XXXX

first and second bit is one, third zero

0x3000

12,288

R_REL

X111 XXXX XXXX XXXX

first, second and third bits are all one

0x7000

28,672

Looking up values in the table above, modes can be specified in numerous ways.

For example: To set world position with relative Z and absolute Pitch, reusing setting for the other components, any of the following identical examples will work:

pos.SetInertiaPosMode(x, y, z, h, p, r, PosMode::Z_REL | PosMode::P_ABS);
=
pos.SetInertiaPosMode(x, y, z, h, p, r, 0x7 | 0x300);
=
pos.SetInertiaPosMode(x, y, z, h, p, r, 0x307);
=
pos.SetInertiaPosMode(x, y, z, h, p, r, 0x0307);
=
pos.SetInertiaPosMode(x, y, z, h, p, r, 7 | 768);
=
pos.SetInertiaPosMode(x, y, z, h, p, r, 7 + 768);
=
pos.SetInertiaPosMode(x, y, z, h, p, r, 775);

6.2.1. Use cases

  1. Simple 2D vehicle model. Report only X, Y and Heading. Align Z, pitch and roll to the road surface: pos.SetMode(Position::PosModeType::SET, PosMode::H_ABS);
    pos.SetInertiaPos(x, y, 0.0, h, 0.0, 0.0);
    or use short version of the same function, applying zero to z, p, r:
    pos.SetInertiaPos(x, y, h);

  2. Advanced 3D vehicle model. Report all 6 DOF: X, Y, Z, Heading, Pitch and Roll. I.e. no explicit alignment to road surface: pos.SetMode(Position::PosModeType::SET, PosMode::Z_ABS | PosMode::H_ABS | PosMode::P_ABS | PosMode::R_ABS);
    pos.SetInertiaPos(x, y, 0.0, h, 0.0, 0.0);
    or use short version of the same function, applying zero to z, p, r:
    pos.SetInertiaPos(x, y, h);

  3. Rotate a pedestrian 90 degree left to move forward facing the road driving direction, as on a hoverboard:
    pos.SetHeadingRelative(1.5708);
    pos.SetMode(Position::PosModeType::UPDATE, PosMode::H_REL);
    Default controller will now move the pedestrian along the road but rotated 90 degree.

  4. Put pedestrian on a hoverboard moving sideways forward (heading=90 degrees) along the road, 1 meter above ground:
    pos.SetHeadingRelative(1.57);
    pos.SetZRelative(1.0);
    pos.SetMode(Position::PosModeType::UPDATE, PosMode::Z_REL | PosMode::H_REL);
    Default controller will now move the pedestrian along the road 1 meter above ground, with maintained heading relative to the road.

6.3. Trajectory interpolation and alignment

For convenience, esmini can calculate heading and align to the tangent of each segment (polyline and NURBS). Further, to smoothen motion esmini can interpolate corners of polyline trajectories. The behavior is based on omitted position components and followingMode settings.

In general specified angles (heading, pitch and roll) will be interpolated over the segment between two vertices/control points. However, in many cases it’s prefered to just align heading with the tangent of the curve. For these cases it should not be necessary to specify the angles, but rather have esmini calculate them.

First, when z value is missing, i.e. elevation is not specified, esmini will project the points to the road surface. Availability of z will also affect alignment of orientation. General rule is that ground trajectories (elevation not specified) will align pitch and roll to the road surface, while these components will be set to zero for floating (specified z values) trajectories.

Missing angles will be handled according to the table below:

Input

Result

missing angle component

z specified?

align orientation to

heading

Yes

trajectory

heading

No

trajectory

pitch

Yes

trajectory

pitch

No

road surface

roll

Yes

0.0

roll

No

road surface

While moving along polylines, which tangent is not continuous, it’s usually preferred to interpolate/smoothen orientation along whole linear segments or at least when passing vertex corners.

The table below shows what interpolation method will be applied based on FollowingMode setting for the trajectory:

FollowingMode

Interpolate mode

position

corner

follow

segment

Polyline trajectory orientation interpolation can be disabled altogether by setting the disable_pline_interpolation option (see esmini Launch commands).

6.4. Example

There is a code and scenario example illustrating the various interpolation and position modes:

Video clip:

This specific example includes four objects:

  1. Pedestrian/skater on the road
    Initially it’s standing still, aligned along the road x-axis (relative heading = 0).

  2. Box2 moving along a NURBS trajectory
    Specified z will make it "floating" (not projected on ground). Omitted heading will have esmini calculate heading along the tangent.

  3. Box3 moving along another NURBS trajectory
    Also floating. Demonstrating specified roll will be interpolated between control points.

  4. Box1 moving along a polyline trajectory
    followingMode = position will make smoothly transitions at vertex corners.

The application will control and report the motion for the skater. At 5 seconds it will rotate 90 degree and move this way along the road, like riding a hoverboard. Then at 15 seconds it will move one meter up from road surface (z=1 relative road) and also rotate back 90 degrees (heading=0 relative road) and move this way along the road, like on a flyboard. During the complete scenario pitch and roll is set to 0 (absolute value - not aligned to road).

6.5. Relative positions and routes

For the OpenSCENARIO RelativeRoadPosition and RelativeLanePosition the ds and dsLane attributes specifies an offset along the s-axis of the road and lane respectively. In case the offset results in a s-value outside the boundary of the current road, the target location will be searched via connected roads and lanes, even through junctions.

In case the action object (the one being positioned) has been assigned a route, the route will be respected, i.e. the target location will be along the route. If the action object lacks a route, any route of the referenced entity will be respected instead. If neither objects has a route assigned the default routing rules will apply.

In the image below two cars, red and blue, are being positioned by means of RelativeRoadPosition or RelativeLanePosition with the white car as the reference entity. They have both the same ds value, i.e. same longitudinal distance (along s-axis) from the white car. Since the red car has an assigned route, it ends up along that route. The blue car, lacking a route, will end up along the route of the referenced white car.

relative position routing
Relative positioning with and without own route

7. Controllers

7.1. Controller concept

OpenSCENARIO provides the controller concept as a way to outsource control the motion and appearance of scenario entities. For the OpenSCENARIO description of the controller concept, see OpenSCENARIO User Guide.

Controllers can be assigned to object of type Vehicle or Pedestrian. Once assigned, controllers are activated for a given domain (e.g longitudinal, lateral, Lighting, Animation) using the ActivateControllerAction. It’s also possible to activate immediately as part of the AssignControllerAction.

While the ActivateControllerAction is executing, the Controller assigned to that object will manage specified domain(s). Controllers may be internal (part of the simulator) or external (defined in another file).

Intended use cases for Controllers include:
- Specifying that a vehicle is controlled by the system under test. - Defining smart actor behavior, where a controller takes intelligent decisions in response to the road network or other actors. Hence, controllers can be used, for example, to make agents in a scenario behave in a human-like way. - Assigning a vehicle to direct human control.

The Controller element contains Properties, which can be used to specify controller behavior either directly or by a file reference.

Although OpenSCENARIO from v1.2 supports assignment of multiple controllers to an object, esmini is currently only supporting one controller to be assigned (and hence activated) to an object at the time.

7.2. Background and motivation

esmini version < 2.0 totally lacked support for controllers. Instead some similar functionality were implemented as part of different example-applications. For example, EgoSimulator provided interactive control of one vehicle. There was also an "external" mode which allowed for an external vehicle simulator to report current position for the typical Ego (VUT/SUT…​) vehicle.

First, this approach made the example code of simple applications complex. Secondly, it limited the use cases of esmini since functionality was tightly embedded in the applications.

Controllers provides a much more flexible way of adding functionality to esmini, in a way harmonizing with the standard (OpenSCENARIO 1.X). A side effect of this "outsourcing" of functionality is that the former demo applications could be reduced to a minimum both in number and size.

7.3. Brief on implementation

Controllers was introduced in esmini v2.0. Briefly it works as follows:

There is a collection of embedded controllers coming with esmini. Each controller inherit from the base class Controller (Controller.h/cpp). In order for esmini to be aware of the existence of a controller it has to be registered. This is done through the ScenarioReader method RegisterController. It will put the controller into a collection to have available when the scenario is being parsed. So all controllers need to be registered prior to loading the scenario.

A controller is registered with the following information:

  1. Its static name which is used as identifier.

  2. A pointer to a function instantiating the controller.

This architecture makes it possible for an external module to create a controller and registering it without modifying any of esmini modules. In that way it is a semi-plugin concept, you can say.

Note: Even though ScenarioReader have a helper function for registering all the embedded controllers the RegisterController can be called from any module directly, at any time prior to scenario initialization.

esmini catalog framework supports controllers as well, so controllers can be defined in catalogs as presets, just like vehicles and routes for example.

7.4. How it works for the user

Controllers are completely handled in the OpenSCENARIO file. With one exception: esmini provides the --disable-controllers option which totally ignore any controllers, just performing the scenario with DefaultControllers, which can be handy when previewing and debugging scenarios.

In terms of OpenSCENARIO a controller is assigned to an entity (object) in any of two ways:

  1. As part of the Entities section and ScenarioObject definition, using the ObjectController element.

  2. The AssignControllerAction which can be triggered as any action at any time during the simulation.

Once assigned the controller must finally be activated using the ActivateControllerAction which can be triggered at any time. It provides an attribute to specify which domain(s) to control: Lateral, Longitudinal or both.

Example 1: Implicit assignment of controller using the ObjectController element:

<Entities>
   <ScenarioObject name="Ego">
     <CatalogReference catalogName="VehicleCatalog" entryName="$HostVehicle"/>
      <ObjectController>
          <Controller name="MyController" >
             <Properties>
                 <Property name="esminiController" value="InteractiveController" />
                 <Property name="speedFactor" value="1.5" />
             </Properties>
          </Controller>
      </ObjectController>
   </ScenarioObject>
</Entities>

Example 2: As above, but using catalog reference:

<Entities>
   <ScenarioObject name="Ego">
     <CatalogReference catalogName="VehicleCatalog" entryName="$HostVehicle"/>
        <ObjectController>
            <CatalogReference catalogName="ControllerCatalog" entryName="interactiveDriver" />
        </ObjectController>
   </ScenarioObject>
</Entities>

Example 3: Activate controller in the Init section:

<Init>
   <Actions>
      <Private entityRef="Ego">
         <PrivateAction>
            <TeleportAction>
               <Position>
                  <LanePosition roadId="0" laneId="-3" offset="0" s="$EgoStartS"/>
               </Position>
            </TeleportAction>
         </PrivateAction>
         <PrivateAction>
              <ActivateControllerAction longitudinal="true" lateral="true" />
         </PrivateAction>
      </Private>
   </Actions>
</Init>

Example 4: Assign and activate the controller in the Storyboard section:

<Event name="InteractiveEvent" maximumExecutionCount="1" priority="overwrite">
   <Action name="AssignControllerAction">
     <PrivateAction>
          <ControllerAction>
              <AssignControllerAction>
                  <CatalogReference catalogName="ControllerCatalog" entryName="interactiveDriver" />
              </AssignControllerAction>
          </ControllerAction>
      </PrivateAction>
   </Action>
   <Action name="ActivateControllerAction">
      <PrivateAction>
          <ControllerAction>
              <ActivateControllerAction longitudinal="true" lateral="true" />
          </ControllerAction>
      </PrivateAction>
   </Action>
   <StartTrigger>
      <ConditionGroup>
         <Condition name="" delay="0" conditionEdge="none">
            <ByValueCondition>
               <SimulationTimeCondition value="20" rule="greaterThan"/>
            </ByValueCondition>
         </Condition>
      </ConditionGroup>
   </StartTrigger>
</Event>

To disable any controller simply apply ActivateControllerAction on no domain, like:

<ActivateControllerAction longitudinal="false" lateral="false" />

That should work in all OpenSCENARIO supporting tools. In esmini you can also assign DefaultController which will disconnect any assigned controller.

7.5. The ghost concept

The purpose of the ghost concept is to support external driver models. The idea is to launch a forerunner, which performs the maneuvers in the OSC file. The resulting trajectory (also called trail), including speed and heading info, is registered. The driver model can then use the ghost trail as guidance. More specifically, the driver model can probe the trail at any timestamp or distance from its current pos and use the result for both steering and speed target.

ghost concept1

A typical use case is having an external Ego vehicle simulator, including System Under test (SUT) and a driver model. Instead of the driver model (in the external simulator) being aware of and execute scenario maneuvers it can hand over to esmini to play the maneuvers and then simply try to mimic it, following in ghost’s trail. This way the external simulator can benefit from esmini scenario engine and just concentrate on following the ghost.

The ghost feature is available in the following esmini embedded controllers:

There are two different methods to probe the ghost trail:

  1. By time: Ask for ghost state (pos, heading, speed…​) at specific timestamp

  2. By distance: Ask for ghost state at a specific distance, from a position (typically current Ego position)

Both methods will return a RoadInfo structure including position details e.g. world position (absolute and relative Ego), road coordinates, road pitch, trail heading, heading angle to the target point and ghost speed at that point.

typedef struct
{
    float global_pos_x;   // target position, in global coordinate system
    float global_pos_y;   // target position, in global coordinate system
    float global_pos_z;   // target position, in global coordinate system
    float local_pos_x;    // target position, relative vehicle (pivot position object) coordinate system
    float local_pos_y;    // target position, relative vehicle (pivot position object) coordinate system
    float local_pos_z;    // target position, relative vehicle (pivot position object) coordinate system
    float angle;          // heading angle to target from and relative vehicle (pivot position object) coordinate system
    float road_heading;   // road heading at steering target point
    float road_pitch;     // road pitch (inclination) at steering target point
    float road_roll;      // road roll (camber) at target point
    float trail_heading;  // trail heading (only when used for trail lookups, else equals road_heading)
    float curvature;      // road curvature at steering target point
    float speed_limit;    // speed limit given by OpenDRIVE type entry
    id_t  roadId;         // target position, road ID
    id_t  junctionId;     // target position, junction ID (SE_ID_UNDEFINED if not in a junction)
    int   laneId;         // target position, lane ID
    float laneOffset;     // target position, lane offset (lateral distance from lane center)
    float s;              // target position, s (longitudinal distance along reference line)
    float t;              // target position, t (lateral distance from reference line)
} SE_RoadInfo;

Time method

This approach is trivial, it will give the state of ghost at the specified timestamp using the API call:

Distance method

The API call is:

The method needs some further explanation. The basic idea is to look specified distance along the ghost trail and receive information at that target point. The tricky part is where to start measuring the distance from. This is how it works:

ghost concept2
  1. From Ego, find closest point on trail (blue point)

  2. From that point, look forward x meters along trail (red dot)

  3. Get info from that ”target” point

Ideally, two points are used: One closer for target speed and one more distant for steering target.

ghost targets
Different lookahead for speed and steering control

Tuning parameters:

  1. Lookahead distance along trail

    • Typically speed dependent (look further at higher speeds)

    • Potentially decrease distance with increased curvature (to stay on road)

  2. Headstart time

    • As small as possible, but enough for required lookahead distance

Note: If lookahead distance is too large, the steering target angle becomes irrelevant, leading to driving off road

ghost concept3
Too large lookahead distance

Actions and triggers

Most actions that affects motion will be transferred from the original entity (Ego) to its ghost, for example: SpeedAction, LaneChangeAction and TeleportAction. In addition, triggers will be affected in the following ways:

  • Entity triggers: Replace any occurrence of original entity with ghost in the list of TriggeringEntities. For example, if the vehicle should brake when reaching a specified position, then it’s the ghost that should trigger the action - not the original entity.

  • Other triggers: Restart ghost from where the Ego is now, in effect rewind ghost headStartTime seconds and recreate trajectory from there. The purpose is to enable ghost to react on triggers that happens in the "real" scenario (which plays out headStartTime seconds behind the ghost)

  • SimulationTime trigger conditions for events where Ego is the actor will be adjusted wrt ghost headStartTime time. For example, if vehicle should brake after 20 seconds of driving (simulationTime = 20), then it will be changed to simulationTime = 17 (for headstart 3s), since the ghost has been driving for 20 seconds at that point.

Examples

The following examples shows two different triggers for a lane change action. First example uses TraveledDistanceCondition. Ghost will be the triggering entity and since this is independent of other road users, there is no need to restart ghost.

The second example, on the other hand, uses TimeHeadwayCondition which calculates the time gap between the white Ego car and the red Target car, hence depending on other road users. Since ghost is running ahead of time it does not make sense to involve ghost in such conditions, hence Ego will be the triggering entity.

Example 1: Trigger based on traveled distance - no restart

<ByEntityCondition>
    <TriggeringEntities triggeringEntitiesRule="any">
        <EntityRef entityRef="Ego"/>
    </TriggeringEntities>
    <EntityCondition>
        <TraveledDistanceCondition value="20"/>
    </EntityCondition>
</ByEntityCondition>

Example 2: Trigger based on time headway - restart ghost

<ByEntityCondition>
    <TriggeringEntities triggeringEntitiesRule="any">
        <EntityRef entityRef="Ego"/>
    </TriggeringEntities>
    <EntityCondition>
        <TimeHeadwayCondition
            entityRef="Target"
            value="4.0"
            freespace="false"
            coordinateSystem="road"
            relativeDistanceType="longitudinal"
            rule="lessThan"/>
    </EntityCondition>
</ByEntityCondition>

Note that the strategy results in desired behavior in probably the majority of cases, but not all. There might be exceptions where another behavior is expected or required. The manipulation described above is handled by the methods SetupGhost() and ReplaceObjectInTrigger() in ScenarioEngine.cpp which can be modified to fit custom needs.

Additional notes on using the ghost feature

Timestamps in log are always of same reference. Whenever a ghost is involved the time will start at -headStartTime, in order for the actual scenario to start at simulationTime = 0. Example: Assuming headStartTime = 3s, a log message for an Ego action like:

-2.100: SpeedChange standbyState → startTransition → runningState

means that action SpeedChange (Ego action that has been moved to ghost) is performed 0.9 (-2.1 + 3) seconds after ghost was launched. This would, for example, be the case for a simulationTime = 0.9 condition.

7.6. esmini embedded controllers

Below is a listing of some of the available controllers in esmini. Note that only DefaultController is related to the OpenSCENARIO standard. The other ones are esmini-specific and will not work in other tools (so far there is no standard plugin-architecture for controllers to enable moving between tools). For updated and complete definition of controllers and their parameters, see ControllerCatalog.xosc.

7.6.1. DefaultController

Performs actions exactly as specified in the OpenSCENARIO file. Assigned to entities by default. Can be assigned at any point to "unassign" any currently assigned controller.

Domain:

n/a (will always take over both Lateral and Longitudinal domains)

Properties:

None

Example:

n/a (any scenario not explicitly assigning and activating another controller)

7.6.2. InteractiveController

A simple vehicle model controlled by the user via keyboard (arrow keys).

Domain:

longitudinal and/or lateral (independent)

Properties:

steeringRate

steering sensitivity

speedFactor

scale speed performance (1.0 is default)

Example:

cut-in_interactive.xosc

7.6.3. FollowGhost

A simple driver model. A ghost-twin performing the events a few seconds ahead. The entity will then do its best to mimic the motion and speed of the ghost by follow the trajectory. The primary purpose of this controller is to provide an example, but it can be useful to smoothen out the sometimes synthetic feel of pure default controller.

Domain:

longitudinal and lateral (in combination only).

Properties:

headstartTime

seconds

followMode

time (time offset), position (distance offset)

lookaheadSpeed

Pick target speed from position at this look-ahead time (time mode) or distance (position mode)

minLookaheadSpeed

Minimum speed target look-ahead time (time mode) or distance (position mode)

lookaheadSteering

Pick target steering point from position at this look-ahead time (time mode) or distance (position mode)

minLookaheadSteering

Minimum steering target look-ahead time (time mode) or distance (position mode)

Example:

follow_ghost.xosc

ghost targets
Illustration of separate look-ahead for speed and steering control

7.6.4. ExternalController

The entity will not be moved by the scenario. Instead its state (position, rotation …​) is expected to be reported from external simulator via API, e.g. SE_ReportObjectPos. Ghost trajectory can optionally be created for an external driver model to use as reference.

Domain:

longitudinal and/or lateral (independent).

Properties:

useGhost

launch a ghost fore-runner, e.g. to provide input for any driver model (true/false)

headstartTime

(for the ghost)

mode

override disable default controller (default), additive: apply default controller before handing over to this controller

Example:

test-driver code example

7.6.5. SumoController

A way of integrating SUMO controlled vehicles in a scenario. OpenSCENARIO vehicles are reported to SUMO, and SUMO vehicles are reported back to esmini. A reference to a SUMO config file is provided as input to the controller. See cut-in_sumo.xosc for an example.

Domain:

longitudinal and lateral (in combination only).

Properties:

file

Path to the SUMO configuration file

Example:

cut-in_sumo.xosc

7.6.6. ALKS_R157SM

Based and inspired by Regulation 157 Safety Models. Includes four safety models relating to the (UNECE ALKS regulation #157). The controller includes preliminary and experimental implementation of four models:

Domain:

longitudinal and lateral (in combination only). Note: No advanced steering, just maintaining current lane offset.

Properties:

logLevel

0: No logging, 1: Brief logging of state changes (default), 2: Verbose logging of some internal variables

model

Regulation, ReferenceDriver, RSS, FSM as described above.

cruise

Activate longitudinal ACC (comfort adaptive speed control) (true/false)

Further properties see ALKS_R157SM_Controller in Controllers/ControllerCatalog.xosc.

Examples:

alks_r157_cut_in_quick_brake.xosc
alks_r157_quick_stop_test.xosc
alks_r157_test.xosc

Example usage

Run example alks_r157_cut_in_quick_brake.xosc:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/alks_r157_cut_in_quick_brake.xosc

Change model:

  • Open resources/xosc/alks_r157_cut_in_quick_brake.xosc in a text editor

  • In the ALKS_R157SM_Controller, change
    <Property name="model" value="Regulation"/> to
    <Property name="model" value="ReferenceDriver"/>

  • Save and run again:
    ./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/alks_r157_cut_in_quick_brake.xosc

You can hardly see any difference. Let’s run the two variants again and record for post analysis.

  • Set model to "ReferenceDriver" (by edit the file as described above)

  • Run:
    ./bin/esmini --headless --fixed_timestep 0.02 --osc ./resources/xosc/alks_r157_cut_in_quick_brake.xosc --record sim-ref.dat

  • Set model to "Regulation"

  • Run:
    ./bin/esmini --headless --fixed_timestep 0.02 --osc ./resources/xosc/alks_r157_cut_in_quick_brake.xosc --record sim-reg.dat

  • View scenarios in parallel:
    ./bin/replayer --window 60 60 800 400 --res_path ./resources --dir . --file sim- --view_mode boundingbox

  • Create a merged .dat file and plot it:
    ./bin/replayer --file sim- --dir . --save_merged sim_merged.dat
    ./scripts/plot_dat.py --param speed sim_merged.dat

Something like this should show:

plot merged alks ex

Of course you can run also FSM and RSS models in similar way and merge all four .dat files. For more information about replayer, see "Plot merged .dat files" example in Plot scenario data section.

7.6.7. FollowRoute

Implementation of the Lane Independent Routing Model (LIRM) developed as part of the master thesis project Olsson, Daniel; Rylander, Eric: "A Framework for Verification and Validation of Simulation Models in esmini".

From the report: "LIRM has solved the issue with lane dependent routing that esmini had, by implementing a new controller that extends the behavior of the pathfinding and route following to incorporate lane changes."

This controller enhance the capabilities for an entity to find a path through the road network according to a specified route (basically a set of waypoints). The main advantage, compared to the route handler of the default controller, is that lane changes will be injected by the path finding algorithm in search for optimal path. The model will also take route strategy (Shortest, Fastest or Least Intersections) into consideration, which the default controller does not yet support.

A good example is found in follow_route_with_lane_change.xosc. The vehicle start out in left-most lane and needs to make three lane changes in order to exit the highway.

follow route highway exit
Route waypoints and resulting trajectory in yellow

./bin/esmini --window 60 60 800 400 --osc ./EnvironmentSimulator/Unittest/xosc/follow_route_with_lane_change.xosc --trail_mode 1

Domain:

longitudinal and/or lateral (independent).

Properties:

minDistForCollision

affects when a lane change will happen

laneChangeTime

duration of lane changes

testMode

stop at reached destination - mainly for test purpose (true/false (default))

Example:

follow_route_with_lane_change.xosc

7.6.8. UDPDriverController

An UDP interface for external driver models or vehicle simulators. The idea is to allow external devices to control entities in an ongoing esmini simulation, over network.

Concept:

udp driver controller concept
  • Assign the controller to any scenario vehicle (in the OpenSCENARIO file)

  • Send input to esmini controller

  • The vehicle will be updated by the controller accordingly (and bypassing esmini default controller)

  • receive ground-truth (OSI over UDP) from esmini

There are a few input mode options:

  1. DRIVER_INPUT
    Control a simple vehicle model in terms of pedals and steering input. Vehicle will apply latest received values until update is received.

    Parameters:

    throttle
    brake
    steeringAngle

  2. VEHICLE_STATE_XYZHPR
    Report explicit complete state, ignoring road geometry. Useful when advanced vehicle dynamics is involved and vehicle for example is pitching as result of acceleration and rolling as result of steering. Optional flag to enable dead reckoning on esmini side, in which case esmini will move entity according to latest received heading and speed (basically extrapolating).

    Parameters:

    x
    y
    z
    h(eading)
    p(itch)
    r(oll)
    speed
    wheelAngle (wheel yaw/stering angle)
    deadReckon (flag)

  3. VEHICLE_STATE_XYH
    Report explicit partial state. z, pitch and roll are aligned to the road geometry.

    Parameters:

    x
    y
    h(eading)
    speed
    wheelAngle (wheel yaw/stering angle)
    deadReckon (flag)

  4. VEHICLE_STATE_H
    Minimal explicit state, useful when the driver model output is only heading and speed.

    Parameters:

    h(eading)
    speed
    wheelAngle (wheel yaw/stering angle)
    deadReckon (flag)

For more info see: UDPDriverController.pdf

Demo (running a somewhat outdated testUDPDriver.py):

7.6.9. OffroadFollowerController

A simple "follow-the-leader" controller which aims to keep a specified distance to a specified leading vehicle. The controller utilizes a simple 2D "bicycle" vehicle model. Each time-step the controller finds out the direction and distance to the lead vehicle. Based on this, the acceleration and steering input is calculated and provided to the vehicle model.

The term off-road indicates that the controller does not consider any road network features. Note that it still can run on roads, parking lots, just that it’s ignoring it in terms of lanes and road-marks.

Below is a video clip showing the controller in action on an open paved area. The white lead vehicle is controlled interactively by the user via arrow keys. The controller is assigned to the red car which then strives to follow the lead car at the distance of 20 meters.

7.7. How to add a new controller

Below are the steps to add new controller in esmini:

  1. Create a sepreate controller module in the controller folder. Suggestion is to copy one of the existing and use as a starting point.

  2. Register the controller in the LoadControllers() fuction in EnvironmentSimulator/Modules/ScenarioEngine/SourceFiles/ScenarioReader.cpp

  3. Create Instantiate method in the new controller. The name shall be unique eg InstantiateControllerLooming

  4. In the new controller, Type name shall be unique eg, CONTROLLER_LOOMING_TYPE_NAME

  5. Add controller type and it shall be unique in EnvironmentSimulator/Modules/Controllers/Controller.hpp

  6. Make sure GetTypeStatic() and GetTypeNameStatic() and methods returns the unique type and type name values

8. OpenSceneGraph and 3D models

esmini make use of OpenSceneGraph (OSG) for visualization of the scenario. The OpenSCENARIO files can optionally refer to existing 3D models of the static environment (scene graph) and dynamic objects (entities). If the scene graph reference is missing, esmini will try to generate a basic model based on the OpenDRIVE road network description. Currently esmini only supports OSG native .osgb 3D file format. However, there are ways to convert 3D models as described next.

OpenSceneGraph (osg) includes readers and writers for quite a few 3D file formats. It comes with a demo application, osgconv, a command line tool that simply takes one file as input and outputs the same content in a different format.

Examples:

Convert from fbx to osgb format:
osgconv car.fbx car.osgb

Convert from osgb to fbx format:
osgconv car.osgb car.fbx

osgconv has a lot of useful options. Run osgconv -h for more info. Here’s a few examples:

osgconv in.fbx out.osgb --compressed will compress and embed textures
osgconv in.fbx out.osgb -t 1,5,7 will translate the model (x=1, y=5, z=7)
osgconv in.fbx out.osgb -o -90-1,0,0 will rotate the model (rotate -90 deg around X-axis)
osgconv in.fbx out.osgb -o -90-1,0,0 --use-world-frame as above but pivot point world origin

A useful environment variable is OSG_OPTIMIZER which affects the structure and content of the scene graph. For example, if the osgb file includes cloned sub trees, like motorway railings, these might not be populated in the fbx file. To solve that set:

OSG_OPTIMIZER = FLATTEN_STATIC_TRANSFORMS_DUPLICATING_SHARED_SUBGRAPHS

Exact syntax depends on your environment. Examples:

bash

export OSG_OPTIMIZER=FLATTEN_STATIC_TRANSFORMS_DUPLICATING_SHARED_SUBGRAPHS

powershell

$env:OSG_OPTIMIZER = "FLATTEN_STATIC_TRANSFORMS_DUPLICATING_SHARED_SUBGRAPHS"

Then run the osgconv command (in the same terminal where you defined the environment variable).

osg native format is osgb which stands for OpenSceneGraph binary format. It’s a stable and compact (quick to load) format, which also can embed textures and animations. Thanks to the osgconv tool, esmini can get away with only supporting the .osgb format and still indirectly support many other 3D formats.

8.1. Get osgconv

There are pre-built binaries available, see below. But for full capabilities, including support for converting from 3D file formats .dae (collada) and .fbx (Autodesk Filmbox) you have to build OpenSceneGraph (osg) yourself.

8.1.1. Build yourself

Prerequisites

The only prerequisite step is for Linux to install some dependecies:

  1. Run software-properties-gtk
    or open the "Software & Updates" application

  2. then under the "Ubuntu Software" tab click "Source code" and add a server of your choice, e.g. within your country

  3. Run sudo apt-get build-dep openscenegraph

Build

You can then try this script: compile_osg_apps.sh. It will first fetch and install FBX SDK and then build OSG with FBX and DAE support. Find more details and instructions in the header of the script.

For more info regarding building OSG for Linux, here’s a great guide:
https://vicrucann.github.io/tutorials/osg-linux-quick-install/
But note that it does not consider FBX support.

Find more info on COLLADA via this old link.

8.1.2. Pre-build binaries

Windows
Binaries including FBX but not DAE support, see here:
https://objexx.com/OpenSceneGraph.html

Linux

sudo apt-get update
sudo apt-get -y install openscenegraph

8.2. Convert osgb models for use in Unity

Here follows an example of how to convert .osgb models into .fbx format for use in the Unity3D framework.

  1. Open a command prompt in the folder where your model.osgb is

  2. Run command:
    osgconv model.osgb out/model.fbx -s 100,100,100
    "-s …​" is for scaling which typically is needed for fbx files. Potentially it also needs to be oriented according to Unity coordinate system. In that case try:
    osgconv model.osgb out/model.fbx -s 100,100,100 --use-world-frame -o 120—​0.5773503,-0.5773503,-0.5773503

A folder named "out" should have been created and including the model.fbx plus any texture files

8.3. Import into Unity

  1. Drag the resulting fbx file, and all textures, into a unity project, preferably an empty folder

  2. Add the model to the Scene hierarchy

  3. Select the model and int the "Inspector", select the "Materials" tab

  4. Change the "Location" to "Use External Materials (Legacy)" and click "Apply"

  5. Open the automatically created "Materials" folder (next to the model file)

  6. Select all materials and change
    for standard template: "Rendering Mode" to "Cutout" (good default option)
    for HDRP template: "Surface type" to "Transparent" and check "alpha clipping"

That should basically be it.

8.4. Colors, textures and wheel rotations

See issue 63 for brief info.

And these two scripts operating on textures and material respectively, can potentially provide some inspiration:
- bake_signs.sh
- fix_dae_materials.py

8.5. Lighting

esmini use a very simple lighting model. Adding a global low ambient plus a stronger directed "sun" light. This is a simplistic model of a bright day where everything is visible but sides facing towards the sun gets brighter than those facing away.

The ambient intensity is 0.4. The "sun" x,y,z position is (-7500, 5000, 10000) and intensity 0.8.

Now, it has been reported that some custom models looks very dark on sides facing away from the sun. The main reason is often that the material definition do not have any ambient contribution.

light original
Example of material lacking ambient component

There are two solutions:

Solution 1: Fix the material

Convert the model to Collada .dae format, example:

osgconv my_model.fbx my_model.dae

Then open the .dae file in a text editor. Look for <library_materials> and the material references inside that element. The actual material definitions are (typically?) found under <library_effects>. There might be many types, but two that we encountered are <phong> and <lambert>. They should have a diffuse element, something like:

<diffuse>
  <color sid="diffuse">0.8 0.8 0.8 1.0</color>
</diffuse>`

There might be a similar one for ambient. Check color values, if they are low, e.g. 0.2, try increase to max 1.0. If element is missing, just add it. In the end, it should look something like:

<ambient>
  <color sid="ambient">1.0 1.0 1.0 1.0</color>
</ambient>`

Save the file as my_model_fixed.dae.

There is a script that automates this process, but it will only recognize phong and lambert material types: fix_dae_materials.py.

Finally, convert the patched dae file to osgb:
osgconv my_model.dae my_model.osgb

light patched material
Same model with patched material

Different formats have different orientation frames. Hence it may be necessary to fix rotation. In that case, try something like:
osgconv my_model.dae -o 90-1,0,0 --use-world-frame my_model.osgb

Solution 2: Add light source

Users can add up to two custom light sources, specifying position (x, y, z) and intensity. Examples:

Add one custom light source:
./bin/esmini --window 60 60 800 400 --osc .\resources\xosc\lane_change_crest.xosc --custom_light 4000,-6000,500,1.0

Add two custom light sources:
./bin/esmini --window 60 60 800 400 --osc .\resources\xosc\lane_change_crest.xosc --custom_light 4000,-6000,500,0.4 --custom_light 5000,5000,200,0.2

light second light
Original model with one light added

9. Support / Q&A

9.1. Various issues

9.1.1. Resources not found

There are no fixed structure where scenario resources, e.g. vehicle 3D model or controller catalog, needs to be stored. Instead, additional search paths can be added by launch argument --path <path>. For example:

./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/slow-lead-vehicle.xosc --path ../../ --path c:/tmp/my_models

will add the relative path ../../ and absolute path c:/tmp/my_models to the list of places where to look for resources referred to by the scenario.

9.1.2. Blocked by Windows Defender SmartScreen

When esmini is downloaded as a zip file from Internet it might be blocked, i.e. prevented from being started, by Windows to protect your system. This is indicated by the following, or similar, message popping up when attempt to start:

"Microsoft Defender SmartScreen prevented an unrecognized app from starting. Running this app might put your PC at risk."

As long as the zip was downloaded from esmini official GitHub repository (https://github.com/esmini/esmini) it is perfectly safe to use.

The simplest solution is to unblock the zip file before unpacking it:

  1. Right click

  2. Click Properties

  3. At bottom right, check "Unblock"

win unblock

Note: This method also works for individual files after unzipping. But it can only be applied for one file at a time, why it’s simpler to unblock the complete zip before unpacking.

It is also possible to unblock from Powershell command line:

  • Single file:
    Unblock-File esmini-demo_Windows.zip

  • Multiple files recursively:
    Get-ChildItem -Path esmini-demo_Windows -Recurse | Unblock-File

9.1.3. Mac issues and limitations

From esmini version 2.26.6 mac executables are universal binaries which means they work on both Intel and Apple Silicon based Macs. Following are some known issues and work arounds:

  • Window can’t be located in the upper part of the screen (some systems). If window won’t open, try to adjust the y value (second entry) of the window argument. 60 pixels usually works fine, e.g: ./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc

  • Issue has been reported that when multiple screens are connected esmini just shows a bland window. Currently only solution is to unplug the additional montitors or unplug Mac from dock station.

  • Graphics can not run in separate thread. Hence the --thread launch flag will have no effect.

  • On Mac the zip-package might be put in quarantine, to release it: xattr -d com.apple.quarantine file.zip or even better: xattr -c file.zip

  • esmini executables are not signed with a developer ID - since we simply don’t have one. If you got the package directly from esmini release page on GitHub it is safe to unpack and run. In order to execute the files you need to do one of following from a terminal in the folder the demo package was extracted:

    • Remove quarantine flags: xattr -c -r esmini-demo/bin, or

    • Sign executables for use on local machine: codesign -f -s - esmini-demo/bin/*

9.1.4. OpenStreetMap (OSM) roads in esmini

SUMO comes with a great tool, netconvert, that can convert road networks from and to various formats.

Prerequisite: Download and install SUMO from here.

Example: Convert an OSM map to OpenDRIVE for use in esmini:

netconvert --osm-files city.osm --opendrive-output city.xodr --no-turnarounds

Preview in odrviewer:

./bin/odrviewer --window 60 60 800 400 --odr city.xodr

9.1.5. Update 3D model pack

Do either:

  • remove any resources/models folder

  • from ./build folder, run cmake ..

or

  • get the package from here and unpack files into ./resources/models

9.1.6. Update 3rd party prebuilt libraries

There are a few external dependencies that takes long time to build from scratch. Instead they are provided in compressed packages including needed headerfiles and prebuilt libraries. The packages are available for Win, Mac and Linux. For Windows and Linux they include Debug mode binaries, in addition to the Release variant.

The cmake script will check for the availability of these libraries, by simply checking for existence of corresponding folders, e.g. osg, osi, sumo, under esmini/externals. If missing, compressed packages will be downloaded and extracted.

Not often, but at some points the prebuilt libraries needs to be updated. In most cases it affects the esmini build configuration and/or code. Hence, sometimes an update of esmini comes with a need to update the external libraries as well. Lack of automatic handling, such update needs to be done manually. One way is to delete the specific folder(s) and run ordinary cmake command again. But the simplest way is to enforce redownload of all packages:

cmake .. -D FORCE_DOWNLOAD_BINARIES=TRUE

The FORCE_DOWNLOAD_BINARIES flag will be checked by the cmake script for enforced download of all packages. Upon successful download, the libraries will be replaced, one by one.

Library updates are not detected by the esmini build dependencies, why a clean rebuild of esmini should be performed manually afterwards.

9.1.7. Entity does not appear

In order to appear in the scenario, without need for AddEntity action, an entity must be represented in the Init section of the Storyboard. Common is to add actions to establish inital position and speed.

Example:

<PrivateAction>
   <TeleportAction>
      <Position>
         <LanePosition roadId="1" laneId="-1" offset="0" s="50"/>
      </Position>
   </TeleportAction>
</PrivateAction>
<PrivateAction>
   <LongitudinalAction>
      <SpeedAction>
         <SpeedActionDynamics dynamicsShape="step" dynamicsDimension="time" value="0.0"/>
         <SpeedActionTarget>
            <AbsoluteTargetSpeed value="30.0"/>
         </SpeedActionTarget>
      </SpeedAction>
   </LongitudinalAction>
</PrivateAction>

9.1.8. Heading behavior in Road vs Lane Position

In esmini, the behavior of the two position types RoadPosition and LanePosition differ in terms of heading.

  • RoadPosition: When not specified, the heading will align to the direction of the road (s-axis)

  • LanePosition: When not specified, the heading will align to the driving direction of the lane

In the image below, the white cars are positioned by RoadPosition with t=-1.5 and t=1.5 respectively while the red cars are positioned by LanePosition with laneId="-1" and laneId="1" respectively.

no heading
Positioning with RoadPosition (white) and LanePosition (red) - heading omitted

Explicitly specified heading (in Orientation sub-element):

  • Absolute heading will be respected and treated in the same way for both position cases

  • Relative heading will be based on the default heading, i.e. road or driving direction respectively.

In next image, an Orientation element was added with absolute heading = 0.4 for all four cars.

absolute heading
Positioning with RoadPosition (white) and LanePosition (red) - absolute heading=0.4

In next image, an Orientation element was added with relative heading = 0.4 for all four cars.

relative heading
Positioning with RoadPosition (white) and LanePosition (red) - relative heading=0.4

Corresponding behavior applies also to RelativeRoadPosition and RelativeLanePosition.

Motivation

In some (maybe most) use cases it’s preferred that vehicles align to the driving direction of the lane. But in other cases not, example: In an overtake maneuver, the overtaking car may start in the neighbor lane. The driving direction of the lane should not affect the heading of the car.

So the different strategies for RoadPosition and LanePosition was decided upon to support as many use cases as possible. For more background, see issues #385 and #228.

9.1.9. Reference line and center lane while using laneOffset

In OpenDRIVE definition of laneOffset: A lane offset may be used to shift the center lane away from the road reference line.

lane offset standard way
OpenDRIVE standards stipulates reference line is not affected by lane offset

In esmini lane offset affects the reference line as well as center lane, which is a deviation from the OpenDRIVE standard. As consequence, t values are also shifted with the lane offset.

lane offset esmini way
esmini shift both center lane and reference line by lane offset

2+1 roads can be defined utilizing lane offset. Consider image below:

two plus one lane ids
2+1 road in esmini, utilizing lane offset

esmini

white car red car

laneId

-2

-1

t

-5.25

-1.75

OpenDRIVE

white car red car

laneId

-2

-1

t

-1.75

1.75

A 2+1 example example scenario is found here: resources/xosc/two_plus_one_road.xosc

9.1.10. OSI for Python on Windows

Follow the following steps to install OSI (and dependent Google Protobuf) for use by some esmini scripts, e.g. osi2csv.py.

Protobuf for Python (skip if already installed)

  1. Open a Powershell

  2. pip install protobuf==3.20.2

protoc (Protobuf compiler)

  1. Download protoc-3.20.2-win64.zip
    (release page: https://github.com/protocolbuffers/protobuf/releases/tag/v3.20.2)

  2. Unzip (at least protoc.exe) to some location

OSI for Python

  1. Open a Powershell in the to-be-parent folder of OSI

  2. git clone https://github.com/OpenSimulationInterface/open-simulation-interface

  3. cd open-simulation-interface

  4. git checkout v3.5.0

  5. ./convert-to-proto3.sh

  6. $env:PROTOC=<location of protoc.exe>
    for example: $env:PROTOC="C:/tmp/protoc-3.20.2-win64/bin/protoc.exe"

  7. pip install .

This should be it.

How to remove old versions (if needed)

  1. pip uninstall protobuf

  2. pip uninstall open-simulation-interface

9.1.11. The scenario runs too fast or too slow

esmini can run in two different time modes: 1. Real-time and 2. Fixed-time. Default is real-time.

The following command, from esmini root, will run real-time:
./bin/esmini --window 60 60 800 400 --osc ./resources/xosc/cut-in.xosc

This one fixed-time:
./bin/esmini --window 60 60 800 400 --fixed_timestep 0.05 --osc ./resources/xosc/cut-in.xosc

The difference is basically that real-time will run with as small time-steps as possible (actually limit at 1 ms to avoid unnecessary CPU load), maxim zing precision but constrained by system clock. In other words, the scenario will play out in "natural" speed, good for previewing or demonstrating sc narios. Each timestep is calculated by esmini based on passed system-time since last frame.

The fixed-time is simpler, esmini will simply execute the scenario as fast as possible applying the specified delta-time for each step. Good for runni g tests for later post processing of resulting logs, data or OSI files.

A common use-case is to run esmini quick/headless and then utilize the replayer for viewing the result. See more info here: Replay scenario

9.1.12. Road and junction string IDs

OpenDRIVE allows for non integer string IDs. Although commonly unsigned integer is used. The standard OpenDRIVE 1.8 states that IF the ID represents an integer number, it should comply to uint32_t and stay within the given range.

esmini partly supports* string IDs, by mapping to internal integer IDs. The way it works:

If the ID is a number in the range of 0..0xfffffffe (4 294 967 294), then:
map it to the same internal ID
Else, if the number is a string or a number out of range:
map it to a unique internal ID

Note: 0xffffffff (4 294 967 295) is reserved for ID_UNDEFINED which is the default value and also used for error indication

Example: Assume an OpenDRIVE includes the following road (or junction) IDs:

"0"
"1"
"124"
"Kalle"
"12k4"
"4294967296"
"4294967295"
"4294967294"

The IDs will be mapped as follows:

"0" -> 0
"1" -> 1
"124" -> 124
"Kalle" -> 125
"4294967296" -> 126 (too large)
"4294967295" -> 127 (conflict with ID_UNDEFINED)
"4294967294" -> 4294967294 (Ok, largest acceptable number)

Now, why not make use of available numbers between 1 and 124? It’s because esmini is lazy, not spending effort (performance) to book-keep sorted list of IDs. Instead, only the largest number in use will be monitored.

In addition, there are esmini lib API functions to lookup string and internal IDs:

const char *SE_GetRoadIdString(id_t road_id);
id_t SE_GetRoadIdFromString(const char *road_id_str);

const char *SE_GetJunctionIdString(id_t junction_id);
id_t SE_GetJunctionIdFromString(const char *junction_id_str);

That way, a custom application can find out what internal IDs esmini has assigned to any non-number string IDs.

Corresponding functions exist in esmini roadmanager lib.

* String ID support was introduced in v2.37.15. By v2.39.0 internal representation changed from 32 bit signed int to unsigned int. At that point also undefined ID changed from -1 to 0xffffffff.

9.2. Further issues at esmini GitHub page

The Issues tab at esmini GitHub page is a valuable source of questions and answers. To search in all issues, make sure to set filter: is:issue.

10. Command reference

10.1. esmini

10.1.1. Launch commands

Usage: esmini [options]
Options:
  --osc <filename>
      OpenSCENARIO filename (required) - if path includes spaces, enclose with ""
  --aa_mode <mode>
      Anti-alias mode=number of multisamples (subsamples, 0=off, 4=default)
  --align_routepositions
      Align t-axis of route positions to the direction of the route
  --bounding_boxes
      Show entities as bounding boxes (toggle modes on key ',')
  --capture_screen
      Continuous screen capture. Warning: Many jpeg files will be created
  --camera_mode <mode>
      Initial camera mode ("orbit" (default), "fixed", "flex", "flex-orbit", "top", "driver", "custom") (swith with key 'k')
  --csv_logger <csv_filename>
      Log data for each vehicle in ASCII csv format
  --collision
      Enable global collision detection, potentially reducing performance
  --custom_camera <position>
      Additional custom camera position <x,y,z>[,h,p] (multiple occurrences supported)
  --custom_fixed_camera <position and optional orientation>
      Additional custom fixed camera position <x,y,z>[,h,p] (multiple occurrences supported)
  --custom_fixed_top_camera <position and rotation>
      Additional custom top camera <x,y,z,rot> (multiple occurrences supported)
  --custom_light <position and intensity>
      Additional custom light source <x,y,z,intensity> intensity range 0..1 (multiple occurrences supported)
  --disable_controllers
      Disable controllers
  --disable_pline_interpolation
      Do not apply orientation interpolation of polyline trajectories
  --disable_log
      Prevent logfile from being created
  --disable_stdout
      Prevent messages to stdout
  --enforce_generate_model
      Generate road 3D model even if SceneGraphFile is specified
  --fixed_timestep <timestep>
      Run simulation decoupled from realtime, with specified timesteps
  --follow_object <index>
      Set index of intial object for camera to follow (change with Tab/shift-Tab)
  --generate_no_road_objects
      Do not generate any OpenDRIVE road objects (e.g. when part of referred 3D model)
  --generate_without_textures
      Do not apply textures on any generated road model (set colors instead as for missing textures)
  --ground_plane
      Add a large flat ground surface
  --headless
      Run without viewer window
  --help
      Show this help message
  --hide_route_waypoints
      Disable route waypoint visualization (toggle with key 'R')
  --hide_trajectories
      Hide trajectories from start (toggle with key 'n')
  --ignore_z
      Ignore provided z values from OSC file and place vehicle relative to road
  --ignore_p
      Ignore provided pitch values from OSC file and place vehicle relative to road
  --ignore_r
      Ignore provided roll values from OSC file and place vehicle relative to road
  --info_text <mode>
      Show on-screen info text (toggle key 'i') mode 0=None 1=current (default) 2=per_object 3=both
  --logfile_path <path>
      logfile path/filename, e.g. "../esmini.log" (default: log.txt)
  --osc_str <string>
      OpenSCENARIO XML string
  --osg_screenshot_event_handler
      Revert to OSG default jpg images ('c'/'C' keys handler)
  --osi_file [filename]  (default = ground_truth.osi)
      save osi trace file
  --osi_freq <frequence>
      relative frequence for writing the .osi file e.g. --osi_freq=2 -> we write every two simulation steps
  --osi_lines
      Show OSI road lines (toggle during simulation by press 'u')
  --osi_points
      Show OSI road pointss (toggle during simulation by press 'y')
  --osi_receiver_ip <IP address>
      IP address where to send OSI UDP packages
  --param_dist <filename>
      Run variations of the scenario according to specified parameter distribution file
  --param_permutation <index (0 .. NumberOfPermutations-1)>
      Run specific permutation of parameter distribution
  --pause
      Pause simulation after initialization
  --path <path>
      Search path prefix for assets, e.g. OpenDRIVE files (multiple occurrences supported)
  --player_server
      Launch UDP server for action/command injection
  --plot [mode (asynchronous|synchronous)]  (default = asynchronous)
      Show window with line-plots of interesting data
  --record <filename>
      Record position data into a file for later replay
  --road_features <mode>
      Show OpenDRIVE road features ("on", "off"  (default)) (toggle during simulation by press 'o')
  --return_nr_permutations
      Return number of permutations without executing the scenario (-1 = error)
  --save_generated_model
      Save generated 3D model (n/a when a scenegraph is loaded)
  --save_xosc
      Save OpenSCENARIO file with any populated parameter values (from distribution)
  --seed <number>
      Specify seed number for random generator
  --sensors
      Show sensor frustums (toggle during simulation by press 'r')
  --server
      Launch server to receive state of external Ego simulator
  --text_scale [factor]  (default = 1.0)
      Scale screen overlay text
  --threads
      Run viewer in a separate thread, parallel to scenario engine
  --trail_mode <mode>
      Show trail lines and/or dots (toggle key 'j') mode 0=None 1=lines 2=dots 3=both
  --use_signs_in_external_model
      When external scenegraph 3D model is loaded, skip creating signs from OpenDRIVE
  --version
      Show version and quit

Additional OSG graphics options:
  --clear-color <color>                      Set the background color of the viewer in the form "r,g,b[,a]"
  --screen <num>                             Set the screen to use when multiple screens are present
  --window <x y w h>                         Set the position x, y and size w, h of the viewer window. -1 -1 -1 -1 for fullscreen.
  --borderless-window <x y w h>              Set the position x, y and size w, h of a borderless viewer window. -1 -1 -1 -1 for fullscreen.
  --SingleThreaded                           Run application and all graphics tasks in one single thread.
  --lodScale <LOD scalefactor>               Adjust Level Of Detail 1=default >1 decrease fidelity <1 increase fidelity

For a complete list of OSG options and environment variables, see here:
https://github.com/esmini/esmini/blob/master/docs/osg_options_and_env_variables.txt

10.1.2. Runtime key shortcut commands

Key shortcuts

    H (shift + h): Print this help text to console
    Space:         Toggle pause/play simulation
    Return:        Step simulation (one timestep) then pause
    TAB:           Move camera to next vehicle
    Shift + TAB:   Move camera to previoius vehicle
    Delete:        Same as above (Shift + TAB)
    o:             Toggle show / hide OpenDRIVE road feature lines
    O:             Toggle show / hide odr signal bounding boxes
    u:             Toggle show / hide OSI road lines
    y:             Toggle show / hide OSI road points
    p:             Toggle show / hide environment 3D model
    r:             Toggle show / hide sensor view frustums
    R:             Toggle route waypoint visualization
    i:             Toggle on-screen info text modes
    j:             Toggle show trails after vehicles(4 modes: none / dots / lines / both)
    n:             Toggle show active trajectories
    , (comma):     Switch entity view : Model only / Bounding box / Model + Bounding box / None
    K:             Print current camera position and orientation to console
    ESC:           quit

    Arrow keys is used to drive externally controlled Ego vehicle:
        Up:    Accelerate
        Down:  Brake
        Left:  Steer left
        Right: Steer right

    1 - 9: Camera models acording to :
        1: Custom camera model
        2: Flight
        3: Drive
        4: Terrain
        5: Orbit
        6: FirstPerson
        7: Spherical
        8: NodeTracker
        9: Trackball

    When custom camera model(1) is activated
        k: Switch between the following sub models:
           - Orbit        (camera facing vehicle, rotating around it)
           - Fixed        (fix rotation, always straight behind vehicle)
           - Flex         (imagine the camera attached to vehicle via an elastic string)
           - Flex - orbit (Like flex but allows for roatation around vehicle)
           - Top          (top view, fixed rotation, always straight above vehicle)
           - Driver       ("driver" view, fixed at center of vehicle)

    Viewer options
        f: Toggle full screen mode
        t: Toggle textures
        s: Rendering statistics
        l: Toggle light
        w: Toggle geometry mode(shading, wireframe, dots)
        c: Save screenshot in JPEG format - in the folder where the application was started from
        C: Toggle continuous screen capture (e.g for video creation)
        h: Help

Mouse control

    Left:   Rotate
    Right:  Zoom
    Middle: Pan

    This is typical, exact behaviour depends on active camera model.

10.2. replayer

Usage: replayer [options]
Options:
  --file <filename>
      Simulation recording data file (.dat)
  --aa_mode <mode>
      Anti-alias mode=number of multisamples (subsamples, 0=off, 4=default)
  --camera_mode <mode>
      Initial camera mode ("orbit" (default), "fixed", "flex", "flex-orbit", "top", "driver") (toggle during simulation by press 'k')
  --capture_screen
      Continuous screen capture. Warning: Many jpeg files will be created
  --collision [mode]  (default = pause)
      Detect collisions and optionally pauses the replay <pause/continue> (pause is default)
  --custom_camera <position>
      Additional custom camera position <x,y,z>[,h,p] (multiple occurrences supported)
  --custom_fixed_camera <position and optional orientation>
      Additional custom fixed camera position <x,y,z>[,h,p] (multiple occurrences supported)
  --custom_fixed_top_camera <position and rotation>
      Additional custom top camera <x,y,z,rot> (multiple occurrences supported)
  --dir <path>
      Directory containing replays to overlay, pair with "file" argument, where "file" is .dat filename match substring
  --ground_plane
      Add a large flat ground surface
  --generate_without_textures
      Do not apply textures on any generated road model (set colors instead as for missing textures)
  --headless
      Run without viewer window
  --hide_trajectories
      Hide trajectories from start (toggle with key 'n')
  --info_text <mode>
      Show on-screen info text (toggle key 'i') mode 0=None 1=current (default) 2=per_object 3=both
  --no_ghost
      Remove ghost entities
  --no_ghost_model
      Remove only ghost model, show trajectory (toggle with key 'g')
  --osg_screenshot_event_handler
      Revert to OSG default jpg images ('c'/'C' keys handler)
  --path <path>
      Search path prefix for assets, e.g. model_ids.txt file (multiple occurrences supported)
  --quit_at_end
      Quit application when reaching end of scenario
  --remove_object <id>
      Remove object(s). Multiple ids separated by comma, e.g. 2,3,4.
  --repeat
      loop scenario
  --res_path <path>
      Path to resources root folder - relative or absolut
  --road_features
      Show OpenDRIVE road features
  --save_merged <filename>
      Save merged data into one dat file, instead of viewing
  --start_time <ms>
      Start playing at timestamp
  --stop_time <ms>
      Stop playing at timestamp (set equal to time_start for single frame)
  --text_scale [factor]  (default = 1.0)
      Scale screen overlay text
  --time_scale <factor>
      Playback speed scale factor (1.0 == normal)
  --view_mode <view_mode>
      Entity visualization: "model"(default)/"boundingbox"/"both"
  --use_signs_in_external_model
      When external scenegraph 3D model is loaded, skip creating signs from OpenDRIVE

Additional OSG graphics options:
  --clear-color <color>                      Set the background color of the viewer in the form "r,g,b[,a]"
  --screen <num>                             Set the screen to use when multiple screens are present
  --window <x y w h>                         Set the position x, y and size w, h of the viewer window. -1 -1 -1 -1 for fullscreen.
  --borderless-window <x y w h>              Set the position x, y and size w, h of a borderless viewer window. -1 -1 -1 -1 for fullscreen.
  --SingleThreaded                           Run application and all graphics tasks in one single thread.
  --lodScale <LOD scalefactor>               Adjust Level Of Detail 1=default >1 decrease fidelity <1 increase fidelity

Key shortcuts

    H (shift + h): Print this help text to console
    TAB:           Move camera to next vehicle
    Shift + TAB:   Move camera to previoius vehicle
    Delete:        Same as above (Shift + TAB)
    Space:         Toggle pause / play
    g:             Toggle show / hide ghost models
    o:             Toggle show / hide OpenDRIVE road feature lines
    u:             Toggle show / hide OSI road lines
    y:             Toggle show / hide OSI road points
    p:             Toggle show / hide environment 3D model
    i:             Toggle on-screen info text modes
    n:             Toggle show active trajectories
    , (comma):     Switch entity view : Model only / Bounding box / Model + Bounding box / None
    K:             Print current camera position and orientation to console
    ESC:           quit

    Arrow keys
        Left:               Pause and move to previous frame(+Shift to skip 10 frames)
        Right:              Pause and move to next frame(+Shift to skip 10 frames)
        Shift + Left:       Pause and jump 0.1s back
        Shift + Right:      Pause and jump 0.1s forward
        Shift + Ctrl Left:  Pause and jump 1.0s back
        Shift + Ctrl Right: Pause and jump 1.0s forward
        Ctrl + Left:        Pause and jump to beginning
        Ctrl + Right:       Pause and jump to end
        Up:                 Increase timeScale(play faster)
        Down:               Decrease timeScale(play slower)

    1 - 9: Camera models acording to :
        1: Custom camera model
        2: Flight
        3: Drive
        4: Terrain
        5: Orbit
        6: FirstPerson
        7: Spherical
        8: NodeTracker
        9: Trackball

    When custom camera model(1) is activated
        k: Switch between the following sub models:
           - Orbit        (camera facing vehicle, rotating around it)
           - Fixed        (fix rotation, always straight behind vehicle)
           - Flex         (imagine the camera attached to vehicle via an elastic string)
           - Flex - orbit (Like flex but allows for roatation around vehicle)
           - Top          (top view, fixed rotation, always straight above vehicle)
           - Driver       ("driver" view, fixed at center of vehicle)

    Viewer options
        f: Toggle full screen mode
        t: Toggle textures
        s: Rendering statistics
        l: Toggle light
        w: Toggle geometry mode(shading, wireframe, dots)
        c: Save screenshot in JPEG format - in the folder where the application was started from
        C: Toggle continuous screen capture (e.g for video creation)
        h: Help

Mouse control

    Left:   Rotate
    Right:  Zoom
    Middle: Pan

    This is typical, exact behaviour depends on active camera model.

Recommended usage:
    Run esmini headless (fast without viewer) and produce a .dat file. Then launch replayer to view it. Example in Windows PowerShell, starting from esmini/bin folder:

    .\esmini --osc ..\resources\xosc\cut-in.xosc --record sim.dat --headless --fixed_timestep 0.01 ; .\replayer --file sim.dat --window 60 60 800 400 --res_path ..\resources --repeat

10.3. odrviewer

Usage: odrviewer [options]
Options:
  --help
      Show this help message
  --odr <odr_filename>
      OpenDRIVE filename (required)
  --aa_mode <mode>
      Anti-alias mode=number of multisamples (subsamples, 0=off, 4=default)
  --capture_screen
      Continuous screen capture. Warning: Many .tga files will be created
  --custom_fixed_camera <position and optional orientation>
      Additional custom camera position <x,y,z>[,h,p] (multiple occurrences supported)
  --custom_fixed_top_camera <position and rotation>
      Additional custom top camera <x,y,z,rot> (multiple occurrences supported)
  --density [density]  (default = 1.000000)
      density (cars / 100 m)
  --enforce_generate_model
      Generate road 3D model even if --model is specified
  --disable_log
      Prevent logfile from being created
  --disable_off_screen
      Disable esmini off-screen rendering, revert to OSG viewer default handling
  --disable_stdout
      Prevent messages to stdout
  --duration <duration>
      Quit automatically after specified time (seconds, floating point)
  --fixed_timestep <timestep>
      Run simulation decoupled from realtime, with specified timesteps
  --generate_no_road_objects
      Do not generate any OpenDRIVE road objects (e.g. when part of referred 3D model)
  --generate_without_textures
      Do not apply textures on any generated road model (set colors instead as for missing textures)
  --ground_plane
      Add a large flat ground surface
  --headless
      Run without viewer window
  --logfile_path <path>
      logfile path/filename, e.g. "../esmini.log" (default: log.txt)
  --model <model_filename>
      3D Model filename
  --osg_screenshot_event_handler
      Revert to OSG default jpg images ('c'/'C' keys handler)
  --osi_lines
      Show OSI road lines (toggle during simulation by press 'u')
  --osi_points
      Show OSI road points (toggle during simulation by press 'y')
  --path <path>
      Search path prefix for assets, e.g. car and sign model files
  --road_features
      Show OpenDRIVE road features (toggle during simulation by press 'o')
  --save_generated_model
      Save generated 3D model (n/a when a scenegraph is loaded)
  --seed <number>
      Specify seed number for random generator
  --speed_factor [speed_factor]  (default = 1.000000)
      speed_factor <number>
  --stop_at_end_of_road
      Instead of respawning elsewhere, stop when no connection exists
  --text_scale [factor]  (default = 1.0)
      Scale screen overlay text
  --traffic_rule <rule (right/left)>
      Enforce left or right hand traffic, regardless OpenDRIVE rule attribute (default: right)
  --use_signs_in_external_model
      When external scenegraph 3D model is loaded, skip creating signs from OpenDRIVE
  --version
      Show version and quit

Additional OSG graphics options:
  --clear-color <color>                      Set the background color of the viewer in the form "r,g,b[,a]"
  --screen <num>                             Set the screen to use when multiple screens are present
  --window <x y w h>                         Set the position x, y and size w, h of the viewer window. -1 -1 -1 -1 for fullscreen.
  --borderless-window <x y w h>              Set the position x, y and size w, h of a borderless viewer window. -1 -1 -1 -1 for fullscreen.
  --SingleThreaded                           Run application and all graphics tasks in one single thread.
  --lodScale <LOD scalefactor>               Adjust Level Of Detail 1=default >1 decrease fidelity <1 increase fidelity

Examples:

1. View the ODR file and some random traffic on a 3D model, window mode 1000 x 500:
   odrviewer --odr xodr\e6mini.xodr --model models\e6mini.osgb --window 60 60 1000 500

2. Just ODR, fullscreen
   odrviewer --odr xodr\e6mini.xodr

3. Remove traffic
   odrviewer --odr xodr\e6mini.xodr --model models\e6mini.osgb --density 0 --window 60 60 1000 500

4. Sparse traffic (about 0.5 vehicle per 100 meter = 1 per 200 m)
   odrviewer --odr xodr\e6mini.xodr --model models\e6mini.osgb --density 0.5 --window 60 60 1000 500


Key shortcuts

    H (shift + h): Print this help text to console
    Space:         Toggle pause/play simulation
    Return:        Step simulation(one timestep) then pause
    TAB:           Move camera to next vehicle
    Shift + TAB:   Move camera to previoius vehicle
    Delete:        Same as above (Shift + TAB)
    o:             Toggle show / hide OpenDRIVE road feature lines
    u:             Toggle show / hide OSI road lines
    y:             Toggle show / hide OSI road points
    p:             Toggle show / hide environment 3D model
    i:             Toggle info text showing time and speed
    , (comma):     Switch entity view : Model only / Bounding box / Model + Bounding box / None
    K:             Print current camera position and orientation to console
    ESC:           quit

    1 - 9: Camera models acording to :
        1: Custom camera model
        2: Flight
        3: Drive
        4: Terrain
        5: Orbit
        6: FirstPerson
        7: Spherical
        8: NodeTracker
        9: Trackball

    When custom camera model(1) is activated
        k: Switch between the following sub models:
           - Orbit        (camera facing vehicle, rotating around it)
           - Fixed        (fix rotation, always straight behind vehicle)
           - Flex         (imagine the camera attached to vehicle via an elastic string)
           - Flex - orbit (Like flex but allows for roatation around vehicle)
           - Top          (top view, fixed rotation, always straight above vehicle)
           - Driver       ("driver" view, fixed at center of vehicle)

    Viewer options
        f: Toggle full screen mode
        t: Toggle textures
        s: Rendering statistics
        l: Toggle light
        w: Toggle geometry mode(shading, wireframe, dots)
        c: Save screenshot in JPEG format - in the folder where the application was started from
        C: Toggle continuous screen capture
        h: Help

Mouse control

    Left:   Rotate
    Right:  Zoom
    Middle: Pan

    This is typical, exact behaviour depends on active camera model.

10.4. plot_dat.py

usage: plot_dat.py [-h] [--x_axis X_AXIS] [--equal_axis_aspect] [--derive] [--dots] (--list_params | --param PARAM) <filename>

positional arguments:
  filename             dat filename

optional arguments:
  -h, --help           show this help message and exit
  --x_axis X_AXIS      x-axis parameter
  --equal_axis_aspect  lock aspect ratio = 1:1
  --derive             derive values wrt x, i.e. dy/dx
  --dots               add dots
  --list_params        list available parameters in given file
  --param PARAM        parameter to plot (can be specified multiple times)

Note: In addition to <filename> one of the arguments --list_params and --param <PARAM> is required

11. Build guide

11.1. Build configurations in Visual Studio and Visual Studio Code

From esmini version 2.29.0 a set of cmake presets are provided. These can be utilized from Visual Studio Code and Visual Studio (from 2019 version 16.10 onwards, see more info here) with the CMake Tools extension.

11.2. Build configurations from command line

CMake tool is used to create standard make configurations. A few example "create…​" batch scripts are supplied (in scripts folder) as examples how to generate desired build setup from command line.

  • VisualStudio / win64 / Windows SDK v10 / Release and Debug

  • Ubuntu and Kubuntu (tested on 18.04) / gcc / Release and Debug

However, it should be possible to configure custom variants using cmake. For example to use Visual Studio 2019 run the following commands from command prompt (CMD or PowerShell), assuming starting point is esmini root folder:

mkdir build
cd build
cmake .. -G "Visual Studio 16 2019"
cmake --build . --config Release --target install

This will first generate Visual Studio solution and then compile esmini using MSVC toolset v142 (default with Visual Studio 2019). Default architecture should be x64.

If you want to compile with MSVC 2017 toolset just add directive to the generator as follows:

cmake .. -G "Visual Studio 16 2019" -T v141

A complete list of supported toolsets are available here.

Provided 3rd party lib binaries are only provided for Windows x64 architecture. To make sure you build for x64 add -A x64, For example:

cmake .. -G "Visual Studio 16 2019" -T v141 -A x64

If you want to compile with MSVC 2015 (v140) toolset and for win32 use the following generator command:

cmake -G "Visual Studio 16 2019" -T v140 -A Win32 ..

Note:

Provided 3rd party libs only available for x64 builds. If you need to build full esmini for Win32 (32bit), you need to compile the 3rd party libraries yourself. Perhaps the "scripts/generate_*_libs.sh" scripts can be a starting point for such an endeavor.

Another possibility for Win32 is to build a slim esmini with a minimum of dependencies. See Slim esmini - customize configration.

Of course, building with a specific toolset requires it to be installed. Use Visual Studio Installer. Steps:

  • choose "Modify"

  • make sure Desktop Development with C++ is checked

  • go to tab "Individual components"

  • scroll down to "Compilers, build tools, and runtimes"

  • check the MSVC versions you need, e.g. "MSVC v140 - VS 2015 C++ build tools (v14.00)" and "MSVC v141 - VS 2017 C++ x64/x86 build tools (v14.16)"

All configurations defines an "Install" build target that compiles (if needed) and copies relevant binaries into a common "esmini/bin" folder recognized by the example scripts under the "esmini/run" folder.

Note:

  • For automatic downloading of external dependencies (OSG binaries) and 3D models, CMake version 3.11.4 or above is required (FetchContent_MakeAvailable was introduced).

  • In Windows, if you get an error like "the c compiler identification is unknown", then please make sure to install "Windows Universal CRT SDK" from the Visual Studio Installer tool.

11.3. External dependencies

esmini is designed to link all dependencies statically. Main reason is to have a all-inclusive library for easy integration either as a shared library/DLL (e.g. plugin in Unity, or S-function in Simulink) or statically linked into a native application.

Note: Nothing stops you from going with all dynamic linking, it’s just that provided build scripts are not prepared for it.

CMake scripts will download several pre-compiled 3rd party packages and 3D model resource files.

Default location for these resources is Google drive. If there is an issue, try switch to the backup location at Dropbox by changing the following line in EnvironmentSimulator/CMakeLists.txt:

set ( FILE_STORAGE "google" )
to
set ( FILE_STORAGE "dropbox" )

Links to all packages can be found in EnvironmentSimulator/CMakeLists.txt.

If you need to (re)build a 3rd party lib for some reason, e.g. for an yet unsupported system or need for a specific version, these build scripts might be a starting point:

or this script that builds all three libs, combining the above ones:

11.4. Additional platform dependencies

Linux Ubuntu

sudo apt install build-essential gdb ninja-build git pkg-config libgl1-mesa-dev libpthread-stubs0-dev libjpeg-dev libxml2-dev libpng-dev libtiff5-dev libgdal-dev libpoppler-dev libdcmtk-dev libgstreamer1.0-dev libgtk2.0-dev libcairo2-dev libpoppler-glib-dev libxrandr-dev libxinerama-dev curl cmake

Also, g++ version >= 5 is needed for c++14 code support.

Windows and Mac: Install the cmake application

11.5. Run and Debug with Linux and visual studio code

To run from the visual studio, the steps below might help with setup configuration.

11.5.1. With preset

Note: Make sure cmake.preset set to auto to recognize the presets (defined in the CMakePresets.json file). Open setting (Ctrl + ,) type cmake.preset, set auto in drop down box. This is a global setting.

To run:

  1. Open the Command Palette (Ctrl+Shift+P) and run CMake: Configure. Select ubuntu_release

  2. Shift + F7 to select and build specific target

    • e.g. install to build and copy all targets to bin folder

  3. Configure custom run arguments (create launch.json)

    • Open the run and debug window (Ctrl+Shift+D)

    • Select create a launch.json file

    • In the drop down list, select C++ (GDB/LLDB)

    • Select Add Configuration…​ (in the lower right corner of the sub window)

    • Select C/C++: (gdb) Launch

    • Change program setting as "${workspaceFolder}/build/EnvironmentSimulator/Applications/esmini/esmini"

    • Change args as ["--window", "60", "60", "800", "400", "--osc", "../resources/xosc/cut-in.xosc"]

  4. Press F5

For Debug, Repeat step 1 and select ubuntu_debug. Repeat step 2 to build debug binaries and then step 4. Breakpoints can be added to the program specified in the launch.json file (do step 3 if not already done).

Build target can also be changed in the status bar, click Set the default build target. If not cmake commands are visible in status bar, check global settings: Ctrl + , then write "cmake status bar" to find Status Bar Visibility setting. Set to visible.

Another way to set build target is via Ctrl+Shift+P, then write "set build target" to find CMake: Set Build Target setting. Select it and choose build target in the drop down list that should appear.

11.5.2. Without preset

Make sure cmake.preset set to never to ignore the preset file (CMakePresets.json):

  • Open setting (Ctrl+,) type cmake.preset, select never in drop down box

Note: This is a global setting, i.e. will affect other VSCode instances. Reset to auto if needed. Another way to ignore the presets is simply to remove the CMakePresets.json file and restart VSCode.

To run:

  1. Open the Command Palette (Ctrl+Shift+P) and run CMake: Select a Kit. Select the compiler you want to use. For example GCC 11.3.0

  2. Open the Command Palette (Ctrl+Shift+P) run the CMake: Select Variant command. Select Release

  3. Open the Command Palette (Ctrl+Shift+P) run the CMake: Set Build Target. Select install

  4. Open the Command Palette (Ctrl+Shift+P) and run the CMake: Build command, or select the Build button from the Status bar. (If cmake commands are not visible in status bar, check global settings: Ctrl + , then write "cmake status bar" to find Status Bar Visibility setting. Set to visible.)

  5. To configure launch arguments, see step 3 in With preset.

  6. Press F5

For debug:

Repeat step 2 and select Debug. Repeat step 3 to 6. Breakpoints can be added to the program specified in the launch.json.

11.6. Dynamic protobuf linking

When linking esmini with software already dependent on Google protobuf there might be need for dynamic linking of shared protobuf library. This can be achieved by defining cmake symbol DYN_PROTOBUF as following example:

cmake .. -D DYN_PROTOBUF=True

Then build as usual. It will link with protobuf shared library instead of linking with a static library.

When running esmini, the protobuf shared library needs to be found. Set dynamic library path environment variable to the folder where the library is. Example:

Linux:
export LD_LIBRARY_PATH=./externals/OSI/linux/lib-dyn

macOS:
export DYLD_LIBRARY_PATH=./externals/OSI/linux/lib-dyn

Note:
The dynamic versions of protobuf were added Aug 31 2021. So you might need to update the OSI library package. Get the latest from following links:

11.7. Slim esmini - customize configration

The external dependencies OSG, OSI, SUMO and implot are optional. Also the unit test suite is optional, in effect making the dependecy to googletest framework optional as well. All these options are simply controlled by the following cmake options:

  • USE_OSG

  • USE_OSI

  • USE_SUMO

  • USE_GTEST

  • USE_IMPLOT

So, for example, to cut dependency to OSG and SUMO, run:
cmake .. -D USE_OSG=False -D USE_SUMO=False

To disable OSG, SUMO, OSI and googletest, run:
cmake .. -D USE_OSG=False -D USE_SUMO=False -D USE_OSI=False -D USE_GTEST=False USE_IMPLOT=False

All options are enabled/True as default.

Note:
Disabling an external dependency will disable corresponding functionality. So, for example, disabling OSI means that no OSI data can be created by esmini. Disabling OSG means that esmini can’t visualize the scenario. However it can still run the scenario and create a .dat file, which can be played and visualized later in another esmini build in which OSG is enabled (even on another platform).

11.8. MSYS2 / MinGW-w64 support

esmini slim can be compiled and executed in the MSYS2 environment. Try following steps:

  • Download MSYS2 from: https://www.msys2.org

  • Install with default options

  • Start MSYS2 MinGW x64 (e.g. from start menu)

  • Update MSYS2 packages, run:
    pacman -Syu --disable-download-timeout --noconfirm
    (MSYS2 should close automatically)

  • Restart MSYS2

  • Finalize update and install needed packages, run:

pacman -Su --disable-download-timeout --noconfirm
pacman -S --needed base-devel mingw-w64-x86_64-toolchain --disable-download-timeout --noconfirm
pacman -S mingw-w64-x86_64-cmake --disable-download-timeout --noconfirm
  • Optional (not needed to compile or run esmini):
    pacman -S git --disable-download-timeout --noconfirm

  • Build esmini (from MSYS2 MinGW x64 command line):

cmake -G "MSYS Makefiles" -D USE_OSG=False -D USE_SUMO=False -D USE_OSI=False -D USE_GTEST=False ..
cmake --build . --config Release --target install

11.9. Build esmini project

First generate build configuration (see above)

Then it should work on all platform to build using cmake as follows:
cmake --build . --config Release --target install

Or you can go with platform specific ways of building:

Windows/Visual Studio

  1. Open generated solution, build*/EnvironmentSimulator.sln

  2. Select configuration, Debug or Release

  3. Build CMakePredefinedTargets/INSTALL (right-click and select build)

macOS

To generate a Xcode project file, run the initial cmake command as follows:
cmake -G Xcode ..

Then build as usual:
cmake --build . --config Release --target install

or using Xcode directly:
xcodebuild -scheme install -configuration Release build

or open the generated project file in Xcode, and build from there.

To create bundles (shared library container), do from esmini root folder:

lipo -create bin/libesminiRMLib.dylib -output bin/esminiRMLib.bundle
lipo -create bin/libesminiLib.dylib -output bin/esminiLib.bundle

Linux

Once cmake .. has created the build configuration, of course you can build by calling the gnu make applciation directly instead of going via cmake --build as described above.

cd build
make -j4 install

This will build all projects, in four parallel jobs, and copy the binaries into a dedicated folder found by the demo batch scripts.

11.10. CentOS 7 (Linux)

CentOS 7 has some limitations, e.g. old versions of C/C++ compiler toolkits and runtimes. So it’s not possible to link with provided 3rd party binary libraries targeting Ubuntu 18++. However, by disabling some featuers in esmini, e.g. OSI and SUMO, it can still be used for previewing scenarios.

VirtualBox image for Windows host here:
https://www.linuxvmimages.com/images/centos-7/

Follow steps below to build and run esmini on CentOS 7.

sudo yum install git
sudo yum install cmake
sudo yum install gcc-c++

sudo yum install freeglut-devel
sudo yum install fontconfig-devel
sudo yum install libXrandr-devel
sudo yum install libXinerama-devel

sudo yum install epel-release
sudo yum install p7zip

git clone https://github.com/esmini/esmini

cd esmini

cd externals
mkdir OpenSceneGraph
cd OpenSceneGraph
curl -L "https://www.dropbox.com/s/mxztf6zbgojyntp/osg_centos.7z?dl=1" -o osg_centos.7z
7za x osg_centos.7z
rm osg_centos.7z

cd ../..
mkdir build
cd build
cmake -D USE_OSG=True -D USE_SUMO=False -D USE_OSI=False -D USE_GTEST=False ..
cmake --build . --target install --config Release
cd ..
./bin/esmini.exe --headless --fixed_timestep 0.01 --record sim.dat --osc ./resources/xosc/cut-in.xosc

12. For esmini developers and contributors

Brief info on how to contribute is found here: https://github.com/esmini/esmini/blob/master/CONTRIBUTING.md

Complementing the Build guide this section contains additional information regarding branch strategy, formatting and test.

12.1. Branch strategy

From v2.30.0 the dev branch was introduced as the new top branch for continuous integration of features and bug fixes.

branch strategy

dev branch contains the latest integrated features and bug fixes. New features should be developed in branches created from dev. master is the default branch and primarily reserved for official releases.

Continuous integration job will be launched for pushed commits on the following branches:

  • master

  • dev

  • any other branch starting with feature/, e.g. feature/support_emergency_vehicles

Pull requests should be targeting the dev branch.

Master branch represent the latest released software. It should be in synch with dev.

12.1.1. Branch out of synch

dev and master branches should be in perfect sync, i.e. based on common history. Unfortunately, to keep this true and to avoid messy history and lost tags, branch history might be changed in rare cases. That can lead to local branch clones out of synch. If that is the case, follow these steps to restore sync of your local branch:

  1. git branch backup (only needed if you have local commits you want to preserve for later cherry-picking)

  2. git fetch origin

  3. git reset --hard origin/<branch name>

<branch name> might be master or dev, or whatever branch to be restored.

12.2. Formatting

From esmini v2.30.0 the CI will include formatting checks, in addition to static analysis and tests. The code styles are defined by following schemes:

To run formatting (both check and apply) locally, follow steps below:

Dependencies

clang-format

esmini makes use of version 15, avoiding some known bugs in v14

Windows:
Download and install LLVM-15.0.7-win64.exe found here:
https://github.com/llvm/llvm-project/releases/tag/llvmorg-15.0.7

Linux:

Check if clang-format version 15 is installed:

clang-format-15

If you get the following message:

Command 'clang-format-15' not found, but can be installed with:
sudo apt install clang-format-15

then clang 15 must be installed.

On Ubuntu systems prior to 22.04, first install llvm version 15:

wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 15

Source: apt.llvm.org

Then, clang v15 can be installed:

sudo apt install clang-format-15

see also: ci.yml

Install dependent Python packages
pip install -r ./support/python/requirements.txt

Check format

./support/python/src/main.py run format --clang_format_checker
./support/python/src/main.py run format --cmake_format_checker
./support/python/src/main.py run format --black_format_checker

12.2.1. Apply formatting

./support/python/src/main.py run format

or specifics:

./support/python/src/main.py run format --clang_format
./support/python/src/main.py run format --cmake_format
./support/python/src/main.py run format --black_format

12.3. Test

12.3.1. Test frameworks

There are two sets of tests on different frameworks:

  1. Unit tests (white box)

  2. Smoke tests (black box)

The unit tests suites are basically C++ code modules of test cases based on the Google Test framework. They range from true unit tests (testing individual classes or functions) to system level tests reading a scenario and inspecting output, e.g. log files or visualization pixel data. The key feature of the unit tests is the possibility to inspect internal objects and variables.

The smoke test suite is basically execution of a selection of scenarios with some strategic inspections of the result. Scenario elements and timing can be checked against log file while object info such as position, rotation and speed can be checked against dat file entries. A Python framework handles the execution and gathering of result for convenient checking.

The smoke test script is found here: test/smoke_test.py

12.3.2. Run tests

For easy execution of all tests, both unit tests and smoke tests, run this main test script: scripts/run_tests.sh, from esmini root folder:

./scripts/run_tests.sh

Tip: On Windows, run it from GIT bash.

12.3.3. Run XML schema validation tests

XML schema validation ensures scenario files (xosc and xodr) complies with the OpenSCENARIO and OpenDrive XSD schemas. The checks are done by the Python script run_schema_comply.py located in the scripts directory. The schema files are located in the resources/schema directory. From release v2.40, all esmini scenario files are validated against the schemas as part of CI.

The script depends on the xmlschema and lxml Python packages. To install required packages, run:

pip install -r support/python/requirements.txt

Run the XML validation tests:

python3 ./scripts/run_schema_comply.py <path1> [path2] [path3] …​

where path can be an XML file or a directory. Any directory subfolders will be included recursively.

Example 1, check single file:

python3 scripts/run_schema_comply.py resources/xosc/cut-in.xosc

Example 2, check two files:

python3 scripts/run_schema_comply.py resources/xosc/cut-in.xosc resources/xodr/two_plus_one.xodr

Example 3, check two directories:

python3 scripts/run_schema_comply.py resources EnvironmentSimulator

Example 4, check one file and one directory:

python3 scripts/run_schema_comply.py EnvironmentSimulator/Unittest/xosc/conflicting-domains.xosc resources

Tip: Use bash find command to apply finer filtering. Example:

python3 scripts/run_schema_comply.py $(find ./resources ./EnvironmentSimulator -name "follow_route*.xosc" -or -name "*500*.xodr" -or -name "*signs.xodr")

validates all OpenSCENARIO files starting with "follow_route" and all OpenDRIVE files containing "500" or ending basename with "signs" or containing "500", in and under the resource and EnvironmentSimulator folders.

12.4. Sanitizers

Google sanitizers framework is utilized to detect memory related errors, for example memory leaks. It’s executed nighly as a GitHub CI actions job on dev branch. The job can also be trigged manually, but will only run checks on dev branch. Sanitizers is currently only applied to Linux builds.

To run the sanitizer checks locally, on Ubuntu:

  1. Set the following environment variables, run from esmini root:

export LSAN_OPTIONS="print_suppressions=false:suppressions=$(pwd)/scripts/LSAN.supp"
export ASAN_OPTIONS="detect_invalid_pointer_pairs=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:fast_unwind_on_malloc=0:suppressions=$(pwd)/scripts/ASAN.supp"
  1. Configure and build esmini with sanitizers enabled

cd build
cmake -G Ninja -DUSE_OSG=FALSE -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_SANITIZERS=TRUE ..
ninja install
cd ..
  1. Run the test suites, including both unit- and smoke tests, from esmini root:

./scripts/run_tests.sh

12.4.1. Troubleshooting

On Ubuntu some combination of gcc and sanitizer randomly cause sanitizer run failure with error message: AddressSanitizer:DEADLYSIGNAL.

If this happens, try the following:

setarch $(uname -m) -R
bash

setarch will affect reported architecture from system to sanitizer. bash is to restore shell environment, it can of course be replaced by any shell program, e.g. tcsh.

To revert settings, simply run:

exit
exit

First exit will escape the latter bash environment, the second reverts effect of the setarch command.

13. esmini lib programming

13.1. About the lib itself

The main idea with esmini lib is to provide a limited, simplifed but useful API to the complex world of esmini, OpenSCENARIO and traffic simulation. By making use of only standard C types it has been possible to wrap and use the library in numerous environments, like for example MATLAB, Simulink and Unity (C#).

esmini lib is a, so called, shared library which is linked dynamically with a custom application. This means that it’s not linked statically with the application, instead it’s loaded whenever the application is launched. An obvious advantage with shared libraries is that they can be shared between multiple applications, reducing size and potentially simplify maintenance.

An additional important advantage with dynamic linking is that the library can be updated without need to recompile the custom application making use of the library (however it can’t be updated during runtime, only in between launches). An obvious requirement is that the API is compatible when replacing a library with a different version, since the API is established at link-time. On the other side of the "same coin", a disadvantage with dynamic linking is that you need to have the shared library present at run-time.

Although esmini lib is a shared library itself, it includes everything needed in terms of 3rd party functionality. It’s actally linking all dependencies statically. Think about it as a single container including everything needed, e.g. support for 3D graphics, XML, expressions, and SUMO traffic simulation. There are two advantages with this approach: 1. Getting rid of dependency to hundreds of 3rd party shared libraries and 2. The linker makes sure to only include relevant functions which reduce the size by extreme (compared to bundle individual and complete shared libraries).

13.2. How to interact with esmini lib API

Below is a typical interaction between a custom application (e.g. vehicle simulator) and esmini (providing simulation of the environment, like road and traffic).

Frame app time app events esmini time esmini events

0 (t=0.0)

0.0

Init esmini("scenario.xosc") ⇒

0.0

0.0

0.0

Step(dt=0.0)

0.0

0.0

  • check conditions

0.0

0.0

  • update actions

0.0

0.0

  • step default controller (establish initial positions and speed)

0.0

0.0

  • save to dat, send OSI

0.0

0.0

  • Draw()

0.0

Get Ego state (initial position, speed…​) ⇒

0.0

0.0

0.0

⇐ Return Ego initial state

1 (t=0.0→0.1)

0.1

Update Ego(dt=0.1)

0.0

0.1

  • Do internal stuff, like vehicle dynamics

0.0

0.1

Report Ego state(state) ⇒

0.0

0.1

0.0

Register state of Ego, protect it from default controller (but do not apply state yet)

0.1

Step esmini(dt=0.1) ⇒

0.0

0.1

0.0

Step(dt=0.1)

0.1

0.0

  • check conditions

0.1

0.0

  • update actions

0.1

0.0

  • step default controller (move entities, except Ego)

0.1

0.1

  • update simulation time

0.1

0.1

  • apply states from external entities (Ego)

0.1

0.1

  • save to dat, send OSI

0.1

0.1

  • Draw()

2 (t=0.1→0.2)

0.2

Update Ego(dt=0.1)

0.1

0.2

  • Do internal stuff, like vehicle dynamics

0.1

0.2

Report Ego state(state) ⇒

0.1

0.2

0.1

Register state of Ego, protect it from default controller (but do not apply state yet)

0.2

Step esmini(dt=0.1) ⇒

0.1

0.2

0.1

Step(dt=0.1)

0.2

0.1

  • check conditions

0.2

0.1

  • update actions

0.2

0.1

  • step default controller (move entities, except Ego)

0.2

0.2

  • update simulation time

0.2

0.2

  • apply states from external entities (Ego)

0.2

0.2

  • save to dat, send OSI

0.2

0.2

  • Draw()

and so on…​

13.3. Functionality

No systematic documentation available. See header files esminiLib.hpp and esminiRMLib.hpp for API details.

The following section covers some specific use cases of the library.

13.3.1. Save or grab images

Screenshots can be controlled via the API:

int SE_SaveImagesToFile(int nrOfFrames);

nrOfFrames: -1 ⇒ continuously, 0 ⇒ stop, > 0 ⇒ number of frames, e.g. 1=next frame only, 2=next two frames

Images will be stored in current folder with name screen_shot_*.tga (* = counter) for post processing.

Images can also be saved to memory for instant processing. Control the feature with:

int SE_SaveImagesToRAM(bool state);

state: true ⇒ capture images, false ⇒ don’t capture (default, might improve performance on some systems)

Grab image with:

int SE_FetchImage(SE_Image *image);

image is a reference to a SE_Image struct which will contain the image meta and image data

See image-capture code example.

Note: In order to grab a rendered frame image, it must have been saved to RAM during the post rendering phase. First frame will be saved to RAM by default. The SE_SaveImagesToRAM() function must be used to enable the feature for frames > 0.

The reason for not enable always by default is performance savings. On some system the operation to read back rendered pixels from graphics frame buffer to application RAM memory is not neglectable (depending on whether color coding or byte order transformation is needed). So a compromize is to have the feature enabled for the first init frame, and then activate by user when needed.

Finally, to disable esmini image handling altogether and revert to OSG default handling, make the following call before SE_Init():

SE_SetOffScreenRendering(false);

Note that disabling the this will permanently disable the callback mechanism for the complete esmini session (overriding any SE_SaveImagesToRAM() call).

13.3.2. Custom camera positions

Similarly as with camera related launch arguments, camera positioning can be controlled via esmini lib API.

  • SE_AddCustomCamera()
    Add a camera with relative position and orientation (heading and pitch)

  • SE_AddCustomFixedCamera()
    Add a fixed camera at custom global position and orientation (heading and pitch)

  • SE_AddCustomAimingCamera()
    Add a camera with relative position looking at current entity

  • SE_AddCustomFixedAimingCamera()
    Add a camera at fixed location but always looking at current entity

  • SE_AddCustomFixedTopCamera()
    Add a top view camera with fixed position and rotation

  • SE_SetCameraObjectFocus
    Set/change object to focus on

More info, see headerfile.

Any number of cameras can be defined. These cameras can then be activated via function: SE_SetCameraMode().

Applications can even loop through the camera modes to produce images from several cameras. See code example multiple-cameras.cpp.

13.3.3. OSI data

OSI data can be retrieved in three ways:

  1. Storing OSI tracefile

  2. Receive in UDP packages

  3. Receive via API call

OSI tracefile

Call SE_EnableOSIFile(filename) in order to create a OSI tracefile. Specify custom filename or set 0 or "" to use default filename (currently "ground_truth.osi").

OSI via UDP

By launch argument --osi_receiver_ip <ip addr> you can have esmini sending OSI packeges in UDP frames. The socket port is hardcoded but can of course be changed in the code (OSI_OUT_PORT in OSIReporter.cpp).

See receiver side code example osi_receiver.cpp.

OSI data via API call

Fetch OSI data by either one of the API calls:

  • SE_GetOSIGroundTruthRaw()

  • SE_GetOSIGroundTruth(int* size)

The "raw" version will simply return a pointer to the already existing OSI data structure. No additional data copying will be performed. This is the preferered option if:

  1. The application is coded in C++, making the Protobuf data structures fully compatible

  2. The data is read before next call to SE_UpdateOSIGroundTruth(), which will update the data

See code example: osi-groundtruth, also covered in User guide - Hello world tutorial OSI groundtruth.

The second function will fetch the OSI data serialized into a string. This will be a true copy and also generic format which can be parsed in any environment, independent of OS and programming language. The backside is additional resources needed for the copying and serialization.

See code examples in ScenarioEngineDLL test cases, e.g: receive_GroundTruth.

Note:

SE_UpdateOSIGroundTruth() needs to be called after each SE_Step* in order to prepare and populate OSI data. This function will also save the OSI frame to file and send over UDP, if requested. Only after this preparation the SE_GetOSIGroundTruth* methods will get updated OSI data.

An exception is when OSI file or UDP receiver is specified by corrsponding launch arguments --osi_file [filename] and --osi_receiver_ip <ip addr>. Then, OSI update will automatically be updated by esmini according to specified multiplier --osi_freq <number> (1 is default). For example, --osi_freq 3 will update OSI every third step.

The purpose of this explicit OSI update control is to support use cases where esmini is running at much higher frequency than needed for OSI data (e.g. for vehicle dynamics purposes). To optimize performance, OSI update can then be skipped most frames and only run when needed.

BTW: Any additional explicit calls to SE_UpdateOSIGroundTruth() will be ignored. Which means that the programmer does not have to worry about additional performance penalty introduced by multiple calls to this function.

13.3.4. Simple ideal sensors

OSI provides information needed for sensor models at bounding box level for both moving and stationary objects, including ones defined in OpenDRIVE. Sensor models can then detect overlap, occlusion and collisions between those bounding boxes. Although sensor models are not within scope of esmini, there is a code example on how to retrieve and extract OSI data. It can perhaps be a starting point: osi-groundtruth.cpp

For even simpler use cases esmini provides a sensor mechanism, basically collecting scenario objects which reference point is within a sensor view frustum. This is too rough for collision detection, but can be useful for low-pass features like adaptive cruse control (ACC) or just optimizing CPU load by filtering objects for some processing.

The view frustum is defined by a horizontal field of view combined with near and far limits. Basically an object is detected by a ideal sensor if the object reference point is within the view frustum, i.e:

  • angle from sensor mounting point is within the field of view

  • distance from sensor mounting point is larger than near and less than far

ideal sensors
A few ideal sensors mounted at various positions and orientations

The sensors are added from the code, see the esmini-dyn code example. API is found here.

Limitations:

  • OpenDRIVE stationary objects not considered

  • Only reference point checking (part of the object can be inside frustum and still not detected)

13.4. Simplest example

Below example is just to show the simplicity of using esmini library. For more information and realistic examples, e.g. involving cmake, please go ahead to Hello World programming tutorial.

If not already done, download and unpack latest esmini demo. Make sure to pick demo, not bin, for your platform.

In esmini root folder, create a file named main.cpp and add the following lines:

Code:

#include "./EnvironmentSimulator/Libraries/esminiLib/esminiLib.hpp"

int main()
{
    SE_Init("./resources/xosc/cut-in.xosc", 0, 1, 0, 0);

    while (SE_GetQuitFlag() == 0)
        SE_Step();

    SE_Close();
    return 0;
}

Compile:

  • Linux:
    gcc main.cpp ./bin/libesminiLib.so -Wl,-rpath,\$ORIGIN -o bin/my_player

  • Windows (x64 Native Tools Command Prompt for Visual Studio):
    cl main.cpp ./bin/esminiLib.lib /Fe:bin/my_player

Run:

from Linux terminal or Windows PowerShell:
./bin/my_player

14. Hello World programming tutorial

This tutorial shows how to utilize esmini as a shared (dynamically loaded) library for use in a custom application. It can be used with or without esmini embedded viewer.

14.1. Preparations

The simplest preparation is to get the demo package from the latest relase and then head on to the steps below.

But, of course, if you already have checked out the esmini project from GitHub and compiled it you should already be set.

Just make sure that the shared library is available in the bin or lib folder, which should be available directly under esmini root. Exact filename depends on the platform as follows:

Windows:

esminiLib.dll

Linux:

libesminiLib.so

Mac:

libesminiLib.dylib

14.2. How to build, run and debug "Hello World"

14.2.1. Windows/Visual Studio

First step: Create the solution file

Run from Hello-World_coding-example root folder:
mkdir build
cd build
cmake .. (will generate solution for multiple variants, including Release and Debug)

Build and run the application from command line
Build

cmake --build . --config Release (release variant), or
cmake --build . --config Debug (debug variant)

Run
  1. open a command shell and put yourself in Hello-World_coding-example/build folder

  2. add shared library location to the environment path variable, e.g:
    CMD: set path=../../bin;%path%
    PowerShell: $env:path+=";../../bin"
    bash: PATH=$PATH:../../bin

  3. Launch application
    CMD:
    .\Debug\esmini-player.exe or
    .\Release\esmini-player.exe (depending on variant)
    PowerShell or bash:
    ./Debug/esmini-player.exe or
    ./Release/esmini-player.exe

Build, run and debug the application from Visual Studio
  1. Open esmini-player.sln in Visual Studio

  2. Build as usual (Ctrl-F7 or BuildBuild Solution)

  3. Set as startup project

    • Right click on esmini-player in Solution Explorer

    • Click on Set as Startup Project

  4. Tell the system where to find the shared library

    • Right click on esmini-player in Solution Explorer

    • Click on Properties

    • Select Configuration → All Configurations

    • In Configuration PropertiesDebuggingEnvironment write: path=$(ProjectDir)/../../bin
      or any other folder containing the esmini library

  5. Run

    • Ctrl-F5 to run without debug

    • F5 to debug

14.2.2. Linux

First step: Create the Makefile

Run from Hello-World_coding-example root folder:
mkdir build
cd build
then:
cmake .. -DCMAKE_BUILD_TYPE=Release for Release variant, or
cmake .. -DCMAKE_BUILD_TYPE=Debug for Debug variant

Build and run from command line
  1. Build
    cmake --build . or just
    make

  2. Tell system where to look for esmini shared library (maybe not necessary)
    export LD_LIBRARY_PATH=../../bin (on macOS: DYLD_LIBRARY_PATH)

  3. Launch application
    ./esmini-player

Build, run and debug from Visual Studio Code

Prerequisites: Install the following VS Code extensions:

  • C/C++

  • CMake Tools

Build
  1. Open visual studio as Hello-World_coding-example as root folder

  2. Open the Command Palette (Ctrl+Shift+P) and run CMake: Select a Kit. Select the compiler you want to use. For example GCC 11.3.0

  3. Open the Command Palette (Ctrl+Shift+P) run the CMake: Select Variant command. Select Debug.

  4. Open the Command Palette (Ctrl+Shift+P) run the CMake:Set Build Target. Select esmini-player.

  5. Open the Command Palette (Ctrl+Shift+P) and run the CMake: Build command, or select the build button from the Status bar.

Open main.cpp file in the Hello-World_coding-example folder and optionally introduce break points.

Run
  1. Open the Command Palette (Ctrl+Shift+P) and run the CMake: Run Without Debugging command, or

  2. Use play button in the status bar, or

  3. Shift-F5

Debug
  1. Open the Command Palette (Ctrl+Shift+P) and run the CMake: Debug command, or

  2. Use debug button in the status bar, or

  3. Ctrl-F5

14.2.3. Next step

Now that you can build and run Hello World, it’s time to explore some functionality of esmini. Next section presents a few code examples to try out, e.g. by modifying main.cpp.

14.3. Examples

14.3.1. Hello world - load and play a scenario

#include "esminiLib.hpp"

int main(int argc, char* argv[])
{
        SE_Init("../../resources/xosc/cut-in.xosc", 0, 1, 0, 0);

        for (int i = 0; i < 500; i++)
        {
                SE_Step();
        }

        SE_Close();

        return 0;
}

Exercise: Change scenario to pedestrian.xosc, then compile and run the program again.

14.3.2. Add optional argument to load any scenario

#include "esminiLib.hpp"

int main(int argc, char* argv[])
{
    if (argc > 1)
    {
        SE_InitWithArgs(argc, const_cast<const char**>(argv));
    }
    else
    {
        SE_Init("../resources/xosc/cut-in.xosc", 0, 1, 0, 0);
    }

    for (int i = 0; i < 500; i++)
    {
        SE_Step();
    }

    SE_Close();

    return 0;
}

You can now specify esmini arguments according to esmini launch commands.

Example:

./esmini-player --window 60 60 1000 500 --osc ../resources/xosc/pedestrian.xosc --trails

14.3.3. Fetch state of scenario objects

This example shows how to retrieve the state of scenario objects.

We also check the return code from the initialization and react on any error.

#include "stdio.h"
#include "esminiLib.hpp"

int main(int argc, char* argv[])
{
    (void)argc;
    (void)argv;

    if (SE_Init("../resources/xosc/cut-in.xosc", 0, 1, 0, 0) != 0)
    {
        printf("Failed to initialize scenario\n");
        return -1;
    }

    for (int i = 0; i < 500; i++)
    {
        SE_Step();

        for (int j = 0; j < SE_GetNumberOfObjects(); j++)
        {
            SE_ScenarioObjectState state;

            SE_GetObjectState(SE_GetId(j), &state);
            printf("time [%.2f] object[%d] id %d pos[%.2f, %.2f] %.2f %.2f \n",
                   static_cast<double>(state.timestamp),
                   j,
                   SE_GetId(j),
                   static_cast<double>(state.x),
                   static_cast<double>(state.y),
                   static_cast<double>(state.wheel_angle),
                   static_cast<double>(state.wheel_rot));
        }
    }

    SE_Close();

    return 0;
}

14.3.4. External control of Ego

A silly example showing how you can just take control over vehicle state via the API. The Ego car will move one meter along the Y-axis for each frame while rotating…​

Now we will also introduce the quit_flag, which lets you quit by pressing 'Esc' key.

In addition we’ll try out the alignment feature. By default objects will be aligned to road surface. This behavior can be controlled with alignment settings. In below example, the Ego vehicle will be aligned to road surface except during iterations 100-200 where it will obey the reported Z coordinate and move accordingly to elevation/Z = 10 meter.

#include "esminiLib.hpp"
#include <stdio.h>

int main(int argc, char* argv[])
{
    const char*            filename = argc > 1 ? argv[1] : "../resources/xosc/cut-in_external.xosc";
    SE_ScenarioObjectState state;
    double                 z = 0.0f;

    if (SE_Init(filename, 0, 1, 0, 0) != 0)
    {
        return -1;
    }

    for (int i = 0; i < 500 && SE_GetQuitFlag() != 1; i++)
    {
        SE_ReportObjectPos(SE_GetId(0), 0.0f, 8.0f, static_cast<float>(i), static_cast<float>(z), float(1.57 + 0.01 * i), 0.0f, 0.0f);

        SE_Step();

        // fetch position in terms of road coordinates
        SE_GetObjectState(SE_GetId(0), &state);

        printf("road_id: %d s: %.3f lane_id %d lane_offset: %.3f z: %.2f\n",
               state.roadId,
               static_cast<double>(state.s),
               state.laneId,
               static_cast<double>(state.laneOffset),
               static_cast<double>(state.z));

        if (i == 100)
        {
            z = 10.0;
            printf("Release relative road alignment, set absolute z = %.2f\n", z);
            SE_SetObjectPositionMode(SE_GetId(0),
                                     SE_PositionModeType::SE_SET,
                                     SE_PositionMode::SE_Z_ABS);  // release relative alignment to road surface
        }

        if (i == 200)
        {
            z = 0.0;
            printf("Restore relative road alignment, set relative z = %.2f\n", z);
            SE_SetObjectPositionMode(SE_GetId(0),
                                     SE_PositionModeType::SE_SET,
                                     SE_PositionMode::SE_Z_REL);  // enforce relative alignment to road surface
        }
    }

    SE_Close();

    return 0;
}

Exercise: Change heading with i, e.g:

SE_ReportObjectPos(0, 0.0f, 8.0f, (float)i, 0.0f, 1.57f + 0.01f*i, 0.0f, 0.0f);

Yes, it looks crazy! But it demonstrates how an application totally can take control of a vehicle.

14.3.5. Control controllers

Try to run cut-in_interactive.xosc, as below.

#include "esminiLib.hpp"

int main(int argc, char* argv[])
{
    (void)argc;
    (void)argv;

    SE_Init("../resources/xosc/cut-in_interactive.xosc", 0, 1, 0, 0);

    for (int i = 0; i < 2000 && SE_GetQuitFlag() != 1; i++)
    {
        SE_Step();
    }

    SE_Close();

    return 0;
}

Control the Ego vehicle with arrow keys.

To disable controllers and hand over to default scenario behavior set first argument flag (see headerfile esminiLib.hpp):

#include "esminiLib.hpp"

int main(int, char**)
{
    SE_Init("../resources/xosc/cut-in_interactive.xosc", 1, 1, 0, 0);

    for (int i = 0; i < 2000 && SE_GetQuitFlag() != 1; i++)
    {
        SE_Step();
    }

    SE_Close();

    return 0;
}

14.3.6. Ideal sensors

#include "stdio.h"
#include "esminiLib.hpp"

#define MAX_HITS 10

int main(int, char**)
{
    SE_Init("../resources/xosc/cut-in.xosc", 0, 1, 0, 0);

    SE_AddObjectSensor(0, 2.0f, 1.0f, 0.5f, 1.57f, 1.0f, 50.0f, 1.57f, MAX_HITS);
    SE_AddObjectSensor(0, -1.0f, 0.0f, 0.5f, 3.14f, 0.5f, 20.0f, 1.57f, MAX_HITS);

    // Turn on visualization of object sensors, toggle key 'r'
    SE_ViewerShowFeature(1, true);

    for (int i = 0; i < 2000 && !(SE_GetQuitFlag() == 1); i++)
    {
        SE_Step();

        int objList[MAX_HITS];
        for (int j = 0; j < 2; j++)  // iterate over added sensors
        {
            int nHits = SE_FetchSensorObjectList(j, objList);
            for (int k = 0; k < nHits; k++)
            {
                printf("Sensor[%d] detected obj: %d\n", j, objList[k]);

                // Get some info of that detected object
                SE_ScenarioObjectState state;

                SE_GetObjectState(objList[k], &state);
                printf("object[%d] pos: (%.2f, %.2f) heading: %.2f\n",
                       objList[k],
                       static_cast<double>(state.x),
                       static_cast<double>(state.y),
                       static_cast<double>(state.h));
            }
        }
    }

    SE_Close();

    return 0;
}

Note: If you want M_PI, add on top (before includes): #define _USE_MATH_DEFINES

14.3.7. Collision detection

There are two approaches to collision detection that fit different use cases:

1. By scenario

Making use of the OpenSCENARIO CollisionCondition a collision can be detected and act upon. This can be useful for example if a scenario or test case should end in case of Ego vehicle gets involved in a collision.

See example EnvironmentSimulator/Unittest/xosc/test-collision-detection.xosc. To run it from command line:

./bin/esmini --window 60 60 800 400 --osc ./EnvironmentSimulator/Unittest/xosc/test-collision-detection.xosc

Control the Ego car with keyboard arrow keys. Press "up" to accelerate, and "left" or "right" to steer. See output in console when colliding with the two other cars.

The interactive controller can be disabled. Then the Ego car will get the speed from the scenario. Run:

./bin/esmini --window 60 60 800 400 --osc ./EnvironmentSimulator/Unittest/xosc/test-collision-detection.xosc --disable_controllers

Note: In this example the action is empty, i.e. the event is triggered on collision but nothing really happens, other than logging the trigger result. In a real use case there could be an actual action, e.g. immediate stop by setting speed to zero in a single step. Another usage is to use the condition in a scenario stop trigger, e.g. to quit the test case as soon as collision happens.

2. By API

In addition, or as alternative, to detect collisions via OpenSCENARIO condition esmini can check for collisions between any objects regardless of the scenario. This is a feature that must be enabled (SE_CollisionDetection(true)) since it’s off by default to avoid unnecessary performance penalties.

See code example:

#include "esminiLib.hpp"
#include "stdio.h"

int main(void)
{
    SE_Init("../EnvironmentSimulator/Unittest/xosc/test-collision-detection.xosc", 1, 1, 0, 0);
    SE_CollisionDetection(true);

    for (int i = 0; i < 500; i++)
    {
        SE_Step();

        for (int j = 0; j < SE_GetNumberOfObjects(); j++)
        {
            for (int k = 0; k < SE_GetObjectNumberOfCollisions(j); k++)
            {
                printf("Collision[%d]: %d\n", j, SE_GetObjectCollision(j, k));
            }
        }
    }

    return 0;
}

Note: It uses the same scenario file as the above (1. By scenario) but the complete "CollisionManeuver" could be removed since no CollisionCondition is needed for API based collision detection. Actually, if maneuver is present the console will show log info from both the condition and the API driven collision detections.

14.3.8. Simple speed control

As an alternative to the external control examples above, you can control the speed of an object directly. Just note that any triggered scenario actions involving the object might conflict with speed settings via lib API. Recommendation is control an object either via API or scenario file. That said, the combination can be useful for some use cases.

The code will modulate speed of Ego (first vehicle) between 2 and 20 mps.

#include "esminiLib.hpp"
#include <iostream>

int main(int argc, char **argv)
{
    float acc   = 10;    // initial acceleration
    float speed = 2.0f;  // initial speed

    if (argc > 1)
    {
        SE_Init(argv[1], 0, 1, 0, 0);
    }
    else
    {
        printf("Usage: %s <path to xosc file>\n", argv[0]);
        return -1;
    }

    while (SE_GetQuitFlag() != 1)
    {
        speed += acc * SE_GetSimTimeStep();  // get latest time step from simulation

        // modulate speed by changing sign of acceleration now and then
        if (speed < 2.0f)
        {
            acc = 10;
        }
        else if (speed > 20.0f)
        {
            acc = -10;
        }

        // report updated speed
        SE_ReportObjectSpeed(0, speed);

        // step the simulation in natural speed, change to SE_Step(<time-step>) for fixed timestep
        SE_Step();
    }

    SE_Close();

    return 0;
}

Example run command, from esmini/code-examples-bin folder:
./speed_controller ../resources/xosc/cut-in.xosc

There is also a Python version of this example:
EnvironmentSimulator/code-examples/hello_world/speed_controller.py

14.3.9. Inject actions

This example demonstrates how you can control longitudinal and lateral motion by applying the following actions:

  • SpeedAction

  • LaneChangeAction

  • LaneOffsetAction

Currently (v2.37.0) the action injection feature is limited to these three actions, but it’s rather easy to extend if needed.

The code will apply three actions according as follows:

  • after 2 seconds, make a slight movement (lane offset) of 0.45m to the right

  • after 7 seconds, make a lane change to the left

  • after 8 seconds, consider to do another lane change, but skip since another is already ongoing

  • after 9.5 seconds, brake softly (braking will start before lane change is done)

  • after 11.0 seconds, brake harder to a stop (will cancel the ongoing soft brake action)

#include "esminiLib.hpp"
#include <string.h>
#include <iostream>

int main(int argc, char **argv)
{
    float dt = -1.0f;  // default to variable timestep

    if (argc > 2)
    {
        if (SE_InitWithArgs(argc, const_cast<const char **>(argv)) != 0)
        {
            printf("Error: Could not initialize scenario %s\n", argv[1]);
            return -1;
        }
        for (int i = 2; i < argc; i++)
        {
            if (strcmp(argv[i], "--fixed_timestep") == 0 && i < argc - 1)
            {
                dt = static_cast<float>(atof(argv[i + 1]));  // use any provided fixed timestep
            }
        }
    }
    else if (argc > 1)
    {
        if (SE_Init(argv[1], 0, 1, 0, 0) != 0)
        {
            printf("Error: Could not initialize scenario %s\n", argv[1]);
            return -1;
        }
    }
    else
    {
        printf("Usage: %s <path to xosc file>\n", argv[0]);
        return -1;
    }

    int state = 0;
    while (SE_GetQuitFlag() == 0 && SE_GetSimulationTime() < 17.0f)
    {
        if (state == 0 && SE_GetSimulationTime() > 2.0f)
        {
            printf("Injecting lane offset action\n");
            SE_LaneOffsetActionStruct lane_offset;
            lane_offset.id               = 0;       // id of object to perform action
            lane_offset.offset           = -0.45f;  // target offset in m
            lane_offset.maxLateralAcc    = 0.5f;    // max lateral acceleration in m/s^2
            lane_offset.transition_shape = 0;       // cubic
            SE_InjectLaneOffsetAction(&lane_offset);
            state++;
        }
        else if (state == 1 && SE_GetSimulationTime() > 7.0f)
        {
            printf("Injecting lane change action\n");
            SE_LaneChangeActionStruct lane_change;
            lane_change.id               = 0;     // id of object to perform action
            lane_change.mode             = 1;     // relative (own vehicle)
            lane_change.target           = 1;     // target lane id (relative)
            lane_change.transition_shape = 2;     // sinusoidal
            lane_change.transition_dim   = 2;     // time
            lane_change.transition_value = 3.0f;  // s
            SE_InjectLaneChangeAction(&lane_change);
            state++;
        }
        else if (state == 2 && SE_GetSimulationTime() > 8.0f)
        {
            if (SE_InjectedActionOngoing(5))  // 5 = LAT_LANE_CHANGE
            {
                printf("Lane change already ongoing, skipping second lane change\n");
            }
            else
            {
                printf("Injecting lane change action 2\n");
                SE_LaneChangeActionStruct lane_change;
                lane_change.id               = 0;     // id of object to perform action
                lane_change.mode             = 1;     // relative (own vehicle)
                lane_change.target           = -1;    // target lane id (relative)
                lane_change.transition_shape = 2;     // sinusoidal
                lane_change.transition_dim   = 2;     // time
                lane_change.transition_value = 3.0f;  // s
                SE_InjectLaneChangeAction(&lane_change);
            }
            state++;
        }
        else if (state == 3 && SE_GetSimulationTime() > 9.5f)  // slightly overlapping lane change action
        {
            printf("Injecting speed action - soft brake\n");
            SE_SpeedActionStruct speed;
            speed.id               = 0;     // id of object to perform action
            speed.speed            = 0.0f;  // target speed in m/s
            speed.transition_shape = 0;     // cubic
            speed.transition_dim   = 1;     // rate
            speed.transition_value = 3.0f;  // m/s^2
            SE_InjectSpeedAction(&speed);
            state++;
        }
        else if (state == 4 && SE_GetSimulationTime() > 11.0f)  // slightly overlapping lane change action
        {
            printf("Injecting speed action - hard brake\n");
            SE_SpeedActionStruct speed;
            speed.id               = 0;     // id of object to perform action
            speed.speed            = 0.0f;  // target speed in m/s
            speed.transition_shape = 1;     // cubic
            speed.transition_dim   = 1;     // rate
            speed.transition_value = 8.0f;  // m/s^2
            SE_InjectSpeedAction(&speed);
            state++;
        }

        if (dt < 0.0f)
        {
            SE_Step();
        }
        else
        {
            SE_StepDT(dt);
        }
    }

    SE_Close();

    return 0;
}

Example run command, from esmini/code-examples-bin folder:
./action_injection ../resources/xosc/cut-in.xosc

There is also a Python version of this example:
EnvironmentSimulator/code-examples/hello_world/action_injection.py

14.3.10. Driver model

Using a simple vehicle model this example demonstrates how a driver model can interact with the scenario in terms of pedal and steering input. This time we use the ExternalController again to ensure any scenario actions is not conflicting with the driver model.

To show different variants we also show how to set scenario parameters during intialization, via callbacks. In effect this is a way to achieve runtime variants of the same basic scenario.

Before heading into the application code we will look into the scenario file EnvironmentSimulator/code-examples/test-driver/test-driver.xosc.

Now, let’s have a look inside it to see how to activate the ExternalController, which will prevent the DefaultController to interfere with the Ego vehicle and instead hand over exclusive control to our application. You can skip this and go to the C++ code example below if you’re not interested in the controller setup.

  • Open test-driver.xosc

  • Look in the Entities section, under the <ScenarioObject name="Ego"\> element. Here the controller is defined and assigned to the Ego vehicle.

          <ObjectController>
                <Controller name="MyExternalControllerWithGhost">
                    <Properties>
                        <Property name="esminiController" value="ExternalController" />
                        <Property name="useGhost" value="$GhostMode" />
                        <Property name="headstartTime" value="2" />
                    </Properties>
                </Controller>
          </ObjectController>

Note: The GhostMode parameter is set to true or false in the ParameterDeclarations section in the top of the scenario file.

  • Then the initial position is set. This could instead be done by the application, but it’s convenient to specify it in the scenario file.

                <PrivateAction>
                    <TeleportAction>
                        <Position>
                            <LanePosition roadId="1" laneId="-1" offset="0" s="50"/>
                        </Position>
                    </TeleportAction>
                </PrivateAction>
  • Finally we need to activate the controller on both lateral and longitudinal domains.

                <PrivateAction>
                      <ActivateControllerAction longitudinal="true" lateral="true" />
                </PrivateAction>

The reason for putting it AFTER the positioning is that oterwise the position action would have no effect, since once the controller is activated all scenario actions will be ignored. The controller then having exclusive control.

Now let’s head into the code example. It will run the scenario three times:

  1. External controller without ghost mode

    • just look ahead and follow along current lane. Find point along the lane and steer towards it.

  2. External controller with ghost mode and time based lookahead

    • find the state of ghost at given time offset. Use it for steering and speed target.

  3. External controller with ghost mode and position based lookahead

    • given the current Ego position, find ghost state at some look ahead distance along its trail. Use it for steering and speed target.

Now, study the code and perhaps play around mainpulating some values.

#include "stdio.h"
#include "math.h"
#include <string>
#include "esminiLib.hpp"

void paramDeclCB(void* user_arg)
{
    bool ghostMode = *(static_cast<bool*>(user_arg));

    SE_LogMessage((std::string("Running with ghostMode = ").append(ghostMode == true ? "true" : "false")).c_str());
    SE_SetParameterBool("GhostMode", ghostMode);
}

int main(int argc, char* argv[])
{
    (void)argc;
    (void)argv;

    const double defaultTargetSpeed = 50.0;
    const double curveWeight        = 30.0;
    const double throttleWeight     = 0.1;
    const float  duration           = 35.0f;
    bool         ghostMode[3]       = {false, true, true};

    void*                  vehicleHandle = 0;
    SE_SimpleVehicleState  vehicleState  = {0, 0, 0, 0, 0, 0, 0, 0};
    SE_ScenarioObjectState objectState;
    SE_RoadInfo            roadInfo;

    for (int i = 0; i < 3; i++)
    {
        float dt = 0.0f;

        SE_RegisterParameterDeclarationCallback(paramDeclCB, &ghostMode[i]);

        if (SE_Init("../EnvironmentSimulator/code-examples/test-driver/test-driver.xosc", 0, 1, 0, 0) != 0)
        {
            SE_LogMessage("Failed to initialize the scenario, quit\n");
            return -1;
        }

        // Lock object to the original lane
        // If setting to false, the object road position will snap to closest lane
        SE_SetLockOnLane(0, true);

        // Initialize the vehicle model, fetch initial state from the scenario
        SE_GetObjectState(0, &objectState);
        vehicleHandle = SE_SimpleVehicleCreate(objectState.x, objectState.y, objectState.h, 4.0, 0.0);
        SE_SimpleVehicleSteeringRate(vehicleHandle, 6.0f);

        // show some road features, including road sensor
        SE_ViewerShowFeature(4 + 8, true);  // NODE_MASK_TRAIL_DOTS (1 << 2) & NODE_MASK_ODR_FEATURES (1 << 3),

        // Run for specified duration or until 'Esc' button is pressed
        while (SE_GetSimulationTime() < duration && SE_GetQuitFlag() != 1)
        {
            // Get simulation delta time since last call (first will be minimum timestep)
            dt = SE_GetSimTimeStep();

            // Get road information at a point some speed dependent distance ahead
            double targetSpeed;
            if (ghostMode[i] == true)
            {
                // ghost version
                float ghost_speed = 0.0f;
                float timestamp   = 0.0f;

                if (i % 2 == 0)  // alternate between time based and position based look ahead modes
                {
                    // Time based look ahead
                    SE_GetRoadInfoGhostTrailTime(0, SE_GetSimulationTime() + 0.25f, &roadInfo, &ghost_speed);
                }
                else
                {
                    // Position based Time based look ahead
                    SE_GetRoadInfoAlongGhostTrail(0, 5 + 0.75f * vehicleState.speed, &roadInfo, &ghost_speed, &timestamp);
                }
                targetSpeed = ghost_speed;
            }
            else
            {
                // Look ahead along the road, to establish target info for the driver model
                SE_GetRoadInfoAtDistance(0, 5 + 0.75f * vehicleState.speed, &roadInfo, 0, true);

                // Slow down when curve ahead - CURVE_WEIGHT is the tuning parameter
                targetSpeed = defaultTargetSpeed / (1 + curveWeight * static_cast<double>(fabs(roadInfo.angle)));
            }

            // Steer towards where the point
            double steerAngle = roadInfo.angle;

            // Accelerate or decelerate towards target speed - THROTTLE_WEIGHT tunes magnitude
            double throttle = throttleWeight * (targetSpeed - static_cast<double>(vehicleState.speed));

            // Step vehicle model with driver input, but wait until time > 0
            if (SE_GetSimulationTime() > 0 && !SE_GetPauseFlag())
            {
                SE_SimpleVehicleControlAnalog(vehicleHandle, dt, throttle, steerAngle);
            }

            // Fetch updated state and report to scenario engine
            SE_SimpleVehicleGetState(vehicleHandle, &vehicleState);

            // Report updated vehicle position and heading. z, pitch and roll will be aligned to the road
            SE_ReportObjectPosXYH(0, 0, vehicleState.x, vehicleState.y, vehicleState.h);

            // The following values are not necessary to report.
            // If not reported, esmini will calculate based on motion over time
            // but for accuracy it's recommendeded to report if available.

            // wheel status (revolution and steering angles)
            SE_ReportObjectWheelStatus(0, vehicleState.wheel_rotation, vehicleState.wheel_angle);

            // speed (along vehicle longitudinal (x) axis)
            SE_ReportObjectSpeed(0, vehicleState.speed);

            // Finally, update scenario using same time step as for vehicle model
            SE_StepDT(dt);
        }
        SE_Close();
    }
    return 0;
}

Don’t worry about the stationary red car on the road, you’ll just drive through it.

Try experimenting with the driver settings, e.g. increase lookahead distance factor from 0.75 to 1.75.

When running the application, press key 'j' to toggle ghost trail dots and lines visualization.

Challenge: Attach a front looking sensor to detect it and have the driver to brake to avoid collision…​

Ghost concept in brief

In the first loop the driver model is just capable of driving along the specified lane. It’s totally detached from any scenario events.

The ghost concept is a solution for a driver model to perform scenario actions. Basically it will launch a fore-runner to perform the scenario actions. The trajectory, including speed, is registered into a "trail" which can then be used as reference for steering and speed target.

Ghost mode is a parameter of the external controller. Normally this is set in the scenario, for example in test-driver.xosc, change line:

<ParameterDeclaration name="GhostMode" parameterType="boolean" value="false"/>

to:

<ParameterDeclaration name="GhostMode" parameterType="boolean" value="true"/>

However, in the example code this parameter is controlled by the paramDeclCB() callback. Once it’s registered with SE_RegisterParameterDeclarationCallback() it will be called each time esmini is initialized (SE_Init()).

14.3.11. OSI groundtruth

To access OSI data we first need to complement the build script (CMakeLists.txt) with OSI include and library info. For example:

Windows:

include_directories(. ../include ../EnvironmentSimulator/Libraries/esminiLib ../externals/osi/v10/include)

link_directories(. ../bin ../externals/osi/v10/lib)

target_link_libraries(${TARGET} esminiLib libprotobuf open_simulation_interface_pic)

Linux:

include_directories(. ../include ../EnvironmentSimulator/Libraries/esminiLib ../externals/osi/linux/include)

link_directories(. ../bin ../externals/osi/linux/lib)

target_link_libraries(${TARGET} esminiLib protobuf open_simulation_interface_pic)

Note:

  • Replace foldername "v10" with linux or mac depending on your platform.

  • If linking with custom applications or libraries: OSI and Google Protobuf versions needs to be consistent (at least major version nr), also with the versions used in esmini (see here).

Then run cmake .. from the build folder to apply the changes in CMakeFiles.cxx.

The following code will update, fetch and print some OSI data each frame.

#include "esminiLib.hpp"

#include "osi_common.pb.h"
#include "osi_object.pb.h"
#include "osi_groundtruth.pb.h"
#include "osi_version.pb.h"

int main(int argc, char* argv[])
{
    (void)argc;
    (void)argv;
    const osi3::GroundTruth* gt;

    SE_EnableOSIFile(0);  // 0 or "" will result in default filename, ground_truth.osi

    SE_Init("../resources/xosc/cut-in_simple.xosc", 0, 1, 0, 0);

    // You could now retrieve the initial state of all objects before stepping the scenario

    // Initial update of complete Ground Truth, including static things
    SE_UpdateOSIGroundTruth();

    // Fetch initial OSI struct
    gt = reinterpret_cast<const osi3::GroundTruth*>(SE_GetOSIGroundTruthRaw());

    // Lane boundaries (static road info only available in first OSI frame
    printf("lane boundaries: %d\n", gt->lane_boundary_size());
    for (int j = 0; j < gt->lane_boundary_size(); j++)
    {
        printf("  lane boundary %d, nr of boundary points: %d\n", j, gt->lane_boundary(j).boundary_line_size());

#if 0  // change to 1 in order to print all boundary points
                        for (int k = 0; k < gt->lane_boundary(j).boundary_line_size(); k++)
                        {
                                printf("    (%.2f, %.2f)\n",
                                        gt->lane_boundary(j).boundary_line(k).position().x(),
                                        gt->lane_boundary(j).boundary_line(k).position().y());
                        }
#endif
    }

    for (int i = 0; i < 4; i++)
    {
        SE_StepDT(0.01f);

        // Further updates will only affect dynamic OSI stuff
        SE_UpdateOSIGroundTruth();

        // Fetch OSI struct
        gt = reinterpret_cast<const osi3::GroundTruth*>(SE_GetOSIGroundTruthRaw());

        // Print timestamp
        printf("Frame %d timestamp: %.2f\n",
               i + 1,
               static_cast<double>(gt->timestamp().seconds()) + 1E-9 * static_cast<double>(gt->timestamp().nanos()));

        // Static content such as lane boundaries should be 0 at this point
        printf("lane boundaries: %d\n", gt->lane_boundary_size());

        // Road markings, e.g. zebra lines
        printf("road markings: %d\n", gt->road_marking_size());

        // Moving objects
        printf("moving objects: %d\n", gt->moving_object_size());

#if 1  // change to 1 in order to print some moving object state info
       // Print object id, position, orientation and velocity
        for (int j = 0; j < gt->moving_object().size(); j++)
        {
            printf("  obj id %u pos (%.2f, %.2f, %.2f) orientation (%.2f, %.2f, %.2f) vel (%.2f, %.2f, %.2f) acc (%.2f, %.2f, %.2f)\n",
                   static_cast<unsigned int>(gt->moving_object(j).id().value()),
                   gt->moving_object(j).base().position().x(),
                   gt->moving_object(j).base().position().y(),
                   gt->moving_object(j).base().position().z(),
                   gt->moving_object(j).base().orientation().yaw(),
                   gt->moving_object(j).base().orientation().pitch(),
                   gt->moving_object(j).base().orientation().roll(),
                   gt->moving_object(j).base().velocity().x(),
                   gt->moving_object(j).base().velocity().y(),
                   gt->moving_object(j).base().velocity().z(),
                   gt->moving_object(j).base().acceleration().x(),
                   gt->moving_object(j).base().acceleration().y(),
                   gt->moving_object(j).base().acceleration().z());
        }
#endif

        // Moving objects
        printf("stationary objects: %d\n", gt->stationary_object_size());
    }

    SE_Close();

    return 0;
}

14.4. Python binding

A Python wrapper for esmini can easily be created using "ctypes" (thanks David Kaplan for the tip!). This section presents a few examples to get you started.

  • Make sure the esminiLib is present in esmini/bin folder

  • Run the script examples from the esmini/Hello-World_coding-example folder

14.4.1. Minimalistic esmini Python player

import ctypes

se = ctypes.CDLL("../bin/esminiLib.dll")
se.SE_Init(b"../resources/xosc/cut-in.xosc", 1, 1, 0, 0, 2)

for i in range (500):
    se.SE_Step()

Note: Library name varies with the platform as mentioned above (see Preparations)

Below is a bit more flexible variant:

  • Portable: Works on all supported platforms

  • Provide scenario via argument

  • Quit at press Escape or end of scenario

import ctypes
import sys

if sys.platform == "linux" or sys.platform == "linux2":
    se = ctypes.CDLL("../bin/libesminiLib.so")
elif sys.platform == "darwin":
    se = ctypes.CDLL("../bin/libesminiLib.dylib")
elif sys.platform == "win32":
    se = ctypes.CDLL("../bin/esminiLib.dll")
else:
    print("Unsupported platform: {}".format(sys.platform))
    quit()

if (len(sys.argv) < 2):
    print('Usage: {} <xosc file>'.format(sys.argv[0]))
    exit(-1)

se.SE_Init(sys.argv[1].encode('ascii'), 0, 1, 0, 0)

while not se.SE_GetQuitFlag():
    se.SE_Step()

14.4.2. Initialize esmini by flexible argument list

Instead of using the limited SE_Init() function you can pass any arguments using SE_InitWithArgs()

# initialize esmini with arguments instead of a few limited arguments
# using ideas from: https://comp.lang.python.narkive.com/RUdYlz64/trying-to-pass-sys-argv-as-int-argc-char-argv-using-ctypes

import ctypes as ct
import sys

if sys.platform == "linux" or sys.platform == "linux2":
    se = ct.CDLL("../bin/libesminiLib.so")
elif sys.platform == "darwin":
    se = ct.CDLL("../bin/libesminiLib.dylib")
elif sys.platform == "win32":
    se = ct.CDLL("../bin/esminiLib.dll")
else:
    print("Unsupported platform: {}".format(platform))
    quit()

# specify arguments types of esmini function
se.SE_InitWithArgs.argtypes = [ct.c_int, ct.POINTER(ct.POINTER(ct.c_char))]

# create the list of arguments
args = [
    "--window", "60", "60", "800", "400",
    "--osc", "../resources/xosc/cut-in.xosc",
    ]

# prepare argument list for ctypes use
argv = (ct.POINTER(ct.c_char) * len(args))()
argc = len(argv)
for i, arg in enumerate(args):
    argv[i] = ct.create_string_buffer(arg.encode())

# init esmini
if se.SE_InitWithArgs(argc, argv) != 0:
    exit(-1)

# execute esmini until end of scenario or user requested quit by ESC key
while se.SE_GetQuitFlag() == 0:
    se.SE_Step()

14.4.3. Forward command line arguments

How to just forward any argument provided from command line:

# forward command line arguments to esmini
# using ideas from: https://comp.lang.python.narkive.com/RUdYlz64/trying-to-pass-sys-argv-as-int-argc-char-argv-using-ctypes

import ctypes as ct
import sys

if sys.platform == "linux" or sys.platform == "linux2":
    se = ct.CDLL("../bin/libesminiLib.so")
elif sys.platform == "darwin":
    se = ct.CDLL("../bin/libesminiLib.dylib")
elif sys.platform == "win32":
    se = ct.CDLL("../bin/esminiLib.dll")
else:
    print("Unsupported platform: {}".format(platform))
    quit()

# specify arguments types of esmini function
se.SE_InitWithArgs.argtypes = [ct.c_int, ct.POINTER(ct.POINTER(ct.c_char))]

# fetch command line arguments
argc = len(sys.argv)
argv = (ct.POINTER(ct.c_char) * (argc + 1))()
for i, arg in enumerate(sys.argv):
    argv[i] = ct.create_string_buffer(arg.encode('utf-8'))

# init esmini
if se.SE_InitWithArgs(argc, argv) != 0:
    exit(-1)

# execute esmini until end of scenario or user requested quit by ESC key
while se.SE_GetQuitFlag() == 0:
    se.SE_Step()

14.4.4. Get object states

How to retrieve the state of all scenario objects:

import ctypes
from sys import platform

if platform == "linux" or platform == "linux2":
    se = ctypes.CDLL("../bin/libesminiLib.so")
elif platform == "darwin":
    se = ctypes.CDLL("../bin/libesminiLib.dylib")
elif platform == "win32":
    se = ctypes.CDLL("../bin/esminiLib.dll")
else:
    print("Unsupported platform: {}".format(platform))
    quit()

# Definition of SE_ScenarioObjectState struct
class SEScenarioObjectState(ctypes.Structure):
    _fields_ = [
        ("id", ctypes.c_int),
        ("model_id", ctypes.c_int),
        ("control", ctypes.c_int),
        ("timestamp", ctypes.c_float),
        ("x", ctypes.c_float),
        ("y", ctypes.c_float),
        ("z", ctypes.c_float),
        ("h", ctypes.c_float),
        ("p", ctypes.c_float),
        ("r", ctypes.c_float),
        ("roadId", ctypes.c_int),
        ("junctionId", ctypes.c_int),
        ("t", ctypes.c_float),
        ("laneId", ctypes.c_int),
        ("laneOffset", ctypes.c_float),
        ("s", ctypes.c_float),
        ("speed", ctypes.c_float),
        ("centerOffsetX", ctypes.c_float),
        ("centerOffsetY", ctypes.c_float),
        ("centerOffsetZ", ctypes.c_float),
        ("width", ctypes.c_float),
        ("length", ctypes.c_float),
        ("height", ctypes.c_float),
        ("objectType", ctypes.c_int),
        ("objectCategory", ctypes.c_int),
        ("wheelAngle", ctypes.c_float),
        ("wheelRot", ctypes.c_float),
    ]

se.SE_Init(b"../resources/xosc/cut-in.xosc", 0, 1, 0, 0)

obj_state = SEScenarioObjectState()  # object that will be passed and filled in with object state info

for i in range(500):
    for j in range(se.SE_GetNumberOfObjects()):
        se.SE_GetObjectState(se.SE_GetId(j), ctypes.byref(obj_state))
        print('Frame {} Time {:.2f} ObjId {} roadId {} laneId {} laneOffset {:.2f} s {:.2f} x {:.2f} y {:.2f} heading {:.2f} speed {:.2f} wheelAngle {:.2f} wheelRot {:.2f}'.format(
            i, obj_state.timestamp, obj_state.id, obj_state.roadId, obj_state.laneId, obj_state.laneOffset,
            obj_state.s, obj_state.x, obj_state.y, obj_state.h, obj_state.speed * 3.6, obj_state.wheelAngle, obj_state.wheelRot))
    se.SE_Step()

14.4.5. Example use of esminiRMLib (RoadManager)

  • Make sure the esminiRMLib is present in esmini/bin folder

  • In the code, check filepath to the OpenDRIVE file to load (current search path should work when script is copied to and run from the esmini/Hello-World_coding-example folder)

import ctypes as ct
import sys

if sys.platform == "linux" or sys.platform == "linux2":
    rm = ct.CDLL("../bin/libesminiRMLib.so")
elif sys.platform == "darwin":
    rm = ct.CDLL("../bin/libesminiRMLib.dylib")
elif sys.platform == "win32":
    rm = ct.CDLL("../bin/esminiRMLib.dll")
else:
    print("Unsupported platform: {}".format(sys.platform))
    sys.exit(-1)

# Definition of RM_PositionData struct - should match esminiRMLib::RM_PositionData struct
class RM_PositionData(ct.Structure):
    _fields_ = [
        ("x", ct.c_float),
        ("y", ct.c_float),
        ("z", ct.c_float),
        ("h", ct.c_float),
        ("p", ct.c_float),
        ("r", ct.c_float),
        ("h_relative", ct.c_float),
        ("road_id", ct.c_int),
        ("junction_id", ct.c_int),  # -1 if not in a junction
        ("lane_id", ct.c_int),
        ("lane_offset", ct.c_float),
        ("s", ct.c_float),
    ]

# Specify argument types to a few functions
rm.RM_SetWorldPosition.argtypes = [ct.c_int, ct.c_float, ct.c_float, ct.c_float, ct.c_float, ct.c_float, ct.c_float]
rm.RM_SetLanePosition.argtypes = [ct.c_int, ct.c_int, ct.c_int, ct.c_float, ct.c_float]


# Initialize esmini RoadManger with given OpenDRIVE file
odr = '../resources/xodr/straight_500m.xodr'
if rm.RM_Init(odr.encode()) == -1:   # encode() converts string to pure byte array
    print("Failed to load OpenDRIVE file ", )
    sys.exit(-1)

rm_pos = rm.RM_CreatePosition()  # create a position object, returns a handle
rm_pos_data = RM_PositionData()  # object that will be passed and filled in with position info

# test a few positions
x = 65
y = -1.7
rm.RM_SetWorldPosition(rm_pos, x, y, 0.0, 0.0, 0.0, 0.0)
rm.RM_GetPositionData(rm_pos, ct.byref(rm_pos_data))
print('road_id {} lane_id {} lane_offset {:.2f} s {:.2f}'.format(
    rm_pos_data.road_id, rm_pos_data.lane_id, rm_pos_data.lane_offset, rm_pos_data.s))

road_id = 1
lane_id = 1
lane_offset = -0.2
s = 40
rm.RM_SetLanePosition(rm_pos, road_id, lane_id, lane_offset, s)
rm.RM_GetPositionData(rm_pos, ct.byref(rm_pos_data))
print('x {:.2f} y {:.2f} h (yaw) {:.2f}'.format(
    rm_pos_data.x, rm_pos_data.y, rm_pos_data.h))

14.4.6. Storyboard element state callback

The following code shows how to get notification of storyboard events using StoryBoardElementStateChangeCallback.

# Storyboard element types (see OSCTypeDefs/OSCAction.hpp)
sbe_type = {
    0:"UNDEFINED_ELEMENT_TYPE",
    1:"STORY_BOARD",
    2:"STORY",
    3:"ACT",
    4:"MANEUVER_GROUP",
    5:"MANEUVER",
    6:"EVENT",
    7:"ACTION"
}

# Storyboard element states (see OSCTypeDefs/OSCAction.hpp)
sbe_state = {
    0:"UNDEFINED_ELEMENT_STATE",
    1:"STANDBY",
    2:"RUNNING",
    3:"COMPLETE"
}

# Define callback for scenario storyboard element state changes
# according to function type: void (*fnPtr)(const char* name, int type, int state)
# required by SE_RegisterStoryBoardElementStateChangeCallback()
def callback(name, type, state, full_path):
    # just print the received info
    print(type)
    print("callback: {} ({}) state: {} full path: {}".format(codecs.decode(name), sbe_type[type], sbe_state[state], codecs.decode(full_path), ))

callback_type = ct.CFUNCTYPE(None, ct.c_char_p, ct.c_int, ct.c_int, ct.c_char_p)
callback_func = callback_type(callback)

# Initialize esmini before register the callback
se.SE_AddPath(b"../../../resources/xosc")  # search path if run from original location
se.SE_Init(b"../resources/xosc/cut-in.xosc", 0, 1, 0, 1)

# register the callback function
se.SE_RegisterStoryBoardElementStateChangeCallback(callback_func)

14.4.7. OSI groundtruth

OSI groundtruth can also be retrieved and processed via Python. The following code shows one way of doing it. This example also shows how the simulation can be encapsulated in a class.

'''
   Python dependencies:
      pip install protobuf==3.20.2

   Note: No need to install OSI as long as
   esmini/scripts/osi3 folder is available
'''

import os
import sys
import ctypes as ct

# Add scripts root directory to module search path in order to find osi3
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../', 'scripts')))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'scripts')))  # if script moved to Hello World folder

from osi3.osi_groundtruth_pb2 import GroundTruth

# Import esmini lib
if sys.platform == "linux" or sys.platform == "linux2":
    se = ct.CDLL("../bin/libesminiLib.so")
elif sys.platform == "darwin":
    se = ct.CDLL("../bin/libesminiLib.dylib")
elif sys.platform == "win32":
    se = ct.CDLL("../bin/esminiLib.dll")
else:
    print("Unsupported platform: {}".format(platform))
    quit()


# Specify argument and return types of some esmini functions
se.SE_GetOSIGroundTruth.restype = ct.c_void_p
se.SE_GetOSIGroundTruth.argtypes = [ct.c_void_p]
se.SE_StepDT.argtypes = [ct.c_float]

class Sim:
    def __init__(self, scenario):
        self.msg = GroundTruth()  # OSI groundtruth message
        self.msg_size = ct.c_int()
        if se.SE_Init(scenario, 0, 1, 0, 0) != 0:
            print('Failed to open ', scenario)
            exit(-1)

        self.GetAndPrintOSIInfo()  # Initial state

    # Retrieve OSI message and print some info
    def GetAndPrintOSIInfo(self):
        se.SE_UpdateOSIGroundTruth()

        msg_string = se.SE_GetOSIGroundTruth(ct.byref(self.msg_size))
        self.msg.ParseFromString(ct.string_at(msg_string, self.msg_size))

        # Print some info
        print("Timestamp: {:.2f}".format(self.msg.timestamp.seconds + 1e-9 * self.msg.timestamp.nanos))
        for j, o in enumerate(self.msg.moving_object):
            print('   Object[{}] id {}'.format(j, o.id.value))
            print('      pos.x {:.2f} pos.y {:.2f} rot.h {:.2f}'.format(\
                o.base.position.x,\
                o.base.position.y,\
                o.base.orientation.yaw))
            print('      vel.x {:.2f} vel.y {:.2f} rot_rate.h {:.2f}'.format(\
                o.base.velocity.x,\
                o.base.velocity.y,\
                o.base.orientation_rate.yaw))
            print('      acc.x {:.2f} acc.y {:.2f} rot_acc.h {:.2f}'.format(\
                o.base.acceleration.x,\
                o.base.acceleration.y,\
                o.base.orientation_acceleration.yaw))


    def Run(self):
        while se.SE_GetQuitFlag() != 1:
            se.SE_StepDT(0.05)
            self.GetAndPrintOSIInfo()  # Updated state

        se.SE_Close()

if __name__ == '__main__':
    # Execute when the module is not initialized from an import statement.

    sim = Sim(b"../resources/xosc/cut-in.xosc")
    sim.Run()

14.4.8. Object state callback

Advanced

Controllers and callbacks can also be utilized via Python. Following code drafts how to grab state of the first scenario object and then, in a callback, manipulate it and report back. If used in combination with ExternalController in mode=additive the scenario action are applied first. On the contrary, if used with mode=override, the scenario actions will not be applied giving exclusive control to the external callback function.

# Definition of SE_ScenarioObjectState struct
class SEScenarioObjectState(ct.Structure):
    _fields_ = [
        ("id", ct.c_int),
        ("model_id", ct.c_int),
        ("control", ct.c_int),
        ("timestamp", ct.c_float),
        ("x", ct.c_float),
        ("y", ct.c_float),
        ("z", ct.c_float),
        ("h", ct.c_float),
        ("p", ct.c_float),
        ("r", ct.c_float),
        ("roadId", ct.c_int),
        ("junctionId", ct.c_int),
        ("t", ct.c_float),
        ("laneId", ct.c_int),
        ("laneOffset", ct.c_float),
        ("s", ct.c_float),
        ("speed", ct.c_float),
        ("centerOffsetX", ct.c_float),
        ("centerOffsetY", ct.c_float),
        ("centerOffsetZ", ct.c_float),
        ("width", ct.c_float),
        ("length", ct.c_float),
        ("height", ct.c_float),
        ("objectType", ct.c_int),
        ("objectCategory", ct.c_int),
        ("wheel_angle", ct.c_float),
        ("wheel_rot", ct.c_float),
    ]


# Define callback for scenario object enabling manipulating the state AFTER scenario step but BEFORE OSI output
# Use in combination with ExternalController in mode=additive in order for scenario actions to be applied first
def callback(state_ptr, b):
    state = state_ptr.contents
    print("callback for obj {}: x={:.2f} y={:.2f} h={:.2f}".format(state.id, state.x, state.y, state.h))
    se.SE_ReportObjectPosXYH(ct.c_int(state.id), ct.c_float(state.timestamp), ct.c_float(state.x + 0.02), ct.c_float(state.y), ct.c_float(state.h + 0.001))

callback_type = ct.CFUNCTYPE(None, ct.POINTER(SEScenarioObjectState), ct.c_void_p)
callback_func = callback_type(callback)

# Initialize esmini before register the callback
se.SE_AddPath(b"../../../resources/xosc")  # search path if run from original location
se.SE_Init(b"../resources/xosc/cut-in.xosc", 0, 1, 0, 1)

# register callback for first object (id=0)
se.SE_RegisterObjectCallback(0, callback_func, 0)

14.5. C# binding

A C# wrapper for the esminiLib is provided (ESMiniWrapper.cs). Coverage is limited, but it might serve as a good starting point.

14.5.1. OSI groundtruth with C#

using System;
using System.Runtime.InteropServices;
using ESMini;

using Google.Protobuf;

using static Osi3.GroundTruth;

namespace esmini_csharp
{
    class Program
    {
        static void Main(string[] args)
        {
            // intitialize esmini
            if (ESMiniLib.SE_Init("../resources/xosc/cut-in.xosc", 0, 1, 0, 0) != 0)
            {
                Console.WriteLine("failed to load scenario");
                return;
            }
            ESMiniLib.SE_UpdateOSIGroundTruth();

            int size = 0;
            while (ESMiniLib.SE_GetQuitFlag() != 1)
            {
                // Step esmini
                ESMiniLib.SE_Step();
                ESMiniLib.SE_UpdateOSIGroundTruth();

                // Get OSI message
                IntPtr int_ptr = ESMiniLib.SE_GetOSIGroundTruth(ref size);
                Byte[] byte_array = new Byte[size];
                Marshal.Copy(int_ptr, byte_array, 0, size);
                Osi3.GroundTruth gt_msg = Osi3.GroundTruth.Parser.ParseFrom(byte_array);

                // Write some info from OSI message
                Console.WriteLine("Time: {0:N3}", gt_msg.Timestamp.Seconds + 1e-9 * gt_msg.Timestamp.Nanos);
                foreach (Osi3.MovingObject o in gt_msg.MovingObject)
                {
                    Console.WriteLine("  Object[{0}], Pos: {1:N2}, {2:N2}, {3:N2}",
                        o.Id.Value, o.Base.Position.X, o.Base.Position.Y, o.Base.Position.Z);
                }
            }
        }
    }
}

This example shows, one way, how to access OSI data in a C# environment.

It’s dependent on the ESMiniWrapper.cs, and targeting Windows environment. If the wrapper is modified to work on Linux (referring .so instead of DLL) it might work there as well, but it’s not been tested.

Build
mkdir build
cd build
cmake ..
cmake --build . --config Release
Run

To have the correct relative path to the scenario file and also access to esminiLib we move into esmini/bin folder for running the example.

cd ..\..\..\..\bin
..\EnvironmentSimulator\code-examples\osi-groundtruth-cs\build\Release\osi-groundtruth.exe
How to create the osi3 C# files

For this example the osi3 C# files were provided. If you need or want to compile them yourself:

15. Run ASAM OpenSCENARIO examples

With some limitations (see details here) esmini can play the example scenarios provided with the ASAM OpenSCENARIO v1.1 release bundle.

  • If you don’t have esmini already, download latest demo package for your platform from here.

  • Download the standard from ASAM here (register and download is free of charge).

  • Extract to any folder.

  • Run the examples from command line in esmini root folder, for example:
    ./bin/esmini --window 60 60 800 400 --osc ../../openscenario-v1.1.0/Examples/DoubleLaneChanger.xosc
    or with absolute path:
    ./bin/esmini --window 60 60 800 400 --osc c:/stuff/openscenario-v1.1.0/Examples/DoubleLaneChanger.xosc

16. About this document

The source of this document is written in the asciidoc format. The html is generated with asciidoctor. Code snippets, images and other resources are included in the process. Also the version numbers are automatically inserted to avoid "human errors".

Install tools:

Linux

sudo apt install asciidoctor
sudo apt install ruby-coderay

Windows
  • First, if not already done, install ruby:

    • Pick latest Ruby x64 package, without devkit, here: https://rubyinstaller.org/downloads/

    • Install it. In the end you can leave "Run 'rid install' to setup MSYS2 and development toolchain" un-checked

  • Then run:
    gem install asciidoctor
    gem install coderay

Generate html: asciidoctor user_guide.adoc

17. Version

ESMINI_GIT_TAG="v2.40.2"
ESMINI_GIT_REV="v2.40.2-0-f46dfe92-dirty"

Full commit history is found here