How to Load a World File into Gazebo – ROS 2

In this tutorial, I will show you how to load a .world file into Gazebo. 

You can learn all about world files at this link.

Real-World Applications

Simulating a world has a number of real-world robotic applications: 

  • Ground Delivery
  • Hospitals and Medical Centers
  • Hotels (Room Service)
  • Offices
  • Restaurants
  • Warehouses
  • And more…

Roboticists like to simulate robots in simulated worlds in order to test out different algorithms. You can imagine the cost of making mistakes with a physical robot can be high (e.g. crashing a mobile robot into a wall at high speed means lost money).

You can get all the code for this tutorial here.

Let’s get started!

Prerequisites

  • ROS 2 Foxy Fitzroy installed on Ubuntu Linux 20.04 or newer. I am using ROS 2 Galactic, which is the latest version of ROS 2 as of the date of this post.
    • If you are using another ROS 2 distribution, you will need to replace ‘galactic’ with the name of your distribution everywhere I mention ‘galactic’ in this tutorial. 
    • I highly recommend you get the newest version of ROS 2. If you are using a newer version of ROS 2, you can still follow all the steps in this tutorial. Everything will work just fine.
  • You have already created a ROS 2 workspace. The name of our workspace is “dev_ws”, which stands for “development workspace.” 
  • (Optional) You have a package named two_wheeled_robot inside your ~/dev_ws/src folder, which I set up in this post. If you have another package, that is fine.

Manually 

Quick Start

The easiest way to load a world is via the command line. 

Open a new terminal window.

Type the following command to open up a world named cafe.world. This file is one of the built-in worlds that comes with the Gazebo installation (I am using Gazebo 11).

gazebo worlds/cafe.world

You can see a list of all the built-in worlds in this GitHub repository.

The world files are located in your /usr/share/gazebo-11/worlds directory.

The models (i.e. models for the objects that are inside your world) are located inside this directory:

~/.gazebo/models

Where focalfossa is the name of your Linux environment.

When you want to close the world, go back to the terminal, and type:

CTRL + C

Your Own World

If you have your own world, you can follow the instructions on this post to test it in Gazebo.

Alternatively, you can launch Gazebo using the following command.

gazebo

Then go to the Insert tab, and start inserting items inside the empty world. 

2-click-insert-tab
3-insert-an-airplane

Once you’re done, you can save the world file by going to File ->Save World As.

Using a Launch File

Example – Cafe World

Let’s launch the cafe world we saw earlier using a ROS 2 launch file.

I want to copy the cafe.world file to the worlds folder of my ROS 2 package.

cd /usr/share/gazebo-11/worlds
cp cafe.world ~/dev_ws/src/two_wheeled_robot/worlds
3b-worlds-file-cafe

The syntax is:

cp <world file> <path to the worlds folder inside your ROS 2 package>

cd ~/dev_ws/src/two_wheeled_robot/worlds
gedit cafe.world

You will see the cafe.world file has various links to other models and mesh files in it. Let’s move those files over to our package so that everything is all inside our two_wheeled_robot package (or whatever ROS 2 package you’re using).

Start from the top of the cafe.world file.

The first model we need to move over is the ground_plane model.

cd ~/.gazebo/models
cp -r ground_plane ~/dev_ws/src/two_wheeled_robot/models

Now let’s copy over the other models.

cp -r cafe ~/dev_ws/src/two_wheeled_robot/models
cp -r cafe_table ~/dev_ws/src/two_wheeled_robot/models
4-models-files

Now let’s create a launch file.

colcon_cd two_wheeled_robot
cd launch
gedit load_world_into_gazebo.launch.py

Write this code inside the file.

# Author: Addison Sears-Collins
# Date: September 19, 2021
# Description: Load a world file into Gazebo.
# https://automaticaddison.com

import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.conditions import IfCondition, UnlessCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import Command, LaunchConfiguration, PythonExpression
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare

def generate_launch_description():

  # Set the path to the Gazebo ROS package
  pkg_gazebo_ros = FindPackageShare(package='gazebo_ros').find('gazebo_ros')   
  
  # Set the path to this package.
  pkg_share = FindPackageShare(package='two_wheeled_robot').find('two_wheeled_robot')

  # Set the path to the world file
  world_file_name = 'cafe.world'
  world_path = os.path.join(pkg_share, 'worlds', world_file_name)
  
  # Set the path to the SDF model files.
  gazebo_models_path = os.path.join(pkg_share, 'models')
  os.environ["GAZEBO_MODEL_PATH"] = gazebo_models_path

  ########### YOU DO NOT NEED TO CHANGE ANYTHING BELOW THIS LINE ##############  
  # Launch configuration variables specific to simulation
  headless = LaunchConfiguration('headless')
  use_sim_time = LaunchConfiguration('use_sim_time')
  use_simulator = LaunchConfiguration('use_simulator')
  world = LaunchConfiguration('world')

  declare_simulator_cmd = DeclareLaunchArgument(
    name='headless',
    default_value='False',
    description='Whether to execute gzclient')
    
  declare_use_sim_time_cmd = DeclareLaunchArgument(
    name='use_sim_time',
    default_value='true',
    description='Use simulation (Gazebo) clock if true')

  declare_use_simulator_cmd = DeclareLaunchArgument(
    name='use_simulator',
    default_value='True',
    description='Whether to start the simulator')

  declare_world_cmd = DeclareLaunchArgument(
    name='world',
    default_value=world_path,
    description='Full path to the world model file to load')
   
  # Specify the actions
  
  # Start Gazebo server
  start_gazebo_server_cmd = IncludeLaunchDescription(
    PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzserver.launch.py')),
    condition=IfCondition(use_simulator),
    launch_arguments={'world': world}.items())

  # Start Gazebo client    
  start_gazebo_client_cmd = IncludeLaunchDescription(
    PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzclient.launch.py')),
    condition=IfCondition(PythonExpression([use_simulator, ' and not ', headless])))

  # Create the launch description and populate
  ld = LaunchDescription()

  # Declare the launch options
  ld.add_action(declare_simulator_cmd)
  ld.add_action(declare_use_sim_time_cmd)
  ld.add_action(declare_use_simulator_cmd)
  ld.add_action(declare_world_cmd)

  # Add any actions
  ld.add_action(start_gazebo_server_cmd)
  ld.add_action(start_gazebo_client_cmd)

  return ld

