Motion planning algorithm under dynamic constraints - State Lattice Planner (State Lattice Planner)

   The graph model used for path search established by algorithms such as A* and RRT does not consider the dynamic constraints of the robot. The path between two adjacent nodes of the graph model constructed may be infeasible or difficult to be determined. Robot tracking, now we want to make the connection between two adjacent nodes in the graph a feasible connection that satisfies dynamic constraints, so how to build such a graph?

   There are usually two methods to construct such a graph: the first is based on control space sampling (forward method) , discretizing the robot's control space, and then driving the robot to move forward to obtain trajectories one by one, such as the commonly used DWA Algorithm, the second one is based on state space sampling (reverse method) , discretizing the robot's state space to generate many states, and then looking for connections from one state to another, such as the commonly used TEB algorithm

   The algorithms such as A* and RRT introduced earlier can also be regarded as discretization of the state space when searching or sampling to construct a graph. Now we establish a state equation describing the robot's motion s˙ = f ( s , u ) \dot { s}=f(s,u)s˙=f(s,u ) , where s represents all possible states, such as position information x, y, speed informationx ˙ , y ˙ \color{red}{\dot{x}}, {\dot{y}}x˙y˙etc., u represents the control input. Of course we also need to know the initial state of the robot s 0 s_0s0

   Based on the current state of the robot, when we give different input u, different motion trajectories will naturally occur, as shown in the left figure below. The advantage of this control space-based sampling (forward method) is that it is simple and easy to implement. Its disadvantage is that it is not task-oriented and does not care where the target point is. Although it can produce better planning results, the planning efficiency is low. ; If the current state of the robot and the state of the target point (such as position information, attitude information) are known, how to solve the middle path (edge) from these two states, and obtain the control variable u at time T, as shown in the right As shown in the figure, this is sampling based on state space (reverse method). Its advantage is that it is very task-oriented, but the difficulty is that it is difficult to implement.

Insert image description here

   In the example below, if the state of the drone is selected as three-dimensional position and three-dimensional velocity, and the input is selected as three-dimensional acceleration, the state equation s˙ = A ⋅ s + B ⋅ u \dot{s}=A\cdot s+ is constructed. B\cdot us˙=As+Bu , using different input values, the trajectory obtained based on the sampling of the control space is shown in the right figure below

Insert image description here

   If the state is selected as three-dimensional position, three-dimensional velocity, and three-dimensional acceleration, and the input is selected as three-dimensional jerk, different input values ​​will be used. The trajectory obtained based on the sampling of the control space is as shown in the right figure below:

Insert image description here


   From the state equation s ˙ = A ⋅ s + B ⋅ u \dot{s}=A\cdot s+B\cdot us˙=As+Bu , the specific state transition equation can be solved as follows. If A is a nilpotent matrix, thenA m A^mAAfter mth power, they are all 0, so that e A te^{At}can be easily calculated through Taylor expansion.eA t item.

Insert image description here


   With the above foundation, let's officially start the introduction of the State Lattice Planner. Given an initial state, given different control inputs, and simulated for a period of time, you can get a series of arrivals at different end positions. Trajectory of (The picture on the left divides u into 9 types, and the picture on the right divides u into 25 types):

Insert image description here

   It should be noted that when the lattice graph is constructed online, it is not necessary to continuously expand outward in all directions as shown in the figure above. We can use heuristic information to prioritize expansion in the direction of the target, which can reduce time and improve efficiency. Different from the raster map built in advance, the lattice graph is constructed while searching, that is, only the required nodes are constructed, which reduces calculation time and storage space.


   The figure below gives an example of sampling (forward method) based on control space. A search tree is constructed while sampling. If the edge generated by the sampling input does not intersect with an obstacle, the edge is added to the search tree.

Insert image description here


   Next, let’s look at an example of sampling (reverse method) based on state space. Assume that the car model in the figure below is the Reeds-Sheep model. That is, the car can only choose to go straight forward, turn left, or turn left based on the current state. Turn right forward, reverse backward, turn left backward, and turn right backward. These six forms of movement are as shown in Figure b below. In this way, after we get the target point, we can move between the current position and the target position. Perform state sampling, and then calculate which of the six motion forms should be used to connect two adjacent state points, and finally form a complete path to the target point.

