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.

997 lines
35 KiB
Python

import time
import numpy as np
import matplotlib.pyplot as plt
from itertools import permutations
import math
import openjij as oj
# ==================== 可調整參數區 ====================
# 問題規模設定
CITIES = 30 # 城市總數(含 depot=0建議範圍: 8-30
RANDOM = False # True: 每次隨機矩陣False: 固定 seed每次執行都一樣
RANDOM_SEED = 42 # 當 RANDOM=False 時使用的固定 seed
COORD_RANGE = (0.0, 10.0) # 城市座標範圍
# 演算法執行設定
CLASSIC = False # 是否執行經典暴力解全域最佳【警告CITIES>10 會非常慢】
NUM_RUNS = 10 # SA / SQA 執行次數(建議 10-100
# QUBO 參數
PENALTY = 20.0 # 約束懲罰權重(一般約束)
BIG_PENALTY = 9999.0 # 硬約束懲罰權重(起點約束)
NUM_READS = 30 # 每次 QUBO 取樣的讀取次數(建議 10-100
# TSP 求解參數
EXACT_LIMIT = 8 # 暴力求解的城市數上限(超過則用貪婪法)
# 魯棒優化參數 (H-infinity Robust QUBO)
USE_ROBUST = False # True or False 是否使用魯棒QUBO考慮干擾/故障風險)
GAMMA = 0.5 # H-infinity 控制器參數(越小越保守,建議 0.3-1.0
SIGMA = 1.0 # 干擾強度參數
ALPHA = 10.0 # 風險權重係數(越大越重視安全性)
# =====================================================
def generate_distance_matrix(num_cities, random=True, seed=None,
coord_range=None):
"""
產生 num_cities x num_cities 的對稱距離矩陣
預設 city 0 depot其餘 1...(num_cities-1) 為任務點
random=False 時使用固定 seed確保每次結果相同
"""
if seed is None:
seed = RANDOM_SEED
if coord_range is None:
coord_range = COORD_RANGE
if not random:
np.random.seed(seed)
else:
# 如果你希望每次執行完全獨立,可以在這裡不要設 seed
# 或用 np.random.seed(None)
np.random.seed(None)
low, high = coord_range
# 這裡把所有城市都放在同一個 2D 平面上隨機產生座標
# 也可以改成固定 depot=(0,0),其餘隨機,看你喜歡:
coords = np.random.uniform(low, high, size=(num_cities, 2))
# 計算歐氏距離矩陣
D = np.zeros((num_cities, num_cities), dtype=float)
for i in range(num_cities):
for j in range(i + 1, num_cities):
dist = np.linalg.norm(coords[i] - coords[j])
D[i, j] = dist
D[j, i] = dist
# 將距離四捨五入到小數點後兩位,易讀一點
D = np.round(D, 2)
return D, coords
def generate_disturbance_matrix(num_cities, seed=None):
"""產生對稱的干擾/故障機率矩陣 F (0~1)"""
if seed is None:
seed = RANDOM_SEED
np.random.seed(seed)
F = np.random.rand(num_cities, num_cities)
# 對稱化 (假設 A到B 和 B到A 的風險一樣)
F = (F + F.T) / 2
# 將對角線設為 0 (自己到自己沒有風險)
np.fill_diagonal(F, 0.0)
return F
# ============================
# 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=None, big_penalty=None):
"""
- 城市: 0..N-1其中 0 depot
- 兩台 UAV: k = 0,1
- 每台 UAV N slot: p = 0..N-1
- 變數: x_{k,i,p}
QUBO包含
距離成本 + 位置約束 + 城市約束 + 起點約束
"""
if penalty is None:
penalty = PENALTY
if big_penalty is None:
big_penalty = BIG_PENALTY
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
# ============================
# 4b. 建立包含 H_infinity 魯棒項的 QUBO
# ============================
def build_robust_qubo(D, F, gamma=None, sigma=None, alpha=None, penalty=None, big_penalty=None):
"""
建立包含 H_infinity 魯棒項的 QUBO
D: 距離矩陣
F: 干擾/故障機率矩陣 (0~1)
gamma: H_infinity 控制器參數越小越保守
sigma: 干擾強度參數
alpha: 用來平衡 Distance Risk 的權重係數
penalty: 約束懲罰權重
big_penalty: 硬約束懲罰權重
"""
if gamma is None:
gamma = GAMMA
if sigma is None:
sigma = SIGMA
if alpha is None:
alpha = ALPHA
if penalty is None:
penalty = PENALTY
if big_penalty is None:
big_penalty = BIG_PENALTY
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
# ==========================
# 1. 目標函數 (距離 + H_infinity 風險)
# ==========================
for k in range(2): # 2台 UAV
for p in range(N):
q = (p + 1) % N
for i in range(N):
for j in range(N):
dij = D[i, j]
fij = F[i, j]
if dij == 0:
continue
# --- H_infinity Robust Cost ---
# Risk = (sigma / gamma^2) * F^2
# 當 F 接近 1 且 gamma 小時,這項會很大
risk_term = (sigma / (gamma**2)) * (fij**2)
# 總權重 = 距離 + alpha * 風險
# alpha 越大,越重視安全性
total_weight = dij + (alpha * risk_term)
u = idx_mtsp(k, i, p, N)
v = idx_mtsp(k, j, q, N)
addQ(u, v, total_weight)
# ==========================
# 2. 約束條件 (與原本 MTSP 相同)
# ==========================
# (A) 位置約束:每台 UAV 在每個時間點 p 只能在一個城市
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)
# (B) 城市約束:每個任務城市 (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)
# (C) 起點約束:所有 UAV 時間 0 都在 depot (city 0)
for k in range(2):
# p=0 必須是 city 0
for i in range(1, N):
u = idx_mtsp(k, i, 0, N)
addQ(u, u, big_penalty) # 懲罰出現在 p=0 的非 depot 點
# 鼓勵 city 0 在 p=0
u_depot = idx_mtsp(k, 0, 0, N)
addQ(u_depot, u_depot, -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 uav_disturbance_energy(path, F, gamma=None, sigma=None):
"""
計算單一 UAV 路徑的擾動能量
path: UAV 的路徑 (list of city indices)
F: 干擾矩陣
公式: Risk = (sigma / gamma^2) * sum(F[i,j]^2 for all edges)
"""
if gamma is None:
gamma = GAMMA
if sigma is None:
sigma = SIGMA
if not path or len(path) == 0:
return 0.0
path = [c for c in path if 0 <= c < len(F)]
if len(path) == 0:
return 0.0
risk = 0.0
# depot (0) -> first city
risk += (sigma / (gamma**2)) * (F[0, path[0]]**2)
# city to city
for i in range(len(path)-1):
risk += (sigma / (gamma**2)) * (F[path[i], path[i+1]]**2)
# last city -> depot (0)
risk += (sigma / (gamma**2)) * (F[path[-1], 0]**2)
return float(risk)
def best_order_for_cities(cities, D, exact_limit=None):
"""
cities: list of city index (1..N-1)
exact_limit: 小於等於這個長度才用暴力全排列太多就改用近鄰貪婪
"""
if exact_limit is None:
exact_limit = EXACT_LIMIT
cities = list(cities)
if len(cities) <= 1:
return cities, uav_cost(cities, D)
# 小問題可以繼續用暴力(例如 <= 8
if len(cities) <= exact_limit:
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
# 太多城市 → 改用最近鄰貪婪法
remaining = set(cities)
path = []
current = 0 # 從 depot 出發
while remaining:
# 找離 current 最近的城市
next_city = min(remaining, key=lambda c: D[current, c])
path.append(next_city)
remaining.remove(next_city)
current = next_city
return path, uav_cost(path, D)
# ============================
# 8. 利用 QUBO 取樣 + repair 得到合法解
# ============================
def sample_and_repair(sampler, Q, N, D, F=None, run_num=None, method_name=""):
"""
一次取樣 + repair即可保證得到合法 2-UAV M-TSP
目標minimize max(UAV1_cost, UAV2_cost)minMax / makespan
F: 干擾矩陣可選
"""
result = sampler.sample_qubo(Q, num_reads=NUM_READS)
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)
# 🔁 這裡把原本的「總成本 = c1 + c2」改成「max(c1, c2)」
makespan = max(c1, c2)
# 計算擾動能量(如果有干擾矩陣)
disturbance_energy = 0.0
if F is not None:
risk1 = uav_disturbance_energy(u1_path, F)
risk2 = uav_disturbance_energy(u2_path, F)
disturbance_energy = risk1 + risk2 # 總擾動能量
return u1_path, u2_path, c1, c2, makespan, disturbance_energy
# ============================
# 9. Classic 2-UAV MTSP 暴力搜尋(全域最佳)
# ============================
def classic_mtsp_two_uav_bruteforce(D):
"""
2-UAV M-TSP 的經典暴力解
目標改為 minimize max(cost(UAV1), cost(UAV2)) = 最短完成時間 (minMax)
"""
N = D.shape[0]
cities = list(range(1, N))
best_makespan = float('inf')
best_sum_cost = float('inf')
best_u1, best_u2 = None, None
best_route = None
def cost_of(path):
return uav_cost(path, D)
# 計算總排列數
from math import factorial
total_perms = factorial(len(cities))
total_combos = total_perms * len(cities) # 每個排列有 len+1 種切分
print(f"📊 暴力搜尋: {total_perms} 種排列 × {len(cities)+1} 種切分 = {total_combos:,} 種組合")
count = 0
last_print_time = time.time()
print_interval = 0.5 # 每 0.5 秒更新一次進度
for perm in permutations(cities):
for cut in range(len(perm) + 1):
count += 1
# 定期顯示進度
current_time = time.time()
if current_time - last_print_time >= print_interval:
progress = (count / total_combos) * 100
print(f"\r 進度: {count:,}/{total_combos:,} ({progress:.1f}%)", end="", flush=True)
last_print_time = current_time
u1 = list(perm[:cut])
u2 = list(perm[cut:])
c1 = cost_of(u1)
c2 = cost_of(u2)
makespan = max(c1, c2)
total_sum = c1 + c2
if (makespan < best_makespan) or \
(math.isclose(makespan, best_makespan) and total_sum < best_sum_cost):
best_makespan = makespan
best_sum_cost = total_sum
best_u1 = u1
best_u2 = u2
best_route = [0] + list(perm)
print(f"\r 進度: {total_combos:,}/{total_combos:,} (100.0%) ✓")
return best_u1, best_u2, best_makespan, 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, F=None, NUM_RUNS=10):
"""
使用 SA / SQA QUBO每次取一組解並經過 repair
評估指標使用 makespan = max(UAV1_cost, UAV2_cost)minMax
F: 干擾矩陣可選
"""
sa_makespans = []
sa_routes = []
sa_u1_list = []
sa_u2_list = []
sa_uav_costs = []
sa_disturbances = [] # 擾動能量
sqa_makespans = []
sqa_routes = []
sqa_u1_list = []
sqa_u2_list = []
sqa_uav_costs = []
sqa_disturbances = [] # 擾動能量
sa = oj.SASampler()
sqa = oj.SQASampler()
# ========== SA 計時開始 ==========
print("\n🔥 Running SA (with repair, always feasible, minMax objective)...")
print(f"📊 總共 {NUM_RUNS} 次運行")
sa_start_time = time.time()
for r in range(NUM_RUNS):
u1, u2, c1, c2, makespan, dist_energy = sample_and_repair(sa, Q, N, D, F, run_num=r+1, method_name="SA")
full_route = [0] + u1 + u2
if F is not None:
print(f" 結果: UAV1={u1}, UAV2={u2}, "
f"UAV1_cost={c1:.2f}, UAV2_cost={c2:.2f}, "
f"Makespan={makespan:.2f}, Disturbance={dist_energy:.4f}")
else:
print(f" 結果: UAV1={u1}, UAV2={u2}, "
f"UAV1_cost={c1:.2f}, UAV2_cost={c2:.2f}, "
f"Makespan={makespan:.2f}")
sa_makespans.append(makespan)
sa_routes.append(full_route)
sa_u1_list.append(u1)
sa_u2_list.append(u2)
sa_uav_costs.append((c1, c2))
sa_disturbances.append(dist_energy)
sa_elapsed = time.time() - sa_start_time
print(f"⏱️ SA Total Time: {sa_elapsed:.3f} seconds ({sa_elapsed/NUM_RUNS:.3f} sec/run)")
# ========== SQA 計時開始 ==========
print("\n🔥 Running SQA (with repair, always feasible, minMax objective)...")
print(f"📊 總共 {NUM_RUNS} 次運行")
sqa_start_time = time.time()
for r in range(NUM_RUNS):
u1, u2, c1, c2, makespan, dist_energy = sample_and_repair(sqa, Q, N, D, F, run_num=r+1, method_name="SQA")
full_route = [0] + u1 + u2
if F is not None:
print(f" 結果: UAV1={u1}, UAV2={u2}, "
f"UAV1_cost={c1:.2f}, UAV2_cost={c2:.2f}, "
f"Makespan={makespan:.2f}, Disturbance={dist_energy:.4f}")
else:
print(f" 結果: UAV1={u1}, UAV2={u2}, "
f"UAV1_cost={c1:.2f}, UAV2_cost={c2:.2f}, "
f"Makespan={makespan:.2f}")
sqa_makespans.append(makespan)
sqa_routes.append(full_route)
sqa_u1_list.append(u1)
sqa_u2_list.append(u2)
sqa_uav_costs.append((c1, c2))
sqa_disturbances.append(dist_energy)
sqa_elapsed = time.time() - sqa_start_time
print(f"⏱️ SQA Total Time: {sqa_elapsed:.3f} seconds ({sqa_elapsed/NUM_RUNS:.3f} sec/run)")
sa_results = (sa_makespans, sa_routes, sa_u1_list, sa_u2_list, sa_uav_costs, sa_disturbances)
sqa_results = (sqa_makespans, sqa_routes, sqa_u1_list, sqa_u2_list, sqa_uav_costs, sqa_disturbances)
return sa_results, sqa_results, sa_elapsed, sqa_elapsed
# ============================
# 12. 綜合分析圖表(你指定的版本)
# ============================
def create_comprehensive_charts(sa_results, sqa_results, classic_results, D, N):
"""創建綜合分析圖表目標minMax makespan"""
fig = plt.figure(figsize=(24, 12))
# 解包結果(第一個 list 現在代表 makespan最後一個是擾動能量
sa_costs, sa_routes, sa_uav1, sa_uav2, sa_uav_costs, sa_disturbances = sa_results
sqa_costs, sqa_routes, sqa_uav1, sqa_uav2, sqa_uav_costs, sqa_disturbances = sqa_results
classic_uav1, classic_uav2, classic_cost, classic_route = classic_results
# ---------------------------------------------------------
# 1. MTSP 完成時間 (makespan) 分布比較 (左上)
# ---------------------------------------------------------
ax1 = plt.subplot(2, 4, 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 Makespan", density=True, color='lightblue')
ax1.hist(sqa_costs, bins=bins, alpha=0.6, label="SQA Makespan", 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 minMax: {classic_cost:.2f}', linewidth=2)
ax1.set_title("System Makespan Distribution (max route length)")
ax1.set_xlabel("Makespan (max cost of UAV1, UAV2)")
ax1.set_ylabel("Density")
ax1.legend()
ax1.grid(True, alpha=0.3)
# ---------------------------------------------------------
# 2. 單一 UAV 成本分布比較 (右上)
# ---------------------------------------------------------
ax2 = plt.subplot(2, 4, 2)
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]
c1_classic = uav_cost(classic_uav1, D)
c2_classic = uav_cost(classic_uav2, D)
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("Route Length Distribution per UAV")
ax2.set_xlabel("Cost per UAV")
ax2.set_ylabel("Density")
ax2.legend()
ax2.grid(True, alpha=0.3)
# ---------------------------------------------------------
# 3. 箱型圖比較 (中上):系統 makespan vs 單機成本
# ---------------------------------------------------------
ax3 = plt.subplot(2, 3, 3)
box_data = [sa_costs, sqa_costs, sa_single_costs, sqa_single_costs]
bp = ax3.boxplot(
box_data,
labels=["SA\nMakespan", "SQA\nMakespan", "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("Makespan vs Individual UAV Cost")
ax3.set_ylabel("Cost")
ax3.grid(True, alpha=0.3)
# ---------------------------------------------------------
# 4. 總能量分布比較 (Total Energy - Distance Cost)
# ---------------------------------------------------------
ax4 = plt.subplot(2, 4, 4)
# 繪製總能量直方圖
all_energies = sa_costs + sqa_costs
bins_energy = range(int(min(all_energies)) - 1, int(max(all_energies)) + 2)
ax4.hist(sa_costs, bins=bins_energy, alpha=0.6, label="SA Total Energy", density=True, color='skyblue')
ax4.hist(sqa_costs, bins=bins_energy, alpha=0.6, label="SQA Total Energy", density=True, color='salmon')
# 添加 KDE
if len(sa_costs) > 1:
xs_sa, ys_sa = kde_1d(sa_costs)
ax4.plot(xs_sa, ys_sa, color='blue', linewidth=2, alpha=0.8)
if len(sqa_costs) > 1:
xs_sqa, ys_sqa = kde_1d(sqa_costs)
ax4.plot(xs_sqa, ys_sqa, color='red', linewidth=2, alpha=0.8)
ax4.set_xlabel("Total Energy (Distance Cost)")
ax4.set_ylabel("Density")
ax4.set_title(f"Total Energy Distribution\nSA: {np.mean(sa_costs):.2f}±{np.std(sa_costs):.2f} | SQA: {np.mean(sqa_costs):.2f}±{np.std(sqa_costs):.2f}")
ax4.legend()
ax4.grid(alpha=0.3)
# ---------------------------------------------------------
# 5. SA 路徑多樣性分析
# ---------------------------------------------------------
ax5 = plt.subplot(2, 4, 5)
sa_mat = route_diversity_matrix(sa_routes, N)
im1 = ax5.imshow(sa_mat, aspect='auto', origin='lower', cmap='viridis')
ax5.set_title("SA Route Diversity")
ax5.set_xlabel("Position")
ax5.set_ylabel("City")
plt.colorbar(im1, ax=ax5, shrink=0.8)
# ---------------------------------------------------------
# 6. SQA 路徑多樣性分析
# ---------------------------------------------------------
ax6 = plt.subplot(2, 4, 6)
sqa_mat = route_diversity_matrix(sqa_routes, N)
im2 = ax6.imshow(sqa_mat, aspect='auto', origin='lower', cmap='viridis')
ax6.set_title("SQA Route Diversity")
ax6.set_xlabel("Position")
ax6.set_ylabel("City")
plt.colorbar(im2, ax=ax6, shrink=0.8)
# ---------------------------------------------------------
# 7. 擾動能量分布比較 (Disturbance Energy Distribution)
# ---------------------------------------------------------
ax7 = plt.subplot(2, 4, 7)
# 繪製擾動能量直方圖
all_disturbances = sa_disturbances + sqa_disturbances
if len(all_disturbances) > 0 and max(all_disturbances) > 0:
bins_disturb = np.linspace(min(all_disturbances), max(all_disturbances), 20)
ax7.hist(sa_disturbances, bins=bins_disturb, alpha=0.6, label="SA Disturbance",
density=True, color='lightgreen')
ax7.hist(sqa_disturbances, bins=bins_disturb, alpha=0.6, label="SQA Disturbance",
density=True, color='lightpink')
# 添加 KDE
if len(sa_disturbances) > 1 and np.std(sa_disturbances) > 0:
xs_sa, ys_sa = kde_1d(sa_disturbances)
ax7.plot(xs_sa, ys_sa, color='green', linewidth=2, alpha=0.8)
if len(sqa_disturbances) > 1 and np.std(sqa_disturbances) > 0:
xs_sqa, ys_sqa = kde_1d(sqa_disturbances)
ax7.plot(xs_sqa, ys_sqa, color='purple', linewidth=2, alpha=0.8)
ax7.set_title(f"Disturbance Energy Distribution\\nSA: {np.mean(sa_disturbances):.4f}±{np.std(sa_disturbances):.4f} | SQA: {np.mean(sqa_disturbances):.4f}±{np.std(sqa_disturbances):.4f}")
else:
ax7.text(0.5, 0.5, "No disturbance data\\n(USE_ROBUST=False)",
ha='center', va='center', transform=ax7.transAxes, fontsize=12)
ax7.set_title("Disturbance Energy Distribution")
ax7.set_xlabel("Disturbance Energy")
ax7.set_ylabel("Density")
ax7.legend()
ax7.grid(alpha=0.3)
# ---------------------------------------------------------
# 8. 最佳路徑可視化 (Best Route Visualization)
# ---------------------------------------------------------
ax8 = plt.subplot(2, 4, 8)
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)
ax8.scatter(x_pos, y_pos, s=200, c='red', zorder=5)
for i, (x, y) in enumerate(zip(x_pos, y_pos)):
ax8.annotate(str(i), (x, y), ha='center', va='center',
fontsize=10, fontweight='bold', color='white', zorder=6)
if len(u1) > 0:
pts = [0] + u1 + [0]
xs = [x_pos[c] for c in pts]
ys = [y_pos[c] for c in pts]
ax8.plot(xs, ys, '-o', color='blue', linewidth=2, alpha=0.8,
label=f'UAV1 ({len(u1)})')
if len(u2) > 0:
pts = [0] + u2 + [0]
xs = [x_pos[c] for c in pts]
ys = [y_pos[c] for c in pts]
ax8.plot(xs, ys, '-o', color='orange', linewidth=2, alpha=0.8,
label=f'UAV2 ({len(u2)})')
ax8.set_title(f"Best {route_name} MTSP Solution (minMax) Makespan: {route_cost:.2f}")
ax8.set_xlim(-1.3, 1.3)
ax8.set_ylim(-1.3, 1.3)
ax8.set_aspect('equal')
ax8.axis('off')
ax8.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 城市
# 讓原本程式沿用 N、D 命名
N = CITIES
D, city_coords = generate_distance_matrix(CITIES, random=RANDOM)
print(f"產生 {CITIES} 個城市的距離矩陣RANDOM={RANDOM}")
print("城市座標 (含 depot=0)")
for idx, (x, y) in enumerate(city_coords):
print(f"City {idx}: ({x:.2f}, {y:.2f})")
'''
D = np.array([
[0.00, 3.16, 4.47, 7.81, 8.54, 7.28, 6.08, 8.60, 5.83, 9.85],
[3.16, 0.00, 3.16, 5.00, 7.00, 4.12, 5.39, 6.32, 2.83, 8.06],
[4.47, 3.16, 0.00, 4.12, 4.12, 5.39, 2.24, 4.24, 3.16, 5.39],
[7.81, 5.00, 4.12, 0.00, 4.24, 3.16, 5.10, 2.24, 2.24, 4.47],
[8.54, 7.00, 4.12, 4.24, 0.00, 7.21, 2.83, 2.24, 5.39, 1.41],
[7.28, 4.12, 5.39, 3.16, 7.21, 0.00, 7.21, 5.39, 2.24, 7.62],
[6.08, 5.39, 2.24, 5.10, 2.83, 7.21, 0.00, 4.12, 5.00, 4.24],
[8.60, 6.32, 4.24, 2.24, 2.24, 5.39, 4.12, 0.00, 4.00, 2.24],
[5.83, 2.83, 3.16, 2.24, 5.39, 2.24, 5.00, 4.00, 0.00, 6.08],
[9.85, 8.06, 5.39, 4.47, 1.41, 7.62, 4.24, 2.24, 6.08, 0.00],
])
D = np.array([
[0. , 7.56, 2.09, 6.49, 7.75, 4.1, 1.24, 7.15, 2.81, 4.79],
[7.56, 0., 9.02, 5.63, 2.44, 6.15, 8.37, 2.37, 8.94, 7.51],
[2.09, 9.02, 0., 8.57, 8.71, 6.19, 0.85, 8.97, 4.27, 6.78],
[6.49, 5.63, 8.57, 0., 7.7, 2.48, 7.72, 3.39, 5.82, 2.9 ],
[7.75, 2.44, 8.71, 7.7, 0., 7.6, 8.23, 4.71, 9.76, 9.05],
[4.1, 6.15, 6.19, 2.48, 7.6, 0., 5.34, 4.52, 3.45, 1.45],
[1.24, 8.37, 0.85, 7.72, 8.23, 5.34, 0., 8.2, 3.64, 5.98],
[7.15, 2.37, 8.97, 3.39, 4.71, 4.52, 8.2, 0., 7.78, 5.7 ],
[2.81, 8.94, 4.27, 5.82, 9.76, 3.45, 3.64, 7.78, 0., 3.17],
[4.79, 7.51, 6.78, 2.9, 9.05, 1.45, 5.98, 5.7, 3.17, 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 (根據 USE_ROBUST 決定使用哪種)
F = None # 初始化干擾矩陣
if USE_ROBUST:
print("\n🛡️ 使用魯棒 QUBO (H-infinity Robust)")
print(f" Gamma={GAMMA}, Sigma={SIGMA}, Alpha={ALPHA}")
F = generate_disturbance_matrix(N)
print(f"\n干擾矩陣 F ({N}x{N}):")
print(F)
Q, _ = build_robust_qubo(D, F)
else:
print("\n📊 使用標準 QUBO (不考慮風險)")
F = generate_disturbance_matrix(N) # 即使不用魯棒QUBO也生成F用於統計
Q, _ = build_mtsp_qubo_variable(D)
# SA / SQA每 run 一定是合法解
sa_results, sqa_results, sa_time, sqa_time = run_mtsp_sa_sqa(Q, N, D, F, NUM_RUNS)
# ========== Classic 計時 ==========
if CLASSIC == True:
print("\n🎯 Running Classic 2-UAV MTSP (Bruteforce ALL solutions)...")
classic_start_time = time.time()
classic_uav1, classic_uav2, classic_total_cost, classic_route = classic_mtsp_two_uav_bruteforce(D)
classic_elapsed = time.time() - classic_start_time
print(f"Classic UAV1={classic_uav1}, UAV2={classic_uav2}, Cost={classic_total_cost}")
print(f"Classic merged route (0 + perm): {classic_route}")
print(f"⏱️ Classic Total Time: {classic_elapsed:.3f} seconds")
else:
classic_total_cost = float('inf')
classic_elapsed = 0.0
classic_uav1, classic_uav2, classic_route = [], [], []
# Summary
sa_costs, _, _, _, _, sa_disturbances = sa_results
sqa_costs, _, _, _, _, sqa_disturbances = sqa_results
print("\n📊 Result Summary:")
print(f"SA best minMax (makespan) = {min(sa_costs):.2f}, mean = {np.mean(sa_costs):.2f}")
print(f"SQA best minMax (makespan) = {min(sqa_costs):.2f}, mean = {np.mean(sqa_costs):.2f}")
print(f"Classic global minMax optimum = {classic_total_cost:.2f}")
# 擾動能量統計
if F is not None:
print("\n⚡ Disturbance Energy Summary:")
print(f"SA min = {min(sa_disturbances):.4f}, max = {max(sa_disturbances):.4f}, mean = {np.mean(sa_disturbances):.4f}")
print(f"SQA min = {min(sqa_disturbances):.4f}, max = {max(sqa_disturbances):.4f}, mean = {np.mean(sqa_disturbances):.4f}")
# ========== 時間比較摘要 ==========
print("\n⏱️ Execution Time Comparison:")
print(f" SA : {sa_time:.3f} sec (avg {sa_time/NUM_RUNS:.3f} sec/run)")
print(f" SQA : {sqa_time:.3f} sec (avg {sqa_time/NUM_RUNS:.3f} sec/run)")
print(f" Classic: {classic_elapsed:.3f} sec")
print(f" SA speedup vs Classic: {classic_elapsed/sa_time:.2f}x")
print(f" SQA speedup vs Classic: {classic_elapsed/sqa_time:.2f}x")
# 畫圖
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.")