DWA는 로봇의 동적 제약(속도, 가속도, 회전 속도 등)을 고려하여, 싥시간으로 안전하고 최적 제어 입력을 선택하는 로컬 플래닝 기법
모바일 로봇의 모션 플래닝을 위한 Dynamic Window approach (DWA)를 구현한 예제
import math
from enum import Enum
import matplotlib.pyplot as plt
import numpy as np
show_animation = True
# 현재 상태 x에서 장애물 정보 ob와 목표 goal을 고려하여, DWA를 통해 최적의 제어 입력과 예측 경로를 계산
def dwa_control(x, config, goal, ob):
# 현재 상태와 로봇의 제원에서 가능한 속도 및 회전 속도의 범위를 계산
dw = calc_dynamic_window(x, config)
# 계산된 동적 창 내에서 샘플링한 입력값들에 대해 경로를 예측하고, 각 경로의 비용을 계산한 후 최적의 입력 u와 해당 예측 경로 trajectory를 반환
u, trajectory = calc_control_and_trajectory(x, dw, config, goal, ob)
return u, trajectory
# 로봇의 형태를 원형 또는 직사각형으로 구분하기 위한 열거형(enum)
class RobotType(Enum):
# 충돌 시 로봇을 원으로 간주
circle = 0
# 직사각형 형태로 간주하며, 더 복잡한 충돌 체크 로직을 사용
rectangle = 1
# 로봇의 파라미터를 설정
class Config:
def __init__(self):
# robot parameter
self.max_speed = 1.0 # [m/s]
self.min_speed = -0.5 # [m/s]
self.max_yaw_rate = 40.0 * math.pi / 180.0 # [rad/s]
self.max_accel = 0.2 # [m/ss]
self.max_delta_yaw_rate = 40.0 * math.pi / 180.0 # [rad/ss]
self.v_resolution = 0.01 # [m/s]
self.yaw_rate_resolution = 0.1 * math.pi / 180.0 # [rad/s]
self.dt = 0.1 # [s] Time tick for motion prediction 시간 간격
self.predict_time = 3.0 # [s] 예측 시간
# 비용 gain : 목표 도달 비용, 속도 비용, 장애물 비용 등의 가중치
self.to_goal_cost_gain = 0.15
self.speed_cost_gain = 1.0
self.obstacle_cost_gain = 1.0
self.robot_stuck_flag_cons = 0.001 # constant to prevent robot stucked
self.robot_type = RobotType.circle
# if robot_type == RobotType.circle
# Also used to check if goal is reached in both types
self.robot_radius = 1.0 # [m] for collision check
# if robot_type == RobotType.rectangle
self.robot_width = 0.5 # [m] for collision check
self.robot_length = 1.2 # [m] for collision check
# obstacles [x(m) y(m), ....]
self.ob = np.array([[-1, -1],
[0, 2],
[4.0, 2.0],
[5.0, 4.0],
[5.0, 5.0],
[5.0, 6.0],
[5.0, 9.0],
[8.0, 9.0],
[7.0, 9.0],
[8.0, 10.0],
[9.0, 11.0],
[12.0, 13.0],
[12.0, 12.0],
[15.0, 15.0],
[13.0, 13.0]
])
@property
def robot_type(self):
return self._robot_type
@robot_type.setter
def robot_type(self, value):
if not isinstance(value, RobotType):
raise TypeError("robot_type must be an instance of RobotType")
self._robot_type = value
config = Config()
# 주어진 제어 입력 u = [v, omega] (전진 속도와 회전 속도)를 사용하여, 시간 dt 동안 로봇의 상태 x(위치, 방향, 속도 등)를 업데이트
def motion(x, u, dt):
x[2] += u[1] * dt
x[0] += u[0] * math.cos(x[2]) * dt
x[1] += u[0] * math.sin(x[2]) * dt
x[3] = u[0]
x[4] = u[1]
return x
# 로봇의 제원과 현재 속도를 기반으로 동적 창 (Dynamic Window)을 게산
def calc_dynamic_window(x, config):
# 제원 기반 동적 창 (로봇이 가질 수 있는 이론적인 속도 범위)
Vs = [config.min_speed, config.max_speed,
-config.max_yaw_rate, config.max_yaw_rate]
# 모션 모델 기반 동적 창 (현재 속도에서 가속/감속 가능한 범위를 계산)
Vd = [x[3] - config.max_accel * config.dt,
x[3] + config.max_accel * config.dt,
x[4] - config.max_delta_yaw_rate * config.dt,
x[4] + config.max_delta_yaw_rate * config.dt]
# 실제로 적용가능한 범위는, 두 창의 교집합
dw = [max(Vs[0], Vd[0]), min(Vs[1], Vd[1]),
max(Vs[2], Vd[2]), min(Vs[3], Vd[3])]
return dw
# 초기 상태 x_init에서 주어진 (v,y)에 따라 예측 시간을 두고 시뮬레이션을 수행, 해당 제어 입력에 대한 경로 trajectory를 생성
def predict_trajectory(x_init, v, y, config):
# 초기 상태를 복사하고, trajectory 배열에 저장
x = np.array(x_init)
trajectory = np.array(x)
# 예측 시간 동안 반복하면서 motion() 함수를 호출해 상태를 업데이트하고, 그 결과를 경로에 추가
time = 0
while time <= config.predict_time:
x = motion(x, [v, y], config.dt)
trajectory = np.vstack((trajectory, x))
time += config.dt
return trajectory
# 동적 창 dw 내에서 가능한 모든 제어 입력을 샘플링하여, 각 제어 입력에 따른 예측 경로를 계산한 후, 비용을 평가하여 최적의 제어 입력 선택
def calc_control_and_trajectory(x, dw, config, goal, ob):
x_init = x[:]
min_cost = float("inf") # 무한대로 초기화
# 최적 제어 입력과 최적 경로 초기화
best_u = [0.0, 0.0]
best_trajectory = np.array([x])
# 선형 속도 v와 회적 속도 y를 각각 동적 창의 범위 내에서 일정 해상도 v_resolution, yaw_rate_resolution으로 샘플링
for v in np.arange(dw[0], dw[1], config.v_resolution):
for y in np.arange(dw[2], dw[3], config.yaw_rate_resolution):
# 각 v,y 쌍에 대해 predict_trajectory를 호출하여 경로를 생성
trajectory = predict_trajectory(x_init, v, y, config)
# 비용 계산
to_goal_cost = config.to_goal_cost_gain * calc_to_goal_cost(trajectory, goal) # 목표 도달 비용
speed_cost = config.speed_cost_gain * (config.max_speed - trajectory[-1, 3]) # 속도 비용
ob_cost = config.obstacle_cost_gain * calc_obstacle_cost(trajectory, ob, config) # 장애물 비용
# 3개 비용의 합
final_cost = to_goal_cost + speed_cost + ob_cost
# 최적 입력 선택
# 계산된 비용이 현재 최소 비용보다 낮으면 해당 입력과 경로를 최적값으로 갱신
# 로봇이 정체(stuck)되지 않도록 작은 속도 일 때 회전 속도를 강제로 조정하는 조건 포함
if min_cost >= final_cost:
min_cost = final_cost
best_u = [v, y]
best_trajectory = trajectory
if abs(best_u[0]) < config.robot_stuck_flag_cons \
and abs(x[3]) < config.robot_stuck_flag_cons:
# to ensure the robot do not get stuck in
# best v=0 m/s (in front of an obstacle) and
# best omega=0 rad/s (heading to the goal with
# angle difference of 0)
best_u[1] = -config.max_delta_yaw_rate
# 최적 제어 입력과 예측 경로 반환
return best_u, best_trajectory
# 예측 경로가 장애물과 얼마나 가까워 충돌 위험이 있는지를 비용으로 평가
# 1. 장애물 좌표 추출 : 장애물 배열 ob에서 각 장애물의 x,y 좌표를 분리
# 2. 거리 계산 : 경로 상의 각 위치와 장애물 사리 거리 계산
# 3. 충돌 체크 : 로봇 형태에 따라 충돌 여부 검사 / 충돌 감지되면 무한대 비용 반환
# 4. 비용 산출 : 가장 가까운 장애물과의 거리에 대해 역수 비용으로 사용
def calc_obstacle_cost(trajectory, ob, config):
ox = ob[:, 0]
oy = ob[:, 1]
dx = trajectory[:, 0] - ox[:, None]
dy = trajectory[:, 1] - oy[:, None]
r = np.hypot(dx, dy)
if config.robot_type == RobotType.rectangle:
yaw = trajectory[:, 2]
rot = np.array([[np.cos(yaw), -np.sin(yaw)], [np.sin(yaw), np.cos(yaw)]])
rot = np.transpose(rot, [2, 0, 1])
local_ob = ob[:, None] - trajectory[:, 0:2]
local_ob = local_ob.reshape(-1, local_ob.shape[-1])
local_ob = np.array([local_ob @ x for x in rot])
local_ob = local_ob.reshape(-1, local_ob.shape[-1])
upper_check = local_ob[:, 0] <= config.robot_length / 2
right_check = local_ob[:, 1] <= config.robot_width / 2
bottom_check = local_ob[:, 0] >= -config.robot_length / 2
left_check = local_ob[:, 1] >= -config.robot_width / 2
if (np.logical_and(np.logical_and(upper_check, right_check),
np.logical_and(bottom_check, left_check))).any():
return float("Inf")
elif config.robot_type == RobotType.circle:
if np.array(r <= config.robot_radius).any():
return float("Inf")
min_r = np.min(r)
return 1.0 / min_r # OK
# 경로의 마지막 사태와 목표 지점 간의 방향 오차를 계산하여, 그 오차가 클수록 높은 비용을 부여
def calc_to_goal_cost(trajectory, goal):
dx = goal[0] - trajectory[-1, 0]
dy = goal[1] - trajectory[-1, 1]
# 목표와 마지막 위치 사이의 각도를 atan2를 사용해 계산
error_angle = math.atan2(dy, dx)
# 경로의 마지막 방향과 목표 방향 간의 차이를 구하고, 그 차이를 절대값으로 변환하여 반환
cost_angle = error_angle - trajectory[-1, 2]
cost = abs(math.atan2(math.sin(cost_angle), math.cos(cost_angle)))
return cost
# 시각화 함수 : 로봇의 현재 위치와 진행 방향을 화살표로 나타냄
def plot_arrow(x, y, yaw, length=0.5, width=0.1): # pragma: no cover
plt.arrow(x, y, length * math.cos(yaw), length * math.sin(yaw),
head_length=width, head_width=width)
plt.plot(x, y)
# 시각화 함수 : 로봇의형태에 따라 현재 위치를 시각화
def plot_robot(x, y, yaw, config): # pragma: no cover
if config.robot_type == RobotType.rectangle:
outline = np.array([[-config.robot_length / 2, config.robot_length / 2,
(config.robot_length / 2), -config.robot_length / 2,
-config.robot_length / 2],
[config.robot_width / 2, config.robot_width / 2,
- config.robot_width / 2, -config.robot_width / 2,
config.robot_width / 2]])
Rot1 = np.array([[math.cos(yaw), math.sin(yaw)],
[-math.sin(yaw), math.cos(yaw)]])
outline = (outline.T.dot(Rot1)).T
outline[0, :] += x
outline[1, :] += y
plt.plot(np.array(outline[0, :]).flatten(),
np.array(outline[1, :]).flatten(), "-k")
elif config.robot_type == RobotType.circle:
circle = plt.Circle((x, y), config.robot_radius, color="b")
plt.gcf().gca().add_artist(circle)
out_x, out_y = (np.array([x, y]) +
np.array([np.cos(yaw), np.sin(yaw)]) * config.robot_radius)
plt.plot([x, out_x], [y, out_y], "-k")
# 전체 시뮬레이션을 초기화하고 실행하는 main 함수
def main(gx=10.0, gy=10.0, robot_type=RobotType.circle):
print(__file__ + " start!!")
# initial state [x(m), y(m), yaw(rad), v(m/s), omega(rad/s)] -> 로봇의 초기 상태와 목표 위치 설정
x = np.array([0.0, 0.0, math.pi / 8.0, 0.0, 0.0])
# goal position [x(m), y(m)]
goal = np.array([gx, gy])
# input [forward speed, yaw_rate]
# 로봇의 형태를 설정하고, 장애물 정보를 가져옴
config.robot_type = robot_type
trajectory = np.array(x)
ob = config.ob
# 시뮬레이션 루프
# 반복문 내에서, dwa_control를 호출하여 최적 제어 입력과 에측 경로를 계산
# motion 함수를 사용해 로봇의 상태를 업데이트하고, 경로를 누적
# 각 반복마다 시각화를 업데이트하여 로봇, 경로, 장애물, 목표를 표시
while True:
u, predicted_trajectory = dwa_control(x, config, goal, ob)
x = motion(x, u, config.dt) # simulate robot
trajectory = np.vstack((trajectory, x)) # store state history
if show_animation:
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(predicted_trajectory[:, 0], predicted_trajectory[:, 1], "-g")
plt.plot(x[0], x[1], "xr")
plt.plot(goal[0], goal[1], "xb")
plt.plot(ob[:, 0], ob[:, 1], "ok")
plot_robot(x[0], x[1], x[2], config)
plot_arrow(x[0], x[1], x[2])
plt.axis("equal")
plt.grid(True)
plt.pause(0.0001)
# check reaching goal
dist_to_goal = math.hypot(x[0] - goal[0], x[1] - goal[1])
if dist_to_goal <= config.robot_radius:
print("Goal!!")
break
print("Done")
if show_animation:
plt.plot(trajectory[:, 0], trajectory[:, 1], "-r")
plt.pause(0.0001)
plt.show()
if __name__ == '__main__':
main(robot_type=RobotType.rectangle)
# main(robot_type=RobotType.circle)
Elevation Mapping Manual (1) | 2025.03.06 |
---|---|
WMP(World Model-bassed Perception for Visual Legged Locomotion) 리뷰 및 테스트 (2) | 2025.03.04 |
IsaacGYM - 22.04 설치 및 테스트 (0) | 2025.02.19 |
Isaac Gym & Isaac Lab 강화학습 관련 Survey (진행중) (1) | 2025.02.18 |
Isaac GYM 기반 Extreme Parkour with Legged Robots (0) | 2025.02.18 |