Particle Swarm Optimization Lab
Watch candidate solutions fly through a search space, learning from their own memory and from their neighbors. Change any parameter and see what it does.
Based on Chapter 9, “Particle swarm optimization,” in Alaa Khamis, Optimization Algorithms: AI techniques for design, planning, and control problems, Manning Publications, 2024 (ISBN 978-1633438835).
A live swarm searching the Rastrigin function; the gold star is the global best.
From flocks and schools to a search algorithm
Particle swarm optimization (Kennedy and Eberhart, 1995) treats each candidate solution as a particle in a flock, guided by one principle: emulate the success of neighboring individuals. The panel shows the three local rules that make a flock emerge.
Chapter 9 opens with the wider family of swarm intelligence algorithms and the elements every swarm system shares.
Bird flocking: three local rules
Reynolds boidsSeparation, alignment and coherence: set any weight to zero to see the flock lose that behavior.
PSO compared with a genetic algorithm
Unlike a GA, PSO eliminates nobody: each particle’s memory (pbest) and its social link (nbest) replace crossover and mutation.
The book compares PSO with GAs term by term and explains it with a memorable theme-park analogy.
Pseudocode, motion equations and steps
Each particle keeps its position x, velocity v, personal best pbest and the best position in its neighborhood, nbest (gbest for the whole swarm, lbest for a few neighbors). Every iteration it updates its velocity, then moves.
Section 9.2 of the book walks through every step, from initialization to stopping criteria, and explains each term of equations 9.1 to 9.4.
xk+1id = xkid + vk+1id(9.2)
Inertia keeps the particle on course, the cognitive term pulls it toward its own best, and the social term toward the swarm’s best. Drag the points below to see how they combine.
Anatomy of one velocity update
Drag x, pbest, gbest or the tip of vkChoosing the parameters
| Parameter | Typical value | Effect |
|---|---|---|
| Inertia weight w | 0.792 | Large w explores; small w exploits. |
| Acceleration c1, c2 | 1.49 | Weights of the cognitive and social pulls. |
| Swarm size N | 20 to 40 | Larger swarms explore more at higher cost. |
Chapter 9 explains how to set every parameter, including Vmax, the initial velocity and the update mode.
Neighborhood topologies
The topology decides who shares information with whom, and so how fast good news spreads. Pick a topology and click a particle to see its neighborhood.
The book explains how each topology trades fast convergence for robust exploration, and which suits unimodal or multimodal problems.
Communication structure
Click a nodeContinuous PSO
Every coordinate is a real number, so equations 9.1 and 9.2 apply directly.
Use case 1: worked example, maximize f(x) = −x² + 2x + 11
Four particles maximize f(x) on −2 ≤ x ≤ 2 (maximum 12 at x = 1). Click Next iteration to apply equations 9.1 and 9.2 by hand; the table shows every update.
Chapter 9 works through the first two iterations by hand, with numbers you can check against the panel.
Hand calculation, one iteration at a time
Iteration 0Use case 2: function optimization with a full swarm
Pick a benchmark, a topology and the parameters, then press Run. Thin lines link each particle to the leader it follows.
Listing 9.1 in the book solves the Michalewicz function from scratch, with pymoo and with PySwarms.
Continuous swarm lab
Minimization in two dimensionsBest cost so far (solid) and the swarm mean cost (faint) per iteration. The dashed line is the known optimum.
Swarm diversity: high means exploring, low means exploiting.
Use case 3: trilateration of a connected vehicle
Three C-V2X cells measure their distances to a vehicle; the vehicle position minimizes Σ ei², the squared distance errors. The table shows one hand-calculated iteration. Select C-V2X trilateration in the swarm lab above to watch a full swarm locate the vehicle.
Listing C.13 in Appendix C solves this localization problem in Python, with the full problem setup.
Binary PSO
In binary PSO each bit is 0 or 1. The velocity stays real but is read as the propensity of the bit to be 1: the sigmoid turns it into a probability, and a random draw sets the bit.
sig(v) = 1 / (1 + e−v)(9.9)
xk+1id = 1 if sig(vk+1id) > r, otherwise 0(9.10)
Section 9.3 of the book explains binary PSO in full, including how φ1, φ2 and Vmax shape the search.
Use case 1: the BPSO worked example
Chapter 9 · Section 9.3Update bit 3 of particle 4 (bits counted from the left, starting at 0) with the book’s values. Change any value.
Use case 2: a binary swarm selecting projects under a budget
Practice problem: choose among 16 projects to maximize value under a $60M budget (optimum 91). Rows are particles, columns projects; the shade is sig(v), and a dot marks a 1.
Binary swarm lab
Maximization over {0,1}16Best value so far (solid) and mean current value (faint). The dashed line is the optimum.
Permutation-based PSO
For the TSP a position is an ordering of cities, so Clerc (2004) redefined the arithmetic: a velocity is a list of swaps. Edit the fields below to try each operation.
Subtraction: position − position = velocity
The swaps that turn Position 2 into Position 1 (Figure 9.9).
Addition: position + velocity = position
Apply the swaps in order (Figure 9.10).
Multiplication: constant × velocity
c = 0 empties, c < 1 truncates and c > 1 augments the list (Figure 9.8).
Section 9.4 of the book builds these operators step by step (Figures 9.8 to 9.10) and shows how equation 9.1 is rewritten with them.
Use case: TSP of 20 major US cities (Listing 9.2)
The 20 US cities from Chapter 8, starting from New York City. A swap-based PSO runs in your browser; turn on 2-opt to see what a simple local search adds.
Listing 9.2 in the book solves this TSP with PySwarms and discusses the route it finds.
Permutation swarm lab
Minimize tour length (km)Best tour length so far. Dashed lines: the book’s PySwarms result and the best known tour ( km). Basemap: Natural Earth (public domain).
Adaptive PSO
Adaptive PSO changes w, c1 and c2 while the search runs: it explores broadly at the start and homes in on the best solutions late.
Section 9.5 explains why w > 1 makes the swarm diverge, how velocity clamping helps, and how each parameter shifts the search (Figure 9.11).
Inertia weight schedules (9.5.1)
Three schedules for w: RS picks a random weight each iteration, LTV falls linearly from 0.9 to 0.4, and NLTV falls nonlinearly and reaches the low end early.
Section 9.5.1 gives the formulas for all three schedules (equations 9.11 to 9.13) and plots them in Figure 9.12.
Cognitive and social components (9.5.2)
c1 (learning from oneself) falls and c2 (learning from neighbors) rises, so the swarm starts individualistic and ends cooperative. Press Play t below to watch where the settings sit over time.
Section 9.5.2 gives the c1 and c2 schedules (equations 9.14 and 9.15) and the limit c1 + c2 ≤ 4.
Schedule explorer
Figures 9.12 and 9.13, liveInertia weight: RS (gold), LTV (blue), NLTV (purple).
c1(t) (gold) falls and c2(t) (blue) rises.
Strategy comparison: does adapting pay off?
Run each strategy several times on the same seeds and compare average convergence, or try one schedule in the continuous swarm lab.
Adaptive strategies, head to head
Mean of repeated runsSuccess means ending within 0.01 of the known optimum; no schedule wins on every problem.
Python in Google Colab
Open the book notebooks in Colab, or the companion notebook with the from-scratch code below, which mirrors each simulation on this page.
Notebooks
From-scratch implementations
# Continuous PSO from scratch (Chapter 9, Section 9.2) # Minimizes the Michalewicz function with a selectable neighborhood topology. import numpy as np import matplotlib.pyplot as plt def michalewicz(x, m=10): i = np.arange(1, x.shape[-1] + 1) return -np.sum(np.sin(x) * np.sin(i * x**2 / np.pi) ** (2 * m), axis=-1) def neighbors(topology, n): """Indices each particle listens to (itself included).""" cols = int(np.ceil(np.sqrt(n))) nb = [] for i in range(n): if topology == "star": # gbest PSO: everyone talks to everyone s = range(n) elif topology == "ring": # lbest PSO: left and right neighbors s = [i, (i - 1) % n, (i + 1) % n] elif topology == "von_neumann": # grid: left, right, up, down s = [i, (i - 1) % n, (i + 1) % n, (i - cols) % n, (i + cols) % n] elif topology == "wheel": # particle 0 is the hub s = range(n) if i == 0 else [i, 0] nb.append(np.unique(list(s))) return nb def pso(f, bounds, n_particles=20, iters=100, w=0.792, c1=1.494, c2=1.494, topology="star", vmax_frac=0.2, seed=0): rng = np.random.default_rng(seed) lo, hi = map(np.asarray, bounds) d = lo.size x = rng.uniform(lo, hi, (n_particles, d)) # random positions v = np.zeros_like(x) # zero initial velocity vmax = vmax_frac * (hi - lo) pbest, pbest_f = x.copy(), f(x) # pbest = initial position nb = neighbors(topology, n_particles) history = [pbest_f.min()] for k in range(iters): # best position found inside each particle's neighborhood nbest = np.array([pbest[idx[np.argmin(pbest_f[idx])]] for idx in nb]) r1, r2 = rng.random((2, n_particles, d)) # new numbers per dimension v = w * v + c1 * r1 * (pbest - x) + c2 * r2 * (nbest - x) # eq. 9.1 v = np.clip(v, -vmax, vmax) x = np.clip(x + v, lo, hi) # eq. 9.2 fx = f(x) improved = fx < pbest_f # eq. 9.3 pbest[improved], pbest_f[improved] = x[improved], fx[improved] history.append(pbest_f.min()) g = np.argmin(pbest_f) return pbest[g], pbest_f[g], history bounds = ([0, 0], [np.pi, np.pi]) for topology in ["star", "ring", "von_neumann", "wheel"]: best_x, best_f, hist = pso(michalewicz, bounds, topology=topology, seed=1) print(f"{topology:12s} x* = {np.round(best_x, 3)} f* = {best_f:.4f}") plt.plot(hist, label=topology) plt.axhline(-1.8013, ls="--", c="gray", label="known minimum") plt.xlabel("iteration"); plt.ylabel("best cost"); plt.legend() plt.title("Michalewicz (d = 2): effect of neighborhood topology") plt.show()
# Binary PSO from scratch (Chapter 9, Section 9.3) import numpy as np import matplotlib.pyplot as plt sigmoid = lambda v: 1 / (1 + np.exp(-v)) # 1) The worked example: update bit 3 of particle 4 (100010) v43, x43, pbest43, gbest3, phi1, phi2, r43 = 0.23, 0, 1, 1, 1.5, 1.9, 0.6 v_new = v43 + phi1 * (pbest43 - x43) + phi2 * (gbest3 - x43) # eq. 9.8 prob = sigmoid(v_new) # eq. 9.9 x_new = 1 if prob > r43 else 0 # eq. 9.10 print(f"v = {v_new:.2f}, sig(v) = {prob:.3f}, new bit = {x_new}") # 3.63, 0.974, 1 # 2) A full BPSO run: select projects under a budget (0/1 knapsack) costs = np.array([12, 7, 15, 5, 9, 6, 8, 11, 4, 7, 13, 14, 3, 4, 10, 5]) values = np.array([18, 11, 21, 9, 12, 10, 11, 16, 6, 9, 17, 20, 5, 5, 13, 7]) budget = 60 def fitness(X): # maximize value, penalize budget overruns over = np.maximum(0, X @ costs - budget) return X @ values - 3 * over def bpso(n_particles=20, iters=100, w=1.0, c1=2.0, c2=2.0, vmax=4.0, seed=0): rng = np.random.default_rng(seed) d = costs.size x = rng.integers(0, 2, (n_particles, d)) v = np.zeros((n_particles, d)) pbest, pbest_f = x.copy(), fitness(x) history = [pbest_f.max()] for k in range(iters): g = pbest[np.argmax(pbest_f)] # star topology r1, r2 = rng.random((2, n_particles, d)) v = w * v + c1 * r1 * (pbest - x) + c2 * r2 * (g - x) v = np.clip(v, -vmax, vmax) # keeps sig(v) off 0 and 1 x = (rng.random((n_particles, d)) < sigmoid(v)).astype(int) fx = fitness(x) better = fx > pbest_f pbest[better], pbest_f[better] = x[better], fx[better] history.append(pbest_f.max()) best = pbest[np.argmax(pbest_f)] return best, history best, hist = bpso(seed=3) print("selected projects:", np.flatnonzero(best) + 1) print("cost =", best @ costs, "/", budget, " value =", best @ values) plt.plot(hist); plt.xlabel("iteration"); plt.ylabel("best value") plt.title("Binary PSO on project selection"); plt.show()
# Permutation-based PSO from scratch (Chapter 9, Sections 9.4 and 9.6) # Velocity = list of swaps. Subtraction, addition and multiplication are redefined. import math import numpy as np import matplotlib.pyplot as plt def subtract(target, x): """target - x: the swaps that turn x into target (eq. 9.1 differences).""" x, swaps = list(x), [] for i, c in enumerate(target): if x[i] != c: swaps.append((c, x[i])) j = x.index(c) x[i], x[j] = x[j], x[i] return swaps def add(x, velocity): """x + v: apply each swap (exchange the two cities) in order.""" x = list(x) for a, b in velocity: i, j = x.index(a), x.index(b) x[i], x[j] = x[j], x[i] return x def multiply(c, velocity): """c * v: c = 0 empties, c < 1 truncates, c > 1 augments from the top.""" n = len(velocity) if c <= 0 or n == 0: return [] k = math.ceil(c * n) if c < 1 else math.floor(c * n) return [velocity[i % n] for i in range(k)] # The book's figures 9.8 to 9.10 print(subtract([2, 4, 6, 1, 5, 3, 8, 7], [6, 4, 7, 1, 5, 3, 8, 2])) # [(2, 6), (6, 7)] print(add([2, 4, 6, 1, 5, 3, 8, 7], [(1, 3), (4, 7)])) # [2, 7, 6, 3, 5, 1, 8, 4] print(multiply(0.4, [(2, 6), (3, 7)]), multiply(1.8, [(2, 6), (3, 7)])) # TSP: the 20 US cities of Listing 9.2 cities = { "New York City": (40.72, -74.00), "Philadelphia": (39.95, -75.17), "Baltimore": (39.28, -76.62), "Charlotte": (35.23, -80.85), "Memphis": (35.12, -89.97), "Jacksonville": (30.32, -81.70), "Houston": (29.77, -95.38), "Austin": (30.27, -97.77), "San Antonio": (29.53, -98.47), "Fort Worth": (32.75, -97.33), "Dallas": (32.78, -96.80), "San Diego": (32.78, -117.15), "Los Angeles": (34.05, -118.25), "San Jose": (37.30, -121.87), "San Francisco": (37.78, -122.42), "Indianapolis": (39.78, -86.15), "Phoenix": (33.45, -112.07), "Columbus": (39.98, -82.98), "Chicago": (41.88, -87.63), "Detroit": (42.33, -83.05), } names = list(cities) lat, lon = np.radians(np.array(list(cities.values()))).T def haversine_km(i, j): a = (np.sin((lat[j] - lat[i]) / 2) ** 2 + np.cos(lat[i]) * np.cos(lat[j]) * np.sin((lon[j] - lon[i]) / 2) ** 2) return 2 * 6371.0088 * np.arcsin(np.sqrt(a)) n = len(names) D = np.array([[haversine_km(i, j) for j in range(n)] for i in range(n)]) tour_length = lambda t: sum(D[t[k], t[(k + 1) % n]] for k in range(n)) def two_opt(t): """Optional local search: reverse segments while the tour gets shorter.""" t, improved = t[:], True while improved: improved = False for i in range(1, n - 1): for j in range(i + 1, n): a, b, c, d = t[i - 1], t[i], t[j], t[(j + 1) % n] if D[a, c] + D[b, d] < D[a, b] + D[c, d] - 1e-9: t[i:j + 1] = t[i:j + 1][::-1] improved = True return t def permutation_pso(n_particles=40, iters=300, w=0.3, c1=1.5, c2=1.5, vmax=10, hybrid=False, seed=0): rng = np.random.default_rng(seed) x = [list(rng.permutation(n)) for _ in range(n_particles)] v = [[] for _ in range(n_particles)] pbest = [p[:] for p in x] pbest_f = [tour_length(p) for p in x] history = [min(pbest_f)] for k in range(iters): g = pbest[int(np.argmin(pbest_f))] # star topology for i in range(n_particles): r1, r2 = rng.random(2) v[i] = (multiply(w, v[i]) + multiply(c1 * r1, subtract(pbest[i], x[i])) + multiply(c2 * r2, subtract(g, x[i])))[:vmax] x[i] = add(x[i], v[i]) f = tour_length(x[i]) if f < pbest_f[i]: pbest[i], pbest_f[i] = x[i][:], f if hybrid: # PSO + 2-opt on the best b = int(np.argmin(pbest_f)) pbest[b] = two_opt(pbest[b]) pbest_f[b] = tour_length(pbest[b]) history.append(min(pbest_f)) b = int(np.argmin(pbest_f)) return pbest[b], pbest_f[b], history best, length, hist = permutation_pso(seed=0) # try hybrid=True start = best.index(0) # start at New York City best = best[start:] + best[:start] print(" -> ".join(names[c] for c in best)) print(f"Route length: {length:.1f} km") plt.plot(hist); plt.xlabel("iteration"); plt.ylabel("best tour length (km)") plt.title("Permutation PSO on the 20-city TSP"); plt.show()
# Adaptive PSO from scratch (Chapter 9, Section 9.5) # Inertia schedules (eqs. 9.11 to 9.13) and time-varying c1, c2 (eqs. 9.14, 9.15). import numpy as np import matplotlib.pyplot as plt W_MAX, W_MIN = 0.9, 0.4 def inertia(strategy, t, t_max, w_prev, rng): if strategy == "RS": # random selection, mean 0.75 (eq. 9.11) return 0.5 + rng.random() / 2 if strategy == "LTV": # linear time varying (eq. 9.12) return (W_MAX - W_MIN) * (t_max - t) / t_max + W_MIN if strategy == "NLTV": # nonlinear time varying, w_0 = 0.9 (eq. 9.13) if t == 0: return W_MAX return (w_prev - W_MIN) * (t_max - (t - 1)) / (t_max + W_MIN) + W_MIN return 0.792 # constant inertia def coefficients(t, t_max, c1=(0.5, 2.0), c2=(0.8, 2.5)): """c1 falls from c1_max to c1_min, c2 rises from c2_min to c2_max.""" c1_t = (c1[1] - c1[0]) * (t_max - t) / t_max + c1[0] # eq. 9.14 c2_t = c2[1] - (c2[1] - c2[0]) * (t_max - t) / t_max # eq. 9.15 return c1_t, c2_t def rastrigin(x): return 10 * x.shape[-1] + np.sum(x**2 - 10 * np.cos(2 * np.pi * x), axis=-1) def adaptive_pso(f, lo, hi, d=10, n=20, t_max=300, strategy="LTV", tvac=False, seed=0): rng = np.random.default_rng(seed) x = rng.uniform(lo, hi, (n, d)) v = np.zeros_like(x) vmax = 0.2 * (hi - lo) pbest, pbest_f = x.copy(), f(x) w, history = None, [pbest_f.min()] for t in range(t_max): w = inertia(strategy, t, t_max, w, rng) c1, c2 = coefficients(t, t_max) if tvac else (1.49, 1.49) g = pbest[np.argmin(pbest_f)] r1, r2 = rng.random((2, n, d)) v = np.clip(w * v + c1 * r1 * (pbest - x) + c2 * r2 * (g - x), -vmax, vmax) x = np.clip(x + v, lo, hi) fx = f(x) better = fx < pbest_f pbest[better], pbest_f[better] = x[better], fx[better] history.append(pbest_f.min()) return pbest_f.min(), history # Figures 9.12 and 9.13: the schedules themselves t_max, rng = 100, np.random.default_rng(1) w_rs = [inertia("RS", t, t_max, None, rng) for t in range(t_max + 1)] w_ltv = [inertia("LTV", t, t_max, None, rng) for t in range(t_max + 1)] w_nltv, w = [], None for t in range(t_max + 1): w = inertia("NLTV", t, t_max, w, rng) w_nltv.append(w) c1c2 = np.array([coefficients(t, t_max) for t in range(t_max + 1)]) fig, ax = plt.subplots(1, 2, figsize=(11, 4)) ax[0].plot(w_rs, "g--", label="RS"); ax[0].plot(w_ltv, "r", label="LTV") ax[0].plot(w_nltv, "b:", label="NLTV"); ax[0].set_title("Inertia weight") ax[1].plot(c1c2[:, 0], "r", label="c1(t)"); ax[1].plot(c1c2[:, 1], "b--", label="c2(t)") ax[1].set_title("Acceleration coefficients") for a in ax: a.set_xlabel("iteration"); a.legend() plt.show() # Compare strategies on 10-dimensional Rastrigin (20 runs each) strategies = [("Constant w", "const", False), ("RS", "RS", False), ("LTV", "LTV", False), ("NLTV", "NLTV", False), ("LTV + varying c1, c2", "LTV", True)] for name, s, tv in strategies: runs = [adaptive_pso(rastrigin, -5.12, 5.12, strategy=s, tvac=tv, seed=k) for k in range(20)] finals = np.array([r[0] for r in runs]) print(f"{name:22s} mean = {finals.mean():7.3f} std = {finals.std():6.3f} best = {finals.min():.3f}") plt.plot(np.mean([r[1] for r in runs], axis=0), label=name) plt.yscale("log"); plt.xlabel("iteration"); plt.ylabel("mean best cost") plt.title("Adaptive PSO on Rastrigin (d = 10)"); plt.legend(); plt.show()
Run each cell in Colab, or paste a tab’s code into any notebook.
What to remember
Continuous PSO
Inertia, cognitive and social pulls, plus the topology, balance exploration and exploitation.
Binary PSO
Velocity becomes the probability, through the sigmoid, that a bit is 1.
Permutation PSO
Velocity is a list of swaps, and local search such as 2-opt helps a lot.
Adaptive PSO
Schedules for w, c1 and c2 move the swarm from exploration to exploitation.
References: J. Kennedy and R. Eberhart, “Particle swarm optimization,” 1995. J. Kennedy and R. C. Eberhart, “A discrete binary version of the particle swarm algorithm,” 1997. M. Clerc, “Discrete particle swarm optimization, illustrated by the traveling salesman problem,” 2004. A. Khamis, Optimization Algorithms: AI techniques for design, planning, and control problems, Manning, 2024, Chapter 9.
Check your understanding
Two quick questions on Chapter 9. Pick an answer to check it.
Q1. In binary PSO, what does the velocity v of one bit represent?
The velocity stays a real number but is read as the propensity of the bit to be 1: sig(v) turns it into a probability, and a random draw sets the bit. A velocity of 0 means a coin flip.Q2. Which setting shifts a PSO swarm toward exploitation, refining the area around elite solutions?
A small inertia weight and a stronger social pull concentrate the swarm around the best positions found. A large w or c1 > c2 favors exploration, and c2 = 0 turns the particles into independent hill climbers.For more exercises with full solutions, see Appendix C, “Exercises and solutions,” in Optimization Algorithms. The coding solutions are also on GitHub (Chapter 9).