Skip to content

Commit 27183d1

Browse files
authored
Merge pull request #6 from movingpandas/ships
Ship movement model: simulates ships moving from origin to destination port, avoiding obstacles, using path planning.
2 parents 393f6a0 + 31e8206 commit 27183d1

27 files changed

+3986
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import numpy as np
2+
3+
4+
class Config:
5+
MAP_WIDTH = 150
6+
MAP_HEIGHT = 150
7+
RESOLUTION = 1.0
8+
OBSTACLE_THRESHOLD = 3
9+
10+
START_POINT = (10, 10)
11+
GOAL_POINT = (140, 140)
12+
13+
LOOKAHEAD = 3
14+
15+
MODEL_CONFIG = {
16+
"max_speed": 3.0,
17+
"min_speed": 0.1,
18+
"max_yaw_rate": np.deg2rad(15.0),
19+
"max_acceleration": 0.5,
20+
"max_delta_yaw_rate": np.deg2rad(15.0),
21+
"dt": 0.1,
22+
"predict_time": 1,
23+
"robot_radius": 1,
24+
"to_goal_cost_gain": 1,
25+
"speed_cost_gain": 1,
26+
"obstacle_cost_gain": 1.0,
27+
"turn_cost_gain": 1.0,
28+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
matplotlib==3.10.0
2+
numpy==2.2.2
3+
Shapely==2.0.7
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from util.simulation_v2_dynamic_obstacle_on_click import Simulation
2+
3+
4+
if __name__ == '__main__':
5+
sim = Simulation()
6+
7+
sim.run()
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import math
2+
from heapq import heappush, heappop
3+
import numpy as np
4+
from shapely import vectorized
5+
6+
7+
def create_occupancy_grid(map_width, map_height, resolution, obstacles, threshold=0):
8+
grid_width = int(map_width / resolution)
9+
grid_height = int(map_height / resolution)
10+
grid = np.zeros((grid_width, grid_height), dtype=int)
11+
12+
xs = (np.arange(grid_width) + 0.5) * resolution
13+
ys = (np.arange(grid_height) + 0.5) * resolution
14+
xx, yy = np.meshgrid(xs, ys, indexing='ij')
15+
16+
for obs in obstacles:
17+
if obs["type"] == "circle":
18+
shape = obs["circle"]
19+
else:
20+
shape = obs["polygon"]
21+
poly = shape.buffer(threshold)
22+
mask = vectorized.contains(poly, xx, yy)
23+
grid[mask] = 1
24+
25+
return grid
26+
27+
28+
def heuristic(a, b):
29+
return math.hypot(a[0] - b[0], a[1] - b[1])
30+
31+
32+
def reconstruct_path(came_from, current):
33+
path = [current]
34+
while current in came_from:
35+
current = came_from[current]
36+
path.append(current)
37+
return path[::-1]
38+
39+
40+
def astar(grid, start, goal):
41+
print(f'Grid shape = {grid.shape}')
42+
width, height = grid.shape
43+
open_set = []
44+
heappush(open_set, (0, start))
45+
came_from = {}
46+
g_score = {start: 0}
47+
f_score = {start: heuristic(start, goal)}
48+
49+
while open_set:
50+
current_f, current = heappop(open_set)
51+
if current == goal:
52+
return reconstruct_path(came_from, current)
53+
for dx in [-1, 0, 1]:
54+
for dy in [-1, 0, 1]:
55+
if dx == 0 and dy == 0:
56+
continue
57+
neighbor = (current[0] + dx, current[1] + dy)
58+
if 0 <= neighbor[0] < width and 0 <= neighbor[1] < height:
59+
if grid[neighbor[0], neighbor[1]] == 1:
60+
continue
61+
tentative_g = g_score[current] + math.hypot(dx, dy)
62+
if neighbor not in g_score or tentative_g < g_score[neighbor]:
63+
came_from[neighbor] = current
64+
g_score[neighbor] = tentative_g
65+
f_score[neighbor] = tentative_g + heuristic(neighbor, goal)
66+
heappush(open_set, (f_score[neighbor], neighbor))
67+
return None
68+
69+
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import math
2+
import numpy as np
3+
from shapely.geometry import Point, Polygon
4+
5+
6+
def motion(state, v, w, dt):
7+
x, y, theta, _, _ = state
8+
# print(f'Current state x = {x} | y = {y} | theta = {theta} | v = {v} | w = {w} | dt = {dt}')
9+
10+
x += v * math.cos(theta) * dt
11+
y += v * math.sin(theta) * dt
12+
theta += w * dt
13+
14+
# print(f'NEW state x = {x} | y = {y} | theta = {theta} | v = {v} | w = {w} | dt = {dt}')
15+
# print(f'-' * 50)
16+
17+
return x, y, theta, v, w
18+
19+
20+
def calc_trajectory(state, v, w, config):
21+
trajectory = []
22+
t = 0.0
23+
new_state = state
24+
while t <= config["predict_time"]:
25+
new_state = motion(new_state, v, w, config["dt"])
26+
trajectory.append(new_state)
27+
t += config["dt"]
28+
29+
return trajectory
30+
31+
32+
def calc_to_goal_cost(trajectory, goal, cost_gain):
33+
x, y, _, _, _ = trajectory[-1]
34+
distance = math.hypot(goal[0] - x, goal[1] - y)
35+
return cost_gain * distance
36+
37+
38+
def calc_turn_cost(state, local_goal, cost_gain):
39+
x, y, theta, _, _ = state
40+
dx = local_goal[0] - x
41+
dy = local_goal[1] - y
42+
desired_theta = math.atan2(dy, dx)
43+
turn_angle = abs(desired_theta - theta)
44+
turn_angle = min(turn_angle, 2 * math.pi - turn_angle)
45+
return cost_gain * turn_angle
46+
47+
48+
def calc_speed_cost(v, config):
49+
return config["speed_cost_gain"] * (config["max_speed"] - v)
50+
51+
52+
def calc_obstacle_cost_v2(trajectory, obstacle_tree, buffered_obstacles, config):
53+
min_distance = float("inf")
54+
for state in trajectory:
55+
x, y, _, _, _ = state
56+
point = Point(x, y)
57+
58+
nearby_indices = obstacle_tree.query(point)
59+
for idx in nearby_indices:
60+
buffered_obs = buffered_obstacles[idx]
61+
if buffered_obs.contains(point):
62+
return float("inf")
63+
distance = buffered_obs.distance(point)
64+
if distance < min_distance:
65+
min_distance = distance
66+
return config["obstacle_cost_gain"] / (min_distance + 1e-6)
67+
68+
69+
def calc_obstacle_cost(trajectory, buffered_obstacles, config):
70+
min_distance = float("inf")
71+
for state in trajectory:
72+
x, y, _, _, _ = state
73+
point = Point(x, y)
74+
for buffered_obs in buffered_obstacles:
75+
if buffered_obs.contains(point):
76+
return float("inf")
77+
distance = buffered_obs.distance(point)
78+
if distance < min_distance:
79+
min_distance = distance
80+
return config["obstacle_cost_gain"] / (min_distance + 1e-6)
81+
82+
83+
def calc_combined_turn_speed_cost(state, local_goal, v, cost_gain):
84+
x, y, theta, _, _ = state
85+
dx = local_goal[0] - x
86+
dy = local_goal[1] - y
87+
desired_theta = math.atan2(dy, dx)
88+
turn_angle = abs(desired_theta - theta)
89+
turn_angle = min(turn_angle, 2 * math.pi - turn_angle)
90+
# If the robot is moving fast and needs to turn a lot, the cost increases.
91+
return cost_gain * turn_angle * v
92+
93+
94+
def dwa_control(state, config, buffered_obstacles, local_goal):
95+
best_cost = float("inf")
96+
best_control = (0.0, 0.0)
97+
best_trajectory = []
98+
best_cost_info = {}
99+
100+
v_min = max(config["min_speed"], state[3] - config["max_acceleration"] * config["dt"])
101+
v_max = min(config["max_speed"], state[3] + config["max_acceleration"] * config["dt"])
102+
w_min = -config["max_yaw_rate"]
103+
w_max = config["max_yaw_rate"]
104+
105+
v_samples = np.linspace(v_min, v_max, num=5)
106+
w_samples = np.linspace(w_min, w_max, num=5)
107+
108+
for v in v_samples:
109+
for w in w_samples:
110+
trajectory = calc_trajectory(state, v, w, config)
111+
to_goal_cost = calc_to_goal_cost(trajectory, local_goal, config["to_goal_cost_gain"])
112+
speed_cost = calc_speed_cost(v, config)
113+
obstacle_cost = calc_obstacle_cost(trajectory, buffered_obstacles, config)
114+
turn_cost = calc_turn_cost(state, local_goal, config["turn_cost_gain"])
115+
116+
total_cost = to_goal_cost + speed_cost + obstacle_cost + turn_cost
117+
if total_cost < best_cost:
118+
best_cost = total_cost
119+
best_control = (v, w)
120+
best_trajectory = trajectory
121+
best_cost_info = {
122+
"total_cost": total_cost,
123+
"to_goal_cost": to_goal_cost,
124+
"speed_cost": speed_cost,
125+
"obstacle_cost": obstacle_cost,
126+
"turn_cost": turn_cost
127+
}
128+
return best_control, best_trajectory, best_cost_info
129+
130+
131+
def dwa_control_v2(state, config, obstacle_tree, buffered_obstacles, local_goal):
132+
best_cost = float("inf")
133+
best_control = (0.0, 0.0)
134+
best_trajectory = []
135+
best_cost_info = {}
136+
137+
# v_min = max(config["min_speed"], state[3] - config["max_acceleration"] * config["dt"])
138+
# v_max = min(config["max_speed"], state[3] + config["max_acceleration"] * config["dt"])
139+
140+
v_lower = config["min_speed"]
141+
v_upper = min(config["max_speed"], state[3] + config["max_acceleration"] * config["dt"])
142+
v_samples = np.linspace(v_lower, v_upper, num=5)
143+
144+
w_min = -config["max_yaw_rate"]
145+
w_max = config["max_yaw_rate"]
146+
147+
# v_samples = np.linspace(v_min, v_max, num=5)
148+
w_samples = np.linspace(w_min, w_max, num=5)
149+
150+
for v in v_samples:
151+
for w in w_samples:
152+
trajectory = calc_trajectory(state, v, w, config)
153+
to_goal_cost = calc_to_goal_cost(trajectory, local_goal, config["to_goal_cost_gain"])
154+
speed_cost = calc_speed_cost(v, config)
155+
obstacle_cost = calc_obstacle_cost_v2(trajectory, obstacle_tree, buffered_obstacles, config)
156+
# turn_cost = calc_turn_cost(state, local_goal, config["turn_cost_gain"])
157+
turn_cost = calc_combined_turn_speed_cost(state, local_goal, v, config["turn_cost_gain"])
158+
total_cost = to_goal_cost + speed_cost + obstacle_cost + turn_cost
159+
if total_cost < best_cost:
160+
best_cost = total_cost
161+
best_control = (v, w)
162+
best_trajectory = trajectory
163+
best_cost_info = {
164+
"total_cost": total_cost,
165+
"to_goal_cost": to_goal_cost,
166+
"speed_cost": speed_cost,
167+
"obstacle_cost": obstacle_cost,
168+
"turn_cost": turn_cost
169+
}
170+
return best_control, best_trajectory, best_cost_info

0 commit comments

Comments
 (0)