[PythonRobotics 분석] LQR_steer_control 분석(Infitie Horzion)

main 코드

def main():
    print("LQR steering control tracking start!!")
    ax = [0.0, 6.0, 12.5, 10.0, 7.5, 3.0, -1.0]
    ay = [0.0, -3.0, -5.0, 6.5, 3.0, 5.0, -2.0]
    goal = [ax[-1], ay[-1]]

    cx, cy, cyaw, ck, s = cubic_spline_planner.calc_spline_course(
        ax, ay, ds=0.1)
    target_speed = 10.0 / 3.6  # simulation parameter km/h -> m/s

    sp = calc_speed_profile(cx, cy, cyaw, target_speed)

    t, x, y, yaw, v = closed_loop_prediction(cx, cy, cyaw, ck, sp, goal)

여기서 cubic_spline_planner는 점을 ds길이에 맞게, interpolate한 점들을 연산해준다.

그래서 나오는 cx, cy, cyaw, ck, s는 생겨난 경로의 특정 지점들의 집합이라고 생각하면된다.

(k : 곡률)

interpolate된 경로

Speed Profile생성

def calc_speed_profile(cx, cy, cyaw, target_speed):
    speed_profile = [target_speed] * len(cx)

    direction = 1.0

    # Set stop point
    for i in range(len(cx) - 1):
        dyaw = abs(cyaw[i + 1] - cyaw[i])
        switch = math.pi / 4.0 <= dyaw < math.pi / 2.0

        if switch:
            direction *= -1

        if direction != 1.0:
            speed_profile[i] = - target_speed
        else:
            speed_profile[i] = target_speed

        if switch:
            speed_profile[i] = 0.0

    speed_profile[-1] = 0.0

    return speed_profile
\[\begin{aligned} \Delta \theta_i &= |\theta_{i+1} - \theta_i| \\ \text{switch} &= \left( \frac{\pi}{4} \leq \Delta \theta_i < \frac{\pi}{2} \right) \end{aligned}\]

$\Delta \theta$가 45도 이상 90도 미만일 경우, 차량의 방향을 바꾼다.

메인 loop

def closed_loop_prediction(cx, cy, cyaw, ck, speed_profile, goal):
    T = 500.0  # max simulation time
    goal_dis = 0.3
    stop_speed = 0.05

    state = State(x=-0.0, y=-0.0, yaw=0.0, v=0.0)

    time = 0.0
    x = [state.x]
    y = [state.y]
    yaw = [state.yaw]
    v = [state.v]
    t = [0.0]

    e, e_th = 0.0, 0.0

    while T >= time:
        dl, target_ind, e, e_th = lqr_steering_control(
            state, cx, cy, cyaw, ck, e, e_th)

        ai = pid_control(speed_profile[target_ind], state.v)
        state = update(state, ai, dl)

        if abs(state.v) <= stop_speed:
            target_ind += 1

        time = time + dt

        # check goal
        dx = state.x - goal[0]
        dy = state.y - goal[1]
        if math.hypot(dx, dy) <= goal_dis:
            print("Goal")
            break

        x.append(state.x)
        y.append(state.y)
        yaw.append(state.yaw)
        v.append(state.v)
        t.append(time)

    return t, x, y, yaw, v

LQR 제어

def solve_DARE(A, B, Q, R):
    """
    solve a discrete time_Algebraic Riccati equation (DARE)
    """
    X = Q
    Xn = Q
    max_iter = 150
    eps = 0.01

    for i in range(max_iter):
        Xn = A.T @ X @ A - A.T @ X @ B @ \
            la.inv(R + B.T @ X @ B) @ B.T @ X @ A + Q
        if (abs(Xn - X)).max() < eps:
            break
        X = Xn

    return Xn

def dlqr(A, B, Q, R):
    """Solve the discrete time lqr controller.
    x[k+1] = A x[k] + B u[k]
    cost = sum x[k].T*Q*x[k] + u[k].T*R*u[k]
    # ref Bertsekas, p.151
    """

    # first, try to solve the ricatti equation
    X = solve_DARE(A, B, Q, R)

    # compute the LQR gain
    K = la.inv(B.T @ X @ B + R) @ (B.T @ X @ A)

    eigVals, eigVecs = la.eig(A - B @ K)

    return K, X, eigVals