Insert image description here

   The figure below shows a Lattice graph with only two layers. First, several sampling points are taken on a circle with the robot as the center and a certain distance as the radius. These sampling points are used as intermediate states, and the connections between the robot and these intermediate states are back calculated. method, the first layer of Lattice graph is formed, and then several sampling points are taken on a circle with the robot as the center and a larger distance as the radius, and the connection method of these sampling points to the mid-state is back-calculated to form the second layer of Lattice graph. . At this point, using the middle state as a springboard, we can get the trajectory from the current position to any sampling point, and get the result of the two-layer Lattice graph shown in the figure below.

   值得注意的是该两层Lattice graph中,第一层,从机器人当前位置到达中状态的轨迹是不确定的,这取决于机器人的当前状态,然而第二层,从中状态到达大圆上采样点的轨迹可以是确定的,他不受机器人当前状态的影响。

Insert image description here


   下图中给出了一些在状态空间采样和在控制空间采样的不同轨迹对比,可以看出相比于在控制空间采样,在状态空间采样的路径更优,但也存在着计算两个状态点之间轨迹不那么容易得问题。

Insert image description here

   下图中给出了在状态空间采样和在控制空间采样的示意图,对于在状态空间采样,可以离线生成Lattice图,然后在线进行搜索求解

Insert image description here

   采用单层网格规划是一种常见的局部避碰方法,如DWA算法。也可以采用轨迹库的形式,生成一系列轨迹,然后基于多项成本函数,对每个轨迹进行评级打分,选择出轨迹库中的最优轨迹。

Insert image description here

Insert image description here

   使用Lattice Planner进行规划时,也可以使用启发函数来提高规划效率,我们设计启发式函数时有两种,一种是不考虑障碍物(下面的左图),另一种是不考虑机器人的动力学模型(下面的右图),在工程上常采用两者的集合,进行叠加或者取最大值

Insert image description here

   下图中给出了使用欧氏距离作为启发函数和使用考虑动力学模型但不考虑障碍物的启发函数的效果对比:

Insert image description here

   最后,我们来看一种特殊的Lattice Planner,在无人驾驶中常使用Frenet-serret坐标系下进行规划,在道路环境中存在着车道线,我们常常希望汽车去做一个车道线的跟随,此时我们可以沿着车道线去分解设定坐标系,沿着车道线方向为s轴,其径向方向为d轴,如下图所示,依然可以采用之前的思路,对运动方程进行参数化

   d ( t ) = a d 0 + a d 1 t + a d 2 t 2 + a d 3 t 3 + a d 4 t 4 + a d 5 t 5 s ( t ) = a s 0 + a s 1 t + a s 2 t 2 + a s 3 t 3 + a s 4 t 4 + a s 5 t 5 \begin{aligned}d(t)&=a_{d0}+a_{d1}t+a_{d2}t^2+a_{d3}t^3+a_{d4}t^4+a_{d5}t^5\\s(t)&=a_{s0}+a_{s1}t+a_{s2}t^2+a_{s3}t^3+a_{s4}t^4+a_{s5}t^5\end{aligned} d(t)s(t)=ad0+ad1t+ad2t2+ad3t3+ad4t4+ad5t5=as0+as1t+as2t2+as3t3+as4t4+as5t5

Insert image description here

   For the radial direction, which satisfies the current attitude and the desired final attitude, we expect the car to be on the lane line at the final moment, that is, we expect its radial velocity and acceleration to be 0, D ( T ) = ( df 0 0 ) D (T)=\begin{pmatrix}d_f&0&0\end{pmatrix}D(T)=(df00) , and then use OBVP to solve the optimal control problem.

   For the tangent direction, the situation is more complicated. There are many situations, such as parking tasks, lane changing tasks, and speed maintenance. Different strategies are generally adopted for different situations.