Save the file, and close it.

Go to your root directory.

cd ~/dev_ws

Build the package.

colcon build

Open a new terminal window.

Run the launch file using the following command:

ros2 launch two_wheeled_robot load_world_into_gazebo.launch.py

Here is the output. You should see the actors walking around the cafe.

5-cafe-world

Summary

In summary, the procedure for creating a world entails the following steps:

  1. Create the .world file inside the worlds folder of your package.
  2. Add the SDF models to the models folder of your package.
  3. Edit the launch file.
  4. Build the package.
  5. Launch the world.

How to Load a URDF File into RViz – ROS 2

In this tutorial, I will show you how to load a URDF File into RViz. Universal Robot Description Format (URDF) is the standard ROS format for robot modeling.

Prerequisites

  • ROS 2 Foxy Fitzroy installed on Ubuntu Linux 20.04 
    • If you are using another ROS 2 distribution, you will need to replace ‘foxy’ with the name of your distribution everywhere I mention ‘foxy’ in this tutorial. 
    • I highly recommend you get the newest version of ROS 2. If you are using a newer version of ROS 2, you can still follow all the steps in this tutorial.
  • You have already created a ROS 2 workspace. The name of our workspace is “dev_ws”, which stands for “development workspace.”

You can find the entire code for this project here on my Google Drive.

What is URDF?

A URDF (Universal Robot Description Format) file is an XML file that describes what a robot should look like in real life. It contains the complete physical description of the robot.

You can learn more about URDF files here.

Install Important ROS 2 Packages

First, we need to install some important ROS 2 packages that we will use in this tutorial. Open a new terminal window, and type the following commands, one right after the other.

sudo apt install ros-foxy-joint-state-publisher-gui
sudo apt install ros-foxy-xacro

The format of the commands above is:

sudo apt install ros-<ros2-distro>-joint-state-publisher-gui
sudo apt install ros-<ros2-distro>-xacro

You will need to replace <ros2-distro> with the ROS 2 distribution you are using. In this case, I am using ROS 2 Foxy Fitzroy, which is ‘foxy’ for short.

Create a ROS 2 Package

Let’s create a ROS 2 package inside our workspace. My workspace is named dev_ws.

In a new terminal window, move to the src (source) folder of your workspace.

cd ~/dev_ws/src

Now create the package using the following command.

ros2 pkg create --build-type ament_cmake two_wheeled_robot

Create Extra Folders

Move inside the package.

cd ~/dev_ws/src/two_wheeled_robot/

Create some extra folders with the following names. We will not use all of these folders. I just like to have these folders in every package I create in case I need to use them.

mkdir config launch maps meshes models params rviz urdf worlds

Type the following command to verify the folders were created.

dir

Now build the package by typing the following command:

cd ~/dev_ws
colcon build

Let’s add a script to our bashrc file which enables us to use the following command to move to the package from any directory inside our terminal window.

colcon_cd two_wheeled_robot

Where ‘cd’ means “change directory”.

Note that in ROS 1, we typed roscd to change directories. In ROS 2, we use the colcon_cd command instead of roscd.

Open a terminal window, and type the following commands, one right after the other:

echo "source /usr/share/colcon_cd/function/colcon_cd.sh" >> ~/.bashrc
echo "export _colcon_cd_root=~/dev_ws" >> ~/.bashrc

Now open a new terminal window.

To go directly to the basic_mobile_robot ROS 2 package, type:

colcon_cd two_wheeled_robot

This command above will get you straight to the two_wheeled_robot package from within any directory in your Linux system. The format is:

colcon_cd [ROS 2 package name]

Create the URDF File

In this section, we will build our mobile robot step-by-step. Our robot will be defined in a Universal Robot Descriptor File (URDF), the XML file that represents a robot model.

Open a new terminal window, and type:

colcon_cd two_wheeled_robot
cd urdf

Make sure you have the Gedit text editor installed.

sudo apt-get install gedit

Create a new file named two_wheeled_robot.urdf.

gedit two_wheeled_robot.urdf

Inside this file, we define what our robot will look like (i.e. visual properties), how the robot will behave when it bumps into stuff (i.e. collision properties), and its mass (i.e. inertial properties). 

Type this code inside the URDF file.

Save and close the file.

Now go to the meshes folder.

cd ~/dev_ws/src/two_wheeled_robot/meshes

Add this STL file to your meshes folder. A mesh is a file that allows your robot to look more realistic (rather than just using basic shapes like boxes and spheres).

Rather than use the STL files I provided, you can use a program like SolidWorks to generate your own STL files. You can also use a program called Blender to create DAE files. URDF files support meshes in either STL or DAE format.

Add Dependencies

Let’s add some packages that our project will depend on. Go to the package.xml file.

cd ~/dev_ws/src/two_wheeled_robot/

OR

colcon_cd two_wheeled_robot 
gedit package.xml

After the <buildtool_depend> tag, add the following lines:

<exec_depend>joint_state_publisher</exec_depend>
<exec_depend>robot_state_publisher</exec_depend>
<exec_depend>rviz</exec_depend>
<exec_depend>xacro</exec_depend>

Save the file, and close it.

1-package_xml

We will be using the Joint State Publisher and the Robot State Publisher. We will also be using RViz to visualize our robot model.

Create the Launch File

Let’s create a launch file. Open a new terminal window, and type:

colcon_cd two_wheeled_robot
cd launch
gedit two_wheeled_robot.launch.py

Copy and paste this code into the file.

# Author: Addison Sears-Collins
# Date: September 14, 2021
# Description: Launch a two-wheeled robot URDF file using Rviz.
# https://automaticaddison.com

import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import Command, LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare

def generate_launch_description():

  # Set the path to this package.
  pkg_share = FindPackageShare(package='two_wheeled_robot').find('two_wheeled_robot')

  # Set the path to the RViz configuration settings
  default_rviz_config_path = os.path.join(pkg_share, 'rviz/rviz_basic_settings.rviz')

  # Set the path to the URDF file
  default_urdf_model_path = os.path.join(pkg_share, 'urdf/two_wheeled_robot.urdf')

  ########### YOU DO NOT NEED TO CHANGE ANYTHING BELOW THIS LINE ##############  
  # Launch configuration variables specific to simulation
  gui = LaunchConfiguration('gui')
  urdf_model = LaunchConfiguration('urdf_model')
  rviz_config_file = LaunchConfiguration('rviz_config_file')
  use_robot_state_pub = LaunchConfiguration('use_robot_state_pub')
  use_rviz = LaunchConfiguration('use_rviz')
  use_sim_time = LaunchConfiguration('use_sim_time')

  # Declare the launch arguments  
  declare_urdf_model_path_cmd = DeclareLaunchArgument(
    name='urdf_model', 
    default_value=default_urdf_model_path, 
    description='Absolute path to robot urdf file')
    
  declare_rviz_config_file_cmd = DeclareLaunchArgument(
    name='rviz_config_file',
    default_value=default_rviz_config_path,
    description='Full path to the RVIZ config file to use')
    
  declare_use_joint_state_publisher_cmd = DeclareLaunchArgument(
    name='gui',
    default_value='True',
    description='Flag to enable joint_state_publisher_gui')
  
  declare_use_robot_state_pub_cmd = DeclareLaunchArgument(
    name='use_robot_state_pub',
    default_value='True',
    description='Whether to start the robot state publisher')

  declare_use_rviz_cmd = DeclareLaunchArgument(
    name='use_rviz',
    default_value='True',
    description='Whether to start RVIZ')
    
  declare_use_sim_time_cmd = DeclareLaunchArgument(
    name='use_sim_time',
    default_value='True',
    description='Use simulation (Gazebo) clock if true')
   
  # Specify the actions

  # Publish the joint state values for the non-fixed joints in the URDF file.
  start_joint_state_publisher_cmd = Node(
    condition=UnlessCondition(gui),
    package='joint_state_publisher',
    executable='joint_state_publisher',
    name='joint_state_publisher')

  # A GUI to manipulate the joint state values
  start_joint_state_publisher_gui_node = Node(
    condition=IfCondition(gui),
    package='joint_state_publisher_gui',
    executable='joint_state_publisher_gui',
    name='joint_state_publisher_gui')

  # Subscribe to the joint states of the robot, and publish the 3D pose of each link.
  start_robot_state_publisher_cmd = Node(
    condition=IfCondition(use_robot_state_pub),
    package='robot_state_publisher',
    executable='robot_state_publisher',
    parameters=[{'use_sim_time': use_sim_time, 
    'robot_description': Command(['xacro ', urdf_model])}],
    arguments=[default_urdf_model_path])

  # Launch RViz
  start_rviz_cmd = Node(
    condition=IfCondition(use_rviz),
    package='rviz2',
    executable='rviz2',
    name='rviz2',
    output='screen',
    arguments=['-d', rviz_config_file])
  
  # Create the launch description and populate
  ld = LaunchDescription()

  # Declare the launch options
  ld.add_action(declare_urdf_model_path_cmd)
  ld.add_action(declare_rviz_config_file_cmd)
  ld.add_action(declare_use_joint_state_publisher_cmd)
  ld.add_action(declare_use_robot_state_pub_cmd)  
  ld.add_action(declare_use_rviz_cmd) 
  ld.add_action(declare_use_sim_time_cmd)

  # Add any actions
  ld.add_action(start_joint_state_publisher_cmd)
  ld.add_action(start_joint_state_publisher_gui_node)
  ld.add_action(start_robot_state_publisher_cmd)
  ld.add_action(start_rviz_cmd)

  return ld