def lqr_steering_control(state, cx, cy, cyaw, ck, pe, pth_e):
    ind, e = calc_nearest_index(state, cx, cy, cyaw)

    k = ck[ind]                                        # 해당 점에서의 곡률
    v = state.v
    th_e = pi_2_pi(state.yaw - cyaw[ind])              # 방향 오차

    A = np.zeros((4, 4))
    A[0, 0] = 1.0
    A[0, 1] = dt
    A[1, 2] = v
    A[2, 2] = 1.0
    A[2, 3] = dt
    # print(A)

    B = np.zeros((4, 1))
    B[3, 0] = v / L

    K, _, _ = dlqr(A, B, Q, R)

    x = np.zeros((4, 1))

    x[0, 0] = e
    x[1, 0] = (e - pe) / dt
    x[2, 0] = th_e
    x[3, 0] = (th_e - pth_e) / dt

    ff = math.atan2(L * k, 1)
    fb = pi_2_pi((-K @ x)[0, 0])

    delta = ff + fb

    return delta, ind, e, th_e

시스템 방정식

\[x_{t+1} = A x_t + B u\]

목표는 최적의 제어입력$u$을 찾는 것이다.

상태 방정식

\[x = \begin{bmatrix} e \\ \dot{e} \\ \theta_e \\ \dot{\theta}_e \end{bmatrix}\]
  • $e$: 횡방향 오차
  • $\theta_e$: 방향 오차
  • $\dot{e}, \dot{\theta}_e$: 각각의 변화율
\[A = \begin{bmatrix} 1 & dt & 0 & 0 \\ 0 & 0 & v & 0 \\ 0 & 0 & 1 & dt \\ 0 & 0 & 0 & 0 \end{bmatrix}, \quad B = \begin{bmatrix} 0 \\ 0 \\ 0 \\ \frac{v}{L} \end{bmatrix}\]

비용함수(Cost Function)

관련 글 해당 게시글 참고

\[J = \sum_{k=0}^{\infty} \left( x[k]^T Q x[k] + u[k]^T R u[k] \right)\]
  • $Q$: 상태$x$ 에 대한 가중치 행렬 (상태를 0으로 수렴시키기 위한 비용)
  • $R$: 입력 $u$ 에 대한 가중치 행렬 (너무 큰 제어 입력을 방지)
  • 해당 cost function의 형태를 보았을떄, Infinite Horizon LQR 형태임을 알수 있다.(terminal cost function이 없다.)

공식 구현에서는 상태 가중치 $Q$는 4×4 identity matrix, 입력 가중치 $R$은 1×1 identity matrix를 사용한다.

cost function이 최소화 될때의 입력이 최적입력임을 알 수 있는데, 이때, ricatti equation을 사용한다.

ricatti equation

관련 글 해당 게시글 참고

\[P = A^T P A - A^T P B (R + B^T P B)^{-1} B^T P A + Q\]

Riccati equation을 반복적으로 풀어, 최적 비용을 나타내는 행렬 $P$를 구한다.

위처럼 $1/2$ 없이 정의한 비용에서, 고정된 선형 모델의 안정화 최적해 $P$를 사용하면 최적 비용은 $J^\star(x_0) = x_0^T P x_0$로 표현된다. $P$ 자체를 최소화하는 것이 아니라, 제어 입력에 대해 최소화한 비용을 $P$로 표현하는 것이다.

코드의 수치적 종료 조건은 $\max_{i,j}\lvert (P_{n+1} - P_n)_{ij}\rvert < \epsilon$이며, $\epsilon = 0.01$이다. 최대 150번 반복하므로, 종료 조건을 만족하지 않아도 마지막 행렬을 반환할 수 있다.

P를 구한뒤 최적이득 $K$를 계산한다.

