First steps tutorial
This tutorial walks you through the first steps in AeroSim. In this tutorial you will learn:
- How to set up a simulator configuration and a Python run file
- How to load an environment in the configuration
- How to add a vehicle in the configuration
- How to set up an FMU to send state information to the simulator
The following tutorial outlines how to set up AeroSim to load a Cesium map tile, add a vehicle and move the vehicle in a simple way through an FMU. You can find all the code files described below in the tutorials folder of the AeroSim repository.
Simulator configuration file
Firstly, in the tutorials folder of the aerosim repository open a new JSON file in the config directory named sim_config_first_steps_tutorial.json. In the JSON file there are several fields to define:
"description": A simple description of your simulation configuration"clock": Define the simulation time-step parameters"orchestrator": A configuration for the orchestrator that coordinates AeroSim's modules"world": The world configuration, containing information about the environment and actors"fmu_models": The configuration for the FMU(s) driving control inputs and dynamics in your simulation
Orchestrator configuration
First, define the time-step size of the simulation in milliseconds with the step_size_ms parameter in the clock field.
In the orchestrator field add the topics to be published by the orchestrator. In this case, we only need one, aerosim.actor1.vehicle_state. We will add two more, aerosim.fmu_tutorial.input and aerosim.fmu_tutorial.output as auxiliary topics, in case we need to pass other variables for experimentation or debugging purposes.
The sync_topics field defines the topics to which the orchestrator will synchronize. The orchestrator will wait to receive data back on all the topics defined in this field before initiating the next simulation time-step by sending another clock message.
{
"description": "First steps tutorial.",
"clock": {
"step_size_ms": 20,
"pace_1x_scale": true
},
"orchestrator": {
"sync_topics": [
{
"topic": "aerosim.actor1.vehicle_state",
"interval_ms": 20
}
]
},
...
}
World configuration
In the world field, we provide several details about how we want to configure the 3D simulation environment and the vehicles within it. The following fields are needed to configure the world object:
-
origin: set alatitude,longitudeandaltitudein theoriginfield to define the starting point of the simulation. Cesium will download a tile from its database corresponding to this location. -
weather: weather setting for the simulation. Can be set toCloudy,ClearSkyorFoggyAndCloudy. -
actors: we define the list of actors we want to add to the simulation. In this case, we only want to add 1 actor, a helicopter model. Each actor must have anid, atypeand a corresponding 3D model file.id: an ID for each actor, this may be used elsewhere in the simulation to reference ittype: the actor type, for example,aircraftorhelicoptertransform: this field applies an initial transform to the actor, useful to position static actors (it should be noted that further updates to the vehicle's state through will override this parameter)state: this sets the state of the actor and the data topic that influences it. In this case we set the state todynamicand the
-
sensor_setup: in the sensor field, we will add a camera so that we can observe the simulation.
{
...
"world": {
"update_interval_ms": 20,
"origin": {
"latitude": 33.936519,
"longitude": -118.412698,
"altitude": 0.0
},
"actors": [
{
"actor_name": "actor1",
"actor_asset": "vehicles/generic_helicopter/generic_helicopter",
"parent": "",
"description": "Generic helicopter model",
"transform": {
"position": [0.0,0.0,0.0],
"rotation": [0.0,0.0,66.5],
"scale": [1.0,1.0,1.0]
},
"state": {
"msg_type": "aerosim::types::VehicleState",
"topic": "aerosim.actor1.vehicle_state"
},
"effectors": []
}
],
"sensor_setup": [
{
"sensor_name": "rgb_camera_0",
"type": "sensors/cameras/rgb_camera",
"parent": "actor1",
"transform": {
"translation": [17.0,-13.0,-5.0],
"rotation": [0.0,-10.0,140.0]
},
"parameters": {
"resolution": [
1920,
1080
],
"tick_rate": 0.02,
"frame_rate": 30,
"fov": 90,
"near_clip": 0.1,
"far_clip": 1000.0,
"capture_enabled": false
}
}
]
},
...
}
FMU model configuration
The FMU models are the logic units that handle any business logic related to controling the simulator. For example, processing input from an external control device such as a joystick, processing input from a machine learning stack or executing a flight dynamics model. The FMU is driven by the FMU driver module. FMU inputs and outputs are handled through data topics either published or subscribed by the FMU driver through the data middleware.
The fmu_models field must contain a list of FMU configurations. Several FMUs can be used simultaneously and can be inter-connected, in addition to being connected to other components of AeroSim. There are several key fields to set up for each FMU configuration:
id: an ID for the FMU, this can be used to reference the FMU elsewhere in the simulationfmu_model_path: the path to the.fmumodel file. This is relative the the directory where the script in launchedcomponent_input_topics: the data topics that the FMU subscribes for input datacomponent_output_topics: the data topics that the FMU uses to publish output data-
msg_type: the type governing the structure of the messages broadcast through a the component intput/output topics -
fmu_aux_input_mapping: manually enforce correspondence between FMU input variables and message attributes for the given topic -
fmu_aux_output_mapping: manually enforce correspondence between FMU output variables and message attributes for the given topic -
fmu_init_vals: values to initialize FMU input variables
{
...
"fmu_models": [
{
"id": "tutorial_fmu",
"fmu_model_path": "fmu/first_steps_fmu.fmu",
"component_input_topics": [],
"component_output_topics": [
{
"msg_type": "vehicle_state",
"topic": "aerosim.actor1.vehicle_state"
}
],
"fmu_aux_input_mapping": {
"aerosim.fmu_tutorial.input": {
"custom_input_topic_var": "custom_fmu_input"
}
},
"fmu_aux_output_mapping": {
"aerosim.fmu_tutorial.output": {
"custom_output_topic_var": "custom_fmu_output"
}
},
"fmu_initial_vals": {
"init_pos_north": 0.0,
"init_pos_east": 0.0,
"init_pos_down": 0.0
}
}
]
...
}
Setting up the FMU
The FMU should be built following the FMI standard version 2.0 or 3.0. PythonFMU3 is installed with AeroSim, or you can install it from PyPi. We will use PythonFMU3 to define and build a simple example FMU. AeroSim provides utility functions in aerosim_core for building FMUs that handle some boilerplate work and help match FMU intput/output variables to key AeroSim data topics. Ensure to activate the AeroSim virtual environment installed during the AeroSim setup using .venv/bin/activate.
Open a new Python file named first_steps_fmu.py. First, we will import utility functions from AeroSim to handle the variables and parameters. Import the Fmi3Slave class from PythonFMU3 to inherit from. We import the math library and transform utilities from SciPy since we need to handle quaternions for the vehicle state.
from aerosim_core import (
register_fmu3_var,
register_fmu3_param,
)
from aerosim_data import types as aerosim_types
from aerosim_data import dict_to_namespace
from pythonfmu3 import Fmi3Slave
import math
from scipy.spatial.transform import Rotation
Create a class named tutorial_fmu and set up the __init__ dunder method. Note that the name of the class will be the name of the FMU file after building. The FMU file for this example will be named tutorial_fmu.fmu. Call the base class __init__ method using the super() keyword. Add strings for the author and description fields and register the independent time variable using the register_fmu3_var utility method. We also add an internal variable to store the vehicle's yaw. We also use the register_fmu3_param method to declare FMU parameters for modifying the helicopter's start position from the configuration.
...
class first_steps_fmu(Fmi3Slave):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# -----------------------------------------------------------------------
# Define FMU standard variables
self.author = "AeroSim"
self.description = "First steps tutorial FMU example"
self.roll = 0.0
self.pitch = 0.0
self.yaw = 0.0
# FMU 3.0 requires a time variable set with independent causality
self.time = 0.0
register_fmu3_var(self, "time", causality="independent")
...
Now we add the vehicle_state variable to store and update the vehicle state. This variable must match up with the definition of the vehicle_state message type used by AeroSim to update the state of the vehicle in the renderer. We use the utility function dict_to_namespace to convert a dictionary into a SimpleNamespace object. From aerosim_types we use the VehicleState type with a timestamp. Note that vehicle state has multiple levels of attributes for acceleration, pose, position etc. Lastly, to register the vehicle_state variable as an FMU output variable we call the register_fmu3_var utility method, with causality set to output.
...
# -----------------------------------------------------------------------
# Define Aerosim interface output variables
self.vehicle_state = dict_to_namespace(aerosim_types.VehicleState().to_dict())
register_fmu3_var(self, "vehicle_state", causality="output")
register_fmu3_var(self, "vehicle_state", causality="output")
...
We will also define some auxiliary variables to use if we need to pass any other data in or out of the FMU for experimentation or debugging purposes. We also declare some FMU parameters with register_fmu3_param to control the helicopter's start position in the simulation from the configration.
...
# -----------------------------------------------------------------------
# Define custom auxiliary variables
self.custom_fmu_input = 0.0
register_fmu3_var(self, "custom_fmu_input", causality="input")
self.custom_fmu_output = 0.0
register_fmu3_var(self, "custom_fmu_output", causality="output")
# -----------------------------------------------------------------------
# Define FMU parameters
self.init_pos_north = 0.0
register_fmu3_param(self, "init_pos_north")
self.init_pos_east = 0.0
register_fmu3_param(self, "init_pos_east")
self.init_pos_down = 0.0
register_fmu3_param(self, "init_pos_down")
...
Next we will initialize the position of the vehicle state with the initial positions given to the FMU:
...
# Inherited enter_initialization_mode() callback from Fmi3Slave
def enter_initialization_mode(self):
"""Initialize the start position"""
self.vehicle_state.state.pose.position.x = self.init_pos_north
self.vehicle_state.state.pose.position.y = self.init_pos_east
self.vehicle_state.state.pose.position.z = self.init_pos_down
...
Exit initialization mode:
...
# Inherited exit_initialization_mode callback from Fmi3Slave
def exit_initialization_mode(self):
pass
...
Finally, we implement the do_step(...) method, which is executed every time the FMU is triggered. We will increment the Z position of the vehicle by incrementing the vehicle_state.state.pose.position.z variable, which is published as an output of the FMU. Finally, we also rotate the vehicle on the Z axis by incrementing the yaw variable after the helicopter has climbed by 10 meters. Since the vehicle_state type uses quaternions, we use a utility function to convert the orientation into a quaternion.
...
# Inherited do_step() callback from Fmi3Slave
def do_step(self, current_time: float, step_size: float) -> bool:
"""Simulation step execution"""
self.time += step_size
#self.custom_fmu_output = self.custom_fmu_input * 2 # Simple transformation
print(self.vehicle_state.state.pose.position.z)
if self.vehicle_state.state.pose.position.z - self.init_pos_down > -10:
self.vehicle_state.state.pose.position.z -= 0.02 # Move upwards in the world
else:
two_pi = 2.0 * math.pi
yaw = (self.yaw/360 + two_pi) % two_pi # Convert to 0-2pi range
rotation = Rotation.from_euler("zyx", [0.0, 0.0, yaw])
q_w, q_x, q_y, q_z = rotation.as_quat(scalar_first=True)
self.vehicle_state.state.pose.orientation.w = q_w
self.vehicle_state.state.pose.orientation.x = q_x
self.vehicle_state.state.pose.orientation.y = q_y
self.vehicle_state.state.pose.orientation.z = q_z
self.yaw += 1
return True
...
Building the FMU
Once the FMU definition is completed in the first_steps_fmu.py file, build the FMU using PythonFMU3. Ensure that you have the AeroSim virtual environment activated:
pythonfmu3 build -f first_steps_fmu.py
This will create a file called first_steps_fmu.fmu in the directory where you run this command. Move the first_stesps_fmu.fmu file into AEROSIM_ROOT/tutorials/fmu and move the sim_config_first_steps_tutorisl.json file into the AEROSIM_ROOT/tutorials folder.
Python launch script
With the FMU and sim config file complete, all that remains is to create the Python launch script. Open a new file named run_first_steps_tutorial.py in the AEROSIM_ROOT/tutorial directory and add the following code:
from aerosim import AeroSim
# --------------------------------------------
# Run AeroSim simulation
json_config_file = "config/sim_config_first_steps_tutorial.json"
aerosim = AeroSim()
aerosim.run(json_config_file)
# --------------------------------------------
# Let the simulation run
try:
input("Simulation is running. Press any key to stop...")
except KeyboardInterrupt:
print("Simulation stopped.")
finally:
# --------------------------------------------
# Stop AeroSim simulation
aerosim.stop()
If you don't already have the AeroSim simulator running, launch the simulator with the Unreal Editor from a terminal open in the root directory:
./launch_aerosim.sh --unreal-editor
Next from a separate terminal from within the tutorials directory, execute the Python launch script for the tutorial. Ensure that you have activated the AeroSim virtual environment:
source .venv/bin/activate
cd tutorials/
# Windows .\.venv\Scripts\activate
# Windows cd .\tutorials\
python run_first_steps_tutorial.py
Now, you will see the helicopter moving in the Unreal Editor spectator window:
