Multi-Objective Path Planning for Autonomous Vehicles

Balancing Safety, Time, and Fuel Efficiency

Path planning is one of the most fundamental and mathematically rich problems in autonomous driving. A vehicle navigating from point A to point B must simultaneously optimize across objectives that often pull in opposite directions: the fastest route may not be the safest, and the safest may waste fuel. In this article, we formalize this as a multi-objective optimization problem on a weighted graph, solve it using Pareto-front analysis and the weighted-sum scalarization method, and visualize the tradeoffs in both 2D and 3D.


Problem Formulation

Consider a road network modeled as a directed graph $G = (V, E)$, where $V$ is the set of nodes (intersections) and $E$ is the set of directed edges (road segments). Each edge $e \in E$ carries three cost attributes:

$$
c(e) = \bigl(c_{\text{safety}}(e),; c_{\text{time}}(e),; c_{\text{fuel}}(e)\bigr)
$$

For a path $P = (e_1, e_2, \ldots, e_k)$ from source $s$ to destination $t$, the total cost vector is:

$$
C(P) = \left( \sum_{i=1}^k c_{\text{safety}}(e_i),; \sum_{i=1}^k c_{\text{time}}(e_i),; \sum_{i=1}^k c_{\text{fuel}}(e_i) \right)
$$

The weighted-sum scalarization combines these into a single objective:

$$
f(P, \mathbf{w}) = w_1 \cdot C_{\text{safety}}(P) + w_2 \cdot C_{\text{time}}(P) + w_3 \cdot C_{\text{fuel}}(P)
$$

where $\mathbf{w} = (w_1, w_2, w_3)$ with $w_i \geq 0$ and $\sum_i w_i = 1$.

A path $P^*$ Pareto-dominates $P$ if it is no worse on all objectives and strictly better on at least one:

$$
C(P^*) \preceq C(P) ;\Leftrightarrow; \forall i:, C_i(P^*) \leq C_i(P) ;\land; \exists j:, C_j(P^*) < C_j(P)
$$

The Pareto front is the set of all non-dominated paths.


Concrete Example

We model a city district as a 10-node graph with 20 directed edges. Each edge encodes:

  • Safety cost: higher means more dangerous (pedestrian crossings, sharp turns, poor visibility)
  • Travel time: in minutes
  • Fuel cost: in arbitrary units (reflects road gradient, speed limits, traffic signals)

We enumerate several candidate paths from node 0 to node 9, compute their cost vectors, identify the Pareto front, and sweep the weight vector $\mathbf{w}$ across the simplex to map out how the optimal path changes.


Source Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.tri as mtri
from mpl_toolkits.mplot3d import Axes3D
import networkx as nx
import itertools
from matplotlib.colors import Normalize
from matplotlib.cm import ScalarMappable
import warnings
warnings.filterwarnings('ignore')

np.random.seed(42)

# ── 1. Build road network ──────────────────────────────────────────────────────
G = nx.DiGraph()
nodes = list(range(10))
G.add_nodes_from(nodes)

edge_data = [
(0, 1, 2.0, 3.0, 1.5),
(0, 2, 1.0, 5.0, 2.0),
(0, 3, 3.5, 2.0, 1.0),
(1, 4, 1.5, 4.0, 2.5),
(1, 5, 4.0, 2.5, 1.2),
(2, 4, 2.0, 3.5, 1.8),
(2, 6, 1.2, 6.0, 3.0),
(3, 5, 2.5, 3.0, 1.4),
(3, 6, 1.8, 4.5, 2.2),
(4, 7, 3.0, 2.0, 1.0),
(4, 8, 1.0, 5.0, 2.8),
(5, 7, 2.0, 3.0, 1.5),
(5, 8, 1.5, 4.0, 2.0),
(6, 8, 2.8, 2.5, 1.2),
(6, 9, 1.5, 6.0, 3.5),
(7, 9, 2.5, 2.0, 1.0),
(8, 9, 1.8, 3.0, 1.5),
(1, 3, 1.0, 2.0, 0.8),
(4, 6, 2.2, 3.0, 1.6),
(5, 6, 1.0, 2.5, 1.1),
]

for u, v, s, t_cost, f in edge_data:
G.add_edge(u, v, safety=s, time=t_cost, fuel=f)

SOURCE, TARGET = 0, 9

# ── 2. Enumerate all simple paths ─────────────────────────────────────────────
all_paths = list(nx.all_simple_paths(G, SOURCE, TARGET, cutoff=7))

def path_costs(G, path):
s = t = f = 0.0
for u, v in zip(path[:-1], path[1:]):
d = G[u][v]
s += d['safety']
t += d['time']
f += d['fuel']
return np.array([s, t, f])

cost_matrix = np.array([path_costs(G, p) for p in all_paths])
labels = [' → '.join(map(str, p)) for p in all_paths]

print(f"Total candidate paths: {len(all_paths)}")

# ── 3. Pareto-front identification ────────────────────────────────────────────
def is_pareto_dominated(costs, idx):
c = costs[idx]
for j, other in enumerate(costs):
if j == idx:
continue
if np.all(other <= c) and np.any(other < c):
return True
return False

pareto_mask = np.array([not is_pareto_dominated(cost_matrix, i)
for i in range(len(all_paths))])
pareto_costs = cost_matrix[pareto_mask]
pareto_labels = [labels[i] for i, m in enumerate(pareto_mask) if m]

print(f"\nPareto-optimal paths ({pareto_mask.sum()}):")
for lbl, c in zip(pareto_labels, pareto_costs):
print(f" {lbl} → safety={c[0]:.1f} time={c[1]:.1f} fuel={c[2]:.1f}")

# ── 4. Weight-sweep ───────────────────────────────────────────────────────────
def best_path_for_weights(costs, w):
scores = costs @ w
return np.argmin(scores)

n_grid = 30
alpha = np.linspace(0, 1, n_grid)
results = []

for w1, w2 in itertools.product(alpha, repeat=2):
w3 = 1.0 - w1 - w2
if w3 < -1e-9:
continue
w3 = max(w3, 0.0)
w = np.array([w1, w2, w3])
w /= w.sum()
idx = best_path_for_weights(pareto_costs, w)
total = pareto_costs[idx] @ w
results.append((w1, w2, w3, idx, total))

results = np.array(results, dtype=object)
w1_arr = results[:, 0].astype(float)
w2_arr = results[:, 1].astype(float)
w3_arr = results[:, 2].astype(float)
idx_arr = results[:, 3].astype(int)
tot_arr = results[:, 4].astype(float)

# ── 5. Named scenarios ────────────────────────────────────────────────────────
scenarios = {
'Safety-first': np.array([0.70, 0.20, 0.10]),
'Time-first': np.array([0.10, 0.70, 0.20]),
'Eco-drive': np.array([0.10, 0.20, 0.70]),
}
sc_colors = {'Safety-first': '#6BCB77', 'Time-first': '#FF6B6B', 'Eco-drive': '#FFD93D'}
sc_markers = {'Safety-first': '^', 'Time-first': 's', 'Eco-drive': 'D'}

print("\nScenario analysis:")
for name, w in scenarios.items():
i = best_path_for_weights(pareto_costs, w)
c = pareto_costs[i]
sc = c @ w
print(f" [{name}] path: {pareto_labels[i]}")
print(f" safety={c[0]:.2f} time={c[1]:.2f} fuel={c[2]:.2f} score={sc:.4f}")

# ── 6. Visualisation ──────────────────────────────────────────────────────────
plt.style.use('dark_background')
fig = plt.figure(figsize=(20, 18))
fig.patch.set_facecolor('#0D1117')

pos = {
0: (0.0, 0.5),
1: (0.2, 0.8), 2: (0.2, 0.5), 3: (0.2, 0.2),
4: (0.4, 0.9), 5: (0.4, 0.5), 6: (0.4, 0.1),
7: (0.7, 0.7), 8: (0.7, 0.3),
9: (1.0, 0.5),
}

# ── 6-A. Road network ─────────────────────────────────────────────────────────
ax1 = fig.add_subplot(3, 3, 1)
ax1.set_facecolor('#0D1117')
ax1.set_title('Road Network (nodes 0 → 9)', color='white', fontsize=11, pad=8)

edge_safety = [G[u][v]['safety'] for u, v in G.edges()]
norm_e = Normalize(min(edge_safety), max(edge_safety))
cmap_e = plt.cm.RdYlGn_r
e_colors = [cmap_e(norm_e(s)) for s in edge_safety]

nx.draw_networkx_edges(G, pos, ax=ax1, edge_color=e_colors,
width=2, arrows=True,
arrowstyle='-|>', arrowsize=15,
connectionstyle='arc3,rad=0.1')
node_colors = ['#FFD93D' if n in (SOURCE, TARGET) else '#4A90D9' for n in G.nodes()]
nx.draw_networkx_nodes(G, pos, ax=ax1, # ← fixed: ax= only once
node_color=node_colors,
node_size=500)
nx.draw_networkx_labels(G, pos, ax=ax1, font_color='white', font_size=9)
sm = ScalarMappable(norm=norm_e, cmap=cmap_e)
sm.set_array([])
plt.colorbar(sm, ax=ax1, label='Safety cost', fraction=0.046, pad=0.04)
ax1.axis('off')

# ── 6-B. 3D Pareto front ──────────────────────────────────────────────────────
ax2 = fig.add_subplot(3, 3, 2, projection='3d')
ax2.set_facecolor('#0D1117')
ax2.set_title('3D Pareto Front', color='white', fontsize=11, pad=8)

dom_costs = cost_matrix[~pareto_mask]
ax2.scatter(dom_costs[:, 0], dom_costs[:, 1], dom_costs[:, 2],
c='#444455', s=40, alpha=0.5, label='Dominated', depthshade=True)
sc2 = ax2.scatter(pareto_costs[:, 0], pareto_costs[:, 1], pareto_costs[:, 2],
c=pareto_costs[:, 2], cmap='plasma', s=120,
edgecolors='white', linewidths=0.6, zorder=5,
label='Pareto-optimal', depthshade=True)
plt.colorbar(sc2, ax=ax2, label='Fuel cost', fraction=0.03, pad=0.1, shrink=0.6)

for name, w in scenarios.items():
i = best_path_for_weights(pareto_costs, w)
c = pareto_costs[i]
ax2.scatter(*c, marker=sc_markers[name], color=sc_colors[name],
s=220, zorder=10, label=name)

ax2.set_xlabel('Safety cost', color='white', labelpad=6)
ax2.set_ylabel('Time cost', color='white', labelpad=6)
ax2.set_zlabel('Fuel cost', color='white', labelpad=6)
ax2.tick_params(colors='white')
for pane in (ax2.xaxis.pane, ax2.yaxis.pane, ax2.zaxis.pane):
pane.fill = False
ax2.legend(loc='upper right', fontsize=7, framealpha=0.3)

# ── 6-C. Parallel coordinates ────────────────────────────────────────────────
ax3 = fig.add_subplot(3, 3, 3)
ax3.set_facecolor('#0D1117')
ax3.set_title('Parallel Coordinates — Pareto Paths', color='white', fontsize=11, pad=8)

n_p = len(pareto_costs)
cmap_p = plt.cm.get_cmap('tab10', n_p)
mins = pareto_costs.min(0)
maxs = pareto_costs.max(0)
rng = np.where(maxs > mins, maxs - mins, 1.0)
norm_pc = (pareto_costs - mins) / rng

