You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

588 lines
19 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import numpy as np
import matplotlib.pyplot as plt
from itertools import permutations
import math
import openjij as oj
import random
# ============================
# 1. KDE 函數
# ============================
def kde_1d(samples, num_points=200):
if len(samples) == 0:
return np.array([]), np.array([])
xs = np.linspace(min(samples), max(samples), num_points)
n = len(samples)
if n < 2:
return xs, np.zeros_like(xs)
std = np.std(samples)
if std == 0:
std = 1.0
h = 1.06 * std * (n ** (-1/5))
ys = []
inv_sqrt_2pi = 1.0 / math.sqrt(2 * math.pi)
for x in xs:
s = 0.0
for xi in samples:
z = (x - xi) / h
s += math.exp(-0.5 * z * z) * inv_sqrt_2pi
ys.append(s / (n * h))
return xs, np.array(ys)
# ============================
# 2. route diversity matrix支援可變長度路徑
# ============================
def route_diversity_matrix(routes, N):
"""
routes: list of routes每條 route 是城市序列,例如 [0, 2, 5, 1, 9]
我們只用前 N 個位置來統計(超過 N 的部分忽略)
"""
freq = np.zeros((N, N), dtype=float)
valid_routes = 0
for route in routes:
if len(route) == 0:
continue
valid_routes += 1
for t in range(min(len(route), N)):
city = route[t]
if 0 <= city < N:
freq[city, t] += 1
if valid_routes > 0:
freq /= valid_routes
return freq
# ============================
# 3. MTSP 變數編碼x[k,i,p] → index
# ============================
def idx_mtsp(k, i, p, N):
""" x[k,i,p] → 1D index """
return k * (N * N) + i * N + p
# ============================
# 4. 建立 2-UAV M-TSP QUBO城市數不固定
# ============================
def build_mtsp_qubo_variable(D, penalty=20.0, big_penalty=9999.0):
"""
- 城市: 0..N-1其中 0 為 depot
- 兩台 UAV: k = 0,1
- 每台 UAV 有 N 個 slot: p = 0..N-1
- 變數: x_{k,i,p}
QUBO包含
距離成本 + 位置約束 + 城市約束 + 起點約束
"""
D = np.asarray(D)
N = D.shape[0]
Q = {}
def addQ(u, v, w):
if w == 0:
return
if u > v:
u, v = v, u
Q[(u, v)] = Q.get((u, v), 0.0) + w
# ---- 距離成本 ----
for k in range(2):
for p in range(N):
q = (p + 1) % N
for i in range(N):
for j in range(N):
dij = D[i, j]
if dij == 0:
continue
u = idx_mtsp(k, i, p, N)
v = idx_mtsp(k, j, q, N)
addQ(u, v, dij)
# ---- 位置約束:每 slot 只能一個城市 ----
for k in range(2):
for p in range(N):
vars_pos = [idx_mtsp(k, i, p, N) for i in range(N)]
for u in vars_pos:
addQ(u, u, -penalty)
for a in range(N):
for b in range(a+1, N):
addQ(vars_pos[a], vars_pos[b], 2*penalty)
# ---- 城市約束1..N-1 每城恰好一次 ----
for i in range(1, N):
vars_city = []
for k in range(2):
for p in range(N):
vars_city.append(idx_mtsp(k, i, p, N))
for u in vars_city:
addQ(u, u, -penalty)
L = len(vars_city)
for a in range(L):
for b in range(a+1, L):
addQ(vars_city[a], vars_city[b], 2*penalty)
# ---- 起點約束:每台 UAV 的 p=0 必須是 city 0 ----
for k in range(2):
for i in range(1, N):
addQ(idx_mtsp(k, i, 0, N), idx_mtsp(k, i, 0, N), big_penalty)
addQ(idx_mtsp(k, 0, 0, N), idx_mtsp(k, 0, 0, N), -big_penalty)
return Q, N
# ============================
# 5. 解碼 slot 序列(不修剪)
# ============================
def decode_slots(sample, N):
"""
回傳:
u1_slots, u2_slots
各自長度 N裡面是城市 index可能重複也可能很多 0
"""
slots = []
for k in range(2):
row = []
for p in range(N):
chosen = 0 # 預設當作待在 depot
for i in range(N):
if sample.get(idx_mtsp(k, i, p, N), 0) == 1:
chosen = i
break
row.append(chosen)
slots.append(row)
return slots[0], slots[1]
# ============================
# 6. 根據 slots 做 repair → 產生合法 uav1 / uav2 path
# ============================
def repair_routes_from_slots(u1_slots, u2_slots, N):
"""
依據 slots 傾向資訊,把城市分配給兩台 UAV
並用暴力 TSP 在各自子集合裡找最短路徑,確保:
- 每個城市 1..N-1 被恰好一台 UAV 拜訪
- 兩台 UAV 路徑皆為 0 -> ... -> 0成本計算時處理
"""
# 統計每個城市在兩台 UAV slots 中出現次數
count1 = [0]*N
count2 = [0]*N
for c in u1_slots:
if 0 <= c < N:
count1[c] += 1
for c in u2_slots:
if 0 <= c < N:
count2[c] += 1
# 分配城市:看誰出現次數多,平手則讓城市較少的一邊拿
assign1 = []
assign2 = []
for city in range(1, N): # city 0 = depot 不分配
if count1[city] > count2[city]:
assign1.append(city)
elif count2[city] > count1[city]:
assign2.append(city)
else:
# 平手 → 給目前負載較少的 UAV
if len(assign1) <= len(assign2):
assign1.append(city)
else:
assign2.append(city)
return sorted(assign1), sorted(assign2)
# ============================
# 7. 單 UAV 成本 & 子集合最佳排列
# ============================
def uav_cost(path, D):
if not path:
return 0.0
path = [c for c in path if 0 <= c < len(D)]
if len(path) == 0:
return 0.0
cost = D[0, path[0]]
for i in range(len(path)-1):
cost += D[path[i], path[i+1]]
cost += D[path[-1], 0]
return float(cost)
def best_order_for_cities(cities, D):
"""
對給定 city set暴力搜尋最短 0 -> perm -> 0 的路徑
cities: list of city index (1..N-1)
"""
if len(cities) <= 1:
return list(cities), uav_cost(list(cities), D)
best_perm = None
best_cost = float('inf')
for perm in permutations(cities):
c = uav_cost(list(perm), D)
if c < best_cost:
best_cost = c
best_perm = list(perm)
return best_perm, best_cost
# ============================
# 8. 利用 QUBO 取樣 + repair 得到合法解
# ============================
def sample_and_repair(sampler, Q, N, D):
"""
一次取樣 + repair即可保證得到合法 2-UAV M-TSP 解。
不需要反覆重試。
"""
result = sampler.sample_qubo(Q, num_reads=30)
sample = result.first.sample
# 1) 先解出 slots
u1_slots, u2_slots = decode_slots(sample, N)
# 2) 依 slots 傾向分配城市給兩台 UAV
assign1, assign2 = repair_routes_from_slots(u1_slots, u2_slots, N)
# 3) 各自子集合內找最短路徑
u1_path, c1 = best_order_for_cities(assign1, D)
u2_path, c2 = best_order_for_cities(assign2, D)
total = c1 + c2
return u1_path, u2_path, c1, c2, total
# ============================
# 9. Classic 2-UAV MTSP 暴力搜尋(全域最佳)
# ============================
def classic_mtsp_two_uav_bruteforce(D):
N = D.shape[0]
cities = list(range(1, N))
best_total = float('inf')
best_u1, best_u2 = None, None
best_route = None
def cost_of(path):
return uav_cost(path, D)
for perm in permutations(cities):
for cut in range(len(perm)+1):
u1 = list(perm[:cut])
u2 = list(perm[cut:])
total = cost_of(u1) + cost_of(u2)
if total < best_total:
best_total = total
best_u1 = u1
best_u2 = u2
best_route = [0] + list(perm)
return best_u1, best_u2, best_total, best_route
# ============================
# 10. 共用:計算兩 UAV 總成本
# ============================
def compute_two_uav_cost(uav1_path, uav2_path, D):
c1 = uav_cost(uav1_path, D)
c2 = uav_cost(uav2_path, D)
return c1 + c2, c1, c2
# ============================
# 11. SA & SQA每 run 使用 sample_and_repair
# ============================
def run_mtsp_sa_sqa(Q, N, D, NUM_RUNS=10):
sa_costs = []
sa_routes = []
sa_u1_list = []
sa_u2_list = []
sa_uav_costs = [] # store tuple (c1, c2)
sqa_costs = []
sqa_routes = []
sqa_u1_list = []
sqa_u2_list = []
sqa_uav_costs = [] # store tuple (c1, c2)
sa = oj.SASampler()
sqa = oj.SQASampler()
print("\n🔥 Running SA (with repair, always feasible)...")
for r in range(NUM_RUNS):
u1, u2, c1, c2, total = sample_and_repair(sa, Q, N, D)
full_route = [0] + u1 + u2
print(f"SA Run {r+1:02d}: UAV1={u1}, UAV2={u2}, "
f"UAV1_cost={c1:.2f}, UAV2_cost={c2:.2f}, Total={total:.2f}")
sa_costs.append(total)
sa_routes.append(full_route)
sa_u1_list.append(u1)
sa_u2_list.append(u2)
sa_uav_costs.append((c1, c2)) # <-- store both costs
print("\n🔥 Running SQA (with repair, always feasible)...")
for r in range(NUM_RUNS):
u1, u2, c1, c2, total = sample_and_repair(sqa, Q, N, D)
full_route = [0] + u1 + u2
print(f"SQA Run {r+1:02d}: UAV1={u1}, UAV2={u2}, "
f"UAV1_cost={c1:.2f}, UAV2_cost={c2:.2f}, Total={total:.2f}")
sqa_costs.append(total)
sqa_routes.append(full_route)
sqa_u1_list.append(u1)
sqa_u2_list.append(u2)
sqa_uav_costs.append((c1, c2)) # <-- store both costs
sa_results = (sa_costs, sa_routes, sa_u1_list, sa_u2_list, sa_uav_costs)
sqa_results = (sqa_costs, sqa_routes, sqa_u1_list, sqa_u2_list, sqa_uav_costs)
return sa_results, sqa_results
# ============================
# 12. 綜合分析圖表(你指定的版本)
# ============================
def create_comprehensive_charts(sa_results, sqa_results, classic_results, D, N):
"""創建綜合分析圖表"""
fig = plt.figure(figsize=(18, 12))
# 解包結果
sa_costs, sa_routes, sa_uav1, sa_uav2, sa_uav_costs = sa_results
sqa_costs, sqa_routes, sqa_uav1, sqa_uav2, sqa_uav_costs = sqa_results
classic_uav1, classic_uav2, classic_cost, classic_route = classic_results
# ---------------------------------------------------------
# 1. MTSP 成本分布比較 (左上) - 系統總成本
# ---------------------------------------------------------
ax1 = plt.subplot(2, 3, 1)
bins = range(int(min(sa_costs + sqa_costs)) - 1,
int(max(sa_costs + sqa_costs)) + 2)
ax1.hist(sa_costs, bins=bins, alpha=0.6, label="SA Total", density=True, color='lightblue')
ax1.hist(sqa_costs, bins=bins, alpha=0.6, label="SQA Total", density=True, color='lightcoral')
# 添加KDE
if len(sa_costs) > 1:
xs_sa, ys_sa = kde_1d(sa_costs)
ax1.plot(xs_sa, ys_sa, label="SA KDE", color='blue', linewidth=2)
if len(sqa_costs) > 1:
xs_sqa, ys_sqa = kde_1d(sqa_costs)
ax1.plot(xs_sqa, ys_sqa, label="SQA KDE", color='red', linewidth=2)
ax1.axvline(x=classic_cost, color='green', linestyle='--',
label=f'Classic: {classic_cost}', linewidth=2)
ax1.set_title("Total System Cost Distribution")
ax1.set_xlabel("Total Cost (UAV1 + UAV2)")
ax1.set_ylabel("Density")
ax1.legend()
ax1.grid(True, alpha=0.3)
# ---------------------------------------------------------
# 2. 單一 UAV 成本分布比較 (右上) - 修正為「個別 UAV」視角
# ---------------------------------------------------------
ax2 = plt.subplot(2, 3, 2)
# 展平數據:將 [(c1, c2), (c1, c2)...] 變成 [c1, c2, c1, c2...]
# 這樣可以看到「單台無人機」通常飛多遠
sa_single_costs = [c for costs in sa_uav_costs for c in costs]
sqa_single_costs = [c for costs in sqa_uav_costs for c in costs]
# 計算 classic 的單機成本
c1_classic = uav_cost(classic_uav1, D)
c2_classic = uav_cost(classic_uav2, D)
# 設定 bins
all_single = sa_single_costs + sqa_single_costs
uav_bins = range(int(min(all_single)) - 1, int(max(all_single)) + 2)
ax2.hist(sa_single_costs, bins=uav_bins, alpha=0.6, label="SA Single UAV", density=True, color='skyblue')
ax2.hist(sqa_single_costs, bins=uav_bins, alpha=0.6, label="SQA Single UAV", density=True, color='salmon')
ax2.axvline(x=c1_classic, color='darkgreen', linestyle=':', label='Classic UAV1')
ax2.axvline(x=c2_classic, color='limegreen', linestyle=':', label='Classic UAV2')
ax2.set_title("Individual UAV Cost Distribution")
ax2.set_xlabel("Cost per UAV")
ax2.set_ylabel("Density")
ax2.legend()
ax2.grid(True, alpha=0.3)
# ---------------------------------------------------------
# 3. 箱型圖比較 (中上) - 修正重點
# ---------------------------------------------------------
ax3 = plt.subplot(2, 3, 3)
# 數據 1 & 2: 系統總成本 (Total Cost)
# 數據 3 & 4: 單機成本 (Individual UAV Cost) - 使用上面展平的數據
# 這樣 Box Plot 會有明顯的高低落差(總成本高,單機成本低)
box_data = [sa_costs, sqa_costs, sa_single_costs, sqa_single_costs]
bp = ax3.boxplot(
box_data,
labels=["SA\nTotal", "SQA\nTotal", "SA\nSingle", "SQA\nSingle"],
patch_artist=True,
showmeans=True
)
colors = ['lightblue', 'lightcoral', 'skyblue', 'salmon']
for patch, color in zip(bp['boxes'], colors):
patch.set_facecolor(color)
ax3.set_title("Cost Comparison: Total vs Single UAV")
ax3.set_ylabel("Cost")
ax3.grid(True, alpha=0.3)
# ---------------------------------------------------------
# 4. Route diversity heatmap - SA (左下)
# ---------------------------------------------------------
ax4 = plt.subplot(2, 3, 4)
sa_mat = route_diversity_matrix(sa_routes, N)
im1 = ax4.imshow(sa_mat, aspect='auto', origin='lower', cmap='viridis')
ax4.set_title("SA Route Diversity")
ax4.set_xlabel("Position")
ax4.set_ylabel("City")
plt.colorbar(im1, ax=ax4, shrink=0.8)
# ---------------------------------------------------------
# 5. Route diversity heatmap - SQA (右下)
# ---------------------------------------------------------
ax5 = plt.subplot(2, 3, 5)
sqa_mat = route_diversity_matrix(sqa_routes, N)
im2 = ax5.imshow(sqa_mat, aspect='auto', origin='lower', cmap='viridis')
ax5.set_title("SQA Route Diversity")
ax5.set_xlabel("Position")
ax5.set_ylabel("City")
plt.colorbar(im2, ax=ax5, shrink=0.8)
# ---------------------------------------------------------
# 6. 最佳路徑可視化 (中下)
# ---------------------------------------------------------
ax6 = plt.subplot(2, 3, 6)
# 找出 SA 或 SQA 的最佳解
best_sa_idx = sa_costs.index(min(sa_costs))
best_sqa_idx = sqa_costs.index(min(sqa_costs))
best_sa_u1 = sa_uav1[best_sa_idx]
best_sa_u2 = sa_uav2[best_sa_idx]
best_sqa_u1 = sqa_uav1[best_sqa_idx]
best_sqa_u2 = sqa_uav2[best_sqa_idx]
# 用成本較好的那組
if min(sa_costs) <= min(sqa_costs):
u1 = best_sa_u1
u2 = best_sa_u2
route_name = "SA"
route_cost = min(sa_costs)
else:
u1 = best_sqa_u1
u2 = best_sqa_u2
route_name = "SQA"
route_cost = min(sqa_costs)
# --- 繪製城市 ---
angles = np.linspace(0, 2*np.pi, N, endpoint=False)
x_pos = np.cos(angles)
y_pos = np.sin(angles)
ax6.scatter(x_pos, y_pos, s=200, c='red', zorder=5)
for i, (x, y) in enumerate(zip(x_pos, y_pos)):
ax6.annotate(str(i), (x, y), ha='center', va='center',
fontsize=10, fontweight='bold', color='white', zorder=6)
# --- UAV1: 藍色 ---
if len(u1) > 0:
pts = [0] + u1 + [0] # 加入 depot
xs = [x_pos[c] for c in pts]
ys = [y_pos[c] for c in pts]
ax6.plot(xs, ys, '-o', color='blue', linewidth=2, alpha=0.8,
label=f'UAV1 ({len(u1)})')
# --- UAV2: 橘色 ---
if len(u2) > 0:
pts = [0] + u2 + [0] # 加入 depot
xs = [x_pos[c] for c in pts]
ys = [y_pos[c] for c in pts]
ax6.plot(xs, ys, '-o', color='orange', linewidth=2, alpha=0.8,
label=f'UAV2 ({len(u2)})')
ax6.set_title(f"Best {route_name} MTSP Solution\nTotal Cost: {route_cost:.2f}")
ax6.set_xlim(-1.3, 1.3)
ax6.set_ylim(-1.3, 1.3)
ax6.set_aspect('equal')
ax6.axis('off')
ax6.legend(loc='upper right')
plt.tight_layout()
plt.savefig("mtsp_two_uav_comprehensive_analysis.png", dpi=300, bbox_inches='tight')
print(f"\n📊 綜合分析圖表已保存: mtsp_two_uav_comprehensive_analysis.png")
plt.show()
# ============================
# 13. main整合執行
# ============================
if __name__ == "__main__":
# 距離矩陣10 城市
D = np.array([
[0, 2, 8, 5, 7, 6, 3, 9, 4, 2],
[2, 0, 5, 7, 3, 8, 4, 6, 9, 1],
[8, 5, 0, 2, 6, 7, 3, 4, 1, 5],
[5, 7, 2, 0, 4, 3, 8, 6, 2, 7],
[7, 3, 6, 4, 0, 5, 9, 2, 7, 3],
[6, 8, 7, 3, 5, 0, 4, 7, 9, 6],
[3, 4, 3, 8, 9, 4, 0, 5, 6, 2],
[9, 6, 4, 6, 2, 7, 5, 0, 8, 3],
[4, 9, 1, 2, 7, 9, 6, 8, 0, 5],
[2, 1, 5, 7, 3, 6, 2, 3, 5, 0]
])
N = D.shape[0]
print("🚀 2-UAV M-TSP (Quantum + Repair vs Classic) 開始運行...\n")
print(f"Distance Matrix ({N}x{N}):")
print(D)
# 建立 QUBO
Q, _ = build_mtsp_qubo_variable(D, penalty=20.0)
# SA / SQA每 run 一定是合法解
NUM_RUNS = 10
sa_results, sqa_results = run_mtsp_sa_sqa(Q, N, D, NUM_RUNS=NUM_RUNS)
# Classic 暴力全域最佳
print("\n🎯 Running Classic 2-UAV MTSP (Bruteforce ALL solutions)...")
classic_uav1, classic_uav2, classic_total_cost, classic_route = classic_mtsp_two_uav_bruteforce(D)
print(f"Classic UAV1={classic_uav1}, UAV2={classic_uav2}, Cost={classic_total_cost}")
print(f"Classic merged route (0 + perm): {classic_route}")
# Summary
sa_costs, _, _, _, _ = sa_results
sqa_costs, _, _, _, _ = sqa_results
print("\n📊 Result Summary:")
print(f"SA best cost = {min(sa_costs):.2f}, mean = {np.mean(sa_costs):.2f}")
print(f"SQA best cost = {min(sqa_costs):.2f}, mean = {np.mean(sqa_costs):.2f}")
print(f"Classic global optimum = {classic_total_cost:.2f}")
# 畫圖
classic_results = (classic_uav1, classic_uav2, classic_total_cost, classic_route)
print("\n🎨 Creating comprehensive analysis charts...")
create_comprehensive_charts(sa_results, sqa_results, classic_results, D, N)
print("\n🏁 Done.")