diff --git a/code/acting/CMakeLists.txt b/code/acting/CMakeLists.txt
index ed2d1742..4529faeb 100644
--- a/code/acting/CMakeLists.txt
+++ b/code/acting/CMakeLists.txt
@@ -53,6 +53,7 @@ find_package(catkin REQUIRED COMPONENTS
add_message_files(
FILES
Debug.msg
+ StanleyDebug.msg
)
## Generate services in the 'srv' folder
diff --git a/code/acting/launch/acting.launch b/code/acting/launch/acting.launch
index 41cb1957..b78b30e3 100644
--- a/code/acting/launch/acting.launch
+++ b/code/acting/launch/acting.launch
@@ -8,10 +8,10 @@
-
-
-
-
+
+
+
+
diff --git a/code/acting/msg/StanleyDebug.msg b/code/acting/msg/StanleyDebug.msg
new file mode 100644
index 00000000..f3074e4e
--- /dev/null
+++ b/code/acting/msg/StanleyDebug.msg
@@ -0,0 +1,5 @@
+float32 heading
+float32 path_heading
+float32 cross_err
+float32 heading_err
+float32 steering_angle
diff --git a/code/acting/src/acting/DummyTrajectoryPublisher.py b/code/acting/src/acting/DummyTrajectoryPublisher.py
index 841aadf1..e9c3ac86 100755
--- a/code/acting/src/acting/DummyTrajectoryPublisher.py
+++ b/code/acting/src/acting/DummyTrajectoryPublisher.py
@@ -36,10 +36,11 @@ def __init__(self):
# Static trajectory for testing purposes
self.initial_trajectory = [
- (985.5, -5433.2),
- (983.5, -5443.2),
+ (986.0, -5430.0),
+ (986.0, -5463.2),
+ (984.5, -5493.2),
- (983.5, -5563.5),
+ (984.5, -5563.5),
(985.0, -5573.2),
(986.3, -5576.5),
(987.3, -5578.5),
@@ -47,12 +48,16 @@ def __init__(self):
(990.5, -5579.8),
(1000.0, -5580.2),
- (1040.0, -5580.2),
- (1050.0, -5580.2),
- (1060.0, -5580.5),
- (1090.0, -5580.5),
- (1164.6, -5583.0),
- (1264.6, -5583.0)
+ (1040.0, -5580.0),
+ (1070.0, -5580.0),
+ (1080.0, -5582.0),
+ (1090.0, -5582.0),
+ (1100.0, -5580.0),
+ (1110.0, -5578.0),
+ (1120.0, -5578.0),
+ (1130.0, -5580.0),
+ (1464.6, -5580.0),
+ (1664.6, -5580.0)
]
self.updated_trajectory(self.initial_trajectory)
diff --git a/code/acting/src/acting/helper_functions.py b/code/acting/src/acting/helper_functions.py
index c64319e7..517258a4 100755
--- a/code/acting/src/acting/helper_functions.py
+++ b/code/acting/src/acting/helper_functions.py
@@ -30,12 +30,12 @@ def vectors_to_angle_abs(x1: float, y1: float, x2: float, y2: float) -> float:
def vector_angle(x1: float, y1: float) -> float:
"""
- Returns the angle (radians) between the two given vectors
+ Returns the angle (radians) of a given vectors
:param x1: v1[x]
:param y1: v1[y]
:return: angle between v1 and x-axis [-pi/2, pi/2]
"""
- # v_0 is a vector parallel on the x-axis
+ # v_0 is a vector parallel to the x-axis
l_v = math.sqrt(x1**2 + y1**2)
x_0 = x1 + l_v
y_0 = 0
@@ -45,13 +45,13 @@ def vector_angle(x1: float, y1: float) -> float:
if y1 < 0:
sign = -1
else:
- sign = 0
+ sign = 1
return alpha * sign
def vector_to_direction(x1, y1, x2, y2) -> float:
"""
- Returns the direction (angle to x-axis) of a vector.
+ Returns the direction (angle to y-axis) of a vector.
:param x1: tail of the vector [x]
:param y1: tail of the vector [y]
:param x2: head of the vector [x]
diff --git a/code/acting/src/acting/pure_pursuit_controller.py b/code/acting/src/acting/pure_pursuit_controller.py
index cae1ba02..15751dc7 100755
--- a/code/acting/src/acting/pure_pursuit_controller.py
+++ b/code/acting/src/acting/pure_pursuit_controller.py
@@ -161,7 +161,7 @@ def __calculate_steer(self) -> float:
:return:
"""
l_vehicle = 2.85 # wheelbase
- k_ld = 2.50 # todo: tune
+ k_ld = 2.0 # todo: tune
look_ahead_dist = 5.0 # offset so that ld is never zero
if round(self.__velocity, 1) < 0.1:
diff --git a/code/acting/src/acting/stanley_controller.py b/code/acting/src/acting/stanley_controller.py
new file mode 100755
index 00000000..2be1d74b
--- /dev/null
+++ b/code/acting/src/acting/stanley_controller.py
@@ -0,0 +1,326 @@
+#!/usr/bin/env python
+# from typing import List
+import math
+from math import atan, sqrt, sin, cos
+import numpy as np
+import ros_compatibility as roscomp
+from carla_msgs.msg import CarlaSpeedometer
+from geometry_msgs.msg import PoseStamped, Point # , Pose, Quaternion
+from nav_msgs.msg import Path
+from ros_compatibility.node import CompatibleNode
+# from ros_compatibility.qos import QoSProfile, DurabilityPolicy
+from rospy import Publisher, Subscriber
+# from sensor_msgs.msg import NavSatFix, Imu
+from std_msgs.msg import Float32 # Bool
+from acting.msg import StanleyDebug
+
+from helper_functions import vector_angle
+from trajectory_interpolation import points_to_vector
+
+
+class StanleyController(CompatibleNode):
+ def __init__(self):
+ super(StanleyController, self).__init__('stanley_controller')
+ self.loginfo('StanleyController node started')
+
+ self.control_loop_rate = self.get_param('control_loop_rate', 0.05)
+ self.role_name = self.get_param('role_name', 'ego_vehicle')
+
+ # Subscriber
+ self.position_sub: Subscriber = self.new_subscription(
+ Path,
+ f"/carla/{self.role_name}/trajectory",
+ self.__set_path,
+ qos_profile=1)
+
+ self.path_sub: Subscriber = self.new_subscription(
+ PoseStamped,
+ f"/carla/{self.role_name}/current_pos",
+ self.__set_position,
+ qos_profile=1)
+
+ self.velocity_sub: Subscriber = self.new_subscription(
+ CarlaSpeedometer,
+ f"/carla/{self.role_name}/Speed",
+ self.__set_velocity,
+ qos_profile=1)
+
+ self.heading_sub: Subscriber = self.new_subscription(
+ Float32,
+ f"/carla/{self.role_name}/current_heading",
+ self.__set_heading,
+ qos_profile=1)
+
+ # Publisher
+ self.stanley_steer_pub: Publisher = self.new_publisher(
+ Float32,
+ f"/carla/{self.role_name}/stanley_steer",
+ qos_profile=1)
+
+ self.debug_publisher: Publisher = self.new_publisher(
+ StanleyDebug,
+ f"/paf/{self.role_name}/stanley_debug",
+ qos_profile=1)
+
+ self.__position: (float, float) = None # x, y
+ self.__last_pos: (float, float) = None
+ self.__path: Path = None
+ self.__heading: float = None
+ self.__velocity: float = None
+ self.__tp_idx: int = 0 # target waypoint index
+ # error when there are no targets
+
+ def run(self):
+ """
+ Starts the main loop of the node
+ :return:
+ """
+ self.loginfo('StanleyController node running')
+
+ def loop(timer_event=None):
+ """
+ Main loop of the acting node
+ :param timer_event: Timer event from ROS
+ :return:
+ """
+ if self.__path is None:
+ self.logwarn("StanleyController hasn't received a path yet"
+ "and can therefore not publish steering")
+ return
+ if self.__position is None:
+ self.logwarn("StanleyController hasn't received the"
+ "position of the vehicle yet "
+ "and can therefore not publish steering")
+ return
+
+ if self.__heading is None:
+ self.logwarn("StanleyController hasn't received the"
+ "heading of the vehicle yet and"
+ "can therefore not publish steering")
+ return
+
+ if self.__velocity is None:
+ self.logwarn("StanleyController hasn't received the "
+ "velocity of the vehicle yet "
+ "and can therefore not publish steering")
+ return
+ self.stanley_steer_pub.publish(self.__calculate_steer())
+
+ self.new_timer(self.control_loop_rate, loop)
+ self.spin()
+
+ def __set_position(self, data: PoseStamped, min_diff=0.001):
+ """
+ Updates the current position of the vehicle
+ To avoid problems when the car is stationary, new positions will only
+ be accepted, if they are a certain distance from the current one
+ :param data: new position as PoseStamped
+ :param min_diff: minium difference between new and current point for
+ the new point to be accepted
+ :return:
+ """
+ if self.__position is None:
+ x0 = data.pose.position.x
+ y0 = data.pose.position.y
+ self.__position = (x0, y0)
+ return
+
+ # check if the new position is valid
+ dist = self.__dist_to(data.pose.position)
+ if dist < min_diff:
+ # for debugging purposes:
+ self.logdebug("New position disregarded, "
+ f"as dist ({round(dist, 3)}) to current pos "
+ f"< min_diff ({round(min_diff, 3)})")
+ return
+
+ old_x = self.__position[0]
+ old_y = self.__position[1]
+ self.__last_pos = (old_x, old_y)
+ new_x = data.pose.position.x
+ new_y = data.pose.position.y
+ self.__position = (new_x, new_y)
+
+ def __set_path(self, data: Path):
+ self.__path = data
+
+ def __set_heading(self, data: Float32):
+ self.__heading = data.data
+
+ def __set_velocity(self, data: CarlaSpeedometer):
+ self.__velocity = data.speed
+
+ def __calculate_steer(self) -> float:
+ """
+ Calculates the steering angle based on the current information
+ using the Stanly algorithm
+ :return: steering angle
+ """
+ k_ce = 0.10 # todo: tune
+ k_v = 1.0
+
+ current_velocity: float
+ if self.__velocity <= 1:
+ current_velocity = 1
+ else:
+ current_velocity = self.__velocity
+
+ closest_point_idx = self.__get_closest_point_index()
+ closest_point: PoseStamped = self.__path.poses[closest_point_idx]
+ traj_heading = self.__get_path_heading(closest_point_idx)
+
+ cross_err = self.__get_cross_err(closest_point.pose.position)
+ heading_err = self.__heading - traj_heading
+
+ steering_angle = heading_err + atan((k_ce * cross_err) /
+ current_velocity * k_v)
+ steering_angle *= - 1
+
+ # for debugging ->
+ debug_msg = StanleyDebug()
+ debug_msg.heading = self.__heading
+ debug_msg.path_heading = traj_heading
+ debug_msg.cross_err = abs(cross_err)
+ debug_msg.heading_err = heading_err
+ debug_msg.steering_angle = steering_angle
+ self.debug_publisher.publish(debug_msg)
+ # <-
+
+ return steering_angle
+
+ def __get_closest_point_index(self) -> int:
+ """
+ Returns index of the nearest point of the trajectory
+ :return: Index of the closest point
+ """
+ if len(self.__path.poses) < 2:
+ return -1
+
+ min_dist = 10e100
+ min_dist_idx = -1
+
+ for i in range(0, len(self.__path.poses)):
+ temp_pose: PoseStamped = self.__path.poses[i]
+ dist = self.__dist_to(temp_pose.pose.position)
+ if min_dist > dist:
+ min_dist = dist
+ min_dist_idx = i
+ return min_dist_idx
+
+ def __get_path_heading(self, index: int) -> float:
+ """
+ Calculates the heading of the current path at index
+ :param index: point of interest
+ :return: heading at path[index]
+ """
+ cur_pos: Point = self.__path.poses[index].pose.position
+ l_path = len(self.__path.poses)
+
+ if l_path == 1:
+ return 0
+
+ heading_sum = 0
+ heading_sum_args = 0
+
+ if index > 0:
+ # Calculate heading from the previous point on the trajectory
+ prv_point: Point = self.__path.poses[index-1].pose.position
+
+ prv_v_x, prv_v_y = points_to_vector((prv_point.x, prv_point.y),
+ (cur_pos.x, cur_pos.y))
+
+ heading_sum += vector_angle(prv_v_x, prv_v_y)
+ heading_sum_args += 1
+
+ elif index < l_path - 1:
+ # Calculate heading to the following point on the trajectory
+ aft_point: Point = self.__path.poses[index + 1].pose.position
+
+ aft_v_x, aft_v_y = points_to_vector((aft_point.x, aft_point.y),
+ (cur_pos.x, cur_pos.y))
+
+ heading_sum += vector_angle(aft_v_x, aft_v_y)
+ heading_sum_args += 1
+
+ return heading_sum / heading_sum_args
+
+ def __get_cross_err(self, pos: Point) -> float:
+ """
+ Returns the Distance between current position and target position.
+ The distance is negative/positive based on whether the closest point
+ is to the left or right of the vehicle.
+ :param pos:
+ :return:
+ """
+ dist = self.__dist_to(pos)
+
+ x = self.__position[0]
+ y = self.__position[1]
+
+ # self.loginfo(f"Cur_X: {round(x, 2)} \t "
+ # f"Cur_Y: {round(y, 2)} \t "
+ # f"Trj_X: {round(pos.x, 2)} \t "
+ # f"Trj_Y: {round(pos.y, 2)} \t ")
+
+ alpha = 0
+ if self.__heading is not None:
+ alpha = self.__heading + (math.pi / 2)
+ v_e_0 = (0, 1)
+ v_e = (cos(alpha)*v_e_0[0] - sin(alpha)*v_e_0[1],
+ sin(alpha)*v_e_0[0] + cos(alpha)*v_e_0[1])
+
+ # define a vector (v_ab) with length 10 centered on the cur pos
+ # of the vehicle, with a heading parallel to that of the vehicle
+ a = (x + (v_e[0] * 2.5), y + (v_e[1] * 2.5))
+ b = (x - (v_e[0] * 2.5), y - (v_e[1] * 2.5))
+
+ v_ab = (b[0] - a[0], b[1] - a[1])
+ v_am = (pos.x - a[0], pos.y - a[1])
+
+ c = np.array([[v_ab[0], v_am[0]],
+ [v_ab[1], v_am[1]]])
+ temp_sign = np.linalg.det(c)
+
+ min_sign = 0.01 # to avoid rounding errors
+
+ if temp_sign > -min_sign:
+ sign = -1
+ else:
+ sign = 1
+
+ res = dist * sign
+
+ return res
+
+ def __dist_to(self, pos: Point) -> float:
+ """
+ Distance between current position and target position (only (x,y))
+ :param pos: targeted position
+ :return: distance
+ """
+ x_cur = self.__position[0]
+ y_cur = self.__position[1]
+ x_target = pos.x
+ y_target = pos.y
+ d = (x_target - x_cur) ** 2 + (y_target - y_cur) ** 2
+ return sqrt(d)
+
+
+def main(args=None):
+ """
+ Main function starts the node
+ :param args:
+ """
+ roscomp.init('stanley_controller', args=args)
+
+ try:
+ node = StanleyController()
+ node.run()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ roscomp.shutdown()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/code/acting/src/acting/vehicle_controller.py b/code/acting/src/acting/vehicle_controller.py
index 3bf917ae..c234f144 100755
--- a/code/acting/src/acting/vehicle_controller.py
+++ b/code/acting/src/acting/vehicle_controller.py
@@ -220,7 +220,7 @@ def __choose_controller(self) -> int:
Chooses with steering controller to use
:return:
"""
- return PURE_PURSUIT_CONTROLLER
+ return STANLEY_CONTROLLER
def main(args=None):
diff --git a/code/acting/src/acting/velocity_publisher_dummy.py b/code/acting/src/acting/velocity_publisher_dummy.py
index e165d02e..7b8f1fb0 100755
--- a/code/acting/src/acting/velocity_publisher_dummy.py
+++ b/code/acting/src/acting/velocity_publisher_dummy.py
@@ -23,10 +23,12 @@ def __init__(self):
Float32,
f"/carla/{self.role_name}/max_velocity",
qos_profile=1)
+
self.velocity = 4.0
self.delta_velocity = 0.125
self.max_velocity = 5.5
self.min_velocity = 4
+
self.__dv = self.delta_velocity
def run(self):
diff --git a/code/agent/config/dev_objects.json b/code/agent/config/dev_objects.json
index 8b103317..c0817bd2 100644
--- a/code/agent/config/dev_objects.json
+++ b/code/agent/config/dev_objects.json
@@ -10,8 +10,8 @@
"id": "hero",
"role_name": "hero",
"spawn_point": {
- "x": 985.5,
- "y": -5433.2,
+ "x": 986.0,
+ "y": -5442.0,
"z": 371.0,
"yaw": -90.0,
"pitch": 0.0,
diff --git a/code/agent/config/rviz_config.rviz b/code/agent/config/rviz_config.rviz
index d7633d3f..9595c6ef 100644
--- a/code/agent/config/rviz_config.rviz
+++ b/code/agent/config/rviz_config.rviz
@@ -5,7 +5,7 @@ Panels:
Property Tree Widget:
Expanded: ~
Splitter Ratio: 0.5
- Tree Height: 519
+ Tree Height: 417
- Class: rviz/Selection
Name: Selection
- Class: rviz/Tool Properties
@@ -200,19 +200,19 @@ Visualization Manager:
Invert Z Axis: false
Name: Current View
Near Clip Distance: 0.009999999776482582
- Pitch: 0.35539835691452026
+ Pitch: 0.19039836525917053
Target Frame:
- Yaw: 1.7953970432281494
+ Yaw: 4.520427227020264
Saved: ~
Window Geometry:
Camera:
collapsed: false
Displays:
collapsed: false
- Height: 1403
+ Height: 1376
Hide Left Dock: false
Hide Right Dock: false
- QMainWindow State: 000000ff00000000fd000000040000000000000304000004e1fc0200000009fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003b000003b9000000c700fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000c00430061006d00650072006101000003fa000001220000001600ffffff000000010000010f000004e1fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003b000004e1000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e1000001970000000300000a000000003efc0100000002fb0000000800540069006d0065010000000000000a000000030700fffffffb0000000800540069006d00650100000000000004500000000000000000000005e1000004e100000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
+ QMainWindow State: 000000ff00000000fd000000040000000000000304000004c6fc0200000009fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003b0000022a000000c700fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000c00430061006d006500720061010000026b000002960000001600ffffff000000010000010f000004c6fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003b000004c6000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000009b80000003efc0100000002fb0000000800540069006d00650100000000000009b80000030700fffffffb0000000800540069006d0065010000000000000450000000000000000000000599000004c600000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
Selection:
collapsed: false
Time:
@@ -221,6 +221,6 @@ Window Geometry:
collapsed: false
Views:
collapsed: false
- Width: 2560
- X: 0
- Y: 0
+ Width: 2488
+ X: 1992
+ Y: 27
diff --git a/doc/00_assets/controller_img/pure_pursuit.png b/doc/00_assets/controller_img/pure_pursuit.png
new file mode 100644
index 00000000..81a03aa8
Binary files /dev/null and b/doc/00_assets/controller_img/pure_pursuit.png differ
diff --git a/doc/00_assets/controller_img/pure_pursuit_v_6_25.png b/doc/00_assets/controller_img/pure_pursuit_v_6_25.png
new file mode 100644
index 00000000..c415713b
Binary files /dev/null and b/doc/00_assets/controller_img/pure_pursuit_v_6_25.png differ
diff --git a/doc/00_assets/controller_img/stanley.png b/doc/00_assets/controller_img/stanley.png
new file mode 100644
index 00000000..3aacdb66
Binary files /dev/null and b/doc/00_assets/controller_img/stanley.png differ
diff --git a/doc/00_assets/controller_img/stanley_v_6_25.png b/doc/00_assets/controller_img/stanley_v_6_25.png
new file mode 100644
index 00000000..b54aa7d4
Binary files /dev/null and b/doc/00_assets/controller_img/stanley_v_6_25.png differ
diff --git a/doc/05_acting/03_lateral_controller.md b/doc/05_acting/03_lateral_controller.md
new file mode 100644
index 00000000..783594b4
--- /dev/null
+++ b/doc/05_acting/03_lateral_controller.md
@@ -0,0 +1,75 @@
+# Overview of the current status of the lateral controller
+
+**Summary:** This page provides an overview of the current status of lateral controllers
+
+---
+
+## Author
+
+Gabriel Schwald
+
+## Date
+
+20.02.2023
+
+
+* [Overview of the current status of the lateral controller](#overview-of-the-current-status-of-the-lateral-controller)
+ * [Author](#author)
+ * [Date](#date)
+ * [Lateral controller](#lateral-controller)
+ * [Baseline](#baseline)
+ * [Velocity](#velocity)
+ * [Parameters](#parameters)
+ * [Tuning](#tuning)
+
+
+## Lateral controller
+
+At the moment the control algorithm is chosen manually by editing Line 223 in vehicle_controller.py.
+Accordingly, only one controller can be used per run, this will later be changed,
+in order to use each controller in its most effective range.
+
+The testing scenario consists of a manually created L-shaped trajectory, with a short slalom section at the end.
+Testing with real trajectories will definitely help in improving the controllers,
+but is not yet available at this time.
+
+## Baseline
+
+With a somewhat constant velocity of around 4.5 m/s the following distance to the trajectory can be observed with the
+default parameters.
+
+![Pure Pursuit Baseline](./../00_assets/controller_img/pure_pursuit.png)
+Pure Pursuit Baseline
+
+![Stanley Baseline](./../00_assets/controller_img/stanley.png)
+Stanley Baseline
+
+### Velocity
+
+Increasing the velcoity by around 2 m/s to 6.25 m/s already has an observable effect on the error.
+
+![Pure Pursuit with increased velocity](./../00_assets/controller_img/pure_pursuit_v_6_25.png)
+Pure Pursuit with increased velocity
+
+![Stanley with increased velocity](./../00_assets/controller_img/stanley_v_6_25.png)
+Stanley with increased velocity
+
+### Parameters
+
+The following parameters may be considered for optimization:
+
+Stanley:
+
+* k_ce <--> cross-track error weight
+* k_v <--> velocity weight
+
+Pure-Pursuit:
+
+* k_ld <--> look-ahead distance weight
+* look_ahead_dist <--> minimum look-ahead distance (maybe remove altogether?)
+
+The PID-Controller for the steering angle may also be tuned independently of the tuning of both controllers.
+
+## Tuning
+
+This document will be updated, whenever sizeable progress is made.