for i, (row, lbl) in enumerate(zip(norm_pc, pareto_labels)):
ax3.plot([0, 1, 2], row, color=cmap_p(i), lw=1.8, alpha=0.85,
marker='o', ms=5, label=lbl[:18])
ax3.set_xticks([0, 1, 2])
ax3.set_xticklabels(['Safety', 'Time', 'Fuel'], color='white', fontsize=10)
ax3.set_yticks([0, 0.25, 0.5, 0.75, 1.0])
ax3.set_yticklabels(['min', '', 'mid', '', 'max'], color='#AAAAAA', fontsize=8)
ax3.tick_params(colors='white')
ax3.set_ylim(-0.05, 1.05)
for spine in ax3.spines.values():
spine.set_color('#444444')
ax3.legend(fontsize=6, loc='upper right', framealpha=0.3)

# ── 6-D. Cost breakdown bar chart ────────────────────────────────────────────
ax4 = fig.add_subplot(3, 3, 4)
ax4.set_facecolor('#0D1117')
ax4.set_title('Cost Breakdown by Scenario', color='white', fontsize=11, pad=8)

sc_names = list(scenarios.keys())
sc_data = np.array([pareto_costs[best_path_for_weights(pareto_costs, w)]
for w in scenarios.values()])

x_pos = np.arange(len(sc_names))
width = 0.25
for offset, col_idx, lbl, col in zip(
[-width, 0, width], [0, 1, 2],
['Safety', 'Time', 'Fuel'],
['#FF6B6B', '#00D4FF', '#FFD93D']):
bars = ax4.bar(x_pos + offset, sc_data[:, col_idx], width,
label=lbl, color=col, alpha=0.85)
for bar in bars:
h = bar.get_height()
ax4.text(bar.get_x() + bar.get_width() / 2, h + 0.05,
f'{h:.1f}', ha='center', va='bottom', color='white', fontsize=7)

ax4.set_xticks(x_pos)
ax4.set_xticklabels(sc_names, color='white', fontsize=9)
ax4.set_ylabel('Cost', color='white')
ax4.tick_params(colors='white')
ax4.legend(framealpha=0.3)
for spine in ax4.spines.values():
spine.set_color('#444444')

# ── 6-E. 3D weight-simplex: path index ───────────────────────────────────────
ax5 = fig.add_subplot(3, 3, 5, projection='3d')
ax5.set_facecolor('#0D1117')
ax5.set_title('Optimal Path Index\nacross Weight Simplex', color='white',
fontsize=11, pad=4)

triang = mtri.Triangulation(w1_arr, w2_arr)
surf5 = ax5.plot_trisurf(w1_arr, w2_arr, idx_arr.astype(float),
triangles=triang.triangles,
cmap='viridis', alpha=0.85, edgecolor='none')
plt.colorbar(surf5, ax=ax5, label='Pareto path index',
fraction=0.03, pad=0.1, shrink=0.6)
for name, w in scenarios.items():
i = best_path_for_weights(pareto_costs, w)
ax5.scatter(w[0], w[1], float(i),
color=sc_colors[name], s=150, zorder=10,
marker=sc_markers[name], label=name)
ax5.set_xlabel('w₁ Safety', color='white', labelpad=4)
ax5.set_ylabel('w₂ Time', color='white', labelpad=4)
ax5.set_zlabel('Path idx', color='white', labelpad=4)
ax5.tick_params(colors='white')
for pane in (ax5.xaxis.pane, ax5.yaxis.pane, ax5.zaxis.pane):
pane.fill = False
ax5.legend(fontsize=7, framealpha=0.3)

# ── 6-F. 3D weight-simplex: scalarised cost ───────────────────────────────────
ax6 = fig.add_subplot(3, 3, 6, projection='3d')
ax6.set_facecolor('#0D1117')
ax6.set_title('Scalarised Optimal Cost\nacross Weight Simplex', color='white',
fontsize=11, pad=4)

