Skip to content

Commit 69d07fc

Browse files
authored
added pso algorithm (AtsushiSakai#1279)
* added pso algorithm also known as bird flocking algorithm * fix: Resolve linting issues in PSO implementation * improved code formattings * added docstring and added pso in main rst file * fixed paths * fixed changes requested * fixed typo * improved readilbility
1 parent 2d3cb83 commit 69d07fc

File tree

6 files changed

+520
-0
lines changed

6 files changed

+520
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ _build/
1515
# Distribution / packaging
1616
.Python
1717
env/
18+
venv/
1819
build/
1920
develop-eggs/
2021
dist/
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
"""
2+
Particle Swarm Optimization (PSO) Path Planning
3+
4+
author: Anish (@anishk85)
5+
6+
See Wikipedia article (https://en.wikipedia.org/wiki/Particle_swarm_optimization)
7+
8+
References:
9+
- Kennedy, J.; Eberhart, R. (1995). "Particle Swarm Optimization"
10+
- Shi, Y.; Eberhart, R. (1998). "A Modified Particle Swarm Optimizer"
11+
- https://machinelearningmastery.com/a-gentle-introduction-to-particle-swarm-optimization/
12+
13+
This implementation uses PSO to find collision-free paths by treating
14+
path planning as an optimization problem where particles explore the
15+
search space to minimize distance to target while avoiding obstacles.
16+
"""
17+
import numpy as np
18+
import matplotlib.pyplot as plt
19+
import matplotlib.animation as animation
20+
import matplotlib.patches as patches
21+
import signal
22+
import sys
23+
# Add show_animation flag for consistency with other planners
24+
show_animation = True
25+
26+
def signal_handler(sig, frame):
27+
print("\nExiting...")
28+
plt.close("all")
29+
sys.exit(0)
30+
31+
signal.signal(signal.SIGINT, signal_handler)
32+
33+
class Particle:
34+
"""Represents a single particle in the PSO swarm.
35+
Each particle maintains its current position, velocity, and personal best
36+
position discovered during the search. Particles explore the search space
37+
by updating their velocity based on personal experience (cognitive component)
38+
and swarm knowledge (social component).
39+
Attributes:
40+
search_bounds: List of tuples [(x_min, x_max), (y_min, y_max)] defining search space
41+
max_velocity: Maximum velocity allowed in each dimension (5% of search space range)
42+
position: Current 2D position [x, y] in search space
43+
velocity: Current velocity vector [vx, vy]
44+
personal_best_position: Personal best position found so far
45+
personal_best_value: Fitness value at personal best position
46+
path: List of all positions visited by this particle
47+
"""
48+
49+
def __init__(self, search_bounds, spawn_bounds):
50+
self.search_bounds = search_bounds
51+
self.max_velocity = np.array([(b[1] - b[0]) * 0.05 for b in search_bounds])
52+
self.position = np.array([np.random.uniform(b[0], b[1]) for b in spawn_bounds])
53+
self.velocity = np.random.randn(2) * 0.1
54+
self.personal_best_position = self.position.copy()
55+
self.personal_best_value = np.inf
56+
self.path = [self.position.copy()]
57+
58+
def update_velocity(self, gbest_pos, w, c1, c2):
59+
"""Update particle velocity using PSO equation:
60+
v = w*v + c1*r1*(personal_best - x) + c2*r2*(gbest - x)
61+
"""
62+
r1 = np.random.rand(2)
63+
r2 = np.random.rand(2)
64+
cognitive = c1 * r1 * (self.personal_best_position - self.position)
65+
social = c2 * r2 * (gbest_pos - self.position)
66+
self.velocity = w * self.velocity + cognitive + social
67+
self.velocity = np.clip(self.velocity, -self.max_velocity, self.max_velocity)
68+
69+
def update_position(self):
70+
self.position = self.position + self.velocity
71+
# Keep in bounds
72+
for i in range(2):
73+
self.position[i] = np.clip(
74+
self.position[i], self.search_bounds[i][0], self.search_bounds[i][1]
75+
)
76+
self.path.append(self.position.copy())
77+
78+
class PSOSwarm:
79+
80+
def __init__(
81+
self, n_particles, max_iter, target, search_bounds, spawn_bounds, obstacles
82+
):
83+
self.n_particles = n_particles
84+
self.max_iter = max_iter
85+
self.target = np.array(target)
86+
self.obstacles = obstacles
87+
self.search_bounds = search_bounds
88+
# PSO parameters
89+
self.w_start = 0.9 # Initial inertia weight
90+
self.w_end = 0.4 # Final inertia weight
91+
self.c1 = 1.5 # Cognitive coefficient
92+
self.c2 = 1.5 # Social coefficient
93+
# Initialize particles
94+
self.particles = [
95+
Particle(search_bounds, spawn_bounds) for _ in range(n_particles)
96+
]
97+
self.gbest_position = None
98+
self.gbest_value = np.inf
99+
self.gbest_path = []
100+
self.iteration = 0
101+
102+
def fitness(self, pos):
103+
"""Calculate fitness - distance to target + obstacle penalty"""
104+
dist = np.linalg.norm(pos - self.target)
105+
# Obstacle penalty
106+
penalty = 0
107+
for ox, oy, r in self.obstacles:
108+
obs_dist = np.linalg.norm(pos - np.array([ox, oy]))
109+
if obs_dist < r:
110+
penalty += 1000 # Inside obstacle
111+
elif obs_dist < r + 5:
112+
penalty += 50 / (obs_dist - r + 0.1) # Too close
113+
return dist + penalty
114+
115+
def check_collision(self, start, end, obstacle):
116+
"""Check if path from start to end hits obstacle using line-circle intersection
117+
Args:
118+
start: Starting position (numpy array)
119+
end: Ending position (numpy array)
120+
obstacle: Tuple (ox, oy, r) representing obstacle center and radius
121+
Returns:
122+
bool: True if collision detected, False otherwise
123+
"""
124+
ox, oy, r = obstacle
125+
center = np.array([ox, oy])
126+
# Vector math for line-circle intersection
127+
d = end - start
128+
f = start - center
129+
a = np.dot(d, d)
130+
# Guard against zero-length steps to prevent ZeroDivisionError
131+
if a < 1e-10: # Near-zero length step
132+
# Check if start point is inside obstacle
133+
return np.linalg.norm(f) <= r
134+
b = 2 * np.dot(f, d)
135+
c = np.dot(f, f) - r * r
136+
discriminant = b * b - 4 * a * c
137+
if discriminant < 0:
138+
return False
139+
# Check if intersection on segment
140+
sqrt_discriminant = np.sqrt(discriminant)
141+
t1 = (-b - sqrt_discriminant) / (2 * a)
142+
t2 = (-b + sqrt_discriminant) / (2 * a)
143+
return (0 <= t1 <= 1) or (0 <= t2 <= 1)
144+
145+
def step(self):
146+
"""Run one PSO iteration
147+
Returns:
148+
bool: True if algorithm should continue, False if completed
149+
"""
150+
if self.iteration >= self.max_iter:
151+
return False
152+
# Update inertia weight (linear decay)
153+
w = self.w_start - (self.w_start - self.w_end) * (
154+
self.iteration / self.max_iter
155+
)
156+
# Evaluate all particles
157+
for particle in self.particles:
158+
value = self.fitness(particle.position)
159+
# Update personal best
160+
if value < particle.personal_best_value:
161+
particle.personal_best_value = value
162+
particle.personal_best_position = particle.position.copy()
163+
# Update global best
164+
if value < self.gbest_value:
165+
self.gbest_value = value
166+
self.gbest_position = particle.position.copy()
167+
if self.gbest_position is not None:
168+
self.gbest_path.append(self.gbest_position.copy())
169+
# Update particles
170+
for particle in self.particles:
171+
particle.update_velocity(self.gbest_position, w, self.c1, self.c2)
172+
# Predict next position
173+
next_pos = particle.position + particle.velocity
174+
# Check collision
175+
collision = False
176+
for obs in self.obstacles:
177+
if self.check_collision(particle.position, next_pos, obs):
178+
collision = True
179+
break
180+
if collision:
181+
# Reduce velocity if collision detected
182+
particle.velocity *= 0.2
183+
particle.update_position()
184+
self.iteration += 1
185+
if show_animation and self.iteration % 20 == 0:
186+
print(
187+
f"Iteration {self.iteration}/{self.max_iter}, Best: {self.gbest_value:.2f}"
188+
)
189+
return True
190+
191+
def main():
192+
"""Run PSO path planning algorithm demonstration.
193+
This function demonstrates PSO-based path planning with the following setup:
194+
- 15 particles exploring a (-50,50) x (-50,50) search space
195+
- Start zone: (-45,-45) to (-35,-35)
196+
- Target: (40, 35)
197+
- 4 circular obstacles with collision avoidance
198+
- Real-time visualization showing particle convergence (if show_animation=True)
199+
- Headless mode support for testing (if show_animation=False)
200+
The algorithm runs for up to 150 iterations, displaying particle movement,
201+
personal/global best positions, and the evolving optimal path.
202+
"""
203+
print(__file__ + " start!!")
204+
# Set matplotlib backend for headless environments
205+
if not show_animation:
206+
plt.switch_backend("Agg") # Use non-GUI backend for testing
207+
# Setup parameters
208+
N_PARTICLES = 15
209+
MAX_ITER = 150
210+
SEARCH_BOUNDS = [(-50, 50), (-50, 50)]
211+
TARGET = [40, 35]
212+
SPAWN_AREA = [(-45, -35), (-45, -35)]
213+
OBSTACLES = [(10, 15, 8), (-20, 0, 12), (20, -25, 10), (-5, -30, 7)]
214+
swarm = PSOSwarm(
215+
n_particles=N_PARTICLES,
216+
max_iter=MAX_ITER,
217+
target=TARGET,
218+
search_bounds=SEARCH_BOUNDS,
219+
spawn_bounds=SPAWN_AREA,
220+
obstacles=OBSTACLES,
221+
)
222+
# pragma: no cover
223+
if show_animation:
224+
# Visualization setup
225+
signal.signal(signal.SIGINT, signal_handler)
226+
fig, ax = plt.subplots(figsize=(10, 10))
227+
ax.set_xlim(SEARCH_BOUNDS[0])
228+
ax.set_ylim(SEARCH_BOUNDS[1])
229+
ax.set_title("PSO Path Planning with Collision Avoidance", fontsize=14)
230+
ax.grid(True, alpha=0.3)
231+
# Draw obstacles
232+
for ox, oy, r in OBSTACLES:
233+
circle = patches.Circle((ox, oy), r, color="gray", alpha=0.7)
234+
ax.add_patch(circle)
235+
# Draw spawn area
236+
spawn_rect = patches.Rectangle(
237+
(SPAWN_AREA[0][0], SPAWN_AREA[1][0]),
238+
SPAWN_AREA[0][1] - SPAWN_AREA[0][0],
239+
SPAWN_AREA[1][1] - SPAWN_AREA[1][0],
240+
linewidth=2,
241+
edgecolor="green",
242+
facecolor="green",
243+
alpha=0.2,
244+
label="Start Zone",
245+
)
246+
ax.add_patch(spawn_rect)
247+
# Draw target
248+
ax.plot(TARGET[0], TARGET[1], "r*", markersize=20, label="Target")
249+
# Initialize plot elements
250+
particles_scatter = ax.scatter(
251+
[], [], c="blue", s=50, alpha=0.6, label="Particles"
252+
)
253+
gbest_scatter = ax.plot([], [], "yo", markersize=12, label="Best Position")[0]
254+
particle_paths = [
255+
ax.plot([], [], "b-", lw=0.5, alpha=0.2)[0] for _ in range(N_PARTICLES)
256+
]
257+
gbest_path_line = ax.plot([], [], "y--", lw=2, alpha=0.8, label="Best Path")[0]
258+
iteration_text = ax.text(
259+
0.02,
260+
0.95,
261+
"",
262+
transform=ax.transAxes,
263+
fontsize=12,
264+
verticalalignment="top",
265+
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
266+
)
267+
ax.legend(loc="upper right")
268+
def animate(frame):
269+
"""Animation function for matplotlib FuncAnimation"""
270+
if not swarm.step():
271+
return (
272+
particles_scatter,
273+
gbest_scatter,
274+
gbest_path_line,
275+
iteration_text,
276+
*particle_paths,
277+
)
278+
# Update particle positions
279+
positions = np.array([p.position for p in swarm.particles])
280+
particles_scatter.set_offsets(positions)
281+
# Update particle paths
282+
for i, particle in enumerate(swarm.particles):
283+
if len(particle.path) > 1:
284+
path = np.array(particle.path)
285+
particle_paths[i].set_data(path[:, 0], path[:, 1])
286+
# Update global best
287+
if swarm.gbest_position is not None:
288+
gbest_scatter.set_data(
289+
[swarm.gbest_position[0]], [swarm.gbest_position[1]]
290+
)
291+
if len(swarm.gbest_path) > 1:
292+
gbest = np.array(swarm.gbest_path)
293+
gbest_path_line.set_data(gbest[:, 0], gbest[:, 1])
294+
# Update text
295+
iteration_text.set_text(
296+
f"Iteration: {swarm.iteration}/{MAX_ITER}\n"
297+
f"Best Fitness: {swarm.gbest_value:.2f}"
298+
)
299+
return (
300+
particles_scatter,
301+
gbest_scatter,
302+
gbest_path_line,
303+
iteration_text,
304+
*particle_paths,
305+
)
306+
# Create animation and store reference to prevent garbage collection
307+
animation_ref = animation.FuncAnimation(
308+
fig, animate, frames=MAX_ITER, interval=100, blit=True, repeat=False
309+
)
310+
plt.tight_layout()
311+
plt.show()
312+
# Keep reference to prevent garbage collection
313+
return animation_ref
314+
else:
315+
# Run without animation for testing
316+
print("Running PSO algorithm without animation...")
317+
iteration_count = 0
318+
while swarm.step():
319+
iteration_count += 1
320+
if iteration_count >= MAX_ITER:
321+
break
322+
print(f"PSO completed after {iteration_count} iterations")
323+
print(f"Best fitness: {swarm.gbest_value:.2f}")
324+
if swarm.gbest_position is not None:
325+
print(
326+
f"Best position: [{swarm.gbest_position[0]:.2f}, {swarm.gbest_position[1]:.2f}]"
327+
)
328+
return None
329+
if __name__ == "__main__":
330+
try:
331+
main()
332+
except KeyboardInterrupt:
333+
print("\nProgram interrupted by user")
334+
plt.close("all")
335+
sys.exit(0)

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Python codes and [textbook](https://atsushisakai.github.io/PythonRobotics/index.
3636
* [D* Lite algorithm](#d-lite-algorithm)
3737
* [Potential Field algorithm](#potential-field-algorithm)
3838
* [Grid based coverage path planning](#grid-based-coverage-path-planning)
39+
* [Particle Swarm Optimization (PSO)](#particle-swarm-optimization-pso)
3940
* [State Lattice Planning](#state-lattice-planning)
4041
* [Biased polar sampling](#biased-polar-sampling)
4142
* [Lane sampling](#lane-sampling)
@@ -356,6 +357,24 @@ This is a 2D grid based coverage path planning simulation.
356357

357358
![PotentialField](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/GridBasedSweepCPP/animation.gif)
358359

360+
### Particle Swarm Optimization (PSO)
361+
362+
This is a 2D path planning simulation using the Particle Swarm Optimization algorithm.
363+
364+
![PSO](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/ParticleSwarmOptimization/animation.gif)
365+
366+
PSO is a metaheuristic optimization algorithm inspired by bird flocking behavior. In path planning, particles explore the search space to find collision-free paths while avoiding obstacles.
367+
368+
The animation shows particles (blue dots) converging towards the optimal path (yellow line) from start (green area) to goal (red star).
369+
370+
References
371+
372+
- [Particle swarm optimization - Wikipedia](https://en.wikipedia.org/wiki/Particle_swarm_optimization)
373+
374+
- [Kennedy, J.; Eberhart, R. (1995). "Particle Swarm Optimization"](https://ieeexplore.ieee.org/document/488968)
375+
376+
377+
359378
## State Lattice Planning
360379

361380
This script is a path planning code with state lattice planning.

0 commit comments

Comments
 (0)