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 ("\n Exiting..." )
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 ("\n Program interrupted by user" )
334+ plt .close ("all" )
335+ sys .exit (0 )
0 commit comments