Save the file, and close it.

You can read more about Launch files here.

Add the RViz Configuration File

Let’s add a configuration file that will initialize RViz with the proper settings so we can view the robot as soon as RViz launches.

Open a new terminal window. Type:

colcon_cd two_wheeled_robot
cd rviz
gedit rviz_basic_settings.rviz

Add this code.

Save the file, and close it.

Build the Package

Now we need to build the package. Open a new terminal window, and type:

colcon_cd two_wheeled_robot
gedit CMakeLists.txt

Add the following snippet to CMakeLists.txt file above the if(BUILD_TESTING) line.

install(
  DIRECTORY config launch maps meshes models params rviz src urdf worlds
  DESTINATION share/${PROJECT_NAME}
)

Save the file and close it.

2-cmakelists

Build the project.

cd ~/dev_ws/
colcon build

In the future, if you would like to build the two_wheeled_robot package only, you can type:

colcon build --packages-select two_wheeled_robot

Launch the Robot in RViz

Open a new terminal, and launch the robot.

cd ~/dev_ws/
ros2 launch two_wheeled_robot two_wheeled_robot.launch.py
3-mobile-robot-1

——

By the way, if you want to see the available arguments you can pass to the launch file from the terminal window, type:

ros2 launch -s two_wheeled_robot two_wheeled_robot.launch.py
4-show-options

And if you want to set the value of an argument (e.g. launch the robot without RViz), you can do something like this:

ros2 launch two_wheeled_robot two_wheeled_robot.launch.py use_rviz:='False'

——

Here is the output after we launch the robot in RViz:

The joint state publisher GUI has sliders that enable you to move the wheels.

If something goes wrong with the launch, close everything down, restart Linux, and launch again.

Check whether you have properly set up the collision properties by enabling Collision Enabled under RobotModel on the left pane.

You can see in the image that I have modeled the base_link as a box for collisions. I could have just as easily used the STL model itself (which was used for visualization) as the collision geometry, but I decided to go with the simpler box representation to give me more flexibility to alter the collision areas if necessary. Also, Gazebo (a popular robotics simulator) recommends using simpler collision geometry to increase performance.

5-collision-enabled

View the Coordinate Frames

Let’s see the coordinate frames. We first need to install the necessary software.

sudo apt install ros-foxy-tf2-tools

Check out the coordinate frames.

colcon_cd two_wheeled_robot
ros2 run tf2_tools view_frames.py

As of ROS Galactic, this command is:

ros2 run tf2_tools view_frames

In the current working directory, you will have a file called frames.pdf. Open that file.

evince frames.pdf

Here is what my coordinate transform (i.e. tf) tree looks like:

6-frames-pdf

If we want to see the coordinate transformation from one link to another, we can type the following command. For example, what is the position and orientation of the front caster wheel relative to the base_link of the robot?

The syntax is:

ros2 run tf2_ros tf2_echo <parent frame> <child frame>

We open a terminal window, and type:

ros2 run tf2_ros tf2_echo base_link front_caster

Here is the output. With respect to the base_link reference frame, the front caster wheel is located at (x=0.217 meters, y=0 meters, and z=-0.1 meters). 

7-tf2-echo

The rotation value is in quaternion format. You can see that the rotation values are all 0s, indicating that the orientation of both coordinate frames is equivalent (i.e. the x, y, and z axes of both coordinate frames point in the same direction).

Remember that, by convention:

  • x-axis is red
  • y-axis is green
  • z-axis is blue 

To see an image of the architecture of our ROS system, open a new terminal window, and type the following command:

rqt_graph

Select the “Nodes/Topics (all) option in the upper-left part of the screen, and then click the refresh arrow right next to it.

8-rqt-graph

You can see how the joint state publisher GUI manipulates the joint states (i.e. the angle of each wheel joint). This data feeds into the robot state publisher. The robot state publisher publishes the coordinate frame relationships to tf, and the updated robot model feeds into RViz, the robot model visualization program.

That’s it! 

When you’re done, press CTRL+C in all terminal windows to shut everything down.

Linear Quadratic Regulator (LQR) With Python Code Example

In this tutorial, we will learn about the Linear Quadratic Regulator (LQR). At the end, I’ll show you my example implementation of LQR in Python.

To get started, let’s take a look at what LQR is all about. Since LQR is an optimal feedback control technique, let’s start with the definition of optimal feedback control and then build our way up to LQR.

Real-World Applications

drone-in-rain

Here are a couple of real-world examples where you might find LQR control:

  • Enabling a self-driving car to stay in the center of a lane or maintain a certain speed
  • Enabling a drone to hover at a specific altitude
  • Enabling a wheeled robot to follow a predetermined path

