Ebook Robotics · RL–UAV
MODULE 2 — Motion Planning (Lecture 6–10)
Bản ôn nhanh theo module
Chế độ Slide · phím ← → · F toàn màn · Esc về đọc · S mở slide
Bản ôn nhanh theo module
📚 Học sâu:
Lectures/Lec06–10. File này = ôn nhanh.
C-space → BFS → A*(cost/gió) → RRT → min-snap → MPC re-plan "vẽ được" ─────────────────────────────► "bay được"
Mục tiêu module.
Robot đã ổn định (Module 1). Module này trả lời: đi đường nào từ A đến B mà không đụng; nếu được thì đường tốt nhất; và đường đó có bay được dưới ràng buộc động lực học hay không.
Luồng module:
Lec 6 Lưới / đồ thị: BFS, DFS (+ ý tưởng C-space)
Lec 7 Đường tối ưu rời rạc: Dijkstra, A*
Lec 8 Không gian liên tục: RRT (và họ hàng)
Lec 9 Differential flatness + min-snap (đặc thù quadrotor)
Lec 10 Kinodynamic planning & MPC
Giải thích C-space; chạy được BFS; hiểu vì sao lưới hóa thất bại ở chiều cao (curse of dimensionality).
Nếu thế giới rời rạc hóa được thành đồ thị (ô lưới, waypoint), tìm đường đi
từ start đến goal thế nào cho có hệ thống?
Configuration space (C-space) — ý tưởng nền của cả module: thay vì nghĩ
“robot hình dạng phức tạp di chuyển giữa vật cản”, ta nghĩ “một điểm di
chuyển trong không gian cấu hình”, nơi vật cản đã được “phồng” lên đúng bằng
kích thước robot. Drone bán kính \(r\) tránh cột bán kính \(R\) ⇔ điểm tránh
đĩa bán kính \(R + r\). Từ đây mọi thuật toán chỉ làm việc với điểm.
BFS = loang như vết dầu: từ start lan đều từng lớp, lớp nào chạm goal thì
xong. DFS = chuột chui hang: lao sâu một hướng đến cùng, cụt thì lùi.
visited — quên nó là lặp vô hạn. bước 0 bước 3 hoàn tất (số = thứ tự lớp)
S . . . # S 1 2 3 # S 1 2 3 #
. . . . . 1 2 3 . . 1 2 3 4 5
. # # . . → 2 # # . . → 2 # # 5 6
. . . . . 3 . . . . 3 4 5 6 7
. . . G . . . . G . 4 5 6 7 G ← đường: lần ngược 8→0
from collections import deque
grid = ["S...#",
".....",
".##..",
".....",
"...G."]
R, C = len(grid), len(grid[0])
start = goal = None
for r in range(R):
for c in range(C):
if grid[r][c] == "S": start = (r, c)
if grid[r][c] == "G": goal = (r, c)
parent, q = {start: None}, deque([start])
while q:
cur = q.popleft()
if cur == goal: break
for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
nxt = (cur[0]+dr, cur[1]+dc)
if (0 <= nxt[0] < R and 0 <= nxt[1] < C
and grid[nxt[0]][nxt[1]] != "#" and nxt not in parent):
parent[nxt] = cur; q.append(nxt)
path, node = [], goal
while node: path.append(node); node = parent[node]
print("Đường đi:", path[::-1])
Phân biệt Dijkstra và A*; định nghĩa heuristic admissible/consistent; liên hệ \(h\) với value function.
Cạnh có giá khác nhau (xa hơn, nguy hiểm hơn, tốn pin hơn) — tìm đường rẻ nhất
thế nào, và tăng tốc bằng gợi ý (heuristic) ra sao?
Dijkstra mở rộng: A* mở rộng:
· · · · ·
· · · · · · · · · ·
· · · S · · · · S · · · · · · · ► G
· · · · · · · · · ·
· · · · · ► G
(tròn đều, tốn) (dồn về goal, tiết kiệm)
import heapq
cost = [[1,1,1,9,1], # 9 = vùng "đắt" (gió mạnh/cấm bay mềm)
[1,9,1,9,1],
[1,9,1,1,1],
[1,1,1,9,1],
[9,9,1,1,1]]
R, C = 5, 5
start, goal = (0,0), (4,4)
h = lambda n: abs(n[0]-goal[0]) + abs(n[1]-goal[1]) # Manhattan: admissible
g = {start: 0}; parent = {start: None}
pq = [(h(start), start)]
while pq:
f, cur = heapq.heappop(pq)
if cur == goal: break
for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
nb = (cur[0]+dr, cur[1]+dc)
if 0 <= nb[0] < R and 0 <= nb[1] < C:
ng = g[cur] + cost[nb[0]][nb[1]]
if ng < g.get(nb, 1e18):
g[nb], parent[nb] = ng, cur
heapq.heappush(pq, (ng + h(nb), nb))
path, n = [], goal
while n: path.append(n); n = parent[n]
print("Giá tối ưu:", g[goal]); print("Đường:", path[::-1])
Thử đổi vài ô 9 → 1 và xem đường “đổi ý” — cảm giác này chính là cost shaping.
Thuật lại được vòng lặp RRT; phân biệt probabilistic completeness và optimality; biết RRT* / smoothing dùng khi nào.
Không gian liên tục, nhiều chiều, không lưới hóa nổi — làm sao vẫn tìm được
đường? Trả lời: lấy mẫu ngẫu nhiên.
RRT (Rapidly-exploring Random Tree) mọc cây từ start: mỗi vòng ném một điểm
ngẫu nhiên vào không gian, tìm nút cây gần nó nhất, rồi mọc một đoạn ngắn từ nút
đó về phía điểm ngẫu nhiên (nếu không đụng vật cản). Điểm ngẫu nhiên là “mồi
nhử” kéo cây tỏa ra vùng chưa khám phá — vùng trống càng lớn càng dễ “câu” được
mẫu, nên cây tự động ưu tiên loang vào nơi rộng rãi. Thỉnh thoảng ném mồi ngay
tại goal (goal bias ~5–10%) để cây có xu hướng ngoặt về đích.
Pseudo-code (thuộc lòng được):
T.init(x_start)
for i = 1..N:
x_rand ← sample ngẫu nhiên (thỉnh thoảng = x_goal)
x_near ← nút trong T gần x_rand nhất
x_new ← x_near + η · hướng(x_near → x_rand) # bước dài η
if đoạn [x_near, x_new] không đụng vật cản:
T.add(x_new, cha = x_near)
if ‖x_new − x_goal‖ < ε: return đường (lần ngược cha)
Tính chất cần nhớ:
- Probabilistically complete: có đường thì xác suất tìm ra → 1 khi \(N \to \infty\).
- Đường tìm được không tối ưu — gấp khúc, vòng vèo. Hai cách vá:
(a) shortcut smoothing: lặp lại việc chọn 2 điểm trên đường, nối thẳng nếu
không đụng; (b) RRT*: khi thêm nút, xét lại “cha rẻ hơn” trong lân cận và
rewire — tiệm cận tối ưu khi mẫu → ∞, trả giá bằng tính toán.
- Họ hàng: RRT-Connect (hai cây từ start và goal mọc vào nhau — nhanh trong thực
tế), PRM (xây roadmap dùng lại cho nhiều truy vấn).
- Chi phí thật nằm ở collision check (~90% thời gian chạy).
S──┬──● ####
│ └──● #### ● = nút cây
● \ ####
│ ●──●
└──● \
\ ●──●──● G
● (cây vòng qua mép vật cản rồi chạm goal)
import numpy as np
rng = np.random.default_rng(0)
obstacles = [(5, 5, 1.6), (7, 2, 1.2), (3, 8, 1.4)] # (cx, cy, bán kính)
def free(p):
return all(np.hypot(p[0]-cx, p[1]-cy) > r for cx, cy, r in obstacles)
def seg_free(a, b, k=20):
return all(free(a + t*(b-a)) for t in np.linspace(0, 1, k))
start, goal = np.array([0.5, 0.5]), np.array([9.5, 9.5])
nodes, parent, eta = [start], {0: None}, 0.6
for i in range(4000):
x_rand = goal if rng.random() < 0.08 else rng.uniform(0, 10, 2)
j = int(np.argmin([np.linalg.norm(n - x_rand) for n in nodes]))
d = x_rand - nodes[j]
x_new = nodes[j] + eta * d / (np.linalg.norm(d) + 1e-9)
if free(x_new) and seg_free(nodes[j], x_new):
nodes.append(x_new); parent[len(nodes)-1] = j
if np.linalg.norm(x_new - goal) < 0.5:
k, path = len(nodes)-1, []
while k is not None: path.append(nodes[k]); k = parent[k]
print(f"Tìm thấy sau {i} vòng, đường {len(path)} nút,",
f"dài ~{sum(np.linalg.norm(path[t]-path[t+1]) for t in range(len(path)-1)):.1f}")
break
Lab 2 của khóa đúng bài này: đo vị trí cột PVC, chạy RRT, nạp waypoint cho
Crazyflie bay theo.
eta và goal bias ảnh hưởng ra sao? (chạy thử)Phát biểu flatness của quadrotor; giải thích vì sao plan trong \((x,y,z,\psi)\); nắm pipeline RRT → min-snap → tracking.
Quadrotor 12 state — plan quỹ đạo trong không gian nào cho khả thi mà không
phải giải bài toán 12 chiều?
Phát hiện đẹp nhất về quadrotor: nó là hệ differentially flat với flat
output \((x, y, z, \psi)\) (vị trí + yaw). Nghĩa là: chỉ cần vẽ quỹ đạo vị trí
đủ mượt \( (x(t), y(t), z(t), \psi(t)) \), thì toàn bộ phần còn lại — góc
nghiêng, tốc độ góc, tổng lực đẩy — suy ra được bằng công thức đại số từ quỹ
đạo và các đạo hàm của nó. Không cần tích phân, không cần mò.
Ví dụ cảm nhận được: drone bay vòng tròn đều — nó phải nghiêng vào tâm một góc
xác định (như xe máy ôm cua). Góc đó không phải lựa chọn tự do: quỹ đạo đã quy
định nó. Flatness tổng quát hóa: quỹ đạo vị trí quy định mọi thứ.
Vì sao đúng (mức trực giác): gia tốc \(\ddot{\mathbf r}\) do quỹ đạo cho trước;
tổng lực = \(m(\ddot{\mathbf r} + g\mathbf e_3)\); mà lực đẩy luôn dọc trục thân
→ hướng trục thân phải trùng hướng vector này → attitude được suy ra. Đạo
hàm tiếp (jerk, snap) cho tốc độ góc và mô-men.
RRT waypoints: ●───●─────●───● (gấp khúc, bay xấu)
Min-snap: ●⌒⌒⌒●⌒⌒⌒⌒⌒●⌒⌒⌒● (mượt đến bậc snap, bay đẹp)
│
flatness map (đại số)
▼
θ(t), ω(t), F(t), M(t) ← lệnh điều khiển đầy đủ, feasible
import numpy as np
# Quỹ đạo phẳng: y(t) = sin(t), z(t) = 0.5 sin(2t); suy góc nghiêng cần có
m, g = 0.03, 9.81
for t in np.linspace(0, 3, 7):
ay = -np.sin(t) # ÿ
az = -2.0*np.sin(2*t) # z̈
# thân drone phải hướng theo vector lực cần thiết m(a + g e3)
theta = np.arctan2(-ay, az + g) # góc nghiêng suy ra
F = m*np.hypot(ay, az + g) # tổng lực đẩy suy ra
print(f"t={t:4.1f} θ={np.degrees(theta):6.2f}° F={F*1000:6.2f} mN")
Không giải ODE nào cả — attitude và thrust “rơi ra” từ đạo hàm quỹ đạo. Đó là flatness.
Phân biệt planning hình học và kinodynamic; nêu ba chiến lược chính; giải thích MPC như re-plan vòng kín.
Đường hình học không đụng vật cản nhưng robot không bay theo nổi (cua gắt
quá, cần gia tốc vượt giới hạn motor) — plan thế nào để đường sinh ra là đường
bay được?
Xe tải không đi được đường thiết kế cho xe đạp. Ba chiến lược:
MPC (Model Predictive Control) — cầu nối planning ↔ control, cần biết dù
khóa chỉ chạm nhẹ: mỗi bước điều khiển, giải trajectory optimization trên chân
trời ngắn (0.5–2s), thực thi chỉ bước đầu tiên, rồi giải lại từ trạng thái
đo mới. Tức là “replan liên tục ở tần số control” — chuẩn hiện nay cho drone
tránh vật cản tốc độ cao.
Decoupled: [RRT hình học] → [smooth+time] → [tracking ctrl]
Kinodynamic: [RRT trong state-space, cạnh = forward sim f(x,u)]
Trajectory opt: min Σc s.t. dynamics + limits + obstacle (NLP)
MPC: trajectory opt chân trời ngắn, lặp mỗi 10-50ms
import numpy as np
# Đường hình học: rẽ vuông góc tại t=1 với tốc độ không đổi 3 m/s
# Drone giới hạn gia tốc a_max = 5 m/s^2 — kiểm tra khả thi
v, dt = 3.0, 0.001
t_turn = 0.2 # cho phép 0.2s để đổi hướng 90°
# đổi hướng 90° ở tốc độ v trong t_turn cần gia tốc ngang:
a_need = v * (np.pi/2) / t_turn # ≈ v·Δψ/Δt
print(f"Cần a ≈ {a_need:.1f} m/s² > a_max = 5 → KHÔNG bay được đường này")
print(f"Muốn khả thi: hoặc chậm lại còn {5*t_turn/(np.pi/2):.2f} m/s,"
f" hoặc cua rộng bán kính ≥ {v*v/5:.2f} m")
Bài học: mọi “đường” đều ngầm chứa thời gian; khả thi hay không nằm ở
đạo hàm (vận tốc/gia tốc), không nằm ở hình vẽ.
BFS/A xây tư duy đồ thị và heuristic (= value).
RRT cho phép làm việc trong không gian liên tục nhiều chiều.
Flatness biến quadrotor thành hệ “plan được dễ” hơn vẻ ngoài 12 chiều.
Kinodynamic / MPC nhắc: đường phải bay được, không chỉ vẽ được*.
Bài tiếp: 03_Estimation_SLAM.md — bỏ giả định biết state; chỉ còn cảm biến nhiễu.