\[K = (B^T P B + R)^{-1} B^T P A\]

이 코드에서 최적입력은 $u = -Kx$이다.

또한 고정된 이산시간 선형 모델에서는 폐루프 행렬 $A - BK$의 모든 고유값이 $\lvert \lambda_i(A - BK)\rvert < 1$을 만족하면 상태가 원점으로 수렴한다. 이 예제처럼 속도에 따라 모델과 이득이 변하고 조향각 제한이 적용되는 경우, 각 시점의 고유값 확인만으로 전체 경로 추종의 안정성이 보장되지는 않는다.

관련 글 해당 게시글 참고

feed forward 조향각 + feedback 조향각

이상치의 feed forward 조향각(차량이 이상적으로 경로를 쫓아갈때의 필요한 조향)과 feeback조향각(현재 차량이 경로에서 벗어나는지 반영하여 보정하는 조향각)을 더하여, 다음 목표 조향각을 구한다.

feed forward 조향각 : $\delta_{\text{ff}} = \tan^{-1}(L \cdot \kappa)$

feedback 조향각 : $\delta_{\text{fb}} = -Kx$

코드에서는 feedback 조향각을 pi_2_pi로 정규화한 뒤, $\delta = \delta_{\text{ff}} + \delta_{\text{fb}}$로 합친다.

P 제어

ai = pid_control(speed_profile[target_ind], state.v)

def pid_control(target, current):
    a = Kp * (target - current)

    return a

앞서 구했던 목표 profile 속도를 사용하여, P제어를 진행한다.

상태 업데이트

state = update(state, ai, dl)

def update(state, a, delta):

    if delta >= max_steer:
        delta = max_steer
    if delta <= - max_steer:
        delta = - max_steer

    state.x = state.x + state.v * math.cos(state.yaw) * dt
    state.y = state.y + state.v * math.sin(state.yaw) * dt
    state.yaw = state.yaw + state.v / L * math.tan(delta) * dt
    state.v = state.v + a * dt

    return state

수식으로 표현하면 다음과 같다.

\[\begin{aligned} \delta &= \text{clip}(\delta, -\delta_{\max}, +\delta_{\max}) \\ x_{t+1} &= x_t + v \cdot \cos(\theta) \cdot dt \\ y_{t+1} &= y_t + v \cdot \sin(\theta) \cdot dt \\ \theta_{t+1} &= \theta_t + \frac{v}{L} \cdot \tan(\delta) \cdot dt \\ v_{t+1} &= v_t + a \cdot dt \end{aligned}\]
  • $\delta$: 조향각

과거 상태와 가속도, 조향각을 사용하여 상태를 업데이트한다.

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

Infinite Horizon LQR의 DARE를 수치적으로 근사하고, 계산된 $P$로 $K$를 구한다.

이 예제는 매 제어 시점의 속도 $v$로 $A$, $B$를 만들고 $K$를 다시 계산한다.

이 예제는 목표와의 거리가 goal_dis = 0.3 이하이면 종료한다. 목표 도달 정밀도를 검토할 때는 이 종료 조건, 속도 제어, 조향각 제한도 함께 확인해야 한다.

Reference

PythonRobotics LQR steering control 공식 구현

Python Control Systems Library: discrete-time LQR

Stanford EE263: Stability of discrete-time systems

https://github.com/AtsushiSakai/PythonRobotics?tab=readme-ov-file#linearquadratic-regulator-lqr-speed-and-steering-control

GitHub - AtsushiSakai/PythonRobotics: Python sample codes and textbook for robotics algorithms. Python sample codes and textbook for robotics algorithms. - AtsushiSakai/PythonRobotics github.com

https://blog.naver.com/deepfahren/221166929497

[차량동역학] 차량 저속 선회와 애커만 기하학 모델 (Vehicle Low Speed Cornering and Ackerman Geometry Model) ■ 저속 선회 차량의 선회(Cornering)를 모델링 하기 위해서 우선 저속(Low-speed)에서의 선회 특성을 살… blog.naver.com