Prerequisites

  • To understand this tutorial, it is helpful if you have completed my state space model tutorial.
  • Otherwise, if you understand the concept of state space representation, jump right into this tutorial.

What is Optimal Feedback Control?

Consider these two real world examples:

  • Example 1: You want a robot car to go from point A to point B along a predetermined path. 
  • Example 2: You have a drone, and you want it to hover in the air at a specific altitude. You want it to take aerial photos of you. 

How do you accomplish both of these tasks?

Both the robot and the drone need to have a feedback control algorithm running inside them. 

The control algorithm’s job will be to output control signals (e.g. velocity of the wheels on the robot car, propeller speed, etc.) that reduce the error between the actual state (e.g. current position of the robot car or altitude for the drone) and the desired state. 

The sensors on the robot car/drone need to read the state of the system and feed that information back to the controller. 

The controller compares the actual state (i.e. the sensor information) with the desired state and then generates the best possible (i.e. optimal) control signals (often known as “system input”) that will reduce or eliminate that error.

In Example 1, the desired state is the (x,y) coordinate or GPS position along a predetermined path. At each timestep, you want the control algorithm to generate velocity commands for your robot that minimize the error between your current actual state (i.e. position) and the desired position (i.e. on top of the predetermined path).

In Example 2, with the drone, you want to minimize the error between your desired altitude of the drone and the actual altitude of the drone.

Here is how all that looks in a picture:

2-feedback-controlJPG

Your robot car and drone go through a continuous process of sensing the state of the system and then generating the best possible control commands to minimize the difference between the desired state xdesired and actual state xt

The goal at each timestep (as the robot car drives along the path or the drone hovers in the air) is to use sensor feedback to drive the state error towards 0, where:

State error = xt – xdesired

How Can We Drive State Error to 0 Efficiently?

Let’s revisit our examples from the previous section. 

Suppose in Example 2, a strong wind from a thunderstorm began to blow the drone downward, causing it to fall way below the desired altitude. 

3-drone_flying_technology_motion

To correct its altitude, the drone could:

  • Action 1: Spin its propellers at top speed so that it shoots right up to the desired altitude as quickly as possible.
  • Action 2: Gradually increase the speed of its propellers until it reaches the desired altitude.

Both Action 1 and 2 have pros and cons. 

Action 1

  • Pros: Your drone returns to your desired altitude quickly. 
  • Cons: Because the motors were spinning at top speed, you drained your drone’s battery significantly. Your drone now has only a few minutes of battery left before it dies, and the drone will soon come crashing down to Earth.

Action 2

  • Pros: Less battery usage than Action 1
  • Cons: It took seemingly forever for the drone to return to the desired altitude. 

How can we write a smart controller that balances both objectives? We want the system to both:

  1. Have good control (i.e. stay on the desired path or altitude as closely as possible).
  2. Minimize actuator (i.e. motor) effort as much as possible so the control inputs don’t cause the battery to drain so quickly. 

This, my friends, is where the Linear Quadratic Regulator (LQR) comes to the rescue. 

LQR Problem

LQR solves a common problem when we’re trying to optimally control a robotic system. Going back to our robot car example, let’s draw its diagram. Here is our real world robot:

obstacle_avoiding_robot

Let’s model this robot in a 3D diagram with coordinate axes x, y, and z:

4-model-in-3d

Here is the bird’s-eye view in 2D form:

5-birds-eyeJPG