Insert image description here


   Here is the Lattice Planner source code in the zhm_real/MotionPlanning motion planning library. It should be noted that the source code calls other programs in the zhm_real/MotionPlanning motion planning library. All the source codes below are just to help understand the Lattice Planner algorithm. If you want to run If so, it is recommended to download the complete zhm_real/MotionPlanning motion planning library, the link is as follows:

   https://github.com/zhm-real/MotionPlanning/tree/master

import os
import sys
import math
import copy
import numpy as np
import matplotlib.pyplot as plt

sys.path.append(os.path.dirname(os.path.abspath(__file__)) +
                "/../../MotionPlanning/")

from CurvesGenerator import cubic_spline, quintic_polynomial, quartic_polynomial
import LatticePlanner.env as env
import LatticePlanner.draw as draw


class C:
    # Parameter
    MAX_SPEED = 50.0 / 3.6
    MAX_ACCEL = 8.0
    MAX_CURVATURE = 2.5

    ROAD_WIDTH = 8.0
    ROAD_SAMPLE_STEP = 1.0

    T_STEP = 0.15
    MAX_T = 5.0
    MIN_T = 4.0

    TARGET_SPEED = 30.0 / 3.6
    SPEED_SAMPLE_STEP = 5.0 / 3.6

    # cost weights for Cruising
    K_JERK = 0.1
    K_TIME = 0.1
    K_V_DIFF = 1.0
    K_OFFSET = 1.5
    K_COLLISION = 500

    # cost weights for Stopping
    # K_JERK = 0.1
    # K_TIME = 0.1
    # K_V_DIFF = 200
    # K_OFFSET = 1.0
    # K_COLLISION = 500

    # parameters for vehicle
    K_SIZE = 0.9
    RF = 4.5 * K_SIZE  # [m] distance from rear to vehicle front end of vehicle
    RB = 1.0 * K_SIZE  # [m] distance from rear to vehicle back end of vehicle
    W = 3.0 * K_SIZE  # [m] width of vehicle
    WD = 0.7 * W  # [m] distance between left-right wheels
    WB = 3.5 * K_SIZE  # [m] Wheel base
    TR = 0.5 * K_SIZE  # [m] Tyre radius
    TW = 1 * K_SIZE  # [m] Tyre width
    MAX_STEER = 0.6  # [rad] maximum steering angle


class Path:
    def __init__(self):
        self.t = []

        self.l = []
        self.l_v = []
        self.l_a = []
        self.l_jerk = []

        self.s = []
        self.s_v = []
        self.s_a = []
        self.s_jerk = []

        self.x = []
        self.y = []
        self.yaw = []
        self.ds = []
        self.curv = []

        self.cost = 0.0


def sampling_paths_for_Cruising(l0, l0_v, l0_a, s0, s0_v, s0_a, ref_path):
    PATHS = dict()

    for s1_v in np.arange(C.TARGET_SPEED * 0.6, C.TARGET_SPEED * 1.4, C.TARGET_SPEED * 0.2):

        for t1 in np.arange(4.5, 5.5, 0.2):
            path_pre = Path()
            path_lon = quartic_polynomial.QuarticPolynomial(s0, s0_v, s0_a, s1_v, 0.0, t1)

            path_pre.t = list(np.arange(0.0, t1, C.T_STEP))
            path_pre.s = [path_lon.calc_xt(t) for t in path_pre.t]
            path_pre.s_v = [path_lon.calc_dxt(t) for t in path_pre.t]
            path_pre.s_a = [path_lon.calc_ddxt(t) for t in path_pre.t]
            path_pre.s_jerk = [path_lon.calc_dddxt(t) for t in path_pre.t]

            for l1 in np.arange(-C.ROAD_WIDTH, C.ROAD_WIDTH, C.ROAD_SAMPLE_STEP):
                path = copy.deepcopy(path_pre)
                path_lat = quintic_polynomial.QuinticPolynomial(l0, l0_v, l0_a, l1, 0.0, 0.0, t1)

                path.l = [path_lat.calc_xt(t) for t in path_pre.t]
                path.l_v = [path_lat.calc_dxt(t) for t in path_pre.t]
                path.l_a = [path_lat.calc_ddxt(t) for t in path_pre.t]
                path.l_jerk = [path_lat.calc_dddxt(t) for t in path_pre.t]

                path.x, path.y = SL_2_XY(path.s, path.l, ref_path)
                path.yaw, path.curv, path.ds = calc_yaw_curv(path.x, path.y)

                l_jerk_sum = sum(np.abs(path.l_jerk))
                s_jerk_sum = sum(np.abs(path.s_jerk))
                v_diff = abs(C.TARGET_SPEED - path.s_v[-1])

                path.cost = C.K_JERK * (l_jerk_sum + s_jerk_sum) + \
                            C.K_V_DIFF * v_diff + \
                            C.K_TIME * t1 * 2 + \
                            C.K_OFFSET * abs(path.l[-1]) + \
                            C.K_COLLISION * is_path_collision(path)

                PATHS[path] = path.cost

    return PATHS