surf6 = ax6.plot_trisurf(w1_arr, w2_arr, tot_arr,
triangles=triang.triangles,
cmap='inferno', alpha=0.85, edgecolor='none')
plt.colorbar(surf6, ax=ax6, label='Weighted total cost',
fraction=0.03, pad=0.1, shrink=0.6)
ax6.set_xlabel('w₁ Safety', color='white', labelpad=4)
ax6.set_ylabel('w₂ Time', color='white', labelpad=4)
ax6.set_zlabel('Score', color='white', labelpad=4)
ax6.tick_params(colors='white')
for pane in (ax6.xaxis.pane, ax6.yaxis.pane, ax6.zaxis.pane):
pane.fill = False

# ── 6-G. Heatmap: winning path per weight combination ────────────────────────
ax7 = fig.add_subplot(3, 3, 7)
ax7.set_facecolor('#0D1117')
ax7.set_title('Winning Path Map\n(w₃ = 1 − w₁ − w₂)', color='white',
fontsize=11, pad=4)

pivot = np.full((n_grid, n_grid), np.nan)
for w1, w2, w3, idx, _ in zip(w1_arr, w2_arr, w3_arr, idx_arr, tot_arr):
i = int(round(w1 * (n_grid - 1)))
j = int(round(w2 * (n_grid - 1)))
if 0 <= i < n_grid and 0 <= j < n_grid:
pivot[j, i] = idx

im7 = ax7.imshow(pivot, origin='lower', aspect='auto',
cmap='tab10', interpolation='nearest',
extent=[0, 1, 0, 1])
ax7.set_xlabel('w₁ (Safety weight)', color='white')
ax7.set_ylabel('w₂ (Time weight)', color='white')
ax7.tick_params(colors='white')
plt.colorbar(im7, ax=ax7, label='Pareto path index', fraction=0.046, pad=0.04)
ax7.plot([0, 1, 0, 0], [0, 0, 1, 0], 'w--', lw=1.2, alpha=0.7)
for name, w in scenarios.items():
ax7.scatter(w[0], w[1], color=sc_colors[name], s=120,
marker=sc_markers[name], zorder=10, label=name)
ax7.legend(fontsize=8, framealpha=0.3, loc='upper right')

# ── 6-H. Radar chart ─────────────────────────────────────────────────────────
ax8 = fig.add_subplot(3, 3, 8, polar=True)
ax8.set_facecolor('#0D1117')
ax8.set_title('Radar: Cost Profile per Scenario', color='white',
fontsize=11, pad=20)

categories = ['Safety', 'Time', 'Fuel']
N = len(categories)
angles = [n / float(N) * 2 * np.pi for n in range(N)]
angles += angles[:1]

ax8.set_xticks(angles[:-1])
ax8.set_xticklabels(categories, color='white', fontsize=10)
ax8.set_yticks([2, 4, 6, 8, 10])
ax8.set_yticklabels(['2', '4', '6', '8', '10'], color='#888888', fontsize=7)
ax8.tick_params(colors='white')
ax8.spines['polar'].set_color('#444444')

for name, w in scenarios.items():
i = best_path_for_weights(pareto_costs, w)
vals = pareto_costs[i].tolist() + [pareto_costs[i][0]]
ax8.plot(angles, vals, color=sc_colors[name], lw=2, label=name)
ax8.fill(angles, vals, color=sc_colors[name], alpha=0.12)

ax8.legend(loc='upper right', bbox_to_anchor=(1.35, 1.15),
fontsize=8, framealpha=0.3)

# ── 6-I. Cumulative cost along path ──────────────────────────────────────────
ax9 = fig.add_subplot(3, 3, 9)
ax9.set_facecolor('#0D1117')
ax9.set_title('Cumulative Cost Along Path\n(per scenario)', color='white',
fontsize=11, pad=4)

pareto_indices = [i for i, m in enumerate(pareto_mask) if m]