Suppose the robot car’s state at time t (position, orientation) is represented by vector xt (e.g. where xt = [xglobal, yglobal, orientation (yaw angle)].

6-state-vectorJPG

A state space model for this robotic system might look like the following:

7-state-space-modelJPG

where 

  • xt is the current state vector at time t in the global coordinate frame… [xt, ytt] = [x coordinate, y coordinate, yaw angle (i.e. rotation angle around the z-axis)]
  • xt-1 is the state vector at time t-1 in the global coordinate frame… [xt-1, yt-1t-1]
  • ut-1 represents the control input vector at time t-1… [vt-1, ωt-1] = [linear velocity, angular velocity]
  • A and B are the state transition matrices.

(If that equation above doesn’t make a lot of sense, please check out the tutorial in the Prerequisites of this article where I dive into it in detail)

We want to choose control inputs ut-1….such that

  1. xt actual – xt desired is small…i.e. we get good control
  2. ut-1 is small…i.e. we use a minimal amount of actuator effort (i.e. control input, motor, etc.).

Both of these are competing objectives. A large u can drive the state error down really fast. A small u means the state error will remain really large. LQR was designed to solve this problem.

How LQR Works

The purpose of LQR is to compute the optimal control inputs ut-1 (i.e. velocity of the wheels of a robotic car, motors for propellers of a drone) given:

  1. Actual state xt
  2. Desired state xt
  3. State cost matrix Q (I’ll explain this term later in this tutorial)
  4. Input cost matrix R (I’ll explain this term later in this tutorial) 
  5. A matrix (See my state space modeling tutorial for how to calculate A)
  6. B matrix (See my state space modeling tutorial for how to calculate B)
  7. Time interval dt

that minimize the cumulative “cost.”

LQR is all about the cost function. It does some fancy math to come up with a function (this Wikipedia page shows the function) that expresses a “cost” (you could also substitute the word “penalty”) that is a function of both state error and actuator effort.

LQR’s goal is to minimize the “cost” of the state error, while minimizing the “cost” of actuator effort. 

  • The higher the state error term, the higher the overall cost. 
  • The higher the actuator effort term, the higher the overall cost. 

LQR determines the point along the cost curve where the “cost function” is minimized…thereby balancing both objectives of keeping state error and actuator effort minimized.

The cost curve below is for illustration purposes only. I’m using it just to explain this concept. In reality, the cost curve does not look like this. I’m using this graphic to convey to you the idea that LQR is all about finding the optimal control commands (i.e. linear velocity, angular velocity in the case of our robot car example) at which the actuator-effort state-error cost function is at a minimum. And from calculus, we know that the minimum of this cost function occurs at the point where the derivative (i.e. slope) is equal to 0.

We use some fancy math (I’ll get into this later) to find out what this point is.

cost-curve

What is Q?

Q is called the state cost matrix. Q helps us weigh the relative importance of each state in the state vector (i.e. [x error,y error,yaw angle error]). 

Q is a square matrix that has the same number of rows and columns as there are states.

Q penalizes bad performance (e.g. penalizes large differences between where you want the robot to be vs. where it actually is in the world)

Q has positive values along the diagonal and zeros elsewhere.

Q enables us to target states where we want low error by making the corresponding value of Q large.

I typically start out with the identity matrix and then tweak the values through trial and error. In a 3-state system, such as with our two-wheeled robot car, Q would be the following 3×3 matrix:

rsz_1q-value

 

What is R?

Similarly, R is the input cost matrix. This matrix penalizes actuator effort (i.e. rotation of the motors on the wheels). 

The R matrix has the same number of rows as are control inputs and the same number of columns as are control inputs. 

For example, imagine if we had a two-wheeled car with two control inputs, one for the linear velocity v and one for the angular velocity ω. 

8-imagine-ifJPG

The control input vector ut-1 would be [linear velocity v in meters per second, angular velocity ω in radians per second].

The input cost matrix often has positive values along the diagonal. We can use this matrix to target actuator states where we want low actuator effort by making the corresponding value of R large.   

A lot of people start with the identity matrix, but I’ll start out with some low numbers. You want to tweak the values through trial and error.

In a case where we have two control inputs into a robotic system, here would be my starting 2×2 R matrix:

rmatrix

LQR Algorithm Pseudocode

LQR uses a technique called dynamic programming. You don’t need to worry what that means. 

At each timestep t as the robot or drone moves in the world, the optimal control input is calculated using a series of for loops (where we run each for loop ~N times) that spit out the u (i.e. control inputs) that corresponds to the minimal overall cost.

Here is the pseudocode for LQR. Hang tight. Go through this slowly. There is a lot of mathematical notation thrown around, and I want you to leave this tutorial with a solid understanding of LQR.

Let’s walk through this together. The pseudocode is in bold italics, and my comments are in plain text. You see below that the LQR function accepts 7 parameters.

LQR(Actual State x, Desired State xf, Q, R, A, B, dt):

x_error = Actual State x – Desired State xf

Initialize N = ## 

I typically set N to some arbitrary integer like 50. Solutions to LQR problems are obtained recursively, starting at the last time step, and working backwards in time to the first time step. 

In other words, the for loops (we’ll get to these in a second) need to go through lots of iterations (50 in this case) to reach a stable value for P (we’ll cover P in the next step).

Set P = [Empty list of N + 1 elements]

Let P[N] = Q

For i = N, …, 1

P[i-1] = Q + ATP[i]A – (ATP[i]B)(R + BTP[i]B)-1(BTP[i]A)   

This ugly equation above is called the Discrete-time Algebraic Riccati equation. Don’t be intimidated by it. You’ll notice that you have the values for all the terms on the right side of the equation. All that is required is plugging in values and computing the answer at each iteration step i so that you can fill in the list P.

K = [Empty list of N elements]

u = [Empty list of N elements]

For i = 0, …, N – 1

      K[i] = -(R + BTP[i+1]B)-1BTP[i+1]A

K will hold the optimal feedback gain values. This is the important matrix that, when multiplied by the state error, will generate the optimal control inputs (see next step).

For i = 0, …, N – 1

      u[i] = K[i] @ x_error

We do N iterations of the for loop until we get a stable value for the optimal control input u (e.g. u = [linear velocity v, angular velocity ω]). I assume that a stable value is achieved when N=50.

u_star = u[N-1]  

Optimal control input is u_star. It is the last value we calculated in the previous step above. It is at the very end of the u list.

Return u_star  

Return the optimal control inputs. These inputs will be sent to our robot so that it can move to a new state (i.e. new x position, y position, and yaw angle γ).

Just for your future reference, you’ll commonly see the LQR algorithm written like this in the literature (and in college lectures):

lqr_algorithmJPG

LQR Python Code

Assumptions

Let’s get back to our two-wheeled mobile robot. It has three states (like in the state space modeling tutorial.) The forward kinematics can be modeled as:

9-assumptionsJPG

The state space model is as follows:

10-state-space-modelJPG

This model above shows the state of the robot at time t [x position, y position, yaw angle] given the state at time t-1, and the control inputs that were applied at time t-1.

The control inputs in this example are the linear velocity (v) — which has components in the x and y direction —  and the angular velocity around the z axis, ω (also known as the “yaw rate”).

Our robot starts out at the origin (x=0 meters, y=0 meters), and the yaw angle is 0 radians. Thus the state at t-1 is:

11-assumption1JPG
11-assumption2JPG

We let dt = 1 second. dt represents the interval between timesteps.

Now suppose we want our robot to drive to coordinate (x=2,y=2). This is our desired state (i.e. our goal destination).

12-goal-pointJPG

Also, we want the robot to face due north (i.e. 90 degrees (1.5708 radians) yaw angle) once it reaches the desired state. Therefore, our desired state is:

13-desired-stateJPG
14-desired-stateJPG

We launch our robot at time t-1, and we feed it our desired state. The robot must use the LQR algorithm to solve for the optimal control inputs that minimize both the state error and actuator effort. In other words, we want the robot to get from (x=0, y=0) to (x=2, y=2) and face due north once we get there. However, we don’t want to get there so fast so that we drain the car’s battery.

These optimal control inputs then get fed into the control input vector to predict the state at time t.

In other words, we use LQR to solve for this:

15-actuator-effortJPG

And then plug that vector into this:

16-plug-into-thisJPG

to get the state at time t.

One more thing to make this simulation more realistic. Let’s assume the maximum linear velocity of the robot car is 3.0 meters per second, and the maximum angular velocity is 90 degrees per second (1.5708 radians per second).

Let’s take a look at all this in Python code.

Code

Here is my Python implementation of the LQR. Don’t be intimidated by all the lines of code. Just go through it one step at a time. Take your time. You can copy and paste the entire thing into your favorite IDE.

Remember to play with the values along the diagonals of the Q and R matrices. You’ll see in my code that the Q matrix has been tweaked a bit.

import numpy as np

# Author: Addison Sears-Collins
# https://automaticaddison.com
# Description: Linear Quadratic Regulator example 
#	(two-wheeled differential drive robot car)

######################## DEFINE CONSTANTS #####################################
# Supress scientific notation when printing NumPy arrays
np.set_printoptions(precision=3,suppress=True)

# Optional Variables
max_linear_velocity = 3.0 # meters per second
max_angular_velocity = 1.5708 # radians per second

def getB(yaw, deltat):
	"""
	Calculates and returns the B matrix
	3x2 matix ---> number of states x number of control inputs

	Expresses how the state of the system [x,y,yaw] changes
	from t-1 to t due to the control commands (i.e. control inputs).
	
	:param yaw: The yaw angle (rotation angle around the z axis) in radians 
	:param deltat: The change in time from timestep t-1 to t in seconds
	
	:return: B matrix ---> 3x2 NumPy array
	"""
	B = np.array([	[np.cos(yaw)*deltat, 0],
									[np.sin(yaw)*deltat, 0],
									[0, deltat]])
	return B


def state_space_model(A, state_t_minus_1, B, control_input_t_minus_1):
	"""
	Calculates the state at time t given the state at time t-1 and
	the control inputs applied at time t-1
	
	:param: A	The A state transition matrix
		3x3 NumPy Array
	:param: state_t_minus_1 	The state at time t-1  
		3x1 NumPy Array given the state is [x,y,yaw angle] ---> 
		[meters, meters, radians]
	:param: B 	The B state transition matrix
		3x2 NumPy Array
	:param: control_input_t_minus_1 	Optimal control inputs at time t-1  
		2x1 NumPy Array given the control input vector is 
		[linear velocity of the car, angular velocity of the car]
		[meters per second, radians per second]
		
	:return: State estimate at time t
		3x1 NumPy Array given the state is [x,y,yaw angle] --->
		[meters, meters, radians]
	"""	
	# These next 6 lines of code which place limits on the angular and linear 
	# velocities of the robot car can be removed if you desire.
	control_input_t_minus_1[0] = np.clip(control_input_t_minus_1[0],
																			-max_linear_velocity,
																			max_linear_velocity)
	control_input_t_minus_1[1] = np.clip(control_input_t_minus_1[1],
																			-max_angular_velocity,
																			max_angular_velocity)
	state_estimate_t = (A @ state_t_minus_1) + (B @ control_input_t_minus_1) 
			
	return state_estimate_t
	
def lqr(actual_state_x, desired_state_xf, Q, R, A, B, dt):
	"""
	Discrete-time linear quadratic regulator for a nonlinear system.

	Compute the optimal control inputs given a nonlinear system, cost matrices, 
	current state, and a final state.
	
	Compute the control variables that minimize the cumulative cost.

	Solve for P using the dynamic programming method.

	:param actual_state_x: The current state of the system 
		3x1 NumPy Array given the state is [x,y,yaw angle] --->
		[meters, meters, radians]
	:param desired_state_xf: The desired state of the system
		3x1 NumPy Array given the state is [x,y,yaw angle] --->
		[meters, meters, radians]	
	:param Q: The state cost matrix
		3x3 NumPy Array
	:param R: The input cost matrix
		2x2 NumPy Array
	:param dt: The size of the timestep in seconds -> float

	:return: u_star: Optimal action u for the current state 
		2x1 NumPy Array given the control input vector is
		[linear velocity of the car, angular velocity of the car]
		[meters per second, radians per second]
    """
	# We want the system to stabilize at desired_state_xf.
	x_error = actual_state_x - desired_state_xf

	# Solutions to discrete LQR problems are obtained using the dynamic 
	# programming method.
	# The optimal solution is obtained recursively, starting at the last 
	# timestep and working backwards.
	# You can play with this number
	N = 50

	# Create a list of N + 1 elements
	P = [None] * (N + 1)
	
	Qf = Q

	# LQR via Dynamic Programming
	P[N] = Qf

	# For i = N, ..., 1
	for i in range(N, 0, -1):

		# Discrete-time Algebraic Riccati equation to calculate the optimal 
		# state cost matrix
		P[i-1] = Q + A.T @ P[i] @ A - (A.T @ P[i] @ B) @ np.linalg.pinv(
			R + B.T @ P[i] @ B) @ (B.T @ P[i] @ A)      

	# Create a list of N elements
	K = [None] * N
	u = [None] * N

	# For i = 0, ..., N - 1
	for i in range(N):

		# Calculate the optimal feedback gain K
		K[i] = -np.linalg.pinv(R + B.T @ P[i+1] @ B) @ B.T @ P[i+1] @ A

		u[i] = K[i] @ x_error

	# Optimal control input is u_star
	u_star = u[N-1]

	return u_star

def main():
	
	# Let the time interval be 1.0 seconds
	dt = 1.0
	
	# Actual state
	# Our robot starts out at the origin (x=0 meters, y=0 meters), and 
	# the yaw angle is 0 radians. 
	actual_state_x = np.array([0,0,0]) 

	# Desired state [x,y,yaw angle]
	# [meters, meters, radians]
	desired_state_xf = np.array([2.000,2.000,np.pi/2])	
	
	# A matrix
	# 3x3 matrix -> number of states x number of states matrix
	# Expresses how the state of the system [x,y,yaw] changes 
	# from t-1 to t when no control command is executed.
	# Typically a robot on wheels only drives when the wheels are told to turn.
	# For this case, A is the identity matrix.
	# Note: A is sometimes F in the literature.
	A = np.array([	[1.0,  0,   0],
									[  0,1.0,   0],
									[  0,  0, 1.0]])

	# R matrix
	# The control input cost matrix
	# Experiment with different R matrices
	# This matrix penalizes actuator effort (i.e. rotation of the 
	# motors on the wheels that drive the linear velocity and angular velocity).
	# The R matrix has the same number of rows as the number of control
	# inputs and same number of columns as the number of 
	# control inputs.
	# This matrix has positive values along the diagonal and 0s elsewhere.
	# We can target control inputs where we want low actuator effort 
	# by making the corresponding value of R large. 
	R = np.array([[0.01,   0],  # Penalty for linear velocity effort
                [  0, 0.01]]) # Penalty for angular velocity effort

	# Q matrix
	# The state cost matrix.
	# Experiment with different Q matrices.
	# Q helps us weigh the relative importance of each state in the 
	# state vector (X, Y, YAW ANGLE). 
	# Q is a square matrix that has the same number of rows as 
	# there are states.
	# Q penalizes bad performance.
	# Q has positive values along the diagonal and zeros elsewhere.
	# Q enables us to target states where we want low error by making the 
	# corresponding value of Q large.
	Q = np.array([[0.639, 0, 0],  # Penalize X position error 
								[0, 1.0, 0],  # Penalize Y position error 
								[0, 0, 1.0]]) # Penalize YAW ANGLE heading error 
				  
	# Launch the robot, and have it move to the desired goal destination
	for i in range(100):
		print(f'iteration = {i} seconds')
		print(f'Current State = {actual_state_x}')
		print(f'Desired State = {desired_state_xf}')
		
		state_error = actual_state_x - desired_state_xf
		state_error_magnitude = np.linalg.norm(state_error)		
		print(f'State Error Magnitude = {state_error_magnitude}')
		
		B = getB(actual_state_x[2], dt)
		
		# LQR returns the optimal control input
		optimal_control_input = lqr(actual_state_x, 
									desired_state_xf, 
									Q, R, A, B, dt)	
		
		print(f'Control Input = {optimal_control_input}')
									
		
		# We apply the optimal control to the robot
		# so we can get a new actual (estimated) state.
		actual_state_x = state_space_model(A, actual_state_x, B, 
										optimal_control_input)	

		# Stop as soon as we reach the goal
		# Feel free to change this threshold value.
		if state_error_magnitude < 0.01:
			print("\nGoal Has Been Reached Successfully!")
			break
			
		print()

# Entry point for the program
main()

Output

You can see in the output that it took about 3 seconds for the robot to reach the goal position and orientation. The LQR algorithm did its job well.

outputJPG

Note that you can remove the maximum linear and angular velocity constraints if you have issues in getting the correct output. You can remove these constraints by going to the state_space_model method inside the code I pasted in the previous section.

Conclusion

That’s it for the fundamentals of LQR. If you ever run across LQR in the future and need a refresher, just come back to this tutorial. 

Keep building!