def sampling_paths_for_Stopping(l0, l0_v, l0_a, s0, s0_v, s0_a, ref_path):
    PATHS = dict()

    for s1_v in [-1.0, 0.0, 1.0, 2.0]:

        for t1 in np.arange(1.0, 16.0, 1.0):
            path_pre = Path()
            path_lon = quintic_polynomial.QuinticPolynomial(s0, s0_v, s0_a, 55.0, s1_v, 0.0, t1)

            path_pre.t = list(np.arange(0.0, t1, C.T_STEP))
            path_pre.s = [path_lon.calc_xt(t) for t in path_pre.t]
            path_pre.s_v = [path_lon.calc_dxt(t) for t in path_pre.t]
            path_pre.s_a = [path_lon.calc_ddxt(t) for t in path_pre.t]
            path_pre.s_jerk = [path_lon.calc_dddxt(t) for t in path_pre.t]

            for l1 in [0.0]:
                path = copy.deepcopy(path_pre)
                path_lat = quintic_polynomial.QuinticPolynomial(l0, l0_v, l0_a, l1, 0.0, 0.0, t1)

                path.l = [path_lat.calc_xt(t) for t in path_pre.t]
                path.l_v = [path_lat.calc_dxt(t) for t in path_pre.t]
                path.l_a = [path_lat.calc_ddxt(t) for t in path_pre.t]
                path.l_jerk = [path_lat.calc_dddxt(t) for t in path_pre.t]

                path.x, path.y = SL_2_XY(path.s, path.l, ref_path)
                path.yaw, path.curv, path.ds = calc_yaw_curv(path.x, path.y)

                if path.yaw is None:
                    continue

                l_jerk_sum = sum(np.abs(path.l_jerk))
                s_jerk_sum = sum(np.abs(path.s_jerk))
                v_diff = (path.s_v[-1]) ** 2

                path.cost = C.K_JERK * (l_jerk_sum + s_jerk_sum) + \
                            C.K_V_DIFF * v_diff + \
                            C.K_TIME * t1 * 2 + \
                            C.K_OFFSET * abs(path.l[-1]) + \
                            50.0 * sum(np.abs(path.s_v))

                PATHS[path] = path.cost

    return PATHS


def SL_2_XY(s_set, l_set, ref_path):
    pathx, pathy = [], []

    for i in range(len(s_set)):
        x_ref, y_ref = ref_path.calc_position(s_set[i])

        if x_ref is None:
            break

        yaw = ref_path.calc_yaw(s_set[i])
        x = x_ref + l_set[i] * math.cos(yaw + math.pi / 2.0)
        y = y_ref + l_set[i] * math.sin(yaw + math.pi / 2.0)

        pathx.append(x)
        pathy.append(y)

    return pathx, pathy


def calc_yaw_curv(x, y):
    yaw, curv, ds = [], [], []

    for i in range(len(x) - 1):
        dx = x[i + 1] - x[i]
        dy = y[i + 1] - y[i]
        ds.append(math.hypot(dx, dy))
        yaw.append(math.atan2(dy, dx))

    if len(yaw) == 0:
        return None, None, None

    yaw.append(yaw[-1])
    ds.append(ds[-1])

    for i in range(len(yaw) - 1):
        curv.append((yaw[i + 1] - yaw[i]) / ds[i])

    return yaw, curv, ds