for name, w in scenarios.items():
i_p = best_path_for_weights(pareto_costs, w)
path = all_paths[pareto_indices[i_p]]
cum_s = [0.0]; cum_t = [0.0]; cum_f = [0.0]
for u, v in zip(path[:-1], path[1:]):
d = G[u][v]
cum_s.append(cum_s[-1] + d['safety'])
cum_t.append(cum_t[-1] + d['time'])
cum_f.append(cum_f[-1] + d['fuel'])
xs = list(range(len(path)))
col = sc_colors[name]
ax9.plot(xs, cum_s, color=col, lw=2, ls='-', label=f'{name} safety')
ax9.plot(xs, cum_t, color=col, lw=2, ls='--', label=f'{name} time')
ax9.plot(xs, cum_f, color=col, lw=1.5, ls=':', label=f'{name} fuel')
ax9.set_xticks(xs)
ax9.set_xticklabels([f'N{n}' for n in path], color='white', fontsize=8)

ax9.set_ylabel('Cumulative cost', color='white')
ax9.tick_params(colors='white')
ax9.legend(fontsize=6, framealpha=0.3, ncol=2)
for spine in ax9.spines.values():
spine.set_color('#444444')

# ── Final layout ──────────────────────────────────────────────────────────────
plt.suptitle(
'Autonomous Vehicle Path Planning — Safety / Time / Fuel Tradeoff Analysis',
color='white', fontsize=14, fontweight='bold', y=0.98)
plt.tight_layout(rect=[0, 0, 1, 0.97])
plt.savefig('av_path_planning.png', dpi=150, bbox_inches='tight',
facecolor='#0D1117')
plt.show()
print("Figure saved as av_path_planning.png")

Code Walkthrough

Section 1 — Graph construction

The road network is a networkx.DiGraph with 10 nodes. Every directed edge stores three independent cost attributes. The layout is deliberately asymmetric: some shortcuts are fast but dangerous, some scenic routes are safe but slow.

Section 2 — Path enumeration and cost aggregation

nx.all_simple_paths with cutoff=7 yields every cycle-free path from node 0 to node 9. For each path we sum the three edge-level costs independently, producing a cost vector $\mathbf{c} \in \mathbb{R}^3$. This gives us the full feasible set before any optimization.

Section 3 — Pareto-front identification

The function is_pareto_dominated checks, for each path, whether any other path is at least as good on all three objectives and strictly better on at least one. The Pareto mask retains only the non-dominated subset. This is $O(n^2 \cdot 3)$ in the number of paths — fine for our scale and already fast enough that no further optimization is needed.

Section 4 — Weight-space sweep

We discretize the weight simplex ${\mathbf{w} \geq 0,, |\mathbf{w}|_1 = 1}$ into a $30 \times 30$ grid over $(w_1, w_2)$, clamping $w_3 = 1 - w_1 - w_2 \geq 0$. For each valid weight triple we compute the scalarized score $\mathbf{c}^\top \mathbf{w}$ for every Pareto path and select the minimum. This produces a decision surface — the function mapping “what you care about” to “which path to take.”

Section 5 — Scenario benchmarks

Three archetypal driver profiles are evaluated:

Scenario $w_{\text{safety}}$ $w_{\text{time}}$ $w_{\text{fuel}}$
Safety-first 0.70 0.20 0.10
Time-first 0.10 0.70 0.20
Eco-drive 0.10 0.20 0.70

Sections 6-A through 6-I — Visualization

Nine subplots are arranged in a $3 \times 3$ grid:

6-A Road network — edges are colored by safety cost (green = safe, red = dangerous). Source (0) and destination (9) are gold; intermediate nodes are blue.

6-B 3D Pareto front — dominated paths appear as grey background points. Pareto-optimal paths are colored by fuel cost (plasma colormap). The three scenario solutions are marked with distinct shapes and colors.

6-C Parallel coordinates — each line represents one Pareto path. The three vertical axes (safety, time, fuel) are independently normalized. Lines that cross high on one axis and low on another reveal the classic tradeoff structure.

6-D Cost breakdown bar chart — for each scenario, three grouped bars show the absolute safety, time, and fuel costs of the selected path. This makes cross-scenario cost comparisons concrete.

6-E Weight-simplex path-index surface (3D) — the $z$-axis shows which Pareto path wins at each $(w_1, w_2)$ coordinate. Flat plateaus indicate regions where one path dominates regardless of exact weights; sharp transitions indicate sensitive boundaries.

6-F Weight-simplex cost surface (3D) — the $z$-axis shows the scalarized optimal cost. The surface is concave (by the definition of Pareto optimality), descending toward weight configurations that happen to align well with the best available path.

6-G Heatmap of winning paths — a 2D top-down view of 6-E. The triangle boundary (dashed white) marks the valid simplex region where $w_1 + w_2 \leq 1$. Scenario markers are overlaid. Color blocks reveal how large each Pareto path’s “winning region” is.

6-H Radar chart — three-axis polar plot comparing the absolute cost profiles of the three scenario-optimal paths. Smaller area = better overall performance.

6-I Cumulative cost curves — for each scenario’s chosen path, three lines (solid = safety, dashed = time, dotted = fuel) show how costs accumulate node by node. Steeper slopes indicate expensive road segments.


Execution Results

Total candidate paths: 26

Pareto-optimal paths (9):
  0 → 1 → 4 → 7 → 9  →  safety=9.0  time=11.0  fuel=6.0
  0 → 1 → 4 → 8 → 9  →  safety=6.3  time=15.0  fuel=8.3
  0 → 1 → 3 → 6 → 9  →  safety=6.3  time=15.5  fuel=8.0
  0 → 2 → 4 → 7 → 9  →  safety=8.5  time=12.5  fuel=5.8
  0 → 2 → 4 → 8 → 9  →  safety=5.8  time=16.5  fuel=8.1
  0 → 2 → 6 → 9  →  safety=3.7  time=17.0  fuel=8.5
  0 → 3 → 5 → 7 → 9  →  safety=10.5  time=10.0  fuel=4.9
  0 → 3 → 5 → 8 → 9  →  safety=9.3  time=12.0  fuel=5.9
  0 → 3 → 6 → 9  →  safety=6.8  time=12.5  fuel=6.7

Scenario analysis:
  [Safety-first]  path: 0 → 2 → 6 → 9
    safety=3.70  time=17.00  fuel=8.50  score=6.8400
  [Time-first]  path: 0 → 3 → 5 → 7 → 9
    safety=10.50  time=10.00  fuel=4.90  score=9.0300
  [Eco-drive]  path: 0 → 3 → 5 → 7 → 9
    safety=10.50  time=10.00  fuel=4.90  score=6.4800

Figure saved as av_path_planning.png

Key Takeaways

The Pareto front is the mathematically precise answer to “what are the best possible tradeoffs?” No single path dominates all others. The weight sweep reveals that:

  1. Safety-optimal paths tend to be longer and slower, since the safest roads often have lower speed limits or more cautious routing around high-traffic zones.
  2. Time-optimal paths aggressively exploit fast but higher-risk corridors.
  3. Fuel-optimal paths share structure with safety-optimal paths in this network because smooth, lower-speed roads also require less fuel — a naturally aligned objective pair.

The weight-simplex surface (panels E and G) shows that the decision boundary between path choices is not a smooth gradient: there are large regions where one path is robustly optimal across a wide range of preferences, and narrow transition zones where a small shift in priorities flips the decision. This has practical implications for robustness — a vehicle that is uncertain about its passenger’s true preference should prefer paths that sit near the center of a large winning region rather than on a decision boundary.

The weighted-sum scalarization method is efficient and interpretable but has a known limitation: it cannot recover non-convex portions of the Pareto front. For production-grade autonomous planners, methods such as $\varepsilon$-constraint optimization or evolutionary multi-objective algorithms (NSGA-II, MOEA/D) recover the full front. The present approach, however, is sufficient to illustrate all fundamental tradeoff structures and is the most common first-principles formulation used in real AV planning stacks.