시뮬레이션 설정 방법
1) 초기값 지정(Initial State)
초기값 지정 구간에서는 시뮬레이션에 참여할 무인기의 대수를 지정하고 각 무인기의 초기 상태를 정의한다. 무인기의 부호 규약 및 좌표계는 아래와 같으며 상태는 [N, E, \(\psi\), \(V_{g}\)] 로 정의하도록 한다.
- 좌표계는 NED좌표계를 사용하며 단의는 [m] 이다.
- 헤딩은 북쪽 (y축 +방향)을 기준 시계방향으로 +부호를 가진다. 단위는 [rad]이다.
- 속도 V는 [m/s]로 정의한다.
# =========================
# Initial State
# =========================
num_uav = 4
x0_list = [
np.array([-800.0, -400.0, np.deg2rad( 20.0), 40.0]), # UAV0
np.array([-500.0, 200.0, np.deg2rad(-10.0), 40.0]), # UAV1
np.array([ 300.0, -700.0, np.deg2rad( 45.0), 40.0]), # UAV2
np.array([ 300.0, -700.0, np.deg2rad( 45.0), 40.0]), # UAV3
]
2) 미션 기획(항로점/경로 설정, Mission Planning)
다음은 무인기가 비행할 항로점을 정의하기 위한 구간이다. 항로점은 튜플 형태로 3가지의 항로점 속성 중에서 선택하고 이를 유지할 시간(duration)을 지정하여 정의하도록 되며 비행 임무는 이 항로점 튜플들의 시퀀스로 구성한다. 즉, 각 항로점의 속성을 참고하여 중괄호 안에 나열하는 식으로 항로점의 종류와 순서를 지정할 수 있다. 시뮬레이션을 시작하면 원과 장주선회 항로점 속성에는 duration 속성, 직선 항로점 속성에는 끝점과의 거리를 활용하여 항로점 임무 수행이 완료되었는지를 판단하고, 이를 기반으로 mission_planner_step함수에서 매 프레임마다 현재 미션의 prev / cur / next 항로점을 계산하여 Guidance부로 넘겨준다. 이렇게 prev / cur / next 항로점의 3점을 지정하는 이유는 무인기가 비행해야하는 기하를 정의하기 위함인데 prev와 cur 항로점을 활용하여 현재 경로의 기하를 정의할 수 있다. 즉, 두 점이 만드는 진행축이 곧 목표 헤딩(\(\psi_{d}\))과 크로스 트랙 오차(\(e_{y}\))의 기준이 된다. 또한 next 항로점을 추가하게 되면 코너에서 cur 항로점에서 next 항로점으로 넘어거는 코너에서 호를 따는 등 스무딩을 붙여 방향·곡률의 연속성을 확보하는데에 사용할 수 있다.
- HOLD: ("HOLD", np, ep radius, direction, duration_s)
중심 (np, ep)에서 반지름 radius로 선회
direction: +1=시계, -1=반시계
duration 초 동안 선회 비행 유지
- LINE: ("LINE", np, ep, mode)
직선 선회 비행의 끝점, 시작점은 prev 항로점의 위치로 지정
직선 비행 모드, FLY_OVER(직선의 끝점을 찍고 다음 항로점 진행)/FLY_BY(직선의 끝점 도달하기 전에 다음 항로점 진행)
- RACETRACK: ("RACETRACK", np, ep length, width, bearing_deg, direction, laps)
중심 (np, ep)을 기준으로 장주(달걀형) 선회
긴축 length, 짧은축 width, 축 방향 bearing_deg(도)
direction(±1), 수행 laps 회
# =========================
# Mission Planning
# =========================
# Description
# HOLD (n, e, radius, direction(+1/-1), duration[s])
# LINE (n, e, mode="FLY_OVER"/"FLY_BY")
# RACETRACK (n, e, length, width, bearing[deg], direction(+1/-1), laps)
m0 = [("HOLD", 0.0, 0.0, 400.0, -1, 30.0),
("LINE", 1200.0, 800.0, "FLY_OVER"),
("HOLD", 800.0, -600.0, 300.0, +1, 30.0),
("RACETRACK", 1500.0, -500.0, 250.0, 1800.0, -20.0, -1, 3),
("LINE", 400.0, -600.0, "FLY_BY")]
m1 = [("RACETRACK", 1500.0, -500.0, 250.0, 1800.0, -20.0, +1, 3),
("HOLD", 0.0, 0.0, 400.0, -1, 60.0),
("LINE", 1200.0, 800.0, "FLY_OVER"),
("HOLD", 400.0, -600.0, 300.0, +1, 120.0)]
m2 = [("HOLD", 800.0, -600.0, 300.0, +1, 120.0),
("LINE", 00.0, 00.0, "FLY_BY"),
("HOLD", 0.0, 0.0, 400.0, -1, 60.0),
("RACETRACK", 1500.0, -500.0, 250.0, 1800.0, -20.0, -1, 3),
("LINE", 1200.0, 800.0, "FLY_OVER"),]
m3 = [("HOLD", 0.0, 0.0, 400.0, -1, 60.0),
("RACETRACK", 1500.0, -500.0, 250.0, 1800.0, -20.0, +1, 3),
("LINE", 1200.0, 800.0, "FLY_OVER"),
("RACETRACK", 1500.0, -500.0, 250.0, 1800.0, -20.0, +1, 3),
("LINE", 400.0, -600.0, "FLY_BY"),
("HOLD", 400.0, -600.0, 300.0, +1, 120.0)]
missions = [m0, m1, m2, m3]
mission_states = [None for _ in range(num_uav)]
guidance_times = [0.0 for _ in range(num_uav)]
WP_save = [None for _ in range(num_uav)]
LEG_HIST = [[] for _ in range(num_uav)] # 프레임별 현재 레그 기록
PATH_BY_LEG = [{} for _ in range(num_uav)]
3) 유도기법 설정과 튜닝(Guidance Method)
이 부분에서 우리가 구현할 유도거법을 적용하고 시험하기 위해 조절해야하는 부분이 되겠다. 현재는 NLPF 그리고 TG 유도기법이 설계되어 있으며 추후에 유도기법의 구현이 완료되면서 추가될 예정이다. 지정된 유도기법에 앞서 지정된 항로점 3점을 정의하게 되면 각 유도기법에서 필요한 작업을 수행하게 된다. 또한 지정된 3 항로점을 저장하여 추후 가시화 작업에서 목표 경로를 도식하는데 활용한다.
- NLPF
> 유도기법 튜닝 파라미터 : \(L_{1}\) (크면 부드럽고 이탈 적음, 너무 크면 둔해짐)
> 항로점 3점을 활용하여 북-동 좌표(N, E)의 촘촘한 점의 경로점으로 재구성한다. 이 작업이 필요한 이유는 NLPF 유도가 경로상의 점들과 무인기의 위치에서 \(L_{1}\) 에 있는 점을 선행 목표점으로 지정한다. 이후 이 선행 목표점과 속도벡터의 상대 각도를 구한 후 유도명령 \(\omega\)를 도출한다.
- TG
> 유도기법 :튜닝 파라미터: \(K_{p}\) , \(\kappa\)
> 항로점 3점을 활용하여 기하를 판정한 뒤, 그 기하 위에서 추종 목표점과 목표 헤딩(\(\psi_{d}\))을 정하고, 그에 따른 크로스 트랙 오차(\(e_{y}\))를 계산해유도명령 \(\omega\)를 도출한다.
# =========================
# Guidance Method
# =========================
def Guidance_Method_0(state, idx=0):
(triplet, meta), mission_states[idx] = mission_planner_step(t=guidance_times[idx],pos_NE=state[:2],psi=state[2],mission_state=mission_states[idx],mission_spec=missions[idx])
omega = TG_Guidance(state, triplet, Kp=0.0002, K=100)
guidance_times[idx] += ctrl_dt
leg = int(meta["leg_idx"]); LEG_HIST[idx].append(leg)
if leg not in PATH_BY_LEG[idx]:
P, _ = build_viz_path_from_triplet(triplet, state=state, num_points=600)
PATH_BY_LEG[idx][leg] = P
return float(np.clip(omega, -1.0, 1.0))
def Guidance_Method_1(state, idx=1):
(triplet, meta), mission_states[idx] = mission_planner_step(t=guidance_times[idx],pos_NE=state[:2],psi=state[2],mission_state=mission_states[idx],mission_spec=missions[idx])
omega = NLPF_Guidance(state, triplet, L1=200.0, num_points=600)
guidance_times[idx] += ctrl_dt
leg = int(meta["leg_idx"]); LEG_HIST[idx].append(leg)
if leg not in PATH_BY_LEG[idx]:
P, _ = build_viz_path_from_triplet(triplet, state=state, num_points=600)
PATH_BY_LEG[idx][leg] = P
return float(np.clip(omega, -1.0, 1.0))
def Guidance_Method_2(state, idx=2):
(triplet, meta), mission_states[idx] = mission_planner_step(t=guidance_times[idx],pos_NE=state[:2],psi=state[2],mission_state=mission_states[idx],mission_spec=missions[idx])
omega = TG_Guidance(state, triplet, Kp=0.0002, K=100)
guidance_times[idx] += ctrl_dt
leg = int(meta["leg_idx"]); LEG_HIST[idx].append(leg)
if leg not in PATH_BY_LEG[idx]:
P, _ = build_viz_path_from_triplet(triplet, state=state, num_points=600)
PATH_BY_LEG[idx][leg] = P
return float(np.clip(omega, -1.0, 1.0))
def Guidance_Method_3(state, idx=3):
(triplet, meta), mission_states[idx] = mission_planner_step(t=guidance_times[idx],pos_NE=state[:2],psi=state[2],mission_state=mission_states[idx],mission_spec=missions[idx])
omega = NLPF_Guidance(state, triplet, L1=200.0, num_points=600)
guidance_times[idx] += ctrl_dt
leg = int(meta["leg_idx"]); LEG_HIST[idx].append(leg)
if leg not in PATH_BY_LEG[idx]:
P, _ = build_viz_path_from_triplet(triplet, state=state, num_points=600)
PATH_BY_LEG[idx][leg] = P
return float(np.clip(omega, -1.0, 1.0))
4) 시뮬레이션 수행 및 가시화 (Simulation & Visualize)
시뮬레이션 & 가시화 구간은 시뮬레이션을 돌리고 가시화해 보여주는 파이프라인을 담당한다. 각 무인기는 지정된 유도기법을 ctrl_dt 주기로 호출해 선회율 유도명령을 만든다. 모델은 sim_dt 간격으로 수치적분하여 시뮬레이션을 수행한다. 그 결과 시간 T, 상태 X, 입력 U를 누적할 수 있다.
적분이 끝나면 각 무인기의 상태에 대한 시계열 정보와 각 시점에서 지정된 항로점 3점의 기록을 넘겨 애니메이션을 그린다. 경로는 점선으로, 기체는 헤딩을 향한 삼각형으로 표현한다. 꼬리는 tail_length_m 만큼 최근 이동 거리만 강조한다.
기본적으로 알고리즘을 수정할 필요가 없고, 표시 옵션만 조정한다.
성능·가독성은 아래의 옵션들을 활용해 조절하며 비디오 파일을 얻고 싶다면 상단에 save_video=True로 두면 영상 저장이 가능하도록 구현하였다.
- interval_ms: 프레임 간 지연(밀리초)이다. 값이 작을수록 재생 FPS가 높아 부드럽고, 크면 느리다. 일반 40~80 ms (≈ 12 – 25 FPS), 발표용은 30~50 ms.
- stride: 무인기 상태 시계열 정보을 stride 간격으로 건너뛰어 재생하는 샘플링 간격이다. 값이 클수록 프레임 수가 줄어 재생이 가벼워지고 진행이 빨라 보이지만 움직임이 덜 부드럽다. 긴 시뮬은 30~80, 디버그 활용시에는 1~10.
- blit: Matplotlib의 “바뀐 요소만 다시 그리는” 가속 모드이다. True면 CPU 사용이 크게 줄어 부드럽다. 특정 환경에서 깜빡임이 보이면 False로 끈다. 팁: 기본 True, 축 한계/배경이 자주 변하면 False.
- uav_scale: 화면에 그리는 기체 삼각형의 시각적 크기 배율이다. 값이 크면 멀리서도 기체가 눈에 잘 띄고, 작으면 경로가 깔끔하다. 물리 의미는 없다. 팁: 지도가 크면 2~4, 줌인 뷰면 1~2.
- tail_length_m: 꼬리 선의 길이(미터) 이다. 최근 궤적만 보여주므로 현재 동작이 또렷하고, 너무 길면 화면이 지저분해진다. 0이면 꼬리 없음. 팁: 패턴 확인은 500~1500 m, 최소 표시면 100~300 m.
# =========================
# Simulation
# =========================
T_steps = int(total_time / sim_dt)
ctrl_chk = ctrl_dt
t = 0.0
X = [np.zeros((T_steps + 1, 4)) for _ in range(num_uav)]
U = [np.zeros((T_steps + 1, 2)) for _ in range(num_uav)]
for i in range(num_uav):
X[i][0] = np.asarray(x0_list[i], dtype=float)
U[i][0] = np.array([0.0, 0.0], float)
for k in range(T_steps):
if t > ctrl_chk - 1e-12:
omega0 = Guidance_Method_0(X[0][k]) # UAV1
U[0][k] = [np.clip(omega0, -1.0, 1.0), 0.0]
omega1 = Guidance_Method_1(X[1][k]) # UAV2
U[1][k] = [np.clip(omega1, -1.0, 1.0), 0.0]
omega2 = Guidance_Method_2(X[2][k]) # UAV3
U[2][k] = [np.clip(omega2, -1.0, 1.0), 0.0]
omega3 = Guidance_Method_3(X[3][k]) # UAV4
U[3][k] = [np.clip(omega3, -1.0, 1.0), 0.0]
ctrl_chk += ctrl_dt
for i in range(num_uav):
X[i][k+1] = integrate_step(unicycle_dynamics, X[i][k], U[i][k], sim_dt, method="rk4", t=t)
U[i][k+1] = U[i][k]
t += sim_dt
# =========================
# Visualize
# =========================
ani, fig = animate_simulation(
histories=X,
leg_index_histories=[np.asarray(h, dtype=int) for h in LEG_HIST],
path_by_leg_list=PATH_BY_LEG,
interval_ms=50, stride=60, tail_length_m=800.0,
uav_scale=3, blit=True, show=True)
if save_video == True:
ani.save("uav_simulation.mp4", writer=animation.FFMpegWriter(fps=180))
plt.show(block=False)
plt.close(fig)