def is_path_collision(path):
    index = range(0, len(path.x), 5)
    x = [path.x[i] for i in index]
    y = [path.y[i] for i in index]
    yaw = [path.yaw[i] for i in index]

    for ix, iy, iyaw in zip(x, y, yaw):
        d = 1.8
        dl = (C.RF - C.RB) / 2.0
        r = math.hypot((C.RF + C.RB) / 2.0, C.W / 2.0) + d

        cx = ix + dl * math.cos(iyaw)
        cy = iy + dl * math.sin(iyaw)

        for i in range(len(C.obs)):
            xo = C.obs[i][0] - cx
            yo = C.obs[i][1] - cy
            dx = xo * math.cos(iyaw) + yo * math.sin(iyaw)
            dy = -xo * math.sin(iyaw) + yo * math.cos(iyaw)

            if abs(dx) < r and abs(dy) < C.W / 2 + d:
                return 1.0

    return 0.0


def verify_path(path):
    # if any([v > C.speed_max for v in path.s_v]) or \
    #         any([abs(a) > C.acceleration_max for a in path.s_a]):
    #     return False

    if any([v > C.MAX_SPEED for v in path.s_v]) or \
            any([abs(a) > C.MAX_ACCEL for a in path.s_a]) or \
            any([abs(curv) > C.MAX_CURVATURE for curv in path.curv]):
        return False

    return True


def extract_optimal_path(paths):
    if len(paths) == 0:
        return None

    while len(paths) > 1:
        path = min(paths, key=paths.get)
        paths.pop(path)
        if verify_path(path) is False:
            continue
        else:
            return path

    return paths[-1]


def lattice_planner_for_Cruising(l0, l0_v, l0_a, s0, s0_v, s0_a, ref_path):
    paths = sampling_paths_for_Cruising(l0, l0_v, l0_a, s0, s0_v, s0_a, ref_path)
    path = extract_optimal_path(paths)

    return path


def lattice_planner_for_Stopping(l0, l0_v, l0_a, s0, s0_v, s0_a, ref_path):
    paths = sampling_paths_for_Stopping(l0, l0_v, l0_a, s0, s0_v, s0_a, ref_path)
    path = extract_optimal_path(paths)

    return path


def get_reference_line(x, y):
    index = range(0, len(x), 3)
    x = [x[i] for i in index]
    y = [y[i] for i in index]

    cubicspline = cubic_spline.Spline2D(x, y)
    s = np.arange(0, cubicspline.s[-1], 0.1)

    rx, ry, ryaw, rk = [], [], [], []
    for i_s in s:
        ix, iy = cubicspline.calc_position(i_s)
        rx.append(ix)
        ry.append(iy)
        ryaw.append(cubicspline.calc_yaw(i_s))
        rk.append(cubicspline.calc_curvature(i_s))

    return rx, ry, ryaw, rk, cubicspline


def pi_2_pi(theta):
    if theta > math.pi:
        return theta - 2.0 * math.pi

    if theta < -math.pi:
        return theta + 2.0 * math.pi

    return theta


