How to run
Runtime Parameters
Parameter |
Description |
|---|---|
|
show help message |
|
(For debugging) print internal debugging output |
|
Write dot files of resulting py-tree ( |
|
(For debugging) Produce tree output of parsed scenario |
|
Parse and resolve scenario, but do not execute |
|
Directory for output (e.g. test results) |
|
Parameter definition used to override default scenario parameter definitions. See Override scenario parameters for details. |
|
Create a template yaml file for overriding scenario parameters. The file will be named like specified by |
|
(For debugging) Show current state of py tree |
|
Command or script to run after scenario execution. The command will be called as |
|
Step-based simulation interface to use. The value must be in |
|
When more than one scenario is executed (multiple |
Run locally with ROS2
First, build the packages:
colcon build --packages-up-to scenario_execution_gazebo
source install/setup.bash
To run an osc-file with ROS2:
ros2 run scenario_execution_ros scenario_execution_ros $(PATH_TO_SCENARIO_FILE)
To log the current state of the behavior tree during execution, add the -t flag as an argument and run it again:
ros2 run scenario_execution_ros scenario_execution_ros $(PATH_TO_SCENARIO_FILE) -t
Additional parameters are describe in section Runtime Parameters.
Run as standalone Python package without ROS2
After installing scenario_execution using pip (see Installation with pip as standalone Python package), you can execute a scenario with the following command
scenario_execution $(PATH_TO_SCENARIO_FILE)
To log the current state of the behavior tree during execution, add the -t flag as an argument and run it again:
scenario_execution $(PATH_TO_SCENARIO_FILE) -t
Additional parameters are describe in section Runtime Parameters.
Run with Development Container inside Visual Studio Code
Prerequisites
If not already installed, install the docker engine on your system according to the installation instructions or, if you need GPU support, follow the nvidia installation instructions.
Make sure you follow the post installation steps.
To make sure, that the docker daemon is properly set up, run
docker run hello-world
Make sure you have installed the necessary Visual Studio Code extensions, namely the docker extension as well as the Dev Container extension.
Open Scenario Execution in Development Container
First, build the packages:
colcon build
Now, open the root folder of the scenario execution repository in Visual Studio Code by running
code /path/to/scenario_execution
in a terminal.
Make sure, that your ROS_DOMAIN_ID is properly set in the terminal you start Visual Studio Code from.
Then, click the blue item in the lower left corner
Afterwards, select "Reopen in Container " in the Selection Window inside Visual Studio Code
Now Visual Studio Code should build the development container and open your current working directory inside the container after it successfully built the image. If you now open a terminal inside Visual Studio Code, you can run and test your development safely inside the development container by running any of the examples (see Tutorials for further details).
Once you are done, you can cancel the remote connection, by again clicking on the blue item in the lower left corner and select "Close Remote Connection"
Visualize Scenario with PyTrees ROS Viewer
Before getting started, ensure that the PyQt5 version 5.14 Python library is installed. You can check PyQt5 version using the following command:
pip freeze | grep -i pyqt
If any PyQt5 libraries are detected, it's recommended to uninstall them to avoid conflicts:
pip3 uninstall PyQt5 PyQt5-Qt5 PyQt5-sip PyQtWebEngine PyQtWebEngine-Qt5
Additionally, if the default PyQtWebEngine is present, remove it using:
sudo apt remove python3-pyqt5.qtwebengine
Next, install PyQt and PyQtWebEngine version 5.14:
pip install PyQt5==5.14
pip install PyQtWebEngine==5.14
Once PyQt is set up, clone the py_trees_ros_viewer repository:
git clone git@github.com:splintered-reality/py_trees_ros_viewer.git
After cloning, build the package using colcon build and source the workspace.
Now, to open the viewer, execute the following command:
py-trees-tree-viewer --no-sandbox
Finally, in a separate terminal, run the scenario file to visualize the behavior tree.
Example:
ros2 run scenario_execution_ros scenario_execution_ros =examples/example_scenario/hello_world.osc
Please note that this method has been tested on Ubuntu 22.04. If you are using any other distribution, please ensure that PyQtEngine works on your machine and render web pages correctly.
Scenario Coverage
The scenario_execution_coverage package provides the ability to run variations of a scenario from a single scenario definition. It offers a fast and efficient method to test scenario with different attribute values, streamlining the development and testing process.
Below are the steps to run a scenario using scenario_execution_coverage..
First, build the packages:
colcon build --packages-up-to scenario_execution_coverage
source install/setup.bash
Then, generate the scenario files for each variation of scenario using the scenario_variation executable, you can pass your own custom scenario as an input. For this exercise, we will use a scenario present in examples/example_scenario_variation/.
scenario_variation examples/example_scenario_variation/example_scenario_variation.osc
This will save scenario variation files with the .sce extension in the out folder within the current working directory.
To execute the generated scenario variations, run the scenario_batch_execution executable. This command will process all scenarios files present in the out folder and execute them sequentially.
scenario_batch_execution -i out -o scenario_output -- ros2 run scenario_execution_ros scenario_execution_ros {SCENARIO} --output-dir {OUTPUT_DIR}
above command requires three arguments.
-i: directory where the scenario files.sceare stored
-o: directory where the outputlogandxmlfiles will be saved (for each scenario file within a separate folder)
-- ros2 run scenario_execution_ros scenario_execution_ros {SCENARIO} --output-dir {OUTPUT_DIR}: launch command to launch scenarios
Note
scenario_batch_execution can be used for any scenario-files, not only those generated by scenario_variation.
The return code of scenario_batch_execution is 0 if all tested scenarios succeeded. The output can be found within the specified output-folder:
<output_folder>/
text.xml # overall test result (summary of all tested scenarios)
<scenario1>/ # directory for scenario
test.xml # test result of scenario
log.txt # log output of scenario execution
... # other files generated by scenario execution run (e.g. rosbag)
Note
scenario_batch_execution creates a junit xml compatible file that can easily be integrated into a CI pipeline. An example can be found here: .github/workflows/test_build.yml
Multiple scenarios per file
A single .osc file can contain any number of scenario declarations.
Scenarios are executed sequentially in the order they appear in the file.
scenario first_scenario:
object_goal_pos: position_2d = position_2d(x: 0.6m, y: 0.6m)
do serial:
wait_for_simulation_end()
scenario second_scenario:
object_goal_pos: position_2d = position_2d(x: 0.3m, y: 0.6m)
do serial:
wait_for_simulation_end()
When --simulation is used the simulation environment is kept alive across
scenarios: setup() and shutdown() are called once for the whole file,
while reset() (together with a clock reset) is called before each
individual scenario. This avoids the overhead of restarting the physics
engine between runs.
Each scenario result appears as a separate <testcase> in test.xml.
Override scenario parameters
To override scenario parameters, specify the required parameters within a yaml file and use the command-line parameter --scenario-parameter-file.
Let's look at the following example scenario my_scenario.osc with the parameter my_base_param and my_struct_param.
import osc.helpers
scenario my_scenario:
my_base_param: string = "default value"
my_struct_param: position_3d
do serial:
log(my_base_param)
log(my_struct_param)
To override the parameter, the following yaml file overrides.yaml can be used.
my_scenario:
my_base_param: "my_val"
my_struct_param:
x: 1.0
y: 2.0
z: 0.0
The following command executes the scenario with the defined override.
ros2 run scenario_execution_ros scenario_execution_ros --scenario-parameter-file overrides.yaml my_scenario.osc
If physical literals get overridden, the values are expected in SI base units: For example specify value in meter (e.g. 42.0) for length; specify value in seconds for time.
An initial override template file can be created using the command-line parameter --create-scenario-parameter-file-template. This will create a yaml file named by --scenario-parameter-file in the current working directory.
Multi-document YAML — cross-product runs
A --scenario-parameter-file can contain multiple YAML documents separated
by ---. Each document is treated as an independent set of overrides
applied to all scenarios in the .osc file, producing
N scenarios × M documents total runs.
# document 0
first_scenario:
object_goal_pos:
x: 0.3
y: 0.6
second_scenario:
object_goal_pos:
x: 0.6
y: 0.3
---
# document 1
first_scenario:
object_goal_pos:
x: 0.1
y: 0.6
second_scenario:
object_goal_pos:
x: 0.6
y: 0.1
With 2 scenarios and 2 override documents this yields 4 runs:
Scenario name |
Override document |
|---|---|
|
document 0 |
|
document 0 |
|
document 1 |
|
document 1 |
The -<index> suffix is appended automatically when more than one document
is present; with a single document names stay as defined in the .osc file.
All results are written as separate <testcase> entries in test.xml.
Per-scenario output directories
When more than one scenario is executed (multiple scenario blocks in the .osc file,
or multiple YAML documents in --scenario-parameter-file), each scenario automatically
gets its own subdirectory inside --output-dir. The subdirectory is named after the
scenario (including any -<idx> suffix for multi-document runs).
<output-dir>/
first_scenario/ # output for scenario "first_scenario"
second_scenario/ # output for scenario "second_scenario"
test.xml # combined results (default)
The subdirectory name can be overridden per scenario via the special _output_dir key
inside the --scenario-parameter-file. Relative values are resolved relative to
--output-dir; absolute values are used as-is (and no existing files are removed).
test_scenario:
_output_dir: my_custom_dir # → <output-dir>/my_custom_dir/
object_goal_pos:
x: 0.3
y: 0.6
test_scenario2:
_output_dir: /tmp/test2_out # absolute path, used directly
object_goal_pos:
x: 0.6
y: 0.3
Note
Relative _output_dir paths must not start with .. (i.e. they must not
escape the root --output-dir).
Per-scenario test.xml
By default a single combined <output-dir>/test.xml is written.
Pass --output-result-per-scenario to write one test.xml per scenario
subdirectory instead:
<output-dir>/
first_scenario/
test.xml # result of "first_scenario" only
second_scenario/
test.xml # result of "second_scenario" only
# no combined test.xml at root level
Step-based simulation
Scenario Execution supports running scenarios against step-based simulators (e.g. MuJoCo, PyBullet, custom hardware-in-the-loop).
In step-based mode the framework drives the loop: it calls simulation.step() once per behavior-tree tick, advances a SimulationClock, and then ticks the behavior tree. There is no time.sleep() — the scenario runs as fast as the simulator allows.
Implementing a SimulationInterface
Create a class that inherits from SimulationInterface and implement its abstract methods:
# my_pkg/my_sim.py
from scenario_execution import SimulationInterface
class MySimulation(SimulationInterface):
@property
def dt(self) -> float:
"""Duration of one simulation step in seconds."""
return 0.002 # 500 Hz
def setup(self, **kwargs) -> None:
"""Called once before any scenario runs. Load worlds, connect to
simulator processes, allocate resources here."""
import mujoco
self._model = mujoco.MjModel.from_xml_path("robot.xml")
self._data = mujoco.MjData(self._model)
def reset(self, object_start_x=0.0, object_start_y=0.0) -> None:
"""Called before each scenario. OSC parameters with matching names
are injected automatically as keyword arguments."""
import mujoco
mujoco.mj_resetData(self._model, self._data)
self._data.qpos[:2] = [object_start_x, object_start_y]
def step(self) -> None:
"""Advance the simulation by one timestep (dt seconds).
Must be non-blocking."""
import mujoco
mujoco.mj_step(self._model, self._data)
def shutdown(self) -> None:
"""Called once after all scenarios complete."""
self._model = None
self._data = None
Passing scenario parameters to the simulation
Declare the OSC parameters you need directly as arguments on your reset()
override. The framework matches argument names to OSC parameter names and
injects values automatically:
scenario my_scenario:
object_start_x: float = 0.0 # metres
object_start_y: float = 0.0
object_mass: float = 1.0 # kg (not consumed by reset)
action my_scenario.run():
do serial:
wait elapsed(5.0s)
def reset(self, object_start_x, object_start_y, gravity=9.81):
# object_start_x / object_start_y injected from OSC
# gravity uses its Python default because it is not in the scenario
...
Required arguments (no default) that are absent from the scenario file cause
a clear error before reset() is ever called. Optional arguments (with
defaults) are passed when the scenario declares them, otherwise the default
is used. Struct parameters are passed as nested dictionaries.
If a --scenario-parameter-file is supplied the overridden values are
applied before reset() is called.
The SimulationInterface lifecycle is aligned with the
ros-simulation/simulation_interfaces standard:
|
|
|---|---|
|
Load world + simulator launch |
|
|
|
|
|
|
Running a scenario with a simulation
Pass the --simulation flag with the fully-qualified class path:
scenario_execution --simulation my_pkg.my_sim:MySimulation my_scenario.osc
Accessing the simulation from behaviors
Behaviors receive the simulation object via kwargs['simulation'] in their
setup() method — the same pattern as ROS behaviors using kwargs['node']:
from scenario_execution.actions.base_action import BaseAction
import py_trees
class ReadSensor(BaseAction):
def setup(self, **kwargs):
self.sim = kwargs['simulation']
def update(self):
obs = self.sim.get_observation()
if obs['done']:
return py_trees.common.Status.SUCCESS
return py_trees.common.Status.RUNNING
Time-based waits with simulation clock
The wait elapsed() directive and timeout() modifier automatically use
the SimulationClock when a
simulation is active. No changes to the OSC scenario file are needed:
scenario test:
do serial:
wait elapsed(1s) # counts 1 / dt simulation steps, not wall-clock seconds
Without a simulation interface the clock falls back to system wall-clock time, so existing scenarios continue to work unchanged.