def main_Crusing():
    ENV = env.ENVCrusing()
    wx, wy = ENV.ref_line
    bx1, by1 = ENV.bound_in
    bx2, by2 = ENV.bound_out

    C.obs = np.array([[50, 10], [96, 25], [70, 40],
                      [40, 50], [25, 75]])

    obs_x = [x for x, y in C.obs]
    obs_y = [y for x, y in C.obs]

    rx, ry, ryaw, rk, ref_path = get_reference_line(wx, wy)

    l0 = 2.0  # current lateral position [m]
    l0_v = 0.0  # current lateral speed [m/s]
    l0_a = 0.0  # current lateral acceleration [m/s]
    s0 = 0.0  # current course position
    s0_v = 20.0 / 3.6  # current speed [m/s]
    s0_a = 0.0

    while True:
        path = lattice_planner_for_Cruising(l0, l0_v, l0_a, s0, s0_v, s0_a, ref_path)

        if path is None:
            print("No feasible path found!!")
            break

        l0 = path.l[1]
        l0_v = path.l_v[1]
        l0_a = path.l_a[1]
        s0 = path.s[1]
        s0_v = path.s_v[1]
        s0_a = path.s_a[1]

        if np.hypot(path.x[1] - rx[-1], path.y[1] - ry[-1]) <= 2.0:
            print("Goal")
            break

        dy = (path.yaw[2] - path.yaw[1]) / path.ds[1]
        steer = pi_2_pi(math.atan(-C.WB * dy))

        plt.cla()
        # for stopping simulation with the esc key.
        plt.gcf().canvas.mpl_connect(
            'key_release_event',
            lambda event: [exit(0) if event.key == 'escape' else None])
        plt.plot(rx, ry, linestyle='--', color='gray')
        plt.plot(bx1, by1, linewidth=1.5, color='k')
        plt.plot(bx2, by2, linewidth=1.5, color='k')
        plt.plot(path.x[1:], path.y[1:], linewidth='2', color='royalblue')
        plt.plot(obs_x, obs_y, 'ok')
        draw.draw_car(path.x[1], path.y[1], path.yaw[1], steer, C)
        plt.title("[Crusing Mode]  v :" + str(s0_v * 3.6)[0:4] + " km/h")
        plt.axis("equal")
        plt.pause(0.0001)

    plt.pause(0.0001)
    plt.show()


def main_Stopping():
    ENV = env.ENVStopping()
    wx, wy = ENV.ref_line
    bx1, by1 = ENV.bound_up
    bx2, by2 = ENV.bound_down

    C.ROAD_WIDTH = ENV.road_width
    rx, ry, ryaw, rk, ref_path = get_reference_line(wx, wy)

    l0 = 0.0  # current lateral position [m]
    l0_v = 0.0  # current lateral speed [m/s]
    l0_a = 0.0  # current lateral acceleration [m/s]
    s0 = 0.0  # current course position
    s0_v = 30.0 / 3.6  # current speed [m/s]
    s0_a = 0.0

    while True:
        path = lattice_planner_for_Stopping(l0, l0_v, l0_a, s0, s0_v, s0_a, ref_path)

        if path is None:
            print("No feasible path found!!")
            break

        l0 = path.l[1]
        l0_v = path.l_v[1]
        l0_a = path.l_a[1]
        s0 = path.s[1]
        s0_v = path.s_v[1]
        s0_a = path.s_a[1]

        if np.hypot(path.x[1] - 56.0, path.y[1] - 0) <= 1.5:
            print("Goal")
            break

        plt.cla()
        # for stopping simulation with the esc key.
        plt.gcf().canvas.mpl_connect(
            'key_release_event',
            lambda event: [exit(0) if event.key == 'escape' else None])
        plt.plot(rx, ry, linestyle='--', color='gray')
        plt.plot(bx1, by1, linewidth=1.5, color='k')
        plt.plot(bx2, by2, linewidth=1.5, color='k')
        plt.plot(path.x[1:], path.y[1:], linewidth='2', color='royalblue')
        draw.draw_car(path.x[1], path.y[1], path.yaw[1], 0.0, C)
        plt.title("[Stopping Mode]  v :" + str(s0_v * 3.6)[0:4] + " km/h")
        plt.axis("equal")
        plt.pause(0.0001)

    plt.pause(0.0001)
    plt.show()

    plt.plot(rx, ry, linestyle='--', color='gray')
    plt.plot(bx1, by1, linewidth=1.5, color='k')
    plt.plot(bx2, by2, linewidth=1.5, color='k')
    plt.axis("equal")
    plt.show()


if __name__ == '__main__':
    main_Crusing()
    # main_Stopping()

   The running effect is as follows:

Lattice test in MotionPlanning



   References:

   1. Deep Blue College-Mobile Robot Motion Planning

   2. zhm_real/MotionPlanning motion planning library

   3. State Lattice Planner


Guess you like

Origin blog.csdn.net/qq_44339029/